@@ -31,12 +31,12 @@ import {
3131 deleteProjectAnnouncement as dbDeleteProjectAnnouncement ,
3232 updateProjectGithubRepo as dbUpdateProjectGithubRepo ,
3333 getUserGithubOAuthToken as dbGetUserGithubOAuthToken ,
34+ getTaskByUuid as dbGetTaskByUuid ,
3435} from '@/lib/db' ;
3536import { z } from 'zod' ;
3637import { auth } from '@/lib/authEdge' ;
3738import { Octokit } from 'octokit' ;
3839import { Buffer } from 'buffer' ;
39- import { getAppAuthOctokit , getInstallationOctokit } from '@/lib/githubAppClient' ;
4040
4141
4242export async function fetchProjectAction ( uuid : string | undefined ) : Promise < Project | null > {
@@ -328,6 +328,18 @@ export interface UpdateTaskFormState {
328328 updatedTask ?: Task ;
329329}
330330
331+ const UpdateTaskSchema = z . object ( {
332+ taskUuid : z . string ( ) . uuid ( "Invalid task UUID." ) ,
333+ projectUuid : z . string ( ) . uuid ( "Invalid project UUID." ) ,
334+ title : z . string ( ) . min ( 1 , "Title is required." ) . max ( 255 ) . optional ( ) ,
335+ description : z . string ( ) . optional ( ) ,
336+ todoListMarkdown : z . string ( ) . optional ( ) . default ( '' ) ,
337+ status : z . enum ( [ 'To Do' , 'In Progress' , 'Done' , 'Archived' ] as [ TaskStatus , ...TaskStatus [ ] ] ) . optional ( ) ,
338+ assigneeUuid : z . string ( ) . uuid ( "Invalid Assignee UUID format." ) . optional ( ) . or ( z . literal ( '' ) ) ,
339+ tagsString : z . string ( ) . optional ( ) ,
340+ } ) ;
341+
342+
331343export async function updateTaskAction ( prevState : UpdateTaskFormState , formData : FormData ) : Promise < UpdateTaskFormState > {
332344 const session = await auth ( ) ;
333345 if ( ! session ?. user ?. uuid ) {
@@ -336,18 +348,6 @@ export async function updateTaskAction(prevState: UpdateTaskFormState, formData:
336348 }
337349 console . log ( "[updateTaskAction] Authenticated user for permission check:" , session . user . uuid ) ;
338350
339- const UpdateTaskSchema = z . object ( {
340- taskUuid : z . string ( ) . uuid ( "Invalid task UUID." ) ,
341- projectUuid : z . string ( ) . uuid ( "Invalid project UUID." ) ,
342- title : z . string ( ) . min ( 1 , "Title is required." ) . max ( 255 ) . optional ( ) ,
343- description : z . string ( ) . optional ( ) ,
344- todoListMarkdown : z . string ( ) . optional ( ) . default ( '' ) ,
345- status : z . enum ( [ 'To Do' , 'In Progress' , 'Done' , 'Archived' ] as [ TaskStatus , ...TaskStatus [ ] ] ) . optional ( ) ,
346- assigneeUuid : z . string ( ) . uuid ( "Invalid Assignee UUID format." ) . optional ( ) . or ( z . literal ( '' ) ) ,
347- tagsString : z . string ( ) . optional ( ) ,
348- } ) ;
349-
350-
351351 const validatedFields = UpdateTaskSchema . safeParse ( {
352352 taskUuid : formData . get ( 'taskUuid' ) ,
353353 projectUuid : formData . get ( 'projectUuid' ) ,
@@ -400,7 +400,8 @@ export async function updateTaskAction(prevState: UpdateTaskFormState, formData:
400400
401401
402402 if ( Object . keys ( dataToUpdate ) . length === 0 ) {
403- return { message : "No changes to update." , updatedTask : await dbGetTaskByUuid ( taskUuid ) || undefined } ;
403+ const currentTaskData = await dbGetTaskByUuid ( taskUuid ) ;
404+ return { message : "No changes to update." , updatedTask : currentTaskData || undefined } ;
404405 }
405406
406407 const updatedTask = await dbUpdateTask ( taskUuid , dataToUpdate ) ;
@@ -530,8 +531,8 @@ async function updateReadmeOnGithub(octokit: Octokit, owner: string, repo: strin
530531 message : 'Update README.md from FlowUp' ,
531532 content : Buffer . from ( content ) . toString ( 'base64' ) ,
532533 committer : {
533- name : 'FlowUp Bot' , // Consider making this configurable or using the user's GitHub name
534- email : 'bot@flowup.app' , // Or a no-reply / user's email
534+ name : 'FlowUp Bot' ,
535+ email : 'bot@flowup.app' ,
535536 } ,
536537 } ;
537538 if ( existingSha ) {
@@ -544,8 +545,8 @@ async function updateReadmeOnGithub(octokit: Octokit, owner: string, repo: strin
544545 } catch ( error : any ) {
545546 if ( error . status === 404 && ! existingSha ) {
546547 console . log ( `[updateReadmeOnGithub] README.md not found for ${ owner } /${ repo } , creating it.` ) ;
547- const paramsForCreation = { ...paramsForUpdate } ;
548- delete paramsForCreation . sha ;
548+ const paramsForCreation = { ...paramsForUpdate } ; // Re-create params for creation
549+ delete paramsForCreation . sha ; // Ensure SHA is not in creation params
549550 try {
550551 await octokit . rest . repos . createOrUpdateFileContents ( paramsForCreation ) ;
551552 console . log ( `[updateReadmeOnGithub] README.md created successfully on GitHub for ${ owner } /${ repo } after initial 404.` ) ;
@@ -711,15 +712,11 @@ export async function toggleProjectVisibilityAction(prevState: ToggleProjectVisi
711712 if ( oauthToken ?. accessToken ) {
712713 const octokit = new Octokit ( { auth : oauthToken . accessToken } ) ;
713714 const repoParts = updatedProjectInDb . githubRepoName . split ( '/' ) ;
714- if ( repoParts . length !== 2 ) {
715+ if ( repoParts . length !== 2 || ! repoParts [ 0 ] || ! repoParts [ 1 ] ) { // Added check for empty parts
715716 console . error ( `[toggleProjectVisibilityAction] Invalid githubRepoName format: ${ updatedProjectInDb . githubRepoName } ` ) ;
716717 return { error : `Project visibility updated in FlowUp, but GitHub repo name format is invalid.` , project : updatedProjectInDb } ;
717718 }
718719 const [ owner , repo ] = repoParts ;
719- if ( ! owner || ! repo ) {
720- console . error ( `[toggleProjectVisibilityAction] Invalid owner or repo after splitting githubRepoName: ${ updatedProjectInDb . githubRepoName } ` ) ;
721- return { error : `Project visibility updated in FlowUp, but GitHub owner or repo name is invalid.` , project : updatedProjectInDb } ;
722- }
723720
724721 try {
725722 await octokit . rest . repos . update ( {
@@ -1139,7 +1136,8 @@ export async function linkProjectToGithubAction(
11391136 if ( githubRepoNameValue && githubRepoNameValue . trim ( ) !== '' ) {
11401137 nameForRepoCreation = githubRepoNameValue ;
11411138 } else {
1142- return { error : "Custom repository name cannot be empty." } ;
1139+ // This case should ideally be caught by client-side validation, but good to have server-side too.
1140+ return { error : "Custom repository name cannot be empty when not using the default." } ;
11431141 }
11441142 }
11451143
@@ -1176,11 +1174,10 @@ export async function linkProjectToGithubAction(
11761174 name : repoSlug ,
11771175 private : repoIsPrivate ,
11781176 description : `Repository for FlowUp project: ${ projectFromDb . name } ` ,
1179- auto_init : true , // Initialize with a README to avoid empty repo issues
1177+ auto_init : true ,
11801178 } ) ;
11811179 console . log ( `Successfully created repository: ${ createdRepo . data . html_url } ` ) ;
11821180
1183- // Sync FlowUp README to GitHub if content exists
11841181 if ( projectFromDb . readmeContent && projectFromDb . readmeContent . trim ( ) !== '' ) {
11851182 let existingReadmeSha : string | undefined = undefined ;
11861183 try {
@@ -1206,7 +1203,7 @@ export async function linkProjectToGithubAction(
12061203 console . error ( `GitHub API error creating repository: ${ apiError . status } ${ apiError . message } ` , apiError . response ?. data ) ;
12071204 const errorMessage = apiError . response ?. data ?. message || apiError . message || 'Unknown GitHub API error.' ;
12081205 if ( apiError . status === 403 ) {
1209- return { error : `GitHub Permission Denied: ${ errorMessage } . Ensure your GitHub account has permissions to create repositories and the FlowUp OAuth App has the 'repo' scope.` } ;
1206+ return { error : `GitHub Permission Denied: ${ errorMessage } . Ensure your GitHub OAuth App has the 'repo' scope and necessary permissions .` } ;
12101207 }
12111208 if ( apiError . status === 422 ) {
12121209 return { error : `Failed to create GitHub repository '${ repoSlug } '. It might already exist or there's a naming conflict. GitHub's message: ${ errorMessage } ` } ;
@@ -1215,7 +1212,7 @@ export async function linkProjectToGithubAction(
12151212 }
12161213
12171214 const repoUrl = createdRepo . data . html_url ;
1218- const actualRepoName = createdRepo . data . full_name ; // e.g., "username/repo-name"
1215+ const actualRepoName = createdRepo . data . full_name ;
12191216
12201217 const updatedProject = await dbUpdateProjectGithubRepo ( projectUuid , repoUrl , actualRepoName ) ;
12211218
@@ -1243,11 +1240,11 @@ export async function getRepoContentsAction(projectUuid: string, path: string =
12431240
12441241 const oauthToken = await dbGetUserGithubOAuthToken ( session . user . uuid ) ;
12451242 if ( ! oauthToken || ! oauthToken . accessToken ) {
1246- return { error : "GitHub account not linked or token missing." } ;
1243+ return { error : "GitHub account not linked or token missing. Please connect your GitHub account from the CodeSpace tab. " } ;
12471244 }
12481245
12491246 const [ owner , repo ] = project . githubRepoName . split ( '/' ) ;
1250- if ( ! owner || ! repo ) return { error : "Invalid GitHub repository name format." } ;
1247+ if ( ! owner || ! repo ) return { error : "Invalid GitHub repository name format in FlowUp project data ." } ;
12511248
12521249 const octokit = new Octokit ( { auth : oauthToken . accessToken } ) ;
12531250
@@ -1260,15 +1257,26 @@ export async function getRepoContentsAction(projectUuid: string, path: string =
12601257 if ( Array . isArray ( data ) ) {
12611258 return data as GithubRepoContentItem [ ] ;
12621259 }
1263- // If data is a single file object (when path points to a file)
1260+ // If data is a single file object (when path points to a file, though this endpoint usually returns array for dirs )
12641261 return [ data as GithubRepoContentItem ] ;
12651262 } catch ( error : any ) {
1266- console . error ( `[getRepoContentsAction] Error fetching content for ${ owner } /${ repo } /${ path } :` , error ) ;
1267- return { error : error . message || "Failed to fetch repository contents." } ;
1263+ console . error ( `[getRepoContentsAction] Error fetching content for ${ owner } /${ repo } /${ path } :` , error . status , error . message , error . response ?. data ) ;
1264+ let userMessage = "Failed to fetch repository contents." ;
1265+ if ( error . status === 404 ) {
1266+ userMessage = `Path '${ path } ' not found in repository '${ project . githubRepoName } '.` ;
1267+ } else if ( error . status === 403 ) {
1268+ userMessage = `Access denied to repository '${ project . githubRepoName } '. Check your GitHub token permissions.` ;
1269+ } else if ( error . message ) {
1270+ userMessage = error . message ;
1271+ }
1272+ return { error : userMessage } ;
12681273 }
12691274}
12701275
1271- export async function getFileContentAction ( projectUuid : string , filePath : string ) : Promise < { content : string ; sha : string } | { error : string } > {
1276+ export async function getFileContentAction (
1277+ projectUuid : string ,
1278+ filePath : string
1279+ ) : Promise < { content : string ; sha : string ; name : string ; path : string ; html_url ?: string | null ; download_url ?: string | null ; encoding ?: string ; size : number } | { error : string } > {
12721280 const session = await auth ( ) ;
12731281 if ( ! session ?. user ?. uuid ) return { error : "Authentication required." } ;
12741282
@@ -1279,7 +1287,7 @@ export async function getFileContentAction(projectUuid: string, filePath: string
12791287
12801288 const oauthToken = await dbGetUserGithubOAuthToken ( session . user . uuid ) ;
12811289 if ( ! oauthToken || ! oauthToken . accessToken ) {
1282- return { error : "GitHub account not linked or token missing." } ;
1290+ return { error : "GitHub account not linked or token missing. Please connect your GitHub account from the CodeSpace tab. " } ;
12831291 }
12841292
12851293 const [ owner , repo ] = project . githubRepoName . split ( '/' ) ;
@@ -1301,9 +1309,39 @@ export async function getFileContentAction(projectUuid: string, filePath: string
13011309 // @ts -ignore
13021310 const content = Buffer . from ( data . content , data . encoding as BufferEncoding || 'base64' ) . toString ( 'utf8' ) ;
13031311 // @ts -ignore
1304- return { content, sha : data . sha } ;
1312+ return { content, sha : data . sha , name : data . name , path : data . path , html_url : data . html_url , download_url : data . download_url , encoding : data . encoding , size : data . size } ;
13051313 } catch ( error : any ) {
1306- console . error ( `[getFileContentAction] Error fetching file content for ${ owner } /${ repo } /${ filePath } :` , error ) ;
1307- return { error : error . message || "Failed to fetch file content." } ;
1314+ console . error ( `[getFileContentAction] Error fetching file content for ${ owner } /${ repo } /${ filePath } :` , error . status , error . message , error . response ?. data ) ;
1315+ let userMessage = "Failed to fetch file content." ;
1316+ if ( error . status === 404 ) {
1317+ userMessage = `File '${ filePath } ' not found in repository '${ project . githubRepoName } '.` ;
1318+ } else if ( error . status === 403 ) {
1319+ userMessage = `Access denied to file '${ filePath } ' in repository '${ project . githubRepoName } '. Check token permissions.` ;
1320+ } else if ( error . response ?. data ?. message ) {
1321+ userMessage = error . response . data . message ;
1322+ } else if ( error . message ) {
1323+ userMessage = error . message ;
1324+ }
1325+ return { error : userMessage } ;
13081326 }
13091327}
1328+
1329+ // Placeholder for future GitHub App related auth, if needed
1330+ // export async function fetchUserGithubAccessDetailsAction(): Promise<{
1331+ // installation: UserGithubInstallation | null;
1332+ // oauthToken: UserGithubOAuthToken | null;
1333+ // } | { error: string }> {
1334+ // const session = await auth();
1335+ // if (!session?.user?.uuid) {
1336+ // return { error: "Authentication required." };
1337+ // }
1338+ // try {
1339+ // const installation = await dbGetUserGithubInstallation(session.user.uuid);
1340+ // const oauthToken = await dbGetUserGithubOAuthToken(session.user.uuid);
1341+ // return { installation, oauthToken };
1342+ // } catch (error: any) {
1343+ // return { error: "Failed to fetch GitHub access details: " + error.message };
1344+ // }
1345+ // }
1346+
1347+
0 commit comments