feat: implement API usage tracking and admin dashboard

- Added database schema for API tracking system:
  * api_usage table - tracks all Google Search and Gemini AI calls
  * user_credits table - prepared for future credits system
  * admin_users table - controls admin dashboard access
- Created comprehensive tracking service (track-api-usage.ts):
  * trackApiUsage() - records API calls with success/failure
  * checkDailyLimit() - enforces 80 Google Search calls/day limit
  * getUserApiStats() - per-user statistics
  * getGlobalApiStats() - app-wide statistics (admin only)
  * checkIsAdmin() - server-side authorization
- Integrated tracking into discover-whiskybase.ts:
  * Pre-call limit checking with friendly error messages
  * Post-call usage tracking for success and failures
  * User authentication verification
- Built admin dashboard at /admin:
  * Global statistics cards (total, today, by API type)
  * Top 10 users by API usage
  * Recent activity log with 50 latest calls
  * Color-coded status indicators
  * Secure access with RLS policies
- Features:
  * Daily limit resets at midnight Europe/Berlin timezone
  * Graceful error handling (allows on tracking failure)
  * Comprehensive indexes for fast queries
  * Ready for future credits/monetization system
This commit is contained in:
2025-12-18 13:56:21 +01:00
parent 334bece471
commit dd27cfe0e7
4 changed files with 536 additions and 0 deletions

View File

@@ -1,5 +1,9 @@
'use server';
import { trackApiUsage, checkDailyLimit } from './track-api-usage';
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
/**
* Service to discover a Whiskybase ID for a given bottle.
* Uses Google Custom Search JSON API to search Google and extracts the ID from the first result.
@@ -24,6 +28,27 @@ export async function discoverWhiskybaseId(bottle: {
};
}
// Get current user for tracking
const supabase = createServerComponentClient({ cookies });
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return {
success: false,
error: 'Benutzer nicht authentifiziert.'
};
}
// Check daily limit before making API call
const limitCheck = await checkDailyLimit('google_search');
if (!limitCheck.allowed) {
return {
success: false,
error: `Tageslimit für Whiskybase-Suchen erreicht (${limitCheck.limit} pro Tag). Versuche es morgen erneut.`,
limitReached: true
};
}
try {
// Construct targeted search query
const queryParts = [
@@ -46,9 +71,25 @@ export async function discoverWhiskybaseId(bottle: {
if (data.error) {
console.error('Google API Error Response:', data.error);
// Track failed API call
await trackApiUsage({
userId: user.id,
apiType: 'google_search',
endpoint: 'customsearch/v1',
success: false,
errorMessage: data.error.message
});
throw new Error(data.error.message || 'Google API Error');
}
// Track successful API call
await trackApiUsage({
userId: user.id,
apiType: 'google_search',
endpoint: 'customsearch/v1',
success: true
});
if (!data.items || data.items.length === 0) {
return {
success: false,
@@ -77,6 +118,16 @@ export async function discoverWhiskybaseId(bottle: {
return { success: false, error: 'Konnte keine gültige Whiskybase-ID im Suchergebnis finden.' };
} catch (error) {
console.error('Whiskybase Discovery Error:', error);
// Track failed attempt (if not already tracked)
if (user) {
await trackApiUsage({
userId: user.id,
apiType: 'google_search',
endpoint: 'customsearch/v1',
success: false,
errorMessage: error instanceof Error ? error.message : 'Unknown error'
});
}
return {
success: false,
error: error instanceof Error ? error.message : 'Fehler bei der Suche auf Whiskybase.'