build: 444813b5-5daf-49b2-bf1a-7e4caeab4e17

This commit is contained in:
AppCakes
2026-07-02 14:33:10 +00:00
commit 77d6d340d5
146 changed files with 26652 additions and 0 deletions
@@ -0,0 +1,77 @@
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'npm:@supabase/supabase-js@2';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY')!;
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response(JSON.stringify({ error: 'Missing auth headers' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const supabase = createClient(supabaseUrl, supabaseAnonKey, {
global: { headers: { Authorization: authHeader } },
});
const body = await req.json();
const { notificationId } = body;
if (!notificationId) {
return new Response(JSON.stringify({ error: 'Missing notificationId' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// Auth validation check: ensure user token resolves
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const readAt = new Date().toISOString();
const { error: updateError } = await supabase
.from('notifications')
.update({ read_at: readAt })
.eq('id', notificationId)
.eq('user_id', user.id); // RLS also enforces this, but explicit is good
if (updateError) {
return new Response(JSON.stringify({ error: updateError.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify({ ok: true, readAt }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});
@@ -0,0 +1,164 @@
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'npm:@supabase/supabase-js@2';
import { cert, getApps, initializeApp } from 'npm:firebase-admin@12.2.0/app';
import { getMessaging } from 'npm:firebase-admin@12.2.0/messaging';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type',
};
// Singleton — Edge Functions can be warm-reused; avoid "app already exists" error
const app =
getApps()[0] ??
initializeApp({
credential: cert(
JSON.parse(
atob(Deno.env.get('FIREBASE_SERVICE_ACCOUNT')!.replace(/\s/g, ''))
)
),
});
const messaging = getMessaging(app);
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY')!;
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response(JSON.stringify({ error: 'Missing auth headers' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const supabase = createClient(supabaseUrl, supabaseAnonKey, {
global: { headers: { Authorization: authHeader } },
});
const body = await req.json();
const {
userId,
title,
body: messageBody,
type,
orderId,
voucherId,
priority = 'important',
persist = true,
} = body;
if (!userId || !title || !messageBody || !type) {
return new Response(
JSON.stringify({ error: 'Missing required parameters' }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
}
// Persist notification if needed (using service role key to insert securely if needed, but here auth is user)
// Wait, if an admin/webhook is calling this, they will use service_role. If a user is calling, they use their token.
// Let's create a service-role client just for DB updates to ensure it works even if sender is different from recipient
const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const adminSupabase = createClient(supabaseUrl, serviceRoleKey);
let notificationId: string | null = null;
let persisted = false;
if (persist) {
const { data: notifData, error: notifError } = await adminSupabase
.from('notifications')
.insert({
user_id: userId,
title,
body: messageBody,
type,
priority,
order_id: orderId || null,
voucher_id: voucherId || null,
})
.select('id')
.single();
if (!notifError && notifData) {
notificationId = notifData.id;
persisted = true;
}
}
// Get user's profile to find fcm_token and preferences
const { data: profile } = await adminSupabase
.from('profiles')
.select('fcm_token, notification_push_enabled')
.eq('id', userId)
.single();
let sent = false;
if (profile && profile.notification_push_enabled && profile.fcm_token) {
const message = {
notification: {
title: title,
body: messageBody,
},
data: {
orderId: orderId || '',
voucherId: voucherId || '',
type: type || '',
},
apns: {
payload: {
aps: {
badge: 1,
sound: 'default',
},
},
},
android: {
notification: {
sound: 'default',
notificationCount: 1,
},
},
token: profile.fcm_token,
};
try {
await messaging.send(message);
sent = true;
} catch (err: any) {
if (
err.code === 'messaging/registration-token-not-registered' ||
err.code === 'messaging/invalid-registration-token'
) {
// Token is stale — remove it from the DB
await adminSupabase
.from('profiles')
.update({ fcm_token: null })
.eq('id', userId);
}
console.error('FCM send error:', err);
}
}
return new Response(
JSON.stringify({ ok: true, sent, persisted, notificationId }),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});