optimize post migration

This commit is contained in:
JCEEE
2026-08-17 07:41:58 +01:00
parent 5204e7bdbc
commit fb18593ac5
17 changed files with 121 additions and 132 deletions
+3 -3
View File
@@ -25,7 +25,7 @@
| Superuser | PB `_superusers` (server-side only, `pb-admin`) | — | — | | Superuser | PB `_superusers` (server-side only, `pb-admin`) | — | — |
- **Admins** (parents) are `users` records (role `parent`). They authenticate via email/password login, get an httpOnly `pb_token` cookie with `{ id, name, username, role: "parent", famId, color }`. - **Admins** (parents) are `users` records (role `parent`). They authenticate via email/password login, get an httpOnly `pb_token` cookie with `{ id, name, username, role: "parent", famId, color }`.
- **Members** (children) are `users` records (role `child`); PB `username` = `{famSlug}:{handle}` (globally-unique auth identity; `handle` = whitespace-free lowercase name), URL segment = `handleOf(username)`, `name` = display name. Their PB password is **derived** server-side (`MEMBER_SECRET + famSlug + handle`); access is gated by a 20-min OTP in `user_configs`, then `authWithPassword`. They get the same httpOnly `pb_token` cookie. There is **no `members` collection**. - **Members** (children) are `users` records (role `child`); PB `username` = `{famSlug}:{handle}` (globally-unique auth identity; `handle` = whitespace-free lowercase name), URL segment = `handleOf(username)`, `name` = display name. Their PB password is **derived** server-side (`MEMBER_SECRET + famSlug + handle`); access is gated by a 20-min OTP in `otp`, then `authWithPassword`. They get the same httpOnly `pb_token` cookie. There is **no `members` collection**.
- **Platform superuser** (`_superusers`) used only server-side by `pb-admin.ts` for cross-family queries (e.g. `/admin` stats dashboard) and OTP/signup writes. Not an app role. - **Platform superuser** (`_superusers`) used only server-side by `pb-admin.ts` for cross-family queries (e.g. `/admin` stats dashboard) and OTP/signup writes. Not an app role.
- The layout (`[fam]/+layout.server.ts`) derives `isParent` and `role` centrally from the session — child pages use `page.data.isParent` or `page.data.role` from `$app/state`. - The layout (`[fam]/+layout.server.ts`) derives `isParent` and `role` centrally from the session — child pages use `page.data.isParent` or `page.data.role` from `$app/state`.
- Because `pb_token` is httpOnly, the browser PB SDK is seeded from `page.data.pbToken` via `initPb(token)` in the layout `onMount` (not `document.cookie`). - Because `pb_token` is httpOnly, the browser PB SDK is seeded from `page.data.pbToken` via `initPb(token)` in the layout `onMount` (not `document.cookie`).
@@ -33,7 +33,7 @@
## PB Collections (all scoped by `famId`; child/member = `users` row) ## PB Collections (all scoped by `famId`; child/member = `users` row)
- `users` — auth collection; famId, role (`parent`|`child`), username (`{famSlug}:{handle}`), name, color, email (admin only) - `users` — auth collection; famId, role (`parent`|`child`), username (`{famSlug}:{handle}`), name, color, email (admin only)
- `user_configs` — famId, userId, otp, colour, updatedAt (OTP gate for child join) - `otp` — famId, userId, otp, updatedAt (OTP gate for child join; display colour lives on `users.color`)
- `fams` — name, slug, stripeCustomerId, featureFlags - `fams` — name, slug, stripeCustomerId, featureFlags
- `chore_templates` — famId, name, defaultValue, defaultFrequency - `chore_templates` — famId, name, defaultValue, defaultFrequency
- `assigned_chores` — famId, userId, templateId, frequency, value - `assigned_chores` — famId, userId, templateId, frequency, value
@@ -197,7 +197,7 @@ All admin and member pages use the following pattern:
- Every collection query includes `famId = @request.auth.famId` filter - Every collection query includes `famId = @request.auth.famId` filter
- Super admin bypasses famId filter (access via PB admin API) - Super admin bypasses famId filter (access via PB admin API)
- Child PB passwords are derived (`MEMBER_SECRET + famSlug + username`); the child join gate is a transient OTP in `user_configs`. No device tokens. Never log raw tokens/secrets. - Child PB passwords are derived (`MEMBER_SECRET + famSlug + username`); the child join gate is a transient OTP in `otp`. No device tokens. Never log raw tokens/secrets.
- **Admin → Proxy**: `hono.admin.*` in `$lib/server/hono.ts` — uses `sessionHeaders(event)` (server-side only, requires `RequestEvent`) - **Admin → Proxy**: `hono.admin.*` in `$lib/server/hono.ts` — uses `sessionHeaders(event)` (server-side only, requires `RequestEvent`)
- **Member → Proxy (server)**: `memberApi.*` in `$lib/client/api.ts` — use inside `+page.server.ts` load/actions; `BASE_URL` resolves to Hono port on server - **Member → Proxy (server)**: `memberApi.*` in `$lib/client/api.ts` — use inside `+page.server.ts` load/actions; `BASE_URL` resolves to Hono port on server
- **Member → Proxy (browser)**: `memberApi.*` in `$lib/client/api.ts` — use inside `+page.svelte`; `BASE_URL` is empty, Vite proxies `/api/*` to Hono - **Member → Proxy (browser)**: `memberApi.*` in `$lib/client/api.ts` — use inside `+page.svelte`; `BASE_URL` is empty, Vite proxies `/api/*` to Hono
+1 -1
View File
@@ -3,7 +3,7 @@
## 2026-08-17 — Hono proxy removed: everything runs in SvelteKit services ## 2026-08-17 — Hono proxy removed: everything runs in SvelteKit services
- **Decision**: deleted the `proxy/` Hono service entirely. All business logic (admin CRUD, member kanban, weekly summary/EOW, bonus evaluation/trigger/progress, rewards claim/issue, chat, payday settlement, debug data-gen) now lives in `frontend/src/lib/server/services/`, grouped **by app area** (not by role): `fam.ts`, `chores.ts`, `completions.ts`, `rewards.ts`, `bonuses.ts`, `chat.ts`, `settings.ts`, `crud.ts`, `debug.ts`, plus a generic per-resource `crud.ts`. `createServices(pb, user)` returns a per-feature binder; `servicesFor(event)` is the shorthand for loads/form actions. **No role guard** — PB collection rules on the acting user's token are the security boundary (the `admin`/`member` split no longer exists as separate files). - **Decision**: deleted the `proxy/` Hono service entirely. All business logic (admin CRUD, member kanban, weekly summary/EOW, bonus evaluation/trigger/progress, rewards claim/issue, chat, payday settlement, debug data-gen) now lives in `frontend/src/lib/server/services/`, grouped **by app area** (not by role): `fam.ts`, `chores.ts`, `completions.ts`, `rewards.ts`, `bonuses.ts`, `chat.ts`, `settings.ts`, `crud.ts`, `debug.ts`, plus a generic per-resource `crud.ts`. `createServices(pb, user)` returns a per-feature binder; `servicesFor(event)` is the shorthand for loads/form actions. **No role guard** — PB collection rules on the acting user's token are the security boundary (the `admin`/`member` split no longer exists as separate files).
- **Wiring**: `hono.admin.*` (form actions/loads) and `memberApi.*`/chat are now direct service calls or SvelteKit `/api/*` routes (`completions/toggle`, `members/rewards/[id]/claim`, `members/me`, `fam/[famId]/payday`, `chat`, `admin/[famId]/assigned-chores`). Browser admin calls (chores grid) hit SvelteKit `/api/admin/*`. `routeAuth.actingClient(event)` resolves the acting user's PB client from the Bearer header or the `pb_token` cookie. - **Wiring**: `hono.admin.*` (form actions/loads) and `memberApi.*`/chat are now direct service calls or SvelteKit `/api/*` routes (`completions/toggle`, `members/rewards/[id]/claim`, `members`, `fam/[famId]/payday`, `chat`, `admin/[famId]/assigned-chores`). Browser admin calls (chores grid) hit SvelteKit `/api/admin/*`. The `/api/*` routes read auth straight from `event.locals` (`locals.user` + `createPbClient(locals.pbToken)`) — a separate `actingClient` helper was dropped as redundant since `hooks.server.ts` already resolves the session. No Authorization header; the httpOnly `pb_token` cookie is the auth for same-origin calls.
- **Migration relocated**: `proxy/src/migrate.ts` → `frontend/src/lib/server/migrate.ts` (env now via `$app/env/private` + `PB_ENDPOINT` from `pocketbase.ts`), run once per process by `migrate-boot.ts`, kicked off in `hooks.server.ts` (`void migrateOnBoot()`). Schema source of truth remains `shared/pb/schema.ts`. - **Migration relocated**: `proxy/src/migrate.ts` → `frontend/src/lib/server/migrate.ts` (env now via `$app/env/private` + `PB_ENDPOINT` from `pocketbase.ts`), run once per process by `migrate-boot.ts`, kicked off in `hooks.server.ts` (`void migrateOnBoot()`). Schema source of truth remains `shared/pb/schema.ts`.
- **Infra**: `pnpm-workspace.yaml` (only `frontend`), root `package.json` (`dev` = `pnpm --filter frontend dev`), `docker/Dockerfile` (no proxy build/deploy), `docker/entrypoint.sh` (no proxy start; app runs schema migration on boot), `docker/nginx.conf` (`/api/` block removed → falls through to `location /` → SvelteKit `:3000`; `/pb/api/` unchanged). Removed `PROXY_URL` env + `PROXY_PORT` from `shared/config.ts` and `frontend/src/env.ts`. Dead `memberApi.myChores`/`requestAll` removed. - **Infra**: `pnpm-workspace.yaml` (only `frontend`), root `package.json` (`dev` = `pnpm --filter frontend dev`), `docker/Dockerfile` (no proxy build/deploy), `docker/entrypoint.sh` (no proxy start; app runs schema migration on boot), `docker/nginx.conf` (`/api/` block removed → falls through to `location /` → SvelteKit `:3000`; `/pb/api/` unchanged). Removed `PROXY_URL` env + `PROXY_PORT` from `shared/config.ts` and `frontend/src/env.ts`. Dead `memberApi.myChores`/`requestAll` removed.
- **Typecheck**: frontend `svelte-check` = 12 pre-existing canary errors (`.svelte` implicit-any, qrcode decl, RewardType/Frequency casts, signup `string|undefined`); **zero errors in the migration's files**. `pnpm build` (adapter-node) succeeds. - **Typecheck**: frontend `svelte-check` = 12 pre-existing canary errors (`.svelte` implicit-any, qrcode decl, RewardType/Frequency casts, signup `string|undefined`); **zero errors in the migration's files**. `pnpm build` (adapter-node) succeeds.
+13
View File
@@ -0,0 +1,13 @@
Items:
- The hono workhorse - see notes later
## the hono workhorse
**Actively used (the workhorse):**
- **Admin reads/writes** via `hono.admin.*` — used heavily by the child kanban (`+page.server.ts` load: members, chore-templates, assigned-chores, weekly-summary, fam, rewards, bonus-configs, completions, settings), the **bonuses** page (17 calls), **ledger** (6), **preferences** (2), **fam dashboard** (4), plus settings `complete-week` and `debug/generate-data`.
- **Member actions** via `memberApi.*` + direct `/api` fetches — `toggleCompletion`, `claimReward`, `payday`, `/api/members/me`, `/api/members/my-chores` (both SSR and browser).
- **Direct client calls** — the chores page hits `/api/admin/:famId/assigned-chores` directly; the kanban hits `/api/members/me`.
We will tackle this as the next feature `feature/migrate-hono-to-kit`
+9 -12
View File
@@ -1,16 +1,13 @@
// Client-only. All /api calls go same-origin (SvelteKit in dev and prod). // Client-only. All /api calls go same-origin (SvelteKit in dev and prod);
// auth rides on the httpOnly `pb_token` cookie, so no token/header needed.
const BASE_URL = ''; const BASE_URL = '';
async function memberFetch<T = unknown>( async function memberFetch<T = unknown>(
method: string, method: string,
path: string, path: string,
token: string,
_famId?: string,
body?: unknown, body?: unknown,
): Promise<T> { ): Promise<T> {
const headers: Record<string, string> = { const headers: Record<string, string> = {};
Authorization: `Bearer ${token}`,
};
if (body !== undefined) headers['Content-Type'] = 'application/json'; if (body !== undefined) headers['Content-Type'] = 'application/json';
const res = await fetch(`${BASE_URL}${path}`, { const res = await fetch(`${BASE_URL}${path}`, {
method, method,
@@ -23,13 +20,13 @@ async function memberFetch<T = unknown>(
} }
export const memberApi = { export const memberApi = {
async toggleCompletion(token: string, famId: string, assignedChoreId: string, date: string) { async toggleCompletion(famId: string, assignedChoreId: string, date: string) {
return memberFetch('POST', '/api/completions/toggle', token, famId, { assignedChoreId, date }); return memberFetch('POST', '/api/completions/toggle', { assignedChoreId, date });
}, },
async claimReward(token: string, famId: string, rewardId: string) { async claimReward(famId: string, rewardId: string) {
return memberFetch('POST', `/api/members/rewards/${rewardId}/claim`, token, famId); return memberFetch('POST', `/api/members/rewards/${rewardId}/claim`);
}, },
async payday(token: string, famId: string) { async payday(famId: string) {
return memberFetch('POST', `/api/fam/${famId}/payday`, token, famId); return memberFetch('POST', `/api/fam/${famId}/payday`);
}, },
}; };
+7 -7
View File
@@ -59,7 +59,7 @@ export async function createChild(opts: {
} }
// Admin grants access to a child: creates the users auth record (or re-issues // Admin grants access to a child: creates the users auth record (or re-issues
// OTP if they already exist) + upserts their user_configs. Returns the OTP and // OTP if they already exist) + upserts their otp. Returns the OTP and
// shareable join link (using the whitespace-free handle) for QR display. // shareable join link (using the whitespace-free handle) for QR display.
export async function issueAccess(opts: { export async function issueAccess(opts: {
famId: string; famId: string;
@@ -67,23 +67,23 @@ export async function issueAccess(opts: {
name: string; name: string;
colour?: string; colour?: string;
}) { }) {
const { famId, famSlug, name, colour } = opts; const { famId, famSlug, name } = opts;
const username = handle(name); const username = handle(name);
const otp = generateOtp(); const otp = generateOtp();
const updatedAt = new Date().toISOString(); const updatedAt = new Date().toISOString();
const user = await createChild({ famId, famSlug, name, colour }); const user = await createChild({ famId, famSlug, name, colour: opts.colour });
const pb = await createSuperClient(); const pb = await createSuperClient();
let config = await pb let config = await pb
.collection('user_configs') .collection('otp')
.getFirstListItem(`famId='${famId}' && userId='${user.id}'`) .getFirstListItem(`famId='${famId}' && userId='${user.id}'`)
.catch(() => null); .catch(() => null);
if (config) { if (config) {
await pb.collection('user_configs').update(config.id, { otp, colour, updatedAt }); await pb.collection('otp').update(config.id, { otp, updatedAt });
} else { } else {
await pb.collection('user_configs').create({ famId, userId: user.id, otp, colour, updatedAt }); await pb.collection('otp').create({ famId, userId: user.id, otp, updatedAt });
} }
return { otp, joinUrl: `/${famSlug}/join/${encodeURIComponent(username)}` }; return { otp, joinUrl: `/${famSlug}/join/${encodeURIComponent(username)}` };
@@ -109,7 +109,7 @@ export async function redeemOtp(opts: { famSlug: string; username: string; otp:
if (!user || user.role !== 'child') throw new Error('Invalid join link'); if (!user || user.role !== 'child') throw new Error('Invalid join link');
let config = await pb let config = await pb
.collection('user_configs') .collection('otp')
.getFirstListItem(`famId='${fam.id}' && userId='${user.id}'`) .getFirstListItem(`famId='${fam.id}' && userId='${user.id}'`)
.catch(() => null); .catch(() => null);
if (!config || config.otp !== otp) throw new Error('Invalid code'); if (!config || config.otp !== otp) throw new Error('Invalid code');
+9 -10
View File
@@ -1752,21 +1752,21 @@ export async function migrate(): Promise<void> {
} }
} }
// ── 30. user_configs: identity + OTP store (superuser-only) ── // ── 30. otp: OTP store (superuser-only) ──
// Holds the rotating one-time code, colour, and the OTP-issue timestamp used // Holds the rotating one-time code and the OTP-issue timestamp used for the
// for the 20-minute window. Sensitive (OTPs) → not public; read/written via // 20-minute join window. Sensitive (OTPs) → not public; read/written via
// createSuperClient in SvelteKit. `created`/`updated` are PB built-ins; the // createSuperClient in SvelteKit. `created`/`updated` are PB built-ins; the
// manual `updatedAt` is written ONLY on OTP (re)issue so the window stays // manual `updatedAt` is written ONLY on OTP (re)issue so the window stays
// accurate (colour edits must not bump it). userId links to the users auth // accurate. userId links to the users auth record so each child's config is
// record so each child's config is uniquely addressable. // uniquely addressable. (Display colour lives on users.color, not here.)
{ {
if (!(await getCollection("user_configs"))) { if (!(await getCollection("otp"))) {
const famsCol = await getCollection("fams"); const famsCol = await getCollection("fams");
const usersCol = await getCollection("users"); const usersCol = await getCollection("users");
if (!famsCol || !usersCol) throw new Error("fams/users collection not found"); if (!famsCol || !usersCol) throw new Error("fams/users collection not found");
console.log("[migrate] Creating user_configs collection..."); console.log("[migrate] Creating otp collection...");
await createCollection({ await createCollection({
name: "user_configs", name: "otp",
type: "base", type: "base",
listRule: null, listRule: null,
viewRule: null, viewRule: null,
@@ -1791,12 +1791,11 @@ export async function migrate(): Promise<void> {
cascadeDelete: false, cascadeDelete: false,
}, },
{ name: "otp", type: "text", required: false }, { name: "otp", type: "text", required: false },
{ name: "colour", type: "text", required: false },
{ name: "updatedAt", type: "text", required: false }, { name: "updatedAt", type: "text", required: false },
], ],
}); });
} else { } else {
console.log(" ↳ user_configs already exists"); console.log(" ↳ otp already exists");
} }
} }
+1 -1
View File
@@ -26,7 +26,7 @@ export function pbUser(event: RequestEvent) {
// Superuser PB client (memoized). Reserved for server-only privileged // Superuser PB client (memoized). Reserved for server-only privileged
// operations that must bypass collection rules: creating child users, minting // operations that must bypass collection rules: creating child users, minting
// OTP-login tokens, and verifying OTPs against the superuser-only user_configs. // OTP-login tokens, and verifying OTPs against the superuser-only otp.
let superClient: PocketBase | null = null; let superClient: PocketBase | null = null;
export async function createSuperClient() { export async function createSuperClient() {
if (superClient) return superClient; if (superClient) return superClient;
-28
View File
@@ -1,28 +0,0 @@
import { error } from '@sveltejs/kit';
import { createPbClient } from '$lib/server/pocketbase';
import type { RequestEvent } from '@sveltejs/kit';
// Resolve the acting user's PB client from a request: prefer the Authorization
// Bearer token (sent by the browser member API), else the httpOnly session
// cookie. Identity comes from the verified session. Used by the in-app /api/*
// routes that replaced the Hono member endpoints.
export function actingClient(event: RequestEvent) {
const token =
event.request.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ||
event.locals.pbToken ||
'';
const u = event.locals.user;
if (!u || !token) throw error(401, 'Unauthorized');
return {
pb: createPbClient(token),
famId: u.famId,
userId: u.id,
role: u.role,
name: u.name || '',
color: u.color || '#6366f1'
};
}
export function err(e: unknown) {
return error(500, e instanceof Error ? e.message : 'Internal error');
}
@@ -212,7 +212,7 @@
if (secondsLeft > 0 || eowFired) return; if (secondsLeft > 0 || eowFired) return;
if (!pbToken || !famId) return; if (!pbToken || !famId) return;
eowFired = true; eowFired = true;
memberApi.payday(pbToken, famId).catch(() => {}); memberApi.payday(famId).catch(() => {});
}); });
function paydayWeekStart(): string { function paydayWeekStart(): string {
@@ -546,7 +546,7 @@
} }
try { try {
await memberApi.toggleCompletion(pbToken, famId, chore.id, todayChild); await memberApi.toggleCompletion(famId, chore.id, todayChild);
const optimistic = completions.find((c) => c.id === 'optimistic-' + chore.id); const optimistic = completions.find((c) => c.id === 'optimistic-' + chore.id);
if (optimistic) { if (optimistic) {
famStore.applyRecord('completions', optimistic, 'delete'); famStore.applyRecord('completions', optimistic, 'delete');
@@ -960,12 +960,9 @@
const old = memberName; const old = memberName;
memberName = nameInput; memberName = nameInput;
try { try {
const res = await fetch('/api/members/me', { const res = await fetch('/api/members', {
method: 'PATCH', method: 'PATCH',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
Authorization: `Bearer ${pbToken}`
},
body: JSON.stringify({ name: nameInput }) body: JSON.stringify({ name: nameInput })
}); });
if (!res.ok) memberName = old; if (!res.ok) memberName = old;
@@ -1022,12 +1019,9 @@
memberColor = color; memberColor = color;
showColorPicker = false; showColorPicker = false;
try { try {
const res = await fetch('/api/members/me', { const res = await fetch('/api/members', {
method: 'PATCH', method: 'PATCH',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
Authorization: `Bearer ${pbToken}`
},
body: JSON.stringify({ color }) body: JSON.stringify({ color })
}); });
if (!res.ok) memberColor = old; if (!res.ok) memberColor = old;
@@ -1253,7 +1247,7 @@
class="wr-cta" class="wr-cta"
onclick={async () => { onclick={async () => {
try { try {
await memberApi.claimReward(pbToken, famId, r.id); await memberApi.claimReward(famId, r.id);
} catch (e) { } catch (e) {
claimError = e instanceof Error ? e.message : 'Claim failed'; claimError = e instanceof Error ? e.message : 'Claim failed';
} }
@@ -1,17 +1,19 @@
import { json } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { actingClient } from '$lib/server/routeAuth'; import { createPbClient } from '$lib/server/pocketbase';
import { createServices } from '$lib/server/services'; import { createServices } from '$lib/server/services';
export async function POST(event: RequestEvent) { export async function POST(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event); const u = event.locals.user;
if (famId !== event.params.famId) { if (!u || !event.locals.pbToken) throw error(401, 'Unauthorized');
const pb = createPbClient(event.locals.pbToken);
if (u.famId !== event.params.famId) {
return json({ error: 'famId mismatch' }, { status: 403 }); return json({ error: 'famId mismatch' }, { status: 403 });
} }
const body = await event.request.json().catch(() => ({})); const body = await event.request.json().catch(() => ({}));
try { try {
const s = createServices(pb, { id: userId, role }); const s = createServices(pb, { id: u.id, role: u.role });
const record = await s.crud.create('assigned-chores', famId, body); const record = await s.crud.create('assigned-chores', u.famId, body);
return json(record); return json(record);
} catch (e) { } catch (e) {
return json({ error: e instanceof Error ? e.message : 'create failed' }, { status: 400 }); return json({ error: e instanceof Error ? e.message : 'create failed' }, { status: 400 });
@@ -1,17 +1,19 @@
import { json } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { actingClient } from '$lib/server/routeAuth'; import { createPbClient } from '$lib/server/pocketbase';
import { createServices } from '$lib/server/services'; import { createServices } from '$lib/server/services';
export async function DELETE(event: RequestEvent) { export async function DELETE(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event); const u = event.locals.user;
if (famId !== event.params.famId) { if (!u || !event.locals.pbToken) throw error(401, 'Unauthorized');
const pb = createPbClient(event.locals.pbToken);
if (u.famId !== event.params.famId) {
return json({ error: 'famId mismatch' }, { status: 403 }); return json({ error: 'famId mismatch' }, { status: 403 });
} }
const id = event.params.id!; const id = event.params.id!;
try { try {
const s = createServices(pb, { id: userId, role }); const s = createServices(pb, { id: u.id, role: u.role });
await s.crud.remove('assigned-chores', famId, id); await s.crud.remove('assigned-chores', u.famId, id);
return json({ ok: true }); return json({ ok: true });
} catch (e) { } catch (e) {
return json({ error: e instanceof Error ? e.message : 'delete failed' }, { status: 400 }); return json({ error: e instanceof Error ? e.message : 'delete failed' }, { status: 400 });
+11 -9
View File
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import { actingClient } from '$lib/server/routeAuth'; import { createPbClient } from '$lib/server/pocketbase';
import { createServices, type ChatActor } from '$lib/server/services'; import { createServices, type ChatActor } from '$lib/server/services';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
@@ -15,18 +15,20 @@ export async function POST(event: RequestEvent) {
const body = (await event.request.json().catch(() => null)) as Body | null; const body = (await event.request.json().catch(() => null)) as Body | null;
if (!body || !body.action) return json({ error: 'missing action' }, { status: 400 }); if (!body || !body.action) return json({ error: 'missing action' }, { status: 400 });
const { pb, famId: sessionFamId, userId, role, name, color } = actingClient(event); const u = event.locals.user;
if (!u || !event.locals.pbToken) throw error(401, 'Unauthorized');
const pb = createPbClient(event.locals.pbToken);
const actor: ChatActor = { const actor: ChatActor = {
id: userId, id: u.id,
type: role === 'parent' ? 'admin' : 'member', type: u.role === 'parent' ? 'admin' : 'member',
name, name: u.name,
color color: u.color || '#6366f1'
}; };
const famId = body.famId || sessionFamId || ''; const famId = body.famId || u.famId || '';
if (!famId) return json({ error: 'famId required' }, { status: 400 }); if (!famId) return json({ error: 'famId required' }, { status: 400 });
try { try {
const s = createServices(pb, { id: userId, role }); const s = createServices(pb, { id: u.id, role: u.role });
const data = const data =
body.action === 'typing' body.action === 'typing'
? await s.chat.typing(famId, actor, { typing: Boolean(body.typing) }) ? await s.chat.typing(famId, actor, { typing: Boolean(body.typing) })
@@ -1,14 +1,16 @@
import { json } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { actingClient } from '$lib/server/routeAuth'; import { createPbClient } from '$lib/server/pocketbase';
import { createServices } from '$lib/server/services'; import { createServices } from '$lib/server/services';
export async function POST(event: RequestEvent) { export async function POST(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event); const u = event.locals.user;
if (!u || !event.locals.pbToken) throw error(401, 'Unauthorized');
const pb = createPbClient(event.locals.pbToken);
const body = await event.request.json().catch(() => ({})); const body = await event.request.json().catch(() => ({}));
try { try {
const s = createServices(pb, { id: userId, role }); const s = createServices(pb, { id: u.id, role: u.role });
return json(await s.completions.toggle(famId, body)); return json(await s.completions.toggle(u.famId, body));
} catch (e) { } catch (e) {
return json({ error: e instanceof Error ? e.message : 'toggle failed' }, { status: 400 }); return json({ error: e instanceof Error ? e.message : 'toggle failed' }, { status: 400 });
} }
@@ -1,13 +1,15 @@
import { json } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { actingClient } from '$lib/server/routeAuth'; import { createPbClient } from '$lib/server/pocketbase';
import { createServices } from '$lib/server/services'; import { createServices } from '$lib/server/services';
export async function POST(event: RequestEvent) { export async function POST(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event); const u = event.locals.user;
if (!u || !event.locals.pbToken) throw error(401, 'Unauthorized');
const pb = createPbClient(event.locals.pbToken);
try { try {
const s = createServices(pb, { id: userId, role }); const s = createServices(pb, { id: u.id, role: u.role });
return json(await s.fam.payday(famId)); return json(await s.fam.payday(u.famId));
} catch (e) { } catch (e) {
return json({ error: e instanceof Error ? e.message : 'payday failed' }, { status: 400 }); return json({ error: e instanceof Error ? e.message : 'payday failed' }, { status: 400 });
} }
@@ -0,0 +1,17 @@
import { json, error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import { createPbClient } from '$lib/server/pocketbase';
import { createServices } from '$lib/server/services';
export async function PATCH(event: RequestEvent) {
const u = event.locals.user;
if (!u || !event.locals.pbToken) throw error(401, 'Unauthorized');
const pb = createPbClient(event.locals.pbToken);
const body = await event.request.json().catch(() => ({}));
try {
const s = createServices(pb, { id: u.id, role: u.role });
return json(await s.fam.updateProfile(u.famId, body));
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'update failed' }, { status: 400 });
}
}
@@ -1,15 +0,0 @@
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import { actingClient } from '$lib/server/routeAuth';
import { createServices } from '$lib/server/services';
export async function PATCH(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event);
const body = await event.request.json().catch(() => ({}));
try {
const s = createServices(pb, { id: userId, role });
return json(await s.fam.updateProfile(famId, body));
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'update failed' }, { status: 400 });
}
}
@@ -1,14 +1,16 @@
import { json } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { actingClient } from '$lib/server/routeAuth'; import { createPbClient } from '$lib/server/pocketbase';
import { createServices } from '$lib/server/services'; import { createServices } from '$lib/server/services';
export async function POST(event: RequestEvent) { export async function POST(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event); const u = event.locals.user;
if (!u || !event.locals.pbToken) throw error(401, 'Unauthorized');
const pb = createPbClient(event.locals.pbToken);
const id = event.params.id!; const id = event.params.id!;
try { try {
const s = createServices(pb, { id: userId, role }); const s = createServices(pb, { id: u.id, role: u.role });
return json(await s.rewards.claim(famId, id)); return json(await s.rewards.claim(u.famId, id));
} catch (e) { } catch (e) {
return json({ error: e instanceof Error ? e.message : 'claim failed' }, { status: 400 }); return json({ error: e instanceof Error ? e.message : 'claim failed' }, { status: 400 });
} }