From 4b0f5742af6e6df0e69902cdc6801172be4e430e Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Sat, 13 Dec 2025 03:17:12 +0000 Subject: [PATCH] Update authentication and authorization logic across multiple API endpoints Replaces direct Supabase client instantiation with a unified authentication and authorization helper, introducing role-based access control to sensitive endpoints like escrow and payroll, and standardizing compliance event logging. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 5eb35c62-c5ab-4c7e-9552-8dc89efa29f3 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/aPpJgbb Replit-Helium-Checkpoint-Created: true --- api/corp/escrow.ts | 53 ++++++++++++++--------------- api/corp/payroll.ts | 45 +++++++----------------- api/foundation/gig-radar.ts | 25 +++++--------- api/nexus-core/talent-profiles.ts | 29 ++++++---------- api/nexus-core/time-logs-approve.ts | 37 ++++++-------------- api/nexus-core/time-logs-submit.ts | 33 +++++++----------- api/nexus-core/time-logs.ts | 33 +++++++----------- api/studio/contracts.ts | 14 ++++---- api/studio/time-logs.ts | 28 ++++++++------- 9 files changed, 116 insertions(+), 181 deletions(-) diff --git a/api/corp/escrow.ts b/api/corp/escrow.ts index d1357a5e..18943e4a 100644 --- a/api/corp/escrow.ts +++ b/api/corp/escrow.ts @@ -1,28 +1,23 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { authenticateRequest, requireAuth, requireRole, logComplianceEvent } from "../_auth"; export default async function handler(req: VercelRequest, res: VercelResponse) { - const supabase = getAdminClient(); - - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const token = authHeader.split(' ')[1]; - const { data: { user }, error: authError } = await supabase.auth.getUser(token); - - if (authError || !user) { - return res.status(401).json({ error: 'Invalid token' }); - } + const auth = await authenticateRequest(req); + if (!requireAuth(auth, res)) return; + + const { userClient, adminClient, user } = auth; if (req.method === 'GET') { + // GET: Only clients and admins can view escrow records + if (!requireRole(auth, ['client', 'admin'], res)) return; + const { contract_id } = req.query; - let query = supabase - .from('nexus_escrow_ledger') - .select('*') - .or(`client_id.eq.${user.id},creator_id.eq.${user.id}`); + // Clients can only see escrow records where they are the client + // Admins can see all escrow records + let query = user.user_type === 'admin' + ? adminClient.from('nexus_escrow_ledger').select('*') + : userClient.from('nexus_escrow_ledger').select('*').eq('client_id', user.id); if (contract_id) { query = query.eq('contract_id', contract_id); @@ -38,13 +33,16 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { } if (req.method === 'POST') { + // POST (funding escrow): Only clients and admins can fund + if (!requireRole(auth, ['client', 'admin'], res)) return; + const { contract_id, amount } = req.body; if (!contract_id || !amount) { return res.status(400).json({ error: 'contract_id and amount required' }); } - const { data: contract } = await supabase + const { data: contract } = await userClient .from('nexus_contracts') .select('id, client_id, creator_id, status') .eq('id', contract_id) @@ -54,18 +52,19 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(404).json({ error: 'Contract not found' }); } - if (contract.client_id !== user.id) { - return res.status(403).json({ error: 'Only the client can fund escrow' }); + // Even admins must be the contract client to fund (or we could allow admin override) + if (contract.client_id !== user.id && user.user_type !== 'admin') { + return res.status(403).json({ error: 'Only the client or admin can fund escrow' }); } - const { data: existing } = await supabase + const { data: existing } = await userClient .from('nexus_escrow_ledger') .select('id, escrow_balance, funds_deposited') .eq('contract_id', contract_id) .single(); if (existing) { - const { data, error } = await supabase + const { data, error } = await userClient .from('nexus_escrow_ledger') .update({ escrow_balance: existing.escrow_balance + amount, @@ -85,7 +84,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(200).json({ data }); } - const { data, error } = await supabase + const { data, error } = await userClient .from('nexus_escrow_ledger') .insert({ contract_id: contract_id, @@ -103,19 +102,19 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(500).json({ error: error.message }); } - await supabase.from('nexus_compliance_events').insert({ + await logComplianceEvent(adminClient, { entity_type: 'escrow', entity_id: data.id, event_type: 'escrow_funded', event_category: 'financial', actor_id: user.id, - actor_role: 'client', + actor_role: user.user_type === 'admin' ? 'admin' : 'client', realm_context: 'corp', description: `Escrow funded with $${amount}`, payload: { contract_id, amount }, financial_amount: amount, legal_entity: 'for_profit' - }); + }, req); return res.status(201).json({ data }); } diff --git a/api/corp/payroll.ts b/api/corp/payroll.ts index 66a2ce00..4d422d86 100644 --- a/api/corp/payroll.ts +++ b/api/corp/payroll.ts @@ -1,35 +1,16 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { authenticateRequest, requireAdmin, logComplianceEvent } from "../_auth"; export default async function handler(req: VercelRequest, res: VercelResponse) { - const supabase = getAdminClient(); - - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const token = authHeader.split(' ')[1]; - const { data: { user }, error: authError } = await supabase.auth.getUser(token); - - if (authError || !user) { - return res.status(401).json({ error: 'Invalid token' }); - } + const auth = await authenticateRequest(req); + if (!requireAdmin(auth, res)) return; - const { data: userProfile } = await supabase - .from('user_profiles') - .select('user_type') - .eq('id', user.id) - .single(); - - if (userProfile?.user_type !== 'admin') { - return res.status(403).json({ error: 'Admin access required' }); - } + const { adminClient, user } = auth; if (req.method === 'GET') { const { status, start_date, end_date, tax_year } = req.query; - let query = supabase + let query = adminClient .from('nexus_payouts') .select(` *, @@ -74,7 +55,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'payout_ids array required' }); } - const { data: payouts } = await supabase + const { data: payouts } = await adminClient .from('nexus_payouts') .select('*') .in('id', payout_ids) @@ -84,7 +65,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'No pending payouts found' }); } - const { data, error } = await supabase + const { data, error } = await adminClient .from('nexus_payouts') .update({ status: 'processing', @@ -97,18 +78,18 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(500).json({ error: error.message }); } - await supabase.from('nexus_compliance_events').insert({ + await logComplianceEvent(adminClient, { entity_type: 'payroll', - entity_id: user.id, + entity_id: user!.id, event_type: 'payroll_batch_processing', event_category: 'financial', - actor_id: user.id, + actor_id: user!.id, actor_role: 'admin', realm_context: 'corp', description: `Processing ${data?.length} payouts`, payload: { payout_ids, total_amount: data?.reduce((sum, p) => sum + Number(p.net_amount), 0) }, legal_entity: 'for_profit' - }); + }, req); return res.status(200).json({ data, @@ -119,12 +100,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { if (req.method === 'GET' && req.query.action === 'summary') { const currentYear = new Date().getFullYear(); - const { data: yearPayouts } = await supabase + const { data: yearPayouts } = await adminClient .from('nexus_payouts') .select('net_amount, status, tax_year') .eq('tax_year', currentYear); - const { data: azHours } = await supabase + const { data: azHours } = await adminClient .from('nexus_time_logs') .select('az_eligible_hours') .eq('submission_status', 'approved') diff --git a/api/foundation/gig-radar.ts b/api/foundation/gig-radar.ts index 1fabdd66..5ba06bbd 100644 --- a/api/foundation/gig-radar.ts +++ b/api/foundation/gig-radar.ts @@ -1,28 +1,19 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { authenticateRequest, requireAuth, logComplianceEvent } from "../_auth"; export default async function handler(req: VercelRequest, res: VercelResponse) { if (req.method !== 'GET') { return res.status(405).json({ error: 'Method not allowed' }); } - const supabase = getAdminClient(); - - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const token = authHeader.split(' ')[1]; - const { data: { user }, error: authError } = await supabase.auth.getUser(token); - - if (authError || !user) { - return res.status(401).json({ error: 'Invalid token' }); - } + const auth = await authenticateRequest(req); + if (!requireAuth(auth, res)) return; + + const { userClient, adminClient, user } = auth; const { category, skills, experience, limit = 20, offset = 0 } = req.query; - const { data, error } = await supabase + const { data, error } = await userClient .from('foundation_gig_radar') .select('*') .order('published_at', { ascending: false }) @@ -49,7 +40,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { filteredData = filteredData.filter(d => d.required_experience === experience); } - await supabase.from('nexus_compliance_events').insert({ + await logComplianceEvent(adminClient, { entity_type: 'gig_radar', entity_id: user.id, event_type: 'gig_radar_accessed', @@ -65,7 +56,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { sensitive_data_accessed: false, cross_entity_access: true, legal_entity: 'non_profit' - }); + }, req); return res.status(200).json({ data: filteredData, diff --git a/api/nexus-core/talent-profiles.ts b/api/nexus-core/talent-profiles.ts index 4b640b47..c6c4482f 100644 --- a/api/nexus-core/talent-profiles.ts +++ b/api/nexus-core/talent-profiles.ts @@ -1,23 +1,14 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { authenticateRequest, requireAuth, logComplianceEvent } from "../_auth"; export default async function handler(req: VercelRequest, res: VercelResponse) { - const supabase = getAdminClient(); - - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const token = authHeader.split(' ')[1]; - const { data: { user }, error: authError } = await supabase.auth.getUser(token); - - if (authError || !user) { - return res.status(401).json({ error: 'Invalid token' }); - } + const auth = await authenticateRequest(req); + if (!requireAuth(auth, res)) return; + + const { userClient, adminClient, user } = auth; if (req.method === 'GET') { - const { data, error } = await supabase + const { data, error } = await userClient .from('nexus_talent_profiles') .select('*') .eq('user_id', user.id) @@ -33,7 +24,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { if (req.method === 'POST') { const body = req.body; - const { data, error } = await supabase + const { data, error } = await userClient .from('nexus_talent_profiles') .upsert({ user_id: user.id, @@ -54,7 +45,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(500).json({ error: error.message }); } - await supabase.from('nexus_compliance_events').insert({ + await logComplianceEvent(adminClient, { entity_type: 'talent', entity_id: data.id, event_type: 'profile_updated', @@ -64,13 +55,13 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { realm_context: 'nexus', description: 'Talent profile updated', payload: { fields_updated: Object.keys(body) } - }); + }, req); return res.status(200).json({ data }); } if (req.method === 'GET' && req.query.action === 'compliance-summary') { - const { data, error } = await supabase + const { data, error } = await userClient .rpc('get_talent_compliance_summary', { p_user_id: user.id }); if (error) { diff --git a/api/nexus-core/time-logs-approve.ts b/api/nexus-core/time-logs-approve.ts index 10a0814a..86190a3e 100644 --- a/api/nexus-core/time-logs-approve.ts +++ b/api/nexus-core/time-logs-approve.ts @@ -1,30 +1,15 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { authenticateRequest, requireAuth, logComplianceEvent } from "../_auth"; export default async function handler(req: VercelRequest, res: VercelResponse) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } - const supabase = getAdminClient(); - - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const token = authHeader.split(' ')[1]; - const { data: { user }, error: authError } = await supabase.auth.getUser(token); - - if (authError || !user) { - return res.status(401).json({ error: 'Invalid token' }); - } + const auth = await authenticateRequest(req); + if (!requireAuth(auth, res)) return; - const { data: userProfile } = await supabase - .from('user_profiles') - .select('user_type') - .eq('id', user.id) - .single(); + const { userClient, adminClient, user } = auth; const { time_log_id, decision, notes } = req.body; @@ -36,7 +21,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'Invalid decision. Must be: approved, rejected, or needs_correction' }); } - const { data: timeLog } = await supabase + const { data: timeLog } = await adminClient .from('nexus_time_logs') .select('*, nexus_contracts!inner(client_id)') .eq('id', time_log_id) @@ -47,7 +32,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { } const isClient = timeLog.nexus_contracts?.client_id === user.id; - const isAdmin = userProfile?.user_type === 'admin'; + const isAdmin = user.user_type === 'admin'; if (!isClient && !isAdmin) { return res.status(403).json({ error: 'Only the contract client or admin can approve time logs' }); @@ -60,7 +45,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { const newStatus = decision === 'approved' ? 'approved' : decision === 'rejected' ? 'rejected' : 'rejected'; - const { data, error } = await supabase + const { data, error } = await adminClient .from('nexus_time_logs') .update({ submission_status: newStatus, @@ -76,17 +61,17 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(500).json({ error: error.message }); } - await supabase.from('nexus_time_log_audits').insert({ + await adminClient.from('nexus_time_log_audits').insert({ time_log_id: time_log_id, reviewer_id: user.id, audit_type: decision === 'approved' ? 'approval' : 'rejection', decision: decision, notes: notes, - ip_address: req.headers['x-forwarded-for']?.toString() || req.socket.remoteAddress, + ip_address: req.headers['x-forwarded-for']?.toString() || req.socket?.remoteAddress, user_agent: req.headers['user-agent'] }); - await supabase.from('nexus_compliance_events').insert({ + await logComplianceEvent(adminClient, { entity_type: 'time_log', entity_id: time_log_id, event_type: `time_log_${decision}`, @@ -96,7 +81,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { realm_context: 'nexus', description: `Time log ${decision} by ${isAdmin ? 'admin' : 'client'}`, payload: { decision, notes } - }); + }, req); return res.status(200).json({ data }); } diff --git a/api/nexus-core/time-logs-submit.ts b/api/nexus-core/time-logs-submit.ts index 81dd8080..071e148d 100644 --- a/api/nexus-core/time-logs-submit.ts +++ b/api/nexus-core/time-logs-submit.ts @@ -1,26 +1,17 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { authenticateRequest, requireAuth, logComplianceEvent } from "../_auth"; export default async function handler(req: VercelRequest, res: VercelResponse) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } - const supabase = getAdminClient(); - - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const token = authHeader.split(' ')[1]; - const { data: { user }, error: authError } = await supabase.auth.getUser(token); - - if (authError || !user) { - return res.status(401).json({ error: 'Invalid token' }); - } + const auth = await authenticateRequest(req); + if (!requireAuth(auth, res)) return; - const { data: talentProfile } = await supabase + const { userClient, adminClient, user } = auth; + + const { data: talentProfile } = await userClient .from('nexus_talent_profiles') .select('id') .eq('user_id', user.id) @@ -36,7 +27,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'time_log_ids array required' }); } - const { data: logs, error: fetchError } = await supabase + const { data: logs, error: fetchError } = await userClient .from('nexus_time_logs') .select('id, submission_status') .in('id', time_log_ids) @@ -59,7 +50,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'No valid time logs found' }); } - const { data, error } = await supabase + const { data, error } = await userClient .from('nexus_time_logs') .update({ submission_status: 'submitted', @@ -74,18 +65,18 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { } for (const log of data || []) { - await supabase.from('nexus_time_log_audits').insert({ + await adminClient.from('nexus_time_log_audits').insert({ time_log_id: log.id, reviewer_id: null, audit_type: 'review', decision: 'submitted', notes: 'Time log submitted for review', - ip_address: req.headers['x-forwarded-for']?.toString() || req.socket.remoteAddress, + ip_address: req.headers['x-forwarded-for']?.toString() || req.socket?.remoteAddress, user_agent: req.headers['user-agent'] }); } - await supabase.from('nexus_compliance_events').insert({ + await logComplianceEvent(adminClient, { entity_type: 'time_log', entity_id: talentProfile.id, event_type: 'batch_submitted', @@ -95,7 +86,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { realm_context: 'nexus', description: `Submitted ${data?.length} time logs for review`, payload: { time_log_ids: validIds } - }); + }, req); return res.status(200).json({ data, diff --git a/api/nexus-core/time-logs.ts b/api/nexus-core/time-logs.ts index 4770fc89..f68679d9 100644 --- a/api/nexus-core/time-logs.ts +++ b/api/nexus-core/time-logs.ts @@ -1,22 +1,13 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { authenticateRequest, requireAuth } from "../_auth"; export default async function handler(req: VercelRequest, res: VercelResponse) { - const supabase = getAdminClient(); - - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const token = authHeader.split(' ')[1]; - const { data: { user }, error: authError } = await supabase.auth.getUser(token); - - if (authError || !user) { - return res.status(401).json({ error: 'Invalid token' }); - } + const auth = await authenticateRequest(req); + if (!requireAuth(auth, res)) return; - const { data: talentProfile } = await supabase + const { userClient, user } = auth; + + const { data: talentProfile } = await userClient .from('nexus_talent_profiles') .select('id, az_eligible') .eq('user_id', user.id) @@ -29,7 +20,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { if (req.method === 'GET') { const { contract_id, start_date, end_date, status } = req.query; - let query = supabase + let query = userClient .from('nexus_time_logs') .select('*') .eq('talent_profile_id', talentProfile.id) @@ -56,7 +47,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { ? body.hours_worked : 0; - const { data, error } = await supabase + const { data, error } = await userClient .from('nexus_time_logs') .insert({ talent_profile_id: talentProfile.id, @@ -95,7 +86,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'Time log ID required' }); } - const { data: existingLog } = await supabase + const { data: existingLog } = await userClient .from('nexus_time_logs') .select('*') .eq('id', id) @@ -114,7 +105,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { ? (body.hours_worked || existingLog.hours_worked) : 0; - const { data, error } = await supabase + const { data, error } = await userClient .from('nexus_time_logs') .update({ ...body, @@ -139,7 +130,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'Time log ID required' }); } - const { data: existingLog } = await supabase + const { data: existingLog } = await userClient .from('nexus_time_logs') .select('submission_status') .eq('id', id) @@ -154,7 +145,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'Can only delete draft time logs' }); } - const { error } = await supabase + const { error } = await userClient .from('nexus_time_logs') .delete() .eq('id', id); diff --git a/api/studio/contracts.ts b/api/studio/contracts.ts index 426d27c4..b56fc47a 100644 --- a/api/studio/contracts.ts +++ b/api/studio/contracts.ts @@ -1,5 +1,5 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { getAdminClient, getUserClient } from "../_auth"; const STUDIO_API_KEY = process.env.STUDIO_API_KEY; @@ -8,23 +8,25 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(405).json({ error: 'Method not allowed' }); } - const supabase = getAdminClient(); - - const apiKey = req.headers['x-studio-api-key'] || req.headers['authorization']?.replace('Bearer ', ''); - + const apiKey = req.headers['x-studio-api-key']; const authHeader = req.headers.authorization; + let userId: string | null = null; let isServiceAuth = false; + let supabase: any; if (apiKey === STUDIO_API_KEY && STUDIO_API_KEY) { isServiceAuth = true; + supabase = getAdminClient(); } else if (authHeader?.startsWith('Bearer ')) { const token = authHeader.split(' ')[1]; - const { data: { user }, error } = await supabase.auth.getUser(token); + const adminClient = getAdminClient(); + const { data: { user }, error } = await adminClient.auth.getUser(token); if (error || !user) { return res.status(401).json({ error: 'Invalid token' }); } userId = user.id; + supabase = getUserClient(token); } else { return res.status(401).json({ error: 'Unauthorized' }); } diff --git a/api/studio/time-logs.ts b/api/studio/time-logs.ts index ffeea937..7170bc59 100644 --- a/api/studio/time-logs.ts +++ b/api/studio/time-logs.ts @@ -1,30 +1,34 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { getAdminClient } from "../_supabase"; +import { getAdminClient, getUserClient, logComplianceEvent } from "../_auth"; const STUDIO_API_KEY = process.env.STUDIO_API_KEY; export default async function handler(req: VercelRequest, res: VercelResponse) { - const supabase = getAdminClient(); - - const apiKey = req.headers['x-studio-api-key'] || req.headers['authorization']?.replace('Bearer ', ''); - + const apiKey = req.headers['x-studio-api-key']; const authHeader = req.headers.authorization; + let userId: string | null = null; let isServiceAuth = false; + let supabase: any; if (apiKey === STUDIO_API_KEY && STUDIO_API_KEY) { isServiceAuth = true; + supabase = getAdminClient(); } else if (authHeader?.startsWith('Bearer ')) { const token = authHeader.split(' ')[1]; - const { data: { user }, error } = await supabase.auth.getUser(token); + const adminClient = getAdminClient(); + const { data: { user }, error } = await adminClient.auth.getUser(token); if (error || !user) { return res.status(401).json({ error: 'Invalid token' }); } userId = user.id; + supabase = getUserClient(token); } else { return res.status(401).json({ error: 'Unauthorized - requires Bearer token or X-Studio-API-Key' }); } + const adminClient = getAdminClient(); + if (req.method === 'POST') { const body = req.body; @@ -34,7 +38,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { const targetUserId = body.user_id || userId; - const { data: talentProfile } = await supabase + const { data: talentProfile } = await adminClient .from('nexus_talent_profiles') .select('id, az_eligible') .eq('user_id', targetUserId) @@ -48,7 +52,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { ? body.hours_worked : 0; - const { data, error } = await supabase + const { data, error } = await adminClient .from('nexus_time_logs') .insert({ talent_profile_id: talentProfile.id, @@ -77,12 +81,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(500).json({ error: error.message }); } - await supabase.from('nexus_compliance_events').insert({ + await logComplianceEvent(adminClient, { entity_type: 'time_log', entity_id: data.id, event_type: 'studio_time_log_created', event_category: 'compliance', - actor_id: isServiceAuth ? null : userId, + actor_id: isServiceAuth ? undefined : userId || undefined, actor_role: isServiceAuth ? 'api' : 'talent', realm_context: 'studio', description: 'Time log submitted via Studio API', @@ -91,7 +95,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { location_verified: data.location_verified, az_eligible_hours: azEligibleHours } - }); + }, req); return res.status(201).json({ data }); } @@ -114,7 +118,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { `); if (targetUserId) { - const { data: talentProfile } = await supabase + const { data: talentProfile } = await adminClient .from('nexus_talent_profiles') .select('id') .eq('user_id', targetUserId)