GitHub

Security

Comprehensive security measures implemented in Mail Assist to protect user data, prevent unauthorized access, and ensure secure email operations.

Security First
Mail Assist implements multiple layers of security to protect your data and ensure safe email operations. Understanding these measures is crucial for maintaining security.

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
lib/auth-security.ts
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()
lib/auth-security.ts
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()
lib/auth-security.ts
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
api/secure-endpoint.ts
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'])
})
api/secure-endpoint.ts
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 }
      )
    }
api/secure-endpoint.ts
// 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 }
      )
    }
api/secure-endpoint.ts
// 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 }
      )
    }
api/secure-endpoint.ts
// 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.

lib/sanitization.ts
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
  })
}
lib/sanitization.ts
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

database-security.sql
-- Enable RLS on all tables
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_mails ENABLE ROW LEVEL SECURITY;
database-security.sql
-- 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);
database-security.sql
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);
database-security.sql
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-functions.sql
-- 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();
secure-functions.sql
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;
secure-functions.sql
-- 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;
secure-functions.sql
-- 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

Critical Security
Never expose API keys in client-side code. All sensitive operations must be performed server-side with proper environment variable protection.
lib/secure-config.ts
// 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
  }
}
lib/secure-config.ts
// 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(', ')}`)
  }
}
lib/secure-config.ts
// 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.

lib/rate-limit.ts
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,
})
lib/rate-limit.ts
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 }
  }
}
lib/rate-limit.ts
// 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.

middleware.ts
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('; ')
middleware.ts
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
}
middleware.ts
export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}