diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index c1127a6..7dc611e 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -4,6 +4,7 @@ import { auth, logout } from "@exosphere/client/auth"; import { useLocation, useRoute } from "@exosphere/client/router"; import { spherePath } from "@exosphere/client/router"; import { sphereState, sphereHandle, loadSphere } from "@exosphere/client/sphere"; +import { canAccessSettings } from "@exosphere/client/permissions"; import { isMultiSphere } from "@exosphere/client/config"; import { Link } from "@exosphere/client/link"; import * as ui from "@exosphere/client/ui.css"; @@ -18,6 +19,7 @@ import { SphereMembersPage } from "./pages/sphere-members.tsx"; import { SpherePermissionsPage } from "./pages/sphere-permissions.tsx"; import { SphereLabelsPage } from "./pages/sphere-labels.tsx"; import { Dashboard } from "./pages/dashboard.tsx"; +import { NotFoundPage } from "./pages/not-found.tsx"; import type { ModuleRoute } from "@exosphere/client/types"; import { feedsModule } from "@exosphere/feeds/client"; import { featureRequestsModule } from "@exosphere/feature-requests/client"; @@ -75,20 +77,6 @@ function MultiSphereDefaultPage() { return ; } -function NotFoundPage() { - return ( -
-
-

Page not found

-

The page you're looking for doesn't exist.

- - Go home - -
-
- ); -} - /** Watches the :sphereHandle route param and reloads sphere data when it changes. */ function SphereLoader() { const { params } = useRoute(); @@ -290,6 +278,7 @@ function Header() { const homeHref = isMultiSphere && sphere ? spherePath("/") : "/"; const showSphereNav = Boolean(sphere); + const showSettings = authenticated && showSphereNav && canAccessSettings(); const settingsActive = isSettingsPathActive(path); const closeDrawer = () => setDrawerOpen(false); @@ -313,7 +302,7 @@ function Header() { )} - {showSphereNav && ( + {showSettings && (
- {showSphereNav && ( + {showSettings && ( +
+

Page not found

+

The page you're looking for doesn't exist.

+ + Go home + +
+
+ ); +} diff --git a/packages/app/src/pages/settings-layout.tsx b/packages/app/src/pages/settings-layout.tsx index 6b2a169..459f372 100644 --- a/packages/app/src/pages/settings-layout.tsx +++ b/packages/app/src/pages/settings-layout.tsx @@ -1,15 +1,11 @@ import type { ComponentChildren } from "preact"; -import { useLocation } from "@exosphere/client/router"; -import { spherePath } from "@exosphere/client/router"; +import { useLocation, spherePath } from "@exosphere/client/router"; import { sphereState } from "@exosphere/client/sphere"; -import { canDo } from "@exosphere/client/permissions"; +import { canDo, canManageMembers, canManageModules } from "@exosphere/client/permissions"; import { Link } from "@exosphere/client/link"; import * as s from "./sphere.css.ts"; import * as ss from "./sphere-settings.css.ts"; -// "members" and "permissions" stay in the union because their pages still pass -// them as `active`, even though the rail doesn't render links to them anymore. -// The pages remain reachable by direct URL. export type SettingsSection = "general" | "modules" | "labels" | "members" | "permissions"; interface Props { @@ -17,20 +13,8 @@ interface Props { children: ComponentChildren; } -function RailLink({ - href, - active, - label, - danger, -}: { - href: string; - active: boolean; - label: string; - danger?: boolean; -}) { - const classes = [ss.railLink, active ? ss.railLinkActive : "", danger ? ss.railLinkDanger : ""] - .filter(Boolean) - .join(" "); +function RailLink({ href, active, label }: { href: string; active: boolean; label: string }) { + const classes = `${ss.railLink}${active ? ` ${ss.railLinkActive}` : ""}`; return ( {label} @@ -43,7 +27,10 @@ export function SettingsLayout({ active, children }: Props) { useLocation(); const data = sphereState.value.data; - const canManageLabels = canDo("sphere", "manageLabels"); + const showModules = canManageModules(); + const showLabels = canDo("sphere", "manageLabels"); + const showMembers = canManageMembers(); + const showPermissions = canDo("sphere", "updatePermissions"); return (
@@ -59,18 +46,34 @@ export function SettingsLayout({ active, children }: Props) {
{children}
diff --git a/packages/app/src/pages/sphere-labels.tsx b/packages/app/src/pages/sphere-labels.tsx index 171a493..83067d3 100644 --- a/packages/app/src/pages/sphere-labels.tsx +++ b/packages/app/src/pages/sphere-labels.tsx @@ -8,6 +8,7 @@ import { GripVertical } from "lucide-preact"; import * as ui from "@exosphere/client/ui.css"; import * as cpUi from "./color-picker.css.ts"; import { SettingsLayout } from "./settings-layout.tsx"; +import { NotFoundPage } from "./not-found.tsx"; import { getSphereLabels, createSphereLabel, @@ -33,7 +34,7 @@ export function SphereLabelsPage() { const handle = sphereHandle.value; if (!data || !handle) return null; - if (!canDo("sphere", "manageLabels")) return null; + if (!canDo("sphere", "manageLabels")) return ; return ( diff --git a/packages/app/src/pages/sphere-members.tsx b/packages/app/src/pages/sphere-members.tsx index e2ceb7f..1024427 100644 --- a/packages/app/src/pages/sphere-members.tsx +++ b/packages/app/src/pages/sphere-members.tsx @@ -1,9 +1,10 @@ import { useSignal } from "@preact/signals"; import { sphereState, sphereHandle } from "@exosphere/client/sphere"; -import { canDo } from "@exosphere/client/permissions"; +import { canDo, canManageMembers } from "@exosphere/client/permissions"; import { useQuery } from "@exosphere/client/hooks"; import * as ui from "@exosphere/client/ui.css"; import { SettingsLayout } from "./settings-layout.tsx"; +import { NotFoundPage } from "./not-found.tsx"; import { getSphereMembers, inviteMember, @@ -29,12 +30,7 @@ export function SphereMembersPage() { const handle = sphereHandle.value; if (!data || !handle) return null; - // Allow access if user has any member management permission - const canManageMembers = - canDo("sphere", "inviteMember") || - canDo("sphere", "revokeMember") || - canDo("sphere", "updateMemberRole"); - if (!canManageMembers) return null; + if (!canManageMembers()) return ; return ( diff --git a/packages/app/src/pages/sphere-permissions.tsx b/packages/app/src/pages/sphere-permissions.tsx index b23a573..40e231b 100644 --- a/packages/app/src/pages/sphere-permissions.tsx +++ b/packages/app/src/pages/sphere-permissions.tsx @@ -4,6 +4,7 @@ import { canDo } from "@exosphere/client/permissions"; import { useQuery } from "@exosphere/client/hooks"; import * as ui from "@exosphere/client/ui.css"; import { SettingsLayout } from "./settings-layout.tsx"; +import { NotFoundPage } from "./not-found.tsx"; import { getSpherePermissions, updateSpherePermissions } from "../api/spheres.ts"; const roleLabels: Record = { @@ -26,7 +27,7 @@ export function SpherePermissionsPage() { const handle = sphereHandle.value; if (!data || !handle) return null; - if (!canDo("sphere", "updatePermissions")) return null; + if (!canDo("sphere", "updatePermissions")) return ; return ( diff --git a/packages/app/src/pages/sphere-settings.css.ts b/packages/app/src/pages/sphere-settings.css.ts index 3a04324..1fd5603 100644 --- a/packages/app/src/pages/sphere-settings.css.ts +++ b/packages/app/src/pages/sphere-settings.css.ts @@ -70,10 +70,6 @@ export const railLinkActive = style({ color: vars.color.primary, }); -export const railLinkDanger = style({ - color: vars.color.danger, -}); - export const railGroup = style({ paddingBlock: "10px 4px", paddingInline: "12px", diff --git a/packages/app/src/pages/sphere-settings.tsx b/packages/app/src/pages/sphere-settings.tsx index dcc8088..fee732a 100644 --- a/packages/app/src/pages/sphere-settings.tsx +++ b/packages/app/src/pages/sphere-settings.tsx @@ -1,11 +1,12 @@ import { useSignal } from "@preact/signals"; import { sphereState, sphereHandle, refreshSphere } from "@exosphere/client/sphere"; -import { canDo } from "@exosphere/client/permissions"; +import { canDo, canAccessSettings, canManageModules } from "@exosphere/client/permissions"; import { useLocation } from "@exosphere/client/router"; import { useQuery } from "@exosphere/client/hooks"; import * as ui from "@exosphere/client/ui.css"; import * as ss from "./sphere-settings.css.ts"; import { SettingsLayout } from "./settings-layout.tsx"; +import { NotFoundPage } from "./not-found.tsx"; import { getSphereModules, enableModule as apiEnableModule, @@ -161,8 +162,10 @@ export function SphereSettingsPage() { const handle = sphereHandle.value; if (!data || !handle) return null; + if (!canAccessSettings()) return ; const section = resolveSection(path ?? ""); + if (section === "modules" && !canManageModules()) return ; return ( diff --git a/packages/client/src/permissions.ts b/packages/client/src/permissions.ts index d7b2218..ca3ffec 100644 --- a/packages/client/src/permissions.ts +++ b/packages/client/src/permissions.ts @@ -13,3 +13,28 @@ export function canDo(module: string, action: string): boolean { export function useCanDo(module: string, action: string) { return computed(() => canDo(module, action)); } + +/** True if the user can manage the sphere's enabled modules. */ +export function canManageModules(): boolean { + return canDo("sphere", "enableModule") || canDo("sphere", "disableModule"); +} + +/** True if the user can manage members (invite, revoke, update role). */ +export function canManageMembers(): boolean { + return ( + canDo("sphere", "inviteMember") || + canDo("sphere", "revokeMember") || + canDo("sphere", "updateMemberRole") + ); +} + +/** True if the user has at least one settings-related permission. + * Used to gate the Settings entry point and the Settings page itself. */ +export function canAccessSettings(): boolean { + return ( + canManageModules() || + canManageMembers() || + canDo("sphere", "manageLabels") || + canDo("sphere", "updatePermissions") + ); +}