import axios, { AxiosError } from 'axios'; import ora from 'ora'; import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute.js'; import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig.js'; import Assignment from '../sharedByClients/models/Assignment.js'; import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js'; import Exercise from '../sharedByClients/models/Exercise.js'; import GitlabToken from '../shared/types/Gitlab/GitlabToken.js'; import User from '../sharedByClients/models/User.js'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js'; import * as Gitlab from '@gitbeaker/rest'; import DojoBackendHelper from '../sharedByClients/helpers/Dojo/DojoBackendHelper.js'; import GitlabPipelineStatus from '../shared/types/Gitlab/GitlabPipelineStatus.js'; class DojoBackendManager { private handleApiError(error: unknown, spinner: ora.Ora, verbose: boolean, defaultErrorMessage?: string, otherErrorHandler?: (error: AxiosError, spinner: ora.Ora, verbose: boolean) => void) { const unknownErrorMessage: string = 'unknown error'; if ( verbose ) { if ( error instanceof AxiosError ) { switch ( error.response?.data?.code ) { case DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED: spinner.fail(`The exercise does not belong to the assignment.`); break; case DojoStatusCode.ASSIGNMENT_PUBLISH_NO_PIPELINE: spinner.fail(`No pipeline found for this assignment.`); break; case DojoStatusCode.ASSIGNMENT_PUBLISH_PIPELINE_FAILED: spinner.fail((error.response?.data?.message as string | undefined) ?? `Last pipeline status is not "${ GitlabPipelineStatus.SUCCESS }".`); break; case DojoStatusCode.EXERCISE_CORRECTION_ALREADY_EXIST: spinner.fail(`This exercise is already labelled as a correction. If you want to update it, please use the update command.`); break; case DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST: spinner.fail(`The exercise is not labelled as a correction so it's not possible to update it.`); break; case DojoStatusCode.ASSIGNMENT_NAME_CONFLICT: spinner.fail(`Assignment creation error: The assignment name already exists. Please choose another name.`); break; case DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR: spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on GitLab (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`); break; case DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR: spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`); break; case DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED: spinner.fail(`The following users have reached the maximum number of exercise of this assignment : ${ ((error.response.data as DojoBackendResponse<Array<Gitlab.UserSchema>>).data).map(user => user.name).join(', ') }.`); break; case DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR: spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`); break; case DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR: spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on GitLab (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`); break; case DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND: spinner.fail(`Template not found or access denied. Please check the template ID or url. Also, please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`); break; case DojoStatusCode.GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED: spinner.fail(`Please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`); break; default: if ( otherErrorHandler ) { otherErrorHandler(error, spinner, verbose); } else { spinner.fail(defaultErrorMessage ?? 'Unknown error'); } break; } } else { spinner.fail(defaultErrorMessage ?? 'Unknown error'); } } } public async login(gitlabTokens: GitlabToken): Promise<User | undefined> { try { return (await axios.post<DojoBackendResponse<User>>(DojoBackendHelper.getApiUrl(ApiRoute.LOGIN), { accessToken : gitlabTokens.access_token, refreshToken: gitlabTokens.refresh_token })).data.data; } catch ( error ) { return undefined; } } public async refreshTokens(refreshToken: string): Promise<GitlabToken> { return (await axios.post<DojoBackendResponse<GitlabToken>>(DojoBackendHelper.getApiUrl(ApiRoute.REFRESH_TOKENS), { refreshToken: refreshToken })).data.data; } public async getAssignment(nameOrUrl: string, getMyExercises: boolean = false): Promise<Assignment | undefined> { try { return (await axios.get<DojoBackendResponse<Assignment>>(DojoBackendHelper.getApiUrl(ApiRoute.ASSIGNMENT_GET, { assignmentNameOrUrl: nameOrUrl }), getMyExercises ? { params: { getMyExercises } } : {})).data.data; } catch ( error ) { return undefined; } } public async checkTemplateAccess(idOrNamespace: string, verbose: boolean = true): Promise<boolean> { const spinner: ora.Ora = ora('Checking template access'); if ( verbose ) { spinner.start(); } try { await axios.get(DojoBackendHelper.getApiUrl(ApiRoute.GITLAB_CHECK_TEMPLATE_ACCESS, { gitlabProjectId: idOrNamespace })); if ( verbose ) { spinner.succeed('Template access granted'); } return true; } catch ( error ) { this.handleApiError(error, spinner, verbose, `Template error: ${ error }`); return false; } } public async createAssignment(name: string, members: Array<Gitlab.UserSchema>, templateIdOrNamespace: string | null, verbose: boolean = true): Promise<Assignment> { const spinner: ora.Ora = ora('Creating assignment...'); if ( verbose ) { spinner.start(); } try { const response = await axios.post<DojoBackendResponse<Assignment>>(DojoBackendHelper.getApiUrl(ApiRoute.ASSIGNMENT_CREATE), Object.assign({ name : name, members: JSON.stringify(members) }, templateIdOrNamespace ? { template: templateIdOrNamespace } : {})); if ( verbose ) { spinner.succeed(`Assignment successfully created`); } return response.data.data; } catch ( error ) { this.handleApiError(error, spinner, verbose, `Assignment creation error: unknown error`); throw error; } } public async createExercise(assignmentName: string, members: Array<Gitlab.UserSchema>, verbose: boolean = true): Promise<Exercise> { const spinner: ora.Ora = ora('Creating exercise...'); if ( verbose ) { spinner.start(); } try { const response = await axios.post<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_CREATE, { assignmentNameOrUrl: assignmentName }), { members: JSON.stringify(members) }); if ( verbose ) { spinner.succeed(`Exercise successfully created`); } return response.data.data; } catch ( error ) { this.handleApiError(error, spinner, verbose, `Exercise creation error: unknown error`); throw error; } } public async changeAssignmentPublishedStatus(assignment: Assignment, publish: boolean, verbose: boolean = true) { const spinner: ora.Ora = ora('Changing published status...'); if ( verbose ) { spinner.start(); } try { await axios.patch<DojoBackendResponse<null>>(DojoBackendHelper.getApiUrl(publish ? ApiRoute.ASSIGNMENT_PUBLISH : ApiRoute.ASSIGNMENT_UNPUBLISH, { assignmentNameOrUrl: assignment.name }), {}); if ( verbose ) { spinner.succeed(`Assignment ${ assignment.name } successfully ${ publish ? 'published' : 'unpublished' }`); } return; } catch ( error ) { this.handleApiError(error, spinner, verbose, `Assignment visibility change error: ${ error }`); throw error; } } public async linkUpdateCorrection(exerciseIdOrUrl: string, assignment: Assignment, commit: string | undefined, description: string | undefined, isUpdate: boolean, verbose: boolean = true): Promise<boolean> { const spinner: ora.Ora = ora(`${ isUpdate ? 'Updating' : 'Linking' } correction`); if ( verbose ) { spinner.start(); } try { const axiosFunction = isUpdate ? axios.patch.bind(axios) : axios.post.bind(axios); const route = isUpdate ? ApiRoute.ASSIGNMENT_CORRECTION_UPDATE_DELETE : ApiRoute.ASSIGNMENT_CORRECTION_LINK; await axiosFunction(DojoBackendHelper.getApiUrl(route, { assignmentNameOrUrl: assignment.name, exerciseIdOrUrl : exerciseIdOrUrl }), { exerciseIdOrUrl: exerciseIdOrUrl, commit : commit, description : description }); if ( verbose ) { spinner.succeed(`Correction ${ isUpdate ? 'updated' : 'linked' }`); } return true; } catch ( error ) { this.handleApiError(error, spinner, verbose, `Correction ${ isUpdate ? 'update' : 'link' } error: ${ error }`); return false; } } } export default new DojoBackendManager();