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",