foodsnap/supabase/functions/validate-access/index.ts

118 lines
4.2 KiB
TypeScript
Raw Normal View History

import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// 1. Initialize Supabase Client with the incoming user's Auth context
// This allows us to use `auth.getUser()` securely based on the JWT sent by the frontend.
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: req.headers.get('Authorization')! },
},
}
);
// 2. Get User from Token
const {
data: { user },
error: authError,
} = await supabaseClient.auth.getUser();
if (authError || !user) {
return new Response(
JSON.stringify({ allowed: false, error: 'Unauthorized', reason: 'auth_failed' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// 3. Check Entitlements (Active Plan?)
// We look for the most recent entitlement that is active.
const { data: entitlement, error: entError } = await supabaseClient
.from('user_entitlements')
.select('is_active, valid_until, entitlement_code')
.eq('user_id', user.id)
.order('valid_until', { ascending: false })
.maybeSingle();
if (entError) {
console.error("Entitlement check error:", entError);
}
// A plan is active if is_active=true AND (valid_until is NULL (lifetime) OR valid_until > now)
const isActive = entitlement?.is_active && (!entitlement.valid_until || new Date(entitlement.valid_until) > new Date());
if (isActive) {
return new Response(
JSON.stringify({
allowed: true,
plan: entitlement.entitlement_code,
reason: 'plan_active',
quota_remaining: -1 // Infinite/Plan
}),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// 4. Check Free Quota (Coach Analyses)
// Counts how many analyses already consumed the free quota.
const { count, error: countError } = await supabaseClient
.from('coach_analyses')
.select('*', { count: 'exact', head: true })
.eq('user_id', user.id)
.eq('used_free_quota', true);
if (countError) {
console.error("Quota check error:", countError);
throw new Error("Failed to check quota usage.");
}
const FREE_LIMIT = 3; // Defined limit for Coach
const used = count || 0;
const remaining = Math.max(0, FREE_LIMIT - used);
if (remaining > 0) {
return new Response(
JSON.stringify({
allowed: true,
plan: 'free',
reason: 'free_quota',
quota_remaining: remaining
}),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// 5. Quota Exceeded
return new Response(
JSON.stringify({
allowed: false,
plan: 'free',
reason: 'quota_exceeded',
quota_remaining: 0
}),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } } // 200 OK because logic was successful, just access denied
);
} catch (error: any) {
console.error("Validate Access Error:", error);
return new Response(
JSON.stringify({ allowed: false, error: error.message }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});