refactor: migrate direct fetch calls to API handler pattern

Replace raw fetch() in settings, share-key, and import-shared with
centralized API handlers via src/lib/api.ts. Also auto-create the data/
directory on startup so the SQLite db path is always valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Varun
2026-04-03 19:34:09 +05:30
co-authored by Claude Sonnet 4.6
parent 362f6d861b
commit eafb44a4e9
9 changed files with 91 additions and 62 deletions
BIN
View File
Binary file not shown.
+6 -7
View File
@@ -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<string | null>(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);
};
+12 -1
View File
@@ -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";
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";
+3
View File
@@ -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 });
+15
View File
@@ -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);
};
+14
View File
@@ -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));
};
+10
View File
@@ -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);
};
+18 -41
View File
@@ -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 ?? [];
+13 -13
View File
@@ -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);
};