UNPKG

recoder-code

Version:

🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!

266 lines (210 loc) • 6.74 kB
/** * ApiKey Entity * Manages API keys for authentication and access control */ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, Index } from 'typeorm'; import { User } from './User'; export enum ApiKeyScope { READ = 'read', WRITE = 'write', ADMIN = 'admin', PUBLISH = 'publish', UNPUBLISH = 'unpublish', DEPRECATE = 'deprecate' } export enum ApiKeyStatus { ACTIVE = 'active', REVOKED = 'revoked', EXPIRED = 'expired', SUSPENDED = 'suspended' } @Entity('api_keys') @Index(['key_hash'], { unique: true }) @Index(['user_id']) @Index(['status']) @Index(['expires_at']) export class ApiKey { @PrimaryGeneratedColumn('uuid') id!: string; @Column({ type: 'varchar', length: 255 }) name!: string; @Column({ type: 'varchar', length: 255, unique: true }) key_hash!: string; @Column({ type: 'varchar', length: 100 }) key_prefix!: string; // First 8 chars for display @Column({ type: 'json' }) scopes!: ApiKeyScope[]; @Column({ type: 'enum', enum: ApiKeyStatus, default: ApiKeyStatus.ACTIVE }) status!: ApiKeyStatus; @Column({ type: 'text', nullable: true }) description?: string; @Column({ type: 'json', nullable: true }) restrictions: { ip_whitelist?: string[]; package_patterns?: string[]; rate_limit?: { requests: number; window: number; }; }; @Column({ type: 'timestamp', nullable: true }) expires_at?: Date; @Column({ type: 'timestamp', nullable: true }) last_used?: Date; @Column({ type: 'varchar', length: 45, nullable: true }) last_used_ip?: string; @Column({ type: 'int', default: 0 }) usage_count!: number; @Column({ type: 'json', nullable: true }) usage_stats: { daily_usage: Record<string, number>; weekly_usage: Record<string, number>; monthly_usage: Record<string, number>; }; @CreateDateColumn() created_at!: Date; @UpdateDateColumn() updated_at!: Date; @Column({ type: 'timestamp', nullable: true }) revoked_at?: Date; @Column({ type: 'text', nullable: true }) revocation_reason?: string; // Relationships @ManyToOne(() => User, user => user.api_keys, { onDelete: 'CASCADE' }) user!: User; @Column({ type: 'uuid' }) user_id!: string; // Methods static generateKey(): { key: string; hash: string; prefix: string } { const crypto = require('crypto'); // Generate a secure random key const key = 'rck_' + crypto.randomBytes(32).toString('hex'); // Hash the key for storage const hash = crypto.createHash('sha256').update(key).digest('hex'); // Get prefix for display const prefix = key.substring(0, 12) + '...'; return { key, hash, prefix }; } static hashKey(key: string): string { const crypto = require('crypto'); return crypto.createHash('sha256').update(key).digest('hex'); } hasScope(scope: ApiKeyScope): boolean { return this.scopes.includes(scope) || this.scopes.includes(ApiKeyScope.ADMIN); } hasScopes(scopes: ApiKeyScope[]): boolean { if (this.scopes.includes(ApiKeyScope.ADMIN)) return true; return scopes.every(scope => this.scopes.includes(scope)); } isExpired(): boolean { if (!this.expires_at) return false; return new Date() > this.expires_at; } isActive(): boolean { return this.status === ApiKeyStatus.ACTIVE && !this.isExpired(); } canAccessPackage(packageName: string): boolean { if (!this.restrictions?.package_patterns) return true; return this.restrictions.package_patterns.some(pattern => { // Simple glob pattern matching const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(packageName); }); } isIpAllowed(ip: string): boolean { if (!this.restrictions?.ip_whitelist) return true; return this.restrictions.ip_whitelist.includes(ip); } recordUsage(ip?: string) { this.last_used = new Date(); this.usage_count += 1; if (ip) { this.last_used_ip = ip; } // Update daily usage stats const today = new Date().toISOString().split('T')[0]; if (!this.usage_stats) { this.usage_stats = { daily_usage: {}, weekly_usage: {}, monthly_usage: {} }; } this.usage_stats.daily_usage[today] = (this.usage_stats.daily_usage[today] || 0) + 1; } revoke(reason?: string) { this.status = ApiKeyStatus.REVOKED; this.revoked_at = new Date(); this.revocation_reason = reason; } suspend() { this.status = ApiKeyStatus.SUSPENDED; } activate() { this.status = ApiKeyStatus.ACTIVE; this.revoked_at = undefined; this.revocation_reason = undefined; } extend(newExpiryDate: Date) { this.expires_at = newExpiryDate; } addScope(scope: ApiKeyScope) { if (!this.scopes.includes(scope)) { this.scopes.push(scope); } } removeScope(scope: ApiKeyScope) { this.scopes = this.scopes.filter(s => s !== scope); } setRestrictions(restrictions: ApiKey['restrictions']) { this.restrictions = restrictions; } getDailyUsage(days: number = 30): Record<string, number> { if (!this.usage_stats?.daily_usage) return {}; const result: Record<string, number> = {}; const today = new Date(); for (let i = 0; i < days; i++) { const date = new Date(today); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; result[dateStr] = this.usage_stats.daily_usage[dateStr] || 0; } return result; } isRateLimited(): boolean { if (!this.restrictions?.rate_limit) return false; const { requests, window } = this.restrictions.rate_limit; const windowStart = new Date(Date.now() - window * 1000); // In a real implementation, this would check against a rate limiting store // For now, we'll use a simple time-based check const recentUsage = this.getDailyUsage(1); const todayUsage = Object.values(recentUsage)[0] || 0; return todayUsage >= requests; } toSafeFormat(): any { return { id: this.id, name: this.name, key_prefix: this.key_prefix, scopes: this.scopes, status: this.status, description: this.description, restrictions: this.restrictions, expires_at: this.expires_at, last_used: this.last_used, usage_count: this.usage_count, created_at: this.created_at, is_expired: this.isExpired(), is_active: this.isActive() }; } toDetailedFormat(): any { return { ...this.toSafeFormat(), last_used_ip: this.last_used_ip, usage_stats: this.usage_stats, revoked_at: this.revoked_at, revocation_reason: this.revocation_reason }; } }