Security
Comprehensive security measures implemented in Mail Assist to protect user data, prevent unauthorized access, and ensure secure email operations.
Authentication Security
Mail Assist uses Supabase Auth for robust authentication with industry-standard security practices and multiple protection layers.
Authentication Features
- JWT Tokens - Secure session management with automatic refresh
- Email Verification - Mandatory email confirmation for new accounts
- Password Hashing - Bcrypt hashing with salt for password storage
- Session Expiry - Automatic session timeout for inactive users
- CSRF Protection - Built-in cross-site request forgery prevention
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function requireAuth() {
const supabase = createServerComponentClient({ cookies })
const {
data: { session },
error
} = await supabase.auth.getSession()if (error || !session) {
redirect('/login')
}
return session.user
}
export async function validateSession(request: Request) {
const supabase = createServerComponentClient({ cookies })
const {
data: { session },
error
} = await supabase.auth.getSession()if (error || !session) {
return new Response('Unauthorized', { status: 401 })
}
return session
}API Route Security
All API endpoints implement comprehensive security measures to prevent unauthorized access and protect sensitive operations.
Protection Mechanisms
- Authentication Checks - Every API route validates user sessions
- Input Validation - Strict validation of all incoming data
- Rate Limiting - Prevent abuse with request throttling
- CORS Configuration - Controlled cross-origin resource sharing
- Error Handling - Secure error responses without data leakage
import { NextRequest, NextResponse } from 'next/server'
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { z } from 'zod'
import rateLimit from '@/lib/rate-limit'
// Input validation schema
const emailSchema = z.object({
to: z.string().email('Invalid email address'),
subject: z.string().min(1, 'Subject is required').max(200, 'Subject too long'),
html: z.string().min(1, 'Content is required'),
mailType: z.enum(['custom', 'template'])
})export async function POST(request: NextRequest) {
try {
// Rate limiting
const identifier = request.ip ?? 'anonymous'
const { success } = await rateLimit.limit(identifier)
if (!success) {
return NextResponse.json(
{ success: false, error: 'Rate limit exceeded' },
{ status: 429 }
)
}// Authentication check
const supabase = createServerComponentClient({ cookies })
const { data: { session }, error: authError } = await supabase.auth.getSession()
if (authError || !session) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}// Input validation
const body = await request.json()
const validationResult = emailSchema.safeParse(body)
if (!validationResult.success) {
return NextResponse.json(
{
success: false,
error: 'Invalid input',
details: validationResult.error.issues
},
{ status: 400 }
)
}// Sanitize HTML content
const sanitizedHtml = sanitizeHtml(validationResult.data.html)
// Process request with validated and sanitized data
// ... rest of the endpoint logic
} catch (error) {
console.error('API Error:', error)
return NextResponse.json(
{ success: false, error: 'Internal server error' },
{ status: 500 }
)
}
}Input Sanitization
All user inputs are sanitized to prevent XSS attacks and ensure data integrity.
import DOMPurify from 'isomorphic-dompurify'
export function sanitizeHtml(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'a', 'img', 'div', 'span', 'table', 'tr', 'td', 'th'
],
ALLOWED_ATTR: [
'href', 'src', 'alt', 'title', 'style', 'class', 'target'
],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i
})
}export function sanitizeText(text: string): string {
return text
.replace(/[<>]/g, '') // Remove HTML brackets
.replace(/javascript:/gi, '') // Remove javascript: URLs
.trim()
.substring(0, 1000) // Limit length
}
export function validateEmail(email: string): boolean {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/
return emailRegex.test(email) && email.length <= 254
}Database Security
Row Level Security (RLS) and proper access controls ensure users can only access their own data at the database level.
Row Level Security Implementation
-- Enable RLS on all tables
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_mails ENABLE ROW LEVEL SECURITY;-- Profiles table policies
CREATE POLICY "Users can only view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can only update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);CREATE POLICY "Users can only insert own profile"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = id);
-- User mails table policies
CREATE POLICY "Users can only view own emails"
ON user_mails FOR SELECT
USING (auth.uid() = user_id);CREATE POLICY "Users can only insert own emails"
ON user_mails FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Prevent updates and deletes on email history
CREATE POLICY "No updates allowed on user_mails"
ON user_mails FOR UPDATE
USING (false);
CREATE POLICY "No deletes allowed on user_mails"
ON user_mails FOR DELETE
USING (false);Secure Database Functions
-- Secure function for credit deduction with atomic operations
CREATE OR REPLACE FUNCTION secure_send_email(
p_recipient_email TEXT,
p_subject TEXT,
p_content TEXT,
p_mail_type TEXT,
p_credits_needed INTEGER
) RETURNS JSON
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
current_user_id UUID;
current_credits INTEGER;
result JSON;
BEGIN
-- Get current user ID from auth context
current_user_id := auth.uid();IF current_user_id IS NULL THEN
RETURN json_build_object('success', false, 'error', 'Unauthorized');
END IF;
-- Validate input parameters
IF p_recipient_email IS NULL OR p_subject IS NULL OR p_content IS NULL THEN
RETURN json_build_object('success', false, 'error', 'Missing required fields');
END IF;
IF p_mail_type NOT IN ('custom', 'template') THEN
RETURN json_build_object('success', false, 'error', 'Invalid mail type');
END IF;-- Lock user profile and check credits
SELECT credits INTO current_credits
FROM profiles
WHERE id = current_user_id
FOR UPDATE;
IF current_credits < p_credits_needed THEN
RETURN json_build_object('success', false, 'error', 'Insufficient credits');
END IF;-- Deduct credits and log email in single transaction
UPDATE profiles
SET credits = credits - p_credits_needed,
updated_at = NOW()
WHERE id = current_user_id;
INSERT INTO user_mails (
user_id, recipient_email, subject, content, mail_type, credits_used
) VALUES (
current_user_id, p_recipient_email, p_subject, p_content, p_mail_type, p_credits_needed
);
RETURN json_build_object(
'success', true,
'credits_used', p_credits_needed,
'remaining_credits', current_credits - p_credits_needed
);
EXCEPTION WHEN OTHERS THEN
RETURN json_build_object('success', false, 'error', 'Database error');
END;
$$;API Key Protection
Sensitive API keys are protected through environment variables and server-side processing to prevent exposure to client-side code.
Environment Variable Security
// Server-side configuration only
export const serverConfig = {
resend: {
apiKey: process.env.RESEND_API_KEY,
fromEmail: process.env.RESEND_FROM_EMAIL || 'noreply@yourdomain.com'
},
supabase: {
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY // Server-only
},
app: {
url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
environment: process.env.NODE_ENV
}
}// Validation function to ensure all required keys are present
export function validateServerConfig() {
const required = [
'RESEND_API_KEY',
'NEXT_PUBLIC_SUPABASE_URL',
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
'SUPABASE_SERVICE_ROLE_KEY'
]
const missing = required.filter(key => !process.env[key])
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`)
}
}// Client-side safe configuration
export const clientConfig = {
supabase: {
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
},
app: {
url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
}
}Rate Limiting
Implement rate limiting to prevent abuse and ensure fair usage of the email service.
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
// Create rate limiter instance
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(60, '1 m'), // 60 requests per minute
analytics: true,
})export async function checkRateLimit(identifier: string) {
try {
const { success, limit, reset, remaining } = await ratelimit.limit(identifier)
return {
success,
limit,
reset,
remaining,
error: success ? null : 'Rate limit exceeded'
}
} catch (error) {
console.error('Rate limit error:', error)
// Fail open - allow request if rate limiting service is down
return { success: true, limit: 0, reset: 0, remaining: 0, error: null }
}
}// Different rate limits for different operations
export const rateLimits = {
email: new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 emails per minute
}),
auth: new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 m'), // 5 auth attempts per minute
}),
api: new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'), // 100 API calls per minute
})
}Content Security Policy
Implement Content Security Policy (CSP) headers to prevent XSS attacks and control resource loading.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Content Security Policy
const csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://*.supabase.co https://api.resend.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
].join('; ')response.headers.set('Content-Security-Policy', csp)
// Additional security headers
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
return response
}export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}