diff --git a/web/supabase/functions/email-verification/index.ts b/web/supabase/functions/email-verification/index.ts new file mode 100644 index 00000000..e2bd0b7c --- /dev/null +++ b/web/supabase/functions/email-verification/index.ts @@ -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, + }); + } +}); diff --git a/web/supabase/functions/notifications-manager/index.ts b/web/supabase/functions/notifications-manager/index.ts new file mode 100644 index 00000000..32f02262 --- /dev/null +++ b/web/supabase/functions/notifications-manager/index.ts @@ -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, + }); + } +}); diff --git a/web/supabase/functions/two-factor-auth/index.ts b/web/supabase/functions/two-factor-auth/index.ts new file mode 100644 index 00000000..2a98b435 --- /dev/null +++ b/web/supabase/functions/two-factor-auth/index.ts @@ -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" }, + }, + ); + } +});