chore: security hardening, mobile PWA fix & analysis expansion

- Applied strict RLS and auth validation to tracking/credit services
- Set Service Worker to Network First to fix mobile session/loading issues
- Expanded Gemini analysis summary to show distilled/bottled/batch info
- Updated SQL schema document with hardening policies
This commit is contained in:
2025-12-18 16:29:16 +01:00
parent a503e1a317
commit 22db65d109
5 changed files with 72 additions and 9 deletions

View File

@@ -3,6 +3,8 @@
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { checkIsAdmin } from './track-api-usage';
interface UserCredits {
user_id: string;
balance: number;
@@ -34,6 +36,17 @@ export async function getUserCredits(userId: string): Promise<UserCredits | null
try {
const supabase = createServerComponentClient({ cookies });
// Security check: Only self or admin can view credits
const { data: { user: currentUser } } = await supabase.auth.getUser();
if (!currentUser) return null;
const isAdmin = await checkIsAdmin(currentUser.id);
if (currentUser.id !== userId && !isAdmin) {
console.error('Unauthorized credit fetch attempt');
return null;
}
const { data, error } = await supabase
.from('user_credits')
.select('*')
@@ -132,6 +145,16 @@ export async function deductCredits(
return { success: false, error: 'Could not fetch credit information' };
}
// Security check: Only self or admin can deduct credits
const { data: { user: currentUser } } = await supabase.auth.getUser();
if (!currentUser) return { success: false, error: 'Nicht authentifiziert' };
const isAdmin = await checkIsAdmin(currentUser.id);
if (currentUser.id !== userId && !isAdmin) {
return { success: false, error: 'Nicht autorisiert' };
}
const cost = apiType === 'google_search'
? credits.google_search_cost
: credits.gemini_ai_cost;
@@ -140,7 +163,7 @@ export async function deductCredits(
if (credits.balance < cost) {
return {
success: false,
error: `Insufficient credits. Need ${cost}, have ${credits.balance}`
error: `Nicht genügend Credits. Du benötigst ${cost}, hast aber nur ${credits.balance}.`
};
}
@@ -197,6 +220,16 @@ export async function addCredits(
try {
const supabase = createServerComponentClient({ cookies });
// Security check
const { data: { user: currentUser } } = await supabase.auth.getUser();
if (!currentUser) return { success: false, error: 'Nicht authentifiziert' };
const isAdmin = await checkIsAdmin(currentUser.id);
if (!isAdmin && adminId) {
return { success: false, error: 'Nur Administratoren können Credits anpassen.' };
}
// Get current credits
const credits = await getUserCredits(userId);
if (!credits) {

View File

@@ -35,6 +35,13 @@ export async function trackApiUsage(params: TrackApiUsageParams): Promise<{ succ
try {
const supabase = createServerComponentClient({ cookies });
// Security check: Ensure user is only tracking their own usage
const { data: { user } } = await supabase.auth.getUser();
if (!user || user.id !== params.userId) {
console.error('Unauthorized API tracking attempt');
return { success: false, error: 'Nicht autorisiert' };
}
const { error } = await supabase
.from('api_usage')
.insert({