Merge pull request #56 from Ravinou/v2.0

V2.0
This commit is contained in:
Ravinou 2023-10-15 11:11:00 +02:00 committed by GitHub
commit 70dbd64aff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 944 additions and 488 deletions

7
.dockerignore Normal file
View file

@ -0,0 +1,7 @@
node_modules
.git
.gitignore
.pre-commit-config.yaml
.prettierrc.json
.env.local
.next

5
.gitignore vendored
View file

@ -108,4 +108,7 @@ dist
# config file for BorgWarehouse
config/repo.json
config/users.json
config/users.json
# docker files
docker-compose.yml

21
.prettierrc.json Normal file
View file

@ -0,0 +1,21 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"singleAttributePerLine": false,
"bracketSameLine": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"useTabs": false,
"embeddedLanguageFormatting": "auto"
}

View file

@ -6,19 +6,20 @@ import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react';
export default function QuickCommands(props) {
////Vars
const wizardEnv = props.wizardEnv;
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
let HOSTNAME;
let FQDN;
let SSH_SERVER_PORT;
if (
props.lanCommand &&
process.env.NEXT_PUBLIC_HOSTNAME_LAN &&
process.env.NEXT_PUBLIC_SSH_SERVER_PORT_LAN
wizardEnv.FQDN_LAN &&
wizardEnv.SSH_SERVER_PORT_LAN
) {
HOSTNAME = process.env.NEXT_PUBLIC_HOSTNAME_LAN;
SSH_SERVER_PORT = process.env.NEXT_PUBLIC_SSH_SERVER_PORT_LAN;
FQDN = wizardEnv.FQDN_LAN;
SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT_LAN;
} else {
HOSTNAME = process.env.NEXT_PUBLIC_HOSTNAME;
SSH_SERVER_PORT = process.env.NEXT_PUBLIC_SSH_SERVER_PORT;
FQDN = wizardEnv.FQDN;
SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT;
}
//State
@ -29,7 +30,7 @@ export default function QuickCommands(props) {
// Asynchronously call copy to clipboard
navigator.clipboard
.writeText(
`borg init -e repokey-blake2 ssh://${props.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.repository}`
`ssh://${wizardEnv.UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.repositoryName}`
)
.then(() => {
// If successful, update the isCopied state value
@ -49,9 +50,9 @@ export default function QuickCommands(props) {
<div className={classes.copyValid}>Copied !</div>
) : (
<div className={classes.tooltip}>
borg init -e repokey-blake2 ssh://{props.unixUser}@
{HOSTNAME}:{SSH_SERVER_PORT}/./
{props.repository}
ssh://{wizardEnv.UNIX_USER}@
{FQDN}:{SSH_SERVER_PORT}/./
{props.repositoryName}
</div>
)}
<div className={classes.icons}>

View file

@ -78,9 +78,9 @@ export default function Repo(props) {
</div>
)}
<QuickCommands
unixUser={props.unixUser}
repository={props.repository}
repositoryName={props.repositoryName}
lanCommand={props.lanCommand}
wizardEnv={props.wizardEnv}
/>
</div>
@ -103,7 +103,7 @@ export default function Repo(props) {
</thead>
<tbody>
<tr>
<th>{props.repository}</th>
<th>{props.repositoryName}</th>
<th>{props.storageSize}Go</th>
<th style={{ padding: '0 4% 0 4%' }}>
<StorageBar

View file

@ -3,7 +3,9 @@
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
width: auto;
max-height: 65px;
margin: 20px 0px 0px 0px;
@ -29,7 +31,9 @@
flex-direction: column;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
width: auto;
max-height: 200px;
margin: 20px 0px 0px 0px;
@ -183,7 +187,9 @@
margin: 0px 0 0 20px;
opacity: 1;
transition: 0.5s opacity;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
box-shadow:
0 3px 6px rgba(0, 0, 0, 0.16),
0 3px 6px rgba(0, 0, 0, 0.23);
overflow: auto;
}

View file

@ -3,10 +3,14 @@
text-align: center;
position: absolute;
bottom: 0;
width: calc(100% + 70px);
width: 100%;
height: 50px;
}
.footer p {
padding-left: 70px;
}
a.site {
color: #6d4aff;
text-decoration: none;
@ -16,4 +20,7 @@ a.site {
.footer {
width: 100%;
}
.footer p {
padding-left: 0;
}
}

View file

@ -1,23 +1,21 @@
//Lib
import classes from "./Header.module.css";
import classes from './Header.module.css';
//Components
import Nav from "./Nav/Nav";
import Nav from './Nav/Nav';
function Header() {
return (
<header className={classes.Header}>
<div className={[classes.flex, 'container'].join(' ')}>
<div className={classes.logo}>
BorgWarehouse
</div>
<div className={classes.logo}>BorgWarehouse</div>
<nav>
<Nav />
</nav>
</nav>
</div>
</header>
)
);
}
export default Header;
export default Header;

View file

@ -1,7 +1,9 @@
.Header {
width: 100%;
background: #111827;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
box-shadow:
0 3px 6px rgba(0, 0, 0, 0.16),
0 3px 6px rgba(0, 0, 0, 0.23);
height: 50px;
color: white;
display: flex;

View file

@ -51,7 +51,9 @@
opacity: 0;
transform: scale(1);
pointer-events: none;
transition: opacity 0.3s 0.1s, transform 0.2s 0.1s;
transition:
opacity 0.3s 0.1s,
transform 0.2s 0.1s;
}
/* Span */
@ -75,7 +77,9 @@
height: 14px;
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
vertical-align: top;
transition: background-color 0.2s, opacity 0.2s;
transition:
background-color 0.2s,
opacity 0.2s;
}
/* Thumb */
@ -88,9 +92,13 @@
width: 20px;
height: 20px;
background-color: rgb(var(--pure-material-onprimary-rgb, 255, 255, 255));
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
transition: background-color 0.2s, transform 0.2s;
box-shadow:
0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
transition:
background-color 0.2s,
transform 0.2s;
}
/* Checked */
@ -112,7 +120,9 @@
.pureMaterialSwitch > input:active {
opacity: 1;
transform: scale(0);
transition: transform 0s, opacity 0s;
transition:
transform 0s,
opacity 0s;
}
.pureMaterialSwitch > input:active + span::before {

View file

@ -1,6 +1,8 @@
.container {
margin: 40px 20px 20px 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
border-radius: 5px;
text-align: left;
padding: 30px 70px;
@ -54,8 +56,15 @@ h1 .icon {
.code {
background-color: #111827;
color: #f8f8f2;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
liberation mono, courier new, monospace;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
liberation mono,
courier new,
monospace;
padding: 5px 15px;
border-radius: 5px;
display: inline-block;
@ -91,8 +100,15 @@ h1 .icon {
.verifyOrange li .sshPublicKey {
background-color: #282a36;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
liberation mono, courier new, monospace;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
liberation mono,
courier new,
monospace;
border-radius: 5px;
padding: 5px;
}

View file

@ -3,23 +3,17 @@ import React from 'react';
import classes from '../WizardStep1/WizardStep1.module.css';
import { IconTool, IconAlertCircle } from '@tabler/icons-react';
import CopyButton from '../../UI/CopyButton/CopyButton';
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
function WizardStep2(props) {
////Vars
const wizardEnv = props.wizardEnv;
const UNIX_USER = wizardEnv.UNIX_USER;
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
let HOSTNAME;
let SSH_SERVER_PORT;
if (
props.selectedOption.lanCommand &&
process.env.NEXT_PUBLIC_HOSTNAME_LAN &&
process.env.NEXT_PUBLIC_SSH_SERVER_PORT_LAN
) {
HOSTNAME = process.env.NEXT_PUBLIC_HOSTNAME_LAN;
SSH_SERVER_PORT = process.env.NEXT_PUBLIC_SSH_SERVER_PORT_LAN;
} else {
HOSTNAME = process.env.NEXT_PUBLIC_HOSTNAME;
SSH_SERVER_PORT = process.env.NEXT_PUBLIC_SSH_SERVER_PORT;
}
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
wizardEnv,
props.selectedOption.lanCommand
);
return (
<div className={classes.container}>
@ -39,12 +33,11 @@ function WizardStep2(props) {
>
<div className={classes.code}>
borg init -e repokey-blake2 ssh://
{props.selectedOption.unixUser}@{HOSTNAME}:
{SSH_SERVER_PORT}/./
{props.selectedOption.repository}
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
</div>
<CopyButton
dataToCopy={`borg init -e repokey-blake2 ssh://${props.selectedOption.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.selectedOption.repository}`}
dataToCopy={`borg init -e repokey-blake2 ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
/>
</div>
<div className={classes.note}>
@ -95,12 +88,11 @@ function WizardStep2(props) {
>
<div className={classes.code}>
ssh://
{props.selectedOption.unixUser}@{HOSTNAME}:
{SSH_SERVER_PORT}/./
{props.selectedOption.repository}
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
</div>
<CopyButton
dataToCopy={`ssh://${props.selectedOption.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.selectedOption.repository}`}
dataToCopy={`ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
/>
</div>
For more information about the Vorta graphical client, please
@ -122,24 +114,21 @@ function WizardStep2(props) {
<b>Check the fingerprint of server</b>
</div>
To check that you are talking to the right server, please make
sure to validate the following sshPublicKeys when you first
connect :
sure to validate one of the following key's fingerprint when you
first connect :
<li>
<span className={classes.sshPublicKey}>
ECDSA :{' '}
{process.env.NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ECDSA}
ECDSA : {wizardEnv.SSH_SERVER_FINGERPRINT_ECDSA}
</span>
</li>
<li>
<span className={classes.sshPublicKey}>
ED25519 :{' '}
{process.env.NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ED25519}
ED25519 : {wizardEnv.SSH_SERVER_FINGERPRINT_ED25519}
</span>
</li>
<li>
<span className={classes.sshPublicKey}>
RSA :{' '}
{process.env.NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_RSA}
RSA : {wizardEnv.SSH_SERVER_FINGERPRINT_RSA}
</span>
</li>
</div>

View file

@ -3,23 +3,17 @@ import React from 'react';
import classes from '../WizardStep1/WizardStep1.module.css';
import { IconChecks, IconPlayerPlay } from '@tabler/icons-react';
import CopyButton from '../../UI/CopyButton/CopyButton';
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
function WizardStep3(props) {
////Vars
const wizardEnv = props.wizardEnv;
const UNIX_USER = wizardEnv.UNIX_USER;
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
let HOSTNAME;
let SSH_SERVER_PORT;
if (
props.selectedOption.lanCommand &&
process.env.NEXT_PUBLIC_HOSTNAME_LAN &&
process.env.NEXT_PUBLIC_SSH_SERVER_PORT_LAN
) {
HOSTNAME = process.env.NEXT_PUBLIC_HOSTNAME_LAN;
SSH_SERVER_PORT = process.env.NEXT_PUBLIC_SSH_SERVER_PORT_LAN;
} else {
HOSTNAME = process.env.NEXT_PUBLIC_HOSTNAME;
SSH_SERVER_PORT = process.env.NEXT_PUBLIC_SSH_SERVER_PORT;
}
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
wizardEnv,
props.selectedOption.lanCommand
);
return (
<div className={classes.container}>
@ -38,13 +32,12 @@ function WizardStep3(props) {
>
<div className={classes.code}>
borg create ssh://
{props.selectedOption.unixUser}@{HOSTNAME}:
{SSH_SERVER_PORT}/./
{props.selectedOption.repository}
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
::archive1 /your/pathToBackup
</div>
<CopyButton
dataToCopy={`borg create ssh://${props.selectedOption.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.selectedOption.repository}::archive1 /your/pathToBackup`}
dataToCopy={`borg create ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 /your/pathToBackup`}
/>
</div>
</div>
@ -85,12 +78,11 @@ function WizardStep3(props) {
>
<div className={classes.code}>
borg check -v --progress ssh://
{props.selectedOption.unixUser}@{HOSTNAME}:
{SSH_SERVER_PORT}/./
{props.selectedOption.repository}
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
</div>
<CopyButton
dataToCopy={`borg check -v --progress ssh://${props.selectedOption.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.selectedOption.repository}`}
dataToCopy={`borg check -v --progress ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
/>
</div>
<li>List the remote archives with :</li>
@ -103,12 +95,11 @@ function WizardStep3(props) {
>
<div className={classes.code}>
borg list ssh://
{props.selectedOption.unixUser}@{HOSTNAME}:
{SSH_SERVER_PORT}/./
{props.selectedOption.repository}
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
</div>
<CopyButton
dataToCopy={`borg list ssh://${props.selectedOption.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.selectedOption.repository}`}
dataToCopy={`borg list ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
/>
</div>
<li>Download a remote archive with the following command :</li>
@ -121,13 +112,12 @@ function WizardStep3(props) {
>
<div className={classes.code}>
borg export-tar --tar-filter="gzip -9" ssh://
{props.selectedOption.unixUser}@{HOSTNAME}:
{SSH_SERVER_PORT}/./
{props.selectedOption.repository}
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
::archive1 archive1.tar.gz
</div>
<CopyButton
dataToCopy={`borg export-tar --tar-filter="gzip -9" ssh://${props.selectedOption.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.selectedOption.repository}::archive1 archive1.tar.gz`}
dataToCopy={`borg export-tar --tar-filter="gzip -9" ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 archive1.tar.gz`}
/>
</div>
<li>
@ -143,13 +133,12 @@ function WizardStep3(props) {
>
<div className={classes.code}>
borg mount ssh://
{props.selectedOption.unixUser}@{HOSTNAME}:
{SSH_SERVER_PORT}/./
{props.selectedOption.repository}
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
::archive1 /tmp/yourMountPoint
</div>
<CopyButton
dataToCopy={`borg mount ssh://${props.selectedOption.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.selectedOption.repository}::archive1 /tmp/yourMountPoint`}
dataToCopy={`borg mount ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 /tmp/yourMountPoint`}
/>
</div>
<br />

View file

@ -3,23 +3,17 @@ import React from 'react';
import classes from '../WizardStep1/WizardStep1.module.css';
import { IconWand } from '@tabler/icons-react';
import CopyButton from '../../UI/CopyButton/CopyButton';
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
function WizardStep4(props) {
////Vars
const wizardEnv = props.wizardEnv;
const UNIX_USER = wizardEnv.UNIX_USER;
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
let HOSTNAME;
let SSH_SERVER_PORT;
if (
props.selectedOption.lanCommand &&
process.env.NEXT_PUBLIC_HOSTNAME_LAN &&
process.env.NEXT_PUBLIC_SSH_SERVER_PORT_LAN
) {
HOSTNAME = process.env.NEXT_PUBLIC_HOSTNAME_LAN;
SSH_SERVER_PORT = process.env.NEXT_PUBLIC_SSH_SERVER_PORT_LAN;
} else {
HOSTNAME = process.env.NEXT_PUBLIC_HOSTNAME;
SSH_SERVER_PORT = process.env.NEXT_PUBLIC_SSH_SERVER_PORT;
}
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
wizardEnv,
props.selectedOption.lanCommand
);
const configBorgmatic = `location:
# List of source directories to backup.
@ -29,10 +23,10 @@ function WizardStep4(props) {
repositories:
# Paths of local or remote repositories to backup to.
- ssh://${props.selectedOption.unixUser}@${HOSTNAME}:${SSH_SERVER_PORT}/./${props.selectedOption.repository}
- ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}
storage:
archive_name_format: '{HOSTNAME}-documents-{now}'
archive_name_format: '{FQDN}-documents-{now}'
encryption_passphrase: "YOUR PASSPHRASE"
retention:

View file

@ -43,11 +43,27 @@ export default function RepoList() {
if (router.pathname.startsWith('/manage-repo/edit')) {
setDisplayRepoEdit(!displayRepoEdit);
}
//Fetch wizardEnv to hydrate Repo components
const fetchWizardEnv = async () => {
try {
const response = await fetch('/api/account/getWizardEnv', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
setWizardEnv((await response.json()).wizardEnv);
} catch (error) {
console.log('Fetching datas error');
}
};
fetchWizardEnv();
}, []);
////States
const [displayRepoAdd, setDisplayRepoAdd] = useState(false);
const [displayRepoEdit, setDisplayRepoEdit] = useState(false);
const [wizardEnv, setWizardEnv] = useState({});
////Functions
@ -100,14 +116,14 @@ export default function RepoList() {
alias={repo.alias}
status={repo.status}
lastSave={repo.lastSave}
repository={repo.repository}
repositoryName={repo.repositoryName}
storageSize={repo.storageSize}
storageUsed={repo.storageUsed}
sshPublicKey={repo.sshPublicKey}
unixUser={repo.unixUser}
comment={repo.comment}
lanCommand={repo.lanCommand}
repoManageEditHandler={() => repoManageEditHandler(repo.id)}
wizardEnv={wizardEnv}
></Repo>
</>
);

View file

@ -108,10 +108,58 @@ export default function RepoManage(props) {
});
};
//Verify that the SSH key is unique
const isSSHKeyUnique = async (sshPublicKey) => {
let isUnique = true;
// Extract the first two columns of the SSH key in the form
const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
await fetch('/api/repo', { method: 'GET' })
.then((response) => response.json())
.then((data) => {
for (let element in data.repoList) {
// Extract the first two columns of the SSH key in the repoList
const repoPublicKeyPrefix = data.repoList[
element
].sshPublicKey
.split(' ')
.slice(0, 2)
.join(' ');
if (
repoPublicKeyPrefix === publicKeyPrefix && // Compare the first two columns of the SSH key
(!targetRepo ||
data.repoList[element].id != targetRepo.id)
) {
toast.error(
'The SSH key is already used in repository #' +
data.repoList[element].id +
'. Please use another key or delete the key from the other repository.',
toastOptions
);
isUnique = false;
break;
}
}
})
.catch((error) => {
console.log(error);
toast.error('An error has occurred', toastOptions);
isUnique = false;
});
return isUnique;
};
//Form submit Handler for ADD or EDIT a repo
const formSubmitHandler = async (dataForm) => {
//Loading button on submit to avoid multiple send.
setIsLoading(true);
//Verify that the SSH key is unique
if (!(await isSSHKeyUnique(dataForm.sshkey))) {
setIsLoading(false);
return;
}
//ADD a repo
if (props.mode == 'add') {
const newRepo = {
@ -130,7 +178,7 @@ export default function RepoManage(props) {
},
body: JSON.stringify(newRepo),
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
toast.success(
'New repository added ! 🥳',
@ -138,9 +186,13 @@ export default function RepoManage(props) {
);
router.replace('/');
} else {
toast.error('An error has occurred', toastOptions);
const errorMessage = await response.json();
toast.error(
`An error has occurred : ${errorMessage.message}`,
toastOptions
);
router.replace('/');
console.log('Fail to post');
console.log(`Fail to ${props.mode}`);
}
})
.catch((error) => {
@ -165,7 +217,7 @@ export default function RepoManage(props) {
},
body: JSON.stringify(dataEdited),
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
toast.success(
'The repository #' +
@ -175,9 +227,13 @@ export default function RepoManage(props) {
);
router.replace('/');
} else {
toast.error('An error has occurred', toastOptions);
const errorMessage = await response.json();
toast.error(
`An error has occurred : ${errorMessage.message}`,
toastOptions
);
router.replace('/');
console.log('Fail to PUT');
console.log(`Fail to ${props.mode}`);
}
})
.catch((error) => {

View file

@ -1,268 +1,268 @@
.modaleWrapper {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
margin: 50px 0px 0px 70px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
margin: 50px 0px 0px 70px;
}
.modale {
position: fixed;
top: 10%;
width: 1000px;
height: auto;
max-width: 75%;
max-height: 85%;
background: #fff;
padding: 20px 20px 20px;
overflow: auto;
border-radius: 10px;
box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.4);
margin: 0 auto;
animation: append-animate 0.3s linear;
position: fixed;
top: 10%;
width: 1000px;
height: auto;
max-width: 75%;
max-height: 85%;
background: #fff;
padding: 20px 20px 20px;
overflow: auto;
border-radius: 10px;
box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.4);
margin: 0 auto;
animation: append-animate 0.3s linear;
}
@keyframes append-animate {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.close {
cursor: pointer;
margin-left: 95%;
color: #494b7a;
cursor: pointer;
margin-left: 95%;
color: #494b7a;
}
.close :hover {
color: #aa60ff;
color: #aa60ff;
}
.repoManageForm {
margin: auto;
width: 80%;
padding: 15px 30px 30px 30px;
border-radius: 5px;
text-align: left;
margin: auto;
width: 80%;
padding: 15px 30px 30px 30px;
border-radius: 5px;
text-align: left;
}
.formWrapper {
text-align: center;
margin: auto;
width: 100%;
height: auto;
color: #494b7a;
text-align: center;
margin: auto;
width: 100%;
height: auto;
color: #494b7a;
}
.repoManageForm label {
display: block;
margin-bottom: 8px;
text-align: center;
margin-top: 20px;
color: #494b7a;
display: block;
margin-bottom: 8px;
text-align: center;
margin-top: 20px;
color: #494b7a;
}
.repoManageForm input,
.repoManageForm textarea,
.repoManageForm select {
border: 1px solid #6d4aff21;
font-size: 16px;
height: auto;
margin: 0;
margin-bottom: 0px;
outline: 0;
padding: 15px;
width: 100%;
background-color: #f5f5f5;
border-radius: 5px;
/* color: #1b1340; */
color: #494b7a;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
border: 1px solid #6d4aff21;
font-size: 16px;
height: auto;
margin: 0;
margin-bottom: 0px;
outline: 0;
padding: 15px;
width: 100%;
background-color: #f5f5f5;
border-radius: 5px;
/* color: #1b1340; */
color: #494b7a;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
}
.repoManageForm textarea {
resize: vertical;
resize: vertical;
}
.repoManageForm textarea:focus,
.repoManageForm input:focus,
.repoManageForm select:focus {
outline: 1px solid #6d4aff;
box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
outline: 1px solid #6d4aff;
box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
}
.repoManageForm .invalid {
background: #f3c7c7;
border: 1px solid #e45454;
outline: 1px solid #ff4a4a;
background: #f3c7c7;
border: 1px solid #e45454;
outline: 1px solid #ff4a4a;
}
.repoManageForm .invalid:focus {
background: #f3c7c7;
border: 1px solid #e45454;
outline: 1px solid #ff4a4a;
box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
background: #f3c7c7;
border: 1px solid #e45454;
outline: 1px solid #ff4a4a;
box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
}
.repoManageForm button {
display: block;
margin: 15px auto;
display: block;
margin: 15px auto;
}
.repoManageForm button:hover {
display: block;
margin: 15px auto;
display: block;
margin: 15px auto;
}
.errorMessage {
color: red;
display: block;
margin-top: 3px;
color: red;
display: block;
margin-top: 3px;
}
.lanCommandWrapper {
display: flex;
margin-top: 20px;
color: #494b7a;
display: flex;
margin-top: 20px;
color: #494b7a;
}
.lanCommandWrapper label {
margin: 0;
margin: 0;
}
.lanCommandWrapper input[type='checkbox'] {
width: auto;
margin-right: 8px;
cursor: pointer;
accent-color: #6d4aff;
width: auto;
margin-right: 8px;
cursor: pointer;
accent-color: #6d4aff;
}
.lanCommandWrapper input[type='checkbox']:focus {
outline: 0;
box-shadow: none;
accent-color: #6d4aff;
outline: 0;
box-shadow: none;
accent-color: #6d4aff;
}
/* DELETE DIALOG */
.deleteDialogWrapper {
text-align: center;
margin: auto;
width: 80%;
height: 100%;
max-height: 590px;
color: #111827;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
margin: auto;
width: 80%;
height: 100%;
max-height: 590px;
color: #111827;
display: flex;
flex-direction: column;
justify-content: center;
}
.deleteDialogMessage {
background-color: #ea1313;
color: #fff;
font-weight: 500;
border-radius: 5px;
padding: 15px;
margin-top: 15px;
margin-bottom: 15px;
font-size: 1.1em;
background-color: #ea1313;
color: #fff;
font-weight: 500;
border-radius: 5px;
padding: 15px;
margin-top: 15px;
margin-bottom: 15px;
font-size: 1.1em;
}
.cancelButton {
border: 0;
padding: 10px 15px;
background-color: #c1c1c1;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
border: 0;
padding: 10px 15px;
background-color: #c1c1c1;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
}
.cancelButton:hover {
border: 0;
padding: 10px 15px;
background-color: #9a9a9a;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
border: 0;
padding: 10px 15px;
background-color: #9a9a9a;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
}
.cancelButton:active {
border: 0;
padding: 10px 15px;
background-color: #9a9a9a;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
transform: scale(0.9);
border: 0;
padding: 10px 15px;
background-color: #9a9a9a;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
transform: scale(0.9);
}
.deleteButton {
border: 0;
padding: 10px 15px;
background-color: #ff0000;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
border: 0;
padding: 10px 15px;
background-color: #ff0000;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
}
.deleteButton:hover {
border: 0;
padding: 10px 15px;
background-color: #ff4b4b;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
border: 0;
padding: 10px 15px;
background-color: #ff4b4b;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
}
.deleteButton:active {
border: 0;
padding: 10px 15px;
background-color: #ff4b4b;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
transform: scale(0.9);
border: 0;
padding: 10px 15px;
background-color: #ff4b4b;
color: white;
margin: 5px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1em;
transform: scale(0.9);
}
.littleDeleteButton {
border: none;
font-weight: 300;
color: red;
text-decoration: underline;
background: none;
cursor: pointer;
border: none;
font-weight: 300;
color: red;
text-decoration: underline;
background: none;
cursor: pointer;
}
.selectAlert {
margin: auto auto 35px auto;
max-width: 160px;
margin: auto auto 35px auto;
max-width: 160px;
}

View file

@ -20,10 +20,10 @@ function SetupWizard(props) {
const [list, setList] = useState([]);
const [listIsLoading, setListIsLoading] = useState(true);
const [step, setStep] = useState();
const [wizardEnv, setWizardEnv] = useState({});
const [selectedOption, setSelectedOption] = useState({
id: '#id',
repository: 'repo',
unixUser: 'user',
});
////LifeCycle
@ -45,6 +45,21 @@ function SetupWizard(props) {
}
};
repoList();
//Fetch wizardEnv to hydrate Wizard' steps
const fetchWizardEnv = async () => {
try {
const response = await fetch('/api/account/getWizardEnv', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
setWizardEnv((await response.json()).wizardEnv);
} catch (error) {
console.log('Fetching datas error');
}
};
fetchWizardEnv();
}, []);
//Component did update
useEffect(() => {
@ -59,8 +74,7 @@ function SetupWizard(props) {
label: `${repo.alias} - #${repo.id}`,
value: `${repo.alias} - #${repo.id}`,
id: repo.id,
repository: repo.repository,
unixUser: repo.unixUser,
repositoryName: repo.repositoryName,
lanCommand: repo.lanCommand,
}));
@ -86,11 +100,26 @@ function SetupWizard(props) {
if (step == 1) {
return <WizardStep1 />;
} else if (step == 2) {
return <WizardStep2 selectedOption={selectedOption} />;
return (
<WizardStep2
selectedOption={selectedOption}
wizardEnv={wizardEnv}
/>
);
} else if (step == 3) {
return <WizardStep3 selectedOption={selectedOption} />;
return (
<WizardStep3
selectedOption={selectedOption}
wizardEnv={wizardEnv}
/>
);
} else {
return <WizardStep4 selectedOption={selectedOption} />;
return (
<WizardStep4
selectedOption={selectedOption}
wizardEnv={wizardEnv}
/>
);
}
};

View file

@ -21,7 +21,7 @@ export default function AppriseMode() {
const [modeFormIsSaved, setModeFormIsSaved] = useState(false);
const [error, setError] = useState(false);
const [displayStatelessURL, setDisplayStatelessURL] = useState(false);
const [appriseMode, setAppriseMode] = useState('');
const [appriseMode, setAppriseMode] = useState('stateless');
const [appriseStatelessURL, setAppriseStatelessURL] = useState();
////LifeCycle

34
Dockerfile Normal file
View file

@ -0,0 +1,34 @@
FROM node:18-bookworm-slim
RUN apt-get update && apt-get install -y \
curl git jq jc borgbackup openssh-server sudo cron && \
apt-get upgrade -y && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN echo "borgwarehouse ALL=(ALL) NOPASSWD: /usr/sbin/service ssh restart" >> /etc/sudoers
RUN echo "borgwarehouse ALL=(ALL) NOPASSWD: /usr/sbin/service cron restart" >> /etc/sudoers
RUN groupadd borgwarehouse
RUN useradd -m -g borgwarehouse borgwarehouse
RUN cp /etc/ssh/sshd_config /etc/ssh/moduli /home/borgwarehouse/
WORKDIR /home/borgwarehouse/app
COPY . .
RUN chown -R borgwarehouse:borgwarehouse * .*
USER borgwarehouse
RUN npm ci --only=production
RUN npm run build
EXPOSE 3000 22
ENTRYPOINT ["./docker-bw-init.sh"]
CMD ["npm", "run", "start"]

View file

@ -1,7 +1,7 @@
<div align="center">
[![Next][Next.js]][Next-url]
[![React][React.js]][React-url]
[![Next][Next.js]][Next-url]
[![React][React.js]][React-url]
</div>
@ -50,33 +50,34 @@ Required variables :
- `NEXTAUTH_URL` : The url of your application as **https://borgwarehouse.com**.
- `NEXTAUTH_SECRET` : A secret random key.
- `CRONJOB_KEY` : A secret API key for cronjob.
- `NEXT_PUBLIC_HOSTNAME` : FQDN as **borgwarehouse.com**
- `NEXT_PUBLIC_SSH_SERVER_PORT` : SSH port of your server as **22**.
- `NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_RSA` : Your server SSH fingerprint for RSA.
- `NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ED25519` : Your server SSH fingerprint for ED25519.
- `NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ECDSA` : Your server SSH fingerprint for ECDSA.
- `UNIX_USER` : The unix user that run BorgWarehouse app.
- `FQDN` : FQDN as **borgwarehouse.com**
- `SSH_SERVER_PORT` : SSH port of your server as **22**.
- `SSH_SERVER_FINGERPRINT_RSA` : Your server SSH fingerprint for RSA.
- `SSH_SERVER_FINGERPRINT_ED25519` : Your server SSH fingerprint for ED25519.
- `SSH_SERVER_FINGERPRINT_ECDSA` : Your server SSH fingerprint for ECDSA.
Example for a valid `.env.local` file :
```bash
# Private variable
# Application's variables
NEXTAUTH_URL=https://yourbwdomain.com
NEXTAUTH_SECRET=YOURFIRSTSECRET
CRONJOB_KEY=YOURSECONDSECRET
# Wizard's variables
UNIX_USER=borgwarehouse
FQDN=yourbwdomain.com
SSH_SERVER_PORT=22
SSH_SERVER_FINGERPRINT_RSA=SHA256:36mfYNRrm1aconVt6cBpi8LhAoPP4kB8QsVW4n8eGHQ
SSH_SERVER_FINGERPRINT_ED25519=SHA256:tYQuzrZZMqaw0Bzvn/sMoDs1CVEitZ9IrRyUg02yTPA
SSH_SERVER_FINGERPRINT_ECDSA=SHA256:nTpxui1oEmH9konPau17qBVIzBQVOsD1BIbBFU5IL04
# SMTP's variables
MAIL_SMTP_FROM=
MAIL_SMTP_HOST=
MAIL_SMTP_PORT=
MAIL_SMTP_LOGIN=
MAIL_SMTP_PWD=
MAIL_REJECT_SELFSIGNED_TLS=true
# Public variable (Any change need a rebuild of app)
NEXT_PUBLIC_HOSTNAME=yourbwdomain.com
NEXT_PUBLIC_SSH_SERVER_PORT=22
NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_RSA=SHA256:36mfYNRrm1aconVt6cBpi8LhAoPP4kB8QsVW4n8eGHQ
NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ED25519=SHA256:tYQuzrZZMqaw0Bzvn/sMoDs1CVEitZ9IrRyUg02yTPA
NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ECDSA=SHA256:nTpxui1oEmH9konPau17qBVIzBQVOsD1BIbBFU5IL04
# Disable NextJS telemetry
NEXT_TELEMETRY_DISABLED=1
```

91
docker-bw-init.sh Executable file
View file

@ -0,0 +1,91 @@
#!/bin/bash
set -e
SSH_DIR="/home/borgwarehouse/.ssh"
AUTHORIZED_KEYS_FILE="$SSH_DIR/authorized_keys"
REPOS_DIR="/home/borgwarehouse/repos"
print_green() {
echo -e "\e[92m$1\e[0m";
}
print_red() {
echo -e "\e[91m$1\e[0m";
}
init_ssh_server() {
if [ -z "$(ls -A /etc/ssh)" ]; then
print_green "/etc/ssh is empty, generating SSH host keys..."
ssh-keygen -A
cp /home/borgwarehouse/sshd_config /home/borgwarehouse/moduli /etc/ssh/
fi
}
check_ssh_directory() {
if [ ! -d "$SSH_DIR" ]; then
print_red "The .ssh directory does not exist, you need to mount it as docker volume."
exit 1
else
chmod 700 "$SSH_DIR"
fi
}
create_authorized_keys_file() {
if [ ! -f "$AUTHORIZED_KEYS_FILE" ]; then
print_green "The authorized_keys file does not exist, creating..."
touch "$AUTHORIZED_KEYS_FILE"
fi
chmod 600 "$AUTHORIZED_KEYS_FILE"
}
check_repos_directory() {
if [ ! -d "$REPOS_DIR" ]; then
print_red "The repos directory does not exist, you need to mount it as docker volume."
exit 2
else
chmod 700 "$REPOS_DIR"
fi
}
add_cron_job() {
print_green "Adding cron job..."
local CRON_JOB="* * * * * curl --request POST --url 'http://localhost:3000/api/cronjob/checkStatus' --header 'Authorization: Bearer $CRONJOB_KEY'; curl --request POST --url 'http://localhost:3000/api/cronjob/getStorageUsed' --header 'Authorization: Bearer $CRONJOB_KEY'"
echo "$CRON_JOB" | crontab -u borgwarehouse -
}
get_SSH_fingerprints() {
print_green "Getting SSH fingerprints..."
RSA_FINGERPRINT=$(ssh-keygen -lf /etc/ssh/ssh_host_rsa_key | awk '{print $2}')
ED25519_FINGERPRINT=$(ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key | awk '{print $2}')
ECDSA_FINGERPRINT=$(ssh-keygen -lf /etc/ssh/ssh_host_ecdsa_key | awk '{print $2}')
export SSH_SERVER_FINGERPRINT_RSA="$RSA_FINGERPRINT"
export SSH_SERVER_FINGERPRINT_ED25519="$ED25519_FINGERPRINT"
export SSH_SERVER_FINGERPRINT_ECDSA="$ECDSA_FINGERPRINT"
}
check_env() {
if [ -z "$CRONJOB_KEY" ]; then
CRONJOB_KEY=$(openssl rand -base64 32)
print_green "CRONJOB_KEY not found or empty. Generating a random key..."
export CRONJOB_KEY
fi
if [ -z "$NEXTAUTH_SECRET" ]; then
NEXTAUTH_SECRET=$(openssl rand -base64 32)
print_green "NEXTAUTH_SECRET not found or empty. Generating a random key..."
export NEXTAUTH_SECRET
fi
}
check_env
init_ssh_server
check_ssh_directory
create_authorized_keys_file
check_repos_directory
add_cron_job
get_SSH_fingerprints
sudo service ssh restart
sudo service cron restart
exec "$@"

32
docker-compose.yml Normal file
View file

@ -0,0 +1,32 @@
version: '3'
services:
borgwarehouse:
container_name: borgwarehouse
# If you want to build the image yourself, uncomment the following lines and comment the image line
#build:
# context: .
# dockerfile: Dockerfile
image: borgwarehouse/borgwarehouse
# UID:GID must match the user and group ID of the host folders and must be > 1000
user: '1001:1001'
ports:
- '3000:3000'
- '2222:22'
environment:
- NEXTAUTH_URL=https://your.domain.com
- NEXTAUTH_SECRET=your-secret
- CRONJOB_KEY=your-other-secret
# The SSH_SERVER_PORT must match the port exposed above
- SSH_SERVER_PORT=2222
- FQDN=your.domain.com
volumes:
# The host folders must be owned by the user with UID and GID specified above
- <host-folder>/config:/home/borgwarehouse/app/config
- <host-folder>/ssh:/home/borgwarehouse/.ssh
- <host-folder>/ssh_host:/etc/ssh
- <host-folder>/repos:/home/borgwarehouse/repos
# Apprise is used to send notifications, it's optional. http://apprise:8000 is the URL to use in BorgWarehouse.
apprise:
container_name: apprise
image: caronc/apprise
user: 'www-data:www-data'

View file

@ -1,9 +0,0 @@
borgwarehouse ALL=(ALL) NOPASSWD: /usr/sbin/userdel -rf [[\:xdigit\:]]*
borgwarehouse ALL=(ALL) NOPASSWD: /usr/sbin/useradd -d /var/borgwarehouse/[[\:xdigit\:]]* -s /bin/bash -m --badname [[\:xdigit\:]]*
borgwarehouse ALL=(ALL) NOPASSWD: /usr/bin/mkdir -p /var/borgwarehouse/[[\:xdigit\:]]*/repos/repo[[\:digit\:]]*, /usr/bin/mkdir -p /var/borgwarehouse/[[\:xdigit\:]]*/.ssh
borgwarehouse ALL=(ALL) NOPASSWD: /usr/bin/touch /var/borgwarehouse/[[\:xdigit\:]]*/.ssh/authorized_keys
borgwarehouse ALL=(ALL) NOPASSWD: /usr/bin/chmod -R 750 /var/borgwarehouse/[[\:xdigit\:]]*, /usr/bin/chmod 600 /var/borgwarehouse/[[\:xdigit\:]]*/.ssh/authorized_keys
borgwarehouse ALL=(ALL) NOPASSWD: /usr/bin/chown -R [[\:xdigit\:]]*\:borgwarehouse /var/borgwarehouse/[[\:xdigit\:]]*
borgwarehouse ALL=(ALL) NOPASSWD: /usr/bin/tee /var/borgwarehouse/[[\:xdigit\:]]*/.ssh/authorized_keys
borgwarehouse ALL=(ALL) NOPASSWD: /usr/bin/sed -ri s|*|g /var/borgwarehouse/[[\:xdigit\:]]*/.ssh/authorized_keys
borgwarehouse ALL=(ALL) NOPASSWD: /usr/bin/du -s -- [[\:xdigit\:]]*

View file

@ -0,0 +1,13 @@
export default function lanCommandOption(wizardEnv, lanCommand) {
let FQDN;
let SSH_SERVER_PORT;
if (lanCommand && wizardEnv.FQDN_LAN && wizardEnv.SSH_SERVER_PORT_LAN) {
FQDN = wizardEnv.FQDN_LAN;
SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT_LAN;
} else {
FQDN = wizardEnv.FQDN;
SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT;
}
return { FQDN, SSH_SERVER_PORT };
}

View file

@ -0,0 +1,45 @@
import { promises as fs } from 'fs';
import path from 'path';
export default async function repoHistory(data) {
try {
const repoHistoryDir = path.join(process.cwd(), '/config/versions');
const maxBackupCount = parseInt(process.env.MAX_REPO_BACKUP_COUNT) || 8;
const timestamp = new Date().toISOString();
const backupDate = timestamp.split('T')[0];
//Create the directory if it does not exist
await fs.mkdir(repoHistoryDir, { recursive: true });
const existingBackups = await fs.readdir(repoHistoryDir);
if (existingBackups.length >= maxBackupCount) {
existingBackups.sort();
const backupsToDelete = existingBackups.slice(
0,
existingBackups.length - maxBackupCount + 1
);
for (const backupToDelete of backupsToDelete) {
const backupFilePathToDelete = path.join(
repoHistoryDir,
backupToDelete
);
await fs.unlink(backupFilePathToDelete);
}
}
const backupFileName = `${backupDate}.log`;
const backupFilePath = path.join(repoHistoryDir, backupFileName);
const jsonData = JSON.stringify(data, 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
);
}
}

View file

@ -1,87 +1,77 @@
#!/usr/bin/env bash
# Shell created by Raven for BorgWarehouse.
# This shell takes 3 arguments : [reponame] X [SSH pub key] X [quota]
# This shell takes 2 arguments : [SSH pub key] X [quota]
# Main steps are :
# - check if args are present
# - check the ssh pub key format
# - check if the ssh pub key is already present in authorized_keys
# - check if borgbackup package is install
# - generate a random username, check if it exists in /etc/passwd
# - add the user (with random name), group, shell and home
# - create a pool which is the folder where all the repositories for a user are located (only one by user for borgwarehouse usage)
# - create the authorized_keys
# - generate a random repositoryName
# - add the SSH public key in the authorized_keys with borg restriction for repository and storage quota.
# This simple method prevents the user from connecting to the server with a shell in SSH.
# He can only use the borg command. Moreover, he will not be able to leave his repository or create a new one.
# It is similar to a jail and that is the goal.
# Limitation : all SSH pubkey are unique : https://github.com/borgbackup/borg/issues/7757
# Exit when any command fails
set -e
# Load .env if exists
if [[ -f .env ]]; then
source .env
fi
# Default value if .env not exists
: "${home:=/home/borgwarehouse}"
# Some variables
pool="${home}/repos"
authorized_keys="${home}/.ssh/authorized_keys"
# Check args
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ];then
echo "This shell takes 3 argument : Reponame, SSH Public Key, Quota in Go [e.g. : 10] "
if [ "$1" == "" ] || [ "$2" == "" ];then
echo -n "This shell takes 2 arguments : SSH Public Key, Quota in Go [e.g. : 10] "
exit 1
fi
# Check if the SSH public key is a valid format
# This pattern validates SSH public keys for : rsa, ed25519, ed25519-sk
pattern='(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?'
if [[ ! "$2" =~ $pattern ]]
if [[ ! "$1" =~ $pattern ]]
then
echo "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
echo -n "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
exit 2
fi
# Check if SSH pub key is already present in authorized_keys
if grep -q "$1" "$authorized_keys"; then
echo -n "SSH pub key already present in authorized_keys"
exit 3
fi
# Check if borgbackup is installed
if ! [ -x "$(command -v borg)" ]; then
echo "You must install borgbackup package."
exit 3
echo -n "You must install borgbackup package."
exit 4
fi
# Generation of a random for username
randUsername () {
# Generation of a random for repositoryName
randRepositoryName () {
openssl rand -hex 4
}
user=$(randUsername)
# Check if the random is already a username.
while grep -q "$user" /etc/passwd
do
user=$(randUsername)
done
# Some variables
home="/var/borgwarehouse/${user}"
pool="${home}/repos"
authorized_keys="${home}/.ssh/authorized_keys"
## add user and create homedirectory ${user} - [shell=/bin/bash home=${home} group=${group}]
sudo useradd -d "${home}" -s "/bin/bash" -m --badname "${user}"
## Create directory ${home}/.ssh
sudo mkdir -p "${home}/.ssh"
## Create autorized_keys file
sudo touch "${home}/.ssh/authorized_keys"
## Create the repo
sudo mkdir -p "${pool}/$1"
## Change permissions
sudo chmod -R 750 "${home}"
sudo chmod 600 "${authorized_keys}"
sudo chown -R "${user}:borgwarehouse" "${home}"
repositoryName=$(randRepositoryName)
## Check if authorized_keys exists
if [ ! -f "${authorized_keys}" ];then
echo "${authorized_keys} must be present"
exit 4
echo -n "${authorized_keys} must be present"
exit 5
fi
## Add ssh public key in authorized_keys with borg restriction for only 1 repository (:$1) and storage quota
restricted_authkeys="command=\"cd ${pool};borg serve --restrict-to-repository ${pool}/$1 --storage-quota $3G\",restrict $2"
echo "$restricted_authkeys" | sudo tee "${authorized_keys}" >/dev/null
## Add ssh public key in authorized_keys with borg restriction for only 1 repository and storage quota
restricted_authkeys="command=\"cd ${pool};borg serve --restrict-to-path ${pool}/${repositoryName} --storage-quota $2G\",restrict $1"
echo "$restricted_authkeys" | tee -a "${authorized_keys}" >/dev/null
## Return the unix user
echo "${user}"
## Return the repositoryName
echo "${repositoryName}"

View file

@ -1,32 +1,47 @@
#!/usr/bin/env bash
# Shell created by Raven for BorgWarehouse.
# This shell takes 1 arg : [user] with 8 char. length only.
# This shell **delete the user** in arg and **all his data**.
# This shell takes 1 arg : [repositoryName] with 8 char. length only.
# This shell **delete the repository** in arg and **all his data** and the line associated in the authorized_keys file.
# Exit when any command fails
set -e
# Load .env if exists
if [[ -f .env ]]; then
source .env
fi
# Default value if .env not exists
: "${home:=/home/borgwarehouse}"
# Some variables
pool="${home}/repos"
authorized_keys="${home}/.ssh/authorized_keys"
# Check arg
if [[ $# -ne 1 || $1 = "" ]]; then
echo "You must provide a username in argument."
echo -n "You must provide a repositoryName in argument."
exit 1
fi
# Check if username length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
# Check if the repositoryName length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
# If we receive another length there is necessarily a problem.
username=$1
if [ ${#username} != 8 ]
then
echo "Error with the length of the username."
repositoryName=$1
if [ ${#repositoryName} != 8 ]; then
echo -n "Error with the length of the repositoryName."
exit 2
fi
# Delete the user if it exists
if id "$1" &>/dev/null; then
sudo userdel -rf "$1"
echo "The user $1 and all his data have been deleted"
# Delete the repository and the line associated in the authorized_keys file
if [ -d "${pool}/${repositoryName}" ]; then
# Delete the repository
rm -rf "${pool}/${repositoryName}"
# Delete the line in the authorized_keys file
sed -i "/${repositoryName}/d" "${authorized_keys}"
echo -n "The folder "${pool}/${repositoryName}" and all its data have been deleted. The line associated in the authorized_keys file has been deleted."
else
echo "The user $1 does not exist"
exit 3
fi
# Delete the line in the authorized_keys file
sed -i "/${repositoryName}/d" "${authorized_keys}"
echo -n "The folder "${pool}/${repositoryName}" did not exist (repository never initialized or used). The line associated in the authorized_keys file has been deleted."
fi

View file

@ -5,16 +5,12 @@
# stdout will be an array like :
# [
# {
# "user": "09d8240f",
# "lastSave": 1668513608
# "repositoryName": "a7035047",
# "lastSave": 1691341603
# },
# {
# "user": "635a6f8b",
# "lastSave": 1667910810
# },
# {
# "user": "83bd4ef1",
# "lastSave": 1667985985
# "repositoryName": "a7035048",
# "lastSave": 1691342688
# }
# ]
@ -22,6 +18,18 @@
# Exit when any command fails
set -e
stat --format='{"user":"%U","lastSave":%Y}' \
/var/borgwarehouse/*/repos/*/integrity* |
jq --slurp
# Load .env if exists
if [[ -f .env ]]; then
source .env
fi
# Default value if .env not exists
: "${home:=/home/borgwarehouse}"
if [ -n "$(find ${home}/repos -mindepth 1 -maxdepth 1 -type d)" ]; then
stat --format='{"repositoryName":"%n","lastSave":%Y}' \
${home}/repos/*/integrity* |
jq --slurp '[.[] | .repositoryName = (.repositoryName | split("/")[-2])]'
else
echo "[]"
fi

View file

@ -14,6 +14,14 @@
# Exit when any command fails
set -e
# Load .env if exists
if [[ -f .env ]]; then
source .env
fi
# Default value if .env not exists
: "${home:=/home/borgwarehouse}"
# Use jc to output a JSON format with du command
cd /var/borgwarehouse
sudo /usr/bin/du -s -- * | jc --du
cd ${home}/repos
du -s -- * | jc --du

View file

@ -1,47 +1,72 @@
#!/usr/bin/env bash
# Shell created by Raven for BorgWarehouse.
# This shell takes 2 args : [user] [new SSH pub key] [quota]
# This shell updates the ssh key for a repository.
# This shell takes 3 args: [repositoryName] [new SSH pub key] [quota]
# This shell updates the SSH key and the quota for a repository.
# Exit when any command fails
set -e
# Check args
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ];then
echo "This shell takes 3 args : [user] [new SSH pub key] [quota]"
exit 1
# Load .env if exists
if [[ -f .env ]]; then
source .env
fi
# Some variables
home="/var/borgwarehouse/$1"
# Default value if .env not exists
: "${home:=/home/borgwarehouse}"
# Check args
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ]; then
echo -n "This shell takes 3 args: [repositoryName] [new SSH pub key] [quota]"
exit 1
fi
# Check if the SSH public key is a valid format
# This pattern validates SSH public keys for : rsa, ed25519, ed25519-sk
pattern='(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?'
if [[ ! "$2" =~ $pattern ]]
then
echo "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
echo -n "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
exit 2
fi
# Check if username length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
# If we receive another length there is necessarily a problem.
username=$1
if [ ${#username} != 8 ]
then
echo "Error with the length of the username."
# Check if repositoryName length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
# If we receive another length, there is necessarily a problem.
repositoryName=$1
if [ ${#repositoryName} != 8 ]; then
echo -n "Error with the length of the repositoryName."
exit 3
fi
# Check if the user exists
if ! id "$1" &>/dev/null; then
echo "The user $1 does not exist"
# Check if a line in authorized_keys contains repository_name
if ! grep -q "command=\".*${repositoryName}.*\",restrict" "$home/.ssh/authorized_keys"; then
echo -n "No line containing $repositoryName found in authorized_keys"
exit 4
fi
# Modify authorized_keys for the user : only the ssh key is modify with this regex
sudo sed -ri "s|(command=\".*\",restrict ).*|\1$2|g" "$home/.ssh/authorized_keys"
# Check if the new SSH pub key is already present on a line OTHER than the one corresponding to repositoryName
found=false
regex="command=\".*${repositoryName}.*\",restrict"
while IFS= read -r line; do
if [[ $line =~ $pattern ]]; then
# Get the SSH pub key of the line (ignore the comment)
key1=$(echo ${BASH_REMATCH[0]} | awk '{print $1 " " $2}')
# Get the SSH pub key of the new SSH pub key (ignore the comment)
key2=$(echo "$2" | awk '{print $1 " " $2}')
if [ "$key1" == "$key2" ]; then
# If the SSH pub key is already present on a line other than the one corresponding to repositoryName
if [[ ! $line =~ $regex ]]; then
found=true
break
fi
fi
fi
done < "$home/.ssh/authorized_keys"
if [ "$found" = true ]; then
echo -n "This SSH pub key is already present in authorized_keys on a different line."
exit 5
fi
# Modify authorized_keys for the user : only the quota is modify with this regex
sudo sed -ri "s|--storage-quota.*\"|--storage-quota $3G\"|g" "$home/.ssh/authorized_keys"
# Modify authorized_keys for the repositoryName: update the line with the quota and the SSH pub key
sed -ri "s|(command=\".*${repositoryName}.*--storage-quota ).*G\",restrict .*|\\1$3G\",restrict $2|g" "$home/.ssh/authorized_keys"

26
package-lock.json generated
View file

@ -1,18 +1,18 @@
{
"name": "borgwarehouse",
"version": "1.6.0",
"version": "2.0.0-rc.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "borgwarehouse",
"version": "1.6.0",
"version": "2.0.0-rc.3",
"dependencies": {
"@tabler/icons-react": "^2.24.0",
"bcryptjs": "^2.4.3",
"chart.js": "^4.3.0",
"next": "^13.4.9",
"next-auth": "^4.22.1",
"next": "^13.5.4",
"next-auth": "^4.23.2",
"nodemailer": "^6.9.3",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
@ -25,7 +25,8 @@
},
"devDependencies": {
"eslint": "8.23.1",
"eslint-config-next": "^13.4.9"
"eslint-config-next": "^13.5.4",
"prettier": "^3.0.3"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -3459,6 +3460,21 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "borgwarehouse",
"version": "1.6.0",
"version": "2.0.0-rc.3",
"private": true,
"scripts": {
"dev": "next dev",
@ -12,8 +12,8 @@
"@tabler/icons-react": "^2.24.0",
"bcryptjs": "^2.4.3",
"chart.js": "^4.3.0",
"next": "^13.4.9",
"next-auth": "^4.22.1",
"next": "^13.5.4",
"next-auth": "^4.23.2",
"nodemailer": "^6.9.3",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
@ -26,6 +26,7 @@
},
"devDependencies": {
"eslint": "8.23.1",
"eslint-config-next": "^13.4.9"
"eslint-config-next": "^13.5.4",
"prettier": "^3.0.3"
}
}

View file

@ -0,0 +1,49 @@
//Lib
import { authOptions } from '../auth/[...nextauth]';
import { getServerSession } from 'next-auth/next';
export default async function handler(req, res) {
if (req.method == 'GET') {
//Verify that the user is logged in.
const session = await getServerSession(req, res, authOptions);
if (!session) {
res.status(401).json({ message: 'You must be logged in.' });
return;
}
try {
function getEnvVariable(envName, defaultValue = '') {
return process.env[envName] || defaultValue;
}
const wizardEnv = {
UNIX_USER: getEnvVariable('UNIX_USER', 'borgwarehouse'),
FQDN: getEnvVariable('FQDN', 'localhost'),
SSH_SERVER_PORT: getEnvVariable('SSH_SERVER_PORT', '22'),
FQDN_LAN: getEnvVariable('FQDN_LAN'),
SSH_SERVER_PORT_LAN: getEnvVariable('SSH_SERVER_PORT_LAN'),
SSH_SERVER_FINGERPRINT_RSA: getEnvVariable(
'SSH_SERVER_FINGERPRINT_RSA'
),
SSH_SERVER_FINGERPRINT_ED25519: getEnvVariable(
'SSH_SERVER_FINGERPRINT_ED25519'
),
SSH_SERVER_FINGERPRINT_ECDSA: getEnvVariable(
'SSH_SERVER_FINGERPRINT_ECDSA'
),
};
res.status(200).json({ wizardEnv });
return;
} catch (error) {
//Log for backend
console.log(error);
//Log for frontend
res.status(500).json({
status: 500,
message: 'API error, contact the administrator',
});
return;
}
} else {
res.status(405).json({ message: 'Bad request on API' });
}
}

View file

@ -21,12 +21,11 @@ export const authOptions = {
JSON.stringify([
{
id: 0,
email: 'admin@demo',
email: '',
username: 'admin',
password:
'$2a$12$20yqRnuaDBH6AE0EvIUcEOzqkuBtn1wDzJdw2Beg8w9S.vEqdso0a',
roles: ['admin'],
emailAlert: 'false',
},
])
);

View file

@ -31,6 +31,20 @@ export default async function handler(req, res) {
////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');
@ -50,19 +64,12 @@ export default async function handler(req, res) {
//Parse the JSON output of getLastSave.sh to use it
const lastSave = JSON.parse(stdout);
//Find the absolute path of the json directory
let repoList = await fs.readFile(
jsonDirectory + '/repo.json',
'utf8'
);
//Parse the repoList
repoList = JSON.parse(repoList);
//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.user === newRepoList[index].unixUser
(x) =>
x.repositoryName === newRepoList[index].repositoryName
);
if (repoFiltered.length === 1) {
//Write the timestamp of the last save
@ -163,12 +170,6 @@ export default async function handler(req, res) {
transporter.sendMail(mailData, function (err, info) {
if (err) {
console.log(err);
res.status(400).json({
message:
'An error occured while sending the email : ' +
err,
});
return;
} else {
console.log(info);
}

View file

@ -59,7 +59,7 @@ export default async function handler(req, res) {
let newRepoList = repoList;
for (let index in newRepoList) {
const repoFiltered = storageUsed.filter(
(x) => x.name === newRepoList[index].unixUser
(x) => x.name === newRepoList[index].repositoryName
);
if (repoFiltered.length === 1) {
newRepoList[index].storageUsed = repoFiltered[0].size;

View file

@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
import path from 'path';
import { authOptions } from '../../../pages/api/auth/[...nextauth]';
import { getServerSession } from 'next-auth/next';
import repoHistory from '../../../helpers/functions/repoHistory';
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
@ -49,7 +50,7 @@ export default async function handler(req, res) {
const newRepo = {
id: newID,
alias: alias,
repository: 'repo' + newID,
repositoryName: '',
status: false,
lastSave: 0,
alert: alert,
@ -58,7 +59,6 @@ export default async function handler(req, res) {
sshPublicKey: sshPublicKey,
comment: comment,
displayDetails: true,
unixUser: '',
lanCommand: lanCommand,
};
@ -66,22 +66,18 @@ export default async function handler(req, res) {
//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/createRepo.sh ${newRepo.repository} "${newRepo.sshPublicKey}" ${newRepo.storageSize}`
const { stdout } = await exec(
`${shellsDirectory}/shells/createRepo.sh "${newRepo.sshPublicKey}" ${newRepo.storageSize}`
);
if (stderr) {
console.log('stderr:', stderr);
res.status(500).json({
status: 500,
message: 'Error on creation, contact the administrator.',
});
return;
}
newRepo.unixUser = stdout.trim();
newRepo.repositoryName = stdout.trim();
//Create the new repoList with the new repo
let newRepoList = [newRepo, ...repoList];
//History the new repoList
await repoHistory(newRepoList);
//Stringify the newRepoList to write it into the json file.
newRepoList = JSON.stringify(newRepoList);
@ -106,7 +102,7 @@ export default async function handler(req, res) {
} else {
res.status(500).json({
status: 500,
message: 'API error, contact the administrator',
message: error.stdout,
});
}
return;

View file

@ -2,6 +2,7 @@ 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';
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
@ -46,11 +47,10 @@ export default async function handler(req, res) {
//Find the absolute path of the shells directory
const shellsDirectory = path.join(process.cwd(), '/helpers');
//Exec the shell
const { stderr } = await exec(
`${shellsDirectory}/shells/deleteRepo.sh ${repoList[indexToDelete].unixUser}`
const { stdout, stderr } = await exec(
`${shellsDirectory}/shells/deleteRepo.sh ${repoList[indexToDelete].repositoryName}`
);
//Ignore this normal error with the command userdel in the shell : "userdel: USERXXX mail spool (/var/mail/USERXXX) not found".
if (stderr && !stderr.includes('mail spool')) {
if (stderr) {
console.log('stderr:', stderr);
res.status(500).json({
status: 500,
@ -70,7 +70,8 @@ export default async function handler(req, res) {
});
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
@ -81,6 +82,7 @@ export default async function handler(req, res) {
if (err) console.log(err);
}
);
res.status(200).json({ message: 'Envoi API réussi' });
} catch (error) {
//Log for backend

View file

@ -2,6 +2,7 @@ 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';
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
@ -28,7 +29,6 @@ export default async function handler(req, res) {
}
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(
@ -48,17 +48,9 @@ export default async function handler(req, res) {
//Find the absolute path of the shells directory
const shellsDirectory = path.join(process.cwd(), '/helpers');
// //Exec the shell
const { stderr } = await exec(
`${shellsDirectory}/shells/updateRepo.sh ${repoList[repoIndex].unixUser} "${sshPublicKey}" ${size}`
await exec(
`${shellsDirectory}/shells/updateRepo.sh ${repoList[repoIndex].repositoryName} "${sshPublicKey}" ${size}`
);
if (stderr) {
console.log('stderr:', stderr);
res.status(500).json({
status: 500,
message: 'Error on update, contact the administrator.',
});
return;
}
//Find the ID in the data and change the values transmitted by the form
let newRepoList = repoList.map((repo) =>
@ -74,6 +66,8 @@ export default async function handler(req, res) {
}
: repo
);
//History the new repoList
await repoHistory(newRepoList);
//Stringify the newRepoList to write it into the json file.
newRepoList = JSON.stringify(newRepoList);
//Write the new json
@ -84,6 +78,7 @@ export default async function handler(req, res) {
if (err) console.log(err);
}
);
res.status(200).json({ message: 'Envoi API réussi' });
} catch (error) {
//Log for backend
@ -97,7 +92,7 @@ export default async function handler(req, res) {
} else {
res.status(500).json({
status: 500,
message: 'API error, contact the administrator',
message: error.stdout,
});
}
return;