Skip to content

Commit 2c60795

Browse files
committed
feat: all routes complete for testing
1 parent dade94f commit 2c60795

22 files changed

Lines changed: 912 additions & 143 deletions

src/controllers/admin.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@ export const listUserSessions = async (req: Request, res: Response) => {
276276
userAgent: s.userAgent,
277277
lastUsedAt: s.lastUsedAt,
278278
expiresAt: s.expiresAt,
279+
current: false,
279280
})),
281+
total: sessions.length,
280282
});
281283
} catch (err) {
282284
logger.error(`Failed to fetch sessions: ${err}`);
@@ -330,7 +332,17 @@ export const listAllSessions = async (req: Request, res: Response) => {
330332
Session.count({ where }),
331333
]);
332334

333-
return res.json({ sessions, total });
335+
const response = sessions.map((session) => ({
336+
id: session.id,
337+
deviceName: session.deviceName,
338+
ipAddress: session.ipAddress,
339+
userAgent: session.userAgent,
340+
lastUsedAt: session.lastUsedAt.toISOString(),
341+
expiresAt: session.expiresAt.toISOString(),
342+
current: false,
343+
}));
344+
345+
return res.json({ sessions: response, total });
334346
};
335347

336348
export const getDatabaseSize = async () => {

src/controllers/authentication.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const login = async (req: Request, res: Response) => {
4848
user_agent: req.headers['user-agent'],
4949
metadata: { reason: 'No identifier supplied' },
5050
});
51-
return res.status(403).json({ message: 'Not allowed' });
51+
return res.status(403).json({ error: 'Not allowed' });
5252
}
5353

5454
logger.info(`Login attempt with ${identifier}`);
@@ -86,7 +86,7 @@ export const login = async (req: Request, res: Response) => {
8686
user_agent: req.headers['user-agent'],
8787
metadata: { reason: `No user found for identifer: ${identifier}` },
8888
});
89-
return res.status(403).json({ message: 'Not allowed' });
89+
return res.status(403).json({ error: 'Not allowed' });
9090
}
9191
} else {
9292
logger.error(`Invalid identifier: ${identifier}`);
@@ -97,7 +97,7 @@ export const login = async (req: Request, res: Response) => {
9797
user_agent: req.headers['user-agent'],
9898
metadata: { reason: `No user found for identifer: ${identifier}` },
9999
});
100-
return res.status(400).json({ message: 'Invalid data' });
100+
return res.status(400).json({ error: 'Invalid data' });
101101
}
102102
} catch (error) {
103103
logger.error(`Failed to find a user with valid Identifier: ${error}`);
@@ -108,7 +108,7 @@ export const login = async (req: Request, res: Response) => {
108108
user_agent: req.headers['user-agent'],
109109
metadata: { reason: `No user found for identifer: ${identifier}` },
110110
});
111-
return res.status(500).json({ message: 'Internal server error' });
111+
return res.status(500).json({ error: 'Internal server error' });
112112
}
113113

114114
try {
@@ -121,7 +121,7 @@ export const login = async (req: Request, res: Response) => {
121121
user_agent: req.headers['user-agent'],
122122
metadata: { reason: `No user found for identifer: ${identifier}` },
123123
});
124-
return res.status(401).json({ message: 'Not Allowed' });
124+
return res.status(401).json({ error: 'Not Allowed' });
125125
}
126126

127127
// pre-auth token
@@ -137,7 +137,7 @@ export const login = async (req: Request, res: Response) => {
137137
metadata: { reason: `Unverified but valid user` },
138138
});
139139

140-
return res.status(401).json({ message: 'Login failed. Need to verify.' });
140+
return res.status(401).json({ error: 'Login failed. Need to verify.' });
141141
}
142142

143143
const credential = await Credential.findOne({ where: { userId: user.id } });
@@ -151,7 +151,7 @@ export const login = async (req: Request, res: Response) => {
151151
user_agent: req.headers['user-agent'],
152152
metadata: { reason: `No credentials ${identifier}` },
153153
});
154-
return res.status(401).json({ message: 'Need to re-register and create passkey' });
154+
return res.status(401).json({ error: 'Need to re-register and create passkey' });
155155
}
156156

157157
if (token) {
@@ -178,7 +178,7 @@ export const login = async (req: Request, res: Response) => {
178178
ttl: parseDurationToSeconds(access_token_ttl || '15m'),
179179
});
180180
}
181-
return res.status(401).json({ message: 'Login failed.' });
181+
return res.status(401).json({ error: 'Login failed.' });
182182
} catch (error: unknown) {
183183
if (error instanceof Error) {
184184
logger.error(`Error during login for email ${error.message}`);
@@ -193,7 +193,7 @@ export const login = async (req: Request, res: Response) => {
193193
user_agent: req.headers['user-agent'],
194194
metadata: { reason: 'Catch all error' },
195195
});
196-
return res.status(500).json({ message: 'Server error' });
196+
return res.status(500).json({ error: 'Server error' });
197197
}
198198
};
199199

src/controllers/internalMetrics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const getAuthEventSummary = async (req: Request, res: Response) => {
4141
return res.status(400).json({ message: 'Invalid query params' });
4242
}
4343

44+
// TODO: need to parse these for valid time ranges
4445
const { from, to } = parsed.data;
4546

4647
const where: WhereOptions<AuthEventAttributes> =

src/controllers/sessions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export const listSessions = async (req: Request, res: Response) => {
3333
deviceName: session.deviceName,
3434
ipAddress: session.ipAddress,
3535
userAgent: session.userAgent,
36-
lastUsedAt: session.lastUsedAt.toISOString(),
37-
expiresAt: session.expiresAt.toISOString(),
36+
lastUsedAt: session.lastUsedAt,
37+
expiresAt: session.expiresAt,
3838
current: session.id === currentSessionId,
3939
}));
4040

src/controllers/webauthn.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ const verifyWebAuthnRegistration = async (req: Request, res: Response) => {
126126
}
127127

128128
if (!verifiedUser.email || !attestationResponse) {
129+
logger.warn('Missing verified user email or attestation response');
129130
await AuthEvent.create({
130131
user_id: null,
131132
type: 'registration_failed',
@@ -154,6 +155,7 @@ const verifyWebAuthnRegistration = async (req: Request, res: Response) => {
154155

155156
const expectedChallenge = user.challenge;
156157
if (!expectedChallenge) {
158+
logger.error('Unexpected user challegnge supplied.');
157159
await AuthEvent.create({
158160
user_id: user.id,
159161
type: 'registration_suspicous',
@@ -223,6 +225,7 @@ const verifyWebAuthnRegistration = async (req: Request, res: Response) => {
223225
await user.update({
224226
challenge: null,
225227
lastLogin: new Date(),
228+
verified: true,
226229
});
227230

228231
logger.info(`Passkey credential saved successfully for user: ${verifiedUser.email}`);
@@ -253,11 +256,6 @@ const verifyWebAuthnRegistration = async (req: Request, res: Response) => {
253256

254257
const token = await signAccessToken(session.id, user.id, user.roles);
255258

256-
user.challenge = '';
257-
user.verified = true;
258-
259-
await user.save();
260-
261259
if (token && refreshToken) {
262260
await AuthEvent.create({
263261
user_id: user.id,
@@ -287,9 +285,10 @@ const verifyWebAuthnRegistration = async (req: Request, res: Response) => {
287285
refreshTtl: parseDurationToSeconds(refresh_token_ttl || '1h'),
288286
});
289287
}
288+
return res.status(500).json({ error: 'Unknown error verifying passkey' });
290289
} catch (err) {
291290
logger.error(`Error in verifyWebAuthnRegistration: ${err}`);
292-
return res.status(500).json({ message: 'Unknown error verifying passkey' });
291+
return res.status(500).json({ error: 'Unknown error verifying passkey' });
293292
}
294293
};
295294

@@ -338,7 +337,7 @@ const generateWebAuthn = async (req: Request, res: Response) => {
338337
user_agent: req.headers['user-agent'],
339338
metadata: { reason: 'No credentials' },
340339
});
341-
logger.info('Valid user with no credentials');
340+
logger.error('Valid user with no credentials');
342341
return res.status(401).send('Credentials not found');
343342
}
344343

@@ -392,6 +391,7 @@ const verifyWebAuthn = async (req: Request, res: Response) => {
392391

393392
try {
394393
const { assertionResponse } = req.body;
394+
395395
const email = verifiedUser.email;
396396
const phone = verifiedUser.phone;
397397
let user = verifiedUser;
@@ -409,14 +409,15 @@ const verifyWebAuthn = async (req: Request, res: Response) => {
409409
}
410410

411411
if (!user || !user.challenge) {
412+
logger.error('User or user challenge missing');
412413
await AuthEventService.log({
413414
userId: user.id,
414415
type: 'webauthn_login_failed',
415416
req,
416417
metadata: { reason: 'No user or user challenge' },
417418
});
418419

419-
return res.status(401).json({ message: 'Authentication failed.' });
420+
return res.status(401).json({ error: 'Authentication failed.' });
420421
}
421422

422423
const cred = await Credential.findOne({
@@ -433,7 +434,7 @@ const verifyWebAuthn = async (req: Request, res: Response) => {
433434
metadata: { reason: 'No credential' },
434435
});
435436

436-
return res.status(401).json({ message: 'Authentication failed.' });
437+
return res.status(401).json({ error: 'Authentication failed.' });
437438
}
438439

439440
const expectedChallenge = user.challenge;
@@ -467,7 +468,7 @@ const verifyWebAuthn = async (req: Request, res: Response) => {
467468
metadata: { reason: 'Incorrect passkey' },
468469
});
469470

470-
return res.status(500).json({ message: 'Internal server error' });
471+
return res.status(500).json({ error: 'Internal server error' });
471472
}
472473

473474
if (verification.verified) {
@@ -539,7 +540,7 @@ const verifyWebAuthn = async (req: Request, res: Response) => {
539540
user_agent: req.headers['user-agent'],
540541
metadata: { reason: 'Verification failed' },
541542
});
542-
res.status(401).send('Authentication failed');
543+
res.status(401).json({ error: 'Authentication failed' });
543544
return;
544545
}
545546
} catch (error) {
@@ -551,7 +552,7 @@ const verifyWebAuthn = async (req: Request, res: Response) => {
551552
user_agent: req.headers['user-agent'],
552553
metadata: { reason: 'Catch all error' },
553554
});
554-
res.status(500).json({ message: 'Internal Server error' });
555+
res.status(500).json({ error: 'Internal Server error' });
555556
return;
556557
}
557558
};

src/routes/admin.routes.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import {
99
getUserDetail,
1010
getUsers,
1111
listAllSessions,
12+
listUserSessions,
13+
revokeAllUserSessions,
1214
updateUser,
1315
} from '../controllers/admin.js';
1416
import { createRouter } from '../lib/createRouter.js';
1517
import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js';
1618
import { verifyServiceToken } from '../middleware/authenticateServiceToken.js';
1719
import { requireAdmin } from '../middleware/requireAdmin.js';
20+
import { UserIdParamSchema } from '../schemas/admin.query.js';
1821
import { UserResponseSchema } from '../schemas/admin.responses.js';
1922
import { InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js';
2023
import { AuthEventQuerySchema, PaginationQuerySchema } from '../schemas/internal.query.js';
@@ -23,6 +26,7 @@ import {
2326
CredentialCountSchema,
2427
UsersListResponseSchema,
2528
} from '../schemas/internal.responses.js';
29+
import { SessionListResponseSchema } from '../schemas/session.responses.js';
2630

2731
const adminRouter = createRouter('/admin');
2832

@@ -154,4 +158,36 @@ adminRouter.get(
154158
listAllSessions,
155159
);
156160

161+
adminRouter.get(
162+
'/sessions/:userId',
163+
{
164+
middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()],
165+
tags: ['Admin'],
166+
schemas: {
167+
params: UserIdParamSchema,
168+
response: {
169+
200: SessionListResponseSchema,
170+
500: InternalErrorSchema,
171+
},
172+
},
173+
},
174+
listUserSessions,
175+
);
176+
177+
adminRouter.delete(
178+
'/sessions/:userId/revoke-all',
179+
{
180+
middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()],
181+
tags: ['Admin'],
182+
schemas: {
183+
params: UserIdParamSchema,
184+
response: {
185+
200: MessageSchema,
186+
500: InternalErrorSchema,
187+
},
188+
},
189+
},
190+
revokeAllUserSessions,
191+
);
192+
157193
export default adminRouter.router;

src/routes/admin.sessions.routes.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

tests/e2e/authFlow.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,7 @@ describe('E2E Auth Flow', () => {
119119
id: 'user-1',
120120
});
121121

122-
(Session.create as any).mockResolvedValue({
123-
id: 'session-2',
124-
});
122+
(Session.create as any).mockResolvedValue(buildSession({ id: 'session-2' }));
125123

126124
const refreshRes = await request(app)
127125
.get('/sessions')
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function buildEvent(overrides: any = {}) {
2+
return {
3+
type: 'login_failed',
4+
ip_address: '127.0.0.1',
5+
user_agent: 'agent',
6+
get: (key: string) => overrides[key],
7+
...overrides,
8+
};
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { vi } from 'vitest';
2+
3+
export function buildCredential(overrides: any = {}) {
4+
return {
5+
id: 'cred-1',
6+
userId: 'user-1',
7+
friendlyName: 'My Device',
8+
transports: [],
9+
deviceType: 'platform',
10+
backedup: false,
11+
counter: 0,
12+
lastUsedAt: new Date(),
13+
platform: 'web',
14+
browser: 'chrome',
15+
deviceInfo: 'test',
16+
createdAt: new Date(),
17+
publicKey: 'key',
18+
update: vi.fn(),
19+
destroy: vi.fn(),
20+
...overrides,
21+
};
22+
}

0 commit comments

Comments
 (0)