From d9ba98f79ee80fb9ac1240e45ba5d3fcd34803b8 Mon Sep 17 00:00:00 2001 From: oladev2026-tech Date: Sat, 17 Oct 2026 10:02:16 +0100 Subject: [PATCH 1/4] Add security headers, image config and standalone output to next.config.ts --- frontend/next.config.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa3083..0aa712e8e 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,30 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", + poweredByHeader: false, + images: { + remotePatterns: [ + { protocol: "https", hostname: "**" }, + ], + }, + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { key: "X-Frame-Options", value: "DENY" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, + { + key: "Content-Security-Policy", + value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' http://localhost:6003 ws://localhost:6003 https://horizon-testnet.stellar.org https://soroban-testnet.stellar.org; font-src 'self' data:;", + }, + ], + }, + ]; + }, }; -export default nextConfig; +export default nextConfig; \ No newline at end of file From bf2754c6da7dcbe49e672addc3d149cf6d959e56 Mon Sep 17 00:00:00 2001 From: oladev2026-tech Date: Sat, 17 Oct 2026 10:02:20 +0100 Subject: [PATCH 2/4] Document single HTTP client convention --- frontend/lib/api/http-client.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 frontend/lib/api/http-client.ts diff --git a/frontend/lib/api/http-client.ts b/frontend/lib/api/http-client.ts new file mode 100644 index 000000000..d9a74a947 --- /dev/null +++ b/frontend/lib/api/http-client.ts @@ -0,0 +1,20 @@ +/** + * Single HTTP client for the frontend. + * + * All API requests should go through the `api` axios instance from `lib/api.ts`. + * Per-resource modules in `lib/api/*.ts` wrap specific endpoints. + * + * The legacy `apiClient.request` wrapper in `lib/api/client.ts` is deprecated; + * new code should use the resource modules directly. Raw `fetch('/api/...')` + * calls should be replaced with `api.get/post/patch/delete(...)` to ensure + * the correct base URL and auth token are attached. + * + * @example + * import { api } from '@/lib/api'; + * const { data } = await api.get('/assets'); + * + * @example + * import { assetApiClient } from '@/lib/api/assets'; + * const assets = await assetApiClient.getAssets(); + */ +export { api } from '../api'; \ No newline at end of file From fc15cf459d760777822e0011fae678a0de15356c Mon Sep 17 00:00:00 2001 From: oladev2026-tech Date: Sat, 17 Oct 2026 10:02:23 +0100 Subject: [PATCH 3/4] Add asset-photos API module for upload, delete and set-primary --- frontend/lib/api/asset-photos.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 frontend/lib/api/asset-photos.ts diff --git a/frontend/lib/api/asset-photos.ts b/frontend/lib/api/asset-photos.ts new file mode 100644 index 000000000..f1c379fcd --- /dev/null +++ b/frontend/lib/api/asset-photos.ts @@ -0,0 +1,27 @@ +import { api } from '../api'; + +export interface AssetPhoto { + id: string; + url: string; + isPrimary: boolean; + createdAt: string; +} + +export const assetPhotosApiClient = { + list: (assetId: string): Promise => + api.get(`/assets/${assetId}/photos`).then((r) => r.data), + + upload: (assetId: string, file: File): Promise => { + const form = new FormData(); + form.append('file', file); + return api.post(`/assets/${assetId}/photos`, form, { + headers: { 'Content-Type': 'multipart/form-data' }, + }).then((r) => r.data); + }, + + delete: (assetId: string, photoId: string): Promise => + api.delete(`/assets/${assetId}/photos/${photoId}`).then(() => undefined), + + setPrimary: (assetId: string, photoId: string): Promise => + api.patch(`/assets/${assetId}/photos/${photoId}/primary`).then((r) => r.data), +}; \ No newline at end of file From 7af7cb81b61849730bb162865f5e9c084abf4579 Mon Sep 17 00:00:00 2001 From: oladev2026-tech Date: Sat, 17 Oct 2026 10:02:26 +0100 Subject: [PATCH 4/4] Add useUpdateAsset hook for inline edit persistence --- frontend/hooks/useUpdateAsset.ts | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/hooks/useUpdateAsset.ts diff --git a/frontend/hooks/useUpdateAsset.ts b/frontend/hooks/useUpdateAsset.ts new file mode 100644 index 000000000..d0f886b7c --- /dev/null +++ b/frontend/hooks/useUpdateAsset.ts @@ -0,0 +1,36 @@ +'use client'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { assetApiClient } from '@/lib/api/assets'; +import { queryKeys } from '@/lib/query/keys'; +import type { Asset } from '@/lib/query/types/asset'; + +export function useUpdateAsset(assetId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: Record) => + assetApiClient.updateAsset(assetId, data), + + onMutate: async (newData) => { + // Optimistic update + await queryClient.cancelQueries({ queryKey: queryKeys.assets.detail(assetId) }); + const previousAsset = queryClient.getQueryData(queryKeys.assets.detail(assetId)); + if (previousAsset) { + queryClient.setQueryData(queryKeys.assets.detail(assetId), { ...previousAsset, ...newData }); + } + return { previousAsset }; + }, + + onError: (_err, _newData, context) => { + // Rollback on error + if (context?.previousAsset) { + queryClient.setQueryData(queryKeys.assets.detail(assetId), context.previousAsset); + } + }, + + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.assets.detail(assetId) }); + }, + }); +} \ No newline at end of file