// Auth ยท Password โ€” self-service password reset. Public but token-gated: request // a link by email, then validate the link and set a new password. // // Mounted at /api/v1/auth/password by auth/index.js, so the routes below emit // POST /auth/password/forgot and GET|POST /auth/password/reset/:token. // // Two anti-enumeration properties are load-bearing and must survive any edit // here: the request step always returns the same generic 200 whether or not the // email matches an account, and the lookup step never distinguishes expired from // used from never-existed. Both live in passwordReset.controller; the per-IP // limiters below are what stop the endpoints being used as an oracle by volume. // // Changing a password while signed in is a different route โ€” // PATCH /auth/me/account/password. const express = require('express') const { body, param } = require('express-validator') const { requestReset, lookupReset, confirmReset } = require('./passwordReset.controller') const { passwordResetRequestLimiter, passwordResetConfirmLimiter, } = require('../../../middleware/rateLimit') const validate = require('../../../middleware/validate') const passwordRouter = express.Router() passwordRouter.post( '/forgot', // #swagger.tags = ['Auth'] // #swagger.summary = 'Request a password-reset link by email' // #swagger.description = 'Emails a single-use, ~1h reset link to the active account on the address. Always returns the same generic 200 whether or not the email matches (no account enumeration). Rate limited per IP.' /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["email"], properties: { email: { type: "string", format: "email" } } } } } } */ /* #swagger.responses[200] = { description: 'Generic acknowledgement (sent if the account exists)', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */ /* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */ /* #swagger.responses[429] = { description: 'Too many requests', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ passwordResetRequestLimiter, body('email').isString().trim().isEmail().isLength({ max: 255 }), validate, requestReset, ) passwordRouter.get( '/reset/:token', // #swagger.tags = ['Auth'] // #swagger.summary = 'Validate a password-reset link' // #swagger.description = 'Returns the target username for a valid, pending, unexpired reset link so the reset form can render. 404 for anything not currently usable (never distinguishes expired from used from never-existed).' /* #swagger.responses[200] = { description: 'Reset link is valid', content: { "application/json": { schema: { type: "object", properties: { username: { type: "string" } } } } } } */ /* #swagger.responses[404] = { description: 'Invalid or expired reset link', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('token').isString().isLength({ min: 8, max: 128 }), validate, lookupReset, ) passwordRouter.post( '/reset/:token', // #swagger.tags = ['Auth'] // #swagger.summary = 'Set a new password from a reset link' // #swagger.description = 'Consumes the single-use link and sets the new password. Rotates the hash and revokes every existing session (web + mobile). Does NOT sign the user in โ€” they log in fresh afterwards (so a 2FA account still passes TOTP). Rate limited per IP.' /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["password"], properties: { password: { type: "string", minLength: 8, maxLength: 64 } } } } } } */ /* #swagger.responses[200] = { description: 'Password changed', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */ /* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */ /* #swagger.responses[404] = { description: 'Invalid, expired, or already-used reset link', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[429] = { description: 'Too many attempts', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ passwordResetConfirmLimiter, param('token').isString().isLength({ min: 8, max: 128 }), body('password').isString().isLength({ min: 8, max: 64 }), validate, confirmReset, ) module.exports = passwordRouter