feat(manadeck): complete Supabase removal from frontend and backend

- Frontend: Updated deckStore to use fetch API calls to backend
- Frontend: Removed supabase.ts utility and @supabase/supabase-js dependency
- Backend: Removed SupabaseService and supabase.service.ts
- Backend: Updated validation schema to use DATABASE_URL
- Backend: Updated health controller to remove Supabase check
- Backend: Marked AI generation endpoint as not implemented (was using Edge Functions)

Phase 4 of PostgreSQL/Drizzle migration complete.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 02:49:34 +01:00
parent 1530efa936
commit be9df4aa85
10 changed files with 87 additions and 553 deletions

View file

@ -1,5 +1,5 @@
import type { Deck, CreateDeckInput, UpdateDeckInput } from '$lib/types/deck';
import { getAuthenticatedSupabase } from '$lib/utils/supabase';
import { PUBLIC_API_URL } from '$env/static/public';
import { authService } from '$lib/auth';
// Svelte 5 runes-based deck store
@ -8,6 +8,35 @@ let currentDeck = $state<Deck | null>(null);
let loading = $state(false);
let error = $state<string | null>(null);
/**
* Helper to make authenticated API requests
*/
async function apiRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const appToken = await authService.getAppToken();
if (!appToken) {
throw new Error('Not authenticated');
}
const response = await fetch(`${PUBLIC_API_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${appToken}`,
...options.headers
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `API error: ${response.status}`);
}
return response.json();
}
export const deckStore = {
get decks() {
return decks;
@ -30,31 +59,8 @@ export const deckStore = {
error = null;
try {
const appToken = await authService.getAppToken();
if (!appToken) {
throw new Error('Not authenticated');
}
const user = await authService.getUserFromToken();
if (!user) {
throw new Error('No user found');
}
const supabase = await getAuthenticatedSupabase(appToken);
const { data, error: fetchError } = await supabase
.from('decks')
.select('*, cards(count)')
.or(`user_id.eq.${user.id},and(is_public.eq.true,user_id.eq.00000000-0000-0000-0000-000000000001)`)
.order('updated_at', { ascending: false });
if (fetchError) throw fetchError;
// Map card count
decks = (data || []).map((deck: any) => ({
...deck,
card_count: deck.cards?.[0]?.count || 0
}));
const response = await apiRequest<{ decks: Deck[]; count: number }>('/v1/api/decks');
decks = response.decks || [];
} catch (err: any) {
error = err.message || 'Failed to fetch decks';
console.error('Fetch decks error:', err);
@ -71,23 +77,15 @@ export const deckStore = {
error = null;
try {
const appToken = await authService.getAppToken();
if (!appToken) throw new Error('Not authenticated');
const supabase = await getAuthenticatedSupabase(appToken);
const { data, error: fetchError } = await supabase
.from('decks')
.select('*, cards(count)')
.eq('id', id)
.single();
if (fetchError) throw fetchError;
currentDeck = {
...data,
card_count: data.cards?.[0]?.count || 0
};
// For now, find in local decks or fetch all
// TODO: Add GET /v1/api/decks/:id endpoint to backend
if (decks.length === 0) {
await this.fetchDecks();
}
currentDeck = decks.find((d) => d.id === id) || null;
if (!currentDeck) {
throw new Error('Deck not found');
}
} catch (err: any) {
error = err.message || 'Failed to fetch deck';
console.error('Fetch deck error:', err);
@ -104,35 +102,23 @@ export const deckStore = {
error = null;
try {
const appToken = await authService.getAppToken();
if (!appToken) throw new Error('Not authenticated');
const response = await apiRequest<{ success: boolean; deck: Deck }>('/v1/api/decks', {
method: 'POST',
body: JSON.stringify({
title: input.title,
description: input.description || '',
isPublic: input.is_public ?? false,
tags: input.tags || [],
settings: input.settings || {}
})
});
const user = await authService.getUserFromToken();
if (!user) throw new Error('No user found');
const supabase = await getAuthenticatedSupabase(appToken);
const newDeck = {
user_id: user.id,
title: input.title,
description: input.description || '',
is_public: input.is_public ?? false,
tags: input.tags || [],
settings: input.settings || {},
metadata: {}
};
const { data, error: createError } = await supabase
.from('decks')
.insert(newDeck)
.select()
.single();
if (createError) throw createError;
const deck = { ...data, card_count: 0 };
decks = [deck, ...decks];
return deck;
if (response.deck) {
const deck = { ...response.deck, card_count: 0 };
decks = [deck, ...decks];
return deck;
}
return null;
} catch (err: any) {
error = err.message || 'Failed to create deck';
console.error('Create deck error:', err);
@ -150,26 +136,22 @@ export const deckStore = {
error = null;
try {
const appToken = await authService.getAppToken();
if (!appToken) throw new Error('Not authenticated');
const response = await apiRequest<{ success: boolean; deck: Deck }>(
`/v1/api/decks/${id}`,
{
method: 'PUT',
body: JSON.stringify(updates)
}
);
const supabase = await getAuthenticatedSupabase(appToken);
if (response.deck) {
// Update in list
decks = decks.map((d) => (d.id === id ? { ...d, ...response.deck } : d));
const { data, error: updateError } = await supabase
.from('decks')
.update(updates)
.eq('id', id)
.select()
.single();
if (updateError) throw updateError;
// Update in list
decks = decks.map((d) => (d.id === id ? { ...d, ...data } : d));
// Update current if it's the same
if (currentDeck?.id === id) {
currentDeck = { ...currentDeck, ...data };
// Update current if it's the same
if (currentDeck?.id === id) {
currentDeck = { ...currentDeck, ...response.deck };
}
}
} catch (err: any) {
error = err.message || 'Failed to update deck';
@ -187,14 +169,9 @@ export const deckStore = {
error = null;
try {
const appToken = await authService.getAppToken();
if (!appToken) throw new Error('Not authenticated');
const supabase = await getAuthenticatedSupabase(appToken);
const { error: deleteError } = await supabase.from('decks').delete().eq('id', id);
if (deleteError) throw deleteError;
await apiRequest<{ success: boolean }>(`/v1/api/decks/${id}`, {
method: 'DELETE'
});
// Remove from list
decks = decks.filter((d) => d.id !== id);

View file

@ -1,44 +0,0 @@
import { createClient } from '@supabase/supabase-js';
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
// Create a singleton Supabase client
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false
}
});
/**
* Get authenticated Supabase client with Mana Core token
* This injects the JWT token from Mana Core for RLS policies
*/
export async function getAuthenticatedSupabase(appToken?: string) {
if (!appToken) {
// Try to get token from sessionStorage
if (typeof window !== 'undefined') {
appToken = sessionStorage.getItem('appToken') || undefined;
}
}
if (!appToken) {
throw new Error('No auth token available');
}
// Create a new client instance with the auth token
const authenticatedClient = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
headers: {
Authorization: `Bearer ${appToken}`
}
},
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false
}
});
return authenticatedClient;
}