11
22'use server' ;
33
4- import type { Project , ProjectMember , ProjectMemberRole , Task , TaskStatus , Tag , Document as ProjectDocumentType , Announcement as ProjectAnnouncement , UserGithubOAuthToken , GithubRepoContentItem } from '@/types' ;
4+ import type { Project , ProjectMember , ProjectMemberRole , Task , TaskStatus , Tag , Document as ProjectDocumentType , Announcement as ProjectAnnouncement , UserGithubOAuthToken , GithubRepoContentItem , User } from '@/types' ;
55import {
66 getProjectByUuid as dbGetProjectByUuid ,
77 getUserByUuid as dbGetUserByUuid ,
@@ -34,6 +34,8 @@ import {
3434 getUserGithubOAuthToken as dbGetUserGithubOAuthToken ,
3535 deleteUserGithubOAuthToken as dbDeleteUserGithubOAuthToken ,
3636 getTaskByUuid as dbGetTaskByUuid ,
37+ // Make sure this is exported if it's a new function or intended to be used
38+ // getUserGithubLoginByUuid as dbGetUserGithubLoginByUuid
3739} from '@/lib/db' ;
3840import { z } from 'zod' ;
3941import { auth } from '@/lib/authEdge' ;
@@ -136,7 +138,8 @@ export async function inviteUserToProjectAction(
136138 console . error ( "[inviteUserToProjectAction] Authentication required. No session user UUID." ) ;
137139 return { error : "Authentication required." } ;
138140 }
139- console . log ( "[inviteUserToProjectAction] Authenticated user for permission check:" , session . user . uuid ) ;
141+ const inviterUserUuid = session . user . uuid ;
142+ console . log ( "[inviteUserToProjectAction] Authenticated user for permission check:" , inviterUserUuid ) ;
140143
141144 const validatedFields = InviteUserSchema . safeParse ( {
142145 projectUuid : formData . get ( 'projectUuid' ) ,
@@ -156,10 +159,10 @@ export async function inviteUserToProjectAction(
156159 const project = await dbGetProjectByUuid ( projectUuid ) ;
157160 if ( ! project ) return { error : "Project not found." } ;
158161
159- const inviterRole = await dbGetProjectMemberRole ( projectUuid , session . user . uuid ) ;
160- console . log ( `[inviteUserToProjectAction] Inviter role check for project ${ projectUuid } (user ${ session . user . uuid } ): ${ inviterRole } ` ) ;
162+ const inviterRole = await dbGetProjectMemberRole ( projectUuid , inviterUserUuid ) ;
163+ console . log ( `[inviteUserToProjectAction] Inviter role check for project ${ projectUuid } (user ${ inviterUserUuid } ): ${ inviterRole } ` ) ;
161164 if ( ! inviterRole || ! [ 'owner' , 'co-owner' ] . includes ( inviterRole ) ) {
162- return { error : `You do not have permission to invite users to this project. Your role: ${ inviterRole || 'not a member' } . UUID: ${ session . user . uuid } ` } ;
165+ return { error : `You do not have permission to invite users to this project. Your role: ${ inviterRole || 'not a member' } . UUID: ${ inviterUserUuid } ` } ;
163166 }
164167
165168 const userToInvite = await dbGetUserByEmail ( email ) ;
@@ -185,13 +188,75 @@ export async function inviteUserToProjectAction(
185188 if ( ! newOrUpdatedMember ) {
186189 return { error : "Failed to add or update user in the project." } ;
187190 }
188- return { message : `${ userToInvite . name } has been successfully ${ existingMembership ? 'updated to role' : 'invited as' } ${ role } .` , invitedMember : newOrUpdatedMember } ;
191+
192+ let githubMessage = "" ;
193+ if ( project . githubRepoName && project . githubRepoUrl ) {
194+ console . log ( "[inviteUserToProjectAction] Project is linked to GitHub. Attempting to add collaborator." ) ;
195+ const inviterOAuthToken = await dbGetUserGithubOAuthToken ( inviterUserUuid ) ;
196+
197+ // Attempt to get invited user's GitHub login details.
198+ // This part needs a way to get the GitHub login for userToInvite.uuid
199+ // For now, we'll try to fetch their details if they have an OAuth token.
200+ let invitedUserGithubLogin : string | undefined ;
201+ const invitedUserOAuthToken = await dbGetUserGithubOAuthToken ( userToInvite . uuid ) ;
202+ if ( invitedUserOAuthToken ?. accessToken ) {
203+ try {
204+ const invitedOctokit = new Octokit ( { auth : invitedUserOAuthToken . accessToken } ) ;
205+ const { data : invitedGithubUser } = await invitedOctokit . users . getAuthenticated ( ) ;
206+ invitedUserGithubLogin = invitedGithubUser . login ;
207+ console . log ( `[inviteUserToProjectAction] Fetched GitHub login for invited user: ${ invitedUserGithubLogin } ` ) ;
208+ } catch ( e : any ) {
209+ console . warn ( `[inviteUserToProjectAction] Could not fetch GitHub login for invited user ${ userToInvite . email } : ${ e . message } ` ) ;
210+ }
211+ } else {
212+ console . log ( `[inviteUserToProjectAction] Invited user ${ userToInvite . email } has not connected their GitHub account via OAuth.` ) ;
213+ }
214+
215+
216+ if ( inviterOAuthToken ?. accessToken && invitedUserGithubLogin ) {
217+ const octokit = new Octokit ( { auth : inviterOAuthToken . accessToken } ) ;
218+ const [ owner , repo ] = project . githubRepoName . split ( '/' ) ;
219+ if ( owner && repo ) {
220+ try {
221+ // Map FlowUp role to GitHub permission
222+ let githubPermission : 'pull' | 'push' | 'admin' | 'maintain' | 'triage' = 'pull' ; // Default to read
223+ if ( role === 'editor' || role === 'co-owner' ) githubPermission = 'push' ; // Write access
224+ if ( role === 'owner' || role === 'co-owner' ) githubPermission = 'admin' ; // Admin access for co-owners (if desired)
225+ // Note: 'owner' role in FlowUp might not directly map to GitHub repo owner, but 'admin' permission is closest for co-owners.
226+
227+ await octokit . rest . repos . addCollaborator ( {
228+ owner,
229+ repo,
230+ username : invitedUserGithubLogin ,
231+ permission : githubPermission ,
232+ } ) ;
233+ githubMessage = ` User also invited as a collaborator to the GitHub repository with '${ githubPermission } ' permission.` ;
234+ console . log ( `[inviteUserToProjectAction] Successfully added ${ invitedUserGithubLogin } to ${ owner } /${ repo } with ${ githubPermission } permission.` ) ;
235+ } catch ( githubError : any ) {
236+ githubMessage = ` Failed to add user to GitHub repository: ${ githubError . message } . Please add them manually if needed.` ;
237+ console . error ( `[inviteUserToProjectAction] Error adding collaborator to GitHub: ${ githubError . status } ${ githubError . message } ` , githubError . response ?. data ) ;
238+ }
239+ } else {
240+ githubMessage = " Could not determine GitHub repository owner/name to add collaborator." ;
241+ console . warn ( "[inviteUserToProjectAction] Invalid project.githubRepoName format:" , project . githubRepoName ) ;
242+ }
243+ } else if ( ! inviterOAuthToken ?. accessToken ) {
244+ githubMessage = " Inviter has not connected their GitHub account, cannot add collaborator to repository." ;
245+ console . warn ( "[inviteUserToProjectAction] Inviter has no GitHub OAuth token." ) ;
246+ } else if ( ! invitedUserGithubLogin ) {
247+ githubMessage = ` Invited user (${ userToInvite . email } ) has not connected their GitHub account or their GitHub login could not be determined. Cannot add as collaborator automatically.` ;
248+ console . warn ( "[inviteUserToProjectAction] Could not determine GitHub login for invited user." ) ;
249+ }
250+ }
251+
252+ return { message : `${ userToInvite . name } has been successfully ${ existingMembership ? 'updated to role' : 'invited as' } ${ role } .${ githubMessage } ` , invitedMember : newOrUpdatedMember } ;
189253 } catch ( error : any ) {
190254 console . error ( "Error inviting user:" , error ) ;
191255 return { error : error . message || "An unexpected error occurred while inviting the user." } ;
192256 }
193257}
194258
259+
195260export async function fetchProjectMembersAction ( projectUuid : string | undefined ) : Promise < ProjectMember [ ] > {
196261 if ( ! projectUuid ) return [ ] ;
197262 try {
@@ -229,6 +294,7 @@ export async function removeUserFromProjectAction(projectUuid: string, userUuidT
229294
230295 const success = await dbRemoveProjectMember ( projectUuid , userUuidToRemove ) ;
231296 if ( success ) {
297+ // TODO: Optionally, try to remove from GitHub repo collaborators if conditions met
232298 return { success : true , message : "User removed from project successfully." } ;
233299 }
234300 return { error : "Failed to remove user from project." } ;
@@ -568,8 +634,8 @@ async function updateReadmeOnGithub(octokit: Octokit, owner: string, repo: strin
568634 message : 'Update README.md from FlowUp' ,
569635 content : Buffer . from ( content ) . toString ( 'base64' ) ,
570636 committer : {
571- name : 'FlowUp Bot' ,
572- email : 'bot@flowup.app' ,
637+ name : 'FlowUp Bot' , // Or use authenticated user's name/email if preferred & available
638+ email : 'bot@flowup.app' , // Or user's email
573639 } ,
574640 } ;
575641 if ( existingSha ) {
@@ -1121,24 +1187,14 @@ export async function fetchUserGithubOAuthTokenAction(): Promise<UserGithubOAuth
11211187 }
11221188}
11231189
1124- export async function disconnectGithubAction ( ) : Promise < { success : boolean ; error ?: string ; message ?: string } > {
1125- const session = await auth ( ) ;
1126- if ( ! session ?. user ?. uuid ) {
1127- return { success : false , error : "Authentication required." } ;
1128- }
1129- try {
1130- const success = await dbDeleteUserGithubOAuthToken ( session . user . uuid ) ;
1131- if ( success ) {
1132- return { success : true , message : "GitHub account disconnected successfully." } ;
1133- }
1134- return { success : false , error : "Failed to disconnect GitHub account from database." } ;
1135- } catch ( error : any ) {
1136- console . error ( "[disconnectGithubAction] Error:" , error ) ;
1137- return { success : false , error : error . message || "An unexpected error occurred." } ;
1138- }
1190+ export interface GithubUserDetails {
1191+ login : string ;
1192+ avatar_url : string ;
1193+ html_url : string ;
1194+ name : string | null ;
11391195}
11401196
1141- export async function fetchGithubUserDetailsAction ( ) : Promise < { login : string ; avatar_url : string ; html_url : string ; name : string | null } | null > {
1197+ export async function fetchGithubUserDetailsAction ( ) : Promise < GithubUserDetails | null > {
11421198 const session = await auth ( ) ;
11431199 if ( ! session ?. user ?. uuid ) {
11441200 console . error ( "[fetchGithubUserDetailsAction] Authentication required." ) ;
@@ -1165,6 +1221,24 @@ export async function fetchGithubUserDetailsAction(): Promise<{ login: string; a
11651221}
11661222
11671223
1224+ export async function disconnectGithubAction ( formData ?: FormData ) : Promise < { success : boolean ; error ?: string ; message ?: string } > {
1225+ const session = await auth ( ) ;
1226+ if ( ! session ?. user ?. uuid ) {
1227+ return { success : false , error : "Authentication required." } ;
1228+ }
1229+ try {
1230+ const success = await dbDeleteUserGithubOAuthToken ( session . user . uuid ) ;
1231+ if ( success ) {
1232+ return { success : true , message : "GitHub account disconnected successfully." } ;
1233+ }
1234+ return { success : false , error : "Failed to disconnect GitHub account from database." } ;
1235+ } catch ( error : any ) {
1236+ console . error ( "[disconnectGithubAction] Error:" , error ) ;
1237+ return { success : false , error : error . message || "An unexpected error occurred." } ;
1238+ }
1239+ }
1240+
1241+
11681242function sanitizeRepoName ( name : string | null | undefined ) : string {
11691243 if ( ! name ) {
11701244 return '' ;
@@ -1178,10 +1252,10 @@ function sanitizeRepoName(name: string | null | undefined): string {
11781252 . replace ( / \s + / g, '-' )
11791253 . replace ( / [ ^ a - z A - Z 0 - 9 _ . - ] / g, '' )
11801254 . replace ( / - + / g, '-' )
1181- . replace ( / ^ \. + | \. $ / g, '' )
1182- . replace ( / ^ - + | - + $ / g, '' ) ;
1255+ . replace ( / ^ \. + | \. $ / g, '' ) // Remove leading/trailing dots
1256+ . replace ( / ^ - + | - + $ / g, '' ) ; // Remove leading/trailing hyphens
11831257
1184- return sanitized . substring ( 0 , 100 ) ;
1258+ return sanitized . substring ( 0 , 100 ) ; // Max repo name length on GitHub
11851259}
11861260
11871261
@@ -1217,14 +1291,13 @@ export async function linkProjectToGithubAction(
12171291 if ( githubRepoNameValue && githubRepoNameValue . trim ( ) !== '' ) {
12181292 nameForRepoCreation = githubRepoNameValue ;
12191293 } else {
1220- // This case should ideally be caught by client-side validation, but good to have server-side too.
12211294 return { error : "Custom repository name cannot be empty when not using the default." } ;
12221295 }
12231296 }
12241297
12251298 const repoSlug = sanitizeRepoName ( nameForRepoCreation ) ;
12261299 if ( ! repoSlug ) {
1227- return { error : "Resulting repository name is invalid after sanitization. Please provide a valid name." } ;
1300+ return { error : "Resulting repository name is invalid after sanitization. Please provide a valid name (e.g., 'my-repo', 'My_Project-123'). Avoid special characters or names that are too short or only dots/hyphens ." } ;
12281301 }
12291302
12301303 const projectFromDb = await dbGetProjectByUuid ( projectUuid ) ;
@@ -1255,10 +1328,11 @@ export async function linkProjectToGithubAction(
12551328 name : repoSlug ,
12561329 private : repoIsPrivate ,
12571330 description : `Repository for FlowUp project: ${ projectFromDb . name } ` ,
1258- auto_init : true ,
1331+ auto_init : true , // Initialize with a README
12591332 } ) ;
12601333 console . log ( `Successfully created repository: ${ createdRepo . data . html_url } ` ) ;
12611334
1335+ // Sync README content from FlowUp to GitHub if it exists
12621336 if ( projectFromDb . readmeContent && projectFromDb . readmeContent . trim ( ) !== '' ) {
12631337 let existingReadmeSha : string | undefined = undefined ;
12641338 try {
@@ -1276,15 +1350,16 @@ export async function linkProjectToGithubAction(
12761350 if ( getContentError . status !== 404 ) {
12771351 console . warn ( `[linkProjectToGithubAction] Could not fetch existing README SHA for ${ createdRepo . data . owner . login } /${ createdRepo . data . name } :` , getContentError . message ) ;
12781352 }
1353+ // If README doesn't exist (404), existingReadmeSha remains undefined, which is fine for createOrUpdateFileContents.
12791354 }
12801355 await updateReadmeOnGithub ( octokit , createdRepo . data . owner . login , createdRepo . data . name , projectFromDb . readmeContent , existingReadmeSha ) ;
12811356 }
12821357
12831358 } catch ( apiError : any ) {
12841359 console . error ( `GitHub API error creating repository: ${ apiError . status } ${ apiError . message } ` , apiError . response ?. data ) ;
12851360 const errorMessage = apiError . response ?. data ?. message || apiError . message || 'Unknown GitHub API error.' ;
1286- if ( apiError . status === 403 ) { // Forbidden - usually permission issue or token scope
1287- return { error : `GitHub Permission Denied: ${ errorMessage } . Ensure your GitHub OAuth token has the 'repo' scope.` } ;
1361+ if ( apiError . status === 403 ) { // Forbidden
1362+ return { error : `GitHub Permission Denied: ${ errorMessage } . Ensure your GitHub OAuth token has the 'repo' scope and necessary permissions .` } ;
12881363 }
12891364 if ( apiError . status === 422 ) { // Unprocessable Entity - e.g., repo already exists
12901365 return { error : `Failed to create GitHub repository '${ repoSlug } '. It might already exist or there's a naming conflict. GitHub's message: ${ errorMessage } ` } ;
@@ -1293,12 +1368,14 @@ export async function linkProjectToGithubAction(
12931368 }
12941369
12951370 const repoUrl = createdRepo . data . html_url ;
1296- const actualRepoName = createdRepo . data . full_name ;
1371+ const actualRepoName = createdRepo . data . full_name ; // e.g., "username/repo-slug"
12971372
12981373 const updatedProject = await dbUpdateProjectGithubRepo ( projectUuid , repoUrl , actualRepoName ) ;
12991374
13001375 if ( ! updatedProject ) {
1301- return { error : "Failed to save GitHub repository details to FlowUp project after creation on GitHub." } ;
1376+ // This is problematic, the repo was created on GitHub but FlowUp failed to save it.
1377+ // Should ideally have a mechanism to delete the GitHub repo or notify admin.
1378+ return { error : "Failed to save GitHub repository details to FlowUp project after creation on GitHub. Please check project settings or contact support." } ;
13021379 }
13031380
13041381 return { message : `GitHub repository '${ actualRepoName } ' created and linked successfully!` , project : updatedProject } ;
@@ -1350,6 +1427,8 @@ export async function getRepoContentsAction(projectUuid: string, path: string =
13501427 } else if ( error . status === 401 ) { // Bad credentials
13511428 userMessage = `GitHub authentication failed. Your token might be invalid or expired. Please try reconnecting your GitHub account.` ;
13521429 await dbDeleteUserGithubOAuthToken ( session . user . uuid ) ; // Invalidate stored token
1430+ } else if ( error . response ?. data ?. message ) {
1431+ userMessage = error . response . data . message ;
13531432 } else if ( error . message ) {
13541433 userMessage = error . message ;
13551434 }
@@ -1422,8 +1501,8 @@ export async function saveFileContentAction(
14221501 commitMessage ?: string
14231502) : Promise < { success : boolean ; newSha ?: string ; error ?: string } > {
14241503 const session = await auth ( ) ;
1425- if ( ! session ?. user ?. uuid ) {
1426- return { success : false , error : "Authentication required." } ;
1504+ if ( ! session ?. user ?. uuid || ! session . user . name || ! session . user . email ) {
1505+ return { success : false , error : "Authentication required, or user name/email missing in session ." } ;
14271506 }
14281507
14291508 const project = await dbGetProjectByUuid ( projectUuid ) ;
@@ -1452,18 +1531,20 @@ export async function saveFileContentAction(
14521531 content : Buffer . from ( content ) . toString ( 'base64' ) ,
14531532 sha,
14541533 committer : {
1455- name : session . user . name || 'FlowUp User' ,
1456- email : session . user . email || 'user@flowup.app' , // GitHub might require a verified email for commits
1534+ name : session . user . name ,
1535+ email : session . user . email ,
14571536 } ,
14581537 author : {
1459- name : session . user . name || 'FlowUp User' ,
1460- email : session . user . email || 'user@flowup.app' ,
1538+ name : session . user . name ,
1539+ email : session . user . email ,
14611540 }
14621541 } ) ;
14631542
1464- if ( response . status === 200 || response . status === 201 ) { // 200 for update, 201 for create
1543+ // GitHub API returns 200 for update, 201 for create (though we use sha so it's usually update)
1544+ if ( response . status === 200 || response . status === 201 ) {
14651545 return { success : true , newSha : response . data . content ?. sha } ;
14661546 } else {
1547+ // This case should ideally not be reached if Octokit throws on non-2xx for this endpoint
14671548 return { success : false , error : `GitHub API returned status ${ response . status } ` } ;
14681549 }
14691550 } catch ( error : any ) {
@@ -1473,14 +1554,17 @@ export async function saveFileContentAction(
14731554 userMessage = "File has been modified since you opened it. Please refresh and try again." ;
14741555 } else if ( error . status === 403 ) { // Forbidden
14751556 userMessage = "Permission denied. Ensure you have write access to this repository and your token has 'repo' scope." ;
1476- } else if ( error . status === 404 ) { // Not found
1557+ } else if ( error . status === 404 ) { // Not found (e.g. path changed, or repo deleted)
14771558 userMessage = "File not found. It might have been deleted or moved." ;
14781559 } else if ( error . status === 401 ) { // Bad credentials
14791560 userMessage = `GitHub authentication failed. Your token might be invalid or expired. Please try reconnecting your GitHub account.` ;
14801561 await dbDeleteUserGithubOAuthToken ( session . user . uuid ) ; // Invalidate stored token
14811562 } else if ( error . status === 422 && error . response ?. data ?. message ?. includes ( "committer email is not associated with the committer" ) ) {
1482- userMessage = `Failed to save file: The committer email ('${ session . user . email } ') is not associated with your GitHub account or is not verified. Please ensure your FlowUp email matches a verified email on GitHub.` ;
1563+ userMessage = `Failed to save file: Your FlowUp email ('${ session . user . email } ') is not associated with your GitHub account or is not verified. Please ensure your FlowUp email matches a verified email on GitHub.` ;
1564+ } else if ( error . response ?. data ?. message ) {
1565+ userMessage = `Failed to save file: ${ error . response . data . message } ` ;
14831566 }
14841567 return { success : false , error : userMessage } ;
14851568 }
14861569}
1570+
0 commit comments