mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
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:
parent
c6d5d4840e
commit
5bd967900f
25 changed files with 896 additions and 2471 deletions
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
230
apps/context/apps/mobile/context/AuthProvider.tsx
Normal file
230
apps/context/apps/mobile/context/AuthProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
438
apps/context/apps/mobile/services/backendApi.ts
Normal file
438
apps/context/apps/mobile/services/backendApi.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
};
|
||||
|
|
@ -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 };
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue