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.

481 lines (423 loc) 14.4 kB
/** * DNSweeper マルチアカウント認証サービス * * JWT認証、セッション管理、権限チェック機能を提供 */ import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; import { EventEmitter } from 'events'; import type { User, Account, Session, AccountMember, Permission, MemberRole, LoginRequest, LoginResponse, RegisterRequest, SwitchAccountRequest, SwitchAccountResponse, AuthError, AuthErrorCode, AuditLog, ApiKey } from '../types/auth'; export class AuthService extends EventEmitter { private users: Map<string, User> = new Map(); private accounts: Map<string, Account> = new Map(); private sessions: Map<string, Session> = new Map(); private accountMembers: Map<string, AccountMember[]> = new Map(); private auditLogs: AuditLog[] = []; private apiKeys: Map<string, ApiKey> = new Map(); private readonly JWT_SECRET = process.env.JWT_SECRET || 'dnsweeper-dev-secret-2025'; private readonly JWT_EXPIRES_IN = '24h'; private readonly REFRESH_TOKEN_EXPIRES_IN = '7d'; private readonly SALT_ROUNDS = 12; constructor() { super(); this.initializeDefaultData(); } /** * デモ用初期データの作成 */ private initializeDefaultData() { // デフォルトアカウント作成 const defaultAccount: Account = { id: 'account_demo', name: 'Demo Company', slug: 'demo-company', description: 'DNSweeper デモンストレーションアカウント', plan: 'professional', status: 'active', createdAt: new Date(), updatedAt: new Date(), billingEmail: 'billing@demo-company.example', settings: { defaultTtl: 300, allowedRecordTypes: [ { type: 'A', enabled: true }, { type: 'AAAA', enabled: true }, { type: 'CNAME', enabled: true }, { type: 'MX', enabled: true }, { type: 'TXT', enabled: true } ], maxZones: 100, enforceSSO: false, ipWhitelist: [], apiAccessEnabled: true, integrations: {} }, quotas: { maxDnsRecords: 10000, maxApiRequestsPerHour: 5000, maxFileUploads: 50, maxChangeHistoryRetentionDays: 90, maxUsers: 20 }, usage: { currentDnsRecords: 0, apiRequestsLastHour: 0, storageUsedBytes: 0, lastCalculatedAt: new Date() } }; // デフォルトユーザー作成 const defaultUser: User = { id: 'user_demo', email: 'admin@demo-company.example', firstName: 'Demo', lastName: 'Administrator', isEmailVerified: true, createdAt: new Date(), updatedAt: new Date(), isActive: true, preferences: { language: 'ja', timezone: 'Asia/Tokyo', theme: 'light', emailNotifications: true, dashboardLayout: 'expanded' } }; this.accounts.set(defaultAccount.id, defaultAccount); this.users.set(defaultUser.id, defaultUser); // デフォルトメンバーシップ作成 const defaultMembership: AccountMember = { id: 'member_demo', accountId: defaultAccount.id, userId: defaultUser.id, role: 'owner', permissions: this.getDefaultPermissionsForRole('owner'), invitedAt: new Date(), joinedAt: new Date(), invitedBy: defaultUser.id, status: 'active' }; this.accountMembers.set(defaultAccount.id, [defaultMembership]); } /** * ユーザーログイン */ async login(request: LoginRequest, ipAddress?: string, userAgent?: string): Promise<LoginResponse> { const user = this.findUserByEmail(request.email); if (!user || !user.isActive) { throw this.createAuthError('INVALID_CREDENTIALS', 'Invalid email or password'); } // パスワード検証(実装では実際のハッシュ検証を行う) // const isValidPassword = await bcrypt.compare(request.password, user.passwordHash); const isValidPassword = true; // デモ用 if (!isValidPassword) { throw this.createAuthError('INVALID_CREDENTIALS', 'Invalid email or password'); } // アカウント確認 let account: Account; if (request.accountSlug) { account = this.findAccountBySlug(request.accountSlug); if (!account) { throw this.createAuthError('ACCOUNT_NOT_FOUND', 'Account not found'); } } else { // ユーザーの最初のアカウントを使用 const userAccounts = this.getUserAccounts(user.id); if (userAccounts.length === 0) { throw this.createAuthError('ACCOUNT_NOT_FOUND', 'No accessible accounts found'); } account = userAccounts[0]; } if (account.status !== 'active') { throw this.createAuthError('ACCOUNT_SUSPENDED', 'Account is not active'); } // メンバーシップ確認 const membership = this.getAccountMembership(account.id, user.id); if (!membership || membership.status !== 'active') { throw this.createAuthError('INSUFFICIENT_PERMISSIONS', 'No access to this account'); } // セッション作成 const session = await this.createSession(user.id, account.id, ipAddress, userAgent); // 最終ログイン時刻更新 user.lastLoginAt = new Date(); this.users.set(user.id, user); // 監査ログ記録 this.logAuditEvent(account.id, user.id, 'user_login', 'session', session.id, { accountSlug: account.slug, ipAddress, userAgent }, ipAddress, userAgent); this.emit('userLoggedIn', { user, account, session }); return { user, account, session: { token: session.token, refreshToken: session.refreshToken, expiresAt: session.expiresAt }, permissions: membership.permissions }; } /** * セッション作成 */ private async createSession(userId: string, accountId: string, ipAddress?: string, userAgent?: string): Promise<Session> { const sessionId = this.generateId('session'); const token = this.generateJwtToken(userId, accountId, sessionId); const refreshToken = this.generateRefreshToken(); const session: Session = { id: sessionId, userId, accountId, token, refreshToken, expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24時間 refreshExpiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7日 ipAddress, userAgent, createdAt: new Date(), lastActiveAt: new Date() }; this.sessions.set(sessionId, session); return session; } /** * JWT トークン生成 */ private generateJwtToken(userId: string, accountId: string, sessionId: string): string { return jwt.sign( { userId, accountId, sessionId, type: 'access' }, this.JWT_SECRET, { expiresIn: this.JWT_EXPIRES_IN } ); } /** * リフレッシュトークン生成 */ private generateRefreshToken(): string { return crypto.randomBytes(64).toString('hex'); } /** * トークン検証 */ async verifyToken(token: string): Promise<{ userId: string; accountId: string; sessionId: string }> { try { const decoded = jwt.verify(token, this.JWT_SECRET) as any; if (decoded.type !== 'access') { throw this.createAuthError('SESSION_EXPIRED', 'Invalid token type'); } const session = this.sessions.get(decoded.sessionId); if (!session || session.expiresAt < new Date()) { throw this.createAuthError('SESSION_EXPIRED', 'Session expired'); } // セッションの最終アクティブ時刻更新 session.lastActiveAt = new Date(); this.sessions.set(session.id, session); return { userId: decoded.userId, accountId: decoded.accountId, sessionId: decoded.sessionId }; } catch (error) { if (error instanceof jwt.JsonWebTokenError) { throw this.createAuthError('SESSION_EXPIRED', 'Invalid token'); } throw error; } } /** * アカウント切り替え */ async switchAccount(userId: string, request: SwitchAccountRequest): Promise<SwitchAccountResponse> { const user = this.users.get(userId); if (!user) { throw this.createAuthError('USER_NOT_FOUND', 'User not found'); } const account = this.accounts.get(request.accountId); if (!account) { throw this.createAuthError('ACCOUNT_NOT_FOUND', 'Account not found'); } const membership = this.getAccountMembership(account.id, userId); if (!membership || membership.status !== 'active') { throw this.createAuthError('INSUFFICIENT_PERMISSIONS', 'No access to this account'); } // 新しいセッショントークン生成 const sessionId = this.generateId('session'); const token = this.generateJwtToken(userId, account.id, sessionId); this.emit('accountSwitched', { user, account }); return { account, permissions: membership.permissions, session: { token, expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) } }; } /** * 権限チェック */ checkPermission( userId: string, accountId: string, resource: string, action: string ): boolean { const membership = this.getAccountMembership(accountId, userId); if (!membership || membership.status !== 'active') { return false; } // オーナーはすべての権限を持つ if (membership.role === 'owner') { return true; } // 権限チェック return membership.permissions.some(permission => permission.resource === resource && permission.actions.includes(action as any) ); } /** * ユーザーのアカウント一覧取得 */ getUserAccounts(userId: string): Account[] { const accounts: Account[] = []; for (const [accountId, members] of this.accountMembers.entries()) { const membership = members.find(m => m.userId === userId && m.status === 'active'); if (membership) { const account = this.accounts.get(accountId); if (account) { accounts.push(account); } } } return accounts; } /** * ロール別デフォルト権限取得 */ private getDefaultPermissionsForRole(role: MemberRole): Permission[] { const basePermissions = { owner: [ { resource: 'dns_records', actions: ['read', 'create', 'update', 'delete'] }, { resource: 'account_settings', actions: ['read', 'update', 'manage'] }, { resource: 'members', actions: ['read', 'create', 'update', 'delete', 'manage'] }, { resource: 'billing', actions: ['read', 'manage'] }, { resource: 'integrations', actions: ['read', 'create', 'update', 'delete', 'manage'] }, { resource: 'exports', actions: ['read', 'create'] }, { resource: 'history', actions: ['read'] } ], admin: [ { resource: 'dns_records', actions: ['read', 'create', 'update', 'delete'] }, { resource: 'account_settings', actions: ['read', 'update'] }, { resource: 'members', actions: ['read', 'create', 'update'] }, { resource: 'integrations', actions: ['read', 'create', 'update', 'delete'] }, { resource: 'exports', actions: ['read', 'create'] }, { resource: 'history', actions: ['read'] } ], editor: [ { resource: 'dns_records', actions: ['read', 'create', 'update', 'delete'] }, { resource: 'exports', actions: ['read', 'create'] }, { resource: 'history', actions: ['read'] } ], viewer: [ { resource: 'dns_records', actions: ['read'] }, { resource: 'exports', actions: ['read'] }, { resource: 'history', actions: ['read'] } ] }; return basePermissions[role] as Permission[]; } /** * 監査ログ記録 */ private logAuditEvent( accountId: string, userId: string, action: string, resource: string, resourceId: string | undefined, details: Record<string, any>, ipAddress?: string, userAgent?: string ) { const auditLog: AuditLog = { id: this.generateId('audit'), accountId, userId, action: action as any, resource, resourceId, details, ipAddress, userAgent, timestamp: new Date() }; this.auditLogs.push(auditLog); // 古いログの削除(メモリ使用量制限) if (this.auditLogs.length > 10000) { this.auditLogs = this.auditLogs.slice(-5000); } } // ヘルパーメソッド private findUserByEmail(email: string): User | undefined { return Array.from(this.users.values()).find(user => user.email === email); } private findAccountBySlug(slug: string): Account | undefined { return Array.from(this.accounts.values()).find(account => account.slug === slug); } private getAccountMembership(accountId: string, userId: string): AccountMember | undefined { const members = this.accountMembers.get(accountId) || []; return members.find(member => member.userId === userId); } private generateId(prefix: string): string { return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private createAuthError(code: AuthErrorCode, message: string): AuthError { const error = new Error(message) as AuthError; error.code = code; return error; } // 公開メソッド getUsers(): User[] { return Array.from(this.users.values()); } getAccounts(): Account[] { return Array.from(this.accounts.values()); } getAuditLogs(accountId: string, limit = 100): AuditLog[] { return this.auditLogs .filter(log => log.accountId === accountId) .slice(-limit) .reverse(); } /** * セッションクリーンアップ(期限切れセッション削除) */ cleanupExpiredSessions(): void { const now = new Date(); for (const [sessionId, session] of this.sessions.entries()) { if (session.expiresAt < now) { this.sessions.delete(sessionId); } } } } // グローバルインスタンス export const authService = new AuthService(); // 定期的なセッションクリーンアップ setInterval(() => { authService.cleanupExpiredSessions(); }, 60 * 60 * 1000); // 1時間ごと