|
@@ -1,7 +1,8 @@
|
|
|
import type { ShareLink } from '../types';
|
|
import type { ShareLink } from '../types';
|
|
|
|
|
+import type { BaaSClient } from '@picobaas/client';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Share links API using the generic CRUD endpoints.
|
|
|
|
|
|
|
+ * Share links API using the PicoBaaS SDK.
|
|
|
* Share links are stored in the `share_links` project table.
|
|
* Share links are stored in the `share_links` project table.
|
|
|
*
|
|
*
|
|
|
* Table schema:
|
|
* Table schema:
|
|
@@ -18,7 +19,19 @@ import type { ShareLink } from '../types';
|
|
|
* - created_at: string (ISO timestamp)
|
|
* - created_at: string (ISO timestamp)
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
-const API_BASE = '/api';
|
|
|
|
|
|
|
+interface ShareLinkRecord {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ link_code: string;
|
|
|
|
|
+ image_id: string;
|
|
|
|
|
+ image_short_code: string;
|
|
|
|
|
+ created_by?: string;
|
|
|
|
|
+ session_token?: string;
|
|
|
|
|
+ expires_at: string;
|
|
|
|
|
+ view_count: number;
|
|
|
|
|
+ last_viewed_at?: string;
|
|
|
|
|
+ is_revoked: boolean;
|
|
|
|
|
+ created_at: string;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
function generateLinkCode(): string {
|
|
function generateLinkCode(): string {
|
|
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
@@ -29,216 +42,155 @@ function generateLinkCode(): string {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function recordToShareLink(record: ShareLinkRecord): ShareLink {
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const expiresAt = new Date(record.expires_at);
|
|
|
|
|
+ const isExpired = expiresAt < now;
|
|
|
|
|
+ const isRevoked = record.is_revoked;
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: record.id,
|
|
|
|
|
+ linkCode: record.link_code,
|
|
|
|
|
+ shareUrl: `${window.location.origin}/#/s/${record.link_code}`,
|
|
|
|
|
+ expiresAt,
|
|
|
|
|
+ viewCount: record.view_count || 0,
|
|
|
|
|
+ lastViewedAt: record.last_viewed_at ? new Date(record.last_viewed_at) : undefined,
|
|
|
|
|
+ isRevoked,
|
|
|
|
|
+ isValid: !isRevoked && !isExpired,
|
|
|
|
|
+ createdAt: new Date(record.created_at),
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export async function createShareLink(
|
|
export async function createShareLink(
|
|
|
|
|
+ client: BaaSClient,
|
|
|
shortCode: string,
|
|
shortCode: string,
|
|
|
expiresIn: number,
|
|
expiresIn: number,
|
|
|
- token?: string,
|
|
|
|
|
sessionToken?: string,
|
|
sessionToken?: string,
|
|
|
imageId?: string
|
|
imageId?: string
|
|
|
-): Promise<{ shareUrl: string; linkCode: string; expiresAt: string }> {
|
|
|
|
|
- const headers: Record<string, string> = {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- };
|
|
|
|
|
- if (token) {
|
|
|
|
|
- headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
- }
|
|
|
|
|
- if (sessionToken) {
|
|
|
|
|
- headers['X-Session-Token'] = sessionToken;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+): Promise<ShareLink> {
|
|
|
const linkCode = generateLinkCode();
|
|
const linkCode = generateLinkCode();
|
|
|
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
|
|
|
|
|
|
- // Create share link via CRUD API
|
|
|
|
|
- const res = await fetch(`${API_BASE}/share_links`, {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- headers,
|
|
|
|
|
- body: JSON.stringify({
|
|
|
|
|
- link_code: linkCode,
|
|
|
|
|
- image_id: imageId || '',
|
|
|
|
|
- image_short_code: shortCode,
|
|
|
|
|
- expires_at: expiresAt,
|
|
|
|
|
- view_count: 0,
|
|
|
|
|
- is_revoked: false,
|
|
|
|
|
- session_token: sessionToken || '',
|
|
|
|
|
- }),
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!res.ok) {
|
|
|
|
|
- const error = await res.json().catch(() => ({}));
|
|
|
|
|
- throw new Error(error.error || 'Failed to create share link');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const data: Partial<ShareLinkRecord> = {
|
|
|
|
|
+ link_code: linkCode,
|
|
|
|
|
+ image_id: imageId || '',
|
|
|
|
|
+ image_short_code: shortCode,
|
|
|
|
|
+ expires_at: expiresAt,
|
|
|
|
|
+ view_count: 0,
|
|
|
|
|
+ is_revoked: false,
|
|
|
|
|
+ session_token: sessionToken || '',
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- const data = await res.json();
|
|
|
|
|
- const shareUrl = `${window.location.origin}/s/${linkCode}`;
|
|
|
|
|
|
|
+ // If authenticated, the SDK will include the auth header automatically
|
|
|
|
|
+ const response = await client.from<ShareLinkRecord>('share_links').insert(data);
|
|
|
|
|
|
|
|
|
|
+ if (response.error) {
|
|
|
|
|
+ throw new Error(response.error.message || 'Failed to create share link');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return the created share link
|
|
|
return {
|
|
return {
|
|
|
- shareUrl,
|
|
|
|
|
|
|
+ id: response.data?.id || linkCode,
|
|
|
linkCode,
|
|
linkCode,
|
|
|
- expiresAt: data.expires_at || expiresAt,
|
|
|
|
|
|
|
+ shareUrl: `${window.location.origin}/#/s/${linkCode}`,
|
|
|
|
|
+ expiresAt: new Date(expiresAt),
|
|
|
|
|
+ viewCount: 0,
|
|
|
|
|
+ isRevoked: false,
|
|
|
|
|
+ isValid: true,
|
|
|
|
|
+ createdAt: new Date(),
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export async function listShareLinks(
|
|
export async function listShareLinks(
|
|
|
- shortCode: string,
|
|
|
|
|
- token?: string,
|
|
|
|
|
- sessionToken?: string
|
|
|
|
|
|
|
+ client: BaaSClient,
|
|
|
|
|
+ shortCode: string
|
|
|
): Promise<ShareLink[]> {
|
|
): Promise<ShareLink[]> {
|
|
|
- const headers: Record<string, string> = {};
|
|
|
|
|
- if (token) {
|
|
|
|
|
- headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
|
|
+ const response = await client
|
|
|
|
|
+ .from<ShareLinkRecord>('share_links')
|
|
|
|
|
+ .select('*')
|
|
|
|
|
+ .eq('image_short_code', shortCode)
|
|
|
|
|
+ .order('created_at', { ascending: false })
|
|
|
|
|
+ .get();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.error) {
|
|
|
|
|
+ throw new Error(response.error.message || 'Failed to load share links');
|
|
|
}
|
|
}
|
|
|
- if (sessionToken) {
|
|
|
|
|
- headers['X-Session-Token'] = sessionToken;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Query share links filtered by image_short_code
|
|
|
|
|
- const params = new URLSearchParams({
|
|
|
|
|
- filter: `image_short_code.eq.${shortCode}`,
|
|
|
|
|
- order: 'created_at.desc',
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const res = await fetch(`${API_BASE}/share_links?${params}`, { headers });
|
|
|
|
|
-
|
|
|
|
|
- if (!res.ok) {
|
|
|
|
|
- throw new Error('Failed to load share links');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const data = await res.json();
|
|
|
|
|
- const now = new Date();
|
|
|
|
|
|
|
|
|
|
- return (data.data || []).map((link: Record<string, unknown>) => {
|
|
|
|
|
- const expiresAt = new Date(link.expires_at as string);
|
|
|
|
|
- const isExpired = expiresAt < now;
|
|
|
|
|
- const isRevoked = link.is_revoked as boolean;
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- id: link.id as string,
|
|
|
|
|
- linkCode: link.link_code as string,
|
|
|
|
|
- shareUrl: `${window.location.origin}/s/${link.link_code}`,
|
|
|
|
|
- expiresAt,
|
|
|
|
|
- viewCount: (link.view_count as number) || 0,
|
|
|
|
|
- lastViewedAt: link.last_viewed_at ? new Date(link.last_viewed_at as string) : undefined,
|
|
|
|
|
- isRevoked,
|
|
|
|
|
- isValid: !isRevoked && !isExpired,
|
|
|
|
|
- createdAt: new Date(link.created_at as string),
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ return (response.data || []).map(recordToShareLink);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export async function revokeShareLink(
|
|
export async function revokeShareLink(
|
|
|
- _shortCode: string, // Kept for API compatibility, but not used with CRUD approach
|
|
|
|
|
- linkCode: string,
|
|
|
|
|
- token?: string,
|
|
|
|
|
- sessionToken?: string
|
|
|
|
|
|
|
+ client: BaaSClient,
|
|
|
|
|
+ linkCode: string
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
- const headers: Record<string, string> = {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- };
|
|
|
|
|
- if (token) {
|
|
|
|
|
- headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
- }
|
|
|
|
|
- if (sessionToken) {
|
|
|
|
|
- headers['X-Session-Token'] = sessionToken;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // First find the link ID by link_code
|
|
|
|
|
- const params = new URLSearchParams({
|
|
|
|
|
- filter: `link_code.eq.${linkCode}`,
|
|
|
|
|
- limit: '1',
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const findRes = await fetch(`${API_BASE}/share_links?${params}`, { headers });
|
|
|
|
|
- if (!findRes.ok) {
|
|
|
|
|
- throw new Error('Failed to find share link');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const findData = await findRes.json();
|
|
|
|
|
- if (!findData.data || findData.data.length === 0) {
|
|
|
|
|
|
|
+ // First find the link by link_code
|
|
|
|
|
+ const findResponse = await client
|
|
|
|
|
+ .from<ShareLinkRecord>('share_links')
|
|
|
|
|
+ .select('id')
|
|
|
|
|
+ .eq('link_code', linkCode)
|
|
|
|
|
+ .limit(1)
|
|
|
|
|
+ .get();
|
|
|
|
|
+
|
|
|
|
|
+ if (findResponse.error || !findResponse.data || findResponse.data.length === 0) {
|
|
|
throw new Error('Share link not found');
|
|
throw new Error('Share link not found');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const linkId = findData.data[0].id;
|
|
|
|
|
|
|
+ const linkId = findResponse.data[0].id;
|
|
|
|
|
|
|
|
// Update the link to mark it as revoked
|
|
// Update the link to mark it as revoked
|
|
|
- const res = await fetch(`${API_BASE}/share_links/${linkId}`, {
|
|
|
|
|
- method: 'PUT',
|
|
|
|
|
- headers,
|
|
|
|
|
- body: JSON.stringify({
|
|
|
|
|
- is_revoked: true,
|
|
|
|
|
- }),
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!res.ok) {
|
|
|
|
|
- throw new Error('Failed to revoke share link');
|
|
|
|
|
|
|
+ const updateResponse = await client
|
|
|
|
|
+ .from<ShareLinkRecord>('share_links')
|
|
|
|
|
+ .eq('id', linkId)
|
|
|
|
|
+ .update({ is_revoked: true });
|
|
|
|
|
+
|
|
|
|
|
+ if (updateResponse.error) {
|
|
|
|
|
+ throw new Error(updateResponse.error.message || 'Failed to revoke share link');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export async function getShareLinkByCode(
|
|
export async function getShareLinkByCode(
|
|
|
|
|
+ client: BaaSClient,
|
|
|
linkCode: string
|
|
linkCode: string
|
|
|
): Promise<ShareLink | null> {
|
|
): Promise<ShareLink | null> {
|
|
|
- const params = new URLSearchParams({
|
|
|
|
|
- filter: `link_code.eq.${linkCode}`,
|
|
|
|
|
- limit: '1',
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const res = await fetch(`${API_BASE}/share_links?${params}`);
|
|
|
|
|
-
|
|
|
|
|
- if (!res.ok) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const data = await res.json();
|
|
|
|
|
- if (!data.data || data.data.length === 0) {
|
|
|
|
|
|
|
+ const response = await client
|
|
|
|
|
+ .from<ShareLinkRecord>('share_links')
|
|
|
|
|
+ .select('*')
|
|
|
|
|
+ .eq('link_code', linkCode)
|
|
|
|
|
+ .limit(1)
|
|
|
|
|
+ .get();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.error || !response.data || response.data.length === 0) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const link = data.data[0];
|
|
|
|
|
- const now = new Date();
|
|
|
|
|
- const expiresAt = new Date(link.expires_at as string);
|
|
|
|
|
- const isExpired = expiresAt < now;
|
|
|
|
|
- const isRevoked = link.is_revoked as boolean;
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- id: link.id as string,
|
|
|
|
|
- linkCode: link.link_code as string,
|
|
|
|
|
- shareUrl: `${window.location.origin}/s/${link.link_code}`,
|
|
|
|
|
- expiresAt,
|
|
|
|
|
- viewCount: (link.view_count as number) || 0,
|
|
|
|
|
- lastViewedAt: link.last_viewed_at ? new Date(link.last_viewed_at as string) : undefined,
|
|
|
|
|
- isRevoked,
|
|
|
|
|
- isValid: !isRevoked && !isExpired,
|
|
|
|
|
- createdAt: new Date(link.created_at as string),
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ return recordToShareLink(response.data[0]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export async function incrementViewCount(
|
|
export async function incrementViewCount(
|
|
|
|
|
+ client: BaaSClient,
|
|
|
linkCode: string
|
|
linkCode: string
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
// Find the link
|
|
// Find the link
|
|
|
- const params = new URLSearchParams({
|
|
|
|
|
- filter: `link_code.eq.${linkCode}`,
|
|
|
|
|
- limit: '1',
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const findRes = await fetch(`${API_BASE}/share_links?${params}`);
|
|
|
|
|
- if (!findRes.ok) return;
|
|
|
|
|
-
|
|
|
|
|
- const findData = await findRes.json();
|
|
|
|
|
- if (!findData.data || findData.data.length === 0) return;
|
|
|
|
|
|
|
+ const findResponse = await client
|
|
|
|
|
+ .from<ShareLinkRecord>('share_links')
|
|
|
|
|
+ .select('id,view_count')
|
|
|
|
|
+ .eq('link_code', linkCode)
|
|
|
|
|
+ .limit(1)
|
|
|
|
|
+ .get();
|
|
|
|
|
+
|
|
|
|
|
+ if (findResponse.error || !findResponse.data || findResponse.data.length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const link = findData.data[0];
|
|
|
|
|
- const currentCount = (link.view_count as number) || 0;
|
|
|
|
|
|
|
+ const link = findResponse.data[0];
|
|
|
|
|
+ const currentCount = link.view_count || 0;
|
|
|
|
|
|
|
|
// Update view count
|
|
// Update view count
|
|
|
- await fetch(`${API_BASE}/share_links/${link.id}`, {
|
|
|
|
|
- method: 'PUT',
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- },
|
|
|
|
|
- body: JSON.stringify({
|
|
|
|
|
|
|
+ await client
|
|
|
|
|
+ .from<ShareLinkRecord>('share_links')
|
|
|
|
|
+ .eq('id', link.id)
|
|
|
|
|
+ .update({
|
|
|
view_count: currentCount + 1,
|
|
view_count: currentCount + 1,
|
|
|
last_viewed_at: new Date().toISOString(),
|
|
last_viewed_at: new Date().toISOString(),
|
|
|
- }),
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|