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(web): add canManageThemes permission check and Themes card on admin landing (ATB-58)

+67
+10
apps/web/src/lib/session.ts
··· 163 163 "space.atbb.permission.moderatePosts", 164 164 "space.atbb.permission.banUsers", 165 165 "space.atbb.permission.lockTopics", 166 + "space.atbb.permission.manageThemes", 166 167 ] as const; 167 168 168 169 /** ··· 214 215 auth.permissions.has("*")) 215 216 ); 216 217 } 218 + 219 + /** Returns true if the session grants permission to manage forum themes. */ 220 + export function canManageThemes(auth: WebSessionWithPermissions): boolean { 221 + return ( 222 + auth.authenticated && 223 + (auth.permissions.has("space.atbb.permission.manageThemes") || 224 + auth.permissions.has("*")) 225 + ); 226 + }
+44
apps/web/src/routes/__tests__/admin.test.tsx
··· 155 155 expect(html).toContain('href="/admin/modlog"'); 156 156 }); 157 157 158 + it("shows Themes card for user with manageThemes permission", async () => { 159 + setupAuthenticatedSession(["space.atbb.permission.manageThemes"]); 160 + const routes = await loadAdminRoutes(); 161 + const res = await routes.request("/admin", { 162 + headers: { cookie: "atbb_session=token" }, 163 + }); 164 + expect(res.status).toBe(200); 165 + const html = await res.text(); 166 + expect(html).toContain('href="/admin/themes"'); 167 + expect(html).toContain("🎨"); 168 + }); 169 + 170 + it("does not show Themes card for user with only manageMembers permission", async () => { 171 + setupAuthenticatedSession(["space.atbb.permission.manageMembers"]); 172 + const routes = await loadAdminRoutes(); 173 + const res = await routes.request("/admin", { 174 + headers: { cookie: "atbb_session=token" }, 175 + }); 176 + expect(res.status).toBe(200); 177 + const html = await res.text(); 178 + expect(html).not.toContain('href="/admin/themes"'); 179 + }); 180 + 181 + it("shows Themes card for wildcard (*) permission user", async () => { 182 + setupAuthenticatedSession(["*"]); 183 + const routes = await loadAdminRoutes(); 184 + const res = await routes.request("/admin", { 185 + headers: { cookie: "atbb_session=token" }, 186 + }); 187 + expect(res.status).toBe(200); 188 + const html = await res.text(); 189 + expect(html).toContain('href="/admin/themes"'); 190 + }); 191 + 192 + it("grants access to /admin landing page for user with only manageThemes", async () => { 193 + setupAuthenticatedSession(["space.atbb.permission.manageThemes"]); 194 + const routes = await loadAdminRoutes(); 195 + const res = await routes.request("/admin", { 196 + headers: { cookie: "atbb_session=token" }, 197 + }); 198 + // manageThemes should be in ADMIN_PERMISSIONS so the landing page is accessible 199 + expect(res.status).toBe(200); 200 + }); 201 + 158 202 // ── Multi-permission combos ────────────────────────────────────────────── 159 203 160 204 it("shows Members and Mod Log cards for manageMembers + moderatePosts", async () => {
+13
apps/web/src/routes/admin.tsx
··· 8 8 canManageCategories, 9 9 canViewModLog, 10 10 canManageRoles, 11 + canManageThemes, 11 12 } from "../lib/session.js"; 12 13 import { isProgrammingError } from "../lib/errors.js"; 13 14 import { logger } from "../lib/logger.js"; ··· 381 382 const showMembers = canManageMembers(auth); 382 383 const showStructure = canManageCategories(auth); 383 384 const showModLog = canViewModLog(auth); 385 + const showThemes = canManageThemes(auth); 384 386 385 387 return c.html( 386 388 <BaseLayout title="Admin Panel — atBB Forum" auth={auth}> ··· 410 412 <p class="admin-nav-card__icon" aria-hidden="true">📋</p> 411 413 <p class="admin-nav-card__title">Mod Log</p> 412 414 <p class="admin-nav-card__description">Audit trail of moderation actions</p> 415 + </Card> 416 + </a> 417 + )} 418 + {showThemes && ( 419 + <a href="/admin/themes" class="admin-nav-card"> 420 + <Card> 421 + <p class="admin-nav-card__icon" aria-hidden="true">🎨</p> 422 + <p class="admin-nav-card__title">Themes</p> 423 + <p class="admin-nav-card__description"> 424 + Customize forum appearance and color schemes 425 + </p> 413 426 </Card> 414 427 </a> 415 428 )}