diff --git a/helpers/functions/shell.utils.ts b/helpers/functions/shell.utils.ts index 04dc009..454d592 100644 --- a/helpers/functions/shell.utils.ts +++ b/helpers/functions/shell.utils.ts @@ -39,3 +39,15 @@ export const updateRepoShell = async ( ); return { stdout, stderr }; }; + +export const createRepoShell = async ( + sshPublicKey: string, + storageSize: number, + appendOnlyMode: boolean +): Promise<{ stdout: string; stderr: string }> => { + const shellsDirectory = path.join(process.cwd(), '/helpers'); + const { stdout, stderr } = await exec( + `${shellsDirectory}/shells/createRepo.sh "${sshPublicKey}" ${storageSize} ${appendOnlyMode}` + ); + return { stdout, stderr }; +}; diff --git a/pages/api/repo/add.js b/pages/api/repo/add.js deleted file mode 100644 index e4edaed..0000000 --- a/pages/api/repo/add.js +++ /dev/null @@ -1,163 +0,0 @@ -import { promises as fs } from 'fs'; -import path from 'path'; -import { authOptions } from '../../../pages/api/auth/[...nextauth]'; -import { getServerSession } from 'next-auth/next'; -import { alertOptions } from '../../../domain/constants'; -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') { - return res.status(405).json({ status: 405, message: 'Method Not Allowed' }); - } - - // AUTHENTICATION - const FROM_IP = req.headers['x-forwarded-for'] || 'unknown'; - const session = await getServerSession(req, res, authOptions); - const { authorization } = req.headers; - - if (!(await isAuthenticated(session, authorization, FROM_IP))) { - res.status(401).json({ message: 'Invalid API key' }); - return; - } - - // DATA CONTROL - const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = req.body; - - if (!isValidRepoData(req.body)) { - return res.status(422).json({ message: 'Unexpected data' }); - } - - try { - const repoList = await getRepoList(); - - if (isSshKeyConflict(sshPublicKey, repoList)) { - return 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.', - }); - } - - const newRepo = await createNewRepo(repoList, { - alias, - sshPublicKey, - storageSize, - comment, - alert, - lanCommand, - appendOnlyMode, - }); - const newRepoList = [newRepo, ...repoList]; - - await saveRepoList(newRepoList); - - return res.status(200).json({ id: newRepo.id, repositoryName: newRepo.repositoryName }); - } catch (error) { - handleError(error, res); - } -} - -// -------------- -// Functions -// -------------- -async function isAuthenticated(session, authorization, FROM_IP) { - if (session) return true; - - if (authorization) { - const API_KEY = authorization.split(' ')[1]; - const permissions = await tokenController(API_KEY, FROM_IP); - return permissions?.create; - } - return false; -} - -function isValidRepoData(body) { - const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = body; - const expectedKeys = [ - 'alias', - 'sshPublicKey', - 'storageSize', - 'comment', - 'alert', - 'lanCommand', - 'appendOnlyMode', - ]; - - const isValidData = - typeof alias === 'string' && - alias.trim() !== '' && - typeof sshPublicKey === 'string' && - sshPublicKey.trim() !== '' && - typeof comment === 'string' && - typeof storageSize === 'number' && - Number.isInteger(storageSize) && - storageSize > 0 && - typeof alert === 'number' && - alertOptions.some((option) => option.value === alert) && - typeof lanCommand === 'boolean' && - typeof appendOnlyMode === 'boolean'; - - const hasUnexpectedKeys = Object.keys(body).some((key) => !expectedKeys.includes(key)); - - return isValidData && !hasUnexpectedKeys; -} - -async function getRepoList() { - const jsonDirectory = path.join(process.cwd(), '/config'); - const repoData = await fs.readFile(jsonDirectory + '/repo.json', 'utf8'); - return JSON.parse(repoData); -} - -function isSshKeyConflict(sshPublicKey, repoList) { - return typeof sshPublicKey === 'string' && isSshPubKeyDuplicate(sshPublicKey, repoList); -} - -async function createNewRepo( - repoList, - { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } -) { - const newID = repoList.length > 0 ? Math.max(...repoList.map((repo) => repo.id)) + 1 : 0; - - const newRepo = { - id: newID, - alias, - repositoryName: '', - status: false, - lastSave: 0, - lastStatusAlertSend: Math.floor(Date.now() / 1000), - alert, - storageSize: Number(storageSize), - storageUsed: 0, - sshPublicKey, - comment, - displayDetails: true, - lanCommand, - appendOnlyMode, - }; - - const shellsDirectory = path.join(process.cwd(), '/helpers'); - const { stdout } = await exec( - `${shellsDirectory}/shells/createRepo.sh "${newRepo.sshPublicKey}" ${newRepo.storageSize} ${newRepo.appendOnlyMode}` - ); - - newRepo.repositoryName = stdout.trim(); - return newRepo; -} - -async function saveRepoList(newRepoList) { - const jsonDirectory = path.join(process.cwd(), '/config'); - await repoHistory(newRepoList); - await fs.writeFile(jsonDirectory + '/repo.json', JSON.stringify(newRepoList)); -} - -function handleError(error, res) { - console.log(error); - if (error.code === 'ENOENT') { - res.status(500).json({ message: 'No such file or directory' }); - } else { - res.status(500).json({ message: error.stdout }); - } -} diff --git a/pages/api/repo/add.ts b/pages/api/repo/add.ts new file mode 100644 index 0000000..3e1c8eb --- /dev/null +++ b/pages/api/repo/add.ts @@ -0,0 +1,125 @@ +import { authOptions } from '../auth/[...nextauth]'; +import { getServerSession } from 'next-auth/next'; +import { + getRepoList, + updateRepoList, + tokenController, + isSshPubKeyDuplicate, +} from '~/helpers/functions'; +import { NextApiRequest, NextApiResponse } from 'next'; +import ApiResponse from '~/helpers/functions/apiResponse'; +import { Repository } from '~/types/domain/config.types'; +import { createRepoShell } from '~/helpers/functions/shell.utils'; +import { getUnixTime } from 'date-fns'; + +export default async function handler( + req: NextApiRequest & { body: Partial }, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return ApiResponse.methodNotAllowed(res); + } + + // AUTHENTICATION + 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.create) { + return ApiResponse.forbidden(res, 'Insufficient permissions'); + } + } + } catch (error) { + return ApiResponse.serverError(res); + } + + try { + validateRequestBody(req); + } catch (error) { + if (error instanceof Error) { + return ApiResponse.badRequest(res, error.message); + } + return ApiResponse.badRequest(res, 'Invalid request data'); + } + + try { + const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = + req.body; + const repoList = await getRepoList(); + + if (sshPublicKey && isSshPubKeyDuplicate(sshPublicKey, repoList)) { + return 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.', + }); + } + + const newRepo: Repository = { + id: repoList.length > 0 ? Math.max(...repoList.map((repo) => repo.id)) + 1 : 0, + alias: alias!, + repositoryName: '', + status: false, + lastSave: 0, + lastStatusAlertSend: getUnixTime(new Date()), + alert: alert!, + storageSize: storageSize!, + storageUsed: 0, + sshPublicKey: sshPublicKey!, + comment: comment!, + lanCommand: lanCommand!, + appendOnlyMode: appendOnlyMode!, + }; + + const { stdout, stderr } = await createRepoShell( + newRepo.sshPublicKey, + newRepo.storageSize, + newRepo.appendOnlyMode ?? false + ); + if (stderr) { + console.log('Create repository error: ', stderr); + throw new Error(); + } + + newRepo.repositoryName = stdout.trim(); + const updatedRepoList = [...repoList, newRepo]; + await updateRepoList(updatedRepoList, true); + + return res.status(200).json({ id: newRepo.id, repositoryName: newRepo.repositoryName }); + } catch (error) { + console.log(error); + return ApiResponse.serverError(res, error as string); + } +} + +const validateRequestBody = (req: NextApiRequest) => { + const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = req.body; + if (!alias || typeof alias !== 'string') { + throw new Error('Alias must be a non-empty string'); + } + if (!sshPublicKey || typeof sshPublicKey !== 'string') { + throw new Error('SSH Public Key must be a non-empty string'); + } + if (typeof storageSize !== 'number' || storageSize <= 0 || !Number.isInteger(storageSize)) { + throw new Error('Storage Size must be a positive integer'); + } + if (typeof comment !== 'string') { + throw new Error('Comment must be a string'); + } + if (typeof alert !== 'number') { + throw new Error('Alert must be a number'); + } + if (typeof lanCommand !== 'boolean') { + throw new Error('Lan Command must be a boolean'); + } + if (typeof appendOnlyMode !== 'boolean') { + throw new Error('Append Only Mode must be a boolean'); + } +}; diff --git a/types/domain/config.types.ts b/types/domain/config.types.ts index 19378fb..6a7ecca 100644 --- a/types/domain/config.types.ts +++ b/types/domain/config.types.ts @@ -11,8 +11,8 @@ export type Repository = { storageUsed: number; sshPublicKey: string; comment: string; - displayDetails: boolean; - unixUser: string; + displayDetails?: boolean; // @deprecated + unixUser?: string; // @deprecated lanCommand?: boolean; appendOnlyMode?: boolean; lastStatusAlertSend?: number;