UNPKG

dnsweeper

Version:

Advanced CLI tool for DNS record risk analysis and cleanup. Features CSV import for Cloudflare/Route53, automated risk assessment, and parallel DNS validation.

342 lines (294 loc) 9.01 kB
/** * DNSweeper 認証・認可ミドルウェア * * JWT検証、権限チェック、リクエストコンテキスト設定 */ import { Request, Response, NextFunction } from 'express'; import { authService } from '../services/authService'; import type { User, Account, Permission } from '../types/auth'; // リクエストにユーザー情報を追加 declare global { namespace Express { interface Request { user?: User; account?: Account; permissions?: Permission[]; sessionId?: string; } } } /** * JWT認証ミドルウェア */ export const authenticateToken = async (req: Request, res: Response, next: NextFunction) => { try { const authHeader = req.headers.authorization; const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN" if (!token) { return res.status(401).json({ success: false, error: { code: 'NO_TOKEN', message: 'アクセストークンが必要です' } }); } // トークン検証 const { userId, accountId, sessionId } = await authService.verifyToken(token); // ユーザー・アカウント情報取得 const users = authService.getUsers(); const accounts = authService.getAccounts(); const user = users.find(u => u.id === userId); const account = accounts.find(a => a.id === accountId); if (!user || !account) { return res.status(401).json({ success: false, error: { code: 'INVALID_TOKEN', message: '無効なトークンです' } }); } // 権限チェック const userAccounts = authService.getUserAccounts(userId); const hasAccess = userAccounts.some(acc => acc.id === accountId); if (!hasAccess) { return res.status(403).json({ success: false, error: { code: 'ACCESS_DENIED', message: 'このアカウントへのアクセス権限がありません' } }); } // リクエストオブジェクトに情報を設定 req.user = user; req.account = account; req.sessionId = sessionId; // TODO: 実際の権限取得ロジック req.permissions = []; // authService.getUserPermissions(userId, accountId); next(); } catch (error: any) { if (error.code) { return res.status(401).json({ success: false, error: { code: error.code, message: error.message } }); } console.error('Token verification error:', error); return res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'サーバーエラーが発生しました' } }); } }; /** * 権限チェックミドルウェア */ export const requirePermission = (resource: string, action: string) => { return (req: Request, res: Response, next: NextFunction) => { if (!req.user || !req.account) { return res.status(401).json({ success: false, error: { code: 'NOT_AUTHENTICATED', message: '認証が必要です' } }); } const hasPermission = authService.checkPermission( req.user.id, req.account.id, resource, action ); if (!hasPermission) { return res.status(403).json({ success: false, error: { code: 'INSUFFICIENT_PERMISSIONS', message: 'この操作を実行する権限がありません' } }); } next(); }; }; /** * オプション認証ミドルウェア(認証不要でも動作するエンドポイント用) */ export const optionalAuth = async (req: Request, res: Response, next: NextFunction) => { try { const authHeader = req.headers.authorization; const token = authHeader && authHeader.split(' ')[1]; if (token) { const { userId, accountId, sessionId } = await authService.verifyToken(token); const users = authService.getUsers(); const accounts = authService.getAccounts(); const user = users.find(u => u.id === userId); const account = accounts.find(a => a.id === accountId); if (user && account) { req.user = user; req.account = account; req.sessionId = sessionId; req.permissions = []; // authService.getUserPermissions(userId, accountId); } } next(); } catch (error) { // 認証エラーでも続行(オプション認証のため) next(); } }; /** * アカウント所有者またはAdmin権限チェック */ export const requireAccountAdmin = (req: Request, res: Response, next: NextFunction) => { if (!req.user || !req.account) { return res.status(401).json({ success: false, error: { code: 'NOT_AUTHENTICATED', message: '認証が必要です' } }); } // アカウント管理権限チェック const hasAdminAccess = authService.checkPermission( req.user.id, req.account.id, 'account_settings', 'manage' ) || authService.checkPermission( req.user.id, req.account.id, 'members', 'manage' ); if (!hasAdminAccess) { return res.status(403).json({ success: false, error: { code: 'ADMIN_REQUIRED', message: 'アカウント管理者権限が必要です' } }); } next(); }; /** * APIキー認証ミドルウェア */ export const authenticateApiKey = async (req: Request, res: Response, next: NextFunction) => { try { const apiKey = req.headers['x-api-key'] as string; if (!apiKey) { return res.status(401).json({ success: false, error: { code: 'NO_API_KEY', message: 'APIキーが必要です' } }); } // TODO: APIキー検証実装 // const keyInfo = await authService.verifyApiKey(apiKey); // 一時的なダミー実装 return res.status(501).json({ success: false, error: { code: 'NOT_IMPLEMENTED', message: 'APIキー認証は実装中です' } }); } catch (error: any) { console.error('API key verification error:', error); return res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'サーバーエラーが発生しました' } }); } }; /** * レート制限ミドルウェア */ export const rateLimit = (requestsPerHour: number = 1000) => { const requests = new Map<string, { count: number; resetTime: number }>(); return (req: Request, res: Response, next: NextFunction) => { const clientId = req.account?.id || req.ip; const now = Date.now(); const hourInMs = 60 * 60 * 1000; const resetTime = Math.floor(now / hourInMs) * hourInMs + hourInMs; let clientRequests = requests.get(clientId); if (!clientRequests || clientRequests.resetTime <= now) { clientRequests = { count: 0, resetTime }; requests.set(clientId, clientRequests); } clientRequests.count++; if (clientRequests.count > requestsPerHour) { return res.status(429).json({ success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: 'リクエスト制限を超過しました', resetTime: new Date(resetTime).toISOString() } }); } // レスポンスヘッダーにレート制限情報を追加 res.set({ 'X-RateLimit-Limit': requestsPerHour.toString(), 'X-RateLimit-Remaining': (requestsPerHour - clientRequests.count).toString(), 'X-RateLimit-Reset': resetTime.toString() }); next(); }; }; /** * CORS設定(アカウント別ドメイン制限対応) */ export const configureCors = (req: Request, res: Response, next: NextFunction) => { const allowedOrigins = [ 'http://localhost:3000', 'http://localhost:5173', 'https://dnsweeper.app' ]; // アカウント固有のドメイン追加 if (req.account?.settings) { // TODO: アカウント設定からallowedDomainsを取得 } const origin = req.headers.origin; if (origin && allowedOrigins.includes(origin)) { res.header('Access-Control-Allow-Origin', origin); } res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin,X-Requested-With,Content-Type,Accept,authorization,cache-control,x-api-key'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }; /** * セキュリティヘッダー設定 */ export const securityHeaders = (req: Request, res: Response, next: NextFunction) => { res.set({ 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'Referrer-Policy': 'strict-origin-when-cross-origin' }); next(); };