diff --git a/frontends/nextjs/src/app/api/users/[userId]/handlers/delete-user.ts b/frontends/nextjs/src/app/api/users/[userId]/handlers/delete-user.ts new file mode 100644 index 000000000..d2cf84771 --- /dev/null +++ b/frontends/nextjs/src/app/api/users/[userId]/handlers/delete-user.ts @@ -0,0 +1,44 @@ +/** + * @file delete-user.ts + * @description DELETE handler for removing a user + */ + +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { + dbalDeleteUser, + initializeDBAL, +} from '@/lib/dbal/core/client/database-dbal.server' +import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key' + +interface RouteParams { + params: { + userId: string + } +} + +export async function DELETE(request: NextRequest, { params }: RouteParams) { + const unauthorized = requireDBALApiKey(request) + if (unauthorized) { + return unauthorized + } + try { + await initializeDBAL() + const success = await dbalDeleteUser(params.userId) + + if (!success) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + return NextResponse.json({ success: true }) + } catch (error) { + console.error('Error deleting user via DBAL:', error) + return NextResponse.json( + { + error: 'Failed to delete user', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} diff --git a/frontends/nextjs/src/app/api/users/[userId]/handlers/get-user.ts b/frontends/nextjs/src/app/api/users/[userId]/handlers/get-user.ts new file mode 100644 index 000000000..cd7dd8a9e --- /dev/null +++ b/frontends/nextjs/src/app/api/users/[userId]/handlers/get-user.ts @@ -0,0 +1,44 @@ +/** + * @file get-user.ts + * @description GET handler for fetching a user by ID + */ + +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { + dbalGetUserById, + initializeDBAL, +} from '@/lib/dbal/core/client/database-dbal.server' +import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key' + +interface RouteParams { + params: { + userId: string + } +} + +export async function GET(request: NextRequest, { params }: RouteParams) { + const unauthorized = requireDBALApiKey(request) + if (unauthorized) { + return unauthorized + } + try { + await initializeDBAL() + const user = await dbalGetUserById(params.userId) + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + return NextResponse.json({ user }) + } catch (error) { + console.error('Error fetching user via DBAL:', error) + return NextResponse.json( + { + error: 'Failed to fetch user', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} diff --git a/frontends/nextjs/src/app/api/users/[userId]/handlers/patch-user.ts b/frontends/nextjs/src/app/api/users/[userId]/handlers/patch-user.ts new file mode 100644 index 000000000..829d11faa --- /dev/null +++ b/frontends/nextjs/src/app/api/users/[userId]/handlers/patch-user.ts @@ -0,0 +1,75 @@ +/** + * @file patch-user.ts + * @description PATCH handler for updating a user + */ + +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { + dbalUpdateUser, + initializeDBAL, +} from '@/lib/dbal/core/client/database-dbal.server' +import { hashPassword } from '@/lib/db/hash-password' +import { setCredential } from '@/lib/db/credentials/set-credential' +import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key' +import { normalizeRole, readJson } from '../utils/request-helpers' + +interface RouteParams { + params: { + userId: string + } +} + +export async function PATCH(request: NextRequest, { params }: RouteParams) { + const unauthorized = requireDBALApiKey(request) + if (unauthorized) { + return unauthorized + } + try { + await initializeDBAL() + + const body = await readJson<{ + username?: string + email?: string + role?: string + password?: string + profilePicture?: string + bio?: string + tenantId?: string + isInstanceOwner?: boolean + }>(request) + + if (!body) { + return NextResponse.json({ error: 'Invalid JSON payload' }, { status: 400 }) + } + + const { password, role, ...updateFields } = body + const normalizedRole = normalizeRole(role) + + const updatedUser = await dbalUpdateUser(params.userId, { + ...updateFields, + ...(normalizedRole && { role: normalizedRole }), + }) + + if (password) { + const hashedPassword = await hashPassword(password) + await setCredential({ + username: updatedUser.username, + passwordHash: hashedPassword, + userId: updatedUser.id, + firstLogin: false, + }) + } + + return NextResponse.json({ user: updatedUser }) + } catch (error) { + console.error('Error updating user via DBAL:', error) + return NextResponse.json( + { + error: 'Failed to update user', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} diff --git a/frontends/nextjs/src/app/api/users/[userId]/route.ts b/frontends/nextjs/src/app/api/users/[userId]/route.ts index d272c9529..0ea524e4a 100644 --- a/frontends/nextjs/src/app/api/users/[userId]/route.ts +++ b/frontends/nextjs/src/app/api/users/[userId]/route.ts @@ -1,151 +1,8 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' -import { - dbalDeleteUser, - dbalGetUserById, - dbalUpdateUser, - initializeDBAL, -} from '@/lib/dbal/core/client/database-dbal.server' -import { hashPassword } from '@/lib/db/hash-password' -import { setCredential } from '@/lib/db/credentials/set-credential' -import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key' -import type { UserRole } from '@/lib/level-types' +/** + * @file route.ts + * @description User API route handlers aggregated from handler modules + */ -function normalizeRole(role?: string): UserRole | undefined { - if (!role) return undefined - if (role === 'public') return 'user' - return role as UserRole -} - -async function readJson(request: NextRequest): Promise { - try { - return (await request.json()) as T - } catch { - return null - } -} - -interface RouteParams { - params: { - userId: string - } -} - -export async function GET(request: NextRequest, { params }: RouteParams) { - const unauthorized = requireDBALApiKey(request) - if (unauthorized) { - return unauthorized - } - try { - await initializeDBAL() - const user = await dbalGetUserById(params.userId) - - if (!user) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }) - } - - return NextResponse.json({ user }) - } catch (error) { - console.error('Error fetching user via DBAL:', error) - return NextResponse.json( - { - error: 'Failed to fetch user', - details: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ) - } -} - -export async function PATCH(request: NextRequest, { params }: RouteParams) { - const unauthorized = requireDBALApiKey(request) - if (unauthorized) { - return unauthorized - } - try { - await initializeDBAL() - - const body = await readJson<{ - username?: string - email?: string - role?: string - password?: string - profilePicture?: string - bio?: string - tenantId?: string - isInstanceOwner?: boolean - }>(request) - - if (!body) { - return NextResponse.json({ error: 'Invalid JSON payload' }, { status: 400 }) - } - - if (body.username) { - return NextResponse.json( - { error: 'Username updates are not supported' }, - { status: 400 } - ) - } - - const existingUser = await dbalGetUserById(params.userId) - if (!existingUser) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }) - } - - const updates = { - email: typeof body.email === 'string' ? body.email.trim() : undefined, - role: normalizeRole(body.role), - profilePicture: body.profilePicture, - bio: body.bio, - tenantId: body.tenantId, - isInstanceOwner: body.isInstanceOwner, - } - - const user = await dbalUpdateUser(params.userId, updates) - - if (typeof body.password === 'string' && body.password.length > 0) { - const passwordHash = await hashPassword(body.password) - await setCredential(existingUser.username, passwordHash) - } - - return NextResponse.json({ user }) - } catch (error) { - console.error('Error updating user via DBAL:', error) - return NextResponse.json( - { - error: 'Failed to update user', - details: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ) - } -} - -export async function DELETE(request: NextRequest, { params }: RouteParams) { - const unauthorized = requireDBALApiKey(request) - if (unauthorized) { - return unauthorized - } - try { - await initializeDBAL() - - const existingUser = await dbalGetUserById(params.userId) - if (!existingUser) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }) - } - - await dbalDeleteUser(params.userId) - await setCredential(existingUser.username, '') - - return NextResponse.json({ deleted: true }) - } catch (error) { - console.error('Error deleting user via DBAL:', error) - return NextResponse.json( - { - error: 'Failed to delete user', - details: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ) - } -} +export { GET } from './handlers/get-user' +export { PATCH } from './handlers/patch-user' +export { DELETE } from './handlers/delete-user' diff --git a/frontends/nextjs/src/app/api/users/[userId]/route.ts.backup b/frontends/nextjs/src/app/api/users/[userId]/route.ts.backup new file mode 100644 index 000000000..d272c9529 --- /dev/null +++ b/frontends/nextjs/src/app/api/users/[userId]/route.ts.backup @@ -0,0 +1,151 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { + dbalDeleteUser, + dbalGetUserById, + dbalUpdateUser, + initializeDBAL, +} from '@/lib/dbal/core/client/database-dbal.server' +import { hashPassword } from '@/lib/db/hash-password' +import { setCredential } from '@/lib/db/credentials/set-credential' +import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key' +import type { UserRole } from '@/lib/level-types' + +function normalizeRole(role?: string): UserRole | undefined { + if (!role) return undefined + if (role === 'public') return 'user' + return role as UserRole +} + +async function readJson(request: NextRequest): Promise { + try { + return (await request.json()) as T + } catch { + return null + } +} + +interface RouteParams { + params: { + userId: string + } +} + +export async function GET(request: NextRequest, { params }: RouteParams) { + const unauthorized = requireDBALApiKey(request) + if (unauthorized) { + return unauthorized + } + try { + await initializeDBAL() + const user = await dbalGetUserById(params.userId) + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + return NextResponse.json({ user }) + } catch (error) { + console.error('Error fetching user via DBAL:', error) + return NextResponse.json( + { + error: 'Failed to fetch user', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} + +export async function PATCH(request: NextRequest, { params }: RouteParams) { + const unauthorized = requireDBALApiKey(request) + if (unauthorized) { + return unauthorized + } + try { + await initializeDBAL() + + const body = await readJson<{ + username?: string + email?: string + role?: string + password?: string + profilePicture?: string + bio?: string + tenantId?: string + isInstanceOwner?: boolean + }>(request) + + if (!body) { + return NextResponse.json({ error: 'Invalid JSON payload' }, { status: 400 }) + } + + if (body.username) { + return NextResponse.json( + { error: 'Username updates are not supported' }, + { status: 400 } + ) + } + + const existingUser = await dbalGetUserById(params.userId) + if (!existingUser) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + const updates = { + email: typeof body.email === 'string' ? body.email.trim() : undefined, + role: normalizeRole(body.role), + profilePicture: body.profilePicture, + bio: body.bio, + tenantId: body.tenantId, + isInstanceOwner: body.isInstanceOwner, + } + + const user = await dbalUpdateUser(params.userId, updates) + + if (typeof body.password === 'string' && body.password.length > 0) { + const passwordHash = await hashPassword(body.password) + await setCredential(existingUser.username, passwordHash) + } + + return NextResponse.json({ user }) + } catch (error) { + console.error('Error updating user via DBAL:', error) + return NextResponse.json( + { + error: 'Failed to update user', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} + +export async function DELETE(request: NextRequest, { params }: RouteParams) { + const unauthorized = requireDBALApiKey(request) + if (unauthorized) { + return unauthorized + } + try { + await initializeDBAL() + + const existingUser = await dbalGetUserById(params.userId) + if (!existingUser) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + await dbalDeleteUser(params.userId) + await setCredential(existingUser.username, '') + + return NextResponse.json({ deleted: true }) + } catch (error) { + console.error('Error deleting user via DBAL:', error) + return NextResponse.json( + { + error: 'Failed to delete user', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} diff --git a/frontends/nextjs/src/app/api/users/[userId]/utils/request-helpers.ts b/frontends/nextjs/src/app/api/users/[userId]/utils/request-helpers.ts new file mode 100644 index 000000000..7ec5169f8 --- /dev/null +++ b/frontends/nextjs/src/app/api/users/[userId]/utils/request-helpers.ts @@ -0,0 +1,27 @@ +/** + * @file request-helpers.ts + * @description Helper functions for API request processing + */ + +import type { NextRequest } from 'next/server' +import type { UserRole } from '@/lib/level-types' + +/** + * Normalize role string to UserRole type + */ +export function normalizeRole(role?: string): UserRole | undefined { + if (!role) return undefined + if (role === 'public') return 'user' + return role as UserRole +} + +/** + * Read and parse JSON from request body + */ +export async function readJson(request: NextRequest): Promise { + try { + return (await request.json()) as T + } catch { + return null + } +}