v3: added password change/reset feature

This commit is contained in:
khannurien
2026-04-06 16:30:00 +00:00
parent 3b6980a8fc
commit 20b9bfe7b4
26 changed files with 1268 additions and 236 deletions

View File

@@ -22,8 +22,17 @@ import {
updateUser,
} from "../services/user-service.ts";
import { redeemInvite, validateInvite } from "../services/invite-service.ts";
import {
requestPasswordReset,
resetPassword,
} from "../services/password-reset-service.ts";
import { isEmailEnabled, sendEmail } from "../services/email-service.ts";
import { FROM_EMAIL, OG_SITE_NAME, WELCOME_EMAIL_BODY } from "../config.ts";
import {
FROM_EMAIL,
OG_SITE_NAME,
VALIDATION,
WELCOME_EMAIL_BODY,
} from "../config.ts";
import { marked } from "marked";
import { broadcastUserUpdated } from "../services/ws-service.ts";
import {
@@ -163,6 +172,74 @@ router.get("/me", authMiddleware, (ctx: AuthContext) => {
}
});
// Request a password reset email (unauthenticated; always returns 200)
router.post("/request-password-reset", async (ctx) => {
const body = await ctx.request.body.json();
const email = typeof body?.email === "string" ? body.email.trim() : "";
if (email) {
await requestPasswordReset(email).catch((err) =>
console.error("[request-password-reset]", err)
);
}
ctx.response.body = { success: true };
});
// Consume a reset token and set a new password (unauthenticated)
router.post("/reset-password", async (ctx) => {
const body = await ctx.request.body.json();
const { token, newPassword } = (body ?? {}) as Record<string, unknown>;
if (typeof token !== "string" || typeof newPassword !== "string") {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
400,
"Invalid request",
);
}
await resetPassword(token, newPassword);
ctx.response.body = { success: true };
});
// Change current user's password (requires current password verification)
router.post("/me/change-password", authMiddleware, async (ctx: AuthContext) => {
const body = await ctx.request.body.json();
const { currentPassword, newPassword } = (body ?? {}) as Record<
string,
unknown
>;
if (typeof currentPassword !== "string" || typeof newPassword !== "string") {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
400,
"Invalid request",
);
}
if (
newPassword.length < VALIDATION.PASSWORD_MIN ||
newPassword.length > VALIDATION.PASSWORD_MAX
) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
400,
`Password must be ${VALIDATION.PASSWORD_MIN}${VALIDATION.PASSWORD_MAX} characters`,
);
}
const user = getUserById(ctx.state.user.userId);
const valid = await verifyPassword(currentPassword, user.passwordHash);
if (!valid) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
401,
"Current password is incorrect",
);
}
await updateUser(ctx.state.user.userId, { password: newPassword });
ctx.response.body = { success: true };
});
// Update current user profile (description, etc.)
router.patch("/me", authMiddleware, async (ctx: AuthContext) => {
const body = await ctx.request.body.json();