@@ -79,7 +79,7 @@ export async function updateProjectAction(
7979 formData : FormData
8080) : Promise < { error ?: string ; errors ?: Record < string , string [ ] | undefined > ; message ?: string ; project ?: Project } > {
8181 const session = await auth ( ) ;
82- if ( ! session ?. user ?. uuid ) {
82+ if ( ! session ?. user ?. uuid || ! session . user . name ) {
8383 console . error ( "[updateProjectAction] Authentication required. No session user UUID." ) ;
8484 return { error : "Authentication required." } ;
8585 }
@@ -114,6 +114,25 @@ export async function updateProjectAction(
114114 if ( ! updatedProject ) {
115115 return { error : 'Failed to update project or project not found.' } ;
116116 }
117+
118+ if ( project . discordWebhookUrl && project . discordNotificationsEnabled && project . discordNotifySettings ) {
119+ const changedFields = [ ] ;
120+ if ( project . name !== updatedProject . name ) changedFields . push ( `Name: from "**${ project . name } **" to "**${ updatedProject . name } **"` ) ;
121+ if ( project . description !== updatedProject . description ) changedFields . push ( "Description updated" ) ;
122+ if ( changedFields . length > 0 ) {
123+ await sendDiscordNotification ( project . discordWebhookUrl , {
124+ embeds : [ {
125+ title : "🛠️ Project Details Updated" ,
126+ description : `**${ session . user . name } ** updated the project details:\n- ${ changedFields . join ( '\n- ' ) } ` ,
127+ color : 15105570 , // Orange
128+ timestamp : new Date ( ) . toISOString ( ) ,
129+ footer : { text : `Project: ${ updatedProject . name } ` }
130+ } ]
131+ } ) ;
132+ }
133+ }
134+
135+
117136 return { message : 'Project updated successfully!' , project : updatedProject } ;
118137 } catch ( error : any ) {
119138 console . error ( 'Failed to update project (server action):' , error ) ;
@@ -276,7 +295,7 @@ export async function fetchProjectMembersAction(projectUuid: string | undefined)
276295
277296export async function removeUserFromProjectAction ( projectUuid : string , userUuidToRemove : string ) : Promise < { success ?: boolean , error ?: string , message ?: string } > {
278297 const session = await auth ( ) ;
279- if ( ! session ?. user ?. uuid ) {
298+ if ( ! session ?. user ?. uuid || ! session . user . name ) {
280299 console . error ( "[removeUserFromProjectAction] Authentication required. No session user UUID." ) ;
281300 return { error : "Authentication required." } ;
282301 }
@@ -299,8 +318,19 @@ export async function removeUserFromProjectAction(projectUuid: string, userUuidT
299318 return { error : "Project owner cannot be removed. Transfer ownership first." } ;
300319 }
301320
302- const success = await dbRemoveProjectMember ( projectUuid , userUuidToRemove ) ;
303- if ( success ) {
321+ const removalResult = await dbRemoveProjectMember ( projectUuid , userUuidToRemove ) ;
322+ if ( removalResult . success ) {
323+ if ( project . discordWebhookUrl && project . discordNotificationsEnabled && project . discordNotifyMembers ) {
324+ await sendDiscordNotification ( project . discordWebhookUrl , {
325+ embeds : [ {
326+ title : "👤 Member Removed" ,
327+ description : `**${ removalResult . userRemoved ?. name || 'A user' } ** was removed from the project by **${ session . user . name } **.` ,
328+ color : 15158332 , // Red
329+ timestamp : new Date ( ) . toISOString ( ) ,
330+ footer : { text : `Project: ${ project . name } ` }
331+ } ]
332+ } ) ;
333+ }
304334 return { success : true , message : "User removed from project successfully." } ;
305335 }
306336 return { error : "Failed to remove user from project." } ;
@@ -435,7 +465,7 @@ const UpdateTaskSchema = z.object({
435465
436466export async function updateTaskAction ( prevState : UpdateTaskFormState , formData : FormData ) : Promise < UpdateTaskFormState > {
437467 const session = await auth ( ) ;
438- if ( ! session ?. user ?. uuid ) {
468+ if ( ! session ?. user ?. uuid || ! session . user . name ) {
439469 console . error ( "[updateTaskAction] Authentication required. No session user UUID." ) ;
440470 return { error : "Authentication required." } ;
441471 }
@@ -468,6 +498,10 @@ export async function updateTaskAction(prevState: UpdateTaskFormState, formData:
468498
469499
470500 try {
501+ const project = await dbGetProjectByUuid ( projectUuid ) ;
502+ const oldTask = await dbGetTaskByUuid ( taskUuid ) ;
503+ if ( ! project || ! oldTask ) return { error : "Project or task not found." } ;
504+
471505 const userRole = await dbGetProjectMemberRole ( projectUuid , session . user . uuid ) ;
472506 console . log ( `[updateTaskAction] User role check for project ${ projectUuid } (user ${ session . user . uuid } ): ${ userRole } ` ) ;
473507
@@ -500,6 +534,33 @@ export async function updateTaskAction(prevState: UpdateTaskFormState, formData:
500534 if ( ! updatedTask ) {
501535 return { error : "Failed to update task." } ;
502536 }
537+
538+ if ( project . discordWebhookUrl && project . discordNotificationsEnabled && project . discordNotifyTasks ) {
539+ const changedFields : string [ ] = [ ] ;
540+ if ( oldTask . status !== updatedTask . status ) {
541+ changedFields . push ( `Status changed from **${ oldTask . status } ** to **${ updatedTask . status } **` ) ;
542+ }
543+ if ( oldTask . todoListMarkdown !== updatedTask . todoListMarkdown ) {
544+ changedFields . push ( "Sub-tasks were modified." ) ;
545+ }
546+ if ( oldTask . title !== updatedTask . title ) {
547+ changedFields . push ( `Title changed to **${ updatedTask . title } **` ) ;
548+ }
549+
550+ if ( changedFields . length > 0 ) {
551+ await sendDiscordNotification ( project . discordWebhookUrl , {
552+ embeds : [ {
553+ title : `🔄 Task Updated: ${ updatedTask . title } ` ,
554+ description : `**${ session . user . name } ** updated a task:\n- ${ changedFields . join ( '\n- ' ) } ` ,
555+ color : 3447003 , // Blue
556+ timestamp : new Date ( ) . toISOString ( ) ,
557+ footer : { text : `Project: ${ project . name } ` }
558+ } ]
559+ } ) ;
560+ }
561+ }
562+
563+
503564 return { message : "Task updated successfully!" , updatedTask } ;
504565 } catch ( error : any ) {
505566 console . error ( "Error updating task:" , error ) ;
@@ -769,7 +830,7 @@ export interface ToggleProjectUrgencyFormState {
769830
770831export async function toggleProjectUrgencyAction ( prevState : ToggleProjectUrgencyFormState , formData : FormData ) : Promise < ToggleProjectUrgencyFormState > {
771832 const session = await auth ( ) ;
772- if ( ! session ?. user ?. uuid ) {
833+ if ( ! session ?. user ?. uuid || ! session . user . name ) {
773834 console . error ( "[toggleProjectUrgencyAction] Authentication required. No session user UUID." ) ;
774835 return { error : "Authentication required." } ;
775836 }
@@ -791,6 +852,19 @@ export async function toggleProjectUrgencyAction(prevState: ToggleProjectUrgency
791852 if ( ! updatedProject ) {
792853 return { error : "Failed to update project urgency." } ;
793854 }
855+
856+ if ( updatedProject . discordWebhookUrl && updatedProject . discordNotificationsEnabled && updatedProject . discordNotifySettings ) {
857+ await sendDiscordNotification ( updatedProject . discordWebhookUrl , {
858+ embeds : [ {
859+ title : isUrgent ? "🔥 Project Marked as Urgent" : "Project No Longer Urgent" ,
860+ description : `**${ session . user . name } ** updated the project status.` ,
861+ color : isUrgent ? 15158332 : 5763719 ,
862+ timestamp : new Date ( ) . toISOString ( ) ,
863+ footer : { text : `Project: ${ updatedProject . name } ` }
864+ } ]
865+ } ) ;
866+ }
867+
794868 return { message : `Project urgency ${ isUrgent ? 'set' : 'unset' } .` , project : updatedProject } ;
795869 } catch ( error : any ) {
796870 return { error : error . message || "An unexpected error occurred." } ;
@@ -805,7 +879,7 @@ export interface ToggleProjectVisibilityFormState {
805879
806880export async function toggleProjectVisibilityAction ( prevState : ToggleProjectVisibilityFormState , formData : FormData ) : Promise < ToggleProjectVisibilityFormState > {
807881 const session = await auth ( ) ;
808- if ( ! session ?. user ?. uuid ) {
882+ if ( ! session ?. user ?. uuid || ! session . user . name ) {
809883 console . error ( "[toggleProjectVisibilityAction] Authentication required. No session user UUID." ) ;
810884 return { error : "Authentication required." } ;
811885 }
@@ -833,6 +907,18 @@ export async function toggleProjectVisibilityAction(prevState: ToggleProjectVisi
833907 if ( ! updatedProjectInDb ) {
834908 return { error : "Failed to update project visibility in FlowUp." } ;
835909 }
910+ if ( updatedProjectInDb . discordWebhookUrl && updatedProjectInDb . discordNotificationsEnabled && updatedProjectInDb . discordNotifySettings ) {
911+ await sendDiscordNotification ( updatedProjectInDb . discordWebhookUrl , {
912+ embeds : [ {
913+ title : isPrivate ? "🔒 Project is now Private" : "🌍 Project is now Public" ,
914+ description : `**${ session . user . name } ** updated the project's visibility.` ,
915+ color : isPrivate ? 15105570 : 3447003 ,
916+ timestamp : new Date ( ) . toISOString ( ) ,
917+ footer : { text : `Project: ${ updatedProjectInDb . name } ` }
918+ } ]
919+ } ) ;
920+ }
921+
836922
837923 if ( updatedProjectInDb . githubRepoUrl && updatedProjectInDb . githubRepoName ) {
838924 const oauthToken = await dbGetUserGithubOAuthToken ( session . user . uuid ) ;
@@ -957,7 +1043,7 @@ const CreateDocumentSchema = z.object({
9571043
9581044export async function createDocumentAction ( prevState : CreateDocumentFormState , formData : FormData ) : Promise < CreateDocumentFormState > {
9591045 const session = await auth ( ) ;
960- if ( ! session ?. user ?. uuid ) return { error : "Authentication required." } ;
1046+ if ( ! session ?. user ?. uuid || ! session . user . name ) return { error : "Authentication required." } ;
9611047
9621048 const validatedFields = CreateDocumentSchema . safeParse ( {
9631049 projectUuid : formData . get ( 'projectUuid' ) ,
@@ -983,6 +1069,20 @@ export async function createDocumentAction(prevState: CreateDocumentFormState, f
9831069 fileType : 'markdown' ,
9841070 createdByUuid : session . user . uuid ,
9851071 } ) ;
1072+
1073+ const project = await dbGetProjectByUuid ( projectUuid ) ;
1074+ if ( project ?. discordWebhookUrl && project . discordNotificationsEnabled && project . discordNotifyDocuments ) {
1075+ await sendDiscordNotification ( project . discordWebhookUrl , {
1076+ embeds : [ {
1077+ title : "📄 New Document Created" ,
1078+ description : `**${ session . user . name } ** created a new document titled **${ createdDocument . title } **.` ,
1079+ color : 8421504 , // Gray
1080+ timestamp : new Date ( ) . toISOString ( ) ,
1081+ footer : { text : `Project: ${ project . name } ` }
1082+ } ]
1083+ } ) ;
1084+ }
1085+
9861086 return { message : "Document created successfully!" , createdDocument } ;
9871087 } catch ( error : any ) {
9881088 console . error ( "Error creating document:" , error ) ;
@@ -1065,25 +1165,38 @@ export interface DeleteDocumentFormState {
10651165}
10661166export async function deleteDocumentAction ( prevState : DeleteDocumentFormState , formData : FormData ) : Promise < DeleteDocumentFormState > {
10671167 const session = await auth ( ) ;
1068- if ( ! session ?. user ?. uuid ) return { error : "Authentication required." } ;
1168+ if ( ! session ?. user ?. uuid || ! session . user . name ) return { error : "Authentication required." } ;
10691169
10701170 const documentUuid = formData . get ( 'documentUuid' ) as string ;
10711171 const projectUuid = formData . get ( 'projectUuid' ) as string ;
10721172
10731173 if ( ! documentUuid ) return { error : "Document UUID is required." } ;
10741174
10751175 try {
1076- if ( projectUuid ) {
1077- const userRole = await dbGetProjectMemberRole ( projectUuid , session . user . uuid ) ;
1078- if ( ! userRole || ! [ 'owner' , 'co-owner' , 'editor' ] . includes ( userRole ) ) {
1079- return { error : "You do not have permission to delete documents in this project." } ;
1080- }
1081- } else {
1082- return { error : "Project context is required for permission check." } ;
1176+ const project = await dbGetProjectByUuid ( projectUuid ) ;
1177+ if ( ! project ) return { error : "Project context not found." } ;
1178+
1179+ const userRole = await dbGetProjectMemberRole ( projectUuid , session . user . uuid ) ;
1180+ if ( ! userRole || ! [ 'owner' , 'co-owner' , 'editor' ] . includes ( userRole ) ) {
1181+ return { error : "You do not have permission to delete documents in this project." } ;
10831182 }
1183+
1184+ const docToDelete = await getDocumentByUuid ( documentUuid ) ;
1185+ if ( ! docToDelete ) return { error : "Document not found." } ;
10841186
10851187 const success = await dbDeleteDocument ( documentUuid ) ;
10861188 if ( success ) {
1189+ if ( project . discordWebhookUrl && project . discordNotificationsEnabled && project . discordNotifyDocuments ) {
1190+ await sendDiscordNotification ( project . discordWebhookUrl , {
1191+ embeds : [ {
1192+ title : "🗑️ Document Deleted" ,
1193+ description : `**${ session . user . name } ** deleted the document titled **${ docToDelete . title } **.` ,
1194+ color : 15158332 , // Red
1195+ timestamp : new Date ( ) . toISOString ( ) ,
1196+ footer : { text : `Project: ${ project . name } ` }
1197+ } ]
1198+ } ) ;
1199+ }
10871200 return { message : "Document deleted successfully." } ;
10881201 }
10891202 return { error : "Failed to delete document." } ;
@@ -1944,6 +2057,8 @@ const updateDiscordSettingsSchema = z.object({
19442057 discordNotifyTasks : z . enum ( [ 'true' , 'false' ] ) . transform ( v => v === 'true' ) . optional ( ) ,
19452058 discordNotifyMembers : z . enum ( [ 'true' , 'false' ] ) . transform ( v => v === 'true' ) . optional ( ) ,
19462059 discordNotifyAnnouncements : z . enum ( [ 'true' , 'false' ] ) . transform ( v => v === 'true' ) . optional ( ) ,
2060+ discordNotifyDocuments : z . enum ( [ 'true' , 'false' ] ) . transform ( v => v === 'true' ) . optional ( ) ,
2061+ discordNotifySettings : z . enum ( [ 'true' , 'false' ] ) . transform ( v => v === 'true' ) . optional ( ) ,
19472062} ) ;
19482063
19492064
@@ -1963,21 +2078,23 @@ export async function updateProjectDiscordSettingsAction(
19632078 discordNotifyTasks : formData . get ( 'discordNotifyTasks' ) ,
19642079 discordNotifyMembers : formData . get ( 'discordNotifyMembers' ) ,
19652080 discordNotifyAnnouncements : formData . get ( 'discordNotifyAnnouncements' ) ,
2081+ discordNotifyDocuments : formData . get ( 'discordNotifyDocuments' ) ,
2082+ discordNotifySettings : formData . get ( 'discordNotifySettings' ) ,
19662083 } ) ;
19672084
19682085 if ( ! validatedFields . success ) {
19692086 return { error : "Invalid input: " + validatedFields . error . flatten ( ) . fieldErrors . discordWebhookUrl ?. join ( ', ' ) } ;
19702087 }
19712088
1972- const { projectUuid, discordWebhookUrl, discordNotificationsEnabled, discordNotifyTasks, discordNotifyMembers, discordNotifyAnnouncements } = validatedFields . data ;
2089+ const { projectUuid, discordWebhookUrl, discordNotificationsEnabled, discordNotifyTasks, discordNotifyMembers, discordNotifyAnnouncements, discordNotifyDocuments , discordNotifySettings } = validatedFields . data ;
19732090
19742091 try {
19752092 const userRole = await dbGetProjectMemberRole ( projectUuid , session . user . uuid ) ;
19762093 if ( ! userRole || ! [ 'owner' , 'co-owner' ] . includes ( userRole ) ) {
19772094 return { error : "You do not have permission to change Discord settings for this project." } ;
19782095 }
19792096
1980- const updatedProject = await dbUpdateProjectDiscordSettings ( projectUuid , discordWebhookUrl , discordNotificationsEnabled , discordNotifyTasks , discordNotifyMembers , discordNotifyAnnouncements ) ;
2097+ const updatedProject = await dbUpdateProjectDiscordSettings ( projectUuid , discordWebhookUrl , discordNotificationsEnabled , discordNotifyTasks , discordNotifyMembers , discordNotifyAnnouncements , discordNotifyDocuments , discordNotifySettings ) ;
19812098
19822099 if ( ! updatedProject ) {
19832100 return { error : "Failed to update project settings in the database." } ;
@@ -2088,17 +2205,21 @@ export async function setupGithubWebhookAction(
20882205
20892206 const updatedProject = await dbUpdateProjectWebhookDetails ( project . uuid , newWebhook . id , webhookSecret ) ;
20902207 if ( ! updatedProject ) {
2091- return { error : "Webhook created on GitHub, but failed to save details in FlowUp." } ;
2208+ // Best effort to clean up if DB write fails
2209+ await octokit . rest . repos . deleteWebhook ( { owner, repo, hook_id : newWebhook . id } ) ;
2210+ return { error : "Webhook created on GitHub, but failed to save details in FlowUp. The webhook on GitHub has been removed." } ;
20922211 }
20932212
20942213 return { success : true , message : `Webhook (ID: ${ newWebhook . id } ) successfully created on GitHub!` , project : updatedProject } ;
20952214
20962215 } catch ( error : any ) {
20972216 console . error ( "Error setting up GitHub webhook:" , error ) ;
20982217 let errorMessage = error . message || "An unexpected error occurred." ;
2099- if ( error . status === 422 ) { // Unprocessable Entity
2100- if ( error . message . includes ( "Hook already exists" ) ) {
2218+ if ( error . status === 422 ) {
2219+ if ( error . message . includes ( "Hook already exists" ) ) {
21012220 errorMessage = "A webhook for this URL already exists on the repository. Please check your repository settings on GitHub." ;
2221+ } else if ( error . message . includes ( "not supported because it isn't reachable" ) ) {
2222+ errorMessage = "GitHub couldn't reach your webhook URL. If you are developing locally, your `localhost` is not accessible from the public internet. Please use a tunneling service like ngrok to expose your local server, then update your NEXT_PUBLIC_APP_URL in the .env file." ;
21022223 } else {
21032224 errorMessage = `Could not create webhook (422): ${ error . message } . Check permissions and configuration.` ;
21042225 }
0 commit comments