feat(onboarding): M2 — route guard + shell + Screen 1 (name)

- PATCH /api/v1/me/profile in mana-auth (name, image with 1–80 char
  validation) — powers the Screen-1 save
- (app)/+layout.svelte:
  * isOnboarding derived from pathname
  * handleAuthReady loads onboardingStatus, redirects brand-new users
    to /onboarding/name (fire-and-forget so sync/data-layer init keeps
    running in parallel)
  * chrome (PillNav, wallpaper, bottom-stack) hidden in onboarding mode;
    AuthGate still wraps so the flow enforces authentication
- /onboarding/+layout.svelte: full-viewport shell with progress dots
  (1/3, 2/3, 3/3) and a skip-all that marks the flow complete and
  sends the user home
- /onboarding/+page.svelte: redirects bare entry to /onboarding/name
- /onboarding/name/+page.svelte: text input (1–40 chars), Enter = Weiter,
  skip falls back to email local-part so Screen 2's greeting is never
  empty

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-23 22:49:52 +02:00
parent 5a92e1168b
commit 5aecf8b90d
6 changed files with 689 additions and 229 deletions

View file

@ -99,7 +99,7 @@ app.route('/api/v1/api-keys', createApiKeyValidationRoute(apiKeysService));
// ─── Me (GDPR) ──────────────────────────────────────────────
app.use('/api/v1/me/*', jwtAuth(config.baseUrl));
app.route('/api/v1/me', createMeRoutes(userDataService));
app.route('/api/v1/me', createMeRoutes(userDataService, db));
// ─── Encryption vault (per-user master key custody) ────────
// Mounted under /me so it inherits the JWT middleware above and shows

View file

@ -7,11 +7,14 @@
*/
import { Hono } from 'hono';
import { eq } from 'drizzle-orm';
import type { AuthUser } from '../middleware/jwt-auth';
import type { UserDataService } from '../services/user-data';
import type { Database } from '../db/connection';
import { users } from '../db/schema/auth';
import { sendAccountDeletionEmail } from '../email/send';
export function createMeRoutes(userDataService: UserDataService) {
export function createMeRoutes(userDataService: UserDataService, db: Database) {
return (
new Hono<{ Variables: { user: AuthUser } }>()
@ -57,5 +60,46 @@ export function createMeRoutes(userDataService: UserDataService) {
return c.json(result);
})
// ─── Update profile (name, avatar) ──────────────────────
// Minimal patch endpoint used by the onboarding flow and
// Settings → Profile. JWT-based like the rest of /me/*; the
// updated name only lands in the user's JWT on next mint, so
// the caller is responsible for refreshing its in-memory
// representation of authStore.user. See docs/plans/onboarding-flow.md.
.patch('/profile', async (c) => {
const user = c.get('user');
const body = (await c.req.json().catch(() => ({}))) as {
name?: unknown;
image?: unknown;
};
const patch: { name?: string; image?: string; updatedAt: Date } = {
updatedAt: new Date(),
};
if (typeof body.name === 'string') {
const trimmed = body.name.trim();
if (trimmed.length < 1 || trimmed.length > 80) {
return c.json({ error: 'name must be 180 characters' }, 400);
}
patch.name = trimmed;
}
if (typeof body.image === 'string') {
patch.image = body.image;
}
if (!('name' in patch) && !('image' in patch)) {
return c.json({ error: 'no fields to update' }, 400);
}
const [updated] = await db
.update(users)
.set(patch)
.where(eq(users.id, user.userId))
.returning({ id: users.id, name: users.name, image: users.image });
if (!updated) return c.json({ error: 'User not found' }, 404);
return c.json({ name: updated.name, image: updated.image });
})
);
}