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

feat(appview): POST /api/admin/boards create endpoint (ATB-45)

+86
+86
apps/appview/src/routes/admin.ts
··· 699 699 } 700 700 ); 701 701 702 + /** 703 + * POST /api/admin/boards 704 + * 705 + * Create a new forum board within a category. Fetches the category's CID from DB 706 + * to build the categoryRef strongRef required by the lexicon. Writes 707 + * space.atbb.forum.board to the Forum DID's PDS via putRecord. 708 + * The firehose indexer creates the DB row asynchronously. 709 + */ 710 + app.post( 711 + "/boards", 712 + requireAuth(ctx), 713 + requirePermission(ctx, "space.atbb.permission.manageCategories"), 714 + async (c) => { 715 + const { body, error: parseError } = await safeParseJsonBody(c); 716 + if (parseError) return parseError; 717 + 718 + const { name, description, sortOrder, categoryUri } = body; 719 + 720 + if (typeof name !== "string" || name.trim().length === 0) { 721 + return c.json({ error: "name is required and must be a non-empty string" }, 400); 722 + } 723 + 724 + if (typeof categoryUri !== "string" || !categoryUri.startsWith("at://")) { 725 + return c.json({ error: "categoryUri is required and must be a valid AT URI" }, 400); 726 + } 727 + 728 + // Derive rkey from the categoryUri to look up the category in the DB 729 + const categoryRkey = categoryUri.split("/").pop(); 730 + 731 + let category: typeof categories.$inferSelect; 732 + try { 733 + const [row] = await ctx.db 734 + .select() 735 + .from(categories) 736 + .where( 737 + and( 738 + eq(categories.did, ctx.config.forumDid), 739 + eq(categories.rkey, categoryRkey ?? "") 740 + ) 741 + ) 742 + .limit(1); 743 + 744 + if (!row) { 745 + return c.json({ error: "Category not found" }, 404); 746 + } 747 + category = row; 748 + } catch (error) { 749 + return handleRouteError(c, error, "Failed to look up category", { 750 + operation: "POST /api/admin/boards", 751 + logger: ctx.logger, 752 + categoryUri, 753 + }); 754 + } 755 + 756 + const { agent, error: agentError } = getForumAgentOrError(ctx, c, "POST /api/admin/boards"); 757 + if (agentError) return agentError; 758 + 759 + const rkey = TID.nextStr(); 760 + const now = new Date().toISOString(); 761 + 762 + try { 763 + const result = await agent.com.atproto.repo.putRecord({ 764 + repo: ctx.config.forumDid, 765 + collection: "space.atbb.forum.board", 766 + rkey, 767 + record: { 768 + $type: "space.atbb.forum.board", 769 + name: name.trim(), 770 + ...(typeof description === "string" && { description: description.trim() }), 771 + ...(Number.isInteger(sortOrder) && sortOrder >= 0 && { sortOrder }), 772 + category: { category: { uri: categoryUri, cid: category.cid } }, 773 + createdAt: now, 774 + }, 775 + }); 776 + 777 + return c.json({ uri: result.data.uri, cid: result.data.cid }, 201); 778 + } catch (error) { 779 + return handleRouteError(c, error, "Failed to create board", { 780 + operation: "POST /api/admin/boards", 781 + logger: ctx.logger, 782 + categoryUri, 783 + }); 784 + } 785 + } 786 + ); 787 + 702 788 return app; 703 789 }