Add admin password reset flow

This commit is contained in:
Zimeng Xiong
2026-02-06 14:11:13 -08:00
parent e4941ad77f
commit 1e617025df
23 changed files with 4205 additions and 698 deletions
+34 -1
View File
@@ -89,6 +89,7 @@ declare global {
name: string;
role: string;
mustResetPassword?: boolean;
impersonatorId?: string;
};
}
}
@@ -98,6 +99,7 @@ interface JwtPayload {
userId: string;
email: string;
type: "access" | "refresh";
impersonatorId?: string;
}
/**
@@ -108,10 +110,13 @@ const isJwtPayload = (decoded: unknown): decoded is JwtPayload => {
return false;
}
const payload = decoded as Record<string, unknown>;
const impersonatorOk =
typeof payload.impersonatorId === "undefined" || typeof payload.impersonatorId === "string";
return (
typeof payload.userId === "string" &&
typeof payload.email === "string" &&
(payload.type === "access" || payload.type === "refresh")
(payload.type === "access" || payload.type === "refresh") &&
impersonatorOk
);
};
@@ -148,6 +153,23 @@ const verifyToken = (token: string): JwtPayload | null => {
}
};
const normalizeRequestPath = (req: Request): string => {
const raw = (req.originalUrl || req.url || "").split("?")[0] || "";
// In some deployments the backend may see a /api prefix.
return raw.replace(/^\/api(?=\/)/, "");
};
const isAllowedWhileMustResetPassword = (req: Request): boolean => {
const path = normalizeRequestPath(req);
// Permit fetching current user and changing password.
if (req.method === "GET" && path === "/auth/me") return true;
if (req.method === "POST" && path === "/auth/change-password") return true;
if (req.method === "POST" && path === "/auth/must-reset-password") return true;
return false;
};
/**
* Require authentication middleware
* Protects routes that require a valid JWT token
@@ -224,6 +246,15 @@ export const requireAuth = async (
return;
}
if (user.mustResetPassword && !isAllowedWhileMustResetPassword(req)) {
res.status(403).json({
error: "Forbidden",
code: "MUST_RESET_PASSWORD",
message: "You must reset your password before using the app",
});
return;
}
// Attach user to request
req.user = {
id: user.id,
@@ -232,6 +263,7 @@ export const requireAuth = async (
name: user.name,
role: user.role,
mustResetPassword: user.mustResetPassword,
impersonatorId: payload.impersonatorId,
};
next();
@@ -297,6 +329,7 @@ export const optionalAuth = async (
name: user.name,
role: user.role,
mustResetPassword: user.mustResetPassword,
impersonatorId: payload.impersonatorId,
};
}
} catch (error) {