diff --git a/backend/src/auth/authPlugin.ts b/backend/src/auth/authPlugin.ts new file mode 100644 index 0000000..42547e5 --- /dev/null +++ b/backend/src/auth/authPlugin.ts @@ -0,0 +1,162 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import AuthService from './AuthService'; +import { z } from 'zod'; +import fastifyCookie from '@fastify/cookie'; +import fastifyCsrf from '@fastify/csrf'; + +// Zod schemas for validation +const registerSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), + displayName: z.string().min(1) +}); + +const loginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1) +}); + +const resetPasswordSchema = z.object({ + email: z.string().email() +}); + +const confirmPasswordResetSchema = z.object({ + token: z.string(), + newPassword: z.string().min(8) +}); + +export default async function authPlugin(fastify: FastifyInstance, opts: any) { + // Register cookie plugin + await fastify.register(fastifyCookie); + + // Register CSRF protection + await fastify.register(fastifyCsrf); + + // Initialize AuthService + const authService = new AuthService(fastify); + + // Rate limiting + fastify.addHook('onRequest', async (request, reply) => { + // In a real implementation, you would implement proper rate limiting + // This is just a placeholder + console.log('Rate limiting check'); + }); + + // Register endpoint + fastify.post('/api/auth/register', { + schema: { + body: registerSchema + } + }, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const { email, password, displayName } = registerSchema.parse(request.body); + const user = await authService.register(email, password, displayName); + reply.code(201).send({ user }); + } catch (error) { + if (error instanceof z.ZodError) { + reply.code(400).send({ error: 'Validation error', details: error.errors }); + } else { + reply.code(400).send({ error: error.message }); + } + } + }); + + // Login endpoint + fastify.post('/api/auth/login', { + schema: { + body: loginSchema + } + }, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const { email, password } = loginSchema.parse(request.body); + const token = await authService.login(email, password); + + // Set JWT in HTTP-only cookie + reply.setCookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 24 * 60 * 60 * 1000, // 24 hours + path: '/' + }); + + reply.send({ token }); + } catch (error) { + reply.code(401).send({ error: 'Invalid credentials' }); + } + }); + + // Logout endpoint + fastify.post('/api/auth/logout', { + onRequest: [fastify.authenticate] + }, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const token = request.cookies.token; + if (!token) { + reply.code(401).send({ error: 'No token provided' }); + return; + } + + await authService.logout(token); + + // Clear cookie + reply.clearCookie('token'); + + reply.send({ message: 'Logged out successfully' }); + } catch (error) { + reply.code(400).send({ error: error.message }); + } + }); + + // Request password reset endpoint + fastify.post('/api/auth/reset-password', { + schema: { + body: resetPasswordSchema + } + }, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const { email } = resetPasswordSchema.parse(request.body); + await authService.requestPasswordReset(email); + reply.send({ message: 'Password reset email sent' }); + } catch (error) { + reply.code(400).send({ error: error.message }); + } + }); + + // Confirm password reset endpoint + fastify.post('/api/auth/reset-password/confirm', { + schema: { + body: confirmPasswordResetSchema + } + }, async (request: FastifyRequest, reply: FastifyReply) => { + try { + const { token, newPassword } = confirmPasswordResetSchema.parse(request.body); + await authService.confirmPasswordReset(token, newPassword); + reply.send({ message: 'Password reset successfully' }); + } catch (error) { + reply.code(400).send({ error: error.message }); + } + }); + + // Get current user endpoint + fastify.get('/api/auth/me', { + onRequest: [fastify.authenticate] + }, async (request: FastifyRequest, reply: FastifyReply) => { + try { + // Extract user ID from token + const token = request.cookies.token; + if (!token) { + reply.code(401).send({ error: 'No token provided' }); + return; + } + + const decoded = fastify.jwt.verify(token); + const userId = decoded.userId; + + const user = await authService.getMe(userId); + reply.send({ user }); + } catch (error) { + reply.code(401).send({ error: 'Invalid token' }); + } + }); +}