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 { AppContext } from "./app-context.js";
2import { roles } from "@atbb/db";
3import { and, eq } from "drizzle-orm";
4
5interface DefaultRole {
6 name: string;
7 description: string;
8 permissions: string[];
9 priority: number;
10 critical: boolean;
11}
12
13const DEFAULT_ROLES: DefaultRole[] = [
14 {
15 name: "Owner",
16 description: "Forum owner with full control",
17 permissions: ["*"],
18 priority: 0,
19 critical: true,
20 },
21 {
22 name: "Admin",
23 description: "Can manage forum structure and users",
24 permissions: [
25 "space.atbb.permission.manageCategories",
26 "space.atbb.permission.manageRoles",
27 "space.atbb.permission.manageMembers",
28 "space.atbb.permission.moderatePosts",
29 "space.atbb.permission.banUsers",
30 "space.atbb.permission.pinTopics",
31 "space.atbb.permission.lockTopics",
32 "space.atbb.permission.createTopics",
33 "space.atbb.permission.createPosts",
34 ],
35 priority: 10,
36 critical: true,
37 },
38 {
39 name: "Moderator",
40 description: "Can moderate content and users",
41 permissions: [
42 "space.atbb.permission.moderatePosts",
43 "space.atbb.permission.banUsers",
44 "space.atbb.permission.pinTopics",
45 "space.atbb.permission.lockTopics",
46 "space.atbb.permission.createTopics",
47 "space.atbb.permission.createPosts",
48 ],
49 priority: 20,
50 critical: true,
51 },
52 {
53 name: "Member",
54 description: "Regular forum member",
55 permissions: [
56 "space.atbb.permission.createTopics",
57 "space.atbb.permission.createPosts",
58 ],
59 priority: 30,
60 critical: true,
61 },
62];
63
64/**
65 * Seed default roles to Forum DID's PDS.
66 *
67 * Idempotent: Checks for existing roles by name before creating.
68 * Safe to run on every startup.
69 *
70 * @throws Error if ForumAgent is unavailable or if any critical role fails to seed
71 */
72export async function seedDefaultRoles(ctx: AppContext): Promise<{ created: number; skipped: number }> {
73 // Check ForumAgent availability
74 if (!ctx.forumAgent) {
75 throw new Error("ForumAgent not available - permission system cannot function without roles. Check FORUM_HANDLE and FORUM_PASSWORD environment variables.");
76 }
77
78 const agent = ctx.forumAgent.getAgent();
79 if (!agent) {
80 throw new Error("ForumAgent not authenticated - permission system cannot function without roles. Check FORUM_HANDLE and FORUM_PASSWORD are valid.");
81 }
82
83 let created = 0;
84 let skipped = 0;
85
86 for (const defaultRole of DEFAULT_ROLES) {
87 try {
88 // Check if role already exists by name
89 const [existingRole] = await ctx.db
90 .select()
91 .from(roles)
92 .where(and(eq(roles.did, ctx.config.forumDid), eq(roles.name, defaultRole.name)))
93 .limit(1);
94
95 if (existingRole) {
96 ctx.logger.info(`Role "${defaultRole.name}" already exists, skipping`, {
97 operation: "seedDefaultRoles",
98 roleName: defaultRole.name,
99 });
100 skipped++;
101 continue;
102 }
103
104 // Create role record on Forum DID's PDS
105 const response = await agent.com.atproto.repo.createRecord({
106 repo: ctx.config.forumDid,
107 collection: "space.atbb.forum.role",
108 record: {
109 $type: "space.atbb.forum.role",
110 name: defaultRole.name,
111 description: defaultRole.description,
112 permissions: defaultRole.permissions,
113 priority: defaultRole.priority,
114 createdAt: new Date().toISOString(),
115 },
116 });
117
118 ctx.logger.info(`Created default role "${defaultRole.name}"`, {
119 operation: "seedDefaultRoles",
120 roleName: defaultRole.name,
121 uri: response.data.uri,
122 cid: response.data.cid,
123 });
124
125 created++;
126 } catch (error) {
127 ctx.logger.error(`Failed to seed role "${defaultRole.name}"`, {
128 operation: "seedDefaultRoles",
129 roleName: defaultRole.name,
130 error: error instanceof Error ? error.message : String(error),
131 });
132 if (defaultRole.critical) {
133 throw new Error(
134 `Failed to seed critical role "${defaultRole.name}": ${error instanceof Error ? error.message : String(error)}`
135 );
136 }
137 }
138 }
139
140 return { created, skipped };
141}