mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
fix(auth): carry purgeAt on the native OIDC deletion-pending redirect
The web leg of the OIDC callback already names the purge date when the account is pending deletion; the native leg redirected with the bare reason. It now adds the same purgeAt parameter to the custom-scheme redirect, and only the date. Refs #1341
This commit is contained in:
1 parent
a812fb699f
commit
66533497de
5 files changed
+78
-15
No files matched your search
@@ -653,6 +653,9 @@ describe("GET /api/auth/oidc/callback — duplicate-callback idempotency", () =>
|
||||
});
|
||||
});
|
||||
|
||||
/** The two columns `pendingDeletionOf` reads off the user row. */
|
||||
type PendingRow = Awaited<ReturnType<typeof prisma.user.findUnique>>;
|
||||
|
||||
describe("GET /api/auth/oidc/callback — native branch", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(checkAuthSurfaceRateLimit).mockResolvedValue({
|
||||
@@ -751,6 +754,40 @@ describe("GET /api/auth/oidc/callback — native branch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("carries purgeAt, and nothing else, on a native sign-in to an account pending deletion", async () => {
|
||||
mockIdentity();
|
||||
mockUserLookups({
|
||||
byIdentity: userRow({ oidcIssuer: METADATA.issuer, oidcSub: "sub-1" }),
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
|
||||
deletionRequestedAt: new Date("2026-11-01T10:00:00.000Z"),
|
||||
deletionPurgeAt: new Date("2026-12-01T10:00:00.000Z"),
|
||||
} as PendingRow);
|
||||
|
||||
const res = await GET(nativeRequest());
|
||||
expect(res.headers.get("location")).toBe(
|
||||
"healthlog://oidc-callback?error=account_deletion_pending&purgeAt=2026-12-01T10%3A00%3A00.000Z",
|
||||
);
|
||||
expect(mintNativeHandoff).not.toHaveBeenCalled();
|
||||
expect(createSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("omits purgeAt on the native leg when the purge date is unknown", async () => {
|
||||
mockIdentity();
|
||||
mockUserLookups({
|
||||
byIdentity: userRow({ oidcIssuer: METADATA.issuer, oidcSub: "sub-1" }),
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
|
||||
deletionRequestedAt: new Date("2026-11-01T10:00:00.000Z"),
|
||||
deletionPurgeAt: null,
|
||||
} as PendingRow);
|
||||
|
||||
const res = await GET(nativeRequest());
|
||||
expect(res.headers.get("location")).toBe(
|
||||
"healthlog://oidc-callback?error=account_deletion_pending",
|
||||
);
|
||||
});
|
||||
|
||||
it("delivers an IdP denial to the custom scheme", async () => {
|
||||
const res = await GET(
|
||||
makeRequest({
|
||||
|
||||
@@ -95,6 +95,26 @@ function secondFactorRedirect(
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* A failure on the native leg: the custom scheme with `error` and, at most,
|
||||
* a non-secret detail beside it. An error redirect carries no code, ticket,
|
||||
* or session.
|
||||
*/
|
||||
function nativeErrorRedirect(params: Record<string, string>): NextResponse {
|
||||
const response = NextResponse.redirect(buildNativeCallbackUrl(params));
|
||||
deleteStateCookie(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The purge date as the `purgeAt` parameter the native leg carries beside
|
||||
* `account_deletion_pending`, mirroring `deletionPendingRedirect` (#1341).
|
||||
* Only the date: nothing else about the account rides the URL.
|
||||
*/
|
||||
function purgeAtParam(purgeAt: Date | null): Record<string, string> {
|
||||
return purgeAt ? { purgeAt: purgeAt.toISOString() } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The web sign-in page for an account pending deletion, with the date it is
|
||||
* erased, so the page can say until when an admin can undo it.
|
||||
@@ -180,16 +200,13 @@ export const GET = apiHandler(async (req: NextRequest) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
const failRedirect = (reason: string): NextResponse => {
|
||||
const failRedirect = (
|
||||
reason: string,
|
||||
nativeExtra: Record<string, string> = {},
|
||||
): NextResponse => {
|
||||
if (isReproof) return reproofBack(reason);
|
||||
if (isClaim) return claimRedirect(reason);
|
||||
if (isNative) {
|
||||
const response = NextResponse.redirect(
|
||||
buildNativeCallbackUrl({ error: reason }),
|
||||
);
|
||||
deleteStateCookie(response);
|
||||
return response;
|
||||
}
|
||||
if (isNative) return nativeErrorRedirect({ error: reason, ...nativeExtra });
|
||||
return errorRedirect(reason);
|
||||
};
|
||||
|
||||
@@ -349,7 +366,10 @@ export const GET = apiHandler(async (req: NextRequest) => {
|
||||
// the caller is this account, so the sign-in page may name the date.
|
||||
const pendingDeletion = await pendingDeletionOf(user.id);
|
||||
if (pendingDeletion) {
|
||||
if (isNative || isClaim) return failRedirect("account_deletion_pending");
|
||||
if (isNative || isClaim) {
|
||||
const extra = purgeAtParam(pendingDeletion.purgeAt);
|
||||
return failRedirect("account_deletion_pending", extra);
|
||||
}
|
||||
return deletionPendingRedirect(pendingDeletion.purgeAt);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user