update ui and other frontend updates
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
import { SERVER_IP, PB_PORT, PB_EMAIL, PB_PASSWORD } from "../../config.ts";
|
||||
|
||||
const PB_ENDPOINT = `http://${SERVER_IP}:${PB_PORT}`;
|
||||
|
||||
interface FieldDef {
|
||||
name: string
|
||||
type: string
|
||||
required?: boolean
|
||||
unique?: boolean
|
||||
max?: number
|
||||
min?: number
|
||||
values?: string[]
|
||||
maxSelect?: number
|
||||
collectionId?: string
|
||||
cascadeDelete?: boolean
|
||||
}
|
||||
|
||||
interface CollectionDef {
|
||||
name: string
|
||||
type: string
|
||||
fields: FieldDef[]
|
||||
listRule?: string | null
|
||||
viewRule?: string | null
|
||||
createRule?: string | null
|
||||
updateRule?: string | null
|
||||
deleteRule?: string | null
|
||||
}
|
||||
|
||||
async function getSuperadminToken(): Promise<string> {
|
||||
const res = await fetch(
|
||||
`${PB_ENDPOINT}/api/collections/_superusers/auth-with-password`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: PB_EMAIL, password: PB_PASSWORD }),
|
||||
},
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(`Auth failed: ${JSON.stringify(data)}`);
|
||||
return data.token;
|
||||
}
|
||||
|
||||
async function createCollection(
|
||||
token: string,
|
||||
col: CollectionDef,
|
||||
): Promise<string | null> {
|
||||
const existing = await fetch(
|
||||
`${PB_ENDPOINT}/api/collections?filter=name='${col.name}'`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } },
|
||||
);
|
||||
const existingData = await existing.json();
|
||||
if (existingData?.items?.length > 0) {
|
||||
console.log(` ↳ Already exists: ${col.name}`);
|
||||
return existingData.items[0].id;
|
||||
}
|
||||
|
||||
const res = await fetch(`${PB_ENDPOINT}/api/collections`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(col),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(`Create ${col.name} failed: ${JSON.stringify(data)}`);
|
||||
console.log(` ✓ Created: ${col.name}`);
|
||||
return data.id;
|
||||
}
|
||||
|
||||
function text(name: string, required = false): FieldDef {
|
||||
return { name, type: "text", required };
|
||||
}
|
||||
|
||||
function uniqueText(name: string): FieldDef {
|
||||
return { name, type: "text", required: true, unique: true };
|
||||
}
|
||||
|
||||
function number(name: string, required = false): FieldDef {
|
||||
return { name, type: "number", required };
|
||||
}
|
||||
|
||||
function bool(name: string): FieldDef {
|
||||
return { name, type: "bool" };
|
||||
}
|
||||
|
||||
function date(name: string): FieldDef {
|
||||
return { name, type: "date" };
|
||||
}
|
||||
|
||||
function jsonField(name: string): FieldDef {
|
||||
return { name, type: "json" };
|
||||
}
|
||||
|
||||
function select(name: string, values: string[], required = false): FieldDef {
|
||||
return { name, type: "select", required, values, maxSelect: 1 };
|
||||
}
|
||||
|
||||
function rel(name: string, collectionId: string, required = false): FieldDef {
|
||||
return {
|
||||
name,
|
||||
type: "relation",
|
||||
required,
|
||||
collectionId,
|
||||
maxSelect: 1,
|
||||
cascadeDelete: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Connecting to PB at", PB_ENDPOINT);
|
||||
const token = await getSuperadminToken();
|
||||
console.log("Authenticated as superadmin\n");
|
||||
|
||||
const ids: Record<string, string> = {};
|
||||
|
||||
// ── Pass 1: Independent collections ──
|
||||
console.log("--- Pass 1: Independent collections ---");
|
||||
|
||||
ids.fams = await createCollection(token, {
|
||||
name: "fams",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
text("name", true),
|
||||
uniqueText("slug"),
|
||||
text("inviteCode"),
|
||||
text("stripeCustomerId"),
|
||||
jsonField("featureFlags"),
|
||||
],
|
||||
});
|
||||
|
||||
// ── Pass 2: Collections that reference fams ──
|
||||
console.log("\n--- Pass 2: fam-scoped collections ---");
|
||||
|
||||
ids.settings = await createCollection(token, {
|
||||
name: "settings",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("webhookUrl"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.members = await createCollection(token, {
|
||||
name: "members",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("name", true),
|
||||
text("color"),
|
||||
text("deviceToken"),
|
||||
text("deviceTokenHint"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.chore_templates = await createCollection(token, {
|
||||
name: "chore_templates",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("name", true),
|
||||
text("description"),
|
||||
select("defaultFrequency", ["daily", "weekly"], true),
|
||||
select("defaultType", ["points", "money"], true),
|
||||
number("defaultValue", true),
|
||||
],
|
||||
});
|
||||
|
||||
ids.fam_admins = await createCollection(token, {
|
||||
name: "fam_admins",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("userId", true),
|
||||
text("email", true),
|
||||
text("name"),
|
||||
text("color"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.bonus_configs = await createCollection(token, {
|
||||
name: "bonus_configs",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("name", true),
|
||||
text("description"),
|
||||
select("target", ["individual", "competitive"], true),
|
||||
select("type", ["threshold", "count", "manual"], true),
|
||||
select("occurrence", ["recurring", "once"], true),
|
||||
select("rewardType", ["points", "cash", "prize"], true),
|
||||
text("rewardValue", true),
|
||||
number("criteriaValue"),
|
||||
select("period", ["weekly", "monthly"]),
|
||||
select("status", ["active", "archived"], true),
|
||||
],
|
||||
});
|
||||
|
||||
// ── Pass 3: Collections with member/template deps ──
|
||||
console.log("\n--- Pass 3: Nested collections ---");
|
||||
|
||||
ids.weekly_history = await createCollection(token, {
|
||||
name: "weekly_history",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
rel("memberId", ids.members!, true),
|
||||
date("weekStart"),
|
||||
number("pointsEarned"),
|
||||
number("moneyEarned"),
|
||||
number("choresCompleted"),
|
||||
number("bonusEarned"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.rewards = await createCollection(token, {
|
||||
name: "rewards",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
rel("memberId", ids.members!, true),
|
||||
rel("bonusConfigId", ids.bonus_configs!),
|
||||
text("label", true),
|
||||
number("value", true),
|
||||
select("rewardType", ["cash", "prize", "points"], true),
|
||||
bool("claimed"),
|
||||
date("claimedAt"),
|
||||
text("date"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.assigned_chores = await createCollection(token, {
|
||||
name: "assigned_chores",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
rel("memberId", ids.members!, true),
|
||||
rel("templateId", ids.chore_templates!, true),
|
||||
select("frequency", ["daily", "weekly"], true),
|
||||
select("type", ["points", "money"], true),
|
||||
number("value", true),
|
||||
text("customName"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.completions = await createCollection(token, {
|
||||
name: "completions",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
rel("memberId", ids.members!, true),
|
||||
rel("assignedChoreId", ids.assigned_chores!, true),
|
||||
date("date"),
|
||||
],
|
||||
});
|
||||
|
||||
console.log("\n✅ All collections created successfully");
|
||||
console.log("Collection IDs:", ids);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Seed failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
const HONO = "http://192.168.1.225:3456";
|
||||
|
||||
async function main() {
|
||||
// Signup
|
||||
const signup = await fetch(`${HONO}/api/admin/signup`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: "admin2@test.com", password: "password1234", famName: "Admin Test" }),
|
||||
});
|
||||
const s = await signup.json();
|
||||
console.log("Signup:", s.famId, s.famSlug);
|
||||
const famId = s.famId;
|
||||
|
||||
// Test admin authenticated calls
|
||||
const headers = {
|
||||
"x-session-famid": famId,
|
||||
"x-session-role": "admin",
|
||||
"x-session-userid": s.userId,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// Create a chore template
|
||||
console.log("\nCreate template...");
|
||||
const tmpl = await fetch(`${HONO}/api/admin/${famId}/chore-templates`, {
|
||||
method: "POST", headers, body: JSON.stringify({
|
||||
name: "Make Bed", defaultFrequency: "daily", defaultType: "points", defaultValue: 10,
|
||||
}),
|
||||
});
|
||||
const t = await tmpl.json();
|
||||
console.log("Template:", t.id, t.name);
|
||||
|
||||
// List templates
|
||||
console.log("\nList templates...");
|
||||
const list = await fetch(`${HONO}/api/admin/${famId}/chore-templates`, { headers });
|
||||
console.log("Templates:", (await list.json()).length);
|
||||
|
||||
// Create a member
|
||||
console.log("\nCreate member...");
|
||||
const mem = await fetch(`${HONO}/api/admin/${famId}/members`, {
|
||||
method: "POST", headers, body: JSON.stringify({ name: "Kid", color: "#6366f1" }),
|
||||
});
|
||||
const m = await mem.json();
|
||||
console.log("Member:", m.id, m.name);
|
||||
|
||||
// Assign chore
|
||||
console.log("\nAssign chore...");
|
||||
const assign = await fetch(`${HONO}/api/admin/${famId}/assigned-chores`, {
|
||||
method: "POST", headers, body: JSON.stringify({
|
||||
memberId: m.id, templateId: t.id, frequency: "daily", type: "points", value: 10,
|
||||
}),
|
||||
});
|
||||
const a = await assign.json();
|
||||
console.log("Assigned:", a.id);
|
||||
|
||||
// Test device token auth
|
||||
console.log("\nTest completion toggle with device token...");
|
||||
const devHeaders = {
|
||||
"x-device-token": "dev-token-test",
|
||||
"x-device-famid": famId,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
// First need to join with this device token
|
||||
const join = await fetch(`${HONO}/api/members/join`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ inviteCode: s.famSlug, ...(await (await fetch(`${HONO}/api/admin/${famId}/members`, { headers })).json()).length > 1 ? {} : { name: "Test", deviceToken: "dev-token-test" } }),
|
||||
});
|
||||
// Actually, just use the member we already created but we can't use device token with it since it has no token
|
||||
// Let's join a new one
|
||||
console.log("Joining with device token...");
|
||||
const j = await fetch(`${HONO}/api/members/join`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ inviteCode: "TEST", name: "Test Kid", deviceToken: "dev-token-test" }),
|
||||
});
|
||||
const joinData = await j.json();
|
||||
console.log("Join result:", JSON.stringify(joinData));
|
||||
|
||||
if (joinData.famId) {
|
||||
// Toggle completion
|
||||
console.log("\nToggle completion...");
|
||||
const toggle = await fetch(`${HONO}/api/completions/toggle`, {
|
||||
method: "POST",
|
||||
headers: { "x-device-token": "dev-token-test", "x-device-famid": famId, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ assignedChoreId: a.id, date: "2026-06-24" }),
|
||||
});
|
||||
console.log("Toggle:", await toggle.json());
|
||||
|
||||
// Toggle again (should undo)
|
||||
const toggle2 = await fetch(`${HONO}/api/completions/toggle`, {
|
||||
method: "POST",
|
||||
headers: { "x-device-token": "dev-token-test", "x-device-famid": famId, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ assignedChoreId: a.id, date: "2026-06-24" }),
|
||||
});
|
||||
console.log("Toggle undo:", await toggle2.json());
|
||||
}
|
||||
|
||||
console.log("\n✅ All admin tests passed");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,63 @@
|
||||
const HONO = "http://192.168.1.225:3456";
|
||||
|
||||
async function main() {
|
||||
// 1. Signup
|
||||
console.log("=== Signup ===");
|
||||
const signup = await fetch(`${HONO}/api/admin/signup`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: "test@fam.com", password: "password123", famName: "Test Fam" }),
|
||||
});
|
||||
const signupData = await signup.json();
|
||||
console.log("Signup:", JSON.stringify(signupData, null, 2));
|
||||
|
||||
// 2. Login
|
||||
console.log("\n=== Login ===");
|
||||
const login = await fetch(`${HONO}/api/admin/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: "test@fam.com", password: "password123" }),
|
||||
});
|
||||
const loginData = await login.json();
|
||||
console.log("Login:", JSON.stringify(loginData, null, 2));
|
||||
|
||||
// 3. Get invite code from fams directly via PB
|
||||
const pbTokenRes = await fetch("http://192.168.1.225:8090/api/collections/_superusers/auth-with-password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: "debug@famchamp.dev", password: "debug123" }),
|
||||
});
|
||||
const pbTokenData = await pbTokenRes.json();
|
||||
const pbSuperToken = pbTokenData.token;
|
||||
|
||||
const famsRes = await fetch("http://192.168.1.225:8090/api/collections/fams/records?sort=-created", {
|
||||
headers: { Authorization: `Bearer ${pbSuperToken}` },
|
||||
});
|
||||
const famsData = await famsRes.json();
|
||||
const fam = famsData.items[0];
|
||||
console.log("\n=== Fam ===");
|
||||
console.log("Fam:", JSON.stringify(fam, null, 2));
|
||||
console.log("Invite code:", fam.inviteCode);
|
||||
|
||||
// 4. Join as member
|
||||
console.log("\n=== Join ===");
|
||||
const join = await fetch(`${HONO}/api/members/join`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ inviteCode: fam.inviteCode, name: "Kid", deviceToken: "dev-token-xyz" }),
|
||||
});
|
||||
const joinData = await join.json();
|
||||
console.log("Join:", JSON.stringify(joinData, null, 2));
|
||||
|
||||
// 5. Verify member
|
||||
console.log("\n=== Verify ===");
|
||||
const verify = await fetch(`${HONO}/api/members/verify`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ famId: fam.id, deviceToken: "dev-token-xyz" }),
|
||||
});
|
||||
const verifyData = await verify.json();
|
||||
console.log("Verify:", JSON.stringify(verifyData, null, 2));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user