Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 216 lines 5.5 kB view raw
1import crypto from 'node:crypto'; 2import type { Kysely } from 'kysely'; 3import { inject } from '../../iocContainer/index.js'; 4import { type CombinedPg } from '../combinedDbTypes.js'; 5 6export interface ApiKeyMetadata { 7 name: string; 8 description: string; 9} 10 11export interface ApiKeyWithMetadata { 12 key: string; 13 metadata: ApiKeyMetadata; 14} 15 16export interface ApiKeyRecord { 17 id: string; 18 orgId: string; 19 keyHash: string; 20 name: string; 21 description: string | null; 22 isActive: boolean; 23 createdAt: Date; 24 updatedAt: Date; 25 lastUsedAt: Date | null; 26 createdBy: string | null; 27} 28 29class ApiKeyService { 30 constructor(private readonly db: Kysely<CombinedPg>) {} 31 32 /** 33 * Generates a secure random API key 34 */ 35 private generateApiKey(): string { 36 return crypto.randomBytes(32).toString('hex'); 37 } 38 39 /** 40 * Hashes an API key for secure storage 41 */ 42 private hashApiKey(apiKey: string): string { 43 return crypto.createHash('sha256').update(apiKey).digest('hex'); 44 } 45 46 /** 47 * Creates a new API key for an organization 48 */ 49 async createApiKey( 50 orgId: string, 51 name: string, 52 description: string | null, 53 createdBy: string | null 54 ): Promise<{ apiKey: string; record: ApiKeyRecord }> { 55 const apiKey = this.generateApiKey(); 56 const keyHash = this.hashApiKey(apiKey); 57 58 // Deactivate any existing active keys for this org 59 await this.deactivateAllKeysForOrg(orgId); 60 61 const result = await this.db 62 .insertInto('public.api_keys') 63 .values({ 64 org_id: orgId, 65 key_hash: keyHash, 66 name, 67 description, 68 is_active: true, 69 created_by: createdBy, 70 }) 71 .returningAll() 72 .executeTakeFirstOrThrow(); 73 74 return { 75 apiKey, 76 record: this.mapDbRecordToApiKeyRecord(result), 77 }; 78 } 79 80 /** 81 * Rotates (creates new and deactivates old) API key for an organization 82 */ 83 async rotateApiKey( 84 orgId: string, 85 name: string, 86 description: string | null, 87 createdBy: string | null 88 ): Promise<{ apiKey: string; record: ApiKeyRecord }> { 89 return this.createApiKey(orgId, name, description, createdBy); 90 } 91 92 /** 93 * Gets the active API key for an organization 94 */ 95 async getActiveApiKeyForOrg(orgId: string): Promise<ApiKeyRecord | null> { 96 const result = await this.db 97 .selectFrom('public.api_keys') 98 .selectAll() 99 .where('org_id', '=', orgId) 100 .where('is_active', '=', true) 101 .executeTakeFirst(); 102 103 return result ? this.mapDbRecordToApiKeyRecord(result) : null; 104 } 105 106 /** 107 * Gets all API keys for an organization 108 */ 109 async getApiKeysForOrg(orgId: string): Promise<ApiKeyRecord[]> { 110 const results = await this.db 111 .selectFrom('public.api_keys') 112 .selectAll() 113 .where('org_id', '=', orgId) 114 .orderBy('created_at', 'desc') 115 .execute(); 116 117 return results.map(this.mapDbRecordToApiKeyRecord); 118 } 119 120 /** 121 * Validates an API key and returns the associated org ID 122 */ 123 async validateApiKey(apiKey: string): Promise<string | null> { 124 const keyHash = this.hashApiKey(apiKey); 125 126 const result = await this.db 127 .selectFrom('public.api_keys') 128 .select(['org_id', 'last_used_at']) 129 .where('key_hash', '=', keyHash) 130 .where('is_active', '=', true) 131 .executeTakeFirst(); 132 133 if (!result) { 134 return null; 135 } 136 137 // Update last used timestamp 138 await this.db 139 .updateTable('public.api_keys') 140 .set({ last_used_at: new Date() }) 141 .where('key_hash', '=', keyHash) 142 .execute(); 143 144 return result.org_id; 145 } 146 147 /** 148 * Deactivates a specific API key 149 */ 150 async deactivateApiKey(keyId: string, orgId: string): Promise<boolean> { 151 const result = await this.db 152 .updateTable('public.api_keys') 153 .set({ is_active: false }) 154 .where('id', '=', keyId) 155 .where('org_id', '=', orgId) 156 .execute(); 157 158 return result.length > 0; 159 } 160 161 /** 162 * Deactivates all API keys for an organization 163 */ 164 async deactivateAllKeysForOrg(orgId: string): Promise<void> { 165 await this.db 166 .updateTable('public.api_keys') 167 .set({ is_active: false }) 168 .where('org_id', '=', orgId) 169 .execute(); 170 } 171 172 /** 173 * Deletes a specific API key 174 */ 175 async deleteApiKey(keyId: string, orgId: string): Promise<boolean> { 176 const result = await this.db 177 .deleteFrom('public.api_keys') 178 .where('id', '=', keyId) 179 .where('org_id', '=', orgId) 180 .execute(); 181 182 return result.length > 0; 183 } 184 /** 185 * Gets the org ID from an activated API key 186 */ 187 async getOrgIdFromActivatedKey(apiKey: string): Promise<string | null> { 188 const keyHash = this.hashApiKey(apiKey); 189 const result = await this.db.selectFrom('public.api_keys').select('org_id').where('key_hash', '=', keyHash).where('is_active', '=', true).executeTakeFirst(); 190 if (!result) { 191 return null; 192 } 193 return result.org_id; 194 } 195 196 /** 197 * Maps database record to ApiKeyRecord 198 */ 199 private mapDbRecordToApiKeyRecord(record: any): ApiKeyRecord { 200 return { 201 id: record.id, 202 orgId: record.org_id, 203 keyHash: record.key_hash, 204 name: record.name, 205 description: record.description, 206 isActive: record.is_active, 207 createdAt: record.created_at, 208 updatedAt: record.updated_at, 209 lastUsedAt: record.last_used_at, 210 createdBy: record.created_by, 211 }; 212 } 213} 214 215export default inject(['KyselyPg'], ApiKeyService); 216export type { ApiKeyService };