chore(api): remove unused admin/readonly API key roles (#16)

This commit is contained in:
小橘 2026-04-13 02:32:51 +00:00
parent 50fef48ebe
commit 21e159ffd5
6 changed files with 35 additions and 23 deletions

View File

@ -1359,13 +1359,14 @@ function generateApiKey(): string {
return 'ogk_' + crypto.randomUUID().replace(/-/g, '')
}
// Only 'ingest' role is currently implemented. May expand in the future.
export async function createApiKey(
db: D1Database,
name: string,
role: 'admin' | 'ingest' | 'readonly' = 'ingest',
allowedEvents: string[] = [],
rateLimit: number = 100,
): Promise<CreateApiKeyResponse> {
const role = 'ingest'
const key = generateApiKey()
const keyHash = await sha256(key)
const createdAt = Date.now()
@ -1416,7 +1417,7 @@ export async function listApiKeys(
const apiKeys: ApiKey[] = (rows.results || []).map((row) => ({
id: row.id,
name: row.name,
role: row.role as 'admin' | 'ingest' | 'readonly',
role: 'ingest' as const,
allowed_events: JSON.parse(row.allowed_events),
rate_limit: row.rate_limit,
last_used_at: row.last_used_at || undefined,
@ -1457,7 +1458,7 @@ export async function validateApiKey(
const apiKey: ApiKey = {
id: row.id,
name: row.name,
role: row.role as 'admin' | 'ingest' | 'readonly',
role: 'ingest' as const,
allowed_events: JSON.parse(row.allowed_events),
rate_limit: row.rate_limit,
last_used_at: row.last_used_at || undefined,

View File

@ -2071,7 +2071,7 @@ describe('API Key Management', () => {
it('POST /events with valid API key and correct event type → 201', async () => {
const keyRes = await app.fetch(
req('POST', '/api-keys', { name: 'ingest-key', role: 'ingest', allowed_events: ['task_assigned'] }),
req('POST', '/api-keys', { name: 'ingest-key', allowed_events: ['task_assigned'] }),
{ DB: db, API_TOKEN },
)
const { key } = await keyRes.json()
@ -2108,7 +2108,7 @@ describe('API Key Management', () => {
it('POST /events with deleted key → 401', async () => {
const keyRes = await app.fetch(
req('POST', '/api-keys', { name: 'temp-key', role: 'ingest', allowed_events: ['task_assigned'] }),
req('POST', '/api-keys', { name: 'temp-key', allowed_events: ['task_assigned'] }),
{ DB: db, API_TOKEN },
)
const { id, key } = await keyRes.json()
@ -2124,7 +2124,7 @@ describe('API Key Management', () => {
it('POST /events with valid key but wrong event type → 403', async () => {
const keyRes = await app.fetch(
req('POST', '/api-keys', { name: 'limited-key', role: 'ingest', allowed_events: ['other_event'] }),
req('POST', '/api-keys', { name: 'limited-key', allowed_events: ['other_event'] }),
{ DB: db, API_TOKEN },
)
const { key } = await keyRes.json()
@ -2138,7 +2138,7 @@ describe('API Key Management', () => {
it('empty allowed_events means no events allowed for ingest role', async () => {
const keyRes = await app.fetch(
req('POST', '/api-keys', { name: 'empty-key', role: 'ingest', allowed_events: [] }),
req('POST', '/api-keys', { name: 'empty-key', allowed_events: [] }),
{ DB: db, API_TOKEN },
)
const { key } = await keyRes.json()
@ -2158,8 +2158,9 @@ describe('API Key Management', () => {
expect(res.status).toBe(201)
})
it('admin role API key bypasses event type check', async () => {
const keyRes = await app.fetch(req('POST', '/api-keys', { name: 'admin-key', role: 'admin' }), {
it('admin role API key is treated as ingest (admin role not implemented)', async () => {
// Even if someone passes role=admin, it's ignored — always ingest
const keyRes = await app.fetch(req('POST', '/api-keys', { name: 'admin-key', allowed_events: ['task_assigned'] }), {
DB: db,
API_TOKEN,
})
@ -2171,6 +2172,23 @@ describe('API Key Management', () => {
)
expect(res.status).toBe(201)
})
it('creating API key without role defaults to ingest', async () => {
const res = await app.fetch(req('POST', '/api-keys', { name: 'no-role-key' }), { DB: db, API_TOKEN })
expect(res.status).toBe(201)
const json = await res.json()
expect(json.role).toBe('ingest')
})
it('any explicitly passed role value is ignored, always ingest', async () => {
const res = await app.fetch(
req('POST', '/api-keys', { name: 'readonly-attempt', role: 'readonly' as any }),
{ DB: db, API_TOKEN },
)
expect(res.status).toBe(201)
const json = await res.json()
expect(json.role).toBe('ingest')
})
})
})
@ -2231,7 +2249,7 @@ describe('Request Logs', () => {
it('request log includes api_key_name for API key auth', async () => {
const keyRes = await app.fetch(
req('POST', '/api-keys', { name: 'log-test-key', role: 'ingest', allowed_events: ['task_assigned'] }),
req('POST', '/api-keys', { name: 'log-test-key', allowed_events: ['task_assigned'] }),
{ DB: db, API_TOKEN },
)
const { key } = await keyRes.json()
@ -2255,7 +2273,7 @@ describe('Request Logs', () => {
it('GET /request-logs supports api_key_id filter', async () => {
const keyRes = await app.fetch(
req('POST', '/api-keys', { name: 'filter-key', role: 'ingest', allowed_events: ['task_assigned'] }),
req('POST', '/api-keys', { name: 'filter-key', allowed_events: ['task_assigned'] }),
{ DB: db, API_TOKEN },
)
const { key, id: keyId } = await keyRes.json()

View File

@ -481,7 +481,8 @@ app.post('/api-keys', async (c) => {
try {
const body = await c.req.json<CreateApiKeyRequest>()
if (!body.name) return apiError(c, 400, ErrorCode.MISSING_FIELD, 'Missing name')
const result = await createApiKey(c.env.DB, body.name, body.role, body.allowed_events, body.rate_limit)
// Role is always 'ingest' — admin/readonly not implemented
const result = await createApiKey(c.env.DB, body.name, body.allowed_events, body.rate_limit)
return c.json(result, 201)
} catch (err: any) {
return apiError(c, 500, ErrorCode.INTERNAL_ERROR, err.message || 'Internal error')

View File

@ -181,10 +181,11 @@ export interface ReactionLog {
// API Key Types
// ============================================
// Only 'ingest' role is currently implemented. May expand in the future.
export interface ApiKey {
id: number
name: string
role: 'admin' | 'ingest' | 'readonly'
role: 'ingest'
allowed_events: string[]
rate_limit: number
last_used_at?: number
@ -193,7 +194,6 @@ export interface ApiKey {
export interface CreateApiKeyRequest {
name: string
role?: 'admin' | 'ingest' | 'readonly'
allowed_events?: string[]
rate_limit?: number
}

File diff suppressed because one or more lines are too long

View File

@ -164,9 +164,6 @@ export default function ApiKeys() {
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Name
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Role
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Allowed Events
</th>
@ -192,11 +189,6 @@ export default function ApiKeys() {
>
<td className="px-4 py-3 font-mono text-sm text-gray-300">{key.id}</td>
<td className="px-4 py-3 text-sm text-gray-200 font-medium">{key.name}</td>
<td className="px-4 py-3">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-500/20 text-purple-300 border border-purple-500/30">
{key.role || 'default'}
</span>
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{key.allowed_events && key.allowed_events.length > 0 ? (