WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1import type { Database } from "@atbb/db";
2import { users, memberships } from "@atbb/db";
3import { eq, and } from "drizzle-orm";
4import type { SeededRole } from "./seed-roles.js";
5
6interface AssignOwnerResult {
7 assigned: boolean;
8 skipped: boolean;
9 roleUri?: string;
10}
11
12/**
13 * Assign the Owner role to a user via direct DB insert.
14 *
15 * The CLI cannot write to the user's PDS (no user credentials), so
16 * this inserts the membership directly into the database as a bootstrap
17 * shortcut. When the user logs in via OAuth, the normal membership
18 * flow will create the PDS record on their repo.
19 *
20 * Idempotent: skips if the user already has a membership with the
21 * Owner role URI.
22 */
23export async function assignOwnerRole(
24 db: Database,
25 forumDid: string,
26 ownerDid: string,
27 ownerHandle: string | undefined,
28 seededRoles: SeededRole[]
29): Promise<AssignOwnerResult> {
30 // Find the Owner role from seeded roles
31 const ownerRole = seededRoles.find((r) => r.name === "Owner");
32
33 if (!ownerRole) {
34 throw new Error(
35 "Owner role not found in seeded roles. Run role seeding first."
36 );
37 }
38
39 const forumUri = `at://${forumDid}/space.atbb.forum.forum/self`;
40
41 // Check if user already has a membership with this role
42 const [existingMembership] = await db
43 .select()
44 .from(memberships)
45 .where(and(eq(memberships.did, ownerDid), eq(memberships.roleUri, ownerRole.uri)))
46 .limit(1);
47
48 if (existingMembership) {
49 return { assigned: false, skipped: true, roleUri: ownerRole.uri };
50 }
51
52 // Ensure user exists in the users table (FK constraint)
53 await db
54 .insert(users)
55 .values({
56 did: ownerDid,
57 handle: ownerHandle ?? null,
58 indexedAt: new Date(),
59 })
60 .onConflictDoNothing();
61
62 // Insert membership directly into DB.
63 // rkey and cid use "bootstrap" sentinel — there is no PDS record backing
64 // this membership yet. When the user first logs in via OAuth,
65 // createMembershipForUser detects cid==="bootstrap" and upgrades the row
66 // by writing a real PDS record and updating rkey/cid.
67 const now = new Date();
68 await db.insert(memberships).values({
69 did: ownerDid,
70 rkey: "bootstrap",
71 cid: "bootstrap",
72 forumUri,
73 roleUri: ownerRole.uri,
74 role: "Owner",
75 joinedAt: now,
76 createdAt: now,
77 indexedAt: now,
78 });
79
80 return { assigned: true, skipped: false, roleUri: ownerRole.uri };
81}