diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index e015c7b..1424198 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -82,12 +82,20 @@ export default async function AdminPage() {

Admin Dashboard

API Usage Monitoring & Statistics

- - Back to App - +
+ + Manage Users + + + Back to App + +
{/* Global Stats Cards */} diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx new file mode 100644 index 0000000..19ebcb2 --- /dev/null +++ b/src/app/admin/users/page.tsx @@ -0,0 +1,98 @@ +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; +import { checkIsAdmin } from '@/services/track-api-usage'; +import { getAllUsersWithCredits } from '@/services/admin-credit-service'; +import Link from 'next/link'; +import { ChevronLeft, Users, Coins, TrendingUp, TrendingDown } from 'lucide-react'; +import UserManagementClient from '@/components/UserManagementClient'; + +export default async function AdminUsersPage() { + const supabase = createServerComponentClient({ cookies }); + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + redirect('/'); + } + + const isAdmin = await checkIsAdmin(user.id); + if (!isAdmin) { + redirect('/'); + } + + // Fetch all users with credits + const users = await getAllUsersWithCredits(); + + // Calculate statistics + const totalUsers = users.length; + const totalCreditsInCirculation = users.reduce((sum, u) => sum + u.balance, 0); + const totalCreditsPurchased = users.reduce((sum, u) => sum + u.total_purchased, 0); + const totalCreditsUsed = users.reduce((sum, u) => sum + u.total_used, 0); + + return ( +
+
+ {/* Header */} +
+
+ + + Back to Dashboard + +

User Management

+

Manage user credits and limits

+
+
+ + {/* Statistics Cards */} +
+
+
+
+ +
+ Total Users +
+
{totalUsers}
+
+ +
+
+
+ +
+ Credits in Circulation +
+
{totalCreditsInCirculation.toLocaleString()}
+
+ +
+
+
+ +
+ Total Purchased +
+
{totalCreditsPurchased.toLocaleString()}
+
+ +
+
+
+ +
+ Total Used +
+
{totalCreditsUsed.toLocaleString()}
+
+
+ + {/* User Management Table */} + +
+
+ ); +} diff --git a/src/components/UserManagementClient.tsx b/src/components/UserManagementClient.tsx new file mode 100644 index 0000000..94edb60 --- /dev/null +++ b/src/components/UserManagementClient.tsx @@ -0,0 +1,318 @@ +'use client'; + +import { useState } from 'react'; +import { Edit, Plus, Search, X, Check, AlertCircle } from 'lucide-react'; +import { updateUserCredits, setUserDailyLimit, setUserApiCosts } from '@/services/admin-credit-service'; + +interface User { + id: string; + email: string; + username: string; + balance: number; + total_purchased: number; + total_used: number; + daily_limit: number | null; + google_search_cost: number; + gemini_ai_cost: number; + last_active?: string; +} + +interface UserManagementClientProps { + initialUsers: User[]; +} + +export default function UserManagementClient({ initialUsers }: UserManagementClientProps) { + const [users, setUsers] = useState(initialUsers); + const [searchTerm, setSearchTerm] = useState(''); + const [editingUser, setEditingUser] = useState(null); + const [creditAmount, setCreditAmount] = useState(''); + const [reason, setReason] = useState(''); + const [dailyLimit, setDailyLimit] = useState(''); + const [googleCost, setGoogleCost] = useState(''); + const [geminiCost, setGeminiCost] = useState(''); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + const filteredUsers = users.filter(user => + user.email.toLowerCase().includes(searchTerm.toLowerCase()) || + user.username.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleEditUser = (user: User) => { + setEditingUser(user); + setCreditAmount(''); + setReason(''); + setDailyLimit(user.daily_limit?.toString() || ''); + setGoogleCost(user.google_search_cost.toString()); + setGeminiCost(user.gemini_ai_cost.toString()); + setMessage(null); + }; + + const handleUpdateCredits = async () => { + if (!editingUser || !creditAmount || !reason) { + setMessage({ type: 'error', text: 'Please fill in all fields' }); + return; + } + + setLoading(true); + setMessage(null); + + const amount = parseInt(creditAmount); + const newBalance = editingUser.balance + amount; + + const result = await updateUserCredits(editingUser.id, newBalance, reason); + + if (result.success) { + // Update local state + setUsers(users.map(u => + u.id === editingUser.id + ? { ...u, balance: newBalance, total_purchased: u.total_purchased + (amount > 0 ? amount : 0) } + : u + )); + setMessage({ type: 'success', text: `Successfully ${amount > 0 ? 'added' : 'removed'} ${Math.abs(amount)} credits` }); + setCreditAmount(''); + setReason(''); + } else { + setMessage({ type: 'error', text: result.error || 'Failed to update credits' }); + } + + setLoading(false); + }; + + const handleUpdateSettings = async () => { + if (!editingUser) return; + + setLoading(true); + setMessage(null); + + // Update daily limit + if (dailyLimit !== (editingUser.daily_limit?.toString() || '')) { + const limitValue = dailyLimit === '' ? null : parseInt(dailyLimit); + const limitResult = await setUserDailyLimit(editingUser.id, limitValue); + if (!limitResult.success) { + setMessage({ type: 'error', text: 'Failed to update daily limit' }); + setLoading(false); + return; + } + } + + // Update API costs + if (googleCost !== editingUser.google_search_cost.toString() || geminiCost !== editingUser.gemini_ai_cost.toString()) { + const costsResult = await setUserApiCosts( + editingUser.id, + parseInt(googleCost), + parseInt(geminiCost) + ); + if (!costsResult.success) { + setMessage({ type: 'error', text: 'Failed to update API costs' }); + setLoading(false); + return; + } + } + + // Update local state + setUsers(users.map(u => + u.id === editingUser.id + ? { + ...u, + daily_limit: dailyLimit === '' ? null : parseInt(dailyLimit), + google_search_cost: parseInt(googleCost), + gemini_ai_cost: parseInt(geminiCost) + } + : u + )); + + setMessage({ type: 'success', text: 'Settings updated successfully' }); + setLoading(false); + }; + + return ( +
+ {/* Search Bar */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ + {/* User Table */} +
+

Users ({filteredUsers.length})

+
+ + + + + + + + + + + + + {filteredUsers.map((user) => ( + + + + + + + + + ))} + +
UserBalanceUsedDaily LimitCostsActions
+
+
{user.username}
+
{user.email}
+
+
+ + {user.balance} + + + {user.total_used} + + {user.daily_limit || 'Global (80)'} + + G:{user.google_search_cost} / AI:{user.gemini_ai_cost} + + +
+
+
+ + {/* Edit Modal */} + {editingUser && ( +
+
+
+
+

Edit User

+

{editingUser.email}

+
+ +
+ + {message && ( +
+ {message.type === 'success' ? : } + {message.text} +
+ )} + +
+ {/* Current Balance */} +
+
Current Balance
+
{editingUser.balance} Credits
+
+ + {/* Add/Remove Credits */} +
+

Adjust Credits

+
+
+ + setCreditAmount(e.target.value)} + placeholder="e.g. 100 or -50" + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ + setReason(e.target.value)} + placeholder="e.g. Monthly bonus" + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ +
+ +
+ + {/* Settings */} +
+

User Settings

+
+
+ + setDailyLimit(e.target.value)} + placeholder="Global (80)" + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ + setGoogleCost(e.target.value)} + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ + setGeminiCost(e.target.value)} + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ +
+
+
+
+ )} +
+ ); +} diff --git a/src/services/admin-credit-service.ts b/src/services/admin-credit-service.ts new file mode 100644 index 0000000..73b9bf4 --- /dev/null +++ b/src/services/admin-credit-service.ts @@ -0,0 +1,236 @@ +'use server'; + +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; +import { cookies } from 'next/headers'; +import { checkIsAdmin } from './track-api-usage'; +import { addCredits, getUserCredits } from './credit-service'; + +interface UserWithCredits { + id: string; + email: string; + username: string; + balance: number; + total_purchased: number; + total_used: number; + daily_limit: number | null; + google_search_cost: number; + gemini_ai_cost: number; + last_active?: string; +} + +/** + * Get all users with their credit information (admin only) + */ +export async function getAllUsersWithCredits(): Promise { + try { + const supabase = createServerComponentClient({ cookies }); + + // Check if current user is admin + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return []; + + const isAdmin = await checkIsAdmin(user.id); + if (!isAdmin) return []; + + // Get all users with their profiles + const { data: profiles, error: profilesError } = await supabase + .from('profiles') + .select('id, username'); + + if (profilesError) { + console.error('Error fetching profiles:', profilesError); + return []; + } + + // Get all user credits + const { data: credits, error: creditsError } = await supabase + .from('user_credits') + .select('*'); + + if (creditsError) { + console.error('Error fetching credits:', creditsError); + return []; + } + + // Get user emails from auth.users + const { data: { users }, error: usersError } = await supabase.auth.admin.listUsers(); + + if (usersError) { + console.error('Error fetching users:', usersError); + return []; + } + + // Combine data + const usersWithCredits: UserWithCredits[] = profiles?.map(profile => { + const userAuth = users.find(u => u.id === profile.id); + const userCredits = credits?.find(c => c.user_id === profile.id); + + return { + id: profile.id, + email: userAuth?.email || 'Unknown', + username: profile.username || 'Unknown', + balance: userCredits?.balance || 0, + total_purchased: userCredits?.total_purchased || 0, + total_used: userCredits?.total_used || 0, + daily_limit: userCredits?.daily_limit || null, + google_search_cost: userCredits?.google_search_cost || 1, + gemini_ai_cost: userCredits?.gemini_ai_cost || 1, + last_active: userAuth?.last_sign_in_at || undefined + }; + }) || []; + + return usersWithCredits; + } catch (err) { + console.error('Error in getAllUsersWithCredits:', err); + return []; + } +} + +/** + * Update user's credit balance (admin only) + */ +export async function updateUserCredits( + userId: string, + newBalance: number, + reason: string +): Promise<{ success: boolean; error?: string }> { + try { + const supabase = createServerComponentClient({ cookies }); + + // Check if current user is admin + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return { success: false, error: 'Not authenticated' }; + + const isAdmin = await checkIsAdmin(user.id); + if (!isAdmin) return { success: false, error: 'Not authorized' }; + + // Get current credits + const currentCredits = await getUserCredits(userId); + if (!currentCredits) { + return { success: false, error: 'User credits not found' }; + } + + const difference = newBalance - currentCredits.balance; + + // Use addCredits which handles the transaction logging + const result = await addCredits(userId, difference, reason, user.id); + + return result; + } catch (err) { + console.error('Error in updateUserCredits:', err); + return { success: false, error: 'Failed to update credits' }; + } +} + +/** + * Set user's daily limit (admin only) + */ +export async function setUserDailyLimit( + userId: string, + dailyLimit: number | null +): Promise<{ success: boolean; error?: string }> { + try { + const supabase = createServerComponentClient({ cookies }); + + // Check if current user is admin + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return { success: false, error: 'Not authenticated' }; + + const isAdmin = await checkIsAdmin(user.id); + if (!isAdmin) return { success: false, error: 'Not authorized' }; + + const { error } = await supabase + .from('user_credits') + .update({ daily_limit: dailyLimit }) + .eq('user_id', userId); + + if (error) { + console.error('Error setting daily limit:', error); + return { success: false, error: 'Failed to set daily limit' }; + } + + return { success: true }; + } catch (err) { + console.error('Error in setUserDailyLimit:', err); + return { success: false, error: 'Failed to set daily limit' }; + } +} + +/** + * Set user's API costs (admin only) + */ +export async function setUserApiCosts( + userId: string, + googleSearchCost: number, + geminiAiCost: number +): Promise<{ success: boolean; error?: string }> { + try { + const supabase = createServerComponentClient({ cookies }); + + // Check if current user is admin + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return { success: false, error: 'Not authenticated' }; + + const isAdmin = await checkIsAdmin(user.id); + if (!isAdmin) return { success: false, error: 'Not authorized' }; + + const { error } = await supabase + .from('user_credits') + .update({ + google_search_cost: googleSearchCost, + gemini_ai_cost: geminiAiCost + }) + .eq('user_id', userId); + + if (error) { + console.error('Error setting API costs:', error); + return { success: false, error: 'Failed to set API costs' }; + } + + return { success: true }; + } catch (err) { + console.error('Error in setUserApiCosts:', err); + return { success: false, error: 'Failed to set API costs' }; + } +} + +/** + * Bulk add credits to multiple users (admin only) + */ +export async function bulkAddCredits( + userIds: string[], + amount: number, + reason: string +): Promise<{ success: boolean; processed: number; failed: number; error?: string }> { + try { + const supabase = createServerComponentClient({ cookies }); + + // Check if current user is admin + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return { success: false, processed: 0, failed: 0, error: 'Not authenticated' }; + + const isAdmin = await checkIsAdmin(user.id); + if (!isAdmin) return { success: false, processed: 0, failed: 0, error: 'Not authorized' }; + + let processed = 0; + let failed = 0; + + for (const userId of userIds) { + const result = await addCredits(userId, amount, reason, user.id); + if (result.success) { + processed++; + } else { + failed++; + } + } + + return { + success: true, + processed, + failed + }; + } catch (err) { + console.error('Error in bulkAddCredits:', err); + return { success: false, processed: 0, failed: 0, error: 'Failed to bulk add credits' }; + } +} diff --git a/src/services/credit-service.ts b/src/services/credit-service.ts new file mode 100644 index 0000000..a29d6f5 --- /dev/null +++ b/src/services/credit-service.ts @@ -0,0 +1,273 @@ +'use server'; + +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; +import { cookies } from 'next/headers'; + +interface UserCredits { + user_id: string; + balance: number; + total_purchased: number; + total_used: number; + daily_limit: number | null; + google_search_cost: number; + gemini_ai_cost: number; + last_reset_at: string; + updated_at: string; +} + +interface CreditTransaction { + id: string; + user_id: string; + amount: number; + type: 'deduction' | 'addition' | 'admin_adjustment'; + reason?: string; + api_type?: 'google_search' | 'gemini_ai'; + admin_id?: string; + balance_after: number; + created_at: string; +} + +/** + * Get user's credit information + */ +export async function getUserCredits(userId: string): Promise { + try { + const supabase = createServerComponentClient({ cookies }); + + const { data, error } = await supabase + .from('user_credits') + .select('*') + .eq('user_id', userId) + .single(); + + if (error) { + // If user doesn't have credits yet, create entry with default values + if (error.code === 'PGRST116') { + const { data: newCredits, error: insertError } = await supabase + .from('user_credits') + .insert({ + user_id: userId, + balance: 100, // Starting credits + total_purchased: 100, + total_used: 0 + }) + .select() + .single(); + + if (insertError) { + console.error('Error creating user credits:', insertError); + return null; + } + + return newCredits; + } + + console.error('Error fetching user credits:', error); + return null; + } + + return data; + } catch (err) { + console.error('Error in getUserCredits:', err); + return null; + } +} + +/** + * Check if user has enough credits + */ +export async function checkCreditBalance( + userId: string, + apiType: 'google_search' | 'gemini_ai' +): Promise<{ allowed: boolean; balance: number; cost: number; message?: string }> { + try { + const credits = await getUserCredits(userId); + + if (!credits) { + return { + allowed: false, + balance: 0, + cost: 1, + message: 'Could not fetch credit information' + }; + } + + const cost = apiType === 'google_search' + ? credits.google_search_cost + : credits.gemini_ai_cost; + + const allowed = credits.balance >= cost; + + return { + allowed, + balance: credits.balance, + cost, + message: allowed ? undefined : `Insufficient credits. You need ${cost} credits but have ${credits.balance}.` + }; + } catch (err) { + console.error('Error in checkCreditBalance:', err); + return { + allowed: false, + balance: 0, + cost: 1, + message: 'Error checking credit balance' + }; + } +} + +/** + * Deduct credits from user's balance + */ +export async function deductCredits( + userId: string, + apiType: 'google_search' | 'gemini_ai', + reason?: string +): Promise<{ success: boolean; newBalance?: number; error?: string }> { + try { + const supabase = createServerComponentClient({ cookies }); + + // Get current credits + const credits = await getUserCredits(userId); + if (!credits) { + return { success: false, error: 'Could not fetch credit information' }; + } + + const cost = apiType === 'google_search' + ? credits.google_search_cost + : credits.gemini_ai_cost; + + // Check if user has enough credits + if (credits.balance < cost) { + return { + success: false, + error: `Insufficient credits. Need ${cost}, have ${credits.balance}` + }; + } + + const newBalance = credits.balance - cost; + + // Update user credits + const { error: updateError } = await supabase + .from('user_credits') + .update({ + balance: newBalance, + total_used: credits.total_used + cost, + updated_at: new Date().toISOString() + }) + .eq('user_id', userId); + + if (updateError) { + console.error('Error updating user credits:', updateError); + return { success: false, error: 'Failed to update credits' }; + } + + // Log transaction + const { error: transactionError } = await supabase + .from('credit_transactions') + .insert({ + user_id: userId, + amount: -cost, + type: 'deduction', + reason: reason || `${apiType} API call`, + api_type: apiType, + balance_after: newBalance + }); + + if (transactionError) { + console.error('Error logging credit transaction:', transactionError); + // Don't fail the operation if logging fails + } + + return { success: true, newBalance }; + } catch (err) { + console.error('Error in deductCredits:', err); + return { success: false, error: 'Failed to deduct credits' }; + } +} + +/** + * Add credits to user's balance + */ +export async function addCredits( + userId: string, + amount: number, + reason: string, + adminId?: string +): Promise<{ success: boolean; newBalance?: number; error?: string }> { + try { + const supabase = createServerComponentClient({ cookies }); + + // Get current credits + const credits = await getUserCredits(userId); + if (!credits) { + return { success: false, error: 'Could not fetch credit information' }; + } + + const newBalance = credits.balance + amount; + + // Update user credits + const { error: updateError } = await supabase + .from('user_credits') + .update({ + balance: newBalance, + total_purchased: adminId ? credits.total_purchased + amount : credits.total_purchased, + updated_at: new Date().toISOString() + }) + .eq('user_id', userId); + + if (updateError) { + console.error('Error updating user credits:', updateError); + return { success: false, error: 'Failed to update credits' }; + } + + // Log transaction + const { error: transactionError } = await supabase + .from('credit_transactions') + .insert({ + user_id: userId, + amount: amount, + type: adminId ? 'admin_adjustment' : 'addition', + reason: reason, + admin_id: adminId, + balance_after: newBalance + }); + + if (transactionError) { + console.error('Error logging credit transaction:', transactionError); + } + + return { success: true, newBalance }; + } catch (err) { + console.error('Error in addCredits:', err); + return { success: false, error: 'Failed to add credits' }; + } +} + +/** + * Get user's credit transaction history + */ +export async function getCreditTransactions( + userId: string, + limit: number = 50 +): Promise { + try { + const supabase = createServerComponentClient({ cookies }); + + const { data, error } = await supabase + .from('credit_transactions') + .select('*') + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .limit(limit); + + if (error) { + console.error('Error fetching credit transactions:', error); + return []; + } + + return data || []; + } catch (err) { + console.error('Error in getCreditTransactions:', err); + return []; + } +} diff --git a/src/services/discover-whiskybase.ts b/src/services/discover-whiskybase.ts index 0bb0487..916f0e3 100644 --- a/src/services/discover-whiskybase.ts +++ b/src/services/discover-whiskybase.ts @@ -1,6 +1,7 @@ 'use server'; import { trackApiUsage, checkDailyLimit } from './track-api-usage'; +import { checkCreditBalance, deductCredits } from './credit-service'; import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; import { cookies } from 'next/headers'; @@ -49,6 +50,16 @@ export async function discoverWhiskybaseId(bottle: { }; } + // Check credit balance before making API call + const creditCheck = await checkCreditBalance(user.id, 'google_search'); + if (!creditCheck.allowed) { + return { + success: false, + error: `Nicht genügend Credits. Du benötigst ${creditCheck.cost} Credits, hast aber nur ${creditCheck.balance}.`, + insufficientCredits: true + }; + } + try { // Construct targeted search query const queryParts = [ @@ -90,6 +101,13 @@ export async function discoverWhiskybaseId(bottle: { success: true }); + // Deduct credits after successful API call + const creditDeduction = await deductCredits(user.id, 'google_search', 'Whiskybase search'); + if (!creditDeduction.success) { + console.error('Failed to deduct credits:', creditDeduction.error); + // Don't fail the search if credit deduction fails + } + if (!data.items || data.items.length === 0) { return { success: false, diff --git a/supa_schema.sql b/supa_schema.sql index 183e4d2..5bbff5e 100644 --- a/supa_schema.sql +++ b/supa_schema.sql @@ -290,3 +290,59 @@ CREATE POLICY "Users can view their own admin record" ON admin_users FOR SELECT -- Note: To add robin as admin, run this after getting the user_id: -- INSERT INTO admin_users (user_id, role) VALUES ('', 'super_admin'); + +-- ============================================ +-- Credits Management System +-- ============================================ + +-- Extend user_credits table with additional fields +ALTER TABLE user_credits +ADD COLUMN IF NOT EXISTS daily_limit INTEGER DEFAULT NULL, +ADD COLUMN IF NOT EXISTS google_search_cost INTEGER DEFAULT 1, +ADD COLUMN IF NOT EXISTS gemini_ai_cost INTEGER DEFAULT 1, +ADD COLUMN IF NOT EXISTS last_reset_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()); + +-- Credit transactions table +CREATE TABLE IF NOT EXISTS credit_transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + amount INTEGER NOT NULL, + type TEXT NOT NULL CHECK (type IN ('deduction', 'addition', 'admin_adjustment')), + reason TEXT, + api_type TEXT CHECK (api_type IN ('google_search', 'gemini_ai')), + admin_id UUID REFERENCES auth.users(id), + balance_after INTEGER, + created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()) +); + +CREATE INDEX idx_credit_transactions_user_id ON credit_transactions(user_id); +CREATE INDEX idx_credit_transactions_created_at ON credit_transactions(created_at); +CREATE INDEX idx_credit_transactions_type ON credit_transactions(type); + +-- Enable RLS for credit_transactions +ALTER TABLE credit_transactions ENABLE ROW LEVEL SECURITY; + +-- Policies for credit_transactions +CREATE POLICY "Users can view their own transactions" ON credit_transactions +FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Admins can view all transactions" ON credit_transactions +FOR SELECT USING ( + auth.uid() IN (SELECT user_id FROM admin_users) +); + +CREATE POLICY "System can insert transactions" ON credit_transactions +FOR INSERT WITH CHECK (true); + +-- Update user_credits policies to allow admin updates +CREATE POLICY "Admins can update credits" ON user_credits +FOR UPDATE USING ( + auth.uid() IN (SELECT user_id FROM admin_users) +); + +-- Initialize credits for existing users (run manually if needed) +-- INSERT INTO user_credits (user_id, balance) +-- SELECT id, 100 +-- FROM auth.users +-- ON CONFLICT (user_id) DO UPDATE SET balance = EXCLUDED.balance +-- WHERE user_credits.balance = 0;