Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 149 lines 3.6 kB view raw
1import { eq } from "@openstatus/db"; 2import { db } from "@openstatus/db"; 3import { apiKey } from "@openstatus/db/src/schema"; 4import { 5 shouldUpdateLastUsed as checkShouldUpdateLastUsed, 6 generateApiKey as generateKey, 7 verifyApiKeyHash, 8} from "@openstatus/db/src/utils/api-key"; 9 10/** 11 * Creates a new API key for a workspace 12 * @param workspaceId - The workspace ID 13 * @param createdById - The ID of the user creating the key 14 * @param name - The name of the API key 15 * @param description - Optional description for the key 16 * @param expiresAt - Optional expiration date 17 * @returns The full token (only shown once) and the created key details 18 */ 19export async function createApiKey( 20 workspaceId: number, 21 createdById: number, 22 name: string, 23 description?: string, 24 expiresAt?: Date, 25): Promise<{ token: string; key: typeof apiKey.$inferSelect }> { 26 const { token, prefix, hash } = await generateKey(); 27 28 const [key] = await db 29 .insert(apiKey) 30 .values({ 31 name, 32 description, 33 prefix, 34 hashedToken: hash, 35 workspaceId, 36 createdById, 37 expiresAt, 38 }) 39 .returning(); 40 41 if (!key) { 42 throw new Error("Failed to create API key"); 43 } 44 45 return { token, key }; 46} 47 48/** 49 * Verifies an API key token 50 * @param token - The API key token to verify 51 * @returns The API key details if valid, null otherwise 52 */ 53export async function verifyApiKey( 54 token: string, 55): Promise<typeof apiKey.$inferSelect | null> { 56 // Validate token format before database query 57 if (!/^os_[a-f0-9]{32}$/.test(token)) { 58 return null; 59 } 60 61 // Extract prefix from token 62 const prefix = token.slice(0, 11); // "os_" + 8 chars = 11 total 63 64 // Look up key by prefix 65 const key = await db 66 .select() 67 .from(apiKey) 68 .where(eq(apiKey.prefix, prefix)) 69 .get(); 70 71 if (!key) { 72 return null; 73 } 74 75 // Verify hash using bcrypt-compatible verification 76 if (!(await verifyApiKeyHash(token, key.hashedToken))) { 77 return null; 78 } 79 80 // Check expiration 81 if (key.expiresAt && key.expiresAt < new Date()) { 82 return null; 83 } 84 85 return key; 86} 87 88/** 89 * Revokes (deletes) an API key 90 * @param id - The API key ID 91 * @param workspaceId - The workspace ID for ownership verification 92 * @returns True if successfully revoked, false otherwise 93 */ 94export async function revokeApiKey( 95 id: number, 96 workspaceId: number, 97): Promise<boolean> { 98 // First, verify the key exists and belongs to the workspace 99 const key = await db.select().from(apiKey).where(eq(apiKey.id, id)).get(); 100 101 if (!key || key.workspaceId !== workspaceId) { 102 return false; 103 } 104 105 // Delete the key 106 await db.delete(apiKey).where(eq(apiKey.id, id)); 107 108 return true; 109} 110 111/** 112 * Gets all API keys for a workspace 113 * @param workspaceId - The workspace ID 114 * @returns Array of API keys for the workspace 115 */ 116export async function getApiKeys( 117 workspaceId: number, 118): Promise<Array<typeof apiKey.$inferSelect>> { 119 const keys = await db 120 .select() 121 .from(apiKey) 122 .where(eq(apiKey.workspaceId, workspaceId)) 123 .all(); 124 125 return keys; 126} 127 128/** 129 * Updates the lastUsedAt timestamp for an API key (with debouncing) 130 * @param id - The API key ID 131 * @param lastUsedAt - The current lastUsedAt value (or null) 132 * @returns True if updated, false if skipped due to debounce 133 */ 134export async function updateLastUsed( 135 id: number, 136 lastUsedAt: Date | null, 137): Promise<boolean> { 138 // Check if update is needed (5-minute debounce) 139 if (!checkShouldUpdateLastUsed(lastUsedAt)) { 140 return false; 141 } 142 143 await db 144 .update(apiKey) 145 .set({ lastUsedAt: new Date() }) 146 .where(eq(apiKey.id, id)); 147 148 return true; 149}