migrate from hono

This commit is contained in:
JCEEE
2026-08-17 07:41:26 +01:00
parent e2b99c45b6
commit 5204e7bdbc
56 changed files with 1815 additions and 3357 deletions
+35 -44
View File
@@ -1,42 +1,40 @@
import { PROXY_URL } from '$app/env/public';
import { pbAdmin } from '$lib/server/pocketbase';
import { pbAdmin, createPbClient } from '$lib/server/pocketbase';
import { createServices, type ChatActor } from '$lib/server/services';
const HONO_URL = PROXY_URL;
async function paydayCheck(famId: string, headers: Record<string, string>) {
async function paydayCheck(famId: string, pbToken: string) {
try {
const res = await fetch(`${HONO_URL}/api/fam/${famId}/payday`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
}
});
// Best-effort: never block render on the payday heartbeat.
await res.json().catch(() => null);
const s = createServices(createPbClient(pbToken));
await s.fam.payday(famId);
} catch {}
}
async function resolveChatIdentity(
api: 'admin' | 'member',
opts: {
session?: { famId: string; id: string };
pbToken?: string;
}
) {
function actorFrom(session: {
famId: string;
id: string;
role: string;
name?: string;
color?: string;
}): ChatActor {
return {
id: session.id,
type: session.role === 'parent' ? 'admin' : 'member',
name: session.name || '',
color: session.color || '#6366f1'
};
}
async function resolveChatIdentity(session: {
famId: string;
id: string;
role: string;
name?: string;
color?: string;
}, pbToken: string) {
try {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (api === 'admin' && opts.session) {
headers['x-session-famid'] = opts.session.famId;
headers['x-session-userid'] = opts.session.id;
} else if (api === 'member' && opts.pbToken) {
headers['Authorization'] = `Bearer ${opts.pbToken}`;
} else {
return null;
}
const res = await fetch(`${HONO_URL}/api/chat/me`, { headers });
if (!res.ok) return null;
return await res.json();
const s = createServices(createPbClient(pbToken));
const actor = actorFrom(session);
return await s.chat.me(session.famId, actor);
} catch {
return null;
}
@@ -49,19 +47,12 @@ export async function load(event) {
const pbToken = event.cookies.get('pb_token') || '';
let famId = '';
let chat: { famId: string; actor: any } | null = null;
let chat: { famId: string; actor: ChatActor } | null = null;
if (session && isParent) {
if (session && pbToken) {
famId = session.famId;
await paydayCheck(famId, {
'x-session-famid': session.famId,
'x-session-userid': session.id
});
chat = await resolveChatIdentity('admin', { session });
} else if (role === 'child' && pbToken && session) {
famId = session.famId;
await paydayCheck(famId, { Authorization: `Bearer ${pbToken}` });
chat = await resolveChatIdentity('member', { pbToken });
await paydayCheck(famId, pbToken);
chat = await resolveChatIdentity(session, pbToken);
}
return {
@@ -85,4 +76,4 @@ export async function load(event) {
? await pbAdmin.getOne('fams', famId).catch(() => null)
: null
};
}
}
+8 -6
View File
@@ -1,20 +1,22 @@
import { hono } from '$lib/server/hono';
import { pbUser } from '$lib/server/pocketbase';
import { createServices } from '$lib/server/services';
export async function load(event) {
const session = event.locals.user;
if (!session) return {};
const famId = session.famId;
const s = createServices(pbUser(event), session);
try {
const [members, templates, assigned, summary] = await Promise.all([
hono.admin.list(event, 'members', famId),
hono.admin.list(event, 'chore-templates', famId),
hono.admin.list(event, 'assigned-chores', famId),
hono.admin.weeklySummary(event, famId),
s.crud.list('members', famId),
s.crud.list('chore-templates', famId),
s.crud.list('assigned-chores', famId),
s.fam.weeklySummary(famId),
]);
return { members, templates, assigned, summary };
} catch {
return {};
}
}
}
@@ -1,8 +1,6 @@
import { fail, redirect } from '@sveltejs/kit';
import { hono } from '$lib/server/hono';
import { PROXY_URL } from '$app/env/public';
const HONO_URL = PROXY_URL;
import { pbUser, createPbClient } from '$lib/server/pocketbase';
import { createServices } from '$lib/server/services';
export async function load(event) {
const session = event.locals.user;
@@ -17,6 +15,7 @@ export async function load(event) {
if (session.name && session.username && session.username !== username) {
throw redirect(303, `/${famSlug}/${session.username}`);
}
const s = createServices(pbUser(event), session);
const [
members,
templates,
@@ -28,15 +27,15 @@ export async function load(event) {
completions,
settings
] = await Promise.all([
hono.admin.list(event, 'members', famId),
hono.admin.list(event, 'chore-templates', famId),
hono.admin.list(event, 'assigned-chores', famId),
hono.admin.weeklySummary(event, famId),
hono.admin.fam(event, famId),
hono.admin.rewards(event, famId),
hono.admin.bonusConfigs(event, famId),
hono.admin.completions(event, famId),
hono.admin.settings(event, famId).catch(() => ({}))
s.crud.list('members', famId),
s.crud.list('chore-templates', famId),
s.crud.list('assigned-chores', famId),
s.fam.weeklySummary(famId),
s.fam.get(famId),
s.crud.list('rewards', famId),
s.crud.list('bonus-configs', famId),
s.crud.list('completions', famId),
s.settings.get(famId).catch(() => ({ simulateEow: false, webhookUrl: '' }))
]);
return {
role: 'parent',
@@ -88,12 +87,8 @@ export async function load(event) {
if (!pbToken || !famId) return empty;
try {
const choresRes = await fetch(`${HONO_URL}/api/members/my-chores`, {
method: 'POST',
headers: { Authorization: `Bearer ${pbToken}` }
});
if (!choresRes.ok) return empty;
const chores = await choresRes.json();
const s = createServices(createPbClient(pbToken), session ?? undefined);
const chores = await s.chores.myChores(famId);
return {
role: 'child',
token: pbToken,
@@ -107,7 +102,7 @@ export async function load(event) {
completions: chores.completions || [],
rewards: chores.rewards || [],
bonusConfigs: chores.bonusConfigs || [],
tallies: chores.tallies || {},
tallies: {} as Record<string, unknown>,
payday: chores.payday,
paydayTime: chores.paydayTime || '18:00',
timezone: chores.timezone || 'auto',
@@ -125,7 +120,8 @@ export const actions = {
const fd = await event.request.formData();
const on = fd.get('on') === 'true';
try {
const result = await hono.admin.updateSettings(event, famId, { simulateEow: on });
const s = createServices(pbUser(event), event.locals.user);
const result = await s.settings.update(famId, { simulateEow: on });
return { simulateEow: result.simulateEow };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to update settings' };
@@ -136,8 +132,9 @@ export const actions = {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
try {
const preview = await hono.admin.eowPreview(event, famId);
await hono.admin.updateSettings(event, famId, { simulateEow: true });
const s = createServices(pbUser(event), event.locals.user);
const preview = await s.fam.eowPreview(famId);
await s.settings.update(famId, { simulateEow: true });
return { preview, simulateEow: true };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to preview payday' };
@@ -150,7 +147,8 @@ export const actions = {
const fd = await event.request.formData();
const rewardId = fd.get('id') as string;
try {
const record = await hono.admin.claimReward(event, famId, rewardId);
const s = createServices(pbUser(event), event.locals.user);
const record = await s.rewards.approve(famId, rewardId);
return { record };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to claim reward' };
@@ -163,7 +161,8 @@ export const actions = {
const fd = await event.request.formData();
const memberId = fd.get('memberId') as string;
try {
const result = await hono.admin.issueAllRewards(event, famId, memberId);
const s = createServices(pbUser(event), event.locals.user);
const result = await s.rewards.issueAll(famId, memberId);
return { count: result.count };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to issue rewards' };
@@ -176,7 +175,8 @@ export const actions = {
const fd = await event.request.formData();
const completionId = fd.get('id') as string;
try {
await hono.admin.revokeCompletion(event, famId, completionId);
const s = createServices(pbUser(event), event.locals.user);
await s.completions.revoke(famId, completionId);
return { revoked: true, id: completionId };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to revoke' };
@@ -191,15 +191,11 @@ export const actions = {
const memberId = fd.get('memberId') as string;
if (!configId) return { error: 'Config ID required' };
try {
const result = await hono.admin.triggerBonusConfig(
event,
famId,
configId,
memberId || undefined
);
const s = createServices(pbUser(event), event.locals.user);
const result = await s.bonuses.trigger(famId, configId, memberId || undefined);
return { records: result.records || [] };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to trigger' };
}
}
};
};
@@ -1,15 +1,16 @@
import { fail, redirect } from '@sveltejs/kit';
import { hono } from '$lib/server/hono';
import { servicesFor } from '$lib/server/servicesFor';
export async function load(event) {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
const s = servicesFor(event);
const [configs, templates, members, progress, rewards] = await Promise.all([
hono.admin.bonusConfigs(event, famId),
hono.admin.list(event, 'bonus-templates', famId),
hono.admin.list(event, 'members', famId),
hono.admin.bonusConfigProgress(event, famId),
hono.admin.rewards(event, famId)
s.crud.list('bonus-configs', famId),
s.crud.list('bonus-templates', famId),
s.crud.list('members', famId),
s.bonuses.progress(famId),
s.crud.list('rewards', famId)
]);
return { configs, templates, members, progress, rewards };
}
@@ -34,7 +35,8 @@ export const actions = {
if (period !== null) data.period = period;
if (data.occurrence === 'once') data.period = '';
try {
const record = await hono.admin.create(event, 'bonus-templates', famId, data);
const s = servicesFor(event);
const record = await s.crud.create('bonus-templates', famId, data);
return { record };
} catch (e) {
return fail(400, { error: e instanceof Error ? e.message : 'Failed to create template' });
@@ -67,7 +69,8 @@ export const actions = {
const description = fd.get('description');
if (description) data.description = description;
try {
const record = await hono.admin.update(event, 'bonus-templates', famId, id, data);
const s = servicesFor(event);
const record = await s.crud.update('bonus-templates', famId, id, data);
return { record };
} catch (e) {
return fail(400, { error: e instanceof Error ? e.message : 'Failed to update template' });
@@ -80,7 +83,8 @@ export const actions = {
const fd = await event.request.formData();
const id = fd.get('id') as string;
try {
await hono.admin.remove(event, 'bonus-templates', famId, id);
const s = servicesFor(event);
await s.crud.remove('bonus-templates', famId, id);
return { deleted: true };
} catch (e) {
return fail(400, { error: e instanceof Error ? e.message : 'Failed to delete template' });
@@ -112,7 +116,8 @@ export const actions = {
const memberId = fd.get('memberId');
if (memberId) data.memberId = memberId;
try {
const record = await hono.admin.create(event, 'bonus-configs', famId, data);
const s = servicesFor(event);
const record = await s.crud.create('bonus-configs', famId, data);
return { record };
} catch (e) {
return fail(400, { error: e instanceof Error ? e.message : 'Failed to create config' });
@@ -147,7 +152,8 @@ export const actions = {
const memberId = fd.get('memberId');
if (memberId !== null) data.memberId = memberId || null;
try {
const record = await hono.admin.update(event, 'bonus-configs', famId, id, data);
const s = servicesFor(event);
const record = await s.crud.update('bonus-configs', famId, id, data);
return { record };
} catch (e) {
return fail(400, { error: e instanceof Error ? e.message : 'Failed to update config' });
@@ -160,7 +166,8 @@ export const actions = {
const fd = await event.request.formData();
const id = fd.get('id') as string;
try {
await hono.admin.remove(event, 'bonus-configs', famId, id);
const s = servicesFor(event);
await s.crud.remove('bonus-configs', famId, id);
return { deleted: true };
} catch (e) {
return fail(400, { error: e instanceof Error ? e.message : 'Failed to delete config' });
@@ -188,7 +195,8 @@ export const actions = {
};
if (data.occurrence === 'once') data.period = '';
try {
const record = await hono.admin.create(event, 'bonus-configs', famId, data);
const s = servicesFor(event);
const record = await s.crud.create('bonus-configs', famId, data);
return { record };
} catch (e) {
return fail(400, {
@@ -205,7 +213,8 @@ export const actions = {
const target = fd.get('target') as string;
const memberId = fd.get('memberId') as string;
try {
const record = await hono.admin.assignBonusConfig(event, famId, id, {
const s = servicesFor(event);
const record = await s.bonuses.assign(famId, id, {
target,
memberId: memberId || null
});
@@ -221,7 +230,8 @@ export const actions = {
const fd = await event.request.formData();
const id = fd.get('id') as string;
try {
const record = await hono.admin.completeBonusConfig(event, famId, id);
const s = servicesFor(event);
const record = await s.bonuses.complete(famId, id);
return { record };
} catch (e) {
return fail(400, { error: e instanceof Error ? e.message : 'Failed to complete bonus' });
@@ -234,7 +244,8 @@ export const actions = {
const fd = await event.request.formData();
const id = fd.get('id') as string;
try {
await hono.admin.destroyBonusConfig(event, famId, id);
const s = servicesFor(event);
await s.bonuses.destroy(famId, id);
return { destroyed: true };
} catch (e) {
return fail(400, { error: e instanceof Error ? e.message : 'Failed to delete bonus' });
@@ -249,7 +260,8 @@ export const actions = {
const currentStatus = fd.get('currentStatus') as string;
const newStatus = currentStatus === 'disabled' ? 'active' : 'disabled';
try {
const record = await hono.admin.update(event, 'bonus-configs', famId, id, {
const s = servicesFor(event);
const record = await s.crud.update('bonus-configs', famId, id, {
status: newStatus
});
return { record };
@@ -262,9 +274,10 @@ export const actions = {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
try {
await hono.admin.evaluateBonusConfig(event, famId);
const s = servicesFor(event);
await s.bonuses.evaluate(famId);
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to evaluate' };
}
}
};
};
@@ -1,12 +1,13 @@
import { hono } from '$lib/server/hono';
import { json } from '@sveltejs/kit';
import { servicesFor } from '$lib/server/servicesFor';
export async function GET(event) {
const famId = event.params.fam;
try {
const progress = await hono.admin.bonusConfigProgress(event, famId);
const s = servicesFor(event);
const progress = await s.bonuses.progress(famId);
return json(progress);
} catch {
return json([]);
}
}
}
@@ -1,15 +1,16 @@
import { redirect } from '@sveltejs/kit';
import { hono } from '$lib/server/hono';
import { servicesFor } from '$lib/server/servicesFor';
export async function load(event) {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
const s = servicesFor(event);
const [rewards, members, assigned, templates, completions] = await Promise.all([
hono.admin.rewards(event, famId),
hono.admin.list(event, 'members', famId),
hono.admin.list(event, 'assigned-chores', famId),
hono.admin.list(event, 'chore-templates', famId),
hono.admin.completions(event, famId)
s.crud.list('rewards', famId),
s.crud.list('members', famId),
s.crud.list('assigned-chores', famId),
s.crud.list('chore-templates', famId),
s.crud.list('completions', famId)
]);
return { rewards, members, assigned, templates, completions };
}
@@ -21,10 +22,11 @@ export const actions = {
const fd = await event.request.formData();
const rewardId = fd.get('id') as string;
try {
const record = await hono.admin.claimReward(event, famId, rewardId);
const s = servicesFor(event);
const record = await s.rewards.approve(famId, rewardId);
return { record };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to claim reward' };
}
}
};
};
@@ -1,22 +1,25 @@
import { redirect } from '@sveltejs/kit';
import { hono } from '$lib/server/hono';
import { PROXY_URL } from '$app/env/public';
const HONO_URL = PROXY_URL;
import { servicesFor } from '$lib/server/servicesFor';
export async function load(event) {
const session = event.locals.user;
const famSlug = event.params.fam;
const username = event.params.username;
// Parent (session auth) — profile lives on the users record
if (session) {
const famId = session.famId;
if (session.username && session.username !== username) {
throw redirect(303, `/${famSlug}/${session.username}/preferences`);
}
if (!session) {
return { verified: false, token: '', memberId: '', famId: '', memberName: '', memberColor: '', email: '' };
}
const famId = session.famId;
if (session.username && session.username !== username) {
throw redirect(303, `/${famSlug}/${session.username}/preferences`);
}
const s = servicesFor(event);
if (session.role === 'parent') {
try {
const me = await hono.admin.getProfile(event, famId);
const me = await s.fam.getProfile(famId);
return {
verified: true, token: '', memberId: me.id, famId,
memberName: me.name, memberColor: me.color, email: me.email || '',
@@ -27,73 +30,49 @@ export async function load(event) {
}
}
// Child (device token) — profile lives in members
const deviceToken = event.cookies.get('device_token') || event.url.searchParams.get('token') || '';
if (!deviceToken) {
return { verified: false, token: '', memberId: '', famId: '', memberName: '', memberColor: '', email: '' };
}
try {
const res = await fetch(`${HONO_URL}/api/members/verify-token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ deviceToken, famSlug }),
});
const data = await res.json();
if (!res.ok || data.name !== username) {
return { verified: false, token: deviceToken, memberId: '', famId: '', memberName: '', memberColor: '', email: '' };
}
return {
verified: true, token: deviceToken, memberId: data.memberId, famId: data.famId,
memberName: data.name, memberColor: data.color, email: data.email || '', session: false,
};
} catch {
return { verified: false, token: '', memberId: '', famId: '', memberName: '', memberColor: '', email: '' };
}
// Child — profile lives on the users record, read from the session.
return {
verified: true, token: event.cookies.get('pb_token') || '', memberId: session.id, famId,
memberName: session.name || '', memberColor: session.color || '', email: '',
session: true,
};
}
export const actions = {
update: async (event) => {
const session = event.locals.user;
if (!session) return { error: 'Not authenticated' };
const fd = await event.request.formData();
const name = fd.get('name') as string;
const color = fd.get('color') as string;
const email = fd.get('email') as string;
if (session) {
const famId = session.famId;
const s = servicesFor(event);
const famId = session.famId;
if (session.role === 'parent') {
try {
const data: Record<string, string> = {};
if (name) data.name = name;
if (color) data.color = color;
data.email = email || '';
const me = await hono.admin.updateProfile(event, famId, data);
const me = await s.fam.updateProfile(famId, data);
return { success: true, name: me.name, color: me.color, email: me.email };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Update failed' };
}
}
const deviceToken = event.cookies.get('device_token') || event.url.searchParams.get('token') || '';
const famSlug = event.params.fam;
if (!deviceToken) return { error: 'Not authenticated' };
// Child — update own users record via their PB token.
try {
const verifyRes = await fetch(`${HONO_URL}/api/members/verify-token`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ deviceToken, famSlug }),
});
const verify = await verifyRes.json();
if (!verifyRes.ok) return { error: 'Verification failed' };
const res = await fetch(`${HONO_URL}/api/members/me`, {
method: 'PATCH', headers: { 'x-device-token': deviceToken, 'x-device-famid': verify.famId, 'Content-Type': 'application/json' },
body: JSON.stringify({ name, color }),
});
const data = await res.json();
if (!res.ok) return { error: data.error || 'Update failed' };
return { success: true, name: data.name, color: data.color };
const data: Record<string, string> = {};
if (name) data.name = name;
if (color) data.color = color;
const me = await s.fam.updateProfile(famId, data);
return { success: true, name: me.name, color: me.color, email: '' };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Update failed' };
}
},
};
};
@@ -2,7 +2,7 @@ import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import { pbUser } from '$lib/server/pocketbase';
import { pbAdmin } from '$lib/server/pocketbase';
import { hono } from '$lib/server/hono';
import { servicesFor } from '$lib/server/servicesFor';
import { issueAccess, createChild } from '$lib/server/member-otp';
import { slugify } from '@shared/slugify';
@@ -124,11 +124,12 @@ export const actions = {
return { deletedChoreIds: deletedIds };
},
// Compute endpoints — still proxied to Hono.
// Compute endpoints — in-process.
completeWeek: async (event: RequestEvent) => {
const famId = famIdOf(event);
try {
const result = await hono.admin.request(event, 'POST', `/api/admin/${famId}/complete-week`);
const s = servicesFor(event);
const result = await s.fam.completeWeek(famId);
return { success: true, result };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to complete week' };
@@ -140,7 +141,8 @@ export const actions = {
const fd = await event.request.formData();
const days = parseInt((fd.get('days') as string) || '7', 10);
try {
const result = await hono.admin.request(event, 'POST', `/api/admin/${famId}/debug/generate-data`, { days });
const s = servicesFor(event);
const result = await s.debug.generateData(famId, days);
return { success: true, result };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to generate data' };
@@ -0,0 +1,19 @@
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 POST(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event);
if (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);
return json(record);
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'create failed' }, { status: 400 });
}
}
@@ -0,0 +1,19 @@
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 DELETE(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event);
if (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);
return json({ ok: true });
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'delete failed' }, { status: 400 });
}
}
+41
View File
@@ -0,0 +1,41 @@
import { json } from '@sveltejs/kit';
import { actingClient } from '$lib/server/routeAuth';
import { createServices, type ChatActor } from '$lib/server/services';
import type { RequestEvent } from '@sveltejs/kit';
type Body = {
action: 'send' | 'typing';
famId?: string;
content?: string;
clientId?: string;
typing?: boolean;
};
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 actor: ChatActor = {
id: userId,
type: role === 'parent' ? 'admin' : 'member',
name,
color
};
const famId = body.famId || sessionFamId || '';
if (!famId) return json({ error: 'famId required' }, { status: 400 });
try {
const s = createServices(pb, { id: userId, role });
const data =
body.action === 'typing'
? await s.chat.typing(famId, actor, { typing: Boolean(body.typing) })
: await s.chat.send(famId, actor, {
content: body.content || '',
clientId: body.clientId || ''
});
return json(data);
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'chat request failed' }, { status: 400 });
}
}
@@ -0,0 +1,15 @@
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 POST(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.completions.toggle(famId, body));
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'toggle failed' }, { status: 400 });
}
}
@@ -0,0 +1,14 @@
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 POST(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event);
try {
const s = createServices(pb, { id: userId, role });
return json(await s.fam.payday(famId));
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'payday failed' }, { status: 400 });
}
}
@@ -0,0 +1,15 @@
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 });
}
}
@@ -0,0 +1,15 @@
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 POST(event: RequestEvent) {
const { pb, famId, userId, role } = actingClient(event);
const id = event.params.id!;
try {
const s = createServices(pb, { id: userId, role });
return json(await s.rewards.claim(famId, id));
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'claim failed' }, { status: 400 });
}
}
-58
View File
@@ -1,58 +0,0 @@
import { json } from '@sveltejs/kit';
import { PROXY_URL } from '$app/env/public';
import type { RequestEvent } from '@sveltejs/kit';
const HONO_URL = PROXY_URL;
type Body = {
action: 'send' | 'typing';
famId?: string;
content?: string;
clientId?: string;
typing?: boolean;
};
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' }, 400);
const session = event.locals.user;
const pbToken = event.cookies.get('pb_token') || '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
let famId = body.famId || '';
if (session?.role === 'parent' && session?.famId && session?.id) {
// Admin (parent) — trust the verified session server-side.
headers['x-session-famid'] = session.famId;
headers['x-session-userid'] = session.id;
famId = session.famId;
} else if (session?.role === 'child' && pbToken && session?.famId) {
// Member (child) — forward the pb_token; the proxy re-validates.
headers['Authorization'] = `Bearer ${pbToken}`;
famId = session.famId;
} else {
return json({ error: 'Unauthorized' }, 401);
}
if (!famId) return json({ error: 'famId required' }, 400);
const path = body.action === 'typing' ? `/api/chat/${famId}/typing` : `/api/chat/${famId}/messages`;
const payload =
body.action === 'typing'
? { typing: Boolean(body.typing) }
: { content: body.content || '', clientId: body.clientId || '' };
try {
const res = await fetch(`${HONO_URL}${path}`, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) return json({ error: data.error || 'chat request failed' }, res.status);
return json(data);
} catch {
return json({ error: 'chat request failed' }, 502);
}
}