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
text/typescript
/**
* 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'
}
export class ApiKey {
id!: string;
name!: string;
key_hash!: string;
key_prefix!: string; // First 8 chars for display
scopes!: ApiKeyScope[];
status!: ApiKeyStatus;
description?: string;
restrictions: {
ip_whitelist?: string[];
package_patterns?: string[];
rate_limit?: {
requests: number;
window: number;
};
};
expires_at?: Date;
last_used?: Date;
last_used_ip?: string;
usage_count!: number;
usage_stats: {
daily_usage: Record<string, number>;
weekly_usage: Record<string, number>;
monthly_usage: Record<string, number>;
};
created_at!: Date;
updated_at!: Date;
revoked_at?: Date;
revocation_reason?: string;
// Relationships
user!: User;
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
};
}
}