2f28260fb2
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)
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
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;
|