ca-grow-ops-manager/backend/src/controllers/auth.controller.ts
fullsizemalt 9dc0586d67 feat: Sprint 2 Phase 1 - Auth Core Complete
 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
2025-12-09 13:52:54 -08:00

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);
}
};