Add editable site name and finish service image upload

User asked to remove the hardcoded "TeaBoy" branding and let admins
change the system name. Also completes the in-progress service image
upload work via App Storage.

Changes:
- New app_settings table (single row id=1) with siteNameAr/siteNameEn
- New /settings endpoints: GET (public) + PATCH (admin)
- Atomic ensureSettingsRow via INSERT ... ON CONFLICT DO NOTHING
  to avoid race conditions
- New SiteSettingsPanel tab in admin page (Arabic + English inputs)
- New useAppName hook reads settings, updates document.title, falls
  back to defaults while loading
- Login + register pages now display the dynamic site name
- Service image upload (App Storage) wired via useUpload + presigned
  GCS URL flow; admin component ServiceImageUploader
- Storage routes: /storage/uploads/request-url and /storage/objects/*
  now require auth (closes previously-open endpoints flagged by review)
- Added AppSettings/UpdateAppSettingsBody + storage schemas to
  openapi.yaml; regenerated client and zod
- Exposed UploadResponse from @workspace/object-storage-web; added
  composite:true so it can be referenced by teaboy-os tsconfig

Validation: typechecks pass for api-server and teaboy-os; settings GET
returns row; upload URL endpoint returns 401 without auth.
This commit is contained in:
Riyadh
2026-04-20 11:06:15 +00:00
parent 0efc4032be
commit 9d353ede69
19 changed files with 446 additions and 29 deletions
@@ -257,6 +257,25 @@ export interface Notification {
createdAt: string;
}
export interface AppSettings {
siteNameAr: string;
siteNameEn: string;
updatedAt?: string;
}
export interface UpdateAppSettingsBody {
/**
* @minLength 1
* @maxLength 200
*/
siteNameAr?: string;
/**
* @minLength 1
* @maxLength 200
*/
siteNameEn?: string;
}
export interface RequestUploadUrlBody {
/** @minLength 1 */
name: string;
+163
View File
@@ -18,6 +18,7 @@ import type {
import type {
App,
AppSettings,
AuthUser,
ConversationWithDetails,
CreateAppBody,
@@ -37,6 +38,7 @@ import type {
ServiceCategory,
SuccessResponse,
UpdateAppBody,
UpdateAppSettingsBody,
UpdateLanguageBody,
UpdateServiceBody,
UpdateUserBody,
@@ -926,6 +928,167 @@ export const useDeleteApp = <
return useMutation(getDeleteAppMutationOptions(options));
};
/**
* @summary Get application settings (public)
*/
export const getGetAppSettingsUrl = () => {
return `/api/settings`;
};
export const getAppSettings = async (
options?: RequestInit,
): Promise<AppSettings> => {
return customFetch<AppSettings>(getGetAppSettingsUrl(), {
...options,
method: "GET",
});
};
export const getGetAppSettingsQueryKey = () => {
return [`/api/settings`] as const;
};
export const getGetAppSettingsQueryOptions = <
TData = Awaited<ReturnType<typeof getAppSettings>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAppSettings>>,
TError,
TData
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAppSettingsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAppSettings>>> = ({
signal,
}) => getAppSettings({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getAppSettings>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAppSettingsQueryResult = NonNullable<
Awaited<ReturnType<typeof getAppSettings>>
>;
export type GetAppSettingsQueryError = ErrorType<unknown>;
/**
* @summary Get application settings (public)
*/
export function useGetAppSettings<
TData = Awaited<ReturnType<typeof getAppSettings>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAppSettings>>,
TError,
TData
>;
request?: SecondParameter<typeof customFetch>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAppSettingsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Update application settings (admin)
*/
export const getUpdateAppSettingsUrl = () => {
return `/api/settings`;
};
export const updateAppSettings = async (
updateAppSettingsBody: UpdateAppSettingsBody,
options?: RequestInit,
): Promise<AppSettings> => {
return customFetch<AppSettings>(getUpdateAppSettingsUrl(), {
...options,
method: "PATCH",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(updateAppSettingsBody),
});
};
export const getUpdateAppSettingsMutationOptions = <
TError = ErrorType<ErrorResponse>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAppSettings>>,
TError,
{ data: BodyType<UpdateAppSettingsBody> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateAppSettings>>,
TError,
{ data: BodyType<UpdateAppSettingsBody> },
TContext
> => {
const mutationKey = ["updateAppSettings"];
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 updateAppSettings>>,
{ data: BodyType<UpdateAppSettingsBody> }
> = (props) => {
const { data } = props ?? {};
return updateAppSettings(data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateAppSettingsMutationResult = NonNullable<
Awaited<ReturnType<typeof updateAppSettings>>
>;
export type UpdateAppSettingsMutationBody = BodyType<UpdateAppSettingsBody>;
export type UpdateAppSettingsMutationError = ErrorType<ErrorResponse>;
/**
* @summary Update application settings (admin)
*/
export const useUpdateAppSettings = <
TError = ErrorType<ErrorResponse>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAppSettings>>,
TError,
{ data: BodyType<UpdateAppSettingsBody> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateAppSettings>>,
TError,
{ data: BodyType<UpdateAppSettingsBody> },
TContext
> => {
return useMutation(getUpdateAppSettingsMutationOptions(options));
};
/**
* @summary Request a presigned URL for file upload
*/