fix: resolve final code review rejections — users CRUD, i18n, typecheck

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)
This commit is contained in:
riyadhafraa
2026-04-20 10:02:09 +00:00
parent b5a24b9c74
commit c752d4ba83
8 changed files with 238 additions and 9 deletions
+86
View File
@@ -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<UserProfile> => {
return customFetch<UserProfile>(getCreateUserUrl(), {
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(registerBody),
});
};
export const getCreateUserMutationOptions = <
TError = ErrorType<void>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data: BodyType<RegisterBody> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data: BodyType<RegisterBody> },
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<ReturnType<typeof createUser>>,
{ data: BodyType<RegisterBody> }
> = (props) => {
const { data } = props ?? {};
return createUser(data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type CreateUserMutationResult = NonNullable<
Awaited<ReturnType<typeof createUser>>
>;
export type CreateUserMutationBody = BodyType<RegisterBody>;
export type CreateUserMutationError = ErrorType<void>;
/**
* @summary Create a user (admin)
*/
export const useCreateUser = <
TError = ErrorType<void>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data: BodyType<RegisterBody> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationResult<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data: BodyType<RegisterBody> },
TContext
> => {
return useMutation(getCreateUserMutationOptions(options));
};
/**
* @summary Get user by ID (admin)
*/
+20
View File
@@ -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
+16
View File
@@ -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)
*/