Skip to content

Commit 4a58142

Browse files
ah nan, je parler pas des cookies, eu marcher !
je parler que eft quand
1 parent 3734134 commit 4a58142

2 files changed

Lines changed: 294 additions & 175 deletions

File tree

src/app/(app)/projects/[id]/actions.ts

Lines changed: 127 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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';
55
import {
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';
3840
import { z } from 'zod';
3941
import { 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+
195260
export 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+
11681242
function 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-zA-Z0-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

Comments
 (0)