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