Add app-permissions impact preview before tightening an app's gate
Mirrors the existing role-permissions impact preview UX for app
permissions. Admins now see how many currently-visible users would lose
access before they add a permission requirement to an app, plus the
groups (via group_apps) that offset the loss because their members keep
access regardless.
Backend
- New endpoint POST /api/apps/:id/permissions/impact-preview in
artifacts/api-server/src/routes/apps.ts. Implements the same OR
semantics as getVisibleAppsForUser: a user "sees" an app if they hold
ANY required permission (direct or via a group role) OR they belong
to a group granted the app via group_apps. Admins are excluded from
counts since they always see every app. Short-circuits with
noChange:true when the candidate set equals the current set.
- OpenAPI schema (lib/api-spec/openapi.yaml): adds the path,
AppPermissionsImpactBody, AppPermissionImpactGroup,
AppPermissionsImpact. Regenerated lib/api-client-react bindings.
Frontend
- AppPermissionsEditor (artifacts/tx-os/src/pages/admin.tsx): debounced
(350ms) cancel-safe preview when a pending permission is selected,
warning banner with affected/visible counts and offsetting groups,
and a confirmation dialog when affectedUserCount > 0. Add button is
disabled while the preview is loading or errored to keep the warning
trustworthy.
- i18n keys added to en.json and ar.json under
admin.appPermissions.{impactTitle, impactLoading, impactError,
impactNone, impactSummary, impactViaGroups, confirmTitle, confirmBody,
confirmAction}.
Tests
- artifacts/api-server/tests/app-permissions-impact.test.mjs: 7 tests
covering noChange short-circuit, unrestricted-app tightening,
candidate that keeps an existing permission, group_apps offset,
unknown app (404), invalid payload (400), and admin-only enforcement.
All 18 app-permissions tests pass.
- E2E flow verified via runTest: admin login → /admin → Apps → edit
app → select permission → preview banner appears → Add → confirm
dialog → cancel without writing.
Out-of-scope (filed as follow-ups #228 and #229): listing the specific
affected user IDs in the preview, and warning when REMOVING a
permission broadens access.
No deviations from the task spec.
Replit-Task-Id: 8b2ff9ea-f95e-4bdb-b268-95b5d17154ca
This commit is contained in:
@@ -486,6 +486,60 @@ export const AddAppPermissionBody = zod.object({
|
||||
.describe("ID of the permission to require for this app."),
|
||||
});
|
||||
|
||||
/**
|
||||
* Given a candidate set of permission IDs that would gate this app,
|
||||
returns how many users would lose visibility of the app (excluding
|
||||
admins, who always see every app), the count of users who currently
|
||||
see the app, and the groups that grant the app via `group_apps`. A
|
||||
member of any returned group always sees the app regardless of
|
||||
permission changes, so listing them lets the admin see which
|
||||
populations are protected from the loss.
|
||||
|
||||
Mirrors `POST /roles/{id}/permissions/impact-preview` so the admin
|
||||
UI can show the same kind of warning before committing the change.
|
||||
|
||||
* @summary Preview the impact of changing the permissions required for an app (admin)
|
||||
*/
|
||||
export const PreviewAppPermissionsImpactParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
export const PreviewAppPermissionsImpactBody = zod.object({
|
||||
permissionIds: zod
|
||||
.array(zod.number())
|
||||
.describe(
|
||||
'Candidate set of permission IDs that would gate this app. Pass an empty array to model \"remove every requirement\" (i.e. make the app unrestricted).',
|
||||
),
|
||||
});
|
||||
|
||||
export const PreviewAppPermissionsImpactResponse = zod.object({
|
||||
affectedUserCount: zod
|
||||
.number()
|
||||
.describe(
|
||||
"Distinct non-admin users who currently see the app and would lose visibility under the candidate permission set. Already accounts for users who keep access via `group_apps` group-grants.",
|
||||
),
|
||||
currentlyVisibleUserCount: zod
|
||||
.number()
|
||||
.describe(
|
||||
'Distinct non-admin users who currently see the app under the existing permission set. Useful for showing the warning as a fraction (e.g. \"12 of 80 will lose access\").',
|
||||
),
|
||||
groups: zod
|
||||
.array(
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
name: zod.string(),
|
||||
}),
|
||||
)
|
||||
.describe(
|
||||
"Groups that currently grant this app via `group_apps`. Members of any of these groups keep access regardless of permission changes.",
|
||||
),
|
||||
noChange: zod
|
||||
.boolean()
|
||||
.describe(
|
||||
"True when the candidate set is identical to the current permission set so no change would actually be saved.",
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Remove a required permission from this app (admin)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user