Fix: Implement Phase 1 and 2 for Account & Profile tab

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 18:44:14 +00:00
parent d4b859f637
commit 3833ba9748
9 changed files with 318 additions and 35 deletions

View File

@@ -40,4 +40,7 @@ verify_jwt = true
verify_jwt = true
[functions.process-oauth-profile]
verify_jwt = true
verify_jwt = true
[functions.validate-email-backend]
verify_jwt = false

View File

@@ -0,0 +1,94 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
// Common disposable email domains (subset for performance)
const DISPOSABLE_DOMAINS = new Set([
'tempmail.com', 'guerrillamail.com', '10minutemail.com', 'mailinator.com',
'throwaway.email', 'temp-mail.org', 'fakeinbox.com', 'maildrop.cc',
'yopmail.com', 'sharklasers.com', 'guerrillamailblock.com'
]);
interface EmailValidationResult {
valid: boolean;
reason?: string;
suggestions?: string[];
}
function validateEmailFormat(email: string): EmailValidationResult {
// Basic format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return { valid: false, reason: 'Invalid email format' };
}
// Extract domain
const domain = email.split('@')[1].toLowerCase();
// Check against disposable domains
if (DISPOSABLE_DOMAINS.has(domain)) {
return {
valid: false,
reason: 'Disposable email addresses are not allowed. Please use a permanent email address.',
suggestions: ['gmail.com', 'outlook.com', 'yahoo.com', 'protonmail.com']
};
}
// Check for suspicious patterns
if (domain.includes('temp') || domain.includes('disposable') || domain.includes('trash')) {
return {
valid: false,
reason: 'This email domain appears to be temporary. Please use a permanent email address.',
};
}
return { valid: true };
}
serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
const { email } = await req.json();
if (!email || typeof email !== 'string') {
return new Response(
JSON.stringify({ valid: false, reason: 'Email is required' }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
// Validate email
const result = validateEmailFormat(email.toLowerCase().trim());
return new Response(
JSON.stringify(result),
{
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
} catch (error) {
console.error('Email validation error:', error);
return new Response(
JSON.stringify({
valid: false,
reason: 'Failed to validate email. Please try again.'
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
});

View File

@@ -0,0 +1,126 @@
-- Phase 2: Create update_profile RPC function with validation and audit logging
CREATE OR REPLACE FUNCTION public.update_profile(
p_username text,
p_display_name text DEFAULT NULL,
p_bio text DEFAULT NULL,
p_preferred_pronouns text DEFAULT NULL,
p_show_pronouns boolean DEFAULT false,
p_preferred_language text DEFAULT 'en',
p_personal_location text DEFAULT NULL,
p_avatar_url text DEFAULT NULL,
p_avatar_image_id text DEFAULT NULL
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id uuid;
v_old_profile record;
v_changes jsonb := '[]'::jsonb;
v_change jsonb;
BEGIN
-- Get authenticated user ID
v_user_id := auth.uid();
IF v_user_id IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
-- Get current profile for comparison
SELECT * INTO v_old_profile
FROM public.profiles
WHERE user_id = v_user_id;
IF v_old_profile IS NULL THEN
RAISE EXCEPTION 'Profile not found';
END IF;
-- Check if account is deactivated
IF v_old_profile.deactivated THEN
RAISE EXCEPTION 'Cannot update deactivated account';
END IF;
-- Validate username length and format (3-20 chars, alphanumeric, underscore, hyphen)
IF p_username IS NOT NULL THEN
IF LENGTH(p_username) < 3 OR LENGTH(p_username) > 20 THEN
RAISE EXCEPTION 'Username must be between 3 and 20 characters';
END IF;
IF p_username !~ '^[a-z0-9_-]+$' THEN
RAISE EXCEPTION 'Username can only contain lowercase letters, numbers, underscores, and hyphens';
END IF;
-- Check for consecutive special characters
IF p_username ~ '__' OR p_username ~ '--' OR p_username ~ '_-' OR p_username ~ '-_' THEN
RAISE EXCEPTION 'Username cannot contain consecutive special characters';
END IF;
END IF;
-- Validate display_name length (max 50 chars)
IF p_display_name IS NOT NULL AND LENGTH(p_display_name) > 50 THEN
RAISE EXCEPTION 'Display name must be 50 characters or less';
END IF;
-- Validate bio length (max 500 chars)
IF p_bio IS NOT NULL AND LENGTH(p_bio) > 500 THEN
RAISE EXCEPTION 'Bio must be 500 characters or less';
END IF;
-- Validate personal_location (max 100 chars, no special chars like <>)
IF p_personal_location IS NOT NULL THEN
IF LENGTH(p_personal_location) > 100 THEN
RAISE EXCEPTION 'Personal location must be 100 characters or less';
END IF;
IF p_personal_location ~ '[<>]' THEN
RAISE EXCEPTION 'Personal location contains invalid characters';
END IF;
END IF;
-- Track changes for audit log
IF v_old_profile.username IS DISTINCT FROM p_username THEN
v_changes := v_changes || jsonb_build_array(
jsonb_build_object('field', 'username', 'old', v_old_profile.username, 'new', p_username)
);
END IF;
IF v_old_profile.display_name IS DISTINCT FROM p_display_name THEN
v_changes := v_changes || jsonb_build_array(
jsonb_build_object('field', 'display_name', 'old', v_old_profile.display_name, 'new', p_display_name)
);
END IF;
IF v_old_profile.bio IS DISTINCT FROM p_bio THEN
v_changes := v_changes || jsonb_build_array(
jsonb_build_object('field', 'bio', 'old', v_old_profile.bio, 'new', p_bio)
);
END IF;
-- Update profile (updated_at is handled by trigger)
UPDATE public.profiles
SET
username = COALESCE(p_username, username),
display_name = p_display_name,
bio = p_bio,
preferred_pronouns = p_preferred_pronouns,
show_pronouns = p_show_pronouns,
preferred_language = COALESCE(p_preferred_language, preferred_language),
personal_location = p_personal_location,
avatar_url = COALESCE(p_avatar_url, avatar_url),
avatar_image_id = COALESCE(p_avatar_image_id, avatar_image_id)
WHERE user_id = v_user_id;
-- Log significant changes
IF jsonb_array_length(v_changes) > 0 THEN
INSERT INTO public.admin_audit_log (admin_user_id, target_user_id, action, details)
VALUES (v_user_id, v_user_id, 'profile_updated', jsonb_build_object('changes', v_changes));
END IF;
RETURN jsonb_build_object(
'success', true,
'username_changed', v_old_profile.username IS DISTINCT FROM p_username,
'changes_count', jsonb_array_length(v_changes)
);
END;
$$;
GRANT EXECUTE ON FUNCTION public.update_profile TO authenticated;