From 149fad13ec96d2a68e68f89a7b5785f8d763bf6c Mon Sep 17 00:00:00 2001 From: Ravinou Date: Sat, 22 Mar 2025 12:12:36 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20=E2=9A=A1=20delete=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helpers/functions/apiResponse.ts | 8 ++ helpers/functions/fileHelpers.ts | 6 +- helpers/functions/index.ts | 1 + helpers/functions/repoHistory.ts | 10 +-- helpers/functions/shell.utils.ts | 10 +++ helpers/functions/tokenController.ts | 28 ++++--- pages/api/repo/id/[slug]/delete.js | 116 --------------------------- pages/api/repo/id/[slug]/delete.ts | 70 ++++++++++++++++ types/api/integration.types.ts | 1 + 9 files changed, 116 insertions(+), 134 deletions(-) delete mode 100644 pages/api/repo/id/[slug]/delete.js create mode 100644 pages/api/repo/id/[slug]/delete.ts diff --git a/helpers/functions/apiResponse.ts b/helpers/functions/apiResponse.ts index a632e3b..c216fbd 100644 --- a/helpers/functions/apiResponse.ts +++ b/helpers/functions/apiResponse.ts @@ -5,6 +5,10 @@ export default class ApiResponse { res.status(200).json({ status: 200, message, data }); } + static badRequest(res: NextApiResponse, message = 'Bad Request') { + res.status(400).json({ status: 400, message }); + } + static unauthorized(res: NextApiResponse, message = 'Unauthorized') { res.status(401).json({ status: 401, message }); } @@ -17,6 +21,10 @@ export default class ApiResponse { res.status(404).json({ status: 404, message }); } + static methodNotAllowed(res: NextApiResponse, message = 'Method Not Allowed') { + res.status(405).json({ status: 405, message }); + } + static validationError(res: NextApiResponse, message = 'Validation Error') { res.status(422).json({ status: 422, message }); } diff --git a/helpers/functions/fileHelpers.ts b/helpers/functions/fileHelpers.ts index dd2aef3..e800f35 100644 --- a/helpers/functions/fileHelpers.ts +++ b/helpers/functions/fileHelpers.ts @@ -2,6 +2,7 @@ import { promises as fs } from 'fs'; import path from 'path'; import { Optional } from '~/types'; import { BorgWarehouseUser, Repository } from '~/types/domain/config.types'; +import repoHistory from './repoHistory'; // Paths definition const jsonDirectory = path.join(process.cwd(), '/config'); @@ -36,8 +37,11 @@ export const getRepoList = async (): Promise => { } }; -export const updateRepoList = async (repoList: Repository[]): Promise => { +export const updateRepoList = async (repoList: Repository[], history = false): Promise => { try { + if (history) { + await repoHistory(repoList); + } await fs.writeFile(repoFilePath, JSON.stringify(repoList, null, 2)); } catch (error) { console.error('Error writing repo.json:', error); diff --git a/helpers/functions/index.ts b/helpers/functions/index.ts index 94dff3a..f3e9ed6 100644 --- a/helpers/functions/index.ts +++ b/helpers/functions/index.ts @@ -6,3 +6,4 @@ export * from './nodemailerSMTP'; export * from './repoHistory'; export * from './timestampConverter'; export * from './tokenController'; +export * from './shell.utils'; diff --git a/helpers/functions/repoHistory.ts b/helpers/functions/repoHistory.ts index 003514c..ecc5677 100644 --- a/helpers/functions/repoHistory.ts +++ b/helpers/functions/repoHistory.ts @@ -1,10 +1,11 @@ import { promises as fs } from 'fs'; import path from 'path'; +import { Repository } from '~/types/domain/config.types'; -export default async function repoHistory(data) { +export default async function repoHistory(repoList: Repository[]) { try { const repoHistoryDir = path.join(process.cwd(), '/config/versions'); - const maxBackupCount = parseInt(process.env.MAX_REPO_BACKUP_COUNT) || 8; + const maxBackupCount = parseInt(process.env.MAX_REPO_BACKUP_COUNT ?? '8', 10); const timestamp = new Date().toISOString(); const backupDate = timestamp.split('T')[0]; @@ -24,13 +25,12 @@ export default async function repoHistory(data) { const backupFileName = `${backupDate}.log`; const backupFilePath = path.join(repoHistoryDir, backupFileName); - const jsonData = JSON.stringify(data, null, 2); + const jsonData = JSON.stringify(repoList, null, 2); const logData = `\n>>>> History of file repo.json at "${timestamp}" <<<<\n${jsonData}\n`; - // Écrire ou réécrire le fichier avec le contenu mis à jour await fs.appendFile(backupFilePath, logData); } catch (error) { - console.error('An error occurred while saving the repo history :', error.message); + console.error('An error occurred while saving the repo history :', error); } } diff --git a/helpers/functions/shell.utils.ts b/helpers/functions/shell.utils.ts index ec00d0d..2ab8e88 100644 --- a/helpers/functions/shell.utils.ts +++ b/helpers/functions/shell.utils.ts @@ -16,3 +16,13 @@ export const getStorageUsed = async (): Promise => { const { stdout } = await exec(`${shellsDirectory}/shells/getStorageUsed.sh`); return JSON.parse(stdout || '[]'); }; + +export const deleteRepo = async ( + repositoryName: string +): Promise<{ stdout: string; stderr: string }> => { + const shellsDirectory = path.join(process.cwd(), '/helpers'); + const { stdout, stderr } = await exec( + `${shellsDirectory}/shells/deleteRepo.sh ${repositoryName}` + ); + return { stdout, stderr }; +}; diff --git a/helpers/functions/tokenController.ts b/helpers/functions/tokenController.ts index 86c7a4f..7c09fc0 100644 --- a/helpers/functions/tokenController.ts +++ b/helpers/functions/tokenController.ts @@ -1,22 +1,26 @@ -import { promises as fs } from 'fs'; -import path from 'path'; +import { IncomingHttpHeaders } from 'http2'; +import { Optional } from '~/types'; +import { TokenPermissionsType } from '~/types/api/integration.types'; +import { getUsersList } from './fileHelpers'; + +export default async function tokenController( + headers: IncomingHttpHeaders +): Promise> { + const API_KEY = headers.authorization?.split(' ')[1]; + const FROM_IP = headers['x-forwarded-for'] || 'unknown'; -export default async function tokenController(API_KEY, FROM_IP) { - const jsonDirectory = path.join(process.cwd(), 'config'); const timestamp = new Date().toISOString(); + try { if (process.env.DISABLE_INTEGRATIONS === 'true') { console.log(`API auth failed from : ${FROM_IP} [${timestamp}]`); - return null; + return undefined; } - const usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8'); - const users = JSON.parse(usersList); - const user = users.find( - (user) => Array.isArray(user.tokens) && user.tokens.some((token) => token.token === API_KEY) - ); + const usersList = await getUsersList(); + const user = usersList.find((u) => u.tokens?.some((t) => t.token === API_KEY)); if (user) { - const token = user.tokens.find((token) => token.token === API_KEY); + const token = user.tokens?.find((token) => token.token === API_KEY); if (token && token.permissions && typeof token.permissions === 'object') { console.log( @@ -27,7 +31,7 @@ export default async function tokenController(API_KEY, FROM_IP) { } console.log(`API auth failed from : ${FROM_IP} [${timestamp}]`); - return null; + return undefined; } catch (error) { throw new Error('Error with tokenController'); } diff --git a/pages/api/repo/id/[slug]/delete.js b/pages/api/repo/id/[slug]/delete.js deleted file mode 100644 index aa4fb14..0000000 --- a/pages/api/repo/id/[slug]/delete.js +++ /dev/null @@ -1,116 +0,0 @@ -import { promises as fs } from 'fs'; -import path from 'path'; -import { authOptions } from '../../../auth/[...nextauth]'; -import { getServerSession } from 'next-auth/next'; -import repoHistory from '../../../../../helpers/functions/repoHistory'; -import tokenController from '../../../../../helpers/functions/tokenController'; -const util = require('node:util'); -const exec = util.promisify(require('node:child_process').exec); - -export default async function handler(req, res) { - if (req.method == 'DELETE') { - //AUTHENTICATION - const FROM_IP = req.headers['x-forwarded-for'] || 'unknown'; - const session = await getServerSession(req, res, authOptions); - const { authorization } = req.headers; - - if (!session && !authorization) { - res.status(401).end(); - return; - } - - try { - if (!session && authorization) { - const API_KEY = authorization.split(' ')[1]; - const permissions = await tokenController(API_KEY, FROM_IP); - if (!permissions) { - res.status(401).json({ message: 'Invalid API key' }); - return; - } - if (!permissions.delete) { - res.status(403).json({ message: 'Insufficient permissions' }); - return; - } - } - } catch (error) { - res.status(500).json({ message: 'Internal Server Error' }); - return; - } - - //If deletion is disabled on the server, return error - if (process.env.DISABLE_DELETE_REPO === 'true') { - res.status(403).json({ - message: 'Deletion is disabled on this server', - }); - return; - } - - try { - //console.log('API call (DELETE)'); - //Find the absolute path of the json directory - const jsonDirectory = path.join(process.cwd(), '/config'); - let repoList = await fs.readFile(jsonDirectory + '/repo.json', 'utf8'); - //Parse the repoList - repoList = JSON.parse(repoList); - - //Find the ID in the repoList and delete the repo. - //NOTE : req.query.slug return a string, so parseInt to use with indexOf. - const indexToDelete = repoList.map((repo) => repo.id).indexOf(parseInt(req.query.slug)); - - ////Call the shell : deleteRepo.sh - //Find the absolute path of the shells directory - const shellsDirectory = path.join(process.cwd(), '/helpers'); - //Exec the shell - const { stdout, stderr } = await exec( - `${shellsDirectory}/shells/deleteRepo.sh ${repoList[indexToDelete].repositoryName}` - ); - if (stderr) { - console.log('stderr:', stderr); - res.status(500).json({ - status: 500, - message: 'Error on delete, contact the administrator.', - }); - return; - } - - //Delete the repo in the repoList - if (indexToDelete !== -1) { - repoList.splice(indexToDelete, 1); - } else { - console.log('The index to delete does not exist'); - res.status(400).json({ - message: 'This repository does not exist', - }); - return; - } - //History the repoList - await repoHistory(repoList); - //Stringify the repoList to write it into the json file. - repoList = JSON.stringify(repoList); - //Write the new json - await fs.writeFile(jsonDirectory + '/repo.json', repoList, (err) => { - if (err) console.log(err); - }); - - res.status(200).json({ message: 'success' }); - } catch (error) { - //Log for backend - console.log(error); - //Log for frontend - if (error.code == 'ENOENT') { - res.status(500).json({ - message: 'No such file or directory', - }); - } else { - res.status(500).json({ - message: 'API error, contact the administrator', - }); - } - return; - } - } else { - res.status(405).json({ - message: 'Method Not Allowed ', - }); - } -} diff --git a/pages/api/repo/id/[slug]/delete.ts b/pages/api/repo/id/[slug]/delete.ts new file mode 100644 index 0000000..6fbddac --- /dev/null +++ b/pages/api/repo/id/[slug]/delete.ts @@ -0,0 +1,70 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { getServerSession } from 'next-auth/next'; +import { getRepoList, updateRepoList } from '~/helpers/functions'; +import ApiResponse from '~/helpers/functions/apiResponse'; +import { deleteRepo } from '~/helpers/functions/shell.utils'; +import { BorgWarehouseApiResponse } from '~/types/api/error.types'; +import tokenController from '~/helpers/functions/tokenController'; +import { authOptions } from '../../../auth/[...nextauth]'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'DELETE') { + return ApiResponse.methodNotAllowed(res); + } + + const session = await getServerSession(req, res, authOptions); + const { authorization } = req.headers; + if (!session && !authorization) { + return ApiResponse.unauthorized(res); + } + + try { + if (!session && authorization) { + const permissions = await tokenController(req.headers); + if (!permissions) { + return ApiResponse.unauthorized(res, 'Invalid API key'); + } + if (!permissions.delete) { + return ApiResponse.forbidden(res, 'Insufficient permissions'); + } + } + } catch (error) { + return ApiResponse.serverError(res); + } + + if (process.env.DISABLE_DELETE_REPO === 'true') { + return ApiResponse.forbidden(res, 'Deletion is disabled on this server'); + } + + try { + const repoList = await getRepoList(); + + const slug = req.query.slug; + if (!slug || Array.isArray(slug)) { + return ApiResponse.badRequest(res, 'Missing slug or slug is malformed'); + } + const indexToDelete = repoList.map((repo) => repo.id).indexOf(parseInt(slug, 10)); + + if (indexToDelete === -1) { + return ApiResponse.notFound(res, 'Repository not found'); + } + + const { stderr } = await deleteRepo(repoList[indexToDelete].repositoryName); + + if (stderr) { + console.log('Delete repository error: ', stderr); + return ApiResponse.serverError(res); + } + + const updatedRepoList = repoList.splice(indexToDelete, 1); + + await updateRepoList(updatedRepoList, true); + return ApiResponse.success(res, `Repository ${repoList[indexToDelete].repositoryName} deleted`); + } catch (error) { + console.error(error); + return ApiResponse.serverError(res); + } +} diff --git a/types/api/integration.types.ts b/types/api/integration.types.ts index 3a6e874..e09cb6e 100644 --- a/types/api/integration.types.ts +++ b/types/api/integration.types.ts @@ -1,4 +1,5 @@ export type IntegrationTokenType = { + token: string; name: string; creation: number; expiration?: number;