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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user