|
@@ -1,8 +1,9 @@
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
|
import { useBaaS, useAuth } from '@picobaas/client/react';
|
|
import { useBaaS, useAuth } from '@picobaas/client/react';
|
|
|
-import type { ImageFile } from '../types';
|
|
|
|
|
|
|
+import type { ImageFile, UploadOptions } from '../types';
|
|
|
import { PUBLIC_BUCKET, USER_BUCKET, generateUUID } from '../config';
|
|
import { PUBLIC_BUCKET, USER_BUCKET, generateUUID } from '../config';
|
|
|
import { getExpiryFromMetadata } from '../utils/expiry';
|
|
import { getExpiryFromMetadata } from '../utils/expiry';
|
|
|
|
|
+import { getSessionToken, trackUpload, untrackUpload } from '../utils/session';
|
|
|
import type { FileMetadata } from '@picobaas/client';
|
|
import type { FileMetadata } from '@picobaas/client';
|
|
|
|
|
|
|
|
export function useImages(bucket: string = PUBLIC_BUCKET) {
|
|
export function useImages(bucket: string = PUBLIC_BUCKET) {
|
|
@@ -51,7 +52,7 @@ export function useImages(bucket: string = PUBLIC_BUCKET) {
|
|
|
}, [client, bucket, userId]);
|
|
}, [client, bucket, userId]);
|
|
|
|
|
|
|
|
const uploadImage = useCallback(
|
|
const uploadImage = useCallback(
|
|
|
- async (file: File, expirySeconds: number): Promise<ImageFile | null> => {
|
|
|
|
|
|
|
+ async (file: File, options: UploadOptions): Promise<ImageFile | null> => {
|
|
|
try {
|
|
try {
|
|
|
const storage = client.storage.from(bucket);
|
|
const storage = client.storage.from(bucket);
|
|
|
|
|
|
|
@@ -63,11 +64,11 @@ export function useImages(bucket: string = PUBLIC_BUCKET) {
|
|
|
|
|
|
|
|
// Calculate expiry
|
|
// Calculate expiry
|
|
|
const expiresAt =
|
|
const expiresAt =
|
|
|
- expirySeconds > 0
|
|
|
|
|
- ? new Date(Date.now() + expirySeconds * 1000).toISOString()
|
|
|
|
|
|
|
+ options.expirySeconds > 0
|
|
|
|
|
+ ? new Date(Date.now() + options.expirySeconds * 1000).toISOString()
|
|
|
: undefined;
|
|
: undefined;
|
|
|
|
|
|
|
|
- // Upload
|
|
|
|
|
|
|
+ // Upload to storage
|
|
|
const { data, error: uploadError } = await storage.upload(path, file, {
|
|
const { data, error: uploadError } = await storage.upload(path, file, {
|
|
|
contentType: file.type,
|
|
contentType: file.type,
|
|
|
metadata: expiresAt ? { expires_at: expiresAt } : undefined,
|
|
metadata: expiresAt ? { expires_at: expiresAt } : undefined,
|
|
@@ -75,18 +76,37 @@ export function useImages(bucket: string = PUBLIC_BUCKET) {
|
|
|
|
|
|
|
|
if (uploadError) throw uploadError;
|
|
if (uploadError) throw uploadError;
|
|
|
|
|
|
|
|
- // Create a short URL alias for the uploaded file
|
|
|
|
|
- let aliasId: string | undefined;
|
|
|
|
|
- let aliasUrl: string | undefined;
|
|
|
|
|
- try {
|
|
|
|
|
- const aliasResult = await storage.createAlias(path, expiresAt);
|
|
|
|
|
- if (aliasResult.data) {
|
|
|
|
|
- aliasId = aliasResult.data.id;
|
|
|
|
|
- aliasUrl = aliasResult.data.url;
|
|
|
|
|
|
|
+ // Register with image API for short URL and tracking
|
|
|
|
|
+ const sessionToken = getSessionToken();
|
|
|
|
|
+ const apiResponse = await fetch('/api/images', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ 'X-Session-Token': sessionToken,
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ bucket,
|
|
|
|
|
+ path,
|
|
|
|
|
+ storage_object_id: data?.id,
|
|
|
|
|
+ download_allowed: options.downloadAllowed,
|
|
|
|
|
+ expires_at: expiresAt,
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ let shortCode: string | undefined;
|
|
|
|
|
+ let shortUrl: string | undefined;
|
|
|
|
|
+
|
|
|
|
|
+ if (apiResponse.ok) {
|
|
|
|
|
+ const imageData = await apiResponse.json();
|
|
|
|
|
+ shortCode = imageData.short_code;
|
|
|
|
|
+ shortUrl = `/i/${shortCode}`;
|
|
|
|
|
+
|
|
|
|
|
+ // Track anonymous uploads for deletion capability
|
|
|
|
|
+ if (!isAuthenticated && shortCode) {
|
|
|
|
|
+ trackUpload(shortCode);
|
|
|
}
|
|
}
|
|
|
- } catch (aliasErr) {
|
|
|
|
|
- // Alias creation failed, but upload succeeded - continue without alias
|
|
|
|
|
- console.warn('Failed to create alias:', aliasErr);
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('Failed to register image with API:', await apiResponse.text());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const newImage: ImageFile = {
|
|
const newImage: ImageFile = {
|
|
@@ -100,8 +120,9 @@ export function useImages(bucket: string = PUBLIC_BUCKET) {
|
|
|
expiresAt: expiresAt ? new Date(expiresAt) : undefined,
|
|
expiresAt: expiresAt ? new Date(expiresAt) : undefined,
|
|
|
isPublic: bucket === PUBLIC_BUCKET,
|
|
isPublic: bucket === PUBLIC_BUCKET,
|
|
|
ownerId: userId || undefined,
|
|
ownerId: userId || undefined,
|
|
|
- aliasId,
|
|
|
|
|
- aliasUrl,
|
|
|
|
|
|
|
+ shortCode,
|
|
|
|
|
+ shortUrl,
|
|
|
|
|
+ downloadAllowed: options.downloadAllowed,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
setImages((prev) => [newImage, ...prev]);
|
|
setImages((prev) => [newImage, ...prev]);
|
|
@@ -112,16 +133,35 @@ export function useImages(bucket: string = PUBLIC_BUCKET) {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- [client, bucket, userId]
|
|
|
|
|
|
|
+ [client, bucket, userId, isAuthenticated]
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const deleteImage = useCallback(
|
|
const deleteImage = useCallback(
|
|
|
async (image: ImageFile): Promise<void> => {
|
|
async (image: ImageFile): Promise<void> => {
|
|
|
try {
|
|
try {
|
|
|
- const storage = client.storage.from(bucket);
|
|
|
|
|
- const { error: deleteError } = await storage.remove([image.path]);
|
|
|
|
|
|
|
+ // If we have a short code, delete via the image API (handles session auth)
|
|
|
|
|
+ if (image.shortCode) {
|
|
|
|
|
+ const sessionToken = getSessionToken();
|
|
|
|
|
+ const apiResponse = await fetch(`/api/images/${image.shortCode}`, {
|
|
|
|
|
+ method: 'DELETE',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'X-Session-Token': sessionToken,
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!apiResponse.ok) {
|
|
|
|
|
+ const errorData = await apiResponse.json().catch(() => ({}));
|
|
|
|
|
+ throw new Error(errorData.error || 'Delete failed');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (deleteError) throw deleteError;
|
|
|
|
|
|
|
+ // Remove from session tracking
|
|
|
|
|
+ untrackUpload(image.shortCode);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Fallback to direct storage deletion
|
|
|
|
|
+ const storage = client.storage.from(bucket);
|
|
|
|
|
+ const { error: deleteError } = await storage.remove([image.path]);
|
|
|
|
|
+ if (deleteError) throw deleteError;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
setImages((prev) => prev.filter((img) => img.id !== image.id));
|
|
setImages((prev) => prev.filter((img) => img.id !== image.id));
|
|
|
} catch (err) {
|
|
} catch (err) {
|