-
Notifications
You must be signed in to change notification settings - Fork 0
🔒 Security Enhancements & Critical Fixes #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7519114
cec9ab9
b4d2ff4
2aadd3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // lib/auth/apiAuth.ts | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { createClient } from '@supabase/supabase-js'; | ||
|
|
||
| const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; | ||
| const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; | ||
|
|
||
| if (!supabaseUrl || !supabaseAnonKey) { | ||
| console.error('[Auth] Missing Supabase credentials'); | ||
| } | ||
|
|
||
| export interface AuthResult { | ||
| user: any | null; | ||
| error: NextResponse | null; | ||
| } | ||
|
Comment on lines
+12
to
+15
|
||
|
|
||
| /** | ||
| * Require authentication for API routes | ||
| * Returns user object if authenticated, or error response | ||
| * | ||
| * @example | ||
| * export async function POST(req: NextRequest) { | ||
| * const auth = await requireAuth(req); | ||
| * if (auth.error) return auth.error; | ||
| * const user = auth.user; | ||
| * // ... rest of your code | ||
| * } | ||
| */ | ||
| export async function requireAuth(req: NextRequest): Promise<AuthResult> { | ||
| // Check for authorization header | ||
| const authHeader = req.headers.get('authorization'); | ||
|
|
||
| if (!authHeader?.startsWith('Bearer ')) { | ||
| return { | ||
| user: null, | ||
| error: NextResponse.json( | ||
| { error: 'Não autorizado. Token de autenticação necessário.' }, | ||
| { status: 401 } | ||
| ), | ||
| }; | ||
| } | ||
|
|
||
| const token = authHeader.substring(7); | ||
|
|
||
| if (!supabaseUrl || !supabaseAnonKey) { | ||
| return { | ||
| user: null, | ||
| error: NextResponse.json( | ||
| { error: 'Serviço de autenticação indisponível.' }, | ||
| { status: 503 } | ||
| ), | ||
| }; | ||
| } | ||
|
|
||
| try { | ||
| const supabase = createClient(supabaseUrl, supabaseAnonKey); | ||
| const { data: { user }, error } = await supabase.auth.getUser(token); | ||
|
|
||
| if (error || !user) { | ||
| return { | ||
| user: null, | ||
| error: NextResponse.json( | ||
| { error: 'Token inválido ou expirado.' }, | ||
| { status: 401 } | ||
| ), | ||
| }; | ||
| } | ||
|
|
||
| return { user, error: null }; | ||
| } catch (e) { | ||
| return { | ||
| user: null, | ||
| error: NextResponse.json( | ||
| { error: 'Erro ao validar autenticação.' }, | ||
| { status: 500 } | ||
| ), | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Optional authentication - doesn't block if no auth provided | ||
| * Useful for endpoints that work differently for authenticated users | ||
| */ | ||
| export async function optionalAuth(req: NextRequest): Promise<{ user: any | null }> { | ||
| const authHeader = req.headers.get('authorization'); | ||
|
|
||
| if (!authHeader?.startsWith('Bearer ') || !supabaseUrl || !supabaseAnonKey) { | ||
| return { user: null }; | ||
| } | ||
|
|
||
| try { | ||
| const token = authHeader.substring(7); | ||
| const supabase = createClient(supabaseUrl, supabaseAnonKey); | ||
| const { data: { user } } = await supabase.auth.getUser(token); | ||
| return { user: user || null }; | ||
| } catch { | ||
| return { user: null }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // lib/logger.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Controlled logging system | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Development: logs to console | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Production: only errors to console (can be extended to Sentry) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type LogLevel = 'info' | 'warn' | 'error' | 'debug'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface LogContext { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [key: string]: any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isDevelopment = process.env.NODE_ENV === 'development'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function formatMessage(level: LogLevel, message: string, context?: LogContext): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const timestamp = new Date().toISOString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const contextStr = context ? ` ${JSON.stringify(context)}` : ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+19
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function formatMessage(level: LogLevel, message: string, context?: LogContext): string { | |
| const timestamp = new Date().toISOString(); | |
| const contextStr = context ? ` ${JSON.stringify(context)}` : ''; | |
| function safeStringify(value: unknown): string { | |
| try { | |
| const seen = new WeakSet<object>(); | |
| return JSON.stringify( | |
| value, | |
| (_key, val) => { | |
| if (typeof val === 'object' && val !== null) { | |
| // Handle circular references | |
| if (seen.has(val as object)) { | |
| return '[Circular]'; | |
| } | |
| seen.add(val as object); | |
| } | |
| // Handle BigInt and other non-JSON-native types | |
| if (typeof val === 'bigint') { | |
| return val.toString(); | |
| } | |
| return val; | |
| } | |
| ); | |
| } catch { | |
| try { | |
| return String(value); | |
| } catch { | |
| return '[Unserializable]'; | |
| } | |
| } | |
| } | |
| function formatMessage(level: LogLevel, message: string, context?: LogContext): string { | |
| const timestamp = new Date().toISOString(); | |
| const contextStr = context ? ` ${safeStringify(context)}` : ''; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,141 @@ | ||||||||||||||||||
| // lib/rateLimit.ts | ||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||||||||||||||
|
|
||||||||||||||||||
| interface RateLimitRecord { | ||||||||||||||||||
| count: number; | ||||||||||||||||||
| resetAt: number; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const rateLimitStore = new Map<string, RateLimitRecord>(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Cleanup old entries every 5 minutes | ||||||||||||||||||
| setInterval(() => { | ||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||
| for (const [key, record] of rateLimitStore.entries()) { | ||||||||||||||||||
| if (now > record.resetAt) { | ||||||||||||||||||
| rateLimitStore.delete(key); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| }, 5 * 60 * 1000); | ||||||||||||||||||
|
Comment on lines
+11
to
+19
|
||||||||||||||||||
|
|
||||||||||||||||||
| export interface RateLimitConfig { | ||||||||||||||||||
| limit?: number; // Max requests per window (default: 10) | ||||||||||||||||||
| windowMs?: number; // Time window in milliseconds (default: 60000 = 1 minute) | ||||||||||||||||||
| keyGenerator?: (req: NextRequest) => string; // Custom key generator | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| export interface RateLimitResult { | ||||||||||||||||||
| allowed: boolean; | ||||||||||||||||||
| limit: number; | ||||||||||||||||||
| remaining: number; | ||||||||||||||||||
| resetAt: number; | ||||||||||||||||||
| retryAfter?: number; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Rate limiting middleware for API routes | ||||||||||||||||||
| * | ||||||||||||||||||
| * @example | ||||||||||||||||||
| * export async function POST(req: NextRequest) { | ||||||||||||||||||
| * const rateLimit = checkRateLimit(req, { limit: 5, windowMs: 60000 }); | ||||||||||||||||||
| * if (!rateLimit.allowed) { | ||||||||||||||||||
| * return NextResponse.json( | ||||||||||||||||||
| * { error: 'Muitas requisições. Tente novamente em breve.' }, | ||||||||||||||||||
| * { | ||||||||||||||||||
| * status: 429, | ||||||||||||||||||
| * headers: { | ||||||||||||||||||
| * 'Retry-After': rateLimit.retryAfter?.toString() || '60', | ||||||||||||||||||
| * 'X-RateLimit-Limit': rateLimit.limit.toString(), | ||||||||||||||||||
| * 'X-RateLimit-Remaining': rateLimit.remaining.toString(), | ||||||||||||||||||
| * 'X-RateLimit-Reset': new Date(rateLimit.resetAt).toISOString(), | ||||||||||||||||||
| * } | ||||||||||||||||||
| * } | ||||||||||||||||||
| * ); | ||||||||||||||||||
| * } | ||||||||||||||||||
| * // ... rest of your code | ||||||||||||||||||
| * } | ||||||||||||||||||
| */ | ||||||||||||||||||
| export function checkRateLimit( | ||||||||||||||||||
| req: NextRequest, | ||||||||||||||||||
| config: RateLimitConfig = {} | ||||||||||||||||||
| ): RateLimitResult { | ||||||||||||||||||
| const limit = config.limit || 10; | ||||||||||||||||||
| const windowMs = config.windowMs || 60000; // 1 minute default | ||||||||||||||||||
|
|
||||||||||||||||||
| // Generate unique key for this client | ||||||||||||||||||
| const key = config.keyGenerator | ||||||||||||||||||
| ? config.keyGenerator(req) | ||||||||||||||||||
| : getClientKey(req); | ||||||||||||||||||
|
|
||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||
| const record = rateLimitStore.get(key); | ||||||||||||||||||
|
|
||||||||||||||||||
| // No record or expired - create new | ||||||||||||||||||
| if (!record || now > record.resetAt) { | ||||||||||||||||||
| const resetAt = now + windowMs; | ||||||||||||||||||
| rateLimitStore.set(key, { count: 1, resetAt }); | ||||||||||||||||||
|
|
||||||||||||||||||
| return { | ||||||||||||||||||
| allowed: true, | ||||||||||||||||||
| limit, | ||||||||||||||||||
| remaining: limit - 1, | ||||||||||||||||||
| resetAt, | ||||||||||||||||||
| }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Limit exceeded | ||||||||||||||||||
| if (record.count >= limit) { | ||||||||||||||||||
| const retryAfter = Math.ceil((record.resetAt - now) / 1000); | ||||||||||||||||||
|
|
||||||||||||||||||
| return { | ||||||||||||||||||
| allowed: false, | ||||||||||||||||||
| limit, | ||||||||||||||||||
| remaining: 0, | ||||||||||||||||||
| resetAt: record.resetAt, | ||||||||||||||||||
| retryAfter, | ||||||||||||||||||
| }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Increment counter | ||||||||||||||||||
| record.count++; | ||||||||||||||||||
|
|
||||||||||||||||||
| return { | ||||||||||||||||||
| allowed: true, | ||||||||||||||||||
| limit, | ||||||||||||||||||
| remaining: limit - record.count, | ||||||||||||||||||
| resetAt: record.resetAt, | ||||||||||||||||||
| }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Generate client identifier from request | ||||||||||||||||||
| */ | ||||||||||||||||||
| function getClientKey(req: NextRequest): string { | ||||||||||||||||||
| // Try to get real IP (considering proxies) | ||||||||||||||||||
| const forwarded = req.headers.get('x-forwarded-for'); | ||||||||||||||||||
| const realIp = req.headers.get('x-real-ip'); | ||||||||||||||||||
| const ip = forwarded?.split(',')[0] || realIp || req.ip || 'unknown'; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Include user agent for additional uniqueness | ||||||||||||||||||
| const userAgent = req.headers.get('user-agent') || 'unknown'; | ||||||||||||||||||
|
|
||||||||||||||||||
| return `${ip}:${userAgent.substring(0, 50)}`; | ||||||||||||||||||
|
Comment on lines
+117
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The rate-limit key combines IP and the Useful? React with 👍 / 👎.
Comment on lines
+118
to
+122
|
||||||||||||||||||
| // Include user agent for additional uniqueness | |
| const userAgent = req.headers.get('user-agent') || 'unknown'; | |
| return `${ip}:${userAgent.substring(0, 50)}`; | |
| // Use only IP by default to avoid easy spoofing and high key cardinality. | |
| return ip; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
supabaseUrl/supabaseAnonKeysão lidos econsole.erroré executado no import do módulo. Isso cria efeito colateral durante build/SSR e foge do logger controlado. Sugestão: remover oconsole.errordo topo e tratar a ausência de env apenas dentro derequireAuth/optionalAuthusando ologger(ou lançar erro) para manter logs consistentes e evitar side effects.