mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-26 19:07:00 -05:00
Fix Supabase linter warnings and backend validation
This commit is contained in:
158
supabase/functions/validate-email/index.ts
Normal file
158
supabase/functions/validate-email/index.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
// Comprehensive list of disposable email domains
|
||||
const DISPOSABLE_DOMAINS = new Set([
|
||||
// Popular disposable email services
|
||||
'tempmail.com', 'temp-mail.org', 'temp-mail.io', 'temp-mail.de',
|
||||
'10minutemail.com', '10minutemail.net', '10minemail.com',
|
||||
'guerrillamail.com', 'guerrillamail.net', 'guerrillamailblock.com',
|
||||
'mailinator.com', 'mailinator.net', 'mailinator2.com',
|
||||
'throwaway.email', 'throwemail.com', 'throwawaymail.com',
|
||||
'yopmail.com', 'yopmail.fr', 'yopmail.net',
|
||||
'maildrop.cc', 'mailnesia.com', 'trashmail.com',
|
||||
'fakeinbox.com', 'fakemail.net', 'fakemailgenerator.com',
|
||||
'getnada.com', 'getairmail.com', 'dispostable.com',
|
||||
'mintemail.com', 'mytemp.email', 'mohmal.com',
|
||||
'sharklasers.com', 'grr.la', 'guerrillamail.biz',
|
||||
'spam4.me', 'tempinbox.com', 'tempr.email',
|
||||
'emailondeck.com', 'mailcatch.com', 'mailexpire.com',
|
||||
'mailforspam.com', 'mailfreeonline.com', 'mailin8r.com',
|
||||
'mailmoat.com', 'mailnull.com', 'mailsac.com',
|
||||
'mailscrap.com', 'mailslite.com', 'mailtemp.info',
|
||||
'mailtothis.com', 'mailzi.ru', 'mytrashmail.com',
|
||||
'no-spam.ws', 'noclickemail.com', 'nodezine.com',
|
||||
'nospam.ze.tc', 'nospamfor.us', 'nowmymail.com',
|
||||
'objectmail.com', 'obobbo.com', 'oneoffemail.com',
|
||||
'onewaymail.com', 'online.ms', 'oopi.org',
|
||||
'ordinaryamerican.net', 'otherinbox.com', 'owlpic.com',
|
||||
'pancakemail.com', 'pjjkp.com', 'plexolan.de',
|
||||
'poofy.org', 'pookmail.com', 'privacy.net',
|
||||
'proxymail.eu', 'prtnx.com', 'putthisinyourspamdatabase.com',
|
||||
'quickinbox.com', 'rcpt.at', 'recode.me',
|
||||
'recursor.net', 'regbypass.com', 'regspaces.tk',
|
||||
'rklips.com', 'rmqkr.net', 'robertspcrepair.com',
|
||||
'royal.net', 'ruffrey.com', 's0ny.net',
|
||||
'safersignup.de', 'safetymail.info', 'safetypost.de',
|
||||
'sandelf.de', 'saynotospams.com', 'schafmail.de',
|
||||
'schrott-email.de', 'secretemail.de', 'secure-mail.biz',
|
||||
'senseless-entertainment.com', 'services391.com', 'sharklasers.com',
|
||||
'shiftmail.com', 'shippingterms.org', 'showslow.de',
|
||||
'sibmail.com', 'sinnlos-mail.de', 'slapsfromlastnight.com',
|
||||
'slaskpost.se', 'smashmail.de', 'smellfear.com',
|
||||
'snakemail.com', 'sneakemail.com', 'sneakmail.de',
|
||||
'snkmail.com', 'sofimail.com', 'solvemail.info',
|
||||
'sogetthis.com', 'soodonims.com', 'spam.la',
|
||||
'spam.su', 'spamail.de', 'spamarrest.com',
|
||||
'spambob.com', 'spambog.com', 'spambog.de',
|
||||
'spambox.us', 'spamcannon.com', 'spamcannon.net',
|
||||
'spamcero.com', 'spamcon.org', 'spamcorptastic.com',
|
||||
'spamcowboy.com', 'spamcowboy.net', 'spamcowboy.org',
|
||||
'spamday.com', 'spamex.com', 'spamfree24.com',
|
||||
'spamfree24.de', 'spamfree24.org', 'spamgourmet.com',
|
||||
'spamherelots.com', 'spamhereplease.com', 'spamhole.com',
|
||||
'spamify.com', 'spaminator.de', 'spamkill.info',
|
||||
]);
|
||||
|
||||
interface ValidateEmailRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ValidationResult {
|
||||
valid: boolean;
|
||||
reason?: string;
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
const handler = async (req: Request): Promise<Response> => {
|
||||
// Handle CORS preflight requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { email }: ValidateEmailRequest = await req.json();
|
||||
|
||||
if (!email || typeof email !== 'string') {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'Email address is required'
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Basic email format validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'Invalid email format'
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Extract domain
|
||||
const domain = email.split('@')[1].toLowerCase();
|
||||
|
||||
// Check if domain is disposable
|
||||
if (DISPOSABLE_DOMAINS.has(domain)) {
|
||||
console.log(`Blocked disposable email domain: ${domain}`);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'Disposable email addresses are not allowed. Please use a permanent email address.',
|
||||
suggestions: [
|
||||
'Use a personal email (Gmail, Outlook, Yahoo, etc.)',
|
||||
'Use your work or school email address',
|
||||
'Use an email from your own domain'
|
||||
]
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Email is valid
|
||||
console.log(`Email validated successfully: ${email}`);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: true
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error in validate-email function:', error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'Internal server error during email validation'
|
||||
} as ValidationResult),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
serve(handler);
|
||||
@@ -0,0 +1,124 @@
|
||||
-- =====================================================
|
||||
-- PRIORITY 1: Fix Function Security (Search Path)
|
||||
-- =====================================================
|
||||
|
||||
-- Fix increment_blog_view_count missing search_path
|
||||
DROP FUNCTION IF EXISTS public.increment_blog_view_count(text);
|
||||
CREATE OR REPLACE FUNCTION public.increment_blog_view_count(post_slug text)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE blog_posts
|
||||
SET view_count = view_count + 1
|
||||
WHERE slug = post_slug;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- =====================================================
|
||||
-- PRIORITY 2: Backend Username Validation
|
||||
-- =====================================================
|
||||
|
||||
-- Add CHECK constraint for username format
|
||||
ALTER TABLE public.profiles
|
||||
DROP CONSTRAINT IF EXISTS username_format_check;
|
||||
|
||||
ALTER TABLE public.profiles
|
||||
ADD CONSTRAINT username_format_check
|
||||
CHECK (
|
||||
username ~ '^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$'
|
||||
AND length(username) >= 3
|
||||
AND length(username) <= 30
|
||||
AND username !~ '[-_]{2,}'
|
||||
);
|
||||
|
||||
-- Create function to check forbidden usernames
|
||||
CREATE OR REPLACE FUNCTION public.check_forbidden_username()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
forbidden_list TEXT[] := ARRAY[
|
||||
'admin', 'administrator', 'moderator', 'mod', 'owner', 'root', 'system', 'support',
|
||||
'staff', 'team', 'official', 'verified', 'bot', 'api', 'service',
|
||||
'thrillwiki', 'lovable', 'supabase', 'cloudflare',
|
||||
'www', 'mail', 'email', 'ftp', 'blog', 'forum', 'shop', 'store', 'app', 'mobile',
|
||||
'help', 'support', 'contact', 'about', 'terms', 'privacy', 'security', 'legal',
|
||||
'login', 'signup', 'register', 'signin', 'signout', 'logout', 'auth', 'oauth',
|
||||
'profile', 'profiles', 'user', 'users', 'account', 'accounts', 'settings',
|
||||
'dashboard', 'console', 'panel', 'manage', 'management',
|
||||
'null', 'undefined', 'true', 'false', 'delete', 'remove', 'test', 'demo',
|
||||
'localhost', 'example', 'temp', 'temporary', 'guest', 'anonymous', 'anon',
|
||||
'fuck', 'shit', 'damn', 'hell', 'ass', 'bitch', 'bastard', 'crap',
|
||||
'nazi', 'hitler', 'stalin', 'terrorist', 'kill', 'death', 'murder',
|
||||
'ceo', 'president', 'manager', 'director', 'executive', 'founder'
|
||||
];
|
||||
BEGIN
|
||||
-- Usernames are already lowercased via Zod transform, but ensure it
|
||||
NEW.username := lower(NEW.username);
|
||||
|
||||
IF NEW.username = ANY(forbidden_list) THEN
|
||||
RAISE EXCEPTION 'Username "%" is not allowed', NEW.username
|
||||
USING ERRCODE = '23514'; -- check_violation
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Create trigger for forbidden username check
|
||||
DROP TRIGGER IF EXISTS enforce_username_rules ON public.profiles;
|
||||
CREATE TRIGGER enforce_username_rules
|
||||
BEFORE INSERT OR UPDATE OF username ON public.profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.check_forbidden_username();
|
||||
|
||||
-- Add index for case-insensitive username lookups (if not exists)
|
||||
CREATE INDEX IF NOT EXISTS profiles_username_lower_idx
|
||||
ON public.profiles (lower(username));
|
||||
|
||||
-- =====================================================
|
||||
-- PRIORITY 2.5: Add Display Name Content Filtering
|
||||
-- =====================================================
|
||||
|
||||
-- Create function to check display name content
|
||||
CREATE OR REPLACE FUNCTION public.check_display_name_content()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
offensive_terms TEXT[] := ARRAY[
|
||||
'nazi', 'hitler', 'terrorist', 'kill', 'murder', 'fuck', 'shit'
|
||||
];
|
||||
term TEXT;
|
||||
BEGIN
|
||||
-- Skip if display_name is null
|
||||
IF NEW.display_name IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Check for offensive terms in display name
|
||||
FOREACH term IN ARRAY offensive_terms
|
||||
LOOP
|
||||
IF lower(NEW.display_name) LIKE '%' || term || '%' THEN
|
||||
RAISE EXCEPTION 'Display name contains inappropriate content'
|
||||
USING ERRCODE = '23514'; -- check_violation
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Create trigger for display name check
|
||||
DROP TRIGGER IF EXISTS enforce_display_name_rules ON public.profiles;
|
||||
CREATE TRIGGER enforce_display_name_rules
|
||||
BEFORE INSERT OR UPDATE OF display_name ON public.profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.check_display_name_content();
|
||||
Reference in New Issue
Block a user