TypeScript · 5620 bytes Raw Blame History
1 import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2 import type { AuthRequest, AuthResponse } from '../../shared/types.js';
3 import { z } from 'zod';
4
5 const loginSchema = z.object({
6 username: z.string().min(1).optional(),
7 password: z.string().min(1).optional(),
8 token: z.string().optional(),
9 });
10
11 const refreshSchema = z.object({
12 refreshToken: z.string().min(1),
13 });
14
15 // Simple in-memory session store (replace with Redis in production)
16 const sessions = new Map<string, {
17 userId: string;
18 username: string;
19 createdAt: Date;
20 lastAccess: Date;
21 }>();
22
23 // Simple user store (replace with proper database in production)
24 const users = new Map([
25 ['admin', {
26 id: 'admin',
27 username: 'admin',
28 passwordHash: 'admin', // In production, use proper bcrypt hashing
29 }],
30 ['demo', {
31 id: 'demo',
32 username: 'demo',
33 passwordHash: 'demo',
34 }],
35 ]);
36
37 export async function authRoutes(fastify: FastifyInstance) {
38 // Login endpoint
39 fastify.post<{
40 Body: z.infer<typeof loginSchema>;
41 }>('/auth/login', {
42 schema: {
43 body: loginSchema,
44 },
45 }, async (request: FastifyRequest, reply: FastifyReply) => {
46 const { username, password, token } = request.body as z.infer<typeof loginSchema>;
47
48 try {
49 let user;
50
51 if (token) {
52 // Token-based authentication (for API clients)
53 try {
54 const decoded = fastify.jwt.verify(token) as { userId: string; username: string };
55 user = users.get(decoded.username);
56 if (!user || user.id !== decoded.userId) {
57 throw new Error('Invalid token');
58 }
59 } catch (error) {
60 throw fastify.httpErrors.unauthorized('Invalid token');
61 }
62 } else if (username && password) {
63 // Password-based authentication
64 user = users.get(username);
65 if (!user || user.passwordHash !== password) {
66 throw fastify.httpErrors.unauthorized('Invalid credentials');
67 }
68 } else {
69 throw fastify.httpErrors.badRequest('Username/password or token required');
70 }
71
72 // Create session
73 const sessionId = crypto.randomUUID();
74 sessions.set(sessionId, {
75 userId: user.id,
76 username: user.username,
77 createdAt: new Date(),
78 lastAccess: new Date(),
79 });
80
81 // Generate tokens
82 const accessToken = fastify.jwt.sign(
83 { userId: user.id, username: user.username, sessionId },
84 { expiresIn: fastify.config.jwtExpiresIn }
85 );
86
87 const refreshToken = fastify.jwt.sign(
88 { userId: user.id, sessionId, type: 'refresh' },
89 { expiresIn: fastify.config.jwtRefreshExpiresIn }
90 );
91
92 const response: AuthResponse = {
93 token: accessToken,
94 refreshToken,
95 expiresIn: 24 * 60 * 60, // 24 hours in seconds
96 user: {
97 id: user.id,
98 username: user.username,
99 },
100 };
101
102 return response;
103 } catch (error) {
104 if (error.statusCode) {
105 throw error;
106 }
107 fastify.log.error(error, 'Login failed');
108 throw fastify.httpErrors.internalServerError('Login failed');
109 }
110 });
111
112 // Refresh token endpoint
113 fastify.post<{
114 Body: z.infer<typeof refreshSchema>;
115 }>('/auth/refresh', {
116 schema: {
117 body: refreshSchema,
118 },
119 }, async (request: FastifyRequest, reply: FastifyReply) => {
120 const { refreshToken } = request.body as z.infer<typeof refreshSchema>;
121
122 try {
123 // Verify refresh token
124 const decoded = fastify.jwt.verify(refreshToken) as {
125 userId: string;
126 sessionId: string;
127 type: string;
128 };
129
130 if (decoded.type !== 'refresh') {
131 throw new Error('Invalid token type');
132 }
133
134 // Check session exists
135 const session = sessions.get(decoded.sessionId);
136 if (!session || session.userId !== decoded.userId) {
137 throw new Error('Invalid session');
138 }
139
140 // Update session
141 session.lastAccess = new Date();
142
143 // Generate new access token
144 const accessToken = fastify.jwt.sign(
145 { userId: session.userId, username: session.username, sessionId: decoded.sessionId },
146 { expiresIn: fastify.config.jwtExpiresIn }
147 );
148
149 const response: AuthResponse = {
150 token: accessToken,
151 refreshToken, // Keep the same refresh token
152 expiresIn: 24 * 60 * 60,
153 user: {
154 id: session.userId,
155 username: session.username,
156 },
157 };
158
159 return response;
160 } catch (error) {
161 fastify.log.error(error, 'Token refresh failed');
162 throw fastify.httpErrors.unauthorized('Invalid refresh token');
163 }
164 });
165
166 // Logout endpoint
167 fastify.post('/auth/logout', {
168 preHandler: fastify.authenticate,
169 }, async (request: FastifyRequest, reply: FastifyReply) => {
170 try {
171 const user = request.user as { sessionId: string };
172
173 // Remove session
174 sessions.delete(user.sessionId);
175
176 return { success: true };
177 } catch (error) {
178 fastify.log.error(error, 'Logout failed');
179 throw fastify.httpErrors.internalServerError('Logout failed');
180 }
181 });
182
183 // Get current user info
184 fastify.get('/auth/me', {
185 preHandler: fastify.authenticate,
186 }, async (request: FastifyRequest, reply: FastifyReply) => {
187 const user = request.user as { userId: string; username: string; sessionId: string };
188
189 // Update session last access
190 const session = sessions.get(user.sessionId);
191 if (session) {
192 session.lastAccess = new Date();
193 }
194
195 return {
196 id: user.userId,
197 username: user.username,
198 };
199 });
200 }