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:
@@ -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);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user