Let admins create and edit roles from the dashboard (Task #82)
What changed:
- Added an `is_system` column to the `roles` table (default 0). The
built-in roles (admin, user, order_receiver) are flagged as system roles
in the seed and via a one-off UPDATE on the existing dev DB.
- Created a new `/api/roles` route module with admin-guarded endpoints:
- POST /api/roles – create a role (validates name format, rejects duplicates).
- PATCH /api/roles/:id – update bilingual descriptions; name is immutable.
- DELETE /api/roles/:id – returns 400 for system roles (also defended
by name allowlist), 409 with userCount/groupCount when in use, 204 on
success. Cleans up role_permissions in a transaction.
GET /api/roles was moved out of groups.ts into the new module.
- OpenAPI updated with createRole / updateRole / deleteRole operations,
isSystem on the Role schema, and CreateRoleBody / UpdateRoleBody /
RoleDeletionConflict schemas. Regenerated api-zod and api-client-react.
- Added a "Roles" item under the User Management nav group in the admin
dashboard, plus a new RolesPanel with search, create dialog, edit dialog
(description-only), and delete dialog with conflict messaging.
- Bilingual translations added in en.json and ar.json (admin.nav.roles +
admin.roles.*).
Notes / deviations:
- Also marked `order_receiver` as a system role since it is seeded by the
system and required by the orders feature, even though the task brief
only called out admin and user.
- Verified end-to-end via the testing skill: list, create, edit, delete,
duplicate-name validation, and protection of system roles all work.
Replit-Task-Id: b1187555-be09-4687-a9ae-83b123d908bd
This commit is contained in:
@@ -39,19 +39,6 @@ function serializeGroup(g: typeof groupsTable.$inferSelect) {
|
||||
};
|
||||
}
|
||||
|
||||
router.get("/roles", requireAdmin, async (_req, res): Promise<void> => {
|
||||
const rows = await db.select().from(rolesTable).orderBy(rolesTable.name);
|
||||
res.json(
|
||||
rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
descriptionAr: r.descriptionAr,
|
||||
descriptionEn: r.descriptionEn,
|
||||
createdAt: r.createdAt,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
router.get("/groups", requireAdmin, async (req, res): Promise<void> => {
|
||||
const q = typeof req.query.q === "string" ? req.query.q.trim() : "";
|
||||
const where = q ? ilike(groupsTable.name, `%${q}%`) : undefined;
|
||||
|
||||
@@ -12,6 +12,7 @@ import storageRouter from "./storage";
|
||||
import settingsRouter from "./settings";
|
||||
import notesRouter from "./notes";
|
||||
import groupsRouter from "./groups";
|
||||
import rolesRouter from "./roles";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
@@ -28,5 +29,6 @@ router.use(storageRouter);
|
||||
router.use(settingsRouter);
|
||||
router.use(notesRouter);
|
||||
router.use(groupsRouter);
|
||||
router.use(rolesRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { db } from "@workspace/db";
|
||||
import {
|
||||
rolesTable,
|
||||
userRolesTable,
|
||||
groupRolesTable,
|
||||
rolePermissionsTable,
|
||||
} from "@workspace/db";
|
||||
import { requireAdmin } from "../middlewares/auth";
|
||||
import { CreateRoleBody, UpdateRoleBody } from "@workspace/api-zod";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
function serializeRole(r: typeof rolesTable.$inferSelect) {
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
descriptionAr: r.descriptionAr,
|
||||
descriptionEn: r.descriptionEn,
|
||||
isSystem: Boolean(r.isSystem),
|
||||
createdAt: r.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
router.get("/roles", requireAdmin, async (_req, res): Promise<void> => {
|
||||
const rows = await db.select().from(rolesTable).orderBy(rolesTable.name);
|
||||
res.json(rows.map(serializeRole));
|
||||
});
|
||||
|
||||
router.post("/roles", requireAdmin, async (req, res): Promise<void> => {
|
||||
const parsed = CreateRoleBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: parsed.error.message });
|
||||
return;
|
||||
}
|
||||
const name = parsed.data.name.trim();
|
||||
if (!/^[a-z][a-z0-9_]*$/i.test(name)) {
|
||||
res.status(400).json({ error: "Role name must start with a letter and contain only letters, numbers, or underscores" });
|
||||
return;
|
||||
}
|
||||
const existing = await db
|
||||
.select({ id: rolesTable.id })
|
||||
.from(rolesTable)
|
||||
.where(eq(rolesTable.name, name));
|
||||
if (existing.length > 0) {
|
||||
res.status(409).json({ error: "Role name already taken" });
|
||||
return;
|
||||
}
|
||||
const [role] = await db
|
||||
.insert(rolesTable)
|
||||
.values({
|
||||
name,
|
||||
descriptionAr: parsed.data.descriptionAr ?? null,
|
||||
descriptionEn: parsed.data.descriptionEn ?? null,
|
||||
})
|
||||
.returning();
|
||||
res.status(201).json(serializeRole(role));
|
||||
});
|
||||
|
||||
router.patch("/roles/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) {
|
||||
res.status(400).json({ error: "Invalid id" });
|
||||
return;
|
||||
}
|
||||
const parsed = UpdateRoleBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: parsed.error.message });
|
||||
return;
|
||||
}
|
||||
const [existing] = await db.select().from(rolesTable).where(eq(rolesTable.id, id));
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Role not found" });
|
||||
return;
|
||||
}
|
||||
const updates: Partial<typeof rolesTable.$inferInsert> = {};
|
||||
if (parsed.data.descriptionAr !== undefined) updates.descriptionAr = parsed.data.descriptionAr;
|
||||
if (parsed.data.descriptionEn !== undefined) updates.descriptionEn = parsed.data.descriptionEn;
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await db.update(rolesTable).set(updates).where(eq(rolesTable.id, id));
|
||||
}
|
||||
const [role] = await db.select().from(rolesTable).where(eq(rolesTable.id, id));
|
||||
res.json(serializeRole(role));
|
||||
});
|
||||
|
||||
router.delete("/roles/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) {
|
||||
res.status(400).json({ error: "Invalid id" });
|
||||
return;
|
||||
}
|
||||
const [existing] = await db.select().from(rolesTable).where(eq(rolesTable.id, id));
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Role not found" });
|
||||
return;
|
||||
}
|
||||
const PROTECTED_NAMES = new Set(["admin", "user", "order_receiver"]);
|
||||
if (existing.isSystem || PROTECTED_NAMES.has(existing.name)) {
|
||||
res.status(400).json({ error: "Cannot delete a system role" });
|
||||
return;
|
||||
}
|
||||
const [userCountRow] = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(userRolesTable)
|
||||
.where(eq(userRolesTable.roleId, id));
|
||||
const [groupCountRow] = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(groupRolesTable)
|
||||
.where(eq(groupRolesTable.roleId, id));
|
||||
const userCount = userCountRow?.count ?? 0;
|
||||
const groupCount = groupCountRow?.count ?? 0;
|
||||
if (userCount > 0 || groupCount > 0) {
|
||||
res.status(409).json({
|
||||
error: "Role is in use",
|
||||
userCount,
|
||||
groupCount,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(rolePermissionsTable).where(eq(rolePermissionsTable.roleId, id));
|
||||
await tx.delete(rolesTable).where(eq(rolesTable.id, id));
|
||||
});
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -356,6 +356,7 @@
|
||||
"services": "الخدمات",
|
||||
"users": "المستخدمين",
|
||||
"groups": "المجموعات",
|
||||
"roles": "الأدوار",
|
||||
"userManagement": "إدارة المستخدمين",
|
||||
"settings": "الإعدادات",
|
||||
"menu": "القائمة"
|
||||
@@ -381,6 +382,30 @@
|
||||
"actions": "إجراءات"
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"searchPlaceholder": "ابحث في الأدوار...",
|
||||
"addRole": "إضافة دور",
|
||||
"editRole": "تعديل الدور",
|
||||
"name": "الاسم",
|
||||
"namePlaceholder": "مثال: content_editor",
|
||||
"nameHint": "حروف وأرقام وشرطات سفلية فقط. لا يمكن تغييره لاحقاً.",
|
||||
"descriptionAr": "الوصف (عربي)",
|
||||
"descriptionEn": "الوصف (إنجليزي)",
|
||||
"system": "نظامي",
|
||||
"empty": "لا توجد أدوار بعد.",
|
||||
"edit": "تعديل الدور",
|
||||
"delete": "حذف الدور",
|
||||
"editHint": "لا يمكن تغيير اسم الدور. يمكنك تحديث الأوصاف بالعربية والإنجليزية.",
|
||||
"deleteTitle": "حذف الدور \"{{name}}\"؟",
|
||||
"deleteEmptyBody": "هذا الدور غير مُعيَّن لأي مستخدم. هل تريد المتابعة؟",
|
||||
"deleteConflict": "هذا الدور قيد الاستخدام ولا يمكن حذفه.",
|
||||
"deleteConflictHint": "أزل هذا الدور من المستخدمين والمجموعات قبل حذفه.",
|
||||
"deleteConfirm": "حذف",
|
||||
"usersCount": "{{count}} مستخدم",
|
||||
"groupsCount": "{{count}} مجموعة",
|
||||
"errorNameTaken": "يوجد دور بنفس الاسم.",
|
||||
"errorInvalidName": "يجب أن يبدأ اسم الدور بحرف ويحتوي على حروف وأرقام وشرطات سفلية فقط."
|
||||
},
|
||||
"groups": {
|
||||
"searchPlaceholder": "ابحث في المجموعات...",
|
||||
"addGroup": "إضافة مجموعة",
|
||||
|
||||
@@ -353,6 +353,7 @@
|
||||
"services": "Services",
|
||||
"users": "Users",
|
||||
"groups": "Groups",
|
||||
"roles": "Roles",
|
||||
"userManagement": "User Management",
|
||||
"settings": "Settings",
|
||||
"menu": "Menu"
|
||||
@@ -378,6 +379,30 @@
|
||||
"actions": "Actions"
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"searchPlaceholder": "Search roles...",
|
||||
"addRole": "Add Role",
|
||||
"editRole": "Edit Role",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "e.g. content_editor",
|
||||
"nameHint": "Letters, numbers and underscores only. Cannot be changed later.",
|
||||
"descriptionAr": "Description (Arabic)",
|
||||
"descriptionEn": "Description (English)",
|
||||
"system": "System",
|
||||
"empty": "No roles yet.",
|
||||
"edit": "Edit role",
|
||||
"delete": "Delete role",
|
||||
"editHint": "Role names cannot be changed. You can update the bilingual descriptions.",
|
||||
"deleteTitle": "Delete role \"{{name}}\"?",
|
||||
"deleteEmptyBody": "This role isn't assigned to anyone. Continue?",
|
||||
"deleteConflict": "This role is still in use and cannot be deleted.",
|
||||
"deleteConflictHint": "Remove this role from the affected users and groups before deleting it.",
|
||||
"deleteConfirm": "Delete",
|
||||
"usersCount": "{{count}} users",
|
||||
"groupsCount": "{{count}} groups",
|
||||
"errorNameTaken": "A role with that name already exists.",
|
||||
"errorInvalidName": "Role name must start with a letter and contain only letters, numbers, or underscores."
|
||||
},
|
||||
"groups": {
|
||||
"searchPlaceholder": "Search groups...",
|
||||
"addGroup": "Add Group",
|
||||
|
||||
@@ -40,6 +40,9 @@ import {
|
||||
useDeleteGroup,
|
||||
useListRoles,
|
||||
getListRolesQueryKey,
|
||||
useCreateRole,
|
||||
useUpdateRole,
|
||||
useDeleteRole,
|
||||
ApiError,
|
||||
} from "@workspace/api-client-react";
|
||||
import type {
|
||||
@@ -72,7 +75,7 @@ import { useToast } from "@/hooks/use-toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatDate as formatDateUtil, formatWeekday as formatWeekdayUtil } from "@/lib/i18n-format";
|
||||
|
||||
type AdminSection = "dashboard" | "apps" | "services" | "users" | "groups" | "settings";
|
||||
type AdminSection = "dashboard" | "apps" | "services" | "users" | "groups" | "roles" | "settings";
|
||||
|
||||
function LeaderboardAvatar({
|
||||
src,
|
||||
@@ -266,6 +269,7 @@ export default function AdminPage() {
|
||||
children: [
|
||||
{ key: "users", label: t("admin.nav.users"), Icon: UsersIcon },
|
||||
{ key: "groups", label: t("admin.nav.groups"), Icon: Shield },
|
||||
{ key: "roles", label: t("admin.nav.roles"), Icon: KeyRound },
|
||||
],
|
||||
},
|
||||
{ key: "settings", label: t("admin.nav.settings"), Icon: Settings },
|
||||
@@ -278,7 +282,7 @@ export default function AdminPage() {
|
||||
const sectionMatch = hash.match(/section=([a-z-]+)/);
|
||||
if (sectionMatch) {
|
||||
const s = sectionMatch[1] as AdminSection;
|
||||
if (["dashboard", "apps", "services", "users", "groups", "settings"].includes(s)) {
|
||||
if (["dashboard", "apps", "services", "users", "groups", "roles", "settings"].includes(s)) {
|
||||
setSection(s);
|
||||
}
|
||||
}
|
||||
@@ -792,6 +796,7 @@ export default function AdminPage() {
|
||||
/>
|
||||
)}
|
||||
{section === "groups" && <GroupsPanel />}
|
||||
{section === "roles" && <RolesPanel />}
|
||||
|
||||
{section === "settings" && (
|
||||
<div className="space-y-3">
|
||||
@@ -2892,3 +2897,340 @@ function GroupDetailEditor({ groupId, onClose, onSaved }: { groupId: number; onC
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RolesPanel() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: roles } = useListRoles({ query: { queryKey: getListRolesQueryKey() } });
|
||||
const createRole = useCreateRole();
|
||||
const updateRole = useUpdateRole();
|
||||
const deleteRole = useDeleteRole();
|
||||
const [search, setSearch] = useState("");
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createForm, setCreateForm] = useState({ name: "", descriptionAr: "", descriptionEn: "" });
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editForm, setEditForm] = useState({ descriptionAr: "", descriptionEn: "" });
|
||||
const [deleteTarget, setDeleteTarget] = useState<{ id: number; name: string } | null>(null);
|
||||
const [deleteConflict, setDeleteConflict] = useState<{ userCount: number; groupCount: number } | null>(null);
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: getListRolesQueryKey() });
|
||||
|
||||
const visibleRoles = (roles ?? []).filter((r) => {
|
||||
const q = search.trim().toLowerCase();
|
||||
if (!q) return true;
|
||||
return (
|
||||
r.name.toLowerCase().includes(q) ||
|
||||
(r.descriptionAr ?? "").toLowerCase().includes(q) ||
|
||||
(r.descriptionEn ?? "").toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
const name = createForm.name.trim();
|
||||
if (!name) return;
|
||||
createRole.mutate(
|
||||
{
|
||||
data: {
|
||||
name,
|
||||
descriptionAr: createForm.descriptionAr.trim() || null,
|
||||
descriptionEn: createForm.descriptionEn.trim() || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
setCreateOpen(false);
|
||||
setCreateForm({ name: "", descriptionAr: "", descriptionEn: "" });
|
||||
toast({ title: t("common.success") });
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
if (err instanceof ApiError && err.status === 409) {
|
||||
toast({ title: t("admin.roles.errorNameTaken"), variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
if (err instanceof ApiError && err.status === 400) {
|
||||
toast({ title: t("admin.roles.errorInvalidName"), variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
toast({ title: t("common.error"), variant: "destructive" });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const beginEdit = (role: { id: number; descriptionAr?: string | null; descriptionEn?: string | null }) => {
|
||||
setEditingId(role.id);
|
||||
setEditForm({
|
||||
descriptionAr: role.descriptionAr ?? "",
|
||||
descriptionEn: role.descriptionEn ?? "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
if (editingId == null) return;
|
||||
updateRole.mutate(
|
||||
{
|
||||
id: editingId,
|
||||
data: {
|
||||
descriptionAr: editForm.descriptionAr.trim() || null,
|
||||
descriptionEn: editForm.descriptionEn.trim() || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
setEditingId(null);
|
||||
toast({ title: t("common.success") });
|
||||
},
|
||||
onError: () => toast({ title: t("common.error"), variant: "destructive" }),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!deleteTarget) return;
|
||||
deleteRole.mutate(
|
||||
{ id: deleteTarget.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
setDeleteTarget(null);
|
||||
setDeleteConflict(null);
|
||||
toast({ title: t("common.success") });
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
if (err instanceof ApiError && err.status === 409 && err.data) {
|
||||
const data = err.data as { userCount: number; groupCount: number };
|
||||
setDeleteConflict({ userCount: data.userCount, groupCount: data.groupCount });
|
||||
return;
|
||||
}
|
||||
toast({ title: t("common.error"), variant: "destructive" });
|
||||
setDeleteTarget(null);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder={t("admin.roles.searchPlaceholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="bg-white/70 border-slate-200 flex-1"
|
||||
data-testid="role-search"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCreateForm({ name: "", descriptionAr: "", descriptionEn: "" });
|
||||
setCreateOpen(true);
|
||||
}}
|
||||
data-testid="create-role-btn"
|
||||
>
|
||||
<Plus size={14} className="me-1" />
|
||||
{t("admin.roles.addRole")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{visibleRoles.map((r) => (
|
||||
<div key={r.id} className="glass-panel rounded-2xl p-4 space-y-2" data-testid={`role-card-${r.id}`}>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold text-foreground flex items-center gap-2">
|
||||
<KeyRound size={16} className="text-primary" />
|
||||
<span className="truncate">{r.name}</span>
|
||||
{r.isSystem && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 text-amber-700 uppercase tracking-wider">
|
||||
{t("admin.roles.system")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(r.descriptionAr || r.descriptionEn) && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{r.descriptionEn || r.descriptionAr}
|
||||
</p>
|
||||
)}
|
||||
{r.descriptionAr && r.descriptionEn && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5" dir="rtl">
|
||||
{r.descriptionAr}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => beginEdit(r)}
|
||||
className="p-1.5 hover:bg-slate-100 rounded-lg text-muted-foreground hover:text-foreground"
|
||||
data-testid={`edit-role-${r.id}`}
|
||||
aria-label={t("admin.roles.edit")}
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
{!r.isSystem && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setDeleteConflict(null);
|
||||
setDeleteTarget({ id: r.id, name: r.name });
|
||||
}}
|
||||
className="p-1.5 hover:bg-destructive/20 rounded-lg text-muted-foreground hover:text-destructive"
|
||||
data-testid={`delete-role-${r.id}`}
|
||||
aria-label={t("admin.roles.delete")}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{visibleRoles.length === 0 && (
|
||||
<div className="glass-panel rounded-2xl p-6 text-center text-sm text-muted-foreground md:col-span-2">
|
||||
{t("admin.roles.empty")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{createOpen && (
|
||||
<div className="fixed inset-0 bg-slate-900/30 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="glass-panel rounded-3xl p-6 w-full max-w-sm space-y-3" data-testid="create-role-dialog">
|
||||
<h2 className="font-semibold text-foreground">{t("admin.roles.addRole")}</h2>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("admin.roles.name")}</Label>
|
||||
<Input
|
||||
value={createForm.name}
|
||||
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
|
||||
className="bg-white/70 border-slate-200"
|
||||
placeholder={t("admin.roles.namePlaceholder")}
|
||||
data-testid="role-name-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t("admin.roles.nameHint")}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("admin.roles.descriptionAr")}</Label>
|
||||
<Input
|
||||
value={createForm.descriptionAr}
|
||||
onChange={(e) => setCreateForm({ ...createForm, descriptionAr: e.target.value })}
|
||||
className="bg-white/70 border-slate-200"
|
||||
dir="rtl"
|
||||
data-testid="role-desc-ar-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("admin.roles.descriptionEn")}</Label>
|
||||
<Input
|
||||
value={createForm.descriptionEn}
|
||||
onChange={(e) => setCreateForm({ ...createForm, descriptionEn: e.target.value })}
|
||||
className="bg-white/70 border-slate-200"
|
||||
dir="ltr"
|
||||
data-testid="role-desc-en-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" className="flex-1" onClick={() => setCreateOpen(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
disabled={!createForm.name.trim() || createRole.isPending}
|
||||
onClick={handleCreate}
|
||||
data-testid="create-role-submit"
|
||||
>
|
||||
{createRole.isPending ? <Loader2 size={14} className="animate-spin" /> : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editingId != null && (
|
||||
<div className="fixed inset-0 bg-slate-900/30 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="glass-panel rounded-3xl p-6 w-full max-w-sm space-y-3" data-testid="edit-role-dialog">
|
||||
<h2 className="font-semibold text-foreground">{t("admin.roles.editRole")}</h2>
|
||||
<p className="text-xs text-muted-foreground">{t("admin.roles.editHint")}</p>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("admin.roles.descriptionAr")}</Label>
|
||||
<Input
|
||||
value={editForm.descriptionAr}
|
||||
onChange={(e) => setEditForm({ ...editForm, descriptionAr: e.target.value })}
|
||||
className="bg-white/70 border-slate-200"
|
||||
dir="rtl"
|
||||
data-testid="role-edit-desc-ar"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("admin.roles.descriptionEn")}</Label>
|
||||
<Input
|
||||
value={editForm.descriptionEn}
|
||||
onChange={(e) => setEditForm({ ...editForm, descriptionEn: e.target.value })}
|
||||
className="bg-white/70 border-slate-200"
|
||||
dir="ltr"
|
||||
data-testid="role-edit-desc-en"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" className="flex-1" onClick={() => setEditingId(null)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
disabled={updateRole.isPending}
|
||||
onClick={handleEdit}
|
||||
data-testid="edit-role-submit"
|
||||
>
|
||||
{updateRole.isPending ? <Loader2 size={14} className="animate-spin" /> : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deleteTarget && (
|
||||
<div className="fixed inset-0 bg-slate-900/30 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="glass-panel rounded-3xl p-6 w-full max-w-sm space-y-3" data-testid="delete-role-dialog">
|
||||
<h2 className="font-semibold text-foreground">
|
||||
{t("admin.roles.deleteTitle", { name: deleteTarget.name })}
|
||||
</h2>
|
||||
{deleteConflict ? (
|
||||
<>
|
||||
<p className="text-sm text-destructive">{t("admin.roles.deleteConflict")}</p>
|
||||
<ul className="text-sm text-foreground space-y-1 ps-4 list-disc">
|
||||
<li>{t("admin.roles.usersCount", { count: deleteConflict.userCount })}</li>
|
||||
<li>{t("admin.roles.groupsCount", { count: deleteConflict.groupCount })}</li>
|
||||
</ul>
|
||||
<p className="text-xs text-muted-foreground">{t("admin.roles.deleteConflictHint")}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">{t("admin.roles.deleteEmptyBody")}</p>
|
||||
)}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setDeleteTarget(null);
|
||||
setDeleteConflict(null);
|
||||
}}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
{!deleteConflict && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex-1"
|
||||
disabled={deleteRole.isPending}
|
||||
onClick={handleDelete}
|
||||
data-testid="delete-role-confirm"
|
||||
>
|
||||
{deleteRole.isPending ? <Loader2 size={14} className="animate-spin" /> : t("admin.roles.deleteConfirm")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,9 +176,32 @@ export interface Role {
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
isSystem: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateRoleBody {
|
||||
/** @minLength 1 */
|
||||
name: string;
|
||||
/** @nullable */
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateRoleBody {
|
||||
/** @nullable */
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
}
|
||||
|
||||
export interface RoleDeletionConflict {
|
||||
error: string;
|
||||
userCount: number;
|
||||
groupCount: number;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
CreateAppBody,
|
||||
CreateConversationBody,
|
||||
CreateGroupBody,
|
||||
CreateRoleBody,
|
||||
CreateServiceBody,
|
||||
CreateServiceOrderBody,
|
||||
CreateUserBody,
|
||||
@@ -56,6 +57,7 @@ import type {
|
||||
RequestUploadUrlResponse,
|
||||
ResetPasswordBody,
|
||||
Role,
|
||||
RoleDeletionConflict,
|
||||
SendMessageBody,
|
||||
Service,
|
||||
ServiceCategory,
|
||||
@@ -70,6 +72,7 @@ import type {
|
||||
UpdateGroupBody,
|
||||
UpdateLanguageBody,
|
||||
UpdateMyAppOrderBody,
|
||||
UpdateRoleBody,
|
||||
UpdateServiceBody,
|
||||
UpdateServiceOrderStatusBody,
|
||||
UpdateUserBody,
|
||||
@@ -4741,6 +4744,265 @@ export function useListRoles<
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create role (admin)
|
||||
*/
|
||||
export const getCreateRoleUrl = () => {
|
||||
return `/api/roles`;
|
||||
};
|
||||
|
||||
export const createRole = async (
|
||||
createRoleBody: CreateRoleBody,
|
||||
options?: RequestInit,
|
||||
): Promise<Role> => {
|
||||
return customFetch<Role>(getCreateRoleUrl(), {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(createRoleBody),
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateRoleMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
TError,
|
||||
{ data: BodyType<CreateRoleBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
TError,
|
||||
{ data: BodyType<CreateRoleBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["createRole"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
{ data: BodyType<CreateRoleBody> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createRole(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createRole>>
|
||||
>;
|
||||
export type CreateRoleMutationBody = BodyType<CreateRoleBody>;
|
||||
export type CreateRoleMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Create role (admin)
|
||||
*/
|
||||
export const useCreateRole = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
TError,
|
||||
{ data: BodyType<CreateRoleBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
TError,
|
||||
{ data: BodyType<CreateRoleBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateRoleMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Update role description (admin)
|
||||
*/
|
||||
export const getUpdateRoleUrl = (id: number) => {
|
||||
return `/api/roles/${id}`;
|
||||
};
|
||||
|
||||
export const updateRole = async (
|
||||
id: number,
|
||||
updateRoleBody: UpdateRoleBody,
|
||||
options?: RequestInit,
|
||||
): Promise<Role> => {
|
||||
return customFetch<Role>(getUpdateRoleUrl(id), {
|
||||
...options,
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(updateRoleBody),
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateRoleMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRole>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<UpdateRoleBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRole>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<UpdateRoleBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["updateRole"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateRole>>,
|
||||
{ id: number; data: BodyType<UpdateRoleBody> }
|
||||
> = (props) => {
|
||||
const { id, data } = props ?? {};
|
||||
|
||||
return updateRole(id, data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateRole>>
|
||||
>;
|
||||
export type UpdateRoleMutationBody = BodyType<UpdateRoleBody>;
|
||||
export type UpdateRoleMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Update role description (admin)
|
||||
*/
|
||||
export const useUpdateRole = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRole>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<UpdateRoleBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateRole>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<UpdateRoleBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateRoleMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Delete role (admin)
|
||||
*/
|
||||
export const getDeleteRoleUrl = (id: number) => {
|
||||
return `/api/roles/${id}`;
|
||||
};
|
||||
|
||||
export const deleteRole = async (
|
||||
id: number,
|
||||
options?: RequestInit,
|
||||
): Promise<void> => {
|
||||
return customFetch<void>(getDeleteRoleUrl(id), {
|
||||
...options,
|
||||
method: "DELETE",
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteRoleMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse | RoleDeletionConflict>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["deleteRole"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
{ id: number }
|
||||
> = (props) => {
|
||||
const { id } = props ?? {};
|
||||
|
||||
return deleteRole(id, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteRole>>
|
||||
>;
|
||||
|
||||
export type DeleteRoleMutationError = ErrorType<
|
||||
ErrorResponse | RoleDeletionConflict
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Delete role (admin)
|
||||
*/
|
||||
export const useDeleteRole = <
|
||||
TError = ErrorType<ErrorResponse | RoleDeletionConflict>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteRoleMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary List groups (admin)
|
||||
*/
|
||||
|
||||
@@ -1258,6 +1258,97 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Role"
|
||||
post:
|
||||
operationId: createRole
|
||||
tags: [users]
|
||||
summary: Create role (admin)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateRoleBody"
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Role"
|
||||
"400":
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
description: Name already taken
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/roles/{id}:
|
||||
patch:
|
||||
operationId: updateRole
|
||||
tags: [users]
|
||||
summary: Update role description (admin)
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UpdateRoleBody"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Role"
|
||||
"404":
|
||||
description: Not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
delete:
|
||||
operationId: deleteRole
|
||||
tags: [users]
|
||||
summary: Delete role (admin)
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"204":
|
||||
description: Deleted
|
||||
"400":
|
||||
description: Cannot delete a system role
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
description: Role is in use
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RoleDeletionConflict"
|
||||
|
||||
# Groups (admin)
|
||||
/groups:
|
||||
@@ -1869,14 +1960,52 @@ components:
|
||||
type: ["string", "null"]
|
||||
descriptionEn:
|
||||
type: ["string", "null"]
|
||||
isSystem:
|
||||
type: boolean
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- isSystem
|
||||
- createdAt
|
||||
|
||||
CreateRoleBody:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
descriptionAr:
|
||||
type: ["string", "null"]
|
||||
descriptionEn:
|
||||
type: ["string", "null"]
|
||||
required:
|
||||
- name
|
||||
|
||||
UpdateRoleBody:
|
||||
type: object
|
||||
properties:
|
||||
descriptionAr:
|
||||
type: ["string", "null"]
|
||||
descriptionEn:
|
||||
type: ["string", "null"]
|
||||
|
||||
RoleDeletionConflict:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
userCount:
|
||||
type: integer
|
||||
groupCount:
|
||||
type: integer
|
||||
required:
|
||||
- error
|
||||
- userCount
|
||||
- groupCount
|
||||
|
||||
GroupSummary:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -1598,10 +1598,49 @@ export const ListRolesResponseItem = zod.object({
|
||||
name: zod.string(),
|
||||
descriptionAr: zod.string().nullish(),
|
||||
descriptionEn: zod.string().nullish(),
|
||||
isSystem: zod.boolean(),
|
||||
createdAt: zod.coerce.date(),
|
||||
});
|
||||
export const ListRolesResponse = zod.array(ListRolesResponseItem);
|
||||
|
||||
/**
|
||||
* @summary Create role (admin)
|
||||
*/
|
||||
|
||||
export const CreateRoleBody = zod.object({
|
||||
name: zod.string().min(1),
|
||||
descriptionAr: zod.string().nullish(),
|
||||
descriptionEn: zod.string().nullish(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Update role description (admin)
|
||||
*/
|
||||
export const UpdateRoleParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
export const UpdateRoleBody = zod.object({
|
||||
descriptionAr: zod.string().nullish(),
|
||||
descriptionEn: zod.string().nullish(),
|
||||
});
|
||||
|
||||
export const UpdateRoleResponse = zod.object({
|
||||
id: zod.number(),
|
||||
name: zod.string(),
|
||||
descriptionAr: zod.string().nullish(),
|
||||
descriptionEn: zod.string().nullish(),
|
||||
isSystem: zod.boolean(),
|
||||
createdAt: zod.coerce.date(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Delete role (admin)
|
||||
*/
|
||||
export const DeleteRoleParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary List groups (admin)
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ export const rolesTable = pgTable("roles", {
|
||||
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(),
|
||||
});
|
||||
|
||||
|
||||
+3
-2
@@ -23,13 +23,13 @@ async function main() {
|
||||
// Create roles
|
||||
const [adminRole] = await db
|
||||
.insert(rolesTable)
|
||||
.values({ name: "admin", descriptionAr: "مدير النظام", descriptionEn: "System Administrator" })
|
||||
.values({ name: "admin", descriptionAr: "مدير النظام", descriptionEn: "System Administrator", isSystem: 1 })
|
||||
.onConflictDoNothing()
|
||||
.returning();
|
||||
|
||||
const [userRole] = await db
|
||||
.insert(rolesTable)
|
||||
.values({ name: "user", descriptionAr: "مستخدم عادي", descriptionEn: "Regular User" })
|
||||
.values({ name: "user", descriptionAr: "مستخدم عادي", descriptionEn: "Regular User", isSystem: 1 })
|
||||
.onConflictDoNothing()
|
||||
.returning();
|
||||
|
||||
@@ -56,6 +56,7 @@ async function main() {
|
||||
name: "order_receiver",
|
||||
descriptionAr: "مستلم الطلبات",
|
||||
descriptionEn: "Order Receiver",
|
||||
isSystem: 1,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning();
|
||||
|
||||
Reference in New Issue
Block a user