diff --git a/bun.lockb b/bun.lockb index f6133ad..f521ce9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/components/shared/ShareAssetsTrigger.tsx b/src/components/shared/ShareAssetsTrigger.tsx index 8b90d36..0e3756a 100644 --- a/src/components/shared/ShareAssetsTrigger.tsx +++ b/src/components/shared/ShareAssetsTrigger.tsx @@ -3,6 +3,7 @@ import { Dialog, DialogTitle, DialogContent, DialogHeader, DialogTrigger, Dialog import { Button, ButtonProps } from '../ui/button'; import { ShareLinkFilters } from '@/types/shareLink'; import { generateShareLink } from '@/handlers/api/shareLink.handler'; +import { createShareKey, getShareKey } from '@/handlers/api/share-key.handler'; import { Input } from '../ui/input'; import { Label } from '@radix-ui/react-label'; @@ -28,8 +29,7 @@ export default function ShareAssetsTrigger({ filters, buttonProps }: ShareAssets const [keyError, setKeyError] = useState(null); const checkShareKey = async () => { - const res = await fetch('/api/share-key'); - const data = await res.json(); + const data = await getShareKey(); setEndpointConfigured(data.endpointConfigured); setShareKeyExists(data.exists); }; @@ -37,12 +37,11 @@ export default function ShareAssetsTrigger({ filters, buttonProps }: ShareAssets const handleCreateKey = async () => { setCreatingKey(true); setKeyError(null); - const res = await fetch('/api/share-key', { method: 'POST' }); - if (res.ok) { + try { + await createShareKey(); setShareKeyExists(true); - } else { - const data = await res.json(); - setKeyError(data.message ?? 'Failed to create share key'); + } catch (err: any) { + setKeyError(err.message ?? 'Failed to create share key'); } setCreatingKey(false); }; diff --git a/src/config/routes.ts b/src/config/routes.ts index 5011619..87e7d32 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -83,4 +83,15 @@ export const SHARE_LINK_PEOPLE_PATH = (token: string) => BASE_API_ENDPOINT + "/s export const SHARE_LINK_DOWNLOAD_PATH = (token: string) => BASE_API_ENDPOINT + "/share-link/" + token + "/download"; export const DOWNLOAD_ASSETS_PATH = BASE_PROXY_ENDPOINT + "/download/archive"; -export const LIST_DUPLICATES_PATH = BASE_PROXY_ENDPOINT + "/duplicates"; \ No newline at end of file +export const LIST_DUPLICATES_PATH = BASE_PROXY_ENDPOINT + "/duplicates"; + +// Settings +export const SETTINGS_API_KEYS_PATH = BASE_API_ENDPOINT + "/settings/api-keys"; +export const SETTINGS_API_KEY_PATH = (purpose: string) => BASE_API_ENDPOINT + "/settings/api-keys/" + purpose; + +// Share Key +export const SHARE_KEY_PATH = BASE_API_ENDPOINT + "/share-key"; + +// Import Shared +export const IMPORT_SHARED_PATH = BASE_API_ENDPOINT + "/import-shared"; +export const IMPORT_SHARED_UPLOAD_ALL_PATH = BASE_API_ENDPOINT + "/import-shared/upload-all"; \ No newline at end of file diff --git a/src/db/index.ts b/src/db/index.ts index ea91391..993fd62 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,12 +1,15 @@ import { createClient } from "@libsql/client"; import { drizzle } from "drizzle-orm/libsql"; import { migrate } from "drizzle-orm/libsql/migrator"; +import fs from "fs"; import path from "path"; import * as schema from "./schema"; const DB_PATH = process.env.APP_DB_PATH || path.join(process.cwd(), "data", "app.db"); +fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }); + const client = createClient({ url: `file:${DB_PATH}` }); export const appDb = drizzle(client, { schema }); diff --git a/src/handlers/api/import-shared.handler.ts b/src/handlers/api/import-shared.handler.ts new file mode 100644 index 0000000..648d86e --- /dev/null +++ b/src/handlers/api/import-shared.handler.ts @@ -0,0 +1,15 @@ +import { IMPORT_SHARED_PATH, IMPORT_SHARED_UPLOAD_ALL_PATH } from "@/config/routes"; +import API from "@/lib/api"; + +export const importShared = async (link: string) => { + return API.post(IMPORT_SHARED_PATH, { link }); +}; + +export const importSharedUploadAll = async (params: { + origin: string; + key: string; + assets: object[]; + albumOptions: object; +}) => { + return API.post(IMPORT_SHARED_UPLOAD_ALL_PATH, params); +}; diff --git a/src/handlers/api/settings.handler.ts b/src/handlers/api/settings.handler.ts new file mode 100644 index 0000000..494f818 --- /dev/null +++ b/src/handlers/api/settings.handler.ts @@ -0,0 +1,14 @@ +import { SETTINGS_API_KEYS_PATH, SETTINGS_API_KEY_PATH } from "@/config/routes"; +import API from "@/lib/api"; + +export const getApiKeys = async () => { + return API.get(SETTINGS_API_KEYS_PATH); +}; + +export const deleteApiKey = async (purpose: string) => { + return API.delete(SETTINGS_API_KEY_PATH(purpose)); +}; + +export const regenerateApiKey = async (purpose: string) => { + return API.put(SETTINGS_API_KEY_PATH(purpose)); +}; diff --git a/src/handlers/api/share-key.handler.ts b/src/handlers/api/share-key.handler.ts new file mode 100644 index 0000000..2fb8b8c --- /dev/null +++ b/src/handlers/api/share-key.handler.ts @@ -0,0 +1,10 @@ +import { SHARE_KEY_PATH } from "@/config/routes"; +import API from "@/lib/api"; + +export const getShareKey = async () => { + return API.get(SHARE_KEY_PATH); +}; + +export const createShareKey = async () => { + return API.post(SHARE_KEY_PATH); +}; diff --git a/src/pages/assets/import-shared.tsx b/src/pages/assets/import-shared.tsx index 86843f4..25cbefb 100644 --- a/src/pages/assets/import-shared.tsx +++ b/src/pages/assets/import-shared.tsx @@ -22,6 +22,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { listAlbums } from "@/handlers/api/album.handler"; +import { importShared, importSharedUploadAll } from "@/handlers/api/import-shared.handler"; import { IAlbum } from "@/types/album"; interface IAlbumContributorCount { @@ -200,27 +201,14 @@ export default function ImportSharedPage() { setLoading(true); try { - const response = await fetch("/api/import-shared", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ link: shareLink }), - }); - - if (!response.ok) { - const errorPayload = await response.json().catch(() => ({})); - throw new Error(errorPayload.error || "Failed to import shared album"); - } - - const payload: IImportSharedResponse = await response.json(); + const payload: IImportSharedResponse = await importShared(shareLink); setSharedData(payload); setSubmittedLink(payload.link); setUploadBanner(null); setImportAllLoading(false); setAlbumImportMode("album"); - } catch (err) { - const message = err instanceof Error ? err.message : "Unexpected error"; + } catch (err: any) { + const message = err.message ?? "Unexpected error"; setError(message); } finally { setLoading(false); @@ -309,33 +297,22 @@ export default function ImportSharedPage() { setUploadBanner(null); setImportAllLoading(true); try { - const response = await fetch("/api/import-shared/upload-all", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - origin: sharedData.origin, - key: sharedData.key, - assets: uploadableAssets.map((asset) => ({ - id: asset.id, - originalFileName: asset.originalFileName, - type: asset.type, - fileCreatedAt: asset.fileCreatedAt ?? null, - localDateTime: asset.localDateTime ?? null, - duration: asset.duration ?? null, - isFavorite: asset.isFavorite ?? false, - isArchived: asset.isArchived ?? false, - })), - albumOptions, - }), + const result = await importSharedUploadAll({ + origin: sharedData.origin, + key: sharedData.key, + assets: uploadableAssets.map((asset) => ({ + id: asset.id, + originalFileName: asset.originalFileName, + type: asset.type, + fileCreatedAt: asset.fileCreatedAt ?? null, + localDateTime: asset.localDateTime ?? null, + duration: asset.duration ?? null, + isFavorite: asset.isFavorite ?? false, + isArchived: asset.isArchived ?? false, + })), + albumOptions, }); - const result = await response.json().catch(() => ({})); - if (!response.ok) { - throw new Error(result.error || "Failed to import shared archive"); - } - const processedAssetIds: string[] = result.processedAssetIds ?? []; const skippedAssetIds: string[] = result.skippedAssetIds ?? []; const failed: { assetId: string; reason?: string }[] = result.failed ?? []; diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx index 65ae191..1e165d8 100644 --- a/src/pages/settings/index.tsx +++ b/src/pages/settings/index.tsx @@ -1,5 +1,6 @@ import PageLayout from "@/components/layouts/PageLayout"; import Header from "@/components/shared/Header"; +import { deleteApiKey, getApiKeys, regenerateApiKey } from "@/handlers/api/settings.handler"; import { AlertDialog } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -23,10 +24,11 @@ export default function SettingsPage() { const fetchKeys = async () => { setLoading(true); - const res = await fetch("/api/settings/api-keys"); - if (res.ok) { - const data = await res.json(); + try { + const data = await getApiKeys(); setKeys(data); + } catch { + // silently fail — keys list stays empty } setLoading(false); }; @@ -37,26 +39,24 @@ export default function SettingsPage() { const handleDelete = async (purpose: string) => { setDeleting(purpose); - const res = await fetch(`/api/settings/api-keys/${purpose}`, { method: "DELETE" }); - if (res.ok) { + try { + await deleteApiKey(purpose); setKeys((prev) => prev.filter((k) => k.purpose !== purpose)); toast.success("API key deleted"); - } else { - const data = await res.json(); - toast.error(data.message ?? "Failed to delete API key"); + } catch (err: any) { + toast.error(err.message ?? "Failed to delete API key"); } setDeleting(null); }; const handleRegenerate = async (purpose: string) => { setRegenerating(purpose); - const res = await fetch(`/api/settings/api-keys/${purpose}`, { method: "PUT" }); - if (res.ok) { + try { + await regenerateApiKey(purpose); await fetchKeys(); toast.success("API key regenerated"); - } else { - const data = await res.json(); - toast.error(data.message ?? "Failed to regenerate API key"); + } catch (err: any) { + toast.error(err.message ?? "Failed to regenerate API key"); } setRegenerating(null); };