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, '') return 'ogk_' + crypto.randomUUID().replace(/-/g, '')
} }
// Only 'ingest' role is currently implemented. May expand in the future.
export async function createApiKey( export async function createApiKey(
db: D1Database, db: D1Database,
name: string, name: string,
role: 'admin' | 'ingest' | 'readonly' = 'ingest',
allowedEvents: string[] = [], allowedEvents: string[] = [],
rateLimit: number = 100, rateLimit: number = 100,
): Promise<CreateApiKeyResponse> { ): Promise<CreateApiKeyResponse> {
const role = 'ingest'
const key = generateApiKey() const key = generateApiKey()
const keyHash = await sha256(key) const keyHash = await sha256(key)
const createdAt = Date.now() const createdAt = Date.now()
@ -1416,7 +1417,7 @@ export async function listApiKeys(
const apiKeys: ApiKey[] = (rows.results || []).map((row) => ({ const apiKeys: ApiKey[] = (rows.results || []).map((row) => ({
id: row.id, id: row.id,
name: row.name, name: row.name,
role: row.role as 'admin' | 'ingest' | 'readonly', role: 'ingest' as const,
allowed_events: JSON.parse(row.allowed_events), allowed_events: JSON.parse(row.allowed_events),
rate_limit: row.rate_limit, rate_limit: row.rate_limit,
last_used_at: row.last_used_at || undefined, last_used_at: row.last_used_at || undefined,
@ -1457,7 +1458,7 @@ export async function validateApiKey(
const apiKey: ApiKey = { const apiKey: ApiKey = {
id: row.id, id: row.id,
name: row.name, name: row.name,
role: row.role as 'admin' | 'ingest' | 'readonly', role: 'ingest' as const,
allowed_events: JSON.parse(row.allowed_events), allowed_events: JSON.parse(row.allowed_events),
rate_limit: row.rate_limit, rate_limit: row.rate_limit,
last_used_at: row.last_used_at || undefined, 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 () => { it('POST /events with valid API key and correct event type → 201', async () => {
const keyRes = await app.fetch( 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 }, { DB: db, API_TOKEN },
) )
const { key } = await keyRes.json() const { key } = await keyRes.json()
@ -2108,7 +2108,7 @@ describe('API Key Management', () => {
it('POST /events with deleted key → 401', async () => { it('POST /events with deleted key → 401', async () => {
const keyRes = await app.fetch( 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 }, { DB: db, API_TOKEN },
) )
const { id, key } = await keyRes.json() 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 () => { it('POST /events with valid key but wrong event type → 403', async () => {
const keyRes = await app.fetch( 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 }, { DB: db, API_TOKEN },
) )
const { key } = await keyRes.json() 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 () => { it('empty allowed_events means no events allowed for ingest role', async () => {
const keyRes = await app.fetch( 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 }, { DB: db, API_TOKEN },
) )
const { key } = await keyRes.json() const { key } = await keyRes.json()
@ -2158,8 +2158,9 @@ describe('API Key Management', () => {
expect(res.status).toBe(201) expect(res.status).toBe(201)
}) })
it('admin role API key bypasses event type check', async () => { it('admin role API key is treated as ingest (admin role not implemented)', async () => {
const keyRes = await app.fetch(req('POST', '/api-keys', { name: 'admin-key', role: 'admin' }), { // 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, DB: db,
API_TOKEN, API_TOKEN,
}) })
@ -2171,6 +2172,23 @@ describe('API Key Management', () => {
) )
expect(res.status).toBe(201) 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 () => { it('request log includes api_key_name for API key auth', async () => {
const keyRes = await app.fetch( 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 }, { DB: db, API_TOKEN },
) )
const { key } = await keyRes.json() const { key } = await keyRes.json()
@ -2255,7 +2273,7 @@ describe('Request Logs', () => {
it('GET /request-logs supports api_key_id filter', async () => { it('GET /request-logs supports api_key_id filter', async () => {
const keyRes = await app.fetch( 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 }, { DB: db, API_TOKEN },
) )
const { key, id: keyId } = await keyRes.json() const { key, id: keyId } = await keyRes.json()

View File

@ -481,7 +481,8 @@ app.post('/api-keys', async (c) => {
try { try {
const body = await c.req.json<CreateApiKeyRequest>() const body = await c.req.json<CreateApiKeyRequest>()
if (!body.name) return apiError(c, 400, ErrorCode.MISSING_FIELD, 'Missing name') 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) return c.json(result, 201)
} catch (err: any) { } catch (err: any) {
return apiError(c, 500, ErrorCode.INTERNAL_ERROR, err.message || 'Internal error') return apiError(c, 500, ErrorCode.INTERNAL_ERROR, err.message || 'Internal error')

View File

@ -181,10 +181,11 @@ export interface ReactionLog {
// API Key Types // API Key Types
// ============================================ // ============================================
// Only 'ingest' role is currently implemented. May expand in the future.
export interface ApiKey { export interface ApiKey {
id: number id: number
name: string name: string
role: 'admin' | 'ingest' | 'readonly' role: 'ingest'
allowed_events: string[] allowed_events: string[]
rate_limit: number rate_limit: number
last_used_at?: number last_used_at?: number
@ -193,7 +194,6 @@ export interface ApiKey {
export interface CreateApiKeyRequest { export interface CreateApiKeyRequest {
name: string name: string
role?: 'admin' | 'ingest' | 'readonly'
allowed_events?: string[] allowed_events?: string[]
rate_limit?: number 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"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Name Name
</th> </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"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Allowed Events Allowed Events
</th> </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 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 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"> <td className="px-4 py-3">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{key.allowed_events && key.allowed_events.length > 0 ? ( {key.allowed_events && key.allowed_events.length > 0 ? (