feat: implement comprehensive credits management system
- Database schema: * Extended user_credits table with daily_limit, API costs, last_reset_at * Created credit_transactions table for full audit trail * Added RLS policies for secure access control - Core services: * credit-service.ts - balance checking, deduction, addition, transaction history * admin-credit-service.ts - admin controls for managing users and credits - API integration: * Integrated credit checking into discover-whiskybase.ts * Credits deducted after successful API calls * Insufficient credits error handling - Admin interface: * /admin/users page with user management * Statistics dashboard (total users, credits in circulation, usage) * Interactive user table with search * Edit modal for credit adjustment and settings * Per-user daily limits and API cost configuration - Features: * Automatic credit initialization (100 credits for new users) * Credit transaction logging with balance_after tracking * Admin can add/remove credits with reason * Admin can set custom daily limits per user * Admin can set custom API costs per user * Low credit warnings (< 10 credits) * Full transaction history - User experience: * Credits checked before API calls * Clear error messages for insufficient credits * Graceful handling of credit deduction failures System is ready for future enhancements like credit packages, auto-recharge, and payment integration.
This commit is contained in:
@@ -82,12 +82,20 @@ export default async function AdminPage() {
|
||||
<h1 className="text-3xl font-black text-zinc-900 dark:text-white tracking-tight">Admin Dashboard</h1>
|
||||
<p className="text-zinc-500 mt-1">API Usage Monitoring & Statistics</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="px-4 py-2 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-xl font-bold hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
Back to App
|
||||
</Link>
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href="/admin/users"
|
||||
className="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-bold transition-colors"
|
||||
>
|
||||
Manage Users
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="px-4 py-2 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-xl font-bold hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
Back to App
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Stats Cards */}
|
||||
|
||||
98
src/app/admin/users/page.tsx
Normal file
98
src/app/admin/users/page.tsx
Normal file
@@ -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 (
|
||||
<main className="min-h-screen bg-zinc-50 dark:bg-black p-4 md:p-12">
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Link
|
||||
href="/admin"
|
||||
className="inline-flex items-center gap-2 text-zinc-500 hover:text-amber-600 transition-colors font-medium mb-2"
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
<h1 className="text-3xl font-black text-zinc-900 dark:text-white tracking-tight">User Management</h1>
|
||||
<p className="text-zinc-500 mt-1">Manage user credits and limits</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<Users size={20} className="text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<span className="text-xs font-black uppercase text-zinc-400">Total Users</span>
|
||||
</div>
|
||||
<div className="text-3xl font-black text-zinc-900 dark:text-white">{totalUsers}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||||
<Coins size={20} className="text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<span className="text-xs font-black uppercase text-zinc-400">Credits in Circulation</span>
|
||||
</div>
|
||||
<div className="text-3xl font-black text-zinc-900 dark:text-white">{totalCreditsInCirculation.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
|
||||
<TrendingUp size={20} className="text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<span className="text-xs font-black uppercase text-zinc-400">Total Purchased</span>
|
||||
</div>
|
||||
<div className="text-3xl font-black text-zinc-900 dark:text-white">{totalCreditsPurchased.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<TrendingDown size={20} className="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<span className="text-xs font-black uppercase text-zinc-400">Total Used</span>
|
||||
</div>
|
||||
<div className="text-3xl font-black text-zinc-900 dark:text-white">{totalCreditsUsed.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Management Table */}
|
||||
<UserManagementClient initialUsers={users} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user