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
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(web): add category proxy routes for structure management (ATB-47)

Malpercio 6acea50b f768317e

+202
+202
apps/web/src/routes/admin.tsx
··· 798 798 ); 799 799 }); 800 800 801 + // ── POST /admin/structure/categories ───────────────────────────────────── 802 + 803 + app.post("/admin/structure/categories", async (c) => { 804 + const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie")); 805 + if (!auth.authenticated) return c.redirect("/login"); 806 + if (!canManageCategories(auth)) { 807 + return c.html( 808 + <BaseLayout title="Access Denied — atBB Forum" auth={auth}> 809 + <PageHeader title="Forum Structure" /> 810 + <p>You don&apos;t have permission to manage forum structure.</p> 811 + </BaseLayout>, 812 + 403 813 + ); 814 + } 815 + 816 + const cookie = c.req.header("cookie") ?? ""; 817 + 818 + let body: Record<string, string | File>; 819 + try { 820 + body = await c.req.parseBody(); 821 + } catch (error) { 822 + if (isProgrammingError(error)) throw error; 823 + return c.redirect( 824 + `/admin/structure?error=${encodeURIComponent("Invalid form submission.")}`, 825 + 302 826 + ); 827 + } 828 + 829 + const name = typeof body.name === "string" ? body.name.trim() : ""; 830 + if (!name) { 831 + return c.redirect( 832 + `/admin/structure?error=${encodeURIComponent("Category name is required.")}`, 833 + 302 834 + ); 835 + } 836 + 837 + const description = typeof body.description === "string" ? body.description.trim() || null : null; 838 + const sortOrder = parseSortOrder(body.sortOrder); 839 + 840 + let appviewRes: Response; 841 + try { 842 + appviewRes = await fetch(`${appviewUrl}/api/admin/categories`, { 843 + method: "POST", 844 + headers: { "Content-Type": "application/json", Cookie: cookie }, 845 + body: JSON.stringify({ name, description, sortOrder }), 846 + }); 847 + } catch (error) { 848 + if (isProgrammingError(error)) throw error; 849 + logger.error("Network error creating category", { 850 + operation: "POST /admin/structure/categories", 851 + error: error instanceof Error ? error.message : String(error), 852 + }); 853 + return c.redirect( 854 + `/admin/structure?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`, 855 + 302 856 + ); 857 + } 858 + 859 + if (!appviewRes.ok) { 860 + const msg = await extractAppviewError(appviewRes, "Failed to create category. Please try again."); 861 + logger.error("AppView error creating category", { 862 + operation: "POST /admin/structure/categories", 863 + status: appviewRes.status, 864 + }); 865 + return c.redirect( 866 + `/admin/structure?error=${encodeURIComponent(msg)}`, 867 + 302 868 + ); 869 + } 870 + 871 + return c.redirect("/admin/structure", 302); 872 + }); 873 + 874 + // ── POST /admin/structure/categories/:id/edit ───────────────────────────── 875 + 876 + app.post("/admin/structure/categories/:id/edit", async (c) => { 877 + const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie")); 878 + if (!auth.authenticated) return c.redirect("/login"); 879 + if (!canManageCategories(auth)) { 880 + return c.html( 881 + <BaseLayout title="Access Denied — atBB Forum" auth={auth}> 882 + <PageHeader title="Forum Structure" /> 883 + <p>You don&apos;t have permission to manage forum structure.</p> 884 + </BaseLayout>, 885 + 403 886 + ); 887 + } 888 + 889 + const categoryId = c.req.param("id"); 890 + const cookie = c.req.header("cookie") ?? ""; 891 + 892 + let body: Record<string, string | File>; 893 + try { 894 + body = await c.req.parseBody(); 895 + } catch (error) { 896 + if (isProgrammingError(error)) throw error; 897 + return c.redirect( 898 + `/admin/structure?error=${encodeURIComponent("Invalid form submission.")}`, 899 + 302 900 + ); 901 + } 902 + 903 + const name = typeof body.name === "string" ? body.name.trim() : ""; 904 + if (!name) { 905 + return c.redirect( 906 + `/admin/structure?error=${encodeURIComponent("Category name is required.")}`, 907 + 302 908 + ); 909 + } 910 + 911 + const description = typeof body.description === "string" ? body.description.trim() || null : null; 912 + const sortOrder = parseSortOrder(body.sortOrder); 913 + 914 + let appviewRes: Response; 915 + try { 916 + appviewRes = await fetch(`${appviewUrl}/api/admin/categories/${categoryId}`, { 917 + method: "PUT", 918 + headers: { "Content-Type": "application/json", Cookie: cookie }, 919 + body: JSON.stringify({ name, description, sortOrder }), 920 + }); 921 + } catch (error) { 922 + if (isProgrammingError(error)) throw error; 923 + logger.error("Network error editing category", { 924 + operation: "POST /admin/structure/categories/:id/edit", 925 + categoryId, 926 + error: error instanceof Error ? error.message : String(error), 927 + }); 928 + return c.redirect( 929 + `/admin/structure?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`, 930 + 302 931 + ); 932 + } 933 + 934 + if (!appviewRes.ok) { 935 + const msg = await extractAppviewError(appviewRes, "Failed to update category. Please try again."); 936 + logger.error("AppView error editing category", { 937 + operation: "POST /admin/structure/categories/:id/edit", 938 + categoryId, 939 + status: appviewRes.status, 940 + }); 941 + return c.redirect( 942 + `/admin/structure?error=${encodeURIComponent(msg)}`, 943 + 302 944 + ); 945 + } 946 + 947 + return c.redirect("/admin/structure", 302); 948 + }); 949 + 950 + // ── POST /admin/structure/categories/:id/delete ─────────────────────────── 951 + 952 + app.post("/admin/structure/categories/:id/delete", async (c) => { 953 + const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie")); 954 + if (!auth.authenticated) return c.redirect("/login"); 955 + if (!canManageCategories(auth)) { 956 + return c.html( 957 + <BaseLayout title="Access Denied — atBB Forum" auth={auth}> 958 + <PageHeader title="Forum Structure" /> 959 + <p>You don&apos;t have permission to manage forum structure.</p> 960 + </BaseLayout>, 961 + 403 962 + ); 963 + } 964 + 965 + const categoryId = c.req.param("id"); 966 + const cookie = c.req.header("cookie") ?? ""; 967 + 968 + let appviewRes: Response; 969 + try { 970 + appviewRes = await fetch(`${appviewUrl}/api/admin/categories/${categoryId}`, { 971 + method: "DELETE", 972 + headers: { Cookie: cookie }, 973 + }); 974 + } catch (error) { 975 + if (isProgrammingError(error)) throw error; 976 + logger.error("Network error deleting category", { 977 + operation: "POST /admin/structure/categories/:id/delete", 978 + categoryId, 979 + error: error instanceof Error ? error.message : String(error), 980 + }); 981 + return c.redirect( 982 + `/admin/structure?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`, 983 + 302 984 + ); 985 + } 986 + 987 + if (!appviewRes.ok) { 988 + const msg = await extractAppviewError(appviewRes, "Failed to delete category. Please try again."); 989 + logger.error("AppView error deleting category", { 990 + operation: "POST /admin/structure/categories/:id/delete", 991 + categoryId, 992 + status: appviewRes.status, 993 + }); 994 + return c.redirect( 995 + `/admin/structure?error=${encodeURIComponent(msg)}`, 996 + 302 997 + ); 998 + } 999 + 1000 + return c.redirect("/admin/structure", 302); 1001 + }); 1002 + 801 1003 return app; 802 1004 }