From 3e6e6ff2cd7400d99e5e1c8249015d17a7015c73 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C3=ABl=20Minelli?= <git@minelli.me>
Date: Tue, 14 May 2024 23:06:25 +0200
Subject: [PATCH] Fix bug on duplicate repo creation

---
 ExpressAPI/.vscode/launch.json            |  38 ++++
 ExpressAPI/assets/OpenAPI/OpenAPI.yaml    |   2 +-
 ExpressAPI/src/helpers/GlobalHelper.ts    |   2 +-
 ExpressAPI/src/managers/GitlabManager.ts  | 201 +++++++++++++++-------
 ExpressAPI/src/routes/AssignmentRoutes.ts |   6 +-
 5 files changed, 180 insertions(+), 69 deletions(-)
 create mode 100644 ExpressAPI/.vscode/launch.json

diff --git a/ExpressAPI/.vscode/launch.json b/ExpressAPI/.vscode/launch.json
new file mode 100644
index 0000000..11cc7b0
--- /dev/null
+++ b/ExpressAPI/.vscode/launch.json
@@ -0,0 +1,38 @@
+{
+    // Utilisez IntelliSense pour en savoir plus sur les attributs possibles.
+    // Pointez pour afficher la description des attributs existants.
+    // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "tsx",
+            "type": "node",
+            "request": "launch",
+        
+            // Debug current file in VSCode
+            "program": "src/app.ts",
+        
+            /*
+            Path to tsx binary
+            Assuming locally installed
+            */
+            "runtimeExecutable": "tsx",
+        
+            /*
+            Open terminal when debugging starts (Optional)
+            Useful to see console.logs
+            */
+            "console": "integratedTerminal",
+            "internalConsoleOptions": "neverOpen",
+        
+            // Files to exclude from debugger (e.g. call stack)
+            "skipFiles": [
+                // Node.js internal core modules
+                "<node_internals>/**",
+        
+                // Ignore all dependencies (optional)
+                "${workspaceFolder}/node_modules/**",
+            ],
+        }
+    ]
+}
\ No newline at end of file
diff --git a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml
index 7712e73..11ce447 100644
--- a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml
+++ b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml
@@ -1,7 +1,7 @@
 openapi: 3.1.0
 info:
     title: Dojo API
-    version: 4.0.0
+    version: 4.1.0
     description: |
         **Backend API of the Dojo project.**
         
diff --git a/ExpressAPI/src/helpers/GlobalHelper.ts b/ExpressAPI/src/helpers/GlobalHelper.ts
index 308a941..9d5e5e8 100644
--- a/ExpressAPI/src/helpers/GlobalHelper.ts
+++ b/ExpressAPI/src/helpers/GlobalHelper.ts
@@ -41,7 +41,7 @@ class GlobalHelper {
     }
 
     isRepoNameAlreadyTaken(errorDescription: unknown) {
-        return errorDescription instanceof Object && 'name' in errorDescription && errorDescription.name instanceof Array && errorDescription.name.length > 0 && errorDescription.name[0] === 'has already been taken';
+        return errorDescription instanceof Array && errorDescription.length > 0 && (errorDescription[0] as string).includes('has already been taken');
     }
 
     addRepoMember(repositoryId: number) {
diff --git a/ExpressAPI/src/managers/GitlabManager.ts b/ExpressAPI/src/managers/GitlabManager.ts
index 5571185..4c25d86 100644
--- a/ExpressAPI/src/managers/GitlabManager.ts
+++ b/ExpressAPI/src/managers/GitlabManager.ts
@@ -24,16 +24,27 @@ class GitlabManager extends SharedGitlabManager {
 
             return profileApi.Users.showCurrentUser();
         } catch ( e ) {
+            logger.error(JSON.stringify(e));
             return undefined;
         }
     }
 
-    getRepositoryMembers(idOrNamespace: string): Promise<Array<MemberSchema>> {
-        return this.api.ProjectMembers.all(idOrNamespace, { includeInherited: true });
+    async getRepositoryMembers(idOrNamespace: string): Promise<Array<MemberSchema>> {
+        try {
+            return await this.api.ProjectMembers.all(idOrNamespace, { includeInherited: true });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    getRepositoryReleases(repoId: number): Promise<Array<ReleaseSchema>> {
-        return this.api.ProjectReleases.all(repoId);
+    async getRepositoryReleases(repoId: number): Promise<Array<ReleaseSchema>> {
+        try {
+            return await this.api.ProjectReleases.all(repoId);
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
     async getRepositoryLastCommit(repoId: number, branch: string = 'main'): Promise<CommitSchema | undefined> {
@@ -51,57 +62,92 @@ class GitlabManager extends SharedGitlabManager {
         }
     }
 
-    createRepository(name: string, description: string, visibility: 'public' | 'internal' | 'private', initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, importUrl: string): Promise<ProjectSchema> {
-        return this.api.Projects.create({
-                                            name                : name,
-                                            description         : description,
-                                            importUrl           : importUrl,
-                                            initializeWithReadme: initializeWithReadme,
-                                            namespaceId         : namespace,
-                                            sharedRunnersEnabled: sharedRunnersEnabled,
-                                            visibility          : visibility,
-                                            wikiAccessLevel     : wikiEnabled ? 'enabled' : 'disabled'
-                                        });
+    async createRepository(name: string, description: string, visibility: 'public' | 'internal' | 'private', initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, importUrl: string): Promise<ProjectSchema> {
+        try {
+            return await this.api.Projects.create({
+                                                      name                : name,
+                                                      description         : description,
+                                                      importUrl           : importUrl,
+                                                      initializeWithReadme: initializeWithReadme,
+                                                      namespaceId         : namespace,
+                                                      sharedRunnersEnabled: sharedRunnersEnabled,
+                                                      visibility          : visibility,
+                                                      wikiAccessLevel     : wikiEnabled ? 'enabled' : 'disabled'
+                                                  });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    deleteRepository(repoId: number): Promise<void> {
-        return this.api.Projects.remove(repoId);
+    async deleteRepository(repoId: number): Promise<void> {
+        try {
+            return await this.api.Projects.remove(repoId);
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    forkRepository(forkId: number, name: string, path: string, description: string, visibility: 'public' | 'internal' | 'private', namespace: number): Promise<ProjectSchema> {
-        return this.api.Projects.fork(forkId, {
-            name       : name,
-            path       : path,
-            description: description,
-            namespaceId: namespace,
-            visibility : visibility
-        });
+    async forkRepository(forkId: number, name: string, path: string, description: string, visibility: 'public' | 'internal' | 'private', namespace: number): Promise<ProjectSchema> {
+        try {
+            return await this.api.Projects.fork(forkId, {
+                name       : name,
+                path       : path,
+                description: description,
+                namespaceId: namespace,
+                visibility : visibility
+            });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    editRepository(repoId: number, newAttributes: EditProjectOptions): Promise<ProjectSchema> {
-        return this.api.Projects.edit(repoId, newAttributes);
+    async editRepository(repoId: number, newAttributes: EditProjectOptions): Promise<ProjectSchema> {
+        try {
+            return await this.api.Projects.edit(repoId, newAttributes);
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
     changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<ProjectSchema> {
         return this.editRepository(repoId, { visibility: visibility });
     }
 
-    addRepositoryMember(repoId: number, userId: number, accessLevel: Exclude<AccessLevel, AccessLevel.ADMIN>): Promise<MemberSchema> {
-        return this.api.ProjectMembers.add(repoId, userId, accessLevel);
+    async addRepositoryMember(repoId: number, userId: number, accessLevel: Exclude<AccessLevel, AccessLevel.ADMIN>): Promise<MemberSchema> {
+        try {
+            return await this.api.ProjectMembers.add(repoId, userId, accessLevel);
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<ProjectVariableSchema> {
-        return this.api.ProjectVariables.create(repoId, key, value, {
-            variableType: 'env_var',
-            protected   : isProtected,
-            masked      : isMasked
-        });
+    async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<ProjectVariableSchema> {
+        try {
+            return await this.api.ProjectVariables.create(repoId, key, value, {
+                variableType: 'env_var',
+                protected   : isProtected,
+                masked      : isMasked
+            });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<ProjectBadgeSchema> {
-        return this.api.ProjectBadges.add(repoId, linkUrl, imageUrl, {
-            name: name
-        });
+    async addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<ProjectBadgeSchema> {
+        try {
+            return await this.api.ProjectBadges.add(repoId, linkUrl, imageUrl, {
+                name: name
+            });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
     async checkTemplateAccess(projectIdOrNamespace: string, req: express.Request, res?: express.Response): Promise<boolean> {
@@ -143,34 +189,54 @@ class GitlabManager extends SharedGitlabManager {
         }
     }
 
-    protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: ProtectedBranchAccessLevel, allowedToPush: ProtectedBranchAccessLevel, allowedToUnprotect: ProtectedBranchAccessLevel): Promise<ProtectedBranchSchema> {
-        return this.api.ProtectedBranches.protect(repoId, branchName, {
-            allowForcePush      : allowForcePush,
-            mergeAccessLevel    : allowedToMerge,
-            pushAccessLevel     : allowedToPush,
-            unprotectAccessLevel: allowedToUnprotect
-        });
+    async protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: ProtectedBranchAccessLevel, allowedToPush: ProtectedBranchAccessLevel, allowedToUnprotect: ProtectedBranchAccessLevel): Promise<ProtectedBranchSchema> {
+        try {
+            return await this.api.ProtectedBranches.protect(repoId, branchName, {
+                allowForcePush      : allowForcePush,
+                mergeAccessLevel    : allowedToMerge,
+                pushAccessLevel     : allowedToPush,
+                unprotectAccessLevel: allowedToUnprotect
+            });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<RepositoryTreeSchema>> {
-        return this.api.Repositories.allRepositoryTrees(repoId, {
-            recursive: recursive,
-            ref      : branch
-        });
+    async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<RepositoryTreeSchema>> {
+        try {
+            return await this.api.Repositories.allRepositoryTrees(repoId, {
+                recursive: recursive,
+                ref      : branch
+            });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<RepositoryFileExpandedSchema> {
-        return this.api.RepositoryFiles.show(repoId, filePath, branch);
+    async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<RepositoryFileExpandedSchema> {
+        try {
+            return await this.api.RepositoryFiles.show(repoId, filePath, branch);
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
-    private createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> {
-        const gitFunction = create ? this.api.RepositoryFiles.create.bind(this.api) : this.api.RepositoryFiles.edit.bind(this.api);
+    private async createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> {
+        try {
+            const gitFunction = create ? this.api.RepositoryFiles.create.bind(this.api) : this.api.RepositoryFiles.edit.bind(this.api);
 
-        return gitFunction(repoId, filePath, branch, fileBase64, commitMessage, {
-            encoding   : 'base64',
-            authorName : authorName,
-            authorEmail: authorMail
-        });
+            return await gitFunction(repoId, filePath, branch, fileBase64, commitMessage, {
+                encoding   : 'base64',
+                authorName : authorName,
+                authorEmail: authorMail
+            });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 
     createFile(repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> {
@@ -181,11 +247,16 @@ class GitlabManager extends SharedGitlabManager {
         return this.createUpdateFile(false, repoId, filePath, fileBase64, commitMessage, branch, authorName, authorMail);
     }
 
-    deleteFile(repoId: number, filePath: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<void> {
-        return this.api.RepositoryFiles.remove(repoId, filePath, branch, commitMessage, {
-            authorName : authorName,
-            authorEmail: authorMail
-        });
+    async deleteFile(repoId: number, filePath: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<void> {
+        try {
+            return await this.api.RepositoryFiles.remove(repoId, filePath, branch, commitMessage, {
+                authorName : authorName,
+                authorEmail: authorMail
+            });
+        } catch ( e ) {
+            logger.error(JSON.stringify(e));
+            return Promise.reject(e);
+        }
     }
 }
 
diff --git a/ExpressAPI/src/routes/AssignmentRoutes.ts b/ExpressAPI/src/routes/AssignmentRoutes.ts
index 18c79a4..2055e65 100644
--- a/ExpressAPI/src/routes/AssignmentRoutes.ts
+++ b/ExpressAPI/src/routes/AssignmentRoutes.ts
@@ -22,6 +22,7 @@ import DojoStatusCode              from '../shared/types/Dojo/DojoStatusCode.js'
 import DojoModelsHelper            from '../helpers/DojoModelsHelper.js';
 import * as Gitlab                 from '@gitbeaker/rest';
 import { GitbeakerRequestError }   from '@gitbeaker/requester-utils';
+import SharedConfig                from '../shared/config/SharedConfig.js';
 
 
 class AssignmentRoutes implements RoutesManager {
@@ -203,10 +204,11 @@ class AssignmentRoutes implements RoutesManager {
             } else if ( isUpdate && !req.boundParams.exercise?.isCorrection ) {
                 return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'This exercise is not a correction', DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST);
             }
-
+            
             const lastCommit = await GitlabManager.getRepositoryLastCommit(req.boundParams.exercise!.gitlabId);
+
             if ( lastCommit ) {
-                if ( !isUpdate ) {
+                if ( !isUpdate && SharedConfig.production ) { //Disable in dev env because gitlab dev group is private and we can't change visibility of sub projects
                     await GitlabManager.changeRepositoryVisibility(req.boundParams.exercise!.gitlabId, 'internal');
                 }
 
-- 
GitLab