Receiving, a home's keys that can't be fetched answer 502, which the sender retries, instead of 401, which it dropped; a bad update answers 400 rather than 502; a blocked instance's events answer 403; events are limited to a burst of 120 per instance, and a token naming an unknown key refetches the keys at most once a minute. An avatar must be on its issuer's origin, and its URL is versioned by its file's write time instead of updatedAt, so a profile edit no longer makes clients download it again. An instance that doesn't answer can be blocked, keeping its name, though not trusted. 303 backend tests and 703 frontend tests pass, the federation tests 8 times in a row, and deno check, tsc, eslint, doc:lint and i18n:compile --strict are clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
/**
|
|
* Resets forgotten passwords. The client's `/login` page asks for a reset
|
|
* link, which the email takes to the `/reset-password` page, whose token
|
|
* names the reset.
|
|
*
|
|
* - `POST /api/password-resets` — public. Emails a reset link to the account
|
|
* with that `email`, if any; always answers 200.
|
|
* - `PUT /api/password-resets/:token` — public, with a reset token. Sets the
|
|
* `newPassword`, which ends the user's sessions here and on the instances
|
|
* they signed in to.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { Router } from "@oak/oak";
|
|
|
|
import { type APIResponse } from "../../shared/api.ts";
|
|
import { APIException } from "../lib/api-exception.ts";
|
|
import { fieldOf } from "../validators/user.ts";
|
|
import {
|
|
requestPasswordReset,
|
|
resetPassword,
|
|
} from "../services/password-reset-service.ts";
|
|
import { notifyClients } from "../services/identity-provider-service.ts";
|
|
|
|
/**
|
|
* Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware.
|
|
* An `APIException` thrown by a handler becomes the JSON error response there.
|
|
*/
|
|
const router: Router = new Router({ prefix: "/api/password-resets" });
|
|
|
|
// Answers the same whatever happens, so it can't tell which addresses have an
|
|
// account.
|
|
router.post("/", async (ctx) => {
|
|
const body: unknown = await ctx.request.body.json();
|
|
const email = fieldOf(body, "email");
|
|
const trimmed = typeof email === "string" ? email.trim() : "";
|
|
if (trimmed) {
|
|
await requestPasswordReset(trimmed).catch((err) =>
|
|
console.error("[password-resets]", err)
|
|
);
|
|
}
|
|
const responseBody: APIResponse<null> = { success: true, data: null };
|
|
ctx.response.body = responseBody;
|
|
});
|
|
|
|
router.put("/:token", async (ctx) => {
|
|
const body: unknown = await ctx.request.body.json();
|
|
const newPassword = fieldOf(body, "newPassword");
|
|
if (typeof newPassword !== "string") {
|
|
throw new APIException(400, "Invalid request");
|
|
}
|
|
const userId = await resetPassword(ctx.params.token, newPassword);
|
|
await notifyClients(userId, { type: "revoke" });
|
|
const responseBody: APIResponse<null> = { success: true, data: null };
|
|
ctx.response.body = responseBody;
|
|
});
|
|
|
|
export default router;
|