mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
Restore the reset-password rate limit and fix copyTrip reservation links
Two correctness/security gaps the NestJS migration introduced: - POST /api/auth/reset-password lost its per-IP rate limiter. Restore it (5 attempts / 15 min on a dedicated bucket, same as the old resetLimiter) so reset tokens can't be brute-forced unthrottled. Covered by AUTH-019. - copyTripById did not copy reservations.end_day_id (a day reference — now remapped through dayMap like day_id) or needs_review, so a duplicated trip lost multi-day transport end-day links and reset the review flag.
This commit is contained in:
@@ -121,6 +121,9 @@ export class AuthPublicController {
|
||||
@Post('reset-password')
|
||||
@HttpCode(200)
|
||||
resetPassword(@Body() body: unknown, @Req() req: Request) {
|
||||
// Per-IP brute-force guard, parity with the legacy resetLimiter (5 / 15 min on
|
||||
// a dedicated bucket) — without it reset tokens could be guessed unthrottled.
|
||||
this.limit('reset', req, 5);
|
||||
const ip = getClientIp(req);
|
||||
const result = this.auth.resetPassword(body);
|
||||
if (result.error) {
|
||||
|
||||
@@ -636,19 +636,22 @@ export function copyTripById(sourceTripId: string | number, newOwnerId: number,
|
||||
|
||||
const oldReservations = db.prepare('SELECT * FROM reservations WHERE trip_id = ?').all(sourceTripId) as any[];
|
||||
const insertReservation = db.prepare(`
|
||||
INSERT INTO reservations (trip_id, day_id, place_id, assignment_id, accommodation_id, title, reservation_time, reservation_end_time,
|
||||
location, confirmation_number, notes, status, type, metadata, day_plan_position)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO reservations (trip_id, day_id, end_day_id, place_id, assignment_id, accommodation_id, title, reservation_time, reservation_end_time,
|
||||
location, confirmation_number, notes, status, type, metadata, day_plan_position, needs_review)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const r of oldReservations) {
|
||||
insertReservation.run(newTripId,
|
||||
r.day_id ? (dayMap.get(r.day_id) ?? null) : null,
|
||||
// end_day_id is a day reference too (multi-day transport) — remap it like
|
||||
// day_id, otherwise the duplicated trip loses the reservation's end-day link.
|
||||
r.end_day_id ? (dayMap.get(r.end_day_id) ?? null) : null,
|
||||
r.place_id ? (placeMap.get(r.place_id) ?? null) : null,
|
||||
r.assignment_id ? (assignmentMap.get(r.assignment_id) ?? null) : null,
|
||||
r.accommodation_id ? (accomMap.get(r.accommodation_id) ?? null) : null,
|
||||
r.title, r.reservation_time, r.reservation_end_time,
|
||||
r.location, r.confirmation_number, r.notes, r.status, r.type,
|
||||
r.metadata, r.day_plan_position);
|
||||
r.metadata, r.day_plan_position, r.needs_review ?? 0);
|
||||
}
|
||||
|
||||
const oldBudget = db.prepare('SELECT * FROM budget_items WHERE trip_id = ?').all(sourceTripId) as any[];
|
||||
|
||||
@@ -835,6 +835,18 @@ describe('Rate limiting', () => {
|
||||
}
|
||||
expect(lastStatus).toBe(429);
|
||||
});
|
||||
|
||||
it('AUTH-019 — reset-password endpoint rate-limits after 5 attempts (parity with the legacy resetLimiter)', async () => {
|
||||
let lastStatus = 0;
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const res = await request(app)
|
||||
.post('/api/auth/reset-password')
|
||||
.send({ token: 'badtoken', new_password: 'NewPassw0rd!' });
|
||||
lastStatus = res.status;
|
||||
if (lastStatus === 429) break;
|
||||
}
|
||||
expect(lastStatus).toBe(429);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user