optimize post migration
This commit is contained in:
@@ -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`);
|
||||
},
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (!pbToken || !famId) return;
|
||||
eowFired = true;
|
||||
memberApi.payday(pbToken, famId).catch(() => {});
|
||||
memberApi.payday(famId).catch(() => {});
|
||||
});
|
||||
|
||||
function paydayWeekStart(): string {
|
||||
@@ -546,7 +546,7 @@
|
||||
}
|
||||
|
||||
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);
|
||||
if (optimistic) {
|
||||
famStore.applyRecord('completions', optimistic, 'delete');
|
||||
@@ -960,14 +960,11 @@
|
||||
const old = memberName;
|
||||
memberName = nameInput;
|
||||
try {
|
||||
const res = await fetch('/api/members/me', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${pbToken}`
|
||||
},
|
||||
body: JSON.stringify({ name: nameInput })
|
||||
});
|
||||
const res = await fetch('/api/members', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: nameInput })
|
||||
});
|
||||
if (!res.ok) memberName = old;
|
||||
} catch {
|
||||
memberName = old;
|
||||
@@ -1022,14 +1019,11 @@
|
||||
memberColor = color;
|
||||
showColorPicker = false;
|
||||
try {
|
||||
const res = await fetch('/api/members/me', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${pbToken}`
|
||||
},
|
||||
body: JSON.stringify({ color })
|
||||
});
|
||||
const res = await fetch('/api/members', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ color })
|
||||
});
|
||||
if (!res.ok) memberColor = old;
|
||||
} catch {
|
||||
memberColor = old;
|
||||
@@ -1253,7 +1247,7 @@
|
||||
class="wr-cta"
|
||||
onclick={async () => {
|
||||
try {
|
||||
await memberApi.claimReward(pbToken, famId, r.id);
|
||||
await memberApi.claimReward(famId, r.id);
|
||||
} catch (e) {
|
||||
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 { actingClient } from '$lib/server/routeAuth';
|
||||
import { createPbClient } from '$lib/server/pocketbase';
|
||||
import { createServices } from '$lib/server/services';
|
||||
|
||||
export async function POST(event: RequestEvent) {
|
||||
const { pb, famId, userId, role } = actingClient(event);
|
||||
if (famId !== event.params.famId) {
|
||||
const u = event.locals.user;
|
||||
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 });
|
||||
}
|
||||
const body = await event.request.json().catch(() => ({}));
|
||||
try {
|
||||
const s = createServices(pb, { id: userId, role });
|
||||
const record = await s.crud.create('assigned-chores', famId, body);
|
||||
const s = createServices(pb, { id: u.id, role: u.role });
|
||||
const record = await s.crud.create('assigned-chores', u.famId, body);
|
||||
return json(record);
|
||||
} catch (e) {
|
||||
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 { actingClient } from '$lib/server/routeAuth';
|
||||
import { createPbClient } from '$lib/server/pocketbase';
|
||||
import { createServices } from '$lib/server/services';
|
||||
|
||||
export async function DELETE(event: RequestEvent) {
|
||||
const { pb, famId, userId, role } = actingClient(event);
|
||||
if (famId !== event.params.famId) {
|
||||
const u = event.locals.user;
|
||||
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 });
|
||||
}
|
||||
const id = event.params.id!;
|
||||
try {
|
||||
const s = createServices(pb, { id: userId, role });
|
||||
await s.crud.remove('assigned-chores', famId, id);
|
||||
const s = createServices(pb, { id: u.id, role: u.role });
|
||||
await s.crud.remove('assigned-chores', u.famId, id);
|
||||
return json({ ok: true });
|
||||
} catch (e) {
|
||||
return json({ error: e instanceof Error ? e.message : 'delete failed' }, { status: 400 });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { actingClient } from '$lib/server/routeAuth';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { createPbClient } from '$lib/server/pocketbase';
|
||||
import { createServices, type ChatActor } from '$lib/server/services';
|
||||
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;
|
||||
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 = {
|
||||
id: userId,
|
||||
type: role === 'parent' ? 'admin' : 'member',
|
||||
name,
|
||||
color
|
||||
id: u.id,
|
||||
type: u.role === 'parent' ? 'admin' : 'member',
|
||||
name: u.name,
|
||||
color: u.color || '#6366f1'
|
||||
};
|
||||
const famId = body.famId || sessionFamId || '';
|
||||
const famId = body.famId || u.famId || '';
|
||||
if (!famId) return json({ error: 'famId required' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const s = createServices(pb, { id: userId, role });
|
||||
const s = createServices(pb, { id: u.id, role: u.role });
|
||||
const data =
|
||||
body.action === '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 { actingClient } from '$lib/server/routeAuth';
|
||||
import { createPbClient } from '$lib/server/pocketbase';
|
||||
import { createServices } from '$lib/server/services';
|
||||
|
||||
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(() => ({}));
|
||||
try {
|
||||
const s = createServices(pb, { id: userId, role });
|
||||
return json(await s.completions.toggle(famId, body));
|
||||
const s = createServices(pb, { id: u.id, role: u.role });
|
||||
return json(await s.completions.toggle(u.famId, body));
|
||||
} catch (e) {
|
||||
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 { actingClient } from '$lib/server/routeAuth';
|
||||
import { createPbClient } from '$lib/server/pocketbase';
|
||||
import { createServices } from '$lib/server/services';
|
||||
|
||||
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 {
|
||||
const s = createServices(pb, { id: userId, role });
|
||||
return json(await s.fam.payday(famId));
|
||||
const s = createServices(pb, { id: u.id, role: u.role });
|
||||
return json(await s.fam.payday(u.famId));
|
||||
} catch (e) {
|
||||
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 { actingClient } from '$lib/server/routeAuth';
|
||||
import { createPbClient } from '$lib/server/pocketbase';
|
||||
import { createServices } from '$lib/server/services';
|
||||
|
||||
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!;
|
||||
try {
|
||||
const s = createServices(pb, { id: userId, role });
|
||||
return json(await s.rewards.claim(famId, id));
|
||||
const s = createServices(pb, { id: u.id, role: u.role });
|
||||
return json(await s.rewards.claim(u.famId, id));
|
||||
} catch (e) {
|
||||
return json({ error: e instanceof Error ? e.message : 'claim failed' }, { status: 400 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user