mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-08-12 17:21:39 +00:00
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:
@@ -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" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user