Task #74: Add groups system + admin User Management UI

Backend:
- New schema: groups, user_groups, group_apps, group_roles (lib/db/src/schema/groups.ts)
- Seeds Admins, TeaBoy, Everyone system groups idempotently and maps existing users
- /api/groups CRUD with admin guard, batch counts, system-group delete protection
- Validates appIds/roleIds/userIds (400 on missing) and wraps assignment writes in
  a single DB transaction (no partial state on failure)
- /api/users gains q/groupId/status filters, batch role+group loading, groupIds
  replacement on PATCH, auto-assigns Everyone on admin-create
- /auth/register also auto-assigns Everyone for consistent default linkage
- buildAuthUser now returns groups (matches updated AuthUser OpenAPI schema)
- App visibility (getVisibleAppsForUser) unions group-granted apps via
  group_apps + user_groups in addition to existing permission gating

Frontend (admin.tsx):
- Nav restructured: User Management section with Users + Groups children
- Section deep-linked via #section=… URL hash
- Users page rebuilt: search, group filter, status filter, sortable table,
  groups column, edit-groups dialog, mobile cards
- New Groups page: cards with member/app/role counts, create dialog,
  detail editor with Info/Apps/Users tabs and system-group guard
- ar/en translations added for all new keys

Testing:
- pnpm typecheck clean (api + web)
- 25/26 api tests pass; the only failure is pre-existing flaky pagination test
  (admin-app-opens-pagination) — left as-is per scratchpad note
- Code review feedback addressed (validation, transactions, register auto-assign)
This commit is contained in:
riyadhafraa
2026-04-22 08:28:31 +00:00
parent a772607636
commit f5273af19f
16 changed files with 2576 additions and 194 deletions
+20 -1
View File
@@ -9,6 +9,8 @@ import {
rolesTable,
userAppOrdersTable,
appOpensTable,
userGroupsTable,
groupAppsTable,
} from "@workspace/db";
import { requireAuth, requireAdmin } from "../middlewares/auth";
import {
@@ -76,9 +78,26 @@ async function getVisibleAppsForUser(userId: number): Promise<typeof appsTable.$
).map((r) => r.appId)
: [];
// Group-granted apps: any app explicitly assigned to one of the user's groups
// is visible regardless of the legacy permission gating.
const groupGrantedAppIds = (
await db
.select({ appId: groupAppsTable.appId })
.from(groupAppsTable)
.innerJoin(
userGroupsTable,
eq(userGroupsTable.groupId, groupAppsTable.groupId),
)
.where(eq(userGroupsTable.userId, userId))
).map((r) => r.appId);
const groupGrantedSet = new Set(groupGrantedAppIds);
const allowedAppIdSet = new Set(allowedRestrictedAppIds);
return apps.filter(
(app) => !restrictedAppIds.has(app.id) || allowedAppIdSet.has(app.id),
(app) =>
!restrictedAppIds.has(app.id) ||
allowedAppIdSet.has(app.id) ||
groupGrantedSet.has(app.id),
);
}