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): POST /admin/themes/:rkey/save — persist token edits to AppView (ATB-59)

Malpercio 5e55ee96 c245255b

+76
+76
apps/web/src/routes/admin-themes.tsx
··· 1003 1003 return c.html(<ThemePreviewContent tokens={tokens} />); 1004 1004 }); 1005 1005 1006 + // ── POST /admin/themes/:rkey/save ───────────────────────────────────────── 1007 + 1008 + app.post("/admin/themes/:rkey/save", async (c) => { 1009 + const auth = await getSessionWithPermissions(appviewUrl, c.req.header("cookie")); 1010 + if (!auth.authenticated) return c.redirect("/login"); 1011 + if (!canManageThemes(auth)) { 1012 + return c.html( 1013 + <BaseLayout title="Access Denied" auth={auth}> 1014 + <p>Access denied.</p> 1015 + </BaseLayout>, 1016 + 403 1017 + ); 1018 + } 1019 + 1020 + const themeRkey = c.req.param("rkey"); 1021 + const cookie = c.req.header("cookie") ?? ""; 1022 + 1023 + let rawBody: Record<string, string | File>; 1024 + try { 1025 + rawBody = await c.req.parseBody(); 1026 + } catch (error) { 1027 + if (isProgrammingError(error)) throw error; 1028 + return c.redirect( 1029 + `/admin/themes/${themeRkey}?error=${encodeURIComponent("Invalid form submission.")}`, 1030 + 302 1031 + ); 1032 + } 1033 + 1034 + const name = typeof rawBody.name === "string" ? rawBody.name.trim() : ""; 1035 + const colorScheme = typeof rawBody.colorScheme === "string" ? rawBody.colorScheme : "light"; 1036 + const fontUrlsRaw = typeof rawBody.fontUrls === "string" ? rawBody.fontUrls : ""; 1037 + const fontUrls = fontUrlsRaw 1038 + .split("\n") 1039 + .map((u) => u.trim()) 1040 + .filter(Boolean); 1041 + 1042 + // Extract token values from form fields 1043 + const tokens: Record<string, string> = {}; 1044 + for (const tokenName of ALL_KNOWN_TOKENS) { 1045 + const raw = rawBody[tokenName]; 1046 + if (typeof raw === "string" && raw.trim()) { 1047 + tokens[tokenName] = raw.trim(); 1048 + } 1049 + } 1050 + 1051 + let apiRes: Response; 1052 + try { 1053 + apiRes = await fetch(`${appviewUrl}/api/admin/themes/${themeRkey}`, { 1054 + method: "PUT", 1055 + headers: { "Content-Type": "application/json", Cookie: cookie }, 1056 + body: JSON.stringify({ name, colorScheme, tokens, fontUrls }), 1057 + }); 1058 + } catch (error) { 1059 + if (isProgrammingError(error)) throw error; 1060 + logger.error("Network error saving theme", { 1061 + operation: "POST /admin/themes/:rkey/save", 1062 + themeRkey, 1063 + error: error instanceof Error ? error.message : String(error), 1064 + }); 1065 + return c.redirect( 1066 + `/admin/themes/${themeRkey}?error=${encodeURIComponent("Forum temporarily unavailable. Please try again.")}`, 1067 + 302 1068 + ); 1069 + } 1070 + 1071 + if (!apiRes.ok) { 1072 + const msg = await extractAppviewError(apiRes, "Failed to save theme. Please try again."); 1073 + return c.redirect( 1074 + `/admin/themes/${themeRkey}?error=${encodeURIComponent(msg)}`, 1075 + 302 1076 + ); 1077 + } 1078 + 1079 + return c.redirect(`/admin/themes/${themeRkey}?success=1`, 302); 1080 + }); 1081 + 1006 1082 return app; 1007 1083 } 1008 1084