import type { User } from 'types'; import type { Handler } from 'hono'; import { updateUser } from 'officerdb'; import { verify } from '@@/jwt'; import argon2 from 'argon2'; import * as errors from '@@/custom-errors'; import { validatePassword } from './validate-password'; // The token must be a reset-password token and nothing else. `verifyToken` — which only renders the form // — has always checked `purpose`; this handler, which actually rewrites the password, did not, so any // valid signed JWT was accepted here. A 30-day session token is a valid signed JWT, which made this a // route from "holds a session token" to "owns the account": it rewrites the password (locking the real // owner out, since passwordChangedAt invalidates their other tokens) and forces `status: 'Active'`, // re-enabling an account somebody had deliberately suspended. The two handlers now agree. export const resetPasswordHandler: Handler = async function (ctx) { const { password, verificationCode } = ctx.get('body'); validatePassword(password); const userInfo = (await verify(verificationCode)) as User & { purpose?: string }; if (!userInfo?.id) throw errors.UNAUTHORIZED(); if (userInfo.purpose !== 'reset-password') throw errors.UNAUTHORIZED(); const passwordHash = await argon2.hash(password); await updateUser(userInfo.id, { password: passwordHash, status: 'Active', passwordChangedAt: new Date() }); return ctx.json({ ok: true }); };