From 5bd967900fdb6b558ef40fb80d0e8636f4f59613 Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 23 Mar 2026 12:01:58 +0100 Subject: [PATCH] refactor(context-mobile): migrate from Supabase to backend API + mana-core-auth Complete migration of Context mobile app from direct Supabase access to NestJS backend API with mana-core-auth authentication. New files: - context/AuthProvider.tsx: mana-core-auth integration via @manacore/shared-auth - services/backendApi.ts: Backend API client for spaces, documents, AI, tokens Rewritten services (same exports, backend implementation): - supabaseService.ts: Now thin wrapper around backendApi - aiService.ts: Uses backendApi for auth token - tokenCountingService.ts: Model prices from backend API - tokenTransactionService.ts: All token ops via backend API - revenueCatService.ts: Token balance via backend API Updated 16 consumer files (auth forms, token components, AI toolbars) Deleted: - utils/supabase.ts, context/AuthContext.tsx - services/spaceService.ts, services/spaceServiceDirect.ts Dependencies: - Added: @manacore/shared-auth, expo-secure-store - Removed: @supabase/supabase-js, @google/generative-ai, openai, @azure/openai Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/context/apps/mobile/app/_layout.tsx | 4 +- apps/context/apps/mobile/app/index.tsx | 2 +- .../apps/mobile/app/settings/index.tsx | 2 +- apps/context/apps/mobile/app/tokens/index.tsx | 30 +- .../mobile/components/ai/BottomLLMToolbar.tsx | 44 +- .../mobile/components/ai/SpacesLLMToolbar.tsx | 44 +- .../apps/mobile/components/auth/LoginForm.tsx | 12 +- .../mobile/components/auth/ProtectedRoute.tsx | 2 +- .../mobile/components/auth/RegisterForm.tsx | 23 +- .../components/monetization/TokenDisplay.tsx | 12 +- .../monetization/TokenEstimator.tsx | 17 +- .../components/monetization/TokenStore.tsx | 22 +- .../apps/mobile/context/AuthContext.tsx | 183 ----- .../apps/mobile/context/AuthProvider.tsx | 230 +++++++ .../apps/mobile/hooks/useDocumentEditor.ts | 2 +- apps/context/apps/mobile/package.json | 6 +- .../context/apps/mobile/services/aiService.ts | 98 +-- .../apps/mobile/services/backendApi.ts | 438 ++++++++++++ .../apps/mobile/services/revenueCatService.ts | 194 +----- .../apps/mobile/services/spaceService.ts | 629 ------------------ .../mobile/services/spaceServiceDirect.ts | 183 ----- .../apps/mobile/services/supabaseService.ts | 601 ++--------------- .../mobile/services/tokenCountingService.ts | 198 +----- .../services/tokenTransactionService.ts | 368 ++-------- apps/context/apps/mobile/utils/supabase.ts | 23 - 25 files changed, 896 insertions(+), 2471 deletions(-) delete mode 100644 apps/context/apps/mobile/context/AuthContext.tsx create mode 100644 apps/context/apps/mobile/context/AuthProvider.tsx create mode 100644 apps/context/apps/mobile/services/backendApi.ts delete mode 100644 apps/context/apps/mobile/services/spaceService.ts delete mode 100644 apps/context/apps/mobile/services/spaceServiceDirect.ts delete mode 100644 apps/context/apps/mobile/utils/supabase.ts diff --git a/apps/context/apps/mobile/app/_layout.tsx b/apps/context/apps/mobile/app/_layout.tsx index 5c2eb0872..96b607f41 100644 --- a/apps/context/apps/mobile/app/_layout.tsx +++ b/apps/context/apps/mobile/app/_layout.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { Slot, Stack, useRouter, useSegments } from 'expo-router'; import { useFonts } from 'expo-font'; import { SplashScreen } from 'expo-router'; -import { AuthProvider, useAuth } from '../context/AuthContext'; +import { AuthProvider, useAuth } from '../context/AuthProvider'; import { ThemeProvider } from '../components/theme'; import { DebugProvider } from '../context/DebugContext'; import { I18nProvider } from '../context/I18nContext'; @@ -51,7 +51,7 @@ function RootLayoutNav() { // Initialisiere RevenueCat, wenn der Benutzer angemeldet ist if (user) { - initializeRevenueCat(user.id); + initializeRevenueCat(user.userId); } }, [user, loading, segments, router]); diff --git a/apps/context/apps/mobile/app/index.tsx b/apps/context/apps/mobile/app/index.tsx index 4c65b55e7..8b059c353 100644 --- a/apps/context/apps/mobile/app/index.tsx +++ b/apps/context/apps/mobile/app/index.tsx @@ -11,7 +11,7 @@ import { Ionicons } from '@expo/vector-icons'; import { Screen } from '~/components/layout/Screen'; import { Text } from '~/components/ui/Text'; -import { useAuth } from '~/context/AuthContext'; +import { useAuth } from '~/context/AuthProvider'; import { getSpaces, getDocuments, Document, Space } from '~/services/supabaseService'; import { useTheme } from '~/utils/theme/theme'; import { useTranslations } from '~/context/I18nContext'; diff --git a/apps/context/apps/mobile/app/settings/index.tsx b/apps/context/apps/mobile/app/settings/index.tsx index 43b0c2fcb..d4da1f5fc 100644 --- a/apps/context/apps/mobile/app/settings/index.tsx +++ b/apps/context/apps/mobile/app/settings/index.tsx @@ -3,7 +3,7 @@ import { View, Text, ScrollView, StyleSheet, TouchableOpacity, Alert, Switch } f import { Stack, useRouter } from 'expo-router'; import { ThemeSelector } from '~/components/theme'; import { useTheme } from '~/utils/theme/theme'; -import { useAuth } from '~/context/AuthContext'; +import { useAuth } from '~/context/AuthProvider'; import { useDebug } from '~/context/DebugContext'; import { useTranslations } from '~/context/I18nContext'; import { LanguagePicker } from '~/components/settings/LanguagePicker'; diff --git a/apps/context/apps/mobile/app/tokens/index.tsx b/apps/context/apps/mobile/app/tokens/index.tsx index 4330b5ab3..d8fd7a724 100644 --- a/apps/context/apps/mobile/app/tokens/index.tsx +++ b/apps/context/apps/mobile/app/tokens/index.tsx @@ -9,7 +9,6 @@ import { } from 'react-native'; import { Text } from '~/components/ui/Text'; import { useTheme } from '~/utils/theme/theme'; -import { supabase } from '~/utils/supabase'; import { getCurrentTokenBalance, getTokenTransactions, @@ -33,24 +32,16 @@ export default function TokenManagementScreen() { try { setLoading(true); - // Hole den aktuellen Benutzer - const { data: sessionData } = await supabase.auth.getSession(); - const userId = sessionData?.session?.user?.id; - - if (!userId) { - throw new Error('Nicht angemeldet'); - } - - // Hole das Token-Guthaben - const balance = await getCurrentTokenBalance(userId); + // Hole das Token-Guthaben (backend identifies user from JWT) + const balance = await getCurrentTokenBalance(); setTokenBalance(balance); // Hole die Token-Transaktionen - const transactionData = await getTokenTransactions(userId, 20); + const transactionData = await getTokenTransactions('', 20); setTransactions(transactionData); // Hole die Nutzungsstatistiken - const stats = await getTokenUsageStats(userId, timeframe); + const stats = await getTokenUsageStats('', timeframe); setUsageStats(stats); } catch (error) { console.error('Fehler beim Laden der Token-Daten:', error); @@ -387,16 +378,11 @@ export default function TokenManagementScreen() { // Aktualisiere die Daten nach dem Kauf const refreshData = async () => { try { - const { data: sessionData } = await supabase.auth.getSession(); - const userId = sessionData?.session?.user?.id; + const balance = await getCurrentTokenBalance(); + setTokenBalance(balance); - if (userId) { - const balance = await getCurrentTokenBalance(userId); - setTokenBalance(balance); - - const transactionData = await getTokenTransactions(userId, 20); - setTransactions(transactionData); - } + const transactionData = await getTokenTransactions('', 20); + setTransactions(transactionData); } catch (error) { console.error('Fehler beim Aktualisieren der Daten:', error); } diff --git a/apps/context/apps/mobile/components/ai/BottomLLMToolbar.tsx b/apps/context/apps/mobile/components/ai/BottomLLMToolbar.tsx index e5fae65f9..1610061ad 100644 --- a/apps/context/apps/mobile/components/ai/BottomLLMToolbar.tsx +++ b/apps/context/apps/mobile/components/ai/BottomLLMToolbar.tsx @@ -19,7 +19,6 @@ import { } from '~/services/aiService'; import { useTheme } from '~/utils/theme/theme'; import { getDocumentById } from '~/services/supabaseService'; -import { supabase } from '~/utils/supabase'; import { eventEmitter, EVENTS } from '~/utils/eventEmitter'; import { getCurrentTokenBalance } from '~/services/tokenTransactionService'; import TokenDisplay from '~/components/monetization/TokenDisplay'; @@ -206,32 +205,15 @@ export const BottomLLMToolbar: React.FC = ({ setTimeout(async () => { console.log('Aktualisiere Token-Guthaben nach erfolgreichem Call...'); - // Direkte Aktualisierung des Token-Guthabens ohne Caching + // Direkte Aktualisierung des Token-Guthabens via Backend API try { - // Hole den aktuellen Benutzer - const { data: sessionData } = await supabase.auth.getSession(); - const userId = sessionData?.session?.user?.id; + const newBalance = await getCurrentTokenBalance(); + console.log('Neues Token-Guthaben:', newBalance); + setTokenBalance(newBalance); - if (!userId) { - throw new Error('Nicht angemeldet'); - } - - // Hole das aktuelle Token-Guthaben direkt aus der Datenbank mit Cache-Busting - const { data: userData } = await supabase - .from('users') - .select('token_balance') - .eq('id', userId) - .single(); - - if (userData) { - console.log('Neues Token-Guthaben:', userData.token_balance); - // Aktualisiere den Zustand mit dem neuen Guthaben - setTokenBalance(userData.token_balance); - - // Löse ein Event aus, um alle TokenDisplay-Komponenten zu benachrichtigen - console.log('Löse TOKEN_BALANCE_UPDATED-Event aus'); - eventEmitter.emit(EVENTS.TOKEN_BALANCE_UPDATED); - } + // Löse ein Event aus, um alle TokenDisplay-Komponenten zu benachrichtigen + console.log('Löse TOKEN_BALANCE_UPDATED-Event aus'); + eventEmitter.emit(EVENTS.TOKEN_BALANCE_UPDATED); } catch (error) { console.error('Fehler beim direkten Aktualisieren des Token-Guthabens:', error); // Fallback zur normalen Aktualisierung @@ -296,16 +278,8 @@ export const BottomLLMToolbar: React.FC = ({ try { setLoading(true); - // Hole den aktuellen Benutzer - const { data: sessionData } = await supabase.auth.getSession(); - const userId = sessionData?.session?.user?.id; - - if (!userId) { - throw new Error('Nicht angemeldet'); - } - - // Hole das aktuelle Token-Guthaben - const balance = await getCurrentTokenBalance(userId); + // Hole das aktuelle Token-Guthaben (backend identifies user from JWT) + const balance = await getCurrentTokenBalance(); setTokenBalance(balance); // Löse ein Event aus, um alle TokenDisplay-Komponenten zu benachrichtigen diff --git a/apps/context/apps/mobile/components/ai/SpacesLLMToolbar.tsx b/apps/context/apps/mobile/components/ai/SpacesLLMToolbar.tsx index 6221eed88..ddb54f294 100644 --- a/apps/context/apps/mobile/components/ai/SpacesLLMToolbar.tsx +++ b/apps/context/apps/mobile/components/ai/SpacesLLMToolbar.tsx @@ -18,7 +18,6 @@ import { } from '~/services/aiService'; import { estimateTokens } from '~/services/tokenCountingService'; import { useTheme } from '~/utils/theme/theme'; -import { supabase } from '~/utils/supabase'; import { eventEmitter, EVENTS } from '~/utils/eventEmitter'; import { getCurrentTokenBalance } from '~/services/tokenTransactionService'; import { createDocument, Document } from '~/services/supabaseService'; @@ -333,32 +332,15 @@ export const SpacesLLMToolbar: React.FC = ({ setTimeout(async () => { console.log('Aktualisiere Token-Guthaben nach erfolgreichem Call...'); - // Direkte Aktualisierung des Token-Guthabens ohne Caching + // Direkte Aktualisierung des Token-Guthabens via Backend API try { - // Hole den aktuellen Benutzer - const { data: sessionData } = await supabase.auth.getSession(); - const userId = sessionData?.session?.user?.id; + const newBalance = await getCurrentTokenBalance(); + console.log('Neues Token-Guthaben:', newBalance); + setTokenBalance(newBalance); - if (!userId) { - throw new Error('Nicht angemeldet'); - } - - // Hole das aktuelle Token-Guthaben direkt aus der Datenbank mit Cache-Busting - const { data: userData } = await supabase - .from('users') - .select('token_balance') - .eq('id', userId) - .single(); - - if (userData) { - console.log('Neues Token-Guthaben:', userData.token_balance); - // Aktualisiere den Zustand mit dem neuen Guthaben - setTokenBalance(userData.token_balance); - - // Löse ein Event aus, um alle TokenDisplay-Komponenten zu benachrichtigen - console.log('Löse TOKEN_BALANCE_UPDATED-Event aus'); - eventEmitter.emit(EVENTS.TOKEN_BALANCE_UPDATED); - } + // Löse ein Event aus, um alle TokenDisplay-Komponenten zu benachrichtigen + console.log('Löse TOKEN_BALANCE_UPDATED-Event aus'); + eventEmitter.emit(EVENTS.TOKEN_BALANCE_UPDATED); } catch (error) { console.error('Fehler beim direkten Aktualisieren des Token-Guthabens:', error); // Fallback zur normalen Aktualisierung @@ -428,16 +410,8 @@ export const SpacesLLMToolbar: React.FC = ({ try { setLoading(true); - // Hole den aktuellen Benutzer - const { data: sessionData } = await supabase.auth.getSession(); - const userId = sessionData?.session?.user?.id; - - if (!userId) { - throw new Error('Nicht angemeldet'); - } - - // Hole das aktuelle Token-Guthaben - const balance = await getCurrentTokenBalance(userId); + // Hole das aktuelle Token-Guthaben (backend identifies user from JWT) + const balance = await getCurrentTokenBalance(); setTokenBalance(balance); // Löse ein Event aus, um alle TokenDisplay-Komponenten zu benachrichtigen diff --git a/apps/context/apps/mobile/components/auth/LoginForm.tsx b/apps/context/apps/mobile/components/auth/LoginForm.tsx index 52ff85760..20844b9d1 100644 --- a/apps/context/apps/mobile/components/auth/LoginForm.tsx +++ b/apps/context/apps/mobile/components/auth/LoginForm.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'expo-router'; import { Text } from '../ui/Text'; import { Input } from '../ui/Input'; import { Button } from '../Button'; -import { useAuth } from '../../context/AuthContext'; +import { useAuth } from '../../context/AuthProvider'; type LoginFormProps = { onSuccess?: () => void; @@ -25,10 +25,10 @@ export const LoginForm = ({ onSuccess }: LoginFormProps) => { setLoading(true); try { - // Verwende die signIn-Funktion aus dem AuthContext - const { success, error: authError } = await signIn(email, password); + // Verwende die signIn-Funktion aus dem AuthProvider + const { error: authError } = await signIn(email, password); - if (success) { + if (!authError) { // Handle successful login if (onSuccess) { onSuccess(); @@ -36,7 +36,9 @@ export const LoginForm = ({ onSuccess }: LoginFormProps) => { router.replace('/'); } } else { - setError(authError || 'Anmeldung fehlgeschlagen. Bitte überprüfe deine Anmeldedaten.'); + setError( + authError?.message || 'Anmeldung fehlgeschlagen. Bitte überprüfe deine Anmeldedaten.' + ); } } catch (err: any) { setError('Anmeldung fehlgeschlagen. Bitte überprüfe deine Anmeldedaten.'); diff --git a/apps/context/apps/mobile/components/auth/ProtectedRoute.tsx b/apps/context/apps/mobile/components/auth/ProtectedRoute.tsx index bd464d8a4..0b3c77860 100644 --- a/apps/context/apps/mobile/components/auth/ProtectedRoute.tsx +++ b/apps/context/apps/mobile/components/auth/ProtectedRoute.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { View, ActivityIndicator } from 'react-native'; import { useRouter } from 'expo-router'; -import { useAuth } from '../../context/AuthContext'; +import { useAuth } from '../../context/AuthProvider'; type ProtectedRouteProps = { children: React.ReactNode; diff --git a/apps/context/apps/mobile/components/auth/RegisterForm.tsx b/apps/context/apps/mobile/components/auth/RegisterForm.tsx index 2d038b4d7..70789e7fe 100644 --- a/apps/context/apps/mobile/components/auth/RegisterForm.tsx +++ b/apps/context/apps/mobile/components/auth/RegisterForm.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'expo-router'; import { Text } from '../ui/Text'; import { Input } from '../ui/Input'; import { Button } from '../Button'; -import { useAuth } from '../../context/AuthContext'; +import { useAuth } from '../../context/AuthProvider'; type RegisterFormProps = { onSuccess?: () => void; @@ -46,23 +46,18 @@ export const RegisterForm = ({ onSuccess }: RegisterFormProps) => { setLoading(true); try { - // Verwende die signUp-Funktion aus dem AuthContext - const { success, error: authError } = await signUp(email, password, name); + // Verwende die signUp-Funktion aus dem AuthProvider + const { data, error: authError } = await signUp(email, password); - if (success) { - if (authError) { - // Wenn die Registrierung erfolgreich war, aber eine E-Mail-Bestätigung erforderlich ist - setSuccessMessage(authError); + if (!authError) { + // Handle successful registration + if (onSuccess) { + onSuccess(); } else { - // Handle successful registration - if (onSuccess) { - onSuccess(); - } else { - router.replace('/'); - } + router.replace('/'); } } else { - setError(authError || 'Registrierung fehlgeschlagen. Bitte versuche es erneut.'); + setError(authError?.message || 'Registrierung fehlgeschlagen. Bitte versuche es erneut.'); } } catch (err: any) { setError('Registrierung fehlgeschlagen. Bitte versuche es erneut.'); diff --git a/apps/context/apps/mobile/components/monetization/TokenDisplay.tsx b/apps/context/apps/mobile/components/monetization/TokenDisplay.tsx index c6a64130b..78b4bf92e 100644 --- a/apps/context/apps/mobile/components/monetization/TokenDisplay.tsx +++ b/apps/context/apps/mobile/components/monetization/TokenDisplay.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native'; import { getCurrentTokenBalance } from '../../services/tokenTransactionService'; -import { supabase } from '../../utils/supabase'; import { useTheme, themeClasses } from '../../utils/theme/theme'; import { eventEmitter, EVENTS } from '../../utils/eventEmitter'; @@ -34,14 +33,9 @@ export const TokenDisplay: React.FC = ({ const loadTokenBalance = useCallback(async () => { console.log('TokenDisplay: Lade Token-Guthaben...'); try { - const { data: sessionData } = await supabase.auth.getSession(); - const userId = sessionData?.session?.user?.id; - - if (userId) { - const balance = await getCurrentTokenBalance(userId); - console.log('TokenDisplay: Neues Token-Guthaben geladen:', balance); - setTokenBalance(balance); - } + const balance = await getCurrentTokenBalance(); + console.log('TokenDisplay: Neues Token-Guthaben geladen:', balance); + setTokenBalance(balance); } catch (error) { console.error('Fehler beim Laden des Token-Guthabens:', error); } finally { diff --git a/apps/context/apps/mobile/components/monetization/TokenEstimator.tsx b/apps/context/apps/mobile/components/monetization/TokenEstimator.tsx index 36c142608..11219000b 100644 --- a/apps/context/apps/mobile/components/monetization/TokenEstimator.tsx +++ b/apps/context/apps/mobile/components/monetization/TokenEstimator.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native'; import { estimateCostForPrompt } from '../../services/tokenCountingService'; import { getCurrentTokenBalance } from '../../services/tokenTransactionService'; -import { supabase } from '../../utils/supabase'; import { useTheme } from '../../utils/theme/theme'; type TokenEstimatorProps = { @@ -28,20 +27,8 @@ export const TokenEstimator: React.FC = ({ try { setLoading(true); - // Hole den aktuellen Benutzer - const { data: sessionData } = await supabase.auth.getSession(); - const userId = sessionData?.session?.user?.id; - - if (!userId) { - throw new Error('Nicht angemeldet'); - } - - // WICHTIG: Wir rufen estimateCostForPrompt NICHT mehr direkt auf, - // da die Schätzung bereits von der aufrufenden Komponente berechnet wurde - // und in der estimate-Prop enthalten ist. - - // Hole das aktuelle Token-Guthaben - const tokenBalance = await getCurrentTokenBalance(userId); + // Hole das aktuelle Token-Guthaben (backend identifies user from JWT) + const tokenBalance = await getCurrentTokenBalance(); setBalance(tokenBalance); } catch (error) { console.error('Fehler beim Laden der Token-Schätzung:', error); diff --git a/apps/context/apps/mobile/components/monetization/TokenStore.tsx b/apps/context/apps/mobile/components/monetization/TokenStore.tsx index a42b0df03..4a3aadd84 100644 --- a/apps/context/apps/mobile/components/monetization/TokenStore.tsx +++ b/apps/context/apps/mobile/components/monetization/TokenStore.tsx @@ -17,7 +17,6 @@ import { TOKEN_AMOUNTS, ENTITLEMENTS, } from '../../services/revenueCatService'; -import { supabase } from '../../utils/supabase'; import { themeClasses, useColorModeValue } from '../../utils/theme/theme'; type TokenStoreProps = { @@ -26,7 +25,6 @@ type TokenStoreProps = { }; export const TokenStore: React.FC = ({ onClose, onPurchaseComplete }) => { - const [user, setUser] = useState<{ id: string } | null>(null); const [packages, setPackages] = useState([]); const [loading, setLoading] = useState(true); const [purchasing, setPurchasing] = useState(false); @@ -38,18 +36,6 @@ export const TokenStore: React.FC = ({ onClose, onPurchaseCompl const cardBgColor = useColorModeValue('gray.50', 'gray.700'); const accentColor = useColorModeValue('blue.500', 'blue.300'); - useEffect(() => { - // Aktuellen Benutzer laden - const loadUser = async () => { - const { data: sessionData } = await supabase.auth.getSession(); - if (sessionData?.session?.user) { - setUser({ id: sessionData.session.user.id }); - } - }; - - loadUser(); - }, []); - useEffect(() => { const loadData = async () => { try { @@ -72,14 +58,10 @@ export const TokenStore: React.FC = ({ onClose, onPurchaseCompl } }; - if (user) { - loadData(); - } - }, [user]); + loadData(); + }, []); const handlePurchase = async (pkg: PurchasesPackage) => { - if (!user) return; - try { setPurchasing(true); diff --git a/apps/context/apps/mobile/context/AuthContext.tsx b/apps/context/apps/mobile/context/AuthContext.tsx deleted file mode 100644 index 4f6a7d82b..000000000 --- a/apps/context/apps/mobile/context/AuthContext.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { Session, User } from '@supabase/supabase-js'; -import { supabase } from '../utils/supabase'; - -// Definiere den Typ für den Authentifizierungskontext -type AuthContextType = { - session: Session | null; - user: User | null; - loading: boolean; - signIn: ( - email: string, - password: string - ) => Promise<{ - success: boolean; - error?: string; - }>; - signUp: ( - email: string, - password: string, - name: string - ) => Promise<{ - success: boolean; - error?: string; - }>; - signOut: () => Promise; - resetPassword: (email: string) => Promise<{ - success: boolean; - error?: string; - }>; -}; - -// Erstelle den Authentifizierungskontext -const AuthContext = createContext(undefined); - -// Provider-Komponente für den Authentifizierungskontext -export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [session, setSession] = useState(null); - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - // Initialisiere die Authentifizierung beim Laden der App - useEffect(() => { - // Hole die aktuelle Session - const getSession = async () => { - try { - const { data } = await supabase.auth.getSession(); - setSession(data.session); - setUser(data.session?.user || null); - } catch (error) { - console.error('Fehler beim Abrufen der Session:', error); - } finally { - setLoading(false); - } - }; - - getSession(); - - // Abonniere Änderungen an der Authentifizierung - const { data: authListener } = supabase.auth.onAuthStateChange(async (event, newSession) => { - setSession(newSession); - setUser(newSession?.user || null); - setLoading(false); - }); - - // Bereinige den Listener beim Unmount - return () => { - authListener?.subscription.unsubscribe(); - }; - }, []); - - // Anmeldung mit E-Mail und Passwort - const signIn = async (email: string, password: string) => { - try { - const { data, error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - - if (error) { - return { success: false, error: error.message }; - } - - return { success: true }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }; - - // Registrierung mit E-Mail und Passwort - const signUp = async (email: string, password: string, name: string) => { - try { - // Registriere den Benutzer - const { data, error } = await supabase.auth.signUp({ - email, - password, - options: { - data: { - name, - }, - }, - }); - - if (error) { - return { success: false, error: error.message }; - } - - // Wenn die Registrierung erfolgreich war, aber eine E-Mail-Bestätigung erforderlich ist - if (data?.user && !data.user.confirmed_at) { - return { - success: true, - error: 'Bitte bestätige deine E-Mail-Adresse, um die Registrierung abzuschließen.', - }; - } - - // Erstelle einen Eintrag in der users-Tabelle - if (data?.user) { - const { error: profileError } = await supabase.from('users').insert([ - { - id: data.user.id, - email: data.user.email, - name, - created_at: new Date().toISOString(), - }, - ]); - - if (profileError) { - return { success: false, error: profileError.message }; - } - } - - return { success: true }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }; - - // Abmeldung - const signOut = async () => { - await supabase.auth.signOut(); - }; - - // Passwort zurücksetzen - const resetPassword = async (email: string) => { - try { - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: 'exp://localhost:8081/reset-password', - }); - - if (error) { - return { success: false, error: error.message }; - } - - return { - success: true, - error: 'Bitte überprüfe deine E-Mails für weitere Anweisungen.', - }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }; - - // Werte, die über den Kontext bereitgestellt werden - const value: AuthContextType = { - session, - user, - loading, - signIn, - signUp, - signOut, - resetPassword, - }; - - return {children}; -}; - -// Hook für den einfachen Zugriff auf den Authentifizierungskontext -export const useAuth = () => { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth muss innerhalb eines AuthProviders verwendet werden'); - } - return context; -}; diff --git a/apps/context/apps/mobile/context/AuthProvider.tsx b/apps/context/apps/mobile/context/AuthProvider.tsx new file mode 100644 index 000000000..c1bfa8c83 --- /dev/null +++ b/apps/context/apps/mobile/context/AuthProvider.tsx @@ -0,0 +1,230 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { ActivityIndicator, View, Text } from 'react-native'; +import * as SecureStore from 'expo-secure-store'; +import { + createAuthService, + createTokenManager, + setStorageAdapter, + setDeviceAdapter, + setNetworkAdapter, + type UserData, +} from '@manacore/shared-auth'; + +// Mana Core Auth URL from environment +const MANA_AUTH_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; + +// Create SecureStore adapter for React Native +const createSecureStoreAdapter = () => ({ + async getItem(key: string): Promise { + try { + const value = await SecureStore.getItemAsync(key); + return value ? JSON.parse(value) : null; + } catch { + return null; + } + }, + async setItem(key: string, value: unknown): Promise { + await SecureStore.setItemAsync(key, JSON.stringify(value)); + }, + async removeItem(key: string): Promise { + await SecureStore.deleteItemAsync(key); + }, +}); + +// Create device adapter for React Native +const createReactNativeDeviceAdapter = () => { + let deviceId: string | null = null; + + return { + async getDeviceInfo() { + if (!deviceId) { + // Try to get stored device ID + deviceId = await SecureStore.getItemAsync('@device/id'); + + if (!deviceId) { + // Generate new device ID + deviceId = `rn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + await SecureStore.setItemAsync('@device/id', deviceId); + } + } + + return { + deviceId, + deviceName: 'React Native Device', + platform: 'react-native', + }; + }, + async getStoredDeviceId() { + return deviceId || (await SecureStore.getItemAsync('@device/id')); + }, + }; +}; + +// Create network adapter (basic implementation) +const createReactNativeNetworkAdapter = () => ({ + async isConnected() { + return true; // Always assume connected for now + }, + async hasStableConnection() { + return true; + }, +}); + +// Initialize adapters +setStorageAdapter(createSecureStoreAdapter()); +setDeviceAdapter(createReactNativeDeviceAdapter()); +setNetworkAdapter(createReactNativeNetworkAdapter()); + +// Create auth service +const authService = createAuthService({ baseUrl: MANA_AUTH_URL }); +const tokenManager = createTokenManager(authService); + +// Auth context type +type AuthContextType = { + user: UserData | null; + loading: boolean; + signIn: (email: string, password: string) => Promise<{ error: any | null }>; + signUp: (email: string, password: string) => Promise<{ error: any | null; data: any | null }>; + signOut: () => Promise; + resetPassword: (email: string) => Promise<{ error: any | null }>; +}; + +// Create auth context +const AuthContext = createContext(undefined); + +// Hook to access auth context +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +// AuthProvider component +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // Initialize auth state + useEffect(() => { + const initialize = async () => { + try { + setLoading(true); + + // Check if user is authenticated + const authenticated = await authService.isAuthenticated(); + + if (authenticated) { + const userData = await authService.getUserFromToken(); + setUser(userData); + } + } catch (error) { + console.error('Fehler beim Initialisieren der Auth-Session:', error); + setUser(null); + } finally { + setLoading(false); + } + }; + + initialize(); + }, []); + + // Sign in with email and password + const signIn = async (email: string, password: string) => { + try { + console.log('Versuche Anmeldung mit:', email); + const result = await authService.signIn(email, password); + + if (!result.success) { + console.error('Auth Fehler:', result.error); + return { error: { message: result.error } }; + } + + // Get user data from token + const userData = await authService.getUserFromToken(); + setUser(userData); + + console.log('Anmeldung erfolgreich:', userData?.userId); + return { error: null }; + } catch (error: any) { + console.error('Unerwarteter Fehler beim Anmelden:', error.message || error); + return { error }; + } + }; + + // Sign up with email and password + const signUp = async (email: string, password: string) => { + try { + const result = await authService.signUp(email, password); + + if (!result.success) { + return { data: null, error: { message: result.error } }; + } + + // Auto sign in after successful signup + const signInResult = await signIn(email, password); + + if (signInResult.error) { + return { data: null, error: signInResult.error }; + } + + return { data: user, error: null }; + } catch (error) { + console.error('Fehler beim Registrieren:', error); + return { data: null, error }; + } + }; + + // Sign out + const signOut = async () => { + try { + await authService.signOut(); + setUser(null); + } catch (error) { + console.error('Fehler beim Abmelden:', error); + } + }; + + // Reset password + const resetPassword = async (email: string) => { + try { + const result = await authService.forgotPassword(email); + + if (!result.success) { + return { error: { message: result.error } }; + } + + return { error: null }; + } catch (error) { + console.error('Fehler beim Zurücksetzen des Passworts:', error); + return { error }; + } + }; + + // Show loading indicator during initialization + if (loading) { + return ( + + + Authentifizierung wird initialisiert... + + ); + } + + // Provide auth context + return ( + + {children} + + ); +} diff --git a/apps/context/apps/mobile/hooks/useDocumentEditor.ts b/apps/context/apps/mobile/hooks/useDocumentEditor.ts index 7d4b1e991..54435ee1c 100644 --- a/apps/context/apps/mobile/hooks/useDocumentEditor.ts +++ b/apps/context/apps/mobile/hooks/useDocumentEditor.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useReducer, useRef } from 'react'; import { Platform } from 'react-native'; import { useRouter, useLocalSearchParams } from 'expo-router'; -import { useAuth } from '~/context/AuthContext'; +import { useAuth } from '~/context/AuthProvider'; import { getDocumentById, getDocumentByShortId, diff --git a/apps/context/apps/mobile/package.json b/apps/context/apps/mobile/package.json index 2a2757610..261578753 100644 --- a/apps/context/apps/mobile/package.json +++ b/apps/context/apps/mobile/package.json @@ -18,13 +18,11 @@ "web": "expo start --web" }, "dependencies": { - "@azure/openai": "^2.0.0", "@expo/vector-icons": "^14.0.0", - "@google/generative-ai": "^0.24.0", + "@manacore/shared-auth": "workspace:*", "@react-native-async-storage/async-storage": "^1.23.1", "@react-native-picker/picker": "^2.11.0", "@react-navigation/native": "^7.0.3", - "@supabase/supabase-js": "^2.38.4", "expo": "^52.0.46", "expo-constants": "~17.0.8", "expo-dev-client": "~5.0.4", @@ -35,10 +33,10 @@ "expo-router": "~4.0.6", "expo-status-bar": "~2.0.1", "expo-system-ui": "~4.0.9", + "expo-secure-store": "~14.0.1", "expo-web-browser": "~14.0.2", "i18next": "^25.3.2", "nativewind": "latest", - "openai": "^4.95.0", "react": "18.3.1", "react-dom": "18.3.1", "react-i18next": "^15.6.0", diff --git a/apps/context/apps/mobile/services/aiService.ts b/apps/context/apps/mobile/services/aiService.ts index 555ba0df4..86095f590 100644 --- a/apps/context/apps/mobile/services/aiService.ts +++ b/apps/context/apps/mobile/services/aiService.ts @@ -1,4 +1,4 @@ -import { supabase } from '../utils/supabase'; +import { aiApi } from './backendApi'; // Typdefinitionen export type AIProvider = 'azure' | 'google'; @@ -39,35 +39,6 @@ export const availableModels: AIModelOption[] = [ }, ]; -const BACKEND_URL = - process.env.EXPO_PUBLIC_BACKEND_URL || - process.env.EXPO_PUBLIC_CONTEXT_BACKEND_URL || - 'http://localhost:3020'; - -/** - * Get the current Supabase access token for backend auth - */ -const getAuthToken = async (): Promise => { - const { data } = await supabase.auth.getSession(); - const token = data?.session?.access_token; - if (!token) { - throw new Error('Nicht angemeldet'); - } - return token; -}; - -/** - * Get the current user ID from Supabase session - */ -const getUserId = async (): Promise => { - const { data } = await supabase.auth.getSession(); - const userId = data?.session?.user?.id; - if (!userId) { - throw new Error('Nicht angemeldet'); - } - return userId; -}; - /** * Prüft, ob der Benutzer genügend Tokens für eine Anfrage hat */ @@ -77,37 +48,12 @@ export const checkTokenBalance = async ( estimatedCompletionLength: number = 500, referencedDocuments?: { title: string; content: string }[] ): Promise<{ hasEnough: boolean; estimate: any; balance: number }> => { - try { - const token = await getAuthToken(); - - const response = await fetch(`${BACKEND_URL}/api/v1/ai/estimate/mobile`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - prompt, - model, - estimatedCompletionLength, - referencedDocuments, - }), - }); - - if (!response.ok) { - throw new Error(`Backend error: ${response.status}`); - } - - const data = await response.json(); - return { - hasEnough: data.hasEnough, - estimate: data.estimate, - balance: data.balance, - }; - } catch (error) { - console.error('Fehler beim Prüfen des Token-Guthabens:', error); - return { hasEnough: false, estimate: null, balance: 0 }; - } + return aiApi.estimate({ + prompt, + model, + estimatedCompletionLength, + referencedDocuments, + }); }; /** @@ -119,31 +65,15 @@ export const generateText = async ( options: AIGenerationOptions = {} ): Promise => { try { - const token = await getAuthToken(); - - const response = await fetch(`${BACKEND_URL}/api/v1/ai/generate/mobile`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - prompt, - model: options.model || 'ollama/gemma3:4b', - temperature: options.temperature, - maxTokens: options.maxTokens, - documentId: options.documentId, - referencedDocuments: options.referencedDocuments, - }), + const result = await aiApi.generate({ + prompt, + model: options.model || 'ollama/gemma3:4b', + temperature: options.temperature, + maxTokens: options.maxTokens, + documentId: options.documentId, + referencedDocuments: options.referencedDocuments, }); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `Backend error: ${response.status}`); - } - - const result = await response.json(); - return { text: result.text, tokenInfo: result.tokenInfo, diff --git a/apps/context/apps/mobile/services/backendApi.ts b/apps/context/apps/mobile/services/backendApi.ts new file mode 100644 index 000000000..bd19ffc79 --- /dev/null +++ b/apps/context/apps/mobile/services/backendApi.ts @@ -0,0 +1,438 @@ +/** + * Backend API Client for Context Mobile App + * Handles all communication with the Context NestJS backend + */ +import * as SecureStore from 'expo-secure-store'; + +const BACKEND_URL = + process.env.EXPO_PUBLIC_BACKEND_URL || + process.env.EXPO_PUBLIC_CONTEXT_BACKEND_URL || + 'http://localhost:3020'; + +// Token storage key (must match what @manacore/shared-auth uses) +const APP_TOKEN_KEY = '@manacore/app_token'; + +// ============================================================================ +// Types (re-exported for consumers) +// ============================================================================ + +export type Space = { + id: string; + name: string; + description: string | null; + user_id: string; + created_at: string; + settings: any | null; + pinned: boolean; + prefix?: string; + text_doc_counter?: number; + context_doc_counter?: number; + prompt_doc_counter?: number; +}; + +export type DocumentMetadata = { + tags?: string[]; + word_count?: number; + token_count?: number; + [key: string]: any; +}; + +export type Document = { + id: string; + title: string; + content: string | null; + type: 'text' | 'context' | 'prompt'; + space_id: string | null; + user_id: string; + created_at: string; + updated_at: string; + metadata: DocumentMetadata | null; + short_id?: string; + pinned?: boolean; +}; + +export type TokenTransaction = { + id: string; + user_id: string; + amount: number; + transaction_type: string; + model_used?: string; + prompt_tokens?: number; + completion_tokens?: number; + total_tokens?: number; + cost_usd?: number; + document_id?: string; + created_at: string; +}; + +export type TokenUsageStats = { + totalUsed: number; + byModel: Record; + byDate: Record; +}; + +export type ModelPrice = { + id: string; + model_name: string; + input_price_per_1k_tokens: number; + output_price_per_1k_tokens: number; + tokens_per_dollar: number; + created_at: string; + updated_at: string; +}; + +// ============================================================================ +// Base API Functions +// ============================================================================ + +async function getAuthToken(): Promise { + try { + return await SecureStore.getItemAsync(APP_TOKEN_KEY); + } catch { + return null; + } +} + +async function apiRequest( + endpoint: string, + options: RequestInit = {} +): Promise<{ data: T | null; error: string | null }> { + try { + const token = await getAuthToken(); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }; + + if (token) { + (headers as Record)['Authorization'] = `Bearer ${token}`; + } + + const response = await fetch(`${BACKEND_URL}${endpoint}`, { + ...options, + headers, + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`API Error [${response.status}]: ${errorText}`); + return { data: null, error: `API Error: ${response.status}` }; + } + + // Handle empty responses (e.g., DELETE 204) + const text = await response.text(); + if (!text) { + return { data: null, error: null }; + } + + const data = JSON.parse(text); + return { data, error: null }; + } catch (error) { + console.error('API Request failed:', error); + return { data: null, error: error instanceof Error ? error.message : 'Unknown error' }; + } +} + +// ============================================================================ +// Spaces API +// ============================================================================ + +export const spacesApi = { + async list(): Promise { + const { data, error } = await apiRequest('/api/v1/spaces'); + if (error) { + console.error('Failed to fetch spaces:', error); + return []; + } + return data || []; + }, + + async get(id: string): Promise { + const { data, error } = await apiRequest(`/api/v1/spaces/${id}`); + if (error) { + console.error('Failed to fetch space:', error); + return null; + } + return data; + }, + + async create(params: { + name: string; + description?: string; + settings?: any; + pinned?: boolean; + }): Promise<{ data: Space | null; error: string | null }> { + return apiRequest('/api/v1/spaces', { + method: 'POST', + body: JSON.stringify(params), + }); + }, + + async update( + id: string, + updates: Partial + ): Promise<{ success: boolean; error: string | null }> { + const { error } = await apiRequest(`/api/v1/spaces/${id}`, { + method: 'PUT', + body: JSON.stringify(updates), + }); + return { success: !error, error }; + }, + + async delete(id: string): Promise<{ success: boolean; error: string | null }> { + const { error } = await apiRequest(`/api/v1/spaces/${id}`, { + method: 'DELETE', + }); + return { success: !error, error }; + }, +}; + +// ============================================================================ +// Documents API +// ============================================================================ + +export const documentsApi = { + async list(params?: { + spaceId?: string; + preview?: boolean; + limit?: number; + }): Promise { + const searchParams = new URLSearchParams(); + if (params?.spaceId) searchParams.set('spaceId', params.spaceId); + if (params?.preview) searchParams.set('preview', 'true'); + if (params?.limit) searchParams.set('limit', String(params.limit)); + + const query = searchParams.toString(); + const endpoint = `/api/v1/documents${query ? `?${query}` : ''}`; + + const { data, error } = await apiRequest(endpoint); + if (error) { + console.error('Failed to fetch documents:', error); + return []; + } + return data || []; + }, + + async listRecent(limit: number = 5): Promise { + const { data, error } = await apiRequest(`/api/v1/documents/recent?limit=${limit}`); + if (error) { + console.error('Failed to fetch recent documents:', error); + return []; + } + return data || []; + }, + + async get(id: string): Promise { + const { data, error } = await apiRequest(`/api/v1/documents/${id}`); + if (error) { + console.error('Failed to fetch document:', error); + return null; + } + return data; + }, + + async create(params: { + content: string; + type: 'text' | 'context' | 'prompt'; + spaceId?: string; + metadata?: any; + title?: string; + }): Promise<{ data: Document | null; error: string | null }> { + return apiRequest('/api/v1/documents', { + method: 'POST', + body: JSON.stringify({ + content: params.content, + type: params.type, + space_id: params.spaceId, + metadata: params.metadata, + title: params.title, + }), + }); + }, + + async update( + id: string, + updates: Partial + ): Promise<{ success: boolean; error: string | null }> { + const { error } = await apiRequest(`/api/v1/documents/${id}`, { + method: 'PUT', + body: JSON.stringify(updates), + }); + return { success: !error, error }; + }, + + async delete(id: string): Promise<{ success: boolean; error: string | null }> { + const { error } = await apiRequest(`/api/v1/documents/${id}`, { + method: 'DELETE', + }); + return { success: !error, error }; + }, + + async updateTags( + id: string, + tags: string[] + ): Promise<{ success: boolean; error: string | null }> { + const { error } = await apiRequest(`/api/v1/documents/${id}/tags`, { + method: 'PUT', + body: JSON.stringify({ tags }), + }); + return { success: !error, error }; + }, + + async togglePinned( + id: string, + pinned: boolean + ): Promise<{ success: boolean; error: string | null }> { + const { error } = await apiRequest(`/api/v1/documents/${id}/pinned`, { + method: 'PUT', + body: JSON.stringify({ pinned }), + }); + return { success: !error, error }; + }, + + async getVersions(id: string): Promise<{ data: Document[]; error: string | null }> { + const { data, error } = await apiRequest(`/api/v1/documents/${id}/versions`); + return { data: data || [], error }; + }, + + async createVersion( + id: string, + params: { + generationType: 'summary' | 'continuation' | 'rewrite' | 'ideas'; + content: string; + aiModel: string; + prompt: string; + } + ): Promise<{ data: Document | null; error: string | null }> { + return apiRequest(`/api/v1/documents/${id}/versions`, { + method: 'POST', + body: JSON.stringify(params), + }); + }, +}; + +// ============================================================================ +// AI API +// ============================================================================ + +export const aiApi = { + async generate(params: { + prompt: string; + model?: string; + temperature?: number; + maxTokens?: number; + documentId?: string; + referencedDocuments?: { title: string; content: string }[]; + }): Promise<{ + text: string; + tokenInfo: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + tokensUsed: number; + remainingTokens: number; + }; + }> { + const { data, error } = await apiRequest<{ + text: string; + tokenInfo: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + tokensUsed: number; + remainingTokens: number; + }; + }>('/api/v1/ai/generate/mobile', { + method: 'POST', + body: JSON.stringify({ + prompt: params.prompt, + model: params.model || 'ollama/gemma3:4b', + temperature: params.temperature, + maxTokens: params.maxTokens, + documentId: params.documentId, + referencedDocuments: params.referencedDocuments, + }), + }); + + if (error || !data) { + throw new Error(error || 'AI generation failed'); + } + + return data; + }, + + async estimate(params: { + prompt: string; + model: string; + estimatedCompletionLength?: number; + referencedDocuments?: { title: string; content: string }[]; + }): Promise<{ hasEnough: boolean; estimate: any; balance: number }> { + const { data, error } = await apiRequest<{ + hasEnough: boolean; + estimate: any; + balance: number; + }>('/api/v1/ai/estimate/mobile', { + method: 'POST', + body: JSON.stringify({ + prompt: params.prompt, + model: params.model, + estimatedCompletionLength: params.estimatedCompletionLength || 500, + referencedDocuments: params.referencedDocuments, + }), + }); + + if (error || !data) { + console.error('Failed to estimate tokens:', error); + return { hasEnough: false, estimate: null, balance: 0 }; + } + + return data; + }, +}; + +// ============================================================================ +// Tokens API +// ============================================================================ + +export const tokensApi = { + async getBalance(): Promise { + const { data, error } = await apiRequest<{ balance: number }>('/api/v1/tokens/balance'); + if (error || !data) { + console.error('Failed to fetch token balance:', error); + return 0; + } + return data.balance; + }, + + async getStats(timeframe: 'day' | 'week' | 'month' | 'year' = 'month'): Promise { + const { data, error } = await apiRequest( + `/api/v1/tokens/stats?timeframe=${timeframe}` + ); + if (error || !data) { + console.error('Failed to fetch token stats:', error); + return { totalUsed: 0, byModel: {}, byDate: {} }; + } + return data; + }, + + async getTransactions(limit: number = 10, offset: number = 0): Promise { + const { data, error } = await apiRequest( + `/api/v1/tokens/transactions?limit=${limit}&offset=${offset}` + ); + if (error || !data) { + console.error('Failed to fetch token transactions:', error); + return []; + } + return data; + }, + + async getModels(): Promise { + const { data, error } = await apiRequest('/api/v1/tokens/models'); + if (error || !data) { + console.error('Failed to fetch model prices:', error); + return []; + } + return data; + }, +}; diff --git a/apps/context/apps/mobile/services/revenueCatService.ts b/apps/context/apps/mobile/services/revenueCatService.ts index f955c2f6d..4572671d6 100644 --- a/apps/context/apps/mobile/services/revenueCatService.ts +++ b/apps/context/apps/mobile/services/revenueCatService.ts @@ -1,7 +1,7 @@ -import Purchases, { PurchasesPackage, CustomerInfo, PurchasesError } from 'react-native-purchases'; +import Purchases, { PurchasesPackage, CustomerInfo } from 'react-native-purchases'; import { Platform } from 'react-native'; -import { supabase } from '../utils/supabase'; import { getEntitlementForProductId } from './revenueCatProductIds'; +import { tokensApi } from './backendApi'; // Direkt im Code setzen für Testzwecke const REVENUECAT_API_KEY_IOS = 'appl_kRiosNzSxUFTkqPhQEFMVyFWtPM'; @@ -21,7 +21,7 @@ export const ENTITLEMENTS = { }; // Token-Mengen für jedes Paket (in Millionen Credits) -export const TOKEN_AMOUNTS = { +export const TOKEN_AMOUNTS: Record = { // Abonnements (monatlich) [ENTITLEMENTS.MINI_SUB]: 5000000, // 5 Mio Credits [ENTITLEMENTS.PLUS_SUB]: 10000000, // 10 Mio Credits @@ -43,9 +43,9 @@ export function getTokenAmountForPackage(pkg: PurchasesPackage): number { if (!entitlement) return 0; // Entitlement zu TOKEN_AMOUNTS zuordnen - for (const [key, value] of Object.entries(ENTITLEMENTS)) { + for (const [_key, value] of Object.entries(ENTITLEMENTS)) { if (value === entitlement) { - return TOKEN_AMOUNTS[value]; + return TOKEN_AMOUNTS[value] || 0; } } @@ -85,19 +85,10 @@ export const initializeRevenueCat = async (userId: string): Promise => { console.log('RevenueCat erfolgreich initialisiert für Benutzer', userId); - try { - // Aktuellen Benutzer in der Datenbank mit RevenueCat ID aktualisieren - await updateUserRevenueCatId(userId); - console.log('RevenueCat ID in Datenbank aktualisiert'); - } catch (updateError) { - console.error('Fehler beim Aktualisieren der RevenueCat ID:', updateError); - // Fortfahren trotz Fehler - } - try { // Bestehende Käufe synchronisieren console.log('Synchronisiere bestehende Käufe...'); - await syncPurchases(userId); + await syncPurchases(); console.log('Käufe erfolgreich synchronisiert'); } catch (syncError) { console.error('Fehler beim Synchronisieren der Käufe:', syncError); @@ -113,35 +104,15 @@ export const initializeRevenueCat = async (userId: string): Promise => { }; /** - * Aktualisiert die RevenueCat ID des Benutzers in der Datenbank + * Synchronisiert bestehende Käufe */ -const updateUserRevenueCatId = async (userId: string): Promise => { - try { - const { error } = await supabase - .from('users') - .update({ revenue_cat_id: userId }) - .eq('id', userId); - - if (error) { - console.error('Fehler beim Aktualisieren der RevenueCat ID:', error); - } - } catch (error) { - console.error('Fehler beim Aktualisieren der RevenueCat ID:', error); - } -}; - -/** - * Synchronisiert bestehende Käufe mit der Datenbank - */ -const syncPurchases = async (userId: string): Promise => { +const syncPurchases = async (): Promise => { // Auf Web-Plattform überspringen if (Platform.OS === 'web') { return; } try { - console.log('Starte Synchronisierung der Käufe für Benutzer:', userId); - // Prüfen, ob RevenueCat initialisiert ist if (!Purchases.isConfigured) { console.warn('RevenueCat ist nicht konfiguriert. Synchronisierung wird übersprungen.'); @@ -152,38 +123,12 @@ const syncPurchases = async (userId: string): Promise => { const customerInfo = await Purchases.getCustomerInfo(); console.log('Kundeninformationen erfolgreich abgerufen'); - // Aktives Abonnement in der Datenbank aktualisieren + // Aktive Entitlements loggen const entitlements = Object.keys(customerInfo.entitlements.active); console.log('Aktive Entitlements:', entitlements); - if (entitlements.length > 0) { - const currentEntitlement = entitlements[0]; - console.log('Aktuelles Entitlement:', currentEntitlement); - - // Datenbank aktualisieren - console.log('Aktualisiere Entitlement in der Datenbank...'); - const { error } = await supabase - .from('users') - .update({ current_entitlement: currentEntitlement }) - .eq('id', userId); - - if (error) { - console.error('Fehler beim Aktualisieren des Entitlements in der Datenbank:', error); - } else { - console.log('Entitlement erfolgreich in der Datenbank aktualisiert'); - } - - // Wenn ein aktives Abonnement vorhanden ist, Token-Guthaben aktualisieren - if (TOKEN_AMOUNTS[currentEntitlement]) { - console.log('Füge Tokens zum Benutzerguthaben hinzu:', TOKEN_AMOUNTS[currentEntitlement]); - await addTokensToUser(userId, TOKEN_AMOUNTS[currentEntitlement], 'purchase'); - console.log('Tokens erfolgreich hinzugefügt'); - } else { - console.log('Kein Token-Betrag für Entitlement gefunden:', currentEntitlement); - } - } else { - console.log('Keine aktiven Entitlements gefunden'); - } + // Note: Token balance management is now handled server-side + // The backend handles purchase verification and token grants console.log('Synchronisierung der Käufe abgeschlossen'); } catch (error) { @@ -192,7 +137,6 @@ const syncPurchases = async (userId: string): Promise => { console.error('Fehlerdetails:', error.message); console.error('Stack:', error.stack); } - // Fehler werfen, damit er in der aufrufenden Funktion behandelt werden kann throw error; } }; @@ -234,44 +178,9 @@ export async function purchasePackage(pkg: PurchasesPackage): Promise { const { customerInfo } = await Purchases.purchasePackage(pkg); console.log('Purchase successful:', customerInfo); - // Aktualisiere das Token-Guthaben des Benutzers - const tokenAmount = getTokenAmountForPackage(pkg); - if (tokenAmount > 0) { - const { data: user } = await supabase.auth.getUser(); - if (user?.user?.id) { - // Token-Guthaben in der Datenbank aktualisieren - const { error } = await supabase.rpc('add_tokens', { - p_user_id: user.user.id, - p_amount: tokenAmount, - }); - - if (error) { - console.error('Error updating token balance:', error); - return false; - } - - // Bestimme den Transaktionstyp basierend auf dem Pakettyp - const transactionType = - pkg.packageType === 'MONTHLY' || pkg.packageType === 'ANNUAL' - ? 'subscription' - : 'purchase'; - - // Token-Transaktion protokollieren - const { error: transactionError } = await supabase.from('token_transactions').insert({ - user_id: user.user.id, - amount: tokenAmount, - transaction_type: transactionType, - metadata: { - package_id: pkg.product.identifier, - package_type: pkg.packageType, - }, - }); - - if (transactionError) { - console.error('Error logging token transaction:', transactionError); - } - } - } + // Note: Token balance updates should be handled server-side + // via a webhook or backend verification of the purchase receipt + // For now, we trust that the backend will sync the balance return true; } catch (error) { @@ -281,80 +190,12 @@ export async function purchasePackage(pkg: PurchasesPackage): Promise { } /** - * Fügt Tokens zum Guthaben des Benutzers hinzu und protokolliert die Transaktion - */ -const addTokensToUser = async ( - userId: string, - amount: number, - source: string -): Promise => { - try { - // Aktuelles Token-Guthaben abrufen - const { data: userData, error: userError } = await supabase - .from('users') - .select('token_balance') - .eq('id', userId) - .single(); - - if (userError || !userData) { - console.error('Fehler beim Abrufen des Token-Guthabens:', userError); - return false; - } - - const currentBalance = userData.token_balance || 0; - const newBalance = currentBalance + amount; - - // Token-Guthaben aktualisieren - const { error: updateError } = await supabase - .from('users') - .update({ token_balance: newBalance }) - .eq('id', userId); - - if (updateError) { - console.error('Fehler beim Aktualisieren des Token-Guthabens:', updateError); - return false; - } - - // Transaktion protokollieren - const { error: transactionError } = await supabase.from('token_transactions').insert({ - user_id: userId, - amount, - transaction_type: source, - }); - - if (transactionError) { - console.error('Fehler beim Protokollieren der Transaktion:', transactionError); - return false; - } - - return true; - } catch (error) { - console.error('Fehler beim Hinzufügen von Tokens:', error); - return false; - } -}; - -/** - * Ruft das aktuelle Token-Guthaben des Benutzers ab + * Ruft das aktuelle Token-Guthaben des Benutzers ab (via backend API) */ export const getCurrentTokenBalance = async (): Promise => { try { - // Benutzer-ID aus der aktuellen Session abrufen - const { data: user } = await supabase.auth.getUser(); - if (!user?.user?.id) return null; - - const { data, error } = await supabase - .from('users') - .select('token_balance') - .eq('id', user.user.id) - .single(); - - if (error || !data) { - console.error('Fehler beim Abrufen des Token-Guthabens:', error); - return null; - } - - return data.token_balance || 0; + const balance = await tokensApi.getBalance(); + return balance; } catch (error) { console.error('Fehler beim Abrufen des Token-Guthabens:', error); return null; @@ -388,7 +229,6 @@ export const getCustomerInfo = async (): Promise => { /** * Setzt die Benutzer-ID für RevenueCat - * Nützlich, wenn sich der Benutzer anmeldet oder abmeldet */ export const setRevenueCatUserId = async (userId: string): Promise => { // Auf Web-Plattform überspringen diff --git a/apps/context/apps/mobile/services/spaceService.ts b/apps/context/apps/mobile/services/spaceService.ts deleted file mode 100644 index 84fabbfb6..000000000 --- a/apps/context/apps/mobile/services/spaceService.ts +++ /dev/null @@ -1,629 +0,0 @@ -import { supabase } from '../utils/supabase'; -import { User } from '@supabase/supabase-js'; - -// Definiere den Typ für einen Space -export type Space = { - id: string; - name: string; - description: string | null; - created_by: string; - created_at: string; - settings: any | null; -}; - -// Definiere den Typ für einen Space mit zusätzlichen Informationen -export type SpaceWithDetails = Space & { - document_count: number; - tags: string[]; - members: { - id: string; - name: string; - email: string; - role: 'owner' | 'editor' | 'viewer'; - }[]; -}; - -// Definiere den Typ für die Erstellung eines neuen Space -export type CreateSpaceParams = { - name: string; - description?: string; - settings?: any; -}; - -// Definiere den Typ für die Aktualisierung eines Space -export type UpdateSpaceParams = { - name?: string; - description?: string; - settings?: any; -}; - -/** - * Ruft alle Spaces ab, auf die der aktuelle Benutzer Zugriff hat - * @returns Promise mit einem Array von Spaces - */ -export const getSpaces = async (): Promise<{ - data: Space[] | null; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { data: null, error: new Error('Benutzer nicht angemeldet') }; - } - - // Rufe alle Spaces ab, bei denen der Benutzer Mitglied ist - const { data, error } = await supabase - .from('spaces') - .select( - ` - *, - space_members!inner(user_id) - ` - ) - .eq('space_members.user_id', user.id); - - if (error) { - console.error('Fehler beim Abrufen der Spaces:', error); - return { data: null, error }; - } - - return { data, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Abrufen der Spaces:', error); - return { data: null, error }; - } -}; - -/** - * Ruft einen Space anhand seiner ID ab - * @param id ID des Space - * @returns Promise mit dem Space - */ -export const getSpaceById = async ( - id: string -): Promise<{ - data: SpaceWithDetails | null; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { data: null, error: new Error('Benutzer nicht angemeldet') }; - } - - // Rufe den Space ab - const { data: spaceData, error: spaceError } = await supabase - .from('spaces') - .select( - ` - *, - space_members!inner(user_id) - ` - ) - .eq('id', id) - .eq('space_members.user_id', user.id) - .single(); - - if (spaceError) { - console.error('Fehler beim Abrufen des Space:', spaceError); - return { data: null, error: spaceError }; - } - - // Rufe die Anzahl der Dokumente im Space ab - const { count: documentCount, error: countError } = await supabase - .from('document_space') - .select('*', { count: 'exact', head: true }) - .eq('space_id', id); - - if (countError) { - console.error('Fehler beim Abrufen der Dokumentanzahl:', countError); - return { data: null, error: countError }; - } - - // Rufe die Mitglieder des Space ab - const { data: membersData, error: membersError } = await supabase - .from('space_members') - .select( - ` - id, - role, - users ( - id, - name, - email - ) - ` - ) - .eq('space_id', id); - - if (membersError) { - console.error('Fehler beim Abrufen der Mitglieder:', membersError); - return { data: null, error: membersError }; - } - - // Extrahiere Tags aus den Einstellungen (falls vorhanden) - const tags = spaceData.settings?.tags || []; - - // Transformiere die Mitgliederdaten - const members = membersData.map((member: any) => ({ - id: member.users?.id, - name: member.users?.name, - email: member.users?.email, - role: member.role, - })); - - // Erstelle das erweiterte Space-Objekt - const spaceWithDetails: SpaceWithDetails = { - ...spaceData, - document_count: documentCount || 0, - tags, - members, - }; - - return { data: spaceWithDetails, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Abrufen des Space:', error); - return { data: null, error }; - } -}; - -/** - * Erstellt einen neuen Space - * @param params Parameter für den neuen Space - * @returns Promise mit dem erstellten Space - */ -export const createSpace = async ( - params: CreateSpaceParams -): Promise<{ - data: Space | null; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { data: null, error: new Error('Benutzer nicht angemeldet') }; - } - - // Verwende eine Transaktion mit rpc, um das Problem mit der RLS-Policy zu umgehen - const { data, error } = await supabase.rpc('create_space_with_owner', { - space_name: params.name, - space_description: params.description || null, - space_settings: params.settings || null, - owner_id: user.id, - }); - - if (error) { - console.error('Fehler beim Erstellen des Space:', error); - return { data: null, error }; - } - - // Hole den neu erstellten Space - const { data: spaceData, error: fetchError } = await supabase - .from('spaces') - .select('*') - .eq('id', data.space_id) - .single(); - - if (fetchError) { - console.error('Fehler beim Abrufen des erstellten Space:', fetchError); - return { data: null, error: fetchError }; - } - - return { data: spaceData, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Erstellen des Space:', error); - return { data: null, error }; - } -}; - -/** - * Aktualisiert einen Space - * @param id ID des Space - * @param params Parameter für die Aktualisierung - * @returns Promise mit dem aktualisierten Space - */ -export const updateSpace = async ( - id: string, - params: UpdateSpaceParams -): Promise<{ - data: Space | null; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { data: null, error: new Error('Benutzer nicht angemeldet') }; - } - - // Prüfe, ob der Benutzer Besitzer oder Editor des Space ist - const { data: memberData, error: memberError } = await supabase - .from('space_members') - .select('role') - .eq('space_id', id) - .eq('user_id', user.id) - .single(); - - if (memberError) { - console.error('Fehler beim Prüfen der Berechtigung:', memberError); - return { data: null, error: memberError }; - } - - if (memberData.role !== 'owner' && memberData.role !== 'editor') { - return { - data: null, - error: new Error('Keine Berechtigung zum Bearbeiten des Space'), - }; - } - - // Erstelle ein Objekt mit den zu aktualisierenden Feldern - const updateData: any = {}; - if (params.name !== undefined) updateData.name = params.name; - if (params.description !== undefined) updateData.description = params.description; - if (params.settings !== undefined) updateData.settings = params.settings; - - // Aktualisiere den Space - const { data: spaceData, error: spaceError } = await supabase - .from('spaces') - .update(updateData) - .eq('id', id) - .select() - .single(); - - if (spaceError) { - console.error('Fehler beim Aktualisieren des Space:', spaceError); - return { data: null, error: spaceError }; - } - - return { data: spaceData, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Aktualisieren des Space:', error); - return { data: null, error }; - } -}; - -/** - * Löscht einen Space - * @param id ID des Space - * @returns Promise mit dem Ergebnis der Löschung - */ -export const deleteSpace = async ( - id: string -): Promise<{ - success: boolean; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { success: false, error: new Error('Benutzer nicht angemeldet') }; - } - - // Prüfe, ob der Benutzer Besitzer des Space ist - const { data: memberData, error: memberError } = await supabase - .from('space_members') - .select('role') - .eq('space_id', id) - .eq('user_id', user.id) - .single(); - - if (memberError) { - console.error('Fehler beim Prüfen der Berechtigung:', memberError); - return { success: false, error: memberError }; - } - - if (memberData.role !== 'owner') { - return { - success: false, - error: new Error('Keine Berechtigung zum Löschen des Space'), - }; - } - - // Lösche alle Mitgliedschaften für diesen Space - const { error: membersError } = await supabase - .from('space_members') - .delete() - .eq('space_id', id); - - if (membersError) { - console.error('Fehler beim Löschen der Mitgliedschaften:', membersError); - return { success: false, error: membersError }; - } - - // Lösche alle Dokument-Space-Verknüpfungen - const { error: docSpaceError } = await supabase - .from('document_space') - .delete() - .eq('space_id', id); - - if (docSpaceError) { - console.error('Fehler beim Löschen der Dokument-Space-Verknüpfungen:', docSpaceError); - return { success: false, error: docSpaceError }; - } - - // Lösche den Space - const { error: spaceError } = await supabase.from('spaces').delete().eq('id', id); - - if (spaceError) { - console.error('Fehler beim Löschen des Space:', spaceError); - return { success: false, error: spaceError }; - } - - return { success: true, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Löschen des Space:', error); - return { success: false, error }; - } -}; - -/** - * Fügt einen Benutzer zu einem Space hinzu - * @param spaceId ID des Space - * @param email E-Mail-Adresse des Benutzers - * @param role Rolle des Benutzers - * @returns Promise mit dem Ergebnis der Hinzufügung - */ -export const addMemberToSpace = async ( - spaceId: string, - email: string, - role: 'editor' | 'viewer' -): Promise<{ - success: boolean; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { success: false, error: new Error('Benutzer nicht angemeldet') }; - } - - // Prüfe, ob der Benutzer Besitzer des Space ist - const { data: memberData, error: memberError } = await supabase - .from('space_members') - .select('role') - .eq('space_id', spaceId) - .eq('user_id', user.id) - .single(); - - if (memberError) { - console.error('Fehler beim Prüfen der Berechtigung:', memberError); - return { success: false, error: memberError }; - } - - if (memberData.role !== 'owner') { - return { - success: false, - error: new Error('Keine Berechtigung zum Hinzufügen von Mitgliedern'), - }; - } - - // Suche den Benutzer anhand der E-Mail-Adresse - const { data: userData, error: userError } = await supabase - .from('users') - .select('id') - .eq('email', email) - .single(); - - if (userError) { - console.error('Fehler beim Suchen des Benutzers:', userError); - return { - success: false, - error: new Error('Benutzer mit dieser E-Mail-Adresse nicht gefunden'), - }; - } - - // Prüfe, ob der Benutzer bereits Mitglied des Space ist - const { data: existingMember, error: existingError } = await supabase - .from('space_members') - .select('id') - .eq('space_id', spaceId) - .eq('user_id', userData.id); - - if (existingError) { - console.error('Fehler beim Prüfen der Mitgliedschaft:', existingError); - return { success: false, error: existingError }; - } - - if (existingMember && existingMember.length > 0) { - return { - success: false, - error: new Error('Benutzer ist bereits Mitglied dieses Space'), - }; - } - - // Füge den Benutzer zum Space hinzu - const { error: addError } = await supabase.from('space_members').insert([ - { - space_id: spaceId, - user_id: userData.id, - role, - }, - ]); - - if (addError) { - console.error('Fehler beim Hinzufügen des Mitglieds:', addError); - return { success: false, error: addError }; - } - - return { success: true, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Hinzufügen des Mitglieds:', error); - return { success: false, error }; - } -}; - -/** - * Ändert die Rolle eines Mitglieds in einem Space - * @param spaceId ID des Space - * @param userId ID des Benutzers - * @param newRole Neue Rolle des Benutzers - * @returns Promise mit dem Ergebnis der Änderung - */ -export const updateMemberRole = async ( - spaceId: string, - userId: string, - newRole: 'owner' | 'editor' | 'viewer' -): Promise<{ - success: boolean; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { success: false, error: new Error('Benutzer nicht angemeldet') }; - } - - // Prüfe, ob der Benutzer Besitzer des Space ist - const { data: memberData, error: memberError } = await supabase - .from('space_members') - .select('role') - .eq('space_id', spaceId) - .eq('user_id', user.id) - .single(); - - if (memberError) { - console.error('Fehler beim Prüfen der Berechtigung:', memberError); - return { success: false, error: memberError }; - } - - if (memberData.role !== 'owner') { - return { - success: false, - error: new Error('Keine Berechtigung zum Ändern von Rollen'), - }; - } - - // Wenn die neue Rolle 'owner' ist, muss der aktuelle Besitzer herabgestuft werden - if (newRole === 'owner') { - // Aktualisiere die Rolle des aktuellen Besitzers zu 'editor' - const { error: updateOwnerError } = await supabase - .from('space_members') - .update({ role: 'editor' }) - .eq('space_id', spaceId) - .eq('user_id', user.id); - - if (updateOwnerError) { - console.error('Fehler beim Aktualisieren des aktuellen Besitzers:', updateOwnerError); - return { success: false, error: updateOwnerError }; - } - } - - // Aktualisiere die Rolle des Mitglieds - const { error: updateError } = await supabase - .from('space_members') - .update({ role: newRole }) - .eq('space_id', spaceId) - .eq('user_id', userId); - - if (updateError) { - console.error('Fehler beim Aktualisieren der Rolle:', updateError); - return { success: false, error: updateError }; - } - - return { success: true, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Aktualisieren der Rolle:', error); - return { success: false, error }; - } -}; - -/** - * Entfernt ein Mitglied aus einem Space - * @param spaceId ID des Space - * @param userId ID des Benutzers - * @returns Promise mit dem Ergebnis der Entfernung - */ -export const removeMemberFromSpace = async ( - spaceId: string, - userId: string -): Promise<{ - success: boolean; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { success: false, error: new Error('Benutzer nicht angemeldet') }; - } - - // Prüfe, ob der Benutzer Besitzer des Space ist - const { data: memberData, error: memberError } = await supabase - .from('space_members') - .select('role') - .eq('space_id', spaceId) - .eq('user_id', user.id) - .single(); - - if (memberError) { - console.error('Fehler beim Prüfen der Berechtigung:', memberError); - return { success: false, error: memberError }; - } - - if (memberData.role !== 'owner') { - return { - success: false, - error: new Error('Keine Berechtigung zum Entfernen von Mitgliedern'), - }; - } - - // Prüfe, ob der zu entfernende Benutzer nicht der Besitzer ist - const { data: targetMemberData, error: targetMemberError } = await supabase - .from('space_members') - .select('role') - .eq('space_id', spaceId) - .eq('user_id', userId) - .single(); - - if (targetMemberError) { - console.error('Fehler beim Prüfen des Zielmitglieds:', targetMemberError); - return { success: false, error: targetMemberError }; - } - - if (targetMemberData.role === 'owner') { - return { - success: false, - error: new Error('Der Besitzer kann nicht entfernt werden'), - }; - } - - // Entferne das Mitglied aus dem Space - const { error: removeError } = await supabase - .from('space_members') - .delete() - .eq('space_id', spaceId) - .eq('user_id', userId); - - if (removeError) { - console.error('Fehler beim Entfernen des Mitglieds:', removeError); - return { success: false, error: removeError }; - } - - return { success: true, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Entfernen des Mitglieds:', error); - return { success: false, error }; - } -}; diff --git a/apps/context/apps/mobile/services/spaceServiceDirect.ts b/apps/context/apps/mobile/services/spaceServiceDirect.ts deleted file mode 100644 index 04bc90baf..000000000 --- a/apps/context/apps/mobile/services/spaceServiceDirect.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { supabase } from '../utils/supabase'; - -// Definiere den Typ für einen Space -export type Space = { - id: string; - name: string; - description: string | null; - created_by: string; - created_at: string; - settings: any | null; -}; - -// Definiere den Typ für einen Space mit zusätzlichen Informationen -export type SpaceWithDetails = Space & { - document_count: number; - tags: string[]; - members: { - id: string; - name: string; - email: string; - role: 'owner' | 'editor' | 'viewer'; - }[]; -}; - -// Definiere den Typ für die Erstellung eines neuen Space -export type CreateSpaceParams = { - name: string; - description?: string; - settings?: any; -}; - -// Definiere den Typ für die Aktualisierung eines Space -export type UpdateSpaceParams = { - name?: string; - description?: string; - settings?: any; -}; - -/** - * Ruft alle Spaces ab, auf die der aktuelle Benutzer Zugriff hat - * Diese Implementierung umgeht die RLS-Policies durch direkte SQL-Ausführung - * @returns Promise mit einem Array von Spaces - */ -export const getSpacesDirectly = async (): Promise<{ - data: Space[] | null; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { data: null, error: new Error('Benutzer nicht angemeldet') }; - } - - // Führe eine direkte SQL-Abfrage aus, um die Spaces abzurufen - const { data, error } = await supabase.rpc('get_user_spaces', { - p_user_id: user.id, - }); - - if (error) { - console.error('Fehler beim Abrufen der Spaces:', error); - return { data: null, error }; - } - - return { data, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Abrufen der Spaces:', error); - return { data: null, error }; - } -}; - -/** - * Ruft einen Space anhand seiner ID ab - * Diese Implementierung verwendet eine direkte SQL-Abfrage, um die RLS-Policies zu umgehen - * @param id ID des Space - * @returns Promise mit dem Space - */ -export const getSpaceByIdDirectly = async ( - id: string -): Promise<{ - data: SpaceWithDetails | null; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { data: null, error: new Error('Benutzer nicht angemeldet') }; - } - - // Verwende eine direkte SQL-Abfrage, um die RLS-Policies zu umgehen - const { data, error } = await supabase.rpc('get_space_by_id_direct', { - p_space_id: id, - p_user_id: user.id, - }); - - if (error) { - console.error('Fehler beim Abrufen des Space:', error); - return { data: null, error }; - } - - // Erstelle ein Dummy-Space-Objekt für die Entwicklung - // Dieses Objekt wird verwendet, wenn die SQL-Funktion noch nicht verfügbar ist - const dummySpace: SpaceWithDetails = { - id: id, - name: 'Test Space', - description: 'Dies ist ein Test-Space für die Entwicklung', - created_by: user.id, - created_at: new Date().toISOString(), - settings: { tags: ['Test', 'Entwicklung'] }, - document_count: 0, - tags: ['Test', 'Entwicklung'], - members: [ - { - id: user.id, - name: user.email?.split('@')[0] || 'Benutzer', - email: user.email || '', - role: 'owner', - }, - ], - }; - - return { data: dummySpace, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Abrufen des Space:', error); - return { data: null, error }; - } -}; - -/** - * Erstellt einen neuen Space und fügt den Ersteller als Besitzer hinzu - * Diese Implementierung umgeht die RLS-Policies durch direkte SQL-Ausführung - * @param params Parameter für den neuen Space - * @returns Promise mit dem erstellten Space - */ -export const createSpaceDirectly = async ( - params: CreateSpaceParams -): Promise<{ - data: Space | null; - error: Error | null; -}> => { - try { - // Rufe die aktuelle Session ab, um die Benutzer-ID zu erhalten - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) { - return { data: null, error: new Error('Benutzer nicht angemeldet') }; - } - - // Führe eine direkte SQL-Abfrage aus, um den Space zu erstellen und den Besitzer hinzuzufügen - const { data, error } = await supabase.rpc('create_space_direct', { - p_name: params.name, - p_description: params.description || null, - p_settings: params.settings || null, - p_user_id: user.id, - }); - - if (error) { - console.error('Fehler beim Erstellen des Space:', error); - return { data: null, error }; - } - - // Konvertiere das Ergebnis in den erwarteten Typ - const space: Space = { - id: data.id, - name: data.name, - description: data.description, - created_by: data.created_by, - created_at: data.created_at, - settings: data.settings, - }; - - return { data: space, error: null }; - } catch (error: any) { - console.error('Unerwarteter Fehler beim Erstellen des Space:', error); - return { data: null, error }; - } -}; diff --git a/apps/context/apps/mobile/services/supabaseService.ts b/apps/context/apps/mobile/services/supabaseService.ts index 328c0f845..180f801fe 100644 --- a/apps/context/apps/mobile/services/supabaseService.ts +++ b/apps/context/apps/mobile/services/supabaseService.ts @@ -1,7 +1,20 @@ -import { supabase } from '../utils/supabase'; -import { updateDocumentTokenCount } from './tokenCountingService'; +/** + * Data service for Context mobile app. + * Now routes all operations through the Context NestJS backend API + * instead of direct Supabase database access. + */ +import { + spacesApi, + documentsApi, + type Space, + type Document, + type DocumentMetadata, +} from './backendApi'; -// Typdefinitionen +// Re-export types for backward compatibility +export type { Space, Document, DocumentMetadata }; + +// Also export the User type for backward compatibility export type User = { id: string; email: string; @@ -9,80 +22,16 @@ export type User = { created_at: string; }; -export type Space = { - id: string; - name: string; - description: string | null; - user_id: string; - created_at: string; - settings: any | null; - pinned: boolean; - prefix?: string; - text_doc_counter?: number; - context_doc_counter?: number; - prompt_doc_counter?: number; -}; +// ============================================================================ +// Space Services +// ============================================================================ -export type DocumentMetadata = { - tags?: string[]; - word_count?: number; - token_count?: number; // Anzahl der Tokens im Dokument - [key: string]: any; // Erlaubt weitere Metadaten -}; - -export type Document = { - id: string; - title: string; - content: string | null; - type: 'text' | 'context' | 'prompt'; - space_id: string | null; - user_id: string; - created_at: string; - updated_at: string; - metadata: DocumentMetadata | null; - short_id?: string; // Neue kurze ID für benutzerfreundliche Referenzen - pinned?: boolean; // Flag, um Dokumente anzupinnen -}; - -// Benutzer-Services -export const getCurrentUser = async (): Promise => { - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) return null; - - const { data } = await supabase.from('users').select('*').eq('id', user.id).single(); - - return data; -}; - -export const updateUserProfile = async ( - name: string -): Promise<{ success: boolean; error: any }> => { - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) return { success: false, error: 'Nicht angemeldet' }; - - const { error } = await supabase.from('users').update({ name }).eq('id', user.id); - - return { success: !error, error }; -}; - -// Space-Services export const getSpaces = async (): Promise => { - const { data } = await supabase - .from('spaces') - .select('*') - .order('created_at', { ascending: false }); - - return data || []; + return spacesApi.list(); }; export const getSpaceById = async (id: string): Promise => { - const { data } = await supabase.from('spaces').select('*').eq('id', id).single(); - - return data; + return spacesApi.get(id); }; export const createSpace = async ( @@ -91,156 +40,57 @@ export const createSpace = async ( settings?: any, pinned: boolean = true ): Promise<{ data: Space | null; error: any }> => { - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) return { data: null, error: 'Nicht angemeldet' }; - - // Überprüfen, ob der Benutzer bereits in der users-Tabelle existiert - const { data: existingUser } = await supabase - .from('users') - .select('id') - .eq('id', user.id) - .single(); - - // Wenn der Benutzer nicht existiert, fügen wir ihn hinzu - if (!existingUser) { - const { error: userError } = await supabase.from('users').insert([ - { - id: user.id, - email: user.email || '', - name: user.user_metadata?.name || null, - }, - ]); - - if (userError) { - return { data: null, error: `Fehler beim Erstellen des Benutzers: ${userError.message}` }; - } - } - - // Jetzt können wir den Space erstellen - const { data, error } = await supabase - .from('spaces') - .insert([ - { - name, - description: description || null, - user_id: user.id, - settings: settings || null, - pinned: pinned, - }, - ]) - .select() - .single(); - - return { data, error }; + const result = await spacesApi.create({ name, description, settings, pinned }); + return { data: result.data, error: result.error }; }; export const updateSpace = async ( id: string, updates: Partial ): Promise<{ success: boolean; error: any }> => { - const { error } = await supabase.from('spaces').update(updates).eq('id', id); - - return { success: !error, error }; + return spacesApi.update(id, updates); }; export const toggleSpacePinned = async ( id: string, pinned: boolean ): Promise<{ success: boolean; error: any }> => { - const { error } = await supabase.from('spaces').update({ pinned }).eq('id', id); - - return { success: !error, error }; + return spacesApi.update(id, { pinned }); }; export const deleteSpace = async (id: string): Promise<{ success: boolean; error: any }> => { - // Zuerst alle Dokumente in diesem Space löschen - await supabase.from('documents').delete().eq('space_id', id); - - // Dann den Space löschen - const { error } = await supabase.from('spaces').delete().eq('id', id); - - return { success: !error, error }; + return spacesApi.delete(id); }; -// Dokument-Services +// ============================================================================ +// Document Services +// ============================================================================ + export const getDocuments = async (spaceId?: string): Promise => { - let query = supabase - .from('documents') - .select('*') - .order('pinned', { ascending: false }) // Zuerst nach Pinned-Status sortieren - .order('updated_at', { ascending: false }); // Dann nach Aktualisierungsdatum - - if (spaceId) { - query = query.eq('space_id', spaceId); - } - - const { data } = await query; - return data || []; + return documentsApi.list({ spaceId }); }; -// Optimierte Funktion für das Laden von Dokumenten mit begrenztem Inhalt für bessere Performance export const getDocumentsWithPreview = async ( spaceId?: string, limit: number = 50 ): Promise => { - let query = supabase - .from('documents') - .select( - 'id, title, content, type, space_id, user_id, created_at, updated_at, metadata, short_id, pinned' - ) - .order('pinned', { ascending: false }) - .order('updated_at', { ascending: false }) - .limit(limit); - - if (spaceId) { - query = query.eq('space_id', spaceId); - } - - const { data } = await query; - - // Truncate content to first 200 characters for preview - return (data || []).map((doc) => ({ - ...doc, - content: - doc.content && doc.content.length > 200 ? `${doc.content.substring(0, 200)}...` : doc.content, - })); + return documentsApi.list({ spaceId, preview: true, limit }); }; -// Holt die neuesten Dokumente für den aktuellen Benutzer export const getRecentDocuments = async (limit: number = 5): Promise => { - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) return []; - - const { data } = await supabase - .from('documents') - .select('*') - .eq('user_id', user.id) - .order('updated_at', { ascending: false }) - .limit(limit); - - return data || []; + return documentsApi.listRecent(limit); }; export const getDocumentById = async (id: string): Promise => { - const { data } = await supabase.from('documents').select('*').eq('id', id).single(); - - return data; + return documentsApi.get(id); }; -// Findet ein Dokument anhand seiner kurzen ID export const getDocumentByShortId = async (shortId: string): Promise => { - const { data } = await supabase.from('documents').select('*').eq('short_id', shortId).single(); - - return data; + // The backend should handle short_id lookup via the same GET endpoint + // Try fetching by ID first - if the backend supports short_id resolution this will work + return documentsApi.get(shortId); }; -import { extractTitleFromMarkdown } from '~/utils/markdown'; -import { countWords } from '~/utils/textUtils'; - export const createDocument = async ( content: string, type: 'text' | 'context' | 'prompt', @@ -248,328 +98,64 @@ export const createDocument = async ( metadata?: any, title?: string ): Promise<{ data: Document | null; error: any }> => { - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - - if (!user) return { data: null, error: 'Nicht angemeldet' }; - - // If title is not provided, extract it from content - const documentTitle = title || extractTitleFromMarkdown(content); - - // Wortanzahl und Token-Anzahl berechnen und zu den Metadaten hinzufügen - const wordCount = countWords(content); - - // Berechne die Token-Anzahl und aktualisiere die Metadaten - const { metadata: updatedMetadataWithTokens } = updateDocumentTokenCount({ + const result = await documentsApi.create({ content, - metadata: metadata || {}, + type, + spaceId, + metadata, + title, }); - - // Füge die Wortanzahl hinzu - const updatedMetadata = { - ...updatedMetadataWithTokens, - word_count: wordCount, - }; - - // Generiere eine kurze ID, wenn ein Space angegeben ist - let shortId: string | undefined; - - if (spaceId) { - try { - // Hole Space-Informationen - const { data: spaceData } = await supabase - .from('spaces') - .select('prefix, text_doc_counter, context_doc_counter, prompt_doc_counter') - .eq('id', spaceId) - .single(); - - console.log('Space-Daten:', spaceData); - - if (!spaceData || !spaceData.prefix) { - console.error('Space nicht gefunden oder kein Präfix vorhanden'); - // Fallback-ID generieren - shortId = `DOC-${Math.random().toString(36).substring(2, 8)}`; - } else { - // Bestimme den richtigen Zähler basierend auf dem Dokumenttyp - let counter = 0; - if (type === 'text') { - counter = (spaceData.text_doc_counter || 0) + 1; - } else if (type === 'context') { - counter = (spaceData.context_doc_counter || 0) + 1; - } else if (type === 'prompt') { - counter = (spaceData.prompt_doc_counter || 0) + 1; - } - - // Aktualisiere den Zähler in der Datenbank - const counterField = `${type}_doc_counter`; - await supabase - .from('spaces') - .update({ [counterField]: counter }) - .eq('id', spaceId); - - // Generiere die short_id - const typeChar = type === 'text' ? 'D' : type === 'context' ? 'C' : 'P'; - shortId = `${spaceData.prefix}${typeChar}${counter}`; - console.log('Generierte short_id:', shortId); - } - } catch (error) { - console.error('Fehler bei der Generierung der kurzen ID:', error); - // Fallback-ID generieren - shortId = `DOC-${Math.random().toString(36).substring(2, 8)}`; - } - } else { - // Für Dokumente ohne Space, generiere eine generische ID - shortId = `DOC-${Math.random().toString(36).substring(2, 8)}`; - } - - const { data, error } = await supabase - .from('documents') - .insert([ - { - title: documentTitle, - content, - type, - space_id: spaceId || null, - user_id: user.id, - metadata: updatedMetadata, - short_id: shortId, - }, - ]) - .select() - .single(); - - return { data, error }; + return { data: result.data, error: result.error }; }; export const updateDocument = async ( id: string, updates: Partial ): Promise<{ success: boolean; error: any }> => { - // Hole das aktuelle Dokument, um Metadaten und andere Informationen zu prüfen - try { - const document = await getDocumentById(id); - if (!document) { - return { success: false, error: 'Dokument nicht gefunden' }; - } - - // Stelle sicher, dass die Metadaten korrekt aktualisiert werden - if (updates.metadata) { - // Wenn Metadaten explizit übergeben wurden, stelle sicher, dass sie mit den bestehenden zusammengeführt werden - updates.metadata = { - ...document.metadata, - ...updates.metadata, - }; - console.log('Aktualisierte Metadaten in updateDocument:', updates.metadata); - } - - // Wenn der Inhalt aktualisiert wird, berechne die neue Wortanzahl und Token-Anzahl - if (updates.content) { - const wordCount = countWords(updates.content); - - // Berechne die Token-Anzahl und aktualisiere die Metadaten - const { metadata: updatedMetadataWithTokens } = updateDocumentTokenCount({ - content: updates.content, - metadata: updates.metadata || document.metadata || {}, - }); - - // Füge die Wortanzahl hinzu - updates.metadata = { - ...updatedMetadataWithTokens, - word_count: wordCount, - }; - } - - // Wenn der Typ geändert wird, müssen wir möglicherweise die short_id aktualisieren - if (updates.type) { - try { - // Prüfe, ob die short_id dem Typ-Präfix-Format entspricht (z.B. MD1, MC2, MP3) - if (document.short_id && document.space_id) { - const currentShortId = document.short_id; - - // Wenn die ID dem Format entspricht (z.B. MD1), aktualisiere nur den Typ-Buchstaben - if (/^[A-Z][CDP]\d+$/.test(currentShortId)) { - const spacePrefix = currentShortId.charAt(0); - const number = currentShortId.substring(2); // Extrahiere die Nummer - const newTypeChar = - updates.type === 'text' ? 'D' : updates.type === 'context' ? 'C' : 'P'; - - // Aktualisiere die short_id mit dem neuen Typ-Buchstaben - updates.short_id = `${spacePrefix}${newTypeChar}${number}`; - console.log(`Aktualisiere short_id von ${currentShortId} zu ${updates.short_id}`); - } - } - } catch (error) { - console.error('Fehler beim Aktualisieren der short_id:', error); - } - } - } catch (error) { - console.error('Fehler beim Laden des Dokuments:', error); - } - - const { error } = await supabase.from('documents').update(updates).eq('id', id); - - return { success: !error, error }; + return documentsApi.update(id, updates); }; export const deleteDocument = async (id: string): Promise<{ success: boolean; error: any }> => { - const { error } = await supabase.from('documents').delete().eq('id', id); - - return { success: !error, error }; + return documentsApi.delete(id); }; -// Setzt oder entfernt das Pinned-Flag für ein Dokument export const toggleDocumentPinned = async ( id: string, pinned: boolean ): Promise<{ success: boolean; error: any }> => { - const { error } = await supabase.from('documents').update({ pinned }).eq('id', id); - - return { success: !error, error }; + return documentsApi.togglePinned(id, pinned); }; -/** - * Speichert Tags für ein Dokument direkt in den Metadaten - * @param id ID des Dokuments - * @param tags Array von Tags - * @returns Erfolg oder Fehler - */ export const saveDocumentTags = async ( id: string, tags: string[] ): Promise<{ success: boolean; error: any }> => { - try { - console.log('saveDocumentTags - Speichere Tags direkt:', tags); - - // Rufe die SQL-Funktion auf, die wir im Supabase SQL Editor erstellt haben - const { error } = await supabase.rpc('update_document_tags', { - document_id: id, - tags_array: tags, - }); - - if (error) { - console.error('Fehler beim Speichern der Tags mit RPC:', error); - - // Fallback-Methode, wenn die RPC-Funktion nicht funktioniert - console.log('Verwende Fallback-Methode für Tag-Update'); - - // Hole das aktuelle Dokument, um die bestehenden Metadaten zu erhalten - const document = await getDocumentById(id); - if (!document) { - return { success: false, error: 'Dokument nicht gefunden' }; - } - - // Aktualisiere die Metadaten mit den neuen Tags - const currentMetadata = document.metadata || {}; - const updatedMetadata = { - ...currentMetadata, - tags: tags, - }; - - console.log('Fallback - Aktualisierte Metadaten:', updatedMetadata); - - // Direktes Update der Metadaten in der Datenbank - const updateResult = await supabase - .from('documents') - .update({ - metadata: updatedMetadata, - // Füge einen Zeitstempel hinzu, um sicherzustellen, dass die Änderung erkannt wird - updated_at: new Date().toISOString(), - }) - .eq('id', id); - - if (updateResult.error) { - console.error('Fehler beim Fallback-Update der Tags:', updateResult.error); - return { success: false, error: updateResult.error }; - } - } - - return { success: true, error: null }; - } catch (error) { - console.error('Fehler beim Speichern der Tags:', error); - return { success: false, error }; - } + return documentsApi.updateTags(id, tags); }; -/** - * Findet alle Versionen eines Dokuments (das Original und alle abgeleiteten Versionen) - * @param documentId ID des Dokuments, für das Versionen gefunden werden sollen - * @returns Array von Dokumenten, die Versionen des angegebenen Dokuments sind - */ export const getDocumentVersions = async ( documentId: string ): Promise<{ data: Document[]; error: any }> => { - try { - // Zuerst das Originaldokument abrufen - const originalDocument = await getDocumentById(documentId); - if (!originalDocument) { - return { data: [], error: 'Dokument nicht gefunden' }; - } - - // Prüfen, ob das Dokument selbst eine abgeleitete Version ist - const isVersion = - originalDocument.metadata && - originalDocument.metadata.parent_document && - originalDocument.metadata.version; - - // Wenn es eine abgeleitete Version ist, verwende die parent_document ID - const rootDocumentId = - isVersion && originalDocument.metadata - ? originalDocument.metadata.parent_document - : documentId; - - // Suche nach allen Dokumenten, die entweder das Original sind oder das Original als parent_document haben - const { data: versions, error } = await supabase - .from('documents') - .select('*') - .or(`id.eq.${rootDocumentId},metadata->parent_document.eq.${rootDocumentId}`); - - if (error) { - return { data: [], error }; - } - - // Sortiere die Versionen nach Erstellungsdatum - const sortedVersions = versions.sort((a, b) => { - // Das Original soll immer zuerst kommen - if (a.id === rootDocumentId) return -1; - if (b.id === rootDocumentId) return 1; - - // Ansonsten nach Erstellungsdatum sortieren - return new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); - }); - - return { data: sortedVersions, error: null }; - } catch (error) { - console.error('Fehler beim Abrufen der Dokumentversionen:', error); - return { data: [], error: `Fehler beim Abrufen der Versionen: ${error}` }; - } + return documentsApi.getVersions(documentId); }; -/** - * Findet die nächste oder vorherige Version eines Dokuments - * @param documentId ID des aktuellen Dokuments - * @param direction 'next' oder 'previous' für die Richtung - * @returns Die ID der nächsten/vorherigen Version oder null, wenn keine existiert - */ export const getAdjacentDocumentVersion = async ( documentId: string, direction: 'next' | 'previous' ): Promise<{ data: string | null; error: any }> => { try { - // Alle Versionen abrufen - const { data: versions, error } = await getDocumentVersions(documentId); + const { data: versions, error } = await documentsApi.getVersions(documentId); if (error || !versions.length) { return { data: null, error: error || 'Keine Versionen gefunden' }; } - // Index des aktuellen Dokuments finden const currentIndex = versions.findIndex((doc) => doc.id === documentId); if (currentIndex === -1) { return { data: null, error: 'Aktuelles Dokument nicht in Versionen gefunden' }; } - // Nächste oder vorherige Version bestimmen let targetIndex; if (direction === 'next') { targetIndex = currentIndex + 1; @@ -577,7 +163,6 @@ export const getAdjacentDocumentVersion = async ( return { data: null, error: 'Keine neuere Version verfügbar' }; } } else { - // 'previous' targetIndex = currentIndex - 1; if (targetIndex < 0) { return { data: null, error: 'Keine ältere Version verfügbar' }; @@ -594,10 +179,6 @@ export const getAdjacentDocumentVersion = async ( } }; -/** - * Erstellt eine neue Version eines Dokuments basierend auf KI-generiertem Text - * Die alte Version bleibt erhalten und wird in der Metadaten-Historie der neuen Version referenziert - */ export const createDocumentVersion = async ( originalDocumentId: string, newContent: string, @@ -605,82 +186,10 @@ export const createDocumentVersion = async ( aiModel: string, prompt: string ): Promise<{ data: Document | null; error: any }> => { - try { - // Originaldokument abrufen - const originalDocument = await getDocumentById(originalDocumentId); - if (!originalDocument) { - return { data: null, error: 'Originaldokument nicht gefunden' }; - } - - // Aktuelle Session abrufen - const { data: sessionData } = await supabase.auth.getSession(); - const user = sessionData?.session?.user; - if (!user) { - return { data: null, error: 'Nicht angemeldet' }; - } - - // Berechne die Wortanzahl des neuen Inhalts - const wordCount = countWords(newContent); - - // Metadaten für die neue Version vorbereiten - const metadata = { - parent_document: originalDocumentId, - original_title: originalDocument.title, - generation_type: generationType, - model_used: aiModel, - prompt_used: prompt, - created_at: new Date().toISOString(), - version: 1, // Startet bei 1 für die erste KI-generierte Version - version_history: [ - { - id: originalDocumentId, - title: originalDocument.title, - type: originalDocument.type, - created_at: originalDocument.created_at, - is_original: true, - }, - ], - word_count: wordCount, - }; - - // Titel für die neue Version basierend auf dem Generationstyp erstellen - let newTitle = ''; - switch (generationType) { - case 'summary': - newTitle = `Zusammenfassung: ${originalDocument.title}`; - break; - case 'continuation': - newTitle = `Fortsetzung: ${originalDocument.title}`; - break; - case 'rewrite': - newTitle = `Umformulierung: ${originalDocument.title}`; - break; - case 'ideas': - newTitle = `Ideen zu: ${originalDocument.title}`; - break; - default: - newTitle = `KI-Version: ${originalDocument.title}`; - } - - // Neue Version des Dokuments erstellen - const { data, error } = await supabase - .from('documents') - .insert([ - { - title: newTitle, - content: newContent, - type: 'generated', // Immer vom Typ 'generated' für KI-generierte Versionen - space_id: originalDocument.space_id, - user_id: user.id, - metadata, - }, - ]) - .select() - .single(); - - return { data, error }; - } catch (error) { - console.error('Fehler beim Erstellen der neuen Dokumentversion:', error); - return { data: null, error: `Fehler beim Erstellen der neuen Version: ${error}` }; - } + return documentsApi.createVersion(originalDocumentId, { + generationType, + content: newContent, + aiModel, + prompt, + }); }; diff --git a/apps/context/apps/mobile/services/tokenCountingService.ts b/apps/context/apps/mobile/services/tokenCountingService.ts index 475258317..fe88c88b6 100644 --- a/apps/context/apps/mobile/services/tokenCountingService.ts +++ b/apps/context/apps/mobile/services/tokenCountingService.ts @@ -1,15 +1,7 @@ -import { supabase } from '../utils/supabase'; +import { tokensApi, type ModelPrice } from './backendApi'; -// Typdefinitionen -export type ModelPrice = { - id: string; - model_name: string; - input_price_per_1k_tokens: number; - output_price_per_1k_tokens: number; - tokens_per_dollar: number; - created_at: string; - updated_at: string; -}; +// Re-export types for backward compatibility +export type { ModelPrice }; export type TokenCostEstimate = { inputTokens: number; @@ -17,7 +9,6 @@ export type TokenCostEstimate = { totalTokens: number; costUsd: number; appTokens: number; - // Neue Felder für die detaillierte Token-Aufschlüsselung basePromptTokens?: number; documentTokens?: number; }; @@ -29,194 +20,57 @@ export type TokenCostEstimate = { */ export const estimateTokens = (text: string): number => { try { - // Prüfe, ob der Text gültig ist if (!text) return 0; if (typeof text !== 'string') { console.warn('estimateTokens: Ungültiger Text-Typ:', typeof text); return 0; } - // Debugging-Ausgabe - console.log(`estimateTokens: Text-Länge = ${text.length} Zeichen`); - // Einfache Schätzung: 1 Token pro 4 Zeichen (für englischen Text) - // Für andere Sprachen kann dies variieren const estimatedTokens = Math.ceil(text.length / 4); - - console.log(`estimateTokens: Geschätzte Tokens = ${estimatedTokens}`); - return estimatedTokens; } catch (error) { console.error('Fehler bei der Token-Schätzung:', error); - return 1; // Fallback-Wert im Fehlerfall + return 1; } }; /** - * Holt die Preisdaten für ein bestimmtes Modell aus der Datenbank + * Holt die Preisdaten für alle Modelle vom Backend */ -export const getModelPrice = async (modelName: string): Promise => { - const { data, error } = await supabase - .from('model_prices') - .select('*') - .eq('model_name', modelName) - .single(); - - if (error) { - console.error('Fehler beim Abrufen der Modellpreise:', error); - return null; - } - - return data; -}; - -/** - * Berechnet die Kosten für eine KI-Anfrage basierend auf dem Modell und der Token-Anzahl - */ -export const calculateCost = async ( - model: string, - promptTokens: number, - completionTokens: number -): Promise => { - try { - // Debugging-Ausgaben - console.log('Berechne Kosten für:', { model, promptTokens, completionTokens }); - - // Stelle sicher, dass die Token-Zahlen gültige Zahlen sind - if (isNaN(promptTokens) || promptTokens < 0) { - console.warn('Ungültige promptTokens:', promptTokens); - promptTokens = 0; - } - - if (isNaN(completionTokens) || completionTokens < 0) { - console.warn('Ungültige completionTokens:', completionTokens); - completionTokens = 0; - } - - // Standardwerte für den Fall, dass keine Preisdaten gefunden werden - let inputPricePer1kTokens = 0.01; // $0.01 pro 1000 Tokens - let outputPricePer1kTokens = 0.03; // $0.03 pro 1000 Tokens - let tokensPerDollar = 50000; // 50.000 App-Tokens pro Dollar - - // Versuche, die tatsächlichen Preisdaten aus der Datenbank zu holen - const modelPrice = await getModelPrice(model); - - if (modelPrice) { - console.log('Modellpreis gefunden:', modelPrice); - inputPricePer1kTokens = modelPrice.input_price_per_1k_tokens; - outputPricePer1kTokens = modelPrice.output_price_per_1k_tokens; - tokensPerDollar = modelPrice.tokens_per_dollar; - } else { - console.warn('Keine Preisdaten für Modell gefunden:', model); - } - - // Berechne die Kosten in USD - const inputCost = (promptTokens / 1000) * inputPricePer1kTokens; - const outputCost = (completionTokens / 1000) * outputPricePer1kTokens; - const totalCostUsd = inputCost + outputCost; - - // Stelle sicher, dass tokensPerDollar ein gültiger Wert ist - if (!tokensPerDollar || isNaN(tokensPerDollar) || tokensPerDollar <= 0) { - tokensPerDollar = 50000; // Standardwert, falls nicht gültig - } - - // Berechne die Anzahl der App-Tokens - const appTokens = Math.ceil(totalCostUsd * tokensPerDollar); - - // Debugging-Ausgabe für das Ergebnis - console.log('Berechnete Kosten:', { - inputCost, - outputCost, - totalCostUsd, - tokensPerDollar, - appTokens, - }); - - return { - inputTokens: promptTokens, - outputTokens: completionTokens, - totalTokens: promptTokens + completionTokens, - costUsd: totalCostUsd, - appTokens: Math.max(1, appTokens), // Mindestens 1 Token - }; - } catch (error) { - console.error('Fehler bei der Kostenberechnung:', error); - - // Fallback-Werte im Fehlerfall - return { - inputTokens: promptTokens || 0, - outputTokens: completionTokens || 0, - totalTokens: (promptTokens || 0) + (completionTokens || 0), - costUsd: 0.01, - appTokens: 1, - }; - } +export const getModelPrices = async (): Promise => { + return tokensApi.getModels(); }; /** * Schätzt die Kosten für einen Prompt und eine erwartete Antwortlänge + * NOTE: For accurate estimates, prefer using aiApi.estimate() which runs server-side */ export const estimateCostForPrompt = async ( prompt: string, model: string, estimatedCompletionLength: number = 500 ): Promise => { - console.log('estimateCostForPrompt aufgerufen mit:', { - promptLength: prompt.length, - model, - estimatedCompletionLength, - }); - const promptTokens = estimateTokens(prompt); - console.log('Berechnete promptTokens:', promptTokens); - const completionTokens = estimatedCompletionLength; - console.log('Rufe calculateCost auf mit:', { - model, - promptTokens, - completionTokens, - }); + // Use default pricing for local estimation + const inputPricePer1kTokens = 0.01; + const outputPricePer1kTokens = 0.03; + const tokensPerDollar = 50000; - const result = await calculateCost(model, promptTokens, completionTokens); - console.log('Ergebnis von calculateCost:', result); + const inputCost = (promptTokens / 1000) * inputPricePer1kTokens; + const outputCost = (completionTokens / 1000) * outputPricePer1kTokens; + const totalCostUsd = inputCost + outputCost; + const appTokens = Math.max(1, Math.ceil(totalCostUsd * tokensPerDollar)); - return result; -}; - -/** - * Konvertiert einen USD-Betrag in App-Tokens basierend auf dem Modell - */ -export const convertUSDToAppTokens = async (usdAmount: number, model: string): Promise => { - // Standardwert für den Fall, dass keine Preisdaten gefunden werden - let tokensPerDollar = 50000; // 50.000 App-Tokens pro Dollar - - // Versuche, die tatsächlichen Preisdaten aus der Datenbank zu holen - const modelPrice = await getModelPrice(model); - - if (modelPrice) { - tokensPerDollar = modelPrice.tokens_per_dollar; - } - - return Math.ceil(usdAmount * tokensPerDollar); -}; - -/** - * Konvertiert App-Tokens in einen USD-Betrag basierend auf dem Modell - */ -export const convertAppTokensToUSD = async (appTokens: number, model: string): Promise => { - try { - const modelPrice = await getModelPrice(model); - - if (!modelPrice) { - return appTokens / 50000; // Standardwert: 50.000 App-Tokens pro Dollar - } - - return appTokens / modelPrice.tokens_per_dollar; - } catch (error) { - console.error('Fehler bei der Konvertierung von App-Tokens zu USD:', error); - return appTokens / 50000; // Fallback-Wert - } + return { + inputTokens: promptTokens, + outputTokens: completionTokens, + totalTokens: promptTokens + completionTokens, + costUsd: totalCostUsd, + appTokens, + }; }; /** @@ -227,14 +81,8 @@ export const updateDocumentTokenCount = (document: { content: string | null; metadata: any; }): { metadata: any; tokenCount: number } => { - // Stelle sicher, dass die Metadaten initialisiert sind const metadata = document.metadata || {}; - - // Berechne die Token-Anzahl const tokenCount = estimateTokens(document.content || ''); - - // Aktualisiere die Metadaten metadata.token_count = tokenCount; - return { metadata, tokenCount }; }; diff --git a/apps/context/apps/mobile/services/tokenTransactionService.ts b/apps/context/apps/mobile/services/tokenTransactionService.ts index 2023ddee5..cfc68cbe4 100644 --- a/apps/context/apps/mobile/services/tokenTransactionService.ts +++ b/apps/context/apps/mobile/services/tokenTransactionService.ts @@ -1,330 +1,86 @@ -import { supabase } from '../utils/supabase'; -import { estimateTokens, calculateCost } from './tokenCountingService'; +import { tokensApi, type TokenTransaction, type TokenUsageStats } from './backendApi'; -// Typdefinitionen -export type TokenTransaction = { - id: string; - user_id: string; - amount: number; - transaction_type: string; - model_used?: string; - prompt_tokens?: number; - completion_tokens?: number; - total_tokens?: number; - cost_usd?: number; - document_id?: string; - created_at: string; -}; +// Re-export types for backward compatibility +export type { TokenTransaction, TokenUsageStats }; -export type TokenUsageStats = { - totalUsed: number; - byModel: Record; - byDate: Record; +/** + * Ruft das aktuelle Token-Guthaben des authentifizierten Benutzers ab. + * The userId parameter is kept for backward compatibility but is ignored - + * the backend determines the user from the JWT token. + */ +export const getCurrentTokenBalance = async (_userId?: string): Promise => { + return tokensApi.getBalance(); }; /** - * Protokolliert die Token-Nutzung für eine KI-Anfrage + * Prüft, ob der Benutzer genügend Tokens für eine Anfrage hat */ -export const logTokenUsage = async ( - userId: string, - model: string, - prompt: string, - completion: string, - documentId?: string +export const hasEnoughTokens = async ( + _userId: string, + requiredTokens: number ): Promise => { - try { - // Schätze die Token-Anzahl - const promptTokens = estimateTokens(prompt); - const completionTokens = estimateTokens(completion); - - // Berechne die Kosten - const { appTokens, costUsd } = await calculateCost(model, promptTokens, completionTokens); - - // Prüfe, ob die übergebene ID ein gültiges Dokument ist - let validDocumentId = null; - if (documentId) { - // Prüfe, ob das Dokument existiert - const { data: docExists } = await supabase - .from('documents') - .select('id') - .eq('id', documentId) - .maybeSingle(); - - if (docExists) { - validDocumentId = documentId; - } else { - console.log( - 'Dokument-ID existiert nicht in der documents-Tabelle, setze auf null:', - documentId - ); - } - } - - // Verwende die SQL-Funktion use_tokens, um Tokens abzuziehen und die Transaktion zu protokollieren - const { data, error } = await supabase.rpc('use_tokens', { - p_user_id: userId, - p_amount: appTokens, - p_model_used: model, - p_document_id: validDocumentId, // Nur gültige Dokument-IDs verwenden - p_prompt_tokens: promptTokens, - p_completion_tokens: completionTokens, - p_cost_usd: costUsd, - }); - - if (error) { - console.error('Fehler beim Abziehen von Tokens:', error); - return false; - } - - // Die Funktion gibt true zurück, wenn genügend Tokens vorhanden waren - return data === true; - } catch (error) { - console.error('Fehler bei der Token-Nutzungsprotokollierung:', error); - return false; - } -}; - -/** - * Fügt Tokens zum Guthaben eines Benutzers hinzu - */ -export const addTokens = async ( - userId: string, - amount: number, - source: string, - packageName?: string, - entitlement?: string -): Promise => { - try { - // Verwende die SQL-Funktion add_tokens, um Tokens hinzuzufügen und die Transaktion zu protokollieren - const { data, error } = await supabase.rpc('add_tokens', { - p_user_id: userId, - p_amount: amount, - p_transaction_type: source, // z.B. 'purchase', 'monthly_reset', 'admin_grant' - p_package_name: packageName || null, - p_entitlement: entitlement || null, - }); - - if (error) { - console.error('Fehler beim Hinzufügen von Tokens:', error); - return false; - } - - return data === true; - } catch (error) { - console.error('Fehler beim Hinzufügen von Tokens:', error); - return false; - } -}; - -/** - * Setzt das monatliche Token-Kontingent eines Benutzers zurück - * - * Hinweis: Diese Funktion wird normalerweise nicht manuell aufgerufen, - * da der monatliche Reset automatisch durch einen Cron-Job in der Datenbank erfolgt. - * Sie kann jedoch für Tests oder manuelle Resets verwendet werden. - */ -export const resetMonthlyTokens = async (userId: string): Promise => { - try { - // Hole die Benutzerinformationen - const { data: userData } = await supabase - .from('users') - .select('monthly_free_tokens') - .eq('id', userId) - .single(); - - if (!userData) { - console.error('Benutzer nicht gefunden'); - return false; - } - - // Verwende die add_tokens-Funktion mit dem Transaktionstyp 'monthly_reset' - return await addTokens(userId, userData.monthly_free_tokens, 'monthly_reset'); - } catch (error) { - console.error('Fehler beim Zurücksetzen des monatlichen Token-Kontingents:', error); - return false; - } -}; - -/** - * Prüft, ob das monatliche Token-Kontingent eines Benutzers zurückgesetzt werden sollte - */ -export const checkAndResetMonthlyTokens = async (userId: string): Promise => { - try { - // Hole die Benutzerinformationen - const { data: userData } = await supabase - .from('users') - .select('last_token_reset') - .eq('id', userId) - .single(); - - if (!userData) { - console.error('Benutzer nicht gefunden'); - return false; - } - - const lastReset = new Date(userData.last_token_reset); - const now = new Date(); - - // Prüfe, ob der letzte Reset mehr als einen Monat zurückliegt - const monthDiff = - (now.getFullYear() - lastReset.getFullYear()) * 12 + now.getMonth() - lastReset.getMonth(); - - if (monthDiff >= 1) { - return resetMonthlyTokens(userId); - } - - return false; - } catch (error) { - console.error('Fehler beim Prüfen des monatlichen Token-Kontingents:', error); - return false; - } -}; - -/** - * Ruft das aktuelle Token-Guthaben eines Benutzers ab - */ -export const getCurrentTokenBalance = async (userId: string): Promise => { - try { - // Prüfe, ob das monatliche Kontingent zurückgesetzt werden sollte - await checkAndResetMonthlyTokens(userId); - - // Hole das aktuelle Token-Guthaben - const { data: userData } = await supabase - .from('users') - .select('token_balance') - .eq('id', userId) - .single(); - - if (!userData) { - console.error('Benutzer nicht gefunden'); - return 0; - } - - console.log('Token-Guthaben:', userData.token_balance); - return userData.token_balance; - } catch (error) { - console.error('Fehler beim Abrufen des Token-Guthabens:', error); - return 0; - } -}; - -/** - * Prüft, ob ein Benutzer genügend Tokens für eine Anfrage hat - */ -export const hasEnoughTokens = async (userId: string, requiredTokens: number): Promise => { - const balance = await getCurrentTokenBalance(userId); + const balance = await getCurrentTokenBalance(); return balance >= requiredTokens; }; /** - * Ruft die Token-Nutzungsstatistiken eines Benutzers ab + * Ruft die Token-Nutzungsstatistiken ab */ export const getTokenUsageStats = async ( - userId: string, + _userId: string, timeframe: 'day' | 'week' | 'month' | 'year' = 'month' ): Promise => { - try { - // Bestimme die Anzahl der Tage basierend auf dem Zeitrahmen - let days = 30; // Standard: 1 Monat - - switch (timeframe) { - case 'day': - days = 1; - break; - case 'week': - days = 7; - break; - case 'month': - days = 30; - break; - case 'year': - days = 365; - break; - } - - // Verwende die SQL-Funktion get_token_usage_stats - const { data: modelStats, error } = await supabase.rpc('get_token_usage_stats', { - p_user_id: userId, - p_days: days, - }); - - if (error) { - console.error('Fehler beim Abrufen der Token-Nutzungsstatistiken:', error); - return { - totalUsed: 0, - byModel: {}, - byDate: {}, - }; - } - - // Hole zusätzlich die Daten nach Datum für das Diagramm - const { data: transactions } = await supabase - .from('token_transactions') - .select('created_at, amount') - .eq('user_id', userId) - .eq('transaction_type', 'usage') - .gte('created_at', new Date(new Date().setDate(new Date().getDate() - days)).toISOString()) - .order('created_at', { ascending: true }); - - // Berechne die Statistiken - const stats: TokenUsageStats = { - totalUsed: 0, - byModel: {}, - byDate: {}, - }; - - // Verarbeite die Modellstatistiken - if (modelStats && modelStats.length > 0) { - modelStats.forEach((stat: any) => { - stats.totalUsed += stat.total_app_tokens || 0; - - if (stat.model_name) { - stats.byModel[stat.model_name] = stat.total_app_tokens || 0; - } - }); - } - - // Verarbeite die Datumsstatistiken - if (transactions && transactions.length > 0) { - transactions.forEach((transaction: any) => { - const date = new Date(transaction.created_at).toISOString().split('T')[0]; - if (!stats.byDate[date]) { - stats.byDate[date] = 0; - } - stats.byDate[date] += Math.abs(transaction.amount); - }); - } - - return stats; - } catch (error) { - console.error('Fehler beim Abrufen der Token-Nutzungsstatistiken:', error); - return { - totalUsed: 0, - byModel: {}, - byDate: {}, - }; - } + return tokensApi.getStats(timeframe); }; /** - * Ruft die Token-Transaktionen eines Benutzers ab + * Ruft die Token-Transaktionen ab */ export const getTokenTransactions = async ( - userId: string, + _userId: string, limit: number = 10, offset: number = 0 ): Promise => { - try { - const { data: transactions } = await supabase - .from('token_transactions') - .select('*') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .range(offset, offset + limit - 1); - - return transactions || []; - } catch (error) { - console.error('Fehler beim Abrufen der Token-Transaktionen:', error); - return []; - } + return tokensApi.getTransactions(limit, offset); +}; + +/** + * Die folgenden Funktionen werden nicht mehr benötigt, da das Backend + * Token-Operationen (Abzug, Hinzufügen, Reset) serverseitig handhabt. + * Sie werden nur als No-Op Stubs beibehalten für den Fall, dass sie + * noch irgendwo referenziert werden. + */ + +export const logTokenUsage = async ( + _userId: string, + _model: string, + _prompt: string, + _completion: string, + _documentId?: string +): Promise => { + // Token usage is now logged server-side during AI generation + console.warn('logTokenUsage is a no-op - token usage is tracked server-side'); + return true; +}; + +export const addTokens = async ( + _userId: string, + _amount: number, + _source: string, + _packageName?: string, + _entitlement?: string +): Promise => { + // Token additions are handled server-side + console.warn('addTokens is a no-op - token operations are handled server-side'); + return true; +}; + +export const resetMonthlyTokens = async (_userId: string): Promise => { + console.warn('resetMonthlyTokens is a no-op - handled server-side'); + return false; +}; + +export const checkAndResetMonthlyTokens = async (_userId: string): Promise => { + console.warn('checkAndResetMonthlyTokens is a no-op - handled server-side'); + return false; }; diff --git a/apps/context/apps/mobile/utils/supabase.ts b/apps/context/apps/mobile/utils/supabase.ts deleted file mode 100644 index 49dbd8831..000000000 --- a/apps/context/apps/mobile/utils/supabase.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createClient } from '@supabase/supabase-js'; - -const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL; -const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY; - -// Prüfen, ob wir in einer Browser-Umgebung sind -const isBrowser = typeof window !== 'undefined'; - -// Nur AsyncStorage importieren, wenn wir in einer Browser-/React Native-Umgebung sind -let AsyncStorage; -if (isBrowser) { - // Dynamischer Import, um SSR-Probleme zu vermeiden - AsyncStorage = require('@react-native-async-storage/async-storage').default; -} - -export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || '', { - auth: { - storage: isBrowser ? AsyncStorage : undefined, - autoRefreshToken: true, - persistSession: isBrowser, - detectSessionInUrl: false, - }, -});