Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2469Done

Every hidden settings destination is GUARDED — the pages render the no-access state instead of a read-only form

Hiding is presentation and never protection. Once the rail stops offering a settings entry, the page behind it is still one typed URL, one bookmark and one old link away — and today most of those pages answer by rendering a read-only form, which is exactly the behaviour the parent story's amended rule replaces. This card closes the other half: a destination the actor cannot use renders the no-access state.

The approach

The exemplar already ships. app/(authed)/settings/project/automation/page.tsx resolves projectAccessService.getPermissions, tests the key, and renders components/projects/NoAccessState when it is absent. Every other settings page follows it.

  • Each page reads its key FROM the registry, never re-declares it. lib/settings/projectSettingsNav.ts is pure data with no JSX and its own comment sanctions server imports; after the registry card each entry carries its PermissionKey. A page that looks up its own entry cannot drift from the rail that hides it — a hardcoded second copy of the key is how a row hides on one permission while its page refuses on another.
  • Three pages re-implement the admin check privatelymembers, fields and components each compute isWorkspaceManager(wsRole) || myMembership?.role === 'admin' inline. That is a second policy, it predates the catalog, and it goes here.
  • The refusal keeps the shipped 404-vs-403 posture. A NON-BROWSER must still be answered as though the project does not exist — the no-existence-leak rule assertPermission implements by throwing ProjectNotFoundError before it tests the key. The no-access state is for a BROWSER who simply does not hold this domain's key. Do not collapse the two.
  • Copy per destination, from panel 3 of the design asset: a title that names the room, a description that says why, and a back action that goes somewhere useful — not one generic apology reused eleven times.
  • The drill-down inherits. roles/[roleKey] has no rail row of its own; it takes its parent entry's key, exactly as nestedRoutes already declares it does for the rail.

Scope boundary

In: every page.tsx under app/(authed)/settings/project/**, their copy and its zh twin, and their tests.

Out: the registry and the rail (the registry card); every SERVICE and ROUTE gate — those were wired by the per-domain split and the member-facing sweep and this card neither adds nor weakens one, so no file under lib/services/** or app/api/** is touched; the workspace-settings area, which is governed by the workspace role this epic does not change.

Acceptance criteria

  • A grep under app/(authed)/settings/project/ for a privately re-implemented admin check — isWorkspaceManager combined with a membership-role comparison — returns nothing.
  • A test that ENUMERATES the filesystem (the technique the shipped route↔registry guard already uses) asserts that every page.tsx under app/(authed)/settings/project/** resolves the actor's permissions and renders NoAccessState when its key is absent — so a settings page added later cannot ship unguarded, and the assertion needs no count to stay true.
  • No page hardcodes a permission-key string: each resolves its key by looking itself up in PROJECT_SETTINGS_NAV, and a grep for a quoted catalog key under app/(authed)/settings/project/ returns nothing.
  • roles/[roleKey] is guarded by the same key as roles.
  • A browser who does not hold the key gets the no-access state; a NON-browser still gets the shipped not-found handling — a test covers both arms, so the refusal never confirms a project the actor may not browse.
  • Each destination's no-access copy matches panel 3 of design/projects/permission-gated-ui.mock.html, and every new en key has its zh twin.
  • A project admin sees every settings page exactly as it renders at the branch's own merge base — no page lost a control, a form or a section — compared against that baseline before the PR lands.
  • git diff --name-only lists no file under lib/services/ or app/api/.
  • The changed files meet the repo's per-file coverage floor.

Context refs

  • app/(authed)/settings/project/automation/page.tsx — the shipped exemplar: getPermissions, the key test, NoAccessState.
  • app/(authed)/settings/project/members/page.tsx, fields/page.tsx, components/page.tsx — the three private admin re-implementations this card removes.
  • components/projects/NoAccessState.tsx — the refused-state component, and its note on why there is no "Request access" action.
  • lib/settings/projectSettingsNav.ts — the registry each page looks its key up in, and nestedRoutes for the drill-down.
  • lib/services/projectAccessService.tsassertPermission's refusal ORDER, which this card must not flatten.
  • tests/settings/projectSettingsNav.test.ts — the filesystem-enumerating guard whose technique the new coverage assertion reuses.
  • The design card — panel 3 is this card's copy reference.