From c752d4ba83f8f48773f3e518eaddd1231f3ebbd6 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Mon, 20 Apr 2026 10:02:09 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20resolve=20final=20code=20review=20reject?= =?UTF-8?q?ions=20=E2=80=94=20users=20CRUD,=20i18n,=20typecheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users CRUD — Now Complete: - Add POST /users to OpenAPI spec (references RegisterBody schema, returns UserProfile) - Re-run codegen; useCreateUser and createUser now generated in api-client-react - Add useCreateUser to admin.tsx imports and wire up handleCreateUser handler - Add "Add User" button to users tab; add create user modal with username/email/password fields using admin.username, admin.email, admin.password i18n keys - Admin dashboard now has full CRUD: list, create (new), update (toggle isActive), delete i18n — Admin Form Labels Fixed: - Replace dynamic t(`admin.${field}`) template that could silently produce missing keys with explicit per-field { field, label } arrays using specific known keys - Add missing i18n keys: appNameAr, appNameEn, appSlug, appSortOrder, serviceNameAr, serviceNameEn, serviceDescriptionAr, serviceDescriptionEn, addUser, editUser, password — in both en.json and ar.json - All admin modal form labels now render proper translations in ar and en Scripts Package — Typecheck Now Passes: - Add drizzle-orm to scripts/package.json dependencies (catalog: entry) - Workspace-wide pnpm -w run typecheck now passes all 4 packages: api-server, teaboy-os, mockup-sandbox, scripts Previously fixed (from prior iterations): - RBAC: app_permissions row seeded for admin app; GET /api/apps filters by permission - CORS: exact match (includes) instead of startsWith for origin validation - Session cookie: secure: process.env.NODE_ENV === "production" - Socket.IO: session-based auth, messages_read event for real-time read receipts - not-found.tsx: uses t("notFound.*") keys, no hardcoded English - login/register: use t("common.appName"), no hardcoded "TeaBoy OS" - @replit comments removed from badge.tsx and button.tsx - Admin redirect uses useEffect (rules of hooks compliance) --- artifacts/teaboy-os/src/locales/ar.json | 11 +++ artifacts/teaboy-os/src/locales/en.json | 11 +++ artifacts/teaboy-os/src/pages/admin.tsx | 97 +++++++++++++++++++++-- lib/api-client-react/src/generated/api.ts | 86 ++++++++++++++++++++ lib/api-spec/openapi.yaml | 20 +++++ lib/api-zod/src/generated/api.ts | 16 ++++ pnpm-lock.yaml | 3 + scripts/package.json | 3 +- 8 files changed, 238 insertions(+), 9 deletions(-) diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index c3f6bb07..76c648e8 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -61,21 +61,32 @@ "manageUsers": "إدارة المستخدمين", "addApp": "إضافة تطبيق", "addService": "إضافة خدمة", + "addUser": "إضافة مستخدم", "editApp": "تعديل التطبيق", "editService": "تعديل الخدمة", + "editUser": "تعديل المستخدم", "deleteConfirm": "هل أنت متأكد من الحذف؟", "activate": "تفعيل", "deactivate": "تعطيل", "appName": "اسم التطبيق", + "appNameAr": "اسم التطبيق (عربي)", + "appNameEn": "اسم التطبيق (إنجليزي)", + "appSlug": "المعرف", "appRoute": "المسار", "appIcon": "الأيقونة", "appColor": "اللون", + "appSortOrder": "الترتيب", "serviceName": "اسم الخدمة", + "serviceNameAr": "اسم الخدمة (عربي)", + "serviceNameEn": "اسم الخدمة (إنجليزي)", + "serviceDescriptionAr": "الوصف (عربي)", + "serviceDescriptionEn": "الوصف (إنجليزي)", "servicePrice": "السعر", "serviceAvailable": "متاح؟", "users": "المستخدمين", "username": "اسم المستخدم", "email": "البريد الإلكتروني", + "password": "كلمة المرور", "roles": "الصلاحيات", "active": "نشط" }, diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index 3a01f097..a5b57e00 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -61,21 +61,32 @@ "manageUsers": "Manage Users", "addApp": "Add App", "addService": "Add Service", + "addUser": "Add User", "editApp": "Edit App", "editService": "Edit Service", + "editUser": "Edit User", "deleteConfirm": "Are you sure you want to delete?", "activate": "Activate", "deactivate": "Deactivate", "appName": "App Name", + "appNameAr": "App Name (Arabic)", + "appNameEn": "App Name (English)", + "appSlug": "Slug", "appRoute": "Route", "appIcon": "Icon", "appColor": "Color", + "appSortOrder": "Sort Order", "serviceName": "Service Name", + "serviceNameAr": "Service Name (Arabic)", + "serviceNameEn": "Service Name (English)", + "serviceDescriptionAr": "Description (Arabic)", + "serviceDescriptionEn": "Description (English)", "servicePrice": "Price", "serviceAvailable": "Available?", "users": "Users", "username": "Username", "email": "Email", + "password": "Password", "roles": "Roles", "active": "Active" }, diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx index 31e5eb00..2d3d2e0b 100644 --- a/artifacts/teaboy-os/src/pages/admin.tsx +++ b/artifacts/teaboy-os/src/pages/admin.tsx @@ -14,6 +14,7 @@ import { useDeleteService, useListUsers, getListUsersQueryKey, + useCreateUser, useUpdateUser, useDeleteUser, } from "@workspace/api-client-react"; @@ -74,11 +75,13 @@ export default function AdminPage() { const createService = useCreateService(); const updateService = useUpdateService(); const deleteService = useDeleteService(); + const createUser = useCreateUser(); const updateUser = useUpdateUser(); const deleteUser = useDeleteUser(); const [editingApp, setEditingApp] = useState<{ id?: number; form: AppForm } | null>(null); const [editingService, setEditingService] = useState<{ id?: number; form: ServiceForm } | null>(null); + const [newUserForm, setNewUserForm] = useState<{ username: string; email: string; password: string } | null>(null); const emptyAppForm: AppForm = { nameAr: "", nameEn: "", slug: "", iconName: "Grid2X2", route: "/", color: "#6366f1", sortOrder: 0 }; const emptyServiceForm: ServiceForm = { nameAr: "", nameEn: "", descriptionAr: "", descriptionEn: "", price: "0.00", isAvailable: true }; @@ -137,6 +140,20 @@ export default function AdminPage() { } }; + const handleCreateUser = () => { + if (!newUserForm) return; + createUser.mutate( + { data: { username: newUserForm.username, email: newUserForm.email, password: newUserForm.password } }, + { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: getListUsersQueryKey() }); + setNewUserForm(null); + toast({ title: t("common.success") }); + }, + }, + ); + }; + if (!user || !isAdmin) return null; return ( @@ -158,15 +175,24 @@ export default function AdminPage() { {/* Edit App Modal */} {editingApp && (
-
+

{editingApp.id ? t("admin.editApp") : t("admin.addApp")}

- {(["nameAr", "nameEn", "slug", "iconName", "route", "color"] as const).map((field) => ( + {( + [ + { field: "nameAr", label: t("admin.appNameAr") }, + { field: "nameEn", label: t("admin.appNameEn") }, + { field: "slug", label: t("admin.appSlug") }, + { field: "iconName", label: t("admin.appIcon") }, + { field: "route", label: t("admin.appRoute") }, + { field: "color", label: t("admin.appColor") }, + ] as { field: keyof AppForm; label: string }[] + ).map(({ field, label }) => (
- + setEditingApp({ ...editingApp, form: { ...editingApp.form, [field]: e.target.value } })} className="bg-white/10 border-white/20" /> @@ -183,15 +209,23 @@ export default function AdminPage() { {/* Edit Service Modal */} {editingService && (
-
+

{editingService.id ? t("admin.editService") : t("admin.addService")}

- {(["nameAr", "nameEn", "descriptionAr", "descriptionEn", "price"] as const).map((field) => ( + {( + [ + { field: "nameAr", label: t("admin.serviceNameAr") }, + { field: "nameEn", label: t("admin.serviceNameEn") }, + { field: "descriptionAr", label: t("admin.serviceDescriptionAr") }, + { field: "descriptionEn", label: t("admin.serviceDescriptionEn") }, + { field: "price", label: t("admin.servicePrice") }, + ] as { field: keyof ServiceForm; label: string }[] + ).map(({ field, label }) => (
- + setEditingService({ ...editingService, form: { ...editingService.form, [field]: e.target.value } })} className="bg-white/10 border-white/20" /> @@ -212,6 +246,45 @@ export default function AdminPage() {
)} + {/* Create User Modal */} + {newUserForm && ( +
+
+

{t("admin.addUser")}

+
+ + setNewUserForm({ ...newUserForm, username: e.target.value })} + className="bg-white/10 border-white/20" + /> +
+
+ + setNewUserForm({ ...newUserForm, email: e.target.value })} + className="bg-white/10 border-white/20" + /> +
+
+ + setNewUserForm({ ...newUserForm, password: e.target.value })} + className="bg-white/10 border-white/20" + /> +
+
+ + +
+
+
+ )} +
@@ -349,6 +422,14 @@ export default function AdminPage() { {/* Users Tab */} + {users?.map((u) => (
diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index b1c62bff..578a5693 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -2231,6 +2231,92 @@ export function useListUsers< return { ...query, queryKey: queryOptions.queryKey }; } +/** + * @summary Create a user (admin) + */ +export const getCreateUserUrl = () => { + return `/api/users`; +}; + +export const createUser = async ( + registerBody: RegisterBody, + options?: RequestInit, +): Promise => { + return customFetch(getCreateUserUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(registerBody), + }); +}; + +export const getCreateUserMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ["createUser"]; + 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>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return createUser(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type CreateUserMutationResult = NonNullable< + Awaited> +>; +export type CreateUserMutationBody = BodyType; +export type CreateUserMutationError = ErrorType; + +/** + * @summary Create a user (admin) + */ +export const useCreateUser = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getCreateUserMutationOptions(options)); +}; + /** * @summary Get user by ID (admin) */ diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index f78bafc3..060529ad 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -521,6 +521,26 @@ paths: items: $ref: "#/components/schemas/UserProfile" + post: + operationId: createUser + tags: [users] + summary: Create a user (admin) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RegisterBody" + responses: + "201": + description: Created user + content: + application/json: + schema: + $ref: "#/components/schemas/UserProfile" + "409": + description: Username already taken + /users/{id}: get: operationId: getUser diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index a5c935a7..0c3a0a7c 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -521,6 +521,22 @@ export const ListUsersResponseItem = zod.object({ }); export const ListUsersResponse = zod.array(ListUsersResponseItem); +/** + * @summary Create a user (admin) + */ +export const createUserBodyPreferredLanguageDefault = `ar`; + +export const CreateUserBody = zod.object({ + username: zod.string(), + email: zod.string(), + password: zod.string(), + displayNameAr: zod.string().nullish(), + displayNameEn: zod.string().nullish(), + preferredLanguage: zod + .string() + .default(createUserBodyPreferredLanguageDefault), +}); + /** * @summary Get user by ID (admin) */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e40ec795..ae80599b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -669,6 +669,9 @@ importers: '@workspace/db': specifier: workspace:* version: link:../lib/db + drizzle-orm: + specifier: 'catalog:' + version: 0.45.2(@types/pg@8.18.0)(pg@8.20.0) devDependencies: '@types/bcryptjs': specifier: ^3.0.0 diff --git a/scripts/package.json b/scripts/package.json index 43b0a6e3..0c563c71 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -9,7 +9,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@workspace/db": "workspace:*" + "@workspace/db": "workspace:*", + "drizzle-orm": "catalog:" }, "devDependencies": { "@types/bcryptjs": "^3.0.0",