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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-23 12:01:58 +01:00
parent c6d5d4840e
commit 5bd967900f
25 changed files with 896 additions and 2471 deletions

View file

@ -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]);

View file

@ -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';

View file

@ -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';

View file

@ -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);
}

View file

@ -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<BottomLLMToolbarProps> = ({
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<BottomLLMToolbarProps> = ({
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

View file

@ -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<SpacesLLMToolbarProps> = ({
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<SpacesLLMToolbarProps> = ({
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

View file

@ -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.');

View file

@ -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;

View file

@ -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.');

View file

@ -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<TokenDisplayProps> = ({
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 {

View file

@ -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<TokenEstimatorProps> = ({
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);

View file

@ -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<TokenStoreProps> = ({ onClose, onPurchaseComplete }) => {
const [user, setUser] = useState<{ id: string } | null>(null);
const [packages, setPackages] = useState<PurchasesPackage[]>([]);
const [loading, setLoading] = useState(true);
const [purchasing, setPurchasing] = useState(false);
@ -38,18 +36,6 @@ export const TokenStore: React.FC<TokenStoreProps> = ({ 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<TokenStoreProps> = ({ onClose, onPurchaseCompl
}
};
if (user) {
loadData();
}
}, [user]);
loadData();
}, []);
const handlePurchase = async (pkg: PurchasesPackage) => {
if (!user) return;
try {
setPurchasing(true);

View file

@ -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<void>;
resetPassword: (email: string) => Promise<{
success: boolean;
error?: string;
}>;
};
// Erstelle den Authentifizierungskontext
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Provider-Komponente für den Authentifizierungskontext
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
// 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;
};

View file

@ -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<T>(key: string): Promise<T | null> {
try {
const value = await SecureStore.getItemAsync(key);
return value ? JSON.parse(value) : null;
} catch {
return null;
}
},
async setItem(key: string, value: unknown): Promise<void> {
await SecureStore.setItemAsync(key, JSON.stringify(value));
},
async removeItem(key: string): Promise<void> {
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<void>;
resetPassword: (email: string) => Promise<{ error: any | null }>;
};
// Create auth context
const AuthContext = createContext<AuthContextType | undefined>(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<UserData | null>(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 (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color="#0A84FF" />
<Text style={{ marginTop: 16 }}>Authentifizierung wird initialisiert...</Text>
</View>
);
}
// Provide auth context
return (
<AuthContext.Provider
value={{
user,
loading,
signIn,
signUp,
signOut,
resetPassword,
}}
>
{children}
</AuthContext.Provider>
);
}

View file

@ -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,

View file

@ -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",

View file

@ -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<string> => {
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<string> => {
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<AIGenerationResult> => {
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,

View file

@ -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<string, number>;
byDate: Record<string, number>;
};
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<string | null> {
try {
return await SecureStore.getItemAsync(APP_TOKEN_KEY);
} catch {
return null;
}
}
async function apiRequest<T>(
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<string, string>)['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<Space[]> {
const { data, error } = await apiRequest<Space[]>('/api/v1/spaces');
if (error) {
console.error('Failed to fetch spaces:', error);
return [];
}
return data || [];
},
async get(id: string): Promise<Space | null> {
const { data, error } = await apiRequest<Space>(`/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<Space>('/api/v1/spaces', {
method: 'POST',
body: JSON.stringify(params),
});
},
async update(
id: string,
updates: Partial<Space>
): 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<Document[]> {
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<Document[]>(endpoint);
if (error) {
console.error('Failed to fetch documents:', error);
return [];
}
return data || [];
},
async listRecent(limit: number = 5): Promise<Document[]> {
const { data, error } = await apiRequest<Document[]>(`/api/v1/documents/recent?limit=${limit}`);
if (error) {
console.error('Failed to fetch recent documents:', error);
return [];
}
return data || [];
},
async get(id: string): Promise<Document | null> {
const { data, error } = await apiRequest<Document>(`/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<Document>('/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<Document>
): 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<Document[]>(`/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<Document>(`/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<number> {
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<TokenUsageStats> {
const { data, error } = await apiRequest<TokenUsageStats>(
`/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<TokenTransaction[]> {
const { data, error } = await apiRequest<TokenTransaction[]>(
`/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<ModelPrice[]> {
const { data, error } = await apiRequest<ModelPrice[]>('/api/v1/tokens/models');
if (error || !data) {
console.error('Failed to fetch model prices:', error);
return [];
}
return data;
},
};

View file

@ -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<string, number> = {
// 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<void> => {
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<void> => {
};
/**
* Aktualisiert die RevenueCat ID des Benutzers in der Datenbank
* Synchronisiert bestehende Käufe
*/
const updateUserRevenueCatId = async (userId: string): Promise<void> => {
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<void> => {
const syncPurchases = async (): Promise<void> => {
// 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<void> => {
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<void> => {
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<boolean> {
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<boolean> {
}
/**
* Fügt Tokens zum Guthaben des Benutzers hinzu und protokolliert die Transaktion
*/
const addTokensToUser = async (
userId: string,
amount: number,
source: string
): Promise<boolean> => {
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<number | null> => {
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<CustomerInfo | null> => {
/**
* Setzt die Benutzer-ID für RevenueCat
* Nützlich, wenn sich der Benutzer anmeldet oder abmeldet
*/
export const setRevenueCatUserId = async (userId: string): Promise<void> => {
// Auf Web-Plattform überspringen

View file

@ -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 };
}
};

View file

@ -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 };
}
};

View file

@ -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<User | null> => {
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<Space[]> => {
const { data } = await supabase
.from('spaces')
.select('*')
.order('created_at', { ascending: false });
return data || [];
return spacesApi.list();
};
export const getSpaceById = async (id: string): Promise<Space | null> => {
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<Space>
): 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<Document[]> => {
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<Document[]> => {
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<Document[]> => {
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<Document | null> => {
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<Document | null> => {
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<Document>
): 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,
});
};

View file

@ -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<ModelPrice | null> => {
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<TokenCostEstimate> => {
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<ModelPrice[]> => {
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<TokenCostEstimate> => {
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<number> => {
// 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<number> => {
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 };
};

View file

@ -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<string, number>;
byDate: Record<string, number>;
/**
* 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<number> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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<number> => {
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<boolean> => {
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<TokenUsageStats> => {
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<TokenTransaction[]> => {
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<boolean> => {
// 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<boolean> => {
// 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<boolean> => {
console.warn('resetMonthlyTokens is a no-op - handled server-side');
return false;
};
export const checkAndResetMonthlyTokens = async (_userId: string): Promise<boolean> => {
console.warn('checkAndResetMonthlyTokens is a no-op - handled server-side');
return false;
};

View file

@ -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,
},
});