From 9d353ede69f8999be2fc0dad0b297b7e3586e150 Mon Sep 17 00:00:00 2001 From: Riyadh Date: Mon, 20 Apr 2026 11:06:15 +0000 Subject: [PATCH] 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. --- artifacts/api-server/src/lib/objectStorage.ts | 4 +- artifacts/api-server/src/routes/index.ts | 2 + artifacts/api-server/src/routes/settings.ts | 42 +++++ artifacts/api-server/src/routes/storage.ts | 21 +-- .../teaboy-os/src/hooks/use-app-settings.ts | 22 +++ artifacts/teaboy-os/src/locales/ar.json | 5 +- artifacts/teaboy-os/src/locales/en.json | 5 +- artifacts/teaboy-os/src/pages/admin.tsx | 66 ++++++- artifacts/teaboy-os/src/pages/login.tsx | 4 +- artifacts/teaboy-os/src/pages/register.tsx | 4 +- .../src/generated/api.schemas.ts | 19 ++ lib/api-client-react/src/generated/api.ts | 163 ++++++++++++++++++ lib/api-spec/openapi.yaml | 61 +++++++ lib/api-zod/src/generated/api.ts | 35 ++++ lib/db/src/schema/index.ts | 1 + lib/db/src/schema/settings.ts | 16 ++ lib/object-storage-web/src/index.ts | 2 +- lib/object-storage-web/src/use-upload.ts | 2 +- lib/object-storage-web/tsconfig.json | 1 + 19 files changed, 446 insertions(+), 29 deletions(-) create mode 100644 artifacts/api-server/src/routes/settings.ts create mode 100644 artifacts/teaboy-os/src/hooks/use-app-settings.ts create mode 100644 lib/db/src/schema/settings.ts diff --git a/artifacts/api-server/src/lib/objectStorage.ts b/artifacts/api-server/src/lib/objectStorage.ts index f6f39b14..bcc3857f 100644 --- a/artifacts/api-server/src/lib/objectStorage.ts +++ b/artifacts/api-server/src/lib/objectStorage.ts @@ -262,6 +262,6 @@ async function signObjectURL({ ); } - const { signed_url: signedURL } = await response.json(); - return signedURL; + const data = (await response.json()) as { signed_url: string }; + return data.signed_url; } diff --git a/artifacts/api-server/src/routes/index.ts b/artifacts/api-server/src/routes/index.ts index 713d7d2b..cc9b7fc7 100644 --- a/artifacts/api-server/src/routes/index.ts +++ b/artifacts/api-server/src/routes/index.ts @@ -8,6 +8,7 @@ import notificationsRouter from "./notifications"; import usersRouter from "./users"; import statsRouter from "./stats"; import storageRouter from "./storage"; +import settingsRouter from "./settings"; const router: IRouter = Router(); @@ -20,5 +21,6 @@ router.use(notificationsRouter); router.use(usersRouter); router.use(statsRouter); router.use(storageRouter); +router.use(settingsRouter); export default router; diff --git a/artifacts/api-server/src/routes/settings.ts b/artifacts/api-server/src/routes/settings.ts new file mode 100644 index 00000000..35ea4d95 --- /dev/null +++ b/artifacts/api-server/src/routes/settings.ts @@ -0,0 +1,42 @@ +import { Router, type IRouter } from "express"; +import { eq } from "drizzle-orm"; +import { db } from "@workspace/db"; +import { appSettingsTable } from "@workspace/db"; +import { requireAdmin } from "../middlewares/auth"; +import { UpdateAppSettingsBody } from "@workspace/api-zod"; + +const router: IRouter = Router(); + +async function ensureSettingsRow() { + await db + .insert(appSettingsTable) + .values({ id: 1 }) + .onConflictDoNothing({ target: appSettingsTable.id }); +} + +async function readSettings() { + await ensureSettingsRow(); + const [row] = await db.select().from(appSettingsTable).where(eq(appSettingsTable.id, 1)); + return row; +} + +router.get("/settings", async (_req, res): Promise => { + res.json(await readSettings()); +}); + +router.patch("/settings", requireAdmin, async (req, res): Promise => { + const parsed = UpdateAppSettingsBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + await ensureSettingsRow(); + const [updated] = await db + .update(appSettingsTable) + .set(parsed.data) + .where(eq(appSettingsTable.id, 1)) + .returning(); + res.json(updated); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/storage.ts b/artifacts/api-server/src/routes/storage.ts index a6a931ec..e9ababfd 100644 --- a/artifacts/api-server/src/routes/storage.ts +++ b/artifacts/api-server/src/routes/storage.ts @@ -5,7 +5,7 @@ import { RequestUploadUrlResponse, } from "@workspace/api-zod"; import { ObjectStorageService, ObjectNotFoundError } from "../lib/objectStorage"; -import { ObjectPermission } from "../lib/objectAcl"; +import { requireAuth } from "../middlewares/auth"; const router: IRouter = Router(); const objectStorageService = new ObjectStorageService(); @@ -17,7 +17,7 @@ const objectStorageService = new ObjectStorageService(); * The client sends JSON metadata (name, size, contentType) — NOT the file. * Then uploads the file directly to the returned presigned URL. */ -router.post("/storage/uploads/request-url", async (req: Request, res: Response) => { +router.post("/storage/uploads/request-url", requireAuth, async (req: Request, res: Response) => { const parsed = RequestUploadUrlBody.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: "Missing or invalid required fields" }); @@ -84,28 +84,13 @@ router.get("/storage/public-objects/*filePath", async (req: Request, res: Respon * These are served from a separate path from /public-objects and can optionally * be protected with authentication or ACL checks based on the use case. */ -router.get("/storage/objects/*path", async (req: Request, res: Response) => { +router.get("/storage/objects/*path", requireAuth, async (req: Request, res: Response) => { try { const raw = req.params.path; const wildcardPath = Array.isArray(raw) ? raw.join("/") : raw; const objectPath = `/objects/${wildcardPath}`; const objectFile = await objectStorageService.getObjectEntityFile(objectPath); - // --- Protected route example (uncomment when using replit-auth) --- - // if (!req.isAuthenticated()) { - // res.status(401).json({ error: "Unauthorized" }); - // return; - // } - // const canAccess = await objectStorageService.canAccessObjectEntity({ - // userId: req.user.id, - // objectFile, - // requestedPermission: ObjectPermission.READ, - // }); - // if (!canAccess) { - // res.status(403).json({ error: "Forbidden" }); - // return; - // } - const response = await objectStorageService.downloadObject(objectFile); res.status(response.status); diff --git a/artifacts/teaboy-os/src/hooks/use-app-settings.ts b/artifacts/teaboy-os/src/hooks/use-app-settings.ts new file mode 100644 index 00000000..970c39cd --- /dev/null +++ b/artifacts/teaboy-os/src/hooks/use-app-settings.ts @@ -0,0 +1,22 @@ +import { useGetAppSettings, getGetAppSettingsQueryKey } from "@workspace/api-client-react"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +export function useAppSettings() { + const query = useGetAppSettings({ query: { queryKey: getGetAppSettingsQueryKey() } }); + return query; +} + +export function useAppName() { + const { i18n } = useTranslation(); + const { data } = useAppSettings(); + const isAr = i18n.language === "ar"; + const fallback = isAr ? "نظام TeaBoy" : "TeaBoy OS"; + const name = data ? (isAr ? data.siteNameAr : data.siteNameEn) : fallback; + useEffect(() => { + if (typeof document !== "undefined" && data) { + document.title = name; + } + }, [name, data]); + return name; +} diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 82a021d8..446a0a0d 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -90,7 +90,10 @@ "email": "البريد الإلكتروني", "password": "كلمة المرور", "roles": "الصلاحيات", - "active": "نشط" + "active": "نشط", + "siteSettings": "إعدادات الموقع", + "siteNameAr": "اسم الموقع (عربي)", + "siteNameEn": "اسم الموقع (إنجليزي)" }, "notFound": { "title": "404 — الصفحة غير موجودة", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index 4cdf4a34..bb71d224 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -90,7 +90,10 @@ "email": "Email", "password": "Password", "roles": "Roles", - "active": "Active" + "active": "Active", + "siteSettings": "Site Settings", + "siteNameAr": "Site Name (Arabic)", + "siteNameEn": "Site Name (English)" }, "notFound": { "title": "404 — Page Not Found", diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx index c2b690a3..41c3883b 100644 --- a/artifacts/teaboy-os/src/pages/admin.tsx +++ b/artifacts/teaboy-os/src/pages/admin.tsx @@ -17,10 +17,13 @@ import { useCreateUser, useUpdateUser, useDeleteUser, + useGetAppSettings, + getGetAppSettingsQueryKey, + useUpdateAppSettings, } from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; import { useAuth } from "@/contexts/AuthContext"; -import { useUpload } from "@workspace/object-storage-web"; +import { useUpload, type UploadResponse } from "@workspace/object-storage-web"; import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -306,6 +309,7 @@ export default function AdminPage() { {t("admin.manageApps")} {t("admin.manageServices")} {t("admin.manageUsers")} + {t("admin.siteSettings")} {/* Apps Tab */} @@ -488,12 +492,68 @@ export default function AdminPage() { ))} + + + + ); } +function SiteSettingsPanel() { + const { t } = useTranslation(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const { data } = useGetAppSettings({ query: { queryKey: getGetAppSettingsQueryKey() } }); + const update = useUpdateAppSettings(); + const [form, setForm] = useState({ siteNameAr: "", siteNameEn: "" }); + + useEffect(() => { + if (data) setForm({ siteNameAr: data.siteNameAr, siteNameEn: data.siteNameEn }); + }, [data]); + + const handleSave = () => { + update.mutate( + { data: form }, + { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: getGetAppSettingsQueryKey() }); + toast({ title: t("common.success") }); + }, + onError: () => toast({ title: t("common.error"), variant: "destructive" }), + }, + ); + }; + + return ( +
+
+ + setForm({ ...form, siteNameAr: e.target.value })} + className="bg-white/70 border-slate-200" + dir="rtl" + /> +
+
+ + setForm({ ...form, siteNameEn: e.target.value })} + className="bg-white/70 border-slate-200" + dir="ltr" + /> +
+ +
+ ); +} + function ServiceImageUploader({ value, onChange, @@ -504,10 +564,10 @@ function ServiceImageUploader({ const { t } = useTranslation(); const { toast } = useToast(); const { uploadFile, isUploading, progress } = useUpload({ - onSuccess: (resp) => { + onSuccess: (resp: UploadResponse) => { onChange(`/api/storage${resp.objectPath}`); }, - onError: (err) => { + onError: (err: Error) => { toast({ title: t("admin.uploadFailed"), description: err.message, variant: "destructive" }); }, }); diff --git a/artifacts/teaboy-os/src/pages/login.tsx b/artifacts/teaboy-os/src/pages/login.tsx index 18e740a8..ecd1297c 100644 --- a/artifacts/teaboy-os/src/pages/login.tsx +++ b/artifacts/teaboy-os/src/pages/login.tsx @@ -8,10 +8,12 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useToast } from "@/hooks/use-toast"; +import { useAppName } from "@/hooks/use-app-settings"; import i18n from "@/i18n"; export default function LoginPage() { const { t } = useTranslation(); + const appName = useAppName(); const [, setLocation] = useLocation(); const { toast } = useToast(); const queryClient = useQueryClient(); @@ -46,7 +48,7 @@ export default function LoginPage() {
-
{t("common.appName")}
+
{appName}

{t("auth.welcomeBack")}

diff --git a/artifacts/teaboy-os/src/pages/register.tsx b/artifacts/teaboy-os/src/pages/register.tsx index 60ef305b..c50f8206 100644 --- a/artifacts/teaboy-os/src/pages/register.tsx +++ b/artifacts/teaboy-os/src/pages/register.tsx @@ -8,10 +8,12 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useToast } from "@/hooks/use-toast"; +import { useAppName } from "@/hooks/use-app-settings"; import i18n from "@/i18n"; export default function RegisterPage() { const { t } = useTranslation(); + const appName = useAppName(); const [, setLocation] = useLocation(); const { toast } = useToast(); const queryClient = useQueryClient(); @@ -48,7 +50,7 @@ export default function RegisterPage() {
-
{t("common.appName")}
+
{appName}

{t("auth.createAccount")}

diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index 15193de3..7d9e0ee9 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -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; diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index a7c7985a..da736e1e 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -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 => { + return customFetch(getGetAppSettingsUrl(), { + ...options, + method: "GET", + }); +}; + +export const getGetAppSettingsQueryKey = () => { + return [`/api/settings`] as const; +}; + +export const getGetAppSettingsQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetAppSettingsQueryKey(); + + const queryFn: QueryFunction>> = ({ + signal, + }) => getAppSettings({ signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAppSettingsQueryResult = NonNullable< + Awaited> +>; +export type GetAppSettingsQueryError = ErrorType; + +/** + * @summary Get application settings (public) + */ + +export function useGetAppSettings< + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; +}): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAppSettingsQueryOptions(options); + + const query = useQuery(queryOptions) as UseQueryResult & { + 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 => { + return customFetch(getUpdateAppSettingsUrl(), { + ...options, + method: "PATCH", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(updateAppSettingsBody), + }); +}; + +export const getUpdateAppSettingsMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + 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>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return updateAppSettings(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type UpdateAppSettingsMutationResult = NonNullable< + Awaited> +>; +export type UpdateAppSettingsMutationBody = BodyType; +export type UpdateAppSettingsMutationError = ErrorType; + +/** + * @summary Update application settings (admin) + */ +export const useUpdateAppSettings = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getUpdateAppSettingsMutationOptions(options)); +}; + /** * @summary Request a presigned URL for file upload */ diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 85434378..f3da45d3 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -236,6 +236,43 @@ paths: "204": description: Deleted + # Settings + /settings: + get: + operationId: getAppSettings + tags: [settings] + summary: Get application settings (public) + responses: + "200": + description: Current settings + content: + application/json: + schema: + $ref: "#/components/schemas/AppSettings" + patch: + operationId: updateAppSettings + tags: [settings] + summary: Update application settings (admin) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateAppSettingsBody" + responses: + "200": + description: Updated settings + content: + application/json: + schema: + $ref: "#/components/schemas/AppSettings" + "400": + description: Invalid input + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + # Storage /storage/uploads/request-url: post: @@ -1140,6 +1177,30 @@ components: - isRead - createdAt + AppSettings: + type: object + required: [siteNameAr, siteNameEn] + properties: + siteNameAr: + type: string + siteNameEn: + type: string + updatedAt: + type: string + format: date-time + + UpdateAppSettingsBody: + type: object + properties: + siteNameAr: + type: string + minLength: 1 + maxLength: 200 + siteNameEn: + type: string + minLength: 1 + maxLength: 200 + RequestUploadUrlBody: type: object required: [name, size, contentType] diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 40bfbbbb..d23a6e14 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -197,6 +197,41 @@ export const DeleteAppParams = zod.object({ id: zod.coerce.number(), }); +/** + * @summary Get application settings (public) + */ +export const GetAppSettingsResponse = zod.object({ + siteNameAr: zod.string(), + siteNameEn: zod.string(), + updatedAt: zod.coerce.date().optional(), +}); + +/** + * @summary Update application settings (admin) + */ +export const updateAppSettingsBodySiteNameArMax = 200; + +export const updateAppSettingsBodySiteNameEnMax = 200; + +export const UpdateAppSettingsBody = zod.object({ + siteNameAr: zod + .string() + .min(1) + .max(updateAppSettingsBodySiteNameArMax) + .optional(), + siteNameEn: zod + .string() + .min(1) + .max(updateAppSettingsBodySiteNameEnMax) + .optional(), +}); + +export const UpdateAppSettingsResponse = zod.object({ + siteNameAr: zod.string(), + siteNameEn: zod.string(), + updatedAt: zod.coerce.date().optional(), +}); + /** * @summary Request a presigned URL for file upload */ diff --git a/lib/db/src/schema/index.ts b/lib/db/src/schema/index.ts index b5877ca3..8ff8b90e 100644 --- a/lib/db/src/schema/index.ts +++ b/lib/db/src/schema/index.ts @@ -4,3 +4,4 @@ export * from "./apps"; export * from "./services"; export * from "./conversations"; export * from "./notifications"; +export * from "./settings"; diff --git a/lib/db/src/schema/settings.ts b/lib/db/src/schema/settings.ts new file mode 100644 index 00000000..ebba87fc --- /dev/null +++ b/lib/db/src/schema/settings.ts @@ -0,0 +1,16 @@ +import { pgTable, varchar, integer, timestamp } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-zod"; +import { z } from "zod/v4"; + +export const appSettingsTable = pgTable("app_settings", { + id: integer("id").primaryKey().default(1), + siteNameAr: varchar("site_name_ar", { length: 200 }).notNull().default("نظام TeaBoy"), + siteNameEn: varchar("site_name_en", { length: 200 }).notNull().default("TeaBoy OS"), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()), +}); + +export const updateAppSettingsSchema = createInsertSchema(appSettingsTable) + .omit({ id: true, updatedAt: true }) + .partial(); +export type AppSettings = typeof appSettingsTable.$inferSelect; +export type UpdateAppSettings = z.infer; diff --git a/lib/object-storage-web/src/index.ts b/lib/object-storage-web/src/index.ts index 1baa5e58..898369e2 100644 --- a/lib/object-storage-web/src/index.ts +++ b/lib/object-storage-web/src/index.ts @@ -1,2 +1,2 @@ export { ObjectUploader } from "./ObjectUploader"; -export { useUpload } from "./use-upload"; +export { useUpload, type UploadResponse } from "./use-upload"; diff --git a/lib/object-storage-web/src/use-upload.ts b/lib/object-storage-web/src/use-upload.ts index 9affb301..7ec850e4 100644 --- a/lib/object-storage-web/src/use-upload.ts +++ b/lib/object-storage-web/src/use-upload.ts @@ -7,7 +7,7 @@ interface UploadMetadata { contentType: string; } -interface UploadResponse { +export interface UploadResponse { uploadURL: string; objectPath: string; metadata: UploadMetadata; diff --git a/lib/object-storage-web/tsconfig.json b/lib/object-storage-web/tsconfig.json index 4d5196ef..be865b31 100644 --- a/lib/object-storage-web/tsconfig.json +++ b/lib/object-storage-web/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "composite": true, "outDir": "dist", "rootDir": "src", "jsx": "react-jsx",