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

@@ -30,9 +30,14 @@ self.addEventListener('activate', (event) => {
});
self.addEventListener('fetch', (event) => {
// Network first, fallback to cache
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
fetch(event.request)
.then((response) => {
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
});

View File

@@ -4,12 +4,20 @@ import type { NextRequest } from 'next/server';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
const url = new URL(req.url);
// Skip logs for static assets
const isStatic = url.pathname.startsWith('/_next') || url.pathname.includes('/icon-') || url.pathname === '/favicon.ico';
// Only attempt session refresh if variables are present
if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
try {
const supabase = createMiddlewareClient({ req, res });
await supabase.auth.getSession();
const { data: { session } } = await supabase.auth.getSession();
if (!isStatic) {
console.log(`[Middleware] Path: ${url.pathname}, Session: ${session ? 'Active' : 'Missing'}, User: ${session?.user?.id || 'N/A'}`);
}
} catch (e) {
console.error('Middleware session refresh failed:', e);
}

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({

View File

@@ -108,9 +108,19 @@ ALTER TABLE tastings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view their own profile" ON profiles
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Admins can view all profiles" ON profiles
FOR SELECT USING (
EXISTS (SELECT 1 FROM admin_users WHERE user_id = auth.uid())
);
CREATE POLICY "Users can update their own profile" ON profiles
FOR UPDATE USING (auth.uid() = id);
CREATE POLICY "Admins can update all profiles" ON profiles
FOR UPDATE USING (
EXISTS (SELECT 1 FROM admin_users WHERE user_id = auth.uid())
);
-- Policies for Bottles
CREATE POLICY "Users can view their own bottles" ON bottles
FOR SELECT USING (auth.uid() = user_id);
@@ -275,7 +285,7 @@ CREATE POLICY "Users can view their own API usage" ON api_usage FOR SELECT USING
CREATE POLICY "Admins can view all API usage" ON api_usage FOR SELECT USING (
EXISTS (SELECT 1 FROM admin_users WHERE user_id = auth.uid())
);
CREATE POLICY "System can insert API usage" ON api_usage FOR INSERT WITH CHECK (true);
CREATE POLICY "Users can insert their own API usage" ON api_usage FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Policies for user_credits
CREATE POLICY "Users can view their own credits" ON user_credits FOR SELECT USING (auth.uid() = user_id);
@@ -328,11 +338,11 @@ 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)
EXISTS (SELECT 1 FROM admin_users WHERE user_id = auth.uid())
);
CREATE POLICY "System can insert transactions" ON credit_transactions
FOR INSERT WITH CHECK (true);
CREATE POLICY "Users can insert their own transactions" ON credit_transactions
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Update user_credits policies to allow admin updates
CREATE POLICY "Admins can update credits" ON user_credits