Email Verification System/Phase 4 of 530-45 minutes

API Routes

Phase 4: API Routes

Overview

Create the API endpoints for sending verification emails and validating tokens. Includes rate limiting and security considerations.

Requirements

Functional:

  • β€’POST /api/auth/send-verification - Send/resend verification email
  • β€’POST /api/auth/verify-email - Validate token and verify user
  • β€’GET /api/auth/verification-status - Check if user is verified

Non-functional:

  • β€’Rate limit resend endpoint
  • β€’No information leakage
  • β€’Proper error codes

Implementation

Send Verification Endpoint

// src/app/api/auth/send-verification/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { verificationService } from '@/lib/verification'
import { sendVerificationEmail } from '@/lib/email'
import { getServerSession } from '@/lib/auth'
import { rateLimit } from '@/lib/rate-limit'

const limiter = rateLimit({
  interval: 5 * 60 * 1000, // 5 minutes
  uniqueTokenPerInterval: 500,
})

export async function POST(req: NextRequest) {
  try {
    // Rate limit by IP
    const ip = req.headers.get('x-forwarded-for') ?? 'anonymous'
    const { success: rateLimitOk } = await limiter.check(5, ip)
    
    if (!rateLimitOk) {
      return NextResponse.json(
        { error: 'Too many requests. Please try again later.' },
        { status: 429 }
      )
    }

    // Get authenticated user
    const session = await getServerSession()
    if (!session?.user) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      )
    }

    const { id: userId, email, name } = session.user

    // Check if already verified
    const isVerified = await verificationService.isUserVerified(userId)
    if (isVerified) {
      return NextResponse.json(
        { error: 'Email already verified' },
        { status: 400 }
      )
    }

    // Create token and send email
    const token = await verificationService.createToken({ userId })
    const { success, error } = await sendVerificationEmail(email, token, name)

    if (!success) {
      console.error('Failed to send verification email:', error)
      return NextResponse.json(
        { error: 'Failed to send verification email' },
        { status: 500 }
      )
    }

    return NextResponse.json({ 
      success: true,
      message: 'Verification email sent'
    })

  } catch (error) {
    console.error('Send verification error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

Verify Email Endpoint

// src/app/api/auth/verify-email/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { verificationService } from '@/lib/verification'
import { z } from 'zod'

const verifySchema = z.object({
  token: z.string().length(64, 'Invalid token format'),
})

export async function POST(req: NextRequest) {
  try {
    const body = await req.json()
    
    // Validate input
    const parsed = verifySchema.safeParse(body)
    if (!parsed.success) {
      return NextResponse.json(
        { error: 'Invalid token format' },
        { status: 400 }
      )
    }

    const { token } = parsed.data

    // Verify the token
    const result = await verificationService.verifyEmail(token)

    if (!result.success) {
      return NextResponse.json(
        { error: result.error },
        { status: 400 }
      )
    }

    return NextResponse.json({
      success: true,
      message: 'Email verified successfully'
    })

  } catch (error) {
    console.error('Verify email error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

Verification Status Endpoint

// src/app/api/auth/verification-status/route.ts
import { NextResponse } from 'next/server'
import { getServerSession } from '@/lib/auth'
import { verificationService } from '@/lib/verification'

export async function GET() {
  try {
    const session = await getServerSession()
    if (!session?.user) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      )
    }

    const isVerified = await verificationService.isUserVerified(session.user.id)

    return NextResponse.json({
      verified: isVerified
    })

  } catch (error) {
    console.error('Verification status error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

Rate Limiting Utility

// src/lib/rate-limit.ts
import { LRUCache } from 'lru-cache'

interface RateLimitOptions {
  interval: number
  uniqueTokenPerInterval: number
}

interface RateLimitResult {
  success: boolean
  remaining: number
  reset: number
}

export function rateLimit(options: RateLimitOptions) {
  const cache = new LRUCache<string, number[]>({
    max: options.uniqueTokenPerInterval,
    ttl: options.interval,
  })

  return {
    async check(limit: number, token: string): Promise<RateLimitResult> {
      const now = Date.now()
      const windowStart = now - options.interval
      
      const timestamps = cache.get(token) || []
      const validTimestamps = timestamps.filter(t => t > windowStart)
      
      if (validTimestamps.length >= limit) {
        return {
          success: false,
          remaining: 0,
          reset: validTimestamps[0] + options.interval,
        }
      }

      validTimestamps.push(now)
      cache.set(token, validTimestamps)

      return {
        success: true,
        remaining: limit - validTimestamps.length,
        reset: now + options.interval,
      }
    },
  }
}

Alternative: Express/Node.js Routes

// routes/auth.ts
import express from 'express'
import { verificationService } from '@/lib/verification'
import { sendVerificationEmail } from '@/lib/email'
import { authMiddleware } from '@/middleware/auth'
import { rateLimitMiddleware } from '@/middleware/rate-limit'

const router = express.Router()

// Send verification email
router.post('/send-verification', 
  authMiddleware,
  rateLimitMiddleware({ windowMs: 5 * 60 * 1000, max: 5 }),
  async (req, res) => {
    try {
      const { id: userId, email, name } = req.user

      const isVerified = await verificationService.isUserVerified(userId)
      if (isVerified) {
        return res.status(400).json({ error: 'Email already verified' })
      }

      const token = await verificationService.createToken({ userId })
      const { success } = await sendVerificationEmail(email, token, name)

      if (!success) {
        return res.status(500).json({ error: 'Failed to send email' })
      }

      res.json({ success: true })
    } catch (error) {
      console.error('Send verification error:', error)
      res.status(500).json({ error: 'Internal server error' })
    }
  }
)

// Verify email
router.post('/verify-email', async (req, res) => {
  try {
    const { token } = req.body

    if (!token || token.length !== 64) {
      return res.status(400).json({ error: 'Invalid token' })
    }

    const result = await verificationService.verifyEmail(token)

    if (!result.success) {
      return res.status(400).json({ error: result.error })
    }

    res.json({ success: true })
  } catch (error) {
    console.error('Verify email error:', error)
    res.status(500).json({ error: 'Internal server error' })
  }
})

export default router

Error Response Standards

// Standard error responses
const ERROR_RESPONSES = {
  INVALID_TOKEN: { status: 400, error: 'Invalid verification link' },
  EXPIRED_TOKEN: { status: 400, error: 'Verification link has expired' },
  ALREADY_VERIFIED: { status: 400, error: 'Email already verified' },
  RATE_LIMITED: { status: 429, error: 'Too many requests. Please try again later.' },
  UNAUTHORIZED: { status: 401, error: 'Unauthorized' },
  INTERNAL_ERROR: { status: 500, error: 'Something went wrong' },
}

Success Criteria

  • β€’ Send verification endpoint requires authentication
  • β€’ Rate limiting prevents abuse (5 requests/5 minutes)
  • β€’ Invalid tokens return generic error (no info leakage)
  • β€’ Expired tokens return appropriate error
  • β€’ Already verified users get informative error
  • β€’ Successful verification returns 200

Related Files

  • β€’Create: src/app/api/auth/send-verification/route.ts
  • β€’Create: src/app/api/auth/verify-email/route.ts
  • β€’Create: src/app/api/auth/verification-status/route.ts
  • β€’Create: src/lib/rate-limit.ts