mirror of
https://github.com/Ravinou/borgwarehouse
synced 2026-03-14 14:25:46 +01:00
refactor: ⚡ status API + misc
This commit is contained in:
parent
da60d50dcb
commit
dac0c41df4
32 changed files with 600 additions and 250 deletions
|
|
@ -9,7 +9,7 @@ import { AppriseModeEnum } from '~/types/domain/config.types';
|
|||
//Components
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import { Optional } from '~/types';
|
||||
import { AppriseModeDTO } from '~/types/api/notifications.types';
|
||||
import { AppriseModeDTO } from '~/types/api/notification.types';
|
||||
import { useFormStatus } from '~/hooks/useFormStatus';
|
||||
|
||||
type AppriseModeDataForm = {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useForm } from 'react-hook-form';
|
|||
//Components
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import { Optional } from '~/types';
|
||||
import { AppriseServicesDTO } from '~/types/api/notifications.types';
|
||||
import { AppriseServicesDTO } from '~/types/api/notification.types';
|
||||
import { useFormStatus } from '~/hooks/useFormStatus';
|
||||
|
||||
type AppriseURLsDataForm = {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Error from '~/Components/UI/Error/Error';
|
|||
import Switch from '~/Components/UI/Switch/Switch';
|
||||
import { useFormStatus } from '~/hooks/useFormStatus';
|
||||
import { Optional } from '~/types';
|
||||
import { EmailAlertDTO } from '~/types/api/notifications.types';
|
||||
import { EmailAlertDTO } from '~/types/api/notification.types';
|
||||
|
||||
export default function EmailAlertSettings() {
|
||||
//Var
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { SpinnerDotted } from 'spinners-react';
|
|||
import Error from '~/Components/UI/Error/Error';
|
||||
import Info from '~/Components/UI/Info/Info';
|
||||
import { useFormStatus } from '~/hooks/useFormStatus';
|
||||
import { EmailSettingDTO } from '~/types/api/settings.types';
|
||||
import { EmailSettingDTO } from '~/types/api/setting.types';
|
||||
|
||||
export default function EmailSettings(props: EmailSettingDTO) {
|
||||
//Var
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
IntegrationTokenType,
|
||||
TokenPermissionEnum,
|
||||
TokenPermissionsType,
|
||||
} from '~/types/api/integrations.types';
|
||||
} from '~/types/api/integration.types';
|
||||
import { useFormStatus } from '~/hooks/useFormStatus';
|
||||
import { Optional } from '~/types';
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { SpinnerDotted } from 'spinners-react';
|
|||
//Components
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import { useFormStatus } from '~/hooks/useFormStatus';
|
||||
import { PasswordSettingDTO } from '~/types/api/settings.types';
|
||||
import { PasswordSettingDTO } from '~/types/api/setting.types';
|
||||
|
||||
export default function PasswordSettings() {
|
||||
//Var
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { SpinnerDotted } from 'spinners-react';
|
|||
import Error from '~/Components/UI/Error/Error';
|
||||
import Info from '~/Components/UI/Info/Info';
|
||||
import { useFormStatus } from '~/hooks/useFormStatus';
|
||||
import { UsernameSettingDTO } from '~/types/api/settings.types';
|
||||
import { UsernameSettingDTO } from '~/types/api/setting.types';
|
||||
|
||||
export default function UsernameSettings(props: UsernameSettingDTO) {
|
||||
//Var
|
||||
|
|
|
|||
27
helpers/functions/apiResponse.ts
Normal file
27
helpers/functions/apiResponse.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { NextApiResponse } from 'next';
|
||||
|
||||
export default class ApiResponse {
|
||||
static success<T>(res: NextApiResponse, message = 'Success', data?: T) {
|
||||
res.status(200).json({ status: 200, message, data });
|
||||
}
|
||||
|
||||
static unauthorized(res: NextApiResponse, message = 'Unauthorized') {
|
||||
res.status(401).json({ status: 401, message });
|
||||
}
|
||||
|
||||
static forbidden(res: NextApiResponse, message = 'Forbidden') {
|
||||
res.status(403).json({ status: 403, message });
|
||||
}
|
||||
|
||||
static notFound(res: NextApiResponse, message = 'Not Found') {
|
||||
res.status(404).json({ status: 404, message });
|
||||
}
|
||||
|
||||
static validationError(res: NextApiResponse, message = 'Validation Error') {
|
||||
res.status(422).json({ status: 422, message });
|
||||
}
|
||||
|
||||
static serverError(res: NextApiResponse, message = 'API error, contact the administrator.') {
|
||||
res.status(500).json({ status: 500, message });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,45 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { BorgWarehouseUser } from '~/types/domain/config.types';
|
||||
import { Optional } from '~/types';
|
||||
import { BorgWarehouseUser, Repository } from '~/types/domain/config.types';
|
||||
|
||||
// Paths definition
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
const usersFilePath = path.join(jsonDirectory, 'users.json');
|
||||
const repoFilePath = path.join(jsonDirectory, 'repo.json');
|
||||
|
||||
export const getUsersList = async (): Promise<BorgWarehouseUser[]> => {
|
||||
const fileContent = await fs.readFile(usersFilePath, 'utf8');
|
||||
return JSON.parse(fileContent);
|
||||
try {
|
||||
const fileContent = await fs.readFile(usersFilePath, 'utf8');
|
||||
return JSON.parse(fileContent) || [];
|
||||
} catch (error) {
|
||||
console.error('Error reading users.json:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUsersList = async (usersList: BorgWarehouseUser[]): Promise<void> => {
|
||||
await fs.writeFile(usersFilePath, JSON.stringify(usersList, null, 2));
|
||||
try {
|
||||
await fs.writeFile(usersFilePath, JSON.stringify(usersList, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error writing users.json:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getRepoList = async (): Promise<Repository[]> => {
|
||||
try {
|
||||
const fileContent = await fs.readFile(repoFilePath, 'utf8');
|
||||
return JSON.parse(fileContent) || [];
|
||||
} catch (error) {
|
||||
console.error('Error reading repo.json:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRepoList = async (repoList: Repository[]): Promise<void> => {
|
||||
try {
|
||||
await fs.writeFile(repoFilePath, JSON.stringify(repoList, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error writing repo.json:', error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { Optional } from '~/types';
|
||||
import { Repository } from '~/types/domain/config.types';
|
||||
|
||||
/**
|
||||
* Checks if the given SSH public key is duplicated in the provided repository list by removing the comment part.
|
||||
*
|
||||
|
|
@ -6,7 +9,10 @@
|
|||
* @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) {
|
||||
export default function isSshPubKeyDuplicate(
|
||||
pubKey: string,
|
||||
repoList: Array<Optional<Repository>>
|
||||
) {
|
||||
if (!pubKey || !repoList || !Array.isArray(repoList)) {
|
||||
throw new Error('Missing or invalid parameters for duplicate SSH public key check.');
|
||||
}
|
||||
|
|
@ -16,7 +22,7 @@ export default function isSshPubKeyDuplicate(pubKey, repoList) {
|
|||
|
||||
// Check if the normalized key is already in the repository list
|
||||
return repoList.some((repo) => {
|
||||
const repoSshKeyWithoutComment = repo.sshPublicKey.split(' ').slice(0, 2).join(' ');
|
||||
const repoSshKeyWithoutComment = repo?.sshPublicKey.split(' ').slice(0, 2).join(' ');
|
||||
return repoSshKeyWithoutComment === pubKeyWithoutComment;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
12
helpers/functions/shell.utils.ts
Normal file
12
helpers/functions/shell.utils.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import path from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { exec as execCallback } from 'node:child_process';
|
||||
import { LastSaveDTO } from '~/types/api/shell.types';
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
export const getLastSaveList = async (): Promise<LastSaveDTO[]> => {
|
||||
const shellsDirectory = path.join(process.cwd(), '/helpers');
|
||||
const { stdout } = await exec(`${shellsDirectory}/shells/getLastSave.sh`);
|
||||
return JSON.parse(stdout || '[]');
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
export default function emailTest(mailTo, username, aliasList) {
|
||||
const aliasTemplate = (x) => {
|
||||
export default function emailTest(mailTo: string, username: string, aliasList: string[]) {
|
||||
const aliasTemplate = (x: string[]) => {
|
||||
let str = '';
|
||||
for (const alias of x) {
|
||||
str = str + '<li>' + alias + '</li>';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { authOptions } from '../auth/[...nextauth]';
|
|||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { BorgWarehouseUser } from '~/types/domain/config.types';
|
||||
import { AppriseAlertResponse } from '~/types/api/notifications.types';
|
||||
import { AppriseAlertResponse } from '~/types/api/notification.types';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
|
||||
export default async function handler(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import path from 'path';
|
|||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { AppriseModeDTO } from '~/types/api/notifications.types';
|
||||
import { AppriseModeDTO } from '~/types/api/notification.types';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
import { BorgWarehouseUser } from '~/types/domain/config.types';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import path from 'path';
|
|||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { AppriseServicesDTO } from '~/types/api/notifications.types';
|
||||
import { AppriseServicesDTO } from '~/types/api/notification.types';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
import { BorgWarehouseUser } from '~/types/domain/config.types';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import path from 'path';
|
|||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { EmailAlertDTO } from '~/types/api/notifications.types';
|
||||
import { EmailAlertDTO } from '~/types/api/notification.types';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
import { BorgWarehouseUser } from '~/types/domain/config.types';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { getUsersList, updateUsersList } from '~/helpers/functions/fileHelpers';
|
|||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { AppriseAlertDTO, AppriseAlertResponse } from '~/types/api/notifications.types';
|
||||
import { AppriseAlertDTO, AppriseAlertResponse } from '~/types/api/notification.types';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
|
||||
export default async function handler(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { getUsersList, updateUsersList } from '~/helpers/functions/fileHelpers';
|
|||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { AppriseModeDTO } from '~/types/api/notifications.types';
|
||||
import { AppriseModeDTO } from '~/types/api/notification.types';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
|
||||
export default async function handler(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { AppriseServicesDTO } from '~/types/api/notifications.types';
|
||||
import { AppriseServicesDTO } from '~/types/api/notification.types';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
import { getUsersList, updateUsersList } from '~/helpers/functions/fileHelpers';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { getUsersList, updateUsersList } from '~/helpers/functions/fileHelpers';
|
|||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { EmailSettingDTO } from '~/types/api/settings.types';
|
||||
import { EmailSettingDTO } from '~/types/api/setting.types';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
|
||||
export default async function handler(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { getUsersList, updateUsersList } from '~/helpers/functions/fileHelpers';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { EmailAlertDTO } from '~/types/api/notifications.types';
|
||||
import { EmailAlertDTO } from '~/types/api/notification.types';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { authOptions } from '../auth/[...nextauth]';
|
|||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
import { PasswordSettingDTO } from '~/types/api/settings.types';
|
||||
import { PasswordSettingDTO } from '~/types/api/setting.types';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest & { body: PasswordSettingDTO },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { getUsersList, updateUsersList } from '~/helpers/functions';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { UsernameSettingDTO } from '~/types/api/settings.types';
|
||||
import { UsernameSettingDTO } from '~/types/api/setting.types';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { ErrorResponse } from '~/types/api/error.types';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,223 +0,0 @@
|
|||
// This API is design to be used by a cron (of your choice). Call it with curl for example
|
||||
//(e.g : curl --request POST --url 'http://localhost:3000/api/cronjob/checkStatus' --header 'Authorization: Bearer 5173f388c0f4a0df92d1412c3036ddc897c22e4448')
|
||||
|
||||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
const util = require('node:util');
|
||||
const exec = util.promisify(require('node:child_process').exec);
|
||||
import nodemailerSMTP from '../../../helpers/functions/nodemailerSMTP';
|
||||
import emailAlertStatus from '../../../helpers/templates/emailAlertStatus';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.headers.authorization == null) {
|
||||
res.status(401).json({
|
||||
status: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const CRONJOB_KEY = process.env.CRONJOB_KEY;
|
||||
const ACTION_KEY = req.headers.authorization.split(' ')[1];
|
||||
|
||||
if (req.method == 'POST' && ACTION_KEY === CRONJOB_KEY) {
|
||||
//Var
|
||||
let newRepoList;
|
||||
let repoListToSendAlert = [];
|
||||
let usersList;
|
||||
const date = Math.round(Date.now() / 1000);
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
|
||||
////PART 1 : Status
|
||||
try {
|
||||
//Check if there are some repositories
|
||||
let repoList = await fs.readFile(jsonDirectory + '/repo.json', 'utf8');
|
||||
repoList = JSON.parse(repoList);
|
||||
if (repoList.length === 0) {
|
||||
res.status(200).json({
|
||||
success: 'Status cron has been executed. No repository to check.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Call the shell : getLastSave.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/getLastSave.sh`);
|
||||
if (stderr) {
|
||||
console.log('stderr:', stderr);
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'Error on getting the date for last save, contact the administrator.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
//Parse the JSON output of getLastSave.sh to use it
|
||||
const lastSave = JSON.parse(stdout);
|
||||
|
||||
//Rebuild a newRepoList with the lastSave timestamp updated and the status updated.
|
||||
newRepoList = repoList;
|
||||
for (let index in newRepoList) {
|
||||
const repoFiltered = lastSave.filter(
|
||||
(x) => x.repositoryName === newRepoList[index].repositoryName
|
||||
);
|
||||
if (repoFiltered.length === 1) {
|
||||
//Write the timestamp of the last save
|
||||
newRepoList[index].lastSave = repoFiltered[0].lastSave;
|
||||
//Trigger the status if the last save is older than alert setting.
|
||||
if (date - newRepoList[index].lastSave > newRepoList[index].alert) {
|
||||
newRepoList[index].status = false;
|
||||
} else if (date - newRepoList[index].lastSave < newRepoList[index].alert) {
|
||||
newRepoList[index].status = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: "API error : can't update the status.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//// PART 2 : check if there is a repo that need an alert
|
||||
try {
|
||||
//Here, a mail is sent every 24H (90000) if a repo has down status
|
||||
for (let index in newRepoList) {
|
||||
if (
|
||||
!newRepoList[index].status &&
|
||||
newRepoList[index].alert !== 0 &&
|
||||
(!newRepoList[index].lastStatusAlertSend ||
|
||||
date - newRepoList[index].lastStatusAlertSend > 90000)
|
||||
) {
|
||||
repoListToSendAlert.push(newRepoList[index].alias);
|
||||
newRepoList[index].lastStatusAlertSend = date;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: "API error : can't check if a repo needs an email alert.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//PART 3 : Save the new repoList
|
||||
try {
|
||||
//Stringify the repoList to write it into the json file.
|
||||
newRepoList = JSON.stringify(newRepoList);
|
||||
//Write the new json
|
||||
await fs.writeFile(jsonDirectory + '/repo.json', newRepoList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: "API error : can't write the new repoList.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
//PART 4 : Send the alerts
|
||||
if (repoListToSendAlert.length > 0) {
|
||||
// Read user informations
|
||||
try {
|
||||
//Read the email of the user
|
||||
usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: "API error : can't read user information.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
////EMAIL
|
||||
// If the user has enabled email alerts
|
||||
if (usersList[0].emailAlert) {
|
||||
//Send mail
|
||||
//Create the SMTP Transporter
|
||||
const transporter = nodemailerSMTP();
|
||||
//Mail options
|
||||
const mailData = emailAlertStatus(
|
||||
usersList[0].email,
|
||||
usersList[0].username,
|
||||
repoListToSendAlert
|
||||
);
|
||||
transporter.sendMail(mailData, function (err, info) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
console.log(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
////APPRISE
|
||||
// If the user has enabled Apprise alerts
|
||||
if (usersList[0].appriseAlert) {
|
||||
let appriseServicesURLs = '';
|
||||
for (let service of usersList[0].appriseServices) {
|
||||
appriseServicesURLs = appriseServicesURLs + service + ' ';
|
||||
}
|
||||
//Mode : package
|
||||
if (usersList[0].appriseMode === 'package') {
|
||||
try {
|
||||
//Send notification via local package.
|
||||
await exec(
|
||||
`apprise -v -b '🔴 Some repositories on BorgWarehouse need attention !\nList of down repositories :\n ${repoListToSendAlert}' ${appriseServicesURLs}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err.stderr);
|
||||
res.status(500).json({
|
||||
message: 'Error : ' + err.stderr,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Mode : stateless
|
||||
} else if (usersList[0].appriseMode === 'stateless') {
|
||||
try {
|
||||
await fetch(usersList[0].appriseStatelessURL + '/notify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
urls: appriseServicesURLs,
|
||||
body:
|
||||
'🔴 Some repositories on BorgWarehouse need attention !\nList of down repositories :\n' +
|
||||
repoListToSendAlert,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({
|
||||
message: 'Error : ' + err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Mode : unknown
|
||||
} else {
|
||||
res.status(422).json({
|
||||
message: 'No Apprise Mode selected or supported.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//PART 5 : Sucess
|
||||
res.status(200).json({
|
||||
success: 'Status cron has been executed.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
res.status(401).json({
|
||||
status: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
101
pages/api/cronjob/checkStatus.ts
Normal file
101
pages/api/cronjob/checkStatus.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { getUnixTime } from 'date-fns';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { exec as execCallback } from 'node:child_process';
|
||||
import { promisify } from 'util';
|
||||
import ApiResponse from '~/helpers/functions/apiResponse';
|
||||
import { getRepoList, getUsersList, updateRepoList } from '~/helpers/functions/fileHelpers';
|
||||
import nodemailerSMTP from '~/helpers/functions/nodemailerSMTP';
|
||||
import { getLastSaveList } from '~/helpers/functions/shell.utils';
|
||||
import emailAlertStatus from '~/helpers/templates/emailAlertStatus';
|
||||
import { BorgWarehouseApiResponse } from '~/types/api/error.types';
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<BorgWarehouseApiResponse>
|
||||
) {
|
||||
if (!req.headers.authorization) {
|
||||
return ApiResponse.unauthorized(res);
|
||||
}
|
||||
|
||||
const CRONJOB_KEY = process.env.CRONJOB_KEY;
|
||||
const ACTION_KEY = req.headers.authorization.split(' ')[1];
|
||||
|
||||
if (req.method !== 'POST' || ACTION_KEY !== CRONJOB_KEY) {
|
||||
return ApiResponse.unauthorized(res);
|
||||
}
|
||||
|
||||
try {
|
||||
const repoList = await getRepoList();
|
||||
const lastSaveList = await getLastSaveList();
|
||||
if (repoList.length === 0 || lastSaveList.length === 0) {
|
||||
return ApiResponse.success(res, 'Status cron executed. No repository to check.');
|
||||
}
|
||||
const date = getUnixTime(new Date());
|
||||
|
||||
// Update the status and the last timestamp backup of each repository
|
||||
const updatedRepoList = repoList.map((repo) => {
|
||||
const repoFiltered = lastSaveList.find((x) => x.repositoryName === repo.repositoryName);
|
||||
if (!repoFiltered) return repo;
|
||||
const lastSaveTimestamp = repoFiltered.lastSave;
|
||||
return {
|
||||
...repo,
|
||||
lastSave: lastSaveTimestamp,
|
||||
status: date - lastSaveTimestamp <= (repo?.alert ?? 0),
|
||||
};
|
||||
});
|
||||
|
||||
const repoListToSendAlert: string[] = [];
|
||||
updatedRepoList.forEach((repo) => {
|
||||
if (
|
||||
!repo.status &&
|
||||
repo.alert !== 0 &&
|
||||
(!repo.lastStatusAlertSend || date - repo.lastStatusAlertSend > 90000)
|
||||
) {
|
||||
repo.lastStatusAlertSend = date;
|
||||
repoListToSendAlert.push(repo.alias);
|
||||
}
|
||||
});
|
||||
|
||||
if (repoListToSendAlert.length > 0) {
|
||||
const usersList = await getUsersList();
|
||||
|
||||
// Send Email Alert
|
||||
if (usersList[0].emailAlert) {
|
||||
const transporter = nodemailerSMTP();
|
||||
const mailData = emailAlertStatus(
|
||||
usersList[0].email,
|
||||
usersList[0].username,
|
||||
repoListToSendAlert
|
||||
);
|
||||
transporter.sendMail(mailData, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
// Send Apprise Alert
|
||||
if (usersList[0].appriseAlert) {
|
||||
const appriseServicesURLs = usersList[0].appriseServices?.join(' ');
|
||||
const message = `🔴 Some repositories on BorgWarehouse need attention !\nList of down repositories :\n ${repoListToSendAlert}`;
|
||||
if (usersList[0].appriseMode === 'package') {
|
||||
await exec(`apprise -v -b '${message}' ${appriseServicesURLs}`);
|
||||
} else if (usersList[0].appriseMode === 'stateless') {
|
||||
await fetch(`${usersList[0].appriseStatelessURL}/notify`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ urls: appriseServicesURLs, body: message }),
|
||||
});
|
||||
} else {
|
||||
return ApiResponse.validationError(res, 'No Apprise Mode selected or supported.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateRepoList(updatedRepoList);
|
||||
return ApiResponse.success(res, 'Status cron executed successfully.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return ApiResponse.serverError(res);
|
||||
}
|
||||
}
|
||||
387
tests/supertest/checkStatus.test.ts
Normal file
387
tests/supertest/checkStatus.test.ts
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
import { createMocks } from 'node-mocks-http';
|
||||
import handler from '~/pages/api/cronjob/checkStatus';
|
||||
import { getRepoList, getUsersList, updateRepoList } from '~/helpers/functions/fileHelpers';
|
||||
import { getLastSaveList } from '~/helpers/functions/shell.utils';
|
||||
import nodemailerSMTP from '~/helpers/functions/nodemailerSMTP';
|
||||
|
||||
jest.mock('~/helpers/functions/fileHelpers', () => ({
|
||||
getRepoList: jest.fn(),
|
||||
getUsersList: jest.fn(),
|
||||
updateRepoList: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/helpers/functions/shell.utils', () => ({
|
||||
getLastSaveList: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/helpers/functions/nodemailerSMTP', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({
|
||||
sendMail: jest.fn().mockResolvedValue({ messageId: 'fake-message-id' }),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('~/helpers/templates/emailAlertStatus', () =>
|
||||
jest.fn(() => ({
|
||||
subject: 'Alert',
|
||||
text: 'Alert text',
|
||||
}))
|
||||
);
|
||||
|
||||
jest.mock('node:child_process', () => ({
|
||||
exec: jest.fn(
|
||||
(
|
||||
command: string,
|
||||
callback: (err: Error | null, result: { stdout: string; stderr: string }) => void
|
||||
) => {
|
||||
callback(null, { stdout: 'mocked output', stderr: '' });
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('Cronjob API Handler', () => {
|
||||
beforeEach(() => {
|
||||
process.env.CRONJOB_KEY = 'test-key';
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('should return 401 if no authorization header', async () => {
|
||||
const { req, res } = createMocks({ method: 'POST' });
|
||||
await handler(req, res);
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
});
|
||||
|
||||
it('should return 401 if method is not POST', async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: 'GET',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
});
|
||||
|
||||
it('should return 401 if wrong authorization key', async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer wrong-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
});
|
||||
|
||||
it('should return 200 with message if no repository to check (empty repoList)', async () => {
|
||||
(getRepoList as jest.Mock).mockResolvedValue([]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([{ repositoryName: 'repo1', lastSave: 123 }]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
status: 200,
|
||||
message: 'Status cron executed. No repository to check.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 with message if no repository to check (empty lastSaveList)', async () => {
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', alert: 100, alias: 'Repo1' },
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
status: 200,
|
||||
message: 'Status cron executed. No repository to check.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute successfully without alerts if all repositories are OK', async () => {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', alert: 1000, alias: 'Repo1', status: true },
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', lastSave: currentTime },
|
||||
]);
|
||||
(updateRepoList as jest.Mock).mockResolvedValue(undefined);
|
||||
(getUsersList as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
status: 200,
|
||||
message: 'Status cron executed successfully.',
|
||||
});
|
||||
expect(updateRepoList).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 500 if an error occurs', async () => {
|
||||
(getRepoList as jest.Mock).mockRejectedValue(new Error('Test error'));
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(500);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send email alert if emailAlert is false', async () => {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', alert: 100, alias: 'Repo1' },
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
|
||||
]);
|
||||
// User has disabled email alert but enabled Apprise alert
|
||||
(getUsersList as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
emailAlert: false,
|
||||
appriseAlert: true,
|
||||
appriseServices: ['http://example.com'],
|
||||
appriseMode: 'package',
|
||||
appriseStatelessURL: 'http://example.com',
|
||||
email: 'test@example.com',
|
||||
username: 'testuser',
|
||||
},
|
||||
]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
|
||||
expect(nodemailerSMTP).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not send apprise alert if appriseAlert is false', async () => {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', alert: 100, alias: 'Repo1' },
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
|
||||
]);
|
||||
// User has disabled Apprise alert but enabled email alert
|
||||
(getUsersList as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
emailAlert: true,
|
||||
appriseAlert: false,
|
||||
appriseServices: ['http://example.com'],
|
||||
appriseMode: 'package',
|
||||
appriseStatelessURL: 'http://example.com',
|
||||
email: 'test@example.com',
|
||||
username: 'testuser',
|
||||
},
|
||||
]);
|
||||
|
||||
// Spy on exec to check if it is called
|
||||
const execSpy = jest.spyOn(require('node:child_process'), 'exec');
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
|
||||
expect(execSpy).not.toHaveBeenCalled();
|
||||
execSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not send alert if alert is disabled on repo (repo.alert === 0)', async () => {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', alert: 0, alias: 'Repo1' },
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', lastSave: currentTime - 1000 },
|
||||
]);
|
||||
(getUsersList as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
emailAlert: true,
|
||||
appriseAlert: true,
|
||||
appriseServices: ['http://example.com'],
|
||||
appriseMode: 'package',
|
||||
appriseStatelessURL: 'http://example.com',
|
||||
email: 'test@example.com',
|
||||
username: 'testuser',
|
||||
},
|
||||
]);
|
||||
|
||||
// Spy on exec to check if it is called
|
||||
const nodemailerSpy = jest.spyOn(require('~/helpers/functions/nodemailerSMTP'), 'default');
|
||||
const execSpy = jest.spyOn(require('node:child_process'), 'exec');
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
await handler(req, res);
|
||||
|
||||
expect(nodemailerSpy).not.toHaveBeenCalled();
|
||||
expect(execSpy).not.toHaveBeenCalled();
|
||||
|
||||
nodemailerSpy.mockRestore();
|
||||
execSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not update lastStatusAlertSend or add to repoListToSendAlert if repo status is OK', async () => {
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', status: true, alert: 100 },
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', lastSave: Math.floor(Date.now() / 1000) },
|
||||
]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(updateRepoList).toHaveBeenCalledWith([
|
||||
{
|
||||
repositoryName: 'repo1',
|
||||
status: true,
|
||||
alert: 100,
|
||||
lastSave: expect.any(Number),
|
||||
},
|
||||
]);
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
});
|
||||
|
||||
it('should update lastStatusAlertSend if repo is down and alert is enabled', async () => {
|
||||
const currentTime = 1741535661;
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
repositoryName: 'repo1',
|
||||
alias: 'Repo1',
|
||||
status: false,
|
||||
alert: 100,
|
||||
},
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
|
||||
]);
|
||||
(getUsersList as jest.Mock).mockResolvedValue([
|
||||
{ emailAlert: true, email: 'test@example.com', username: 'TestUser' },
|
||||
]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(updateRepoList).toHaveBeenCalledWith([
|
||||
{
|
||||
repositoryName: 'repo1',
|
||||
alias: 'Repo1',
|
||||
status: false,
|
||||
alert: 100,
|
||||
lastStatusAlertSend: expect.any(Number),
|
||||
lastSave: expect.any(Number),
|
||||
},
|
||||
]);
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
});
|
||||
|
||||
it('should not update lastStatusAlertSend or send alerts if alert is disabled', async () => {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
repositoryName: 'repo1',
|
||||
alias: 'Repo1',
|
||||
status: false,
|
||||
alert: 0,
|
||||
lastStatusAlertSend: null,
|
||||
},
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
|
||||
]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(updateRepoList).toHaveBeenCalledWith([
|
||||
{
|
||||
repositoryName: 'repo1',
|
||||
alias: 'Repo1',
|
||||
status: false,
|
||||
alert: 0,
|
||||
lastStatusAlertSend: null,
|
||||
lastSave: currentTime - 200,
|
||||
},
|
||||
]);
|
||||
expect(nodemailerSMTP).not.toHaveBeenCalled();
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
});
|
||||
|
||||
it('should update lastStatusAlertSend only if the last alert was sent more than 90000 seconds ago', async () => {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
(getRepoList as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
repositoryName: 'repo1',
|
||||
alias: 'Repo1',
|
||||
status: false,
|
||||
alert: 100,
|
||||
lastStatusAlertSend: currentTime - 80000,
|
||||
},
|
||||
]);
|
||||
(getLastSaveList as jest.Mock).mockResolvedValue([
|
||||
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
|
||||
]);
|
||||
(getUsersList as jest.Mock).mockResolvedValue([
|
||||
{ emailAlert: true, email: 'test@example.com', username: 'TestUser' },
|
||||
]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
headers: { authorization: 'Bearer test-key' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(updateRepoList).toHaveBeenCalledWith([
|
||||
{
|
||||
repositoryName: 'repo1',
|
||||
alias: 'Repo1',
|
||||
status: false,
|
||||
alert: 100,
|
||||
lastStatusAlertSend: expect.any(Number),
|
||||
lastSave: currentTime - 200,
|
||||
},
|
||||
]);
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
});
|
||||
});
|
||||
|
|
@ -6,3 +6,9 @@ export type ErrorResponse = {
|
|||
export type SuccessResponse = {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type BorgWarehouseApiResponse<T = any> = {
|
||||
status: number;
|
||||
message: string;
|
||||
data?: T;
|
||||
};
|
||||
|
|
|
|||
4
types/api/shell.types.ts
Normal file
4
types/api/shell.types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export type LastSaveDTO = {
|
||||
repositoryName: string;
|
||||
lastSave: number;
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { IntegrationTokenType } from '../api/integrations.types';
|
||||
import { IntegrationTokenType } from '../api/integration.types';
|
||||
|
||||
export type Repository = {
|
||||
id: number;
|
||||
|
|
@ -15,6 +15,7 @@ export type Repository = {
|
|||
unixUser: string;
|
||||
lanCommand?: boolean;
|
||||
appendOnlyMode?: boolean;
|
||||
lastStatusAlertSend?: number;
|
||||
};
|
||||
|
||||
export type BorgWarehouseUser = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue