import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { confluenceCreateSpaceContract, confluenceDeleteSpaceContract, confluenceGetSpaceContract, confluenceUpdateSpaceContract, } from '@/lib/api/contracts/selectors/confluence' import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getConfluenceCloudId } from '@/tools/confluence/utils' import { parseAtlassianErrorMessage } from '@/tools/jira/utils' const logger = createLogger('ConfluenceSpaceAPI') export const dynamic = 'force-dynamic' // Get a specific space export const GET = withRouteHandler(async (request: NextRequest) => { try { const auth = await checkSessionOrInternalAuth(request) if (!auth.success || !auth.userId) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } const parsed = await parseRequest(confluenceGetSpaceContract, request, {}) if (!parsed.success) return parsed.response const { domain, accessToken, spaceId, cloudId: providedCloudId } = parsed.data.query if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) } if (!accessToken) { return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) } if (!spaceId) { return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) } const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) if (!spaceIdValidation.isValid) { return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) } const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') if (!cloudIdValidation.isValid) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, }, }) if (!response.ok) { const errorText = await response.text() logger.error('Confluence API error response:', { status: response.status, statusText: response.statusText, error: errorText, }) return NextResponse.json( { error: parseAtlassianErrorMessage(response.status, response.statusText, errorText) }, { status: response.status } ) } const data = await response.json() return NextResponse.json(data) } catch (error) { logger.error('Error getting Confluence space:', error) return NextResponse.json( { error: (error as Error).message || 'Internal server error' }, { status: 500 } ) } }) /** * Create a new Confluence space. * Uses POST /wiki/api/v2/spaces */ export const POST = withRouteHandler(async (request: NextRequest) => { try { const auth = await checkSessionOrInternalAuth(request) if (!auth.success || !auth.userId) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } const parsed = await parseRequest(confluenceCreateSpaceContract, request, {}) if (!parsed.success) return parsed.response const { domain, accessToken, name, key, description, cloudId: providedCloudId, } = parsed.data.body if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) } if (!accessToken) { return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) } if (!name) { return NextResponse.json({ error: 'Space name is required' }, { status: 400 }) } if (!key) { return NextResponse.json({ error: 'Space key is required' }, { status: 400 }) } const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') if (!cloudIdValidation.isValid) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces` const createBody: Record = { name, key } if (description) { createBody.description = { value: description, representation: 'plain' } } logger.info(`Creating space with key ${key}`) const response = await fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify(createBody), }) if (!response.ok) { const errorText = await response.text() logger.error('Confluence API error response:', { status: response.status, statusText: response.statusText, error: errorText, }) return NextResponse.json( { error: parseAtlassianErrorMessage(response.status, response.statusText, errorText) }, { status: response.status } ) } const data = await response.json() return NextResponse.json(data) } catch (error) { logger.error('Error creating Confluence space:', error) return NextResponse.json( { error: (error as Error).message || 'Internal server error' }, { status: 500 } ) } }) /** * Update a Confluence space. * Uses PUT /wiki/api/v2/spaces/{id} */ export const PUT = withRouteHandler(async (request: NextRequest) => { try { const auth = await checkSessionOrInternalAuth(request) if (!auth.success || !auth.userId) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } const parsed = await parseRequest(confluenceUpdateSpaceContract, request, {}) if (!parsed.success) return parsed.response const { domain, accessToken, spaceId, name, description, cloudId: providedCloudId, } = parsed.data.body if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) } if (!accessToken) { return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) } if (!spaceId) { return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) } const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) if (!spaceIdValidation.isValid) { return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) } const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') if (!cloudIdValidation.isValid) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } if (!name && description === undefined) { return NextResponse.json( { error: 'At least one of name or description is required for update' }, { status: 400 } ) } const lookupUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` const lookupResponse = await fetch(lookupUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, }, }) if (!lookupResponse.ok) { const errorText = await lookupResponse.text() return NextResponse.json( { error: parseAtlassianErrorMessage( lookupResponse.status, lookupResponse.statusText, errorText ), }, { status: lookupResponse.status } ) } const currentSpace = await lookupResponse.json() const spaceKey = currentSpace.key const updateBody: Record = { name: name || currentSpace.name, } if (description !== undefined) { updateBody.description = { plain: { value: description, representation: 'plain' } } } const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/space/${encodeURIComponent(spaceKey)}` logger.info(`Updating space ${spaceKey}`) const response = await fetch(url, { method: 'PUT', headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify(updateBody), }) if (!response.ok) { const errorText = await response.text() logger.error('Confluence API error response:', { status: response.status, statusText: response.statusText, error: errorText, }) return NextResponse.json( { error: parseAtlassianErrorMessage(response.status, response.statusText, errorText) }, { status: response.status } ) } const data = await response.json() return NextResponse.json(data) } catch (error) { logger.error('Error updating Confluence space:', error) return NextResponse.json( { error: (error as Error).message || 'Internal server error' }, { status: 500 } ) } }) /** * Delete a Confluence space. * Uses DELETE /wiki/api/v2/spaces/{id} */ export const DELETE = withRouteHandler(async (request: NextRequest) => { try { const auth = await checkSessionOrInternalAuth(request) if (!auth.success || !auth.userId) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } const parsed = await parseRequest(confluenceDeleteSpaceContract, request, {}) if (!parsed.success) return parsed.response const { domain, accessToken, spaceId, cloudId: providedCloudId } = parsed.data.body if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) } if (!accessToken) { return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) } if (!spaceId) { return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) } const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) if (!spaceIdValidation.isValid) { return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) } const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') if (!cloudIdValidation.isValid) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } const lookupUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` const lookupResponse = await fetch(lookupUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, }, }) if (!lookupResponse.ok) { const errorText = await lookupResponse.text() return NextResponse.json( { error: parseAtlassianErrorMessage( lookupResponse.status, lookupResponse.statusText, errorText ), }, { status: lookupResponse.status } ) } const currentSpace = await lookupResponse.json() const spaceKey = currentSpace.key const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/space/${encodeURIComponent(spaceKey)}` logger.info(`Deleting space ${spaceKey}`) const response = await fetch(url, { method: 'DELETE', headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, }, }) if (!response.ok) { const errorText = await response.text() logger.error('Confluence API error response:', { status: response.status, statusText: response.statusText, error: errorText, }) return NextResponse.json( { error: parseAtlassianErrorMessage(response.status, response.statusText, errorText) }, { status: response.status } ) } let longTask: { id?: string; statusLink?: string } = {} try { const text = await response.text() if (text) { const data = JSON.parse(text) longTask = { id: data?.id, statusLink: data?.links?.status, } } } catch { // 204 No Content or non-JSON body — ignore } return NextResponse.json({ spaceId, deleted: true, longTaskId: longTask.id, longTaskStatusLink: longTask.statusLink, }) } catch (error) { logger.error('Error deleting Confluence space:', error) return NextResponse.json( { error: (error as Error).message || 'Internal server error' }, { status: 500 } ) } })