mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:01:09 +02:00
- auth.users: new nullable `onboarding_completed_at` column - new /api/v1/me/onboarding routes: GET, POST /complete, PATCH /reset - onboardingStatus Svelte store in the web app that reads/writes via those endpoints (no JWT claim so completing the flow takes effect without a token re-mint) - docs/plans/onboarding-flow.md adjusted: no backfill (launch without existing users), better-auth `name` clarified, 7 templates including "Arbeit" confirmed Foundation for the 3-screen first-login flow (Name → Look → Templates). No UI and no route guard yet — those ship in M2 when the redirect target actually exists. Schema change is a pure column-add, applied via `pnpm --filter @mana/auth db:push`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
/**
|
|
* Onboarding routes — per-user completion status for the 3-screen
|
|
* first-login flow (Name → Look → Templates).
|
|
*
|
|
* GET / — { completedAt: ISO string | null }
|
|
* POST /complete — idempotent; sets `onboardingCompletedAt = now()` if null
|
|
* PATCH /reset — sets back to null (for "Onboarding erneut durchlaufen")
|
|
*
|
|
* Mounted under `/api/v1/me/onboarding`, so it inherits the same
|
|
* `jwtAuth` middleware as the GDPR `/me/*` routes.
|
|
*
|
|
* Design notes — see docs/plans/onboarding-flow.md §"Data changes":
|
|
* we keep the state on a first-class column (not in `user_settings`
|
|
* JSONB) so a brand-new account reliably returns `null` without having
|
|
* to distinguish "no settings row" from "explicitly null". And we use
|
|
* a dedicated endpoint rather than a JWT claim so finishing the flow
|
|
* takes effect without a token re-mint.
|
|
*/
|
|
|
|
import { Hono } from 'hono';
|
|
import { eq } from 'drizzle-orm';
|
|
import type { AuthUser } from '../middleware/jwt-auth';
|
|
import type { Database } from '../db/connection';
|
|
import { users } from '../db/schema/auth';
|
|
|
|
type OnboardingApp = Hono<{ Variables: { user: AuthUser } }>;
|
|
|
|
export function createOnboardingRoutes(db: Database) {
|
|
const app: OnboardingApp = new Hono();
|
|
|
|
app.get('/', async (c) => {
|
|
const user = c.get('user');
|
|
const [row] = await db
|
|
.select({ completedAt: users.onboardingCompletedAt })
|
|
.from(users)
|
|
.where(eq(users.id, user.userId))
|
|
.limit(1);
|
|
|
|
if (!row) return c.json({ error: 'User not found' }, 404);
|
|
return c.json({ completedAt: row.completedAt?.toISOString() ?? null });
|
|
});
|
|
|
|
app.post('/complete', async (c) => {
|
|
const user = c.get('user');
|
|
const now = new Date();
|
|
const [updated] = await db
|
|
.update(users)
|
|
.set({ onboardingCompletedAt: now, updatedAt: now })
|
|
.where(eq(users.id, user.userId))
|
|
.returning({ completedAt: users.onboardingCompletedAt });
|
|
|
|
if (!updated) return c.json({ error: 'User not found' }, 404);
|
|
return c.json({ completedAt: updated.completedAt?.toISOString() ?? null });
|
|
});
|
|
|
|
app.patch('/reset', async (c) => {
|
|
const user = c.get('user');
|
|
const [updated] = await db
|
|
.update(users)
|
|
.set({ onboardingCompletedAt: null, updatedAt: new Date() })
|
|
.where(eq(users.id, user.userId))
|
|
.returning({ completedAt: users.onboardingCompletedAt });
|
|
|
|
if (!updated) return c.json({ error: 'User not found' }, 404);
|
|
return c.json({ completedAt: null });
|
|
});
|
|
|
|
return app;
|
|
}
|