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
+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 = '';
async function memberFetch<T = unknown>(
method: string,
path: string,
token: string,
_famId?: string,
body?: unknown,
): Promise<T> {
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
};
const headers: Record<string, string> = {};
if (body !== undefined) headers['Content-Type'] = 'application/json';
const res = await fetch(`${BASE_URL}${path}`, {
method,
@@ -23,13 +20,13 @@ async function memberFetch<T = unknown>(
}
export const memberApi = {
async toggleCompletion(token: string, famId: string, assignedChoreId: string, date: string) {
return memberFetch('POST', '/api/completions/toggle', token, famId, { assignedChoreId, date });
async toggleCompletion(famId: string, assignedChoreId: string, date: string) {
return memberFetch('POST', '/api/completions/toggle', { assignedChoreId, date });
},
async claimReward(token: string, famId: string, rewardId: string) {
return memberFetch('POST', `/api/members/rewards/${rewardId}/claim`, token, famId);
async claimReward(famId: string, rewardId: string) {
return memberFetch('POST', `/api/members/rewards/${rewardId}/claim`);
},
async payday(token: string, famId: string) {
return memberFetch('POST', `/api/fam/${famId}/payday`, token, famId);
async payday(famId: string) {
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
// 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.
export async function issueAccess(opts: {
famId: string;
@@ -67,23 +67,23 @@ export async function issueAccess(opts: {
name: string;
colour?: string;
}) {
const { famId, famSlug, name, colour } = opts;
const { famId, famSlug, name } = opts;
const username = handle(name);
const otp = generateOtp();
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();
let config = await pb
.collection('user_configs')
.collection('otp')
.getFirstListItem(`famId='${famId}' && userId='${user.id}'`)
.catch(() => null);
if (config) {
await pb.collection('user_configs').update(config.id, { otp, colour, updatedAt });
await pb.collection('otp').update(config.id, { otp, updatedAt });
} 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)}` };
@@ -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');
let config = await pb
.collection('user_configs')
.collection('otp')
.getFirstListItem(`famId='${fam.id}' && userId='${user.id}'`)
.catch(() => null);
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) ──
// Holds the rotating one-time code, colour, and the OTP-issue timestamp used
// for the 20-minute window. Sensitive (OTPs) → not public; read/written via
// ── 30. otp: OTP store (superuser-only) ──
// Holds the rotating one-time code and the OTP-issue timestamp used for the
// 20-minute join window. Sensitive (OTPs) → not public; read/written via
// createSuperClient in SvelteKit. `created`/`updated` are PB built-ins; the
// 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
// record so each child's config is uniquely addressable.
// accurate. userId links to the users auth record so each child's config is
// 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 usersCol = await getCollection("users");
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({
name: "user_configs",
name: "otp",
type: "base",
listRule: null,
viewRule: null,
@@ -1791,12 +1791,11 @@ export async function migrate(): Promise<void> {
cascadeDelete: false,
},
{ name: "otp", type: "text", required: false },
{ name: "colour", type: "text", required: false },
{ name: "updatedAt", type: "text", required: false },
],
});
} 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
// 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;
export async function createSuperClient() {
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');
}