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