/** * Component: Admin API Token Management Routes * Documentation: documentation/backend/services/api-tokens.md */ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; import { RMABLogger } from '@/lib/utils/logger'; import { checkApiTokenCreateRateLimit } from '@/lib/utils/rateLimit'; import { MAX_TOKENS_PER_USER } from '@/lib/constants/api-tokens'; import { generateApiToken } from '@/lib/utils/api-token'; import { z } from 'zod'; const logger = RMABLogger.create('API.Admin.ApiTokens'); const CreateTokenSchema = z.object({ name: z.string().min(1).max(100), expiresAt: z.string().datetime().nullable().optional(), userId: z.string().uuid().optional(), // Admin can specify which user the token acts as role: z.enum(['admin', 'user']).optional(), // Accepted for compatibility, but cannot differ from target user role }); /** * GET /api/admin/api-tokens * List ALL API tokens across all users */ export async function GET(request: NextRequest) { return requireAuth(request, (req: AuthenticatedRequest) => requireAdmin(req, async () => { try { const tokens = await prisma.apiToken.findMany({ include: { createdBy: { select: { id: true, plexUsername: true }, }, tokenUser: { select: { id: true, plexUsername: true, role: true }, }, }, orderBy: { createdAt: 'desc' }, }); const sanitized = tokens.map((t) => ({ id: t.id, name: t.name, tokenPrefix: t.tokenPrefix, role: t.role, createdBy: t.createdBy.plexUsername, createdById: t.createdBy.id, tokenUser: t.tokenUser.plexUsername, tokenUserId: t.tokenUser.id, lastUsedAt: t.lastUsedAt, expiresAt: t.expiresAt, createdAt: t.createdAt, })); return NextResponse.json({ tokens: sanitized }); } catch (error) { logger.error('Failed to list API tokens', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json({ error: 'Failed to list API tokens' }, { status: 500 }); } }) ); } /** * POST /api/admin/api-tokens * Create a new API token. Admin can optionally specify userId. * Token role is always derived from the target user's current role. * Returns the full token ONCE. */ export async function POST(request: NextRequest) { return requireAuth(request, (req: AuthenticatedRequest) => requireAdmin(req, async () => { try { const rateLimit = checkApiTokenCreateRateLimit(req.user!.id); if (!rateLimit.allowed) { return NextResponse.json( { error: 'Too many API token create attempts. Please try again later.' }, { status: 429, headers: { 'Retry-After': String(rateLimit.retryAfterSeconds), }, } ); } const body = await req.json(); const { name, expiresAt, userId, role } = CreateTokenSchema.parse(body); // Determine target user (defaults to the admin themselves) const targetUserId = userId || req.user!.id; // Verify the target user exists const targetUser = await prisma.user.findUnique({ where: { id: targetUserId }, select: { id: true, role: true, plexUsername: true }, }); if (!targetUser) { return NextResponse.json({ error: 'Target user not found' }, { status: 404 }); } // Enforce per-user token cap (count only active, non-expired tokens) const activeTokenCount = await prisma.apiToken.count({ where: { userId: targetUserId, OR: [ { expiresAt: null }, { expiresAt: { gt: new Date() } }, ], }, }); if (activeTokenCount >= MAX_TOKENS_PER_USER) { return NextResponse.json( { error: `Token limit reached. Users may have at most ${MAX_TOKENS_PER_USER} active API tokens.` }, { status: 403 } ); } // Security guard: token role must always match the target user's persisted role. // This avoids role/identity mismatch (for example: acting as user A with admin role). if (role && role !== targetUser.role) { logger.warn('Admin attempted token role override that differs from target user role', { requestedRole: role, userActualRole: targetUser.role, targetUser: targetUser.plexUsername, createdBy: req.user!.username, }); return NextResponse.json( { error: `Token role must match target user's role (${targetUser.role}).`, }, { status: 400 } ); } const tokenRole = targetUser.role; // Generate the token const { fullToken, tokenHash, tokenPrefix } = generateApiToken(); const apiToken = await prisma.apiToken.create({ data: { name, tokenHash, tokenPrefix, role: tokenRole, createdById: req.user!.id, userId: targetUserId, expiresAt: expiresAt ? new Date(expiresAt) : null, }, }); logger.info('Admin API token created', { tokenId: apiToken.id, name, createdBy: req.user!.username, targetUser: targetUser.plexUsername, role: tokenRole, }); return NextResponse.json({ token: { id: apiToken.id, name: apiToken.name, tokenPrefix: apiToken.tokenPrefix, role: apiToken.role, expiresAt: apiToken.expiresAt, createdAt: apiToken.createdAt, }, // Full token is returned ONLY on creation fullToken, }, { status: 201 }); } catch (error) { logger.error('Failed to create API token', { error: error instanceof Error ? error.message : String(error) }); if (error instanceof z.ZodError) { return NextResponse.json({ error: 'Validation error', details: error.errors }, { status: 400 }); } return NextResponse.json({ error: 'Failed to create API token' }, { status: 500 }); } }) ); }