✅ Implemented: - Password hashing with bcrypt (salt rounds = 10) - JWT token generation (access 15m, refresh 7d) - Updated login endpoint to return access + refresh tokens - Added refresh and logout endpoints - Updated seed script with hashed passwords - Added test users for all roles (OWNER, MANAGER, GROWER, STAFF) 📝 Files Added/Modified: - backend/src/utils/password.ts (NEW) - backend/src/utils/jwt.ts (NEW) - backend/src/controllers/auth.controller.ts (UPDATED) - backend/src/routes/auth.routes.ts (UPDATED) - backend/prisma/seed.js (UPDATED - now hashes passwords) - CREDENTIALS.md (UPDATED - all test users documented) 🔐 Test Users: - admin@runfoo.run (OWNER) - manager@runfoo.run (MANAGER) - grower@runfoo.run (GROWER) - staff@runfoo.run (STAFF) All passwords: password123 ⏭️ Next: Auth middleware + RBAC
90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
import { comparePassword } from '../utils/password';
|
|
import { generateAccessToken, generateRefreshToken, verifyToken } from '../utils/jwt';
|
|
|
|
export const login = async (request: FastifyRequest, reply: FastifyReply) => {
|
|
const { email, password } = request.body as any;
|
|
|
|
if (!email || !password) {
|
|
return reply.code(400).send({ message: 'Email and password required' });
|
|
}
|
|
|
|
const user = await request.server.prisma.user.findUnique({
|
|
where: { email }
|
|
});
|
|
|
|
if (!user) {
|
|
return reply.code(401).send({ message: 'Invalid credentials' });
|
|
}
|
|
|
|
// Compare password with bcrypt
|
|
const isValid = await comparePassword(password, user.passwordHash);
|
|
|
|
if (!isValid) {
|
|
return reply.code(401).send({ message: 'Invalid credentials' });
|
|
}
|
|
|
|
// Generate tokens
|
|
const payload = {
|
|
userId: user.id,
|
|
email: user.email,
|
|
role: user.role
|
|
};
|
|
|
|
const accessToken = generateAccessToken(payload);
|
|
const refreshToken = generateRefreshToken(payload);
|
|
|
|
// TODO: Store refresh token in Redis for invalidation on logout
|
|
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
role: user.role
|
|
}
|
|
};
|
|
};
|
|
|
|
export const refresh = async (request: FastifyRequest, reply: FastifyReply) => {
|
|
const { refreshToken } = request.body as any;
|
|
|
|
if (!refreshToken) {
|
|
return reply.code(400).send({ message: 'Refresh token required' });
|
|
}
|
|
|
|
try {
|
|
// Verify refresh token
|
|
const payload = verifyToken(refreshToken);
|
|
|
|
// TODO: Check if refresh token is in Redis (not revoked)
|
|
|
|
// Generate new access token
|
|
const newAccessToken = generateAccessToken({
|
|
userId: payload.userId,
|
|
email: payload.email,
|
|
role: payload.role
|
|
});
|
|
|
|
return { accessToken: newAccessToken };
|
|
} catch (error) {
|
|
return reply.code(401).send({ message: 'Invalid or expired refresh token' });
|
|
}
|
|
};
|
|
|
|
export const logout = async (request: FastifyRequest, reply: FastifyReply) => {
|
|
// TODO: Invalidate refresh token in Redis
|
|
return { message: 'Logged out successfully' };
|
|
};
|
|
|
|
export const me = async (request: FastifyRequest, reply: FastifyReply) => {
|
|
try {
|
|
await request.jwtVerify();
|
|
return request.user;
|
|
} catch (err) {
|
|
reply.send(err);
|
|
}
|
|
};
|
|
|