@@ -38,6 +38,45 @@ const createBlogPostSchema = z.object({
3838 status : z . enum ( [ 'current' , 'draft' ] ) . optional ( ) ,
3939} )
4040
41+ const updateBlogPostSchema = z
42+ . object ( {
43+ domain : z . string ( ) . min ( 1 , 'Domain is required' ) ,
44+ accessToken : z . string ( ) . min ( 1 , 'Access token is required' ) ,
45+ cloudId : z . string ( ) . optional ( ) ,
46+ blogPostId : z . string ( ) . min ( 1 , 'Blog post ID is required' ) ,
47+ title : z . string ( ) . optional ( ) ,
48+ content : z . string ( ) . optional ( ) ,
49+ status : z . enum ( [ 'current' , 'draft' ] ) . optional ( ) ,
50+ } )
51+ . refine (
52+ ( data ) => {
53+ const validation = validateAlphanumericId ( data . blogPostId , 'blogPostId' , 255 )
54+ return validation . isValid
55+ } ,
56+ ( data ) => {
57+ const validation = validateAlphanumericId ( data . blogPostId , 'blogPostId' , 255 )
58+ return { message : validation . error || 'Invalid blog post ID' , path : [ 'blogPostId' ] }
59+ }
60+ )
61+
62+ const deleteBlogPostSchema = z
63+ . object ( {
64+ domain : z . string ( ) . min ( 1 , 'Domain is required' ) ,
65+ accessToken : z . string ( ) . min ( 1 , 'Access token is required' ) ,
66+ cloudId : z . string ( ) . optional ( ) ,
67+ blogPostId : z . string ( ) . min ( 1 , 'Blog post ID is required' ) ,
68+ } )
69+ . refine (
70+ ( data ) => {
71+ const validation = validateAlphanumericId ( data . blogPostId , 'blogPostId' , 255 )
72+ return validation . isValid
73+ } ,
74+ ( data ) => {
75+ const validation = validateAlphanumericId ( data . blogPostId , 'blogPostId' , 255 )
76+ return { message : validation . error || 'Invalid blog post ID' , path : [ 'blogPostId' ] }
77+ }
78+ )
79+
4180/**
4281 * List all blog posts or get a specific blog post
4382 */
@@ -283,3 +322,174 @@ export async function POST(request: NextRequest) {
283322 )
284323 }
285324}
325+
326+ /**
327+ * Update a blog post
328+ */
329+ export async function PUT ( request : NextRequest ) {
330+ try {
331+ const auth = await checkSessionOrInternalAuth ( request )
332+ if ( ! auth . success || ! auth . userId ) {
333+ return NextResponse . json ( { error : auth . error || 'Unauthorized' } , { status : 401 } )
334+ }
335+
336+ const body = await request . json ( )
337+
338+ const validation = updateBlogPostSchema . safeParse ( body )
339+ if ( ! validation . success ) {
340+ const firstError = validation . error . errors [ 0 ]
341+ return NextResponse . json ( { error : firstError . message } , { status : 400 } )
342+ }
343+
344+ const {
345+ domain,
346+ accessToken,
347+ cloudId : providedCloudId ,
348+ blogPostId,
349+ title,
350+ content,
351+ status,
352+ } = validation . data
353+
354+ const cloudId = providedCloudId || ( await getConfluenceCloudId ( domain , accessToken ) )
355+
356+ const cloudIdValidation = validateJiraCloudId ( cloudId , 'cloudId' )
357+ if ( ! cloudIdValidation . isValid ) {
358+ return NextResponse . json ( { error : cloudIdValidation . error } , { status : 400 } )
359+ }
360+
361+ const blogPostUrl = `https://api.atlassian.com/ex/confluence/${ cloudId } /wiki/api/v2/blogposts/${ blogPostId } `
362+
363+ const currentResponse = await fetch ( blogPostUrl , {
364+ headers : {
365+ Accept : 'application/json' ,
366+ Authorization : `Bearer ${ accessToken } ` ,
367+ } ,
368+ } )
369+
370+ if ( ! currentResponse . ok ) {
371+ const errorData = await currentResponse . json ( ) . catch ( ( ) => null )
372+ const errorMessage =
373+ errorData ?. message || `Failed to fetch blog post for update (${ currentResponse . status } )`
374+ return NextResponse . json ( { error : errorMessage } , { status : currentResponse . status } )
375+ }
376+
377+ const currentPost = await currentResponse . json ( )
378+ const currentVersion = currentPost . version . number
379+
380+ const updateBody : Record < string , unknown > = {
381+ id : blogPostId ,
382+ version : {
383+ number : currentVersion + 1 ,
384+ message : 'Updated via Sim' ,
385+ } ,
386+ status : status || currentPost . status || 'current' ,
387+ title : title || currentPost . title ,
388+ }
389+
390+ if ( content ) {
391+ updateBody . body = {
392+ representation : 'storage' ,
393+ value : content ,
394+ }
395+ }
396+
397+ const response = await fetch ( blogPostUrl , {
398+ method : 'PUT' ,
399+ headers : {
400+ Accept : 'application/json' ,
401+ 'Content-Type' : 'application/json' ,
402+ Authorization : `Bearer ${ accessToken } ` ,
403+ } ,
404+ body : JSON . stringify ( updateBody ) ,
405+ } )
406+
407+ if ( ! response . ok ) {
408+ const errorData = await response . json ( ) . catch ( ( ) => null )
409+ logger . error ( 'Confluence API error response:' , {
410+ status : response . status ,
411+ statusText : response . statusText ,
412+ error : JSON . stringify ( errorData , null , 2 ) ,
413+ } )
414+ const errorMessage = errorData ?. message || `Failed to update blog post (${ response . status } )`
415+ return NextResponse . json ( { error : errorMessage } , { status : response . status } )
416+ }
417+
418+ const data = await response . json ( )
419+ return NextResponse . json ( {
420+ id : data . id ,
421+ title : data . title ,
422+ status : data . status ?? null ,
423+ spaceId : data . spaceId ?? null ,
424+ authorId : data . authorId ?? null ,
425+ createdAt : data . createdAt ?? null ,
426+ version : data . version ?? null ,
427+ body : data . body ?? null ,
428+ webUrl : data . _links ?. webui ?? null ,
429+ } )
430+ } catch ( error ) {
431+ logger . error ( 'Error updating blog post:' , error )
432+ return NextResponse . json (
433+ { error : ( error as Error ) . message || 'Internal server error' } ,
434+ { status : 500 }
435+ )
436+ }
437+ }
438+
439+ /**
440+ * Delete a blog post
441+ */
442+ export async function DELETE ( request : NextRequest ) {
443+ try {
444+ const auth = await checkSessionOrInternalAuth ( request )
445+ if ( ! auth . success || ! auth . userId ) {
446+ return NextResponse . json ( { error : auth . error || 'Unauthorized' } , { status : 401 } )
447+ }
448+
449+ const body = await request . json ( )
450+
451+ const validation = deleteBlogPostSchema . safeParse ( body )
452+ if ( ! validation . success ) {
453+ const firstError = validation . error . errors [ 0 ]
454+ return NextResponse . json ( { error : firstError . message } , { status : 400 } )
455+ }
456+
457+ const { domain, accessToken, cloudId : providedCloudId , blogPostId } = validation . data
458+
459+ const cloudId = providedCloudId || ( await getConfluenceCloudId ( domain , accessToken ) )
460+
461+ const cloudIdValidation = validateJiraCloudId ( cloudId , 'cloudId' )
462+ if ( ! cloudIdValidation . isValid ) {
463+ return NextResponse . json ( { error : cloudIdValidation . error } , { status : 400 } )
464+ }
465+
466+ const url = `https://api.atlassian.com/ex/confluence/${ cloudId } /wiki/api/v2/blogposts/${ blogPostId } `
467+
468+ const response = await fetch ( url , {
469+ method : 'DELETE' ,
470+ headers : {
471+ Accept : 'application/json' ,
472+ Authorization : `Bearer ${ accessToken } ` ,
473+ } ,
474+ } )
475+
476+ if ( ! response . ok ) {
477+ const errorData = await response . json ( ) . catch ( ( ) => null )
478+ logger . error ( 'Confluence API error response:' , {
479+ status : response . status ,
480+ statusText : response . statusText ,
481+ error : JSON . stringify ( errorData , null , 2 ) ,
482+ } )
483+ const errorMessage = errorData ?. message || `Failed to delete blog post (${ response . status } )`
484+ return NextResponse . json ( { error : errorMessage } , { status : response . status } )
485+ }
486+
487+ return NextResponse . json ( { blogPostId, deleted : true } )
488+ } catch ( error ) {
489+ logger . error ( 'Error deleting blog post:' , error )
490+ return NextResponse . json (
491+ { error : ( error as Error ) . message || 'Internal server error' } ,
492+ { status : 500 }
493+ )
494+ }
495+ }
0 commit comments