feat(auth): add authPlugin.ts
This commit is contained in:
@@ -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' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user