mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
fix(auth): move onboarding redirect from AuthShell useEffect to proxy
The post-hydration redirect for users with `onboardingCompletedAt === null` caused a brief dashboard flash before the client-side effect could fire. The redirect now lives in `src/proxy.ts` so it lands on the first server response. The proxy runs in the Edge runtime and can't reach Prisma, so the auth routes (login / passkey-verify / register / password / /api/auth/me) mirror the DB `onboardingCompletedAt` column into a non-httpOnly `hl_onboarding` cookie. The cookie is a UX hint, not a security signal — a user editing it locally only skips the dashboard flash; they still can't bypass any server check. `/api/onboarding/complete` clears the cookie so the next navigation drops the redirect immediately, and `destroySession` + expired-session purge wipe it alongside the session cookie. The e2e onboarding-flicker spec can return to testing the actual incomplete-onboarding case: the proxy reads the cookie (absent for the fully-onboarded storage-state user), not the mocked `/api/auth/me` response, so the test now sets `onboardingCompletedAt: null` honestly instead of pinning it to a non-null value as a workaround. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b281c0655b
commit
15796c148b
14 files changed
+208
-32
No files matched your search
@@ -17,6 +17,7 @@ vi.mock("@/lib/auth/password", () => ({
|
||||
|
||||
vi.mock("@/lib/auth/session", () => ({
|
||||
createSession: vi.fn().mockResolvedValue("session-id"),
|
||||
setOnboardingPendingCookie: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/audit", () => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { loginPasswordSchema } from "@/lib/validations/auth";
|
||||
import { verifyPassword } from "@/lib/auth/password";
|
||||
import { createSession } from "@/lib/auth/session";
|
||||
import { createSession, setOnboardingPendingCookie } from "@/lib/auth/session";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
@@ -77,6 +77,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const ua = request.headers.get("user-agent");
|
||||
await createSession(user.id, ip, ua);
|
||||
// v1.4.22 C4 — mirror the user's onboarding status into a cookie so
|
||||
// the proxy can short-circuit the redirect before the page hydrates.
|
||||
await setOnboardingPendingCookie(user.onboardingCompletedAt == null);
|
||||
|
||||
await auditLog("auth.login.password", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -2,12 +2,19 @@ import { apiSuccess } from "@/lib/api-response";
|
||||
import { getGravatarUrl } from "@/lib/gravatar";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { setOnboardingPendingCookie } from "@/lib/auth/session";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
// v1.4.22 C4 — keep the proxy-readable onboarding cookie in sync
|
||||
// with the DB state on every /me roundtrip. Old sessions that
|
||||
// predate the cookie pick it up on first dashboard load; the
|
||||
// /onboarding/complete handler always wins on the cleared edge.
|
||||
await setOnboardingPendingCookie(user.onboardingCompletedAt == null);
|
||||
|
||||
annotate({ action: { name: "auth.me" } });
|
||||
|
||||
return apiSuccess({
|
||||
|
||||
@@ -15,6 +15,7 @@ vi.mock("@/lib/auth/passkey", () => ({
|
||||
|
||||
vi.mock("@/lib/auth/session", () => ({
|
||||
createSession: vi.fn().mockResolvedValue("session-id"),
|
||||
setOnboardingPendingCookie: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/audit", () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { verifyAuthentication } from "@/lib/auth/passkey";
|
||||
import { createSession } from "@/lib/auth/session";
|
||||
import { createSession, setOnboardingPendingCookie } from "@/lib/auth/session";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
@@ -67,6 +67,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const ua = request.headers.get("user-agent");
|
||||
await createSession(user.id, ip, ua);
|
||||
// v1.4.22 C4 — mirror onboarding status into the proxy-readable cookie
|
||||
// so the redirect short-circuits before the page hydrates.
|
||||
await setOnboardingPendingCookie(user.onboardingCompletedAt == null);
|
||||
|
||||
await auditLog("auth.login.passkey", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -16,7 +16,11 @@ import {
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { destroyAllSessions, createSession } from "@/lib/auth/session";
|
||||
import {
|
||||
destroyAllSessions,
|
||||
createSession,
|
||||
setOnboardingPendingCookie,
|
||||
} from "@/lib/auth/session";
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
@@ -82,6 +86,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
getClientIp(request),
|
||||
request.headers.get("user-agent"),
|
||||
);
|
||||
// v1.4.22 C4 — re-anchor the onboarding cookie to the user's real
|
||||
// status after the session rebuild.
|
||||
await setOnboardingPendingCookie(user.onboardingCompletedAt == null);
|
||||
|
||||
await auditLog("auth.password.change", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { registerSchema } from "@/lib/validations/auth";
|
||||
import { hashPassword, checkPasswordStrength } from "@/lib/auth/password";
|
||||
import { createSession } from "@/lib/auth/session";
|
||||
import { createSession, setOnboardingPendingCookie } from "@/lib/auth/session";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
@@ -97,6 +97,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// Create session immediately
|
||||
const ua = request.headers.get("user-agent");
|
||||
await createSession(user.id, ip, ua);
|
||||
// v1.4.22 C4 — fresh users are always onboarding-pending. The cookie
|
||||
// lets the proxy redirect on first navigation instead of waiting for
|
||||
// `<AuthShell>` to hydrate and trigger a useEffect bounce.
|
||||
await setOnboardingPendingCookie(true);
|
||||
|
||||
await auditLog("auth.register", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
|
||||
import { setOnboardingPendingCookie } from "@/lib/auth/session";
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
@@ -56,5 +57,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
data,
|
||||
});
|
||||
|
||||
// v1.4.22 C4 — clear the proxy-readable onboarding cookie so the
|
||||
// next navigation drops the /onboarding redirect immediately.
|
||||
await setOnboardingPendingCookie(false);
|
||||
|
||||
return apiSuccess({ completed: true });
|
||||
});
|
||||
Reference in new issue
Block a user