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
at atb-52-css-token-extraction 141 lines 4.2 kB view raw
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}