diff --git a/Containers/RepoManage/RepoManage.js b/Containers/RepoManage/RepoManage.js index ad9748a..5008260 100644 --- a/Containers/RepoManage/RepoManage.js +++ b/Containers/RepoManage/RepoManage.js @@ -78,7 +78,6 @@ export default function RepoManage(props) { headers: { 'Content-type': 'application/json', }, - body: JSON.stringify({ toDelete: true }), }) .then((response) => { if (response.ok) { diff --git a/Containers/UserSettings/Integrations/Integrations.js b/Containers/UserSettings/Integrations/Integrations.js index bd0669d..3767569 100644 --- a/Containers/UserSettings/Integrations/Integrations.js +++ b/Containers/UserSettings/Integrations/Integrations.js @@ -66,7 +66,6 @@ export default function Integrations() { setError(); //Loading button on submit to avoid multiple send. setIsLoading(true); - console.log(data); //Generate a UUIDv4 const token = uuidv4(); setLastGeneratedToken({ name: data.tokenName, value: token }); @@ -105,7 +104,6 @@ export default function Integrations() { } catch (error) { reset(); setIsLoading(false); - console.log(error); toast.error("Can't generate your token. Contact your administrator.", toastOptions); setTimeout(() => setError(), 4000); } diff --git a/helpers/functions/isSshPubKeyDuplicate.js b/helpers/functions/isSshPubKeyDuplicate.js new file mode 100644 index 0000000..456f062 --- /dev/null +++ b/helpers/functions/isSshPubKeyDuplicate.js @@ -0,0 +1,22 @@ +/** + * Checks if the given SSH public key is duplicated in the provided repository list by removing the comment part. + * + * @param {string} pubKey - The SSH public key to check. + * @param {Array} repoList - The list of repositories with their SSH public keys. + * @returns {boolean} - Returns true if the SSH public key is duplicated, otherwise false. + * @throws {Error} - Throws an error if required parameters are missing or invalid. + */ +export default function isSshPubKeyDuplicate(pubKey, repoList) { + if (!pubKey || !repoList || !Array.isArray(repoList)) { + throw new Error('Missing or invalid parameters for duplicate SSH public key check.'); + } + + // Compare the key part only by removing the comment + const pubKeyWithoutComment = pubKey.split(' ').slice(0, 2).join(' '); + + // Check if the normalized key is already in the repository list + return repoList.some((repo) => { + const repoSshKeyWithoutComment = repo.sshPublicKey.split(' ').slice(0, 2).join(' '); + return repoSshKeyWithoutComment === pubKeyWithoutComment; + }); +} diff --git a/helpers/functions/tokenController.js b/helpers/functions/tokenController.js new file mode 100644 index 0000000..dd8aca5 --- /dev/null +++ b/helpers/functions/tokenController.js @@ -0,0 +1,25 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +export default async function tokenController(API_KEY) { + const jsonDirectory = path.join(process.cwd(), 'config'); + try { + 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) + ); + if (user) { + const token = user.tokens.find((token) => token.token === API_KEY); + + if (token && token.permissions && typeof token.permissions === 'object') { + return token.permissions; + } + } + + return null; + } catch (error) { + throw new Error('Error with tokenController'); + } +} diff --git a/pages/api/repo/add.js b/pages/api/repo/add.js index 8fabcae..7bb817d 100644 --- a/pages/api/repo/add.js +++ b/pages/api/repo/add.js @@ -3,21 +3,43 @@ import path from 'path'; import { authOptions } from '../../../pages/api/auth/[...nextauth]'; import { getServerSession } from 'next-auth/next'; import repoHistory from '../../../helpers/functions/repoHistory'; +import tokenController from '../../../helpers/functions/tokenController'; +import isSshPubKeyDuplicate from '../../../helpers/functions/isSshPubKeyDuplicate'; const util = require('node:util'); const exec = util.promisify(require('node:child_process').exec); export default async function handler(req, res) { - if (req.method == 'POST') { - //Verify that the user is logged in. + if (req.method === 'POST') { + //AUTHENTICATION const session = await getServerSession(req, res, authOptions); - if (!session) { - res.status(401).json({ message: 'You must be logged in.' }); + const { authorization } = req.headers; + + if (!session && !authorization) { + res.status(401).end(); return; } - //The data we expect to receive + try { + if (authorization) { + const API_KEY = authorization.split(' ')[1]; + const permissions = await tokenController(API_KEY); + if (!permissions) { + res.status(403).json({ message: 'Invalid API key' }); + return; + } + if (!permissions.write) { + res.status(401).json({ message: 'Insufficient permissions' }); + return; + } + } + } catch (error) { + res.status(500).json({ message: 'Internal Server Error' }); + return; + } + + //DATA CONTROL const { alias, sshPublicKey, size, comment, alert, lanCommand, appendOnlyMode } = req.body; - //We check that we receive data for each variable. Only "comment" and "lanCommand" are optional in the form. + //Only "comment" and "lanCommand" are optional in the form. if ( !alias || !sshPublicKey || @@ -25,22 +47,27 @@ export default async function handler(req, res) { typeof appendOnlyMode !== 'boolean' || (!alert && alert !== 0) ) { - //If a variable is empty. - res.status(422).json({ - message: 'Unexpected data', - }); - //A return to make sure we don't go any further if data are incorrect. + res.status(422).json({ message: 'Unexpected data' }); return; } + //ADD REPO try { - //console.log('API call (PUT)'); //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); + const sshKeyAlreadyUsed = isSshPubKeyDuplicate(sshPublicKey, repoList); + if (sshKeyAlreadyUsed) { + res.status(409).json({ + message: + 'The SSH key is already used in another repository. Please use another key or delete the key from the other repository.', + }); + return; + } + //Find the first biggest ID available to assign it, so the highest ID is already the last added. let newID = 0; for (let element in repoList) { diff --git a/pages/api/repo/id/[slug]/delete.js b/pages/api/repo/id/[slug]/delete.js index d294069..8e4125b 100644 --- a/pages/api/repo/id/[slug]/delete.js +++ b/pages/api/repo/id/[slug]/delete.js @@ -3,36 +3,46 @@ 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') { - //Verify that the user is logged in. + //AUTHENTICATION const session = await getServerSession(req, res, authOptions); - if (!session) { - res.status(401).json({ message: 'You must be logged in.' }); + const { authorization } = req.headers; + + if (!session && !authorization) { + res.status(401).end(); return; } + + try { + if (authorization) { + const API_KEY = authorization.split(' ')[1]; + const permissions = await tokenController(API_KEY); + if (!permissions) { + res.status(403).json({ message: 'Invalid API key' }); + return; + } + if (!permissions.write) { + res.status(401).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({ - status: 403, message: 'Deletion is disabled on this server', }); return; } - //The data we expect to receive - const { toDelete } = req.body; - ////We check that we receive toDelete and it must be a bool. - if (typeof toDelete != 'boolean' || toDelete === false) { - //If a variable is empty. - res.status(422).json({ - message: 'Unexpected data', - }); - //A return to make sure we don't go any further if data are incorrect. - return; - } try { //console.log('API call (DELETE)'); diff --git a/pages/api/repo/id/[slug]/edit.js b/pages/api/repo/id/[slug]/edit.js index 67dcb54..3bb1438 100644 --- a/pages/api/repo/id/[slug]/edit.js +++ b/pages/api/repo/id/[slug]/edit.js @@ -3,19 +3,41 @@ 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'; +import isSshPubKeyDuplicate from '../../../../../helpers/functions/isSshPubKeyDuplicate'; const util = require('node:util'); const exec = util.promisify(require('node:child_process').exec); export default async function handler(req, res) { if (req.method == 'PUT') { - //Verify that the user is logged in. + //AUTHENTICATION const session = await getServerSession(req, res, authOptions); - if (!session) { - res.status(401).json({ message: 'You must be logged in.' }); + const { authorization } = req.headers; + + if (!session && !authorization) { + res.status(401).end(); return; } - //The data we expect to receive + try { + if (authorization) { + const API_KEY = authorization.split(' ')[1]; + const permissions = await tokenController(API_KEY); + if (!permissions) { + res.status(403).json({ message: 'Invalid API key' }); + return; + } + if (!permissions.write) { + res.status(401).json({ message: 'Insufficient permissions' }); + return; + } + } + } catch (error) { + res.status(500).json({ message: 'Internal Server Error' }); + return; + } + + //DATA CONTROL const { alias, sshPublicKey, size, comment, alert, lanCommand, appendOnlyMode } = req.body; //Only "comment" and "lanCommand" are optional in the form. if ( @@ -31,6 +53,7 @@ export default async function handler(req, res) { return; } + //UPDATE REPO try { //Find the absolute path of the json directory const jsonDirectory = path.join(process.cwd(), '/config'); @@ -38,6 +61,18 @@ export default async function handler(req, res) { //Parse the repoList repoList = JSON.parse(repoList); + const sshKeyAlreadyUsed = isSshPubKeyDuplicate( + sshPublicKey, + repoList.filter((repo) => repo.id != parseInt(req.query.slug)) + ); + if (sshKeyAlreadyUsed) { + res.status(409).json({ + message: + 'The SSH key is already used in another repository. Please use another key or delete the key from the other repository.', + }); + return; + } + //Find the index of the repo in repoList //NOTE : req.query.slug return a string, so parseInt to use with indexOf. const repoIndex = repoList.map((repo) => repo.id).indexOf(parseInt(req.query.slug)); diff --git a/pages/api/repo/index.js b/pages/api/repo/index.js index f5d6790..62daf2c 100644 --- a/pages/api/repo/index.js +++ b/pages/api/repo/index.js @@ -2,19 +2,39 @@ import fs from 'fs'; import path from 'path'; import { authOptions } from '../../../pages/api/auth/[...nextauth]'; import { getServerSession } from 'next-auth/next'; +import tokenController from '../../../helpers/functions/tokenController'; export default async function handler(req, res) { - if (req.method == 'GET') { - //Verify that the user is logged in. + if (req.method === 'GET') { + // AUTHENTICATION const session = await getServerSession(req, res, authOptions); - if (!session) { - // res.status(401).json({ message: 'You must be logged in.' }); + const { authorization } = req.headers; + + if (!session && !authorization) { res.status(401).end(); return; } try { - //console.log('API call (GET)'); + if (authorization) { + const API_KEY = authorization.split(' ')[1]; + const permissions = await tokenController(API_KEY); + if (!permissions) { + res.status(403).json({ message: 'Invalid API key' }); + return; + } + if (!permissions.read) { + res.status(401).json({ message: 'Insufficient permissions' }); + return; + } + } + } catch (error) { + res.status(500).json({ message: 'Internal Server Error' }); + return; + } + + // GET REPO LIST + try { //Find the absolute path of the json directory const jsonDirectory = path.join(process.cwd(), '/config'); //Check if the repo.json file exists and initialize it if not. @@ -23,14 +43,10 @@ export default async function handler(req, res) { } //Read the file repo.json let repoList = await fs.promises.readFile(jsonDirectory + '/repo.json', 'utf8'); - //Parse the JSON repoList = JSON.parse(repoList); - //Send the response res.status(200).json({ repoList }); } catch (error) { - //Log for backend console.log(error); - //Log for frontend if (error.code == 'ENOENT') { res.status(500).json({ status: 500, @@ -44,5 +60,11 @@ export default async function handler(req, res) { } return; } + } else { + res.status(405).json({ + status: 405, + message: 'Method Not Allowed ', + }); + return; } }