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:
@@ -0,0 +1,62 @@
|
||||
import { pgTable, text, serial, timestamp, integer, varchar, primaryKey } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod/v4";
|
||||
import { usersTable } from "./users";
|
||||
import { rolesTable } from "./roles";
|
||||
import { appsTable } from "./apps";
|
||||
|
||||
export const groupsTable = pgTable("groups", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: varchar("name", { length: 100 }).notNull().unique(),
|
||||
descriptionAr: text("description_ar"),
|
||||
descriptionEn: text("description_en"),
|
||||
isSystem: integer("is_system").notNull().default(0),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const userGroupsTable = pgTable(
|
||||
"user_groups",
|
||||
{
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
groupId: integer("group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.id, { onDelete: "cascade" }),
|
||||
assignedAt: timestamp("assigned_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.userId, t.groupId] })],
|
||||
);
|
||||
|
||||
export const groupAppsTable = pgTable(
|
||||
"group_apps",
|
||||
{
|
||||
groupId: integer("group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.id, { onDelete: "cascade" }),
|
||||
appId: integer("app_id")
|
||||
.notNull()
|
||||
.references(() => appsTable.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.groupId, t.appId] })],
|
||||
);
|
||||
|
||||
export const groupRolesTable = pgTable(
|
||||
"group_roles",
|
||||
{
|
||||
groupId: integer("group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.id, { onDelete: "cascade" }),
|
||||
roleId: integer("role_id")
|
||||
.notNull()
|
||||
.references(() => rolesTable.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.groupId, t.roleId] })],
|
||||
);
|
||||
|
||||
export const insertGroupSchema = createInsertSchema(groupsTable).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
export type InsertGroup = z.infer<typeof insertGroupSchema>;
|
||||
export type Group = typeof groupsTable.$inferSelect;
|
||||
@@ -10,3 +10,4 @@ export * from "./user-app-orders";
|
||||
export * from "./app-opens";
|
||||
export * from "./password-reset-tokens";
|
||||
export * from "./notes";
|
||||
export * from "./groups";
|
||||
|
||||
Reference in New Issue
Block a user