Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
571 changes: 571 additions & 0 deletions app/api/sign/route.improved.ts

Large diffs are not rendered by default.

263 changes: 111 additions & 152 deletions app/api/upload/route.ts

Large diffs are not rendered by default.

100 changes: 100 additions & 0 deletions lib/auth/apiAuth.ts
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');
}

Comment on lines +8 to +11
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supabaseUrl/supabaseAnonKey são lidos e console.error é executado no import do módulo. Isso cria efeito colateral durante build/SSR e foge do logger controlado. Sugestão: remover o console.error do topo e tratar a ausência de env apenas dentro de requireAuth/optionalAuth usando o logger (ou lançar erro) para manter logs consistentes e evitar side effects.

Suggested change
if (!supabaseUrl || !supabaseAnonKey) {
console.error('[Auth] Missing Supabase credentials');
}

Copilot uses AI. Check for mistakes.
export interface AuthResult {
user: any | null;
error: NextResponse | null;
}
Comment on lines +12 to +15
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthResult.user está tipado como any. Como esse valor é usado para autorização (user.id), vale tipar como o tipo de usuário do Supabase (ex.: User de @supabase/supabase-js) ou pelo menos unknown + narrowing. Isso reduz risco de erros em runtime e melhora autocomplete/segurança de tipos.

Copilot uses AI. Check for mistakes.

/**
* 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 };
}
}
88 changes: 88 additions & 0 deletions lib/logger.ts
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
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatMessage usa JSON.stringify(context) diretamente. Se context (ou error passado via logger.error) contiver referências circulares ou valores não serializáveis, JSON.stringify lança exceção e pode quebrar a rota durante o log. Sugestão: usar stringify seguro (try/catch) com fallback (ex.: remover campos problemáticos) para garantir que logging nunca cause falha de request.

Suggested change
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)}` : '';

Copilot uses AI. Check for mistakes.
return `[${timestamp}] [${level.toUpperCase()}] ${message}${contextStr}`;
}

export const logger = {
/**
* Info messages - only in development
*/
info: (message: string, context?: LogContext) => {
if (isDevelopment) {
console.log(formatMessage('info', message, context));
}
},

/**
* Warning messages - only in development
*/
warn: (message: string, context?: LogContext) => {
if (isDevelopment) {
console.warn(formatMessage('warn', message, context));
}
},

/**
* Error messages - always logged
* In production, these should be sent to error tracking service (Sentry)
*/
error: (message: string, error?: Error | unknown, context?: LogContext) => {
const errorContext = {
...context,
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
name: error.name,
} : error,
};

console.error(formatMessage('error', message, errorContext));

// TODO: Send to Sentry in production
// if (!isDevelopment && typeof window !== 'undefined' && window.Sentry) {
// window.Sentry.captureException(error, { extra: context });
// }
},

/**
* Debug messages - only in development
*/
debug: (message: string, context?: LogContext) => {
if (isDevelopment) {
console.debug(formatMessage('debug', message, context));
}
},
};

/**
* API route logger with request context
*/
export function createApiLogger(route: string) {
return {
info: (message: string, context?: LogContext) =>
logger.info(`[${route}] ${message}`, context),
warn: (message: string, context?: LogContext) =>
logger.warn(`[${route}] ${message}`, context),
error: (message: string, error?: Error | unknown, context?: LogContext) =>
logger.error(`[${route}] ${message}`, error, context),
debug: (message: string, context?: LogContext) =>
logger.debug(`[${route}] ${message}`, context),
};
}
141 changes: 141 additions & 0 deletions lib/rateLimit.ts
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
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este setInterval é criado no escopo de módulo. Em ambientes serverless isso pode manter o event loop ativo e impedir a finalização da execução (ou gerar consumo desnecessário). Considere evitar timer global ou chamar .unref() no timer (Node) e/ou fazer cleanup sob demanda (ex.: limpar entradas expiradas dentro de checkRateLimit).

Copilot uses AI. Check for mistakes.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid user-agent in rate-limit key to prevent bypass

The rate-limit key combines IP and the User-Agent header, so clients can bypass throttling by rotating user-agents (a trivial change in most scripts). This makes the new rate limiting ineffective against abuse scenarios it’s meant to stop. Consider keying on IP alone or, when available, authenticated user ID instead.

Useful? React with 👍 / 👎.

Comment on lines +118 to +122
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A chave padrão inclui user-agent (ip:userAgent). Isso reduz a efetividade do rate limit porque um cliente pode burlar facilmente alterando o User-Agent, além de aumentar cardinalidade na Map. Sugestão: usar só IP (ou, quando autenticado, preferir user.id) e deixar keyGenerator para casos específicos.

Suggested change
// 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;

Copilot uses AI. Check for mistakes.
}

/**
* Helper to add rate limit headers to response
*/
export function addRateLimitHeaders(
response: NextResponse,
result: RateLimitResult
): NextResponse {
response.headers.set('X-RateLimit-Limit', result.limit.toString());
response.headers.set('X-RateLimit-Remaining', result.remaining.toString());
response.headers.set('X-RateLimit-Reset', new Date(result.resetAt).toISOString());

if (result.retryAfter) {
response.headers.set('Retry-After', result.retryAfter.toString());
}

return response;
}
Loading
Loading