Merge pull request #23 from pezkuwichain/feat/adopt-cloud-functions

feat(supabase): adopt the three functions left behind on the cloud project
This commit is contained in:
SatoshiQaziMuhammed
2026-07-30 02:40:23 -07:00
committed by GitHub
3 changed files with 531 additions and 0 deletions
@@ -0,0 +1,100 @@
export const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
};
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const { action, token, email } = await req.json();
if (action === "send") {
// Generate verification token
const verificationToken = crypto.randomUUID();
// Store token in database (expires in 24 hours)
const { createClient } =
await import("https://esm.sh/@supabase/supabase-js@2");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, supabaseKey);
const { data: user } = await supabase.auth.admin.getUserByEmail(email);
if (!user?.user) {
throw new Error("User not found");
}
await supabase.from("email_verification_tokens").insert({
user_id: user.user.id,
token: verificationToken,
email: email,
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
});
// In production, send email via email service
// For now, return the token
return new Response(
JSON.stringify({
success: true,
message: "Verification email sent",
token: verificationToken, // Remove in production
}),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
if (action === "verify") {
const { createClient } =
await import("https://esm.sh/@supabase/supabase-js@2");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, supabaseKey);
// Check token validity
const { data: tokenData } = await supabase
.from("email_verification_tokens")
.select("*")
.eq("token", token)
.single();
if (!tokenData || new Date(tokenData.expires_at) < new Date()) {
throw new Error("Invalid or expired token");
}
// Update user profile
await supabase
.from("profiles")
.update({
email_verified: true,
email_verified_at: new Date().toISOString(),
})
.eq("id", tokenData.user_id);
// Delete used token
await supabase
.from("email_verification_tokens")
.delete()
.eq("token", token);
return new Response(
JSON.stringify({
success: true,
message: "Email verified successfully",
}),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
throw new Error("Invalid action");
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 400,
});
}
});
@@ -0,0 +1,126 @@
export const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
};
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const {
action,
userId,
notificationId,
title,
message,
type,
actionUrl,
notificationIds,
} = await req.json();
// Import Supabase client
const { createClient } =
await import("https://esm.sh/@supabase/supabase-js@2");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, supabaseServiceKey);
let result;
switch (action) {
case "create":
// Create notification
const { data: notification, error: createError } = await supabase
.from("notifications")
.insert({
user_id: userId,
title,
message,
type: type || "info",
action_url: actionUrl,
})
.select()
.single();
if (createError) throw createError;
result = { success: true, notification };
break;
case "markRead":
// Mark notification as read
const { error: readError } = await supabase
.from("notifications")
.update({ read: true, read_at: new Date().toISOString() })
.eq("id", notificationId)
.eq("user_id", userId);
if (readError) throw readError;
result = { success: true };
break;
case "markAllRead":
// Mark all notifications as read
const { error: allReadError } = await supabase
.from("notifications")
.update({ read: true, read_at: new Date().toISOString() })
.eq("user_id", userId)
.eq("read", false);
if (allReadError) throw allReadError;
result = { success: true };
break;
case "delete":
// Delete notification
const { error: deleteError } = await supabase
.from("notifications")
.delete()
.eq("id", notificationId)
.eq("user_id", userId);
if (deleteError) throw deleteError;
result = { success: true };
break;
case "deleteMultiple":
// Delete multiple notifications
const { error: deleteMultipleError } = await supabase
.from("notifications")
.delete()
.in("id", notificationIds)
.eq("user_id", userId);
if (deleteMultipleError) throw deleteMultipleError;
result = { success: true };
break;
case "getUnreadCount":
// Get unread notification count
const { count, error: countError } = await supabase
.from("notifications")
.select("*", { count: "exact", head: true })
.eq("user_id", userId)
.eq("read", false);
if (countError) throw countError;
result = { success: true, count };
break;
default:
throw new Error("Invalid action");
}
return new Response(JSON.stringify(result), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 200,
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 400,
});
}
});
@@ -0,0 +1,305 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.39.3";
export const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
};
function generateSecret(): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let secret = "";
for (let i = 0; i < 32; i++) {
secret += chars[Math.floor(Math.random() * chars.length)];
}
return secret;
}
function generateBackupCodes(): string[] {
const codes: string[] = [];
for (let i = 0; i < 10; i++) {
let code = "";
for (let j = 0; j < 8; j++) {
code += Math.floor(Math.random() * 10).toString();
}
codes.push(code);
}
return codes;
}
function generateTOTP(secret: string, window: number = 0): string {
// Simple TOTP implementation
const time = Math.floor(Date.now() / 30000) + window;
const encoder = new TextEncoder();
const data = encoder.encode(secret + time.toString());
// Simple hash-based OTP
let hash = 0;
for (let i = 0; i < data.length; i++) {
hash = (hash << 5) - hash + data[i];
hash = hash & hash;
}
const otp = Math.abs(hash) % 1000000;
return otp.toString().padStart(6, "0");
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, supabaseServiceKey);
const { action, userId, code, backupCode } = await req.json();
switch (action) {
case "setup": {
// Generate new 2FA setup
const secret = generateSecret();
const backupCodes = generateBackupCodes();
// Store in database
const { error } = await supabase.from("two_factor_auth").upsert({
user_id: userId,
secret,
backup_codes: backupCodes,
enabled: false,
updated_at: new Date().toISOString(),
});
if (error) throw error;
// Generate QR code URL for authenticator apps
const otpUrl = `otpauth://totp/PezKuwiChain:user?secret=${secret}&issuer=PezKuwiChain`;
return new Response(
JSON.stringify({
success: true,
secret,
qrCode: otpUrl,
backupCodes,
}),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
case "enable": {
// Verify code and enable 2FA
const { data: twoFA } = await supabase
.from("two_factor_auth")
.select("*")
.eq("user_id", userId)
.single();
if (!twoFA) {
throw new Error("2FA not set up");
}
// Check code with time window
let isValid = false;
for (let window = -1; window <= 1; window++) {
if (code === generateTOTP(twoFA.secret, window)) {
isValid = true;
break;
}
}
if (!isValid) {
return new Response(
JSON.stringify({
success: false,
error: "Invalid verification code",
}),
{
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
// Enable 2FA
await supabase
.from("two_factor_auth")
.update({ enabled: true, updated_at: new Date().toISOString() })
.eq("user_id", userId);
return new Response(
JSON.stringify({
success: true,
message: "2FA enabled successfully",
}),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
case "verify": {
// Verify 2FA code during login
const { data: twoFA } = await supabase
.from("two_factor_auth")
.select("*")
.eq("user_id", userId)
.eq("enabled", true)
.single();
if (!twoFA) {
return new Response(
JSON.stringify({
success: true,
required: false,
}),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
// Check if it's a backup code
if (backupCode) {
const codes = twoFA.backup_codes || [];
const codeIndex = codes.indexOf(backupCode);
if (codeIndex === -1) {
return new Response(
JSON.stringify({
success: false,
error: "Invalid backup code",
}),
{
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
// Remove used backup code
codes.splice(codeIndex, 1);
await supabase
.from("two_factor_auth")
.update({ backup_codes: codes })
.eq("user_id", userId);
return new Response(
JSON.stringify({
success: true,
message: "Backup code verified",
}),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
// Verify TOTP code with time window
let isValid = false;
for (let window = -1; window <= 1; window++) {
if (code === generateTOTP(twoFA.secret, window)) {
isValid = true;
break;
}
}
if (!isValid) {
return new Response(
JSON.stringify({
success: false,
error: "Invalid verification code",
}),
{
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
return new Response(
JSON.stringify({
success: true,
message: "Code verified successfully",
}),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
case "disable": {
// Disable 2FA
await supabase.from("two_factor_auth").delete().eq("user_id", userId);
return new Response(
JSON.stringify({
success: true,
message: "2FA disabled successfully",
}),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
case "check": {
// Check if 2FA is enabled
const { data: twoFA } = await supabase
.from("two_factor_auth")
.select("enabled")
.eq("user_id", userId)
.single();
return new Response(
JSON.stringify({
success: true,
enabled: twoFA?.enabled || false,
}),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
case "regenerate-backup": {
// Regenerate backup codes
const newBackupCodes = generateBackupCodes();
await supabase
.from("two_factor_auth")
.update({
backup_codes: newBackupCodes,
updated_at: new Date().toISOString(),
})
.eq("user_id", userId);
return new Response(
JSON.stringify({
success: true,
backupCodes: newBackupCodes,
}),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
default:
throw new Error("Invalid action");
}
} catch (error) {
return new Response(
JSON.stringify({
success: false,
error: error.message,
}),
{
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
},
);
}
});