mirror of
https://github.com/Ravinou/borgwarehouse
synced 2026-03-14 14:25:46 +01:00
release 3.0.0 - breaking changes
This commit is contained in:
commit
d601c6dadf
157 changed files with 14554 additions and 10462 deletions
|
|
@ -19,6 +19,8 @@ export default {
|
|||
'ui',
|
||||
'wip',
|
||||
'publish',
|
||||
'docker',
|
||||
'WIP',
|
||||
],
|
||||
],
|
||||
},
|
||||
|
|
|
|||
6
.eslintrc.json
Normal file
6
.eslintrc.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"next/typescript"
|
||||
]
|
||||
}
|
||||
10
.github/workflows/shellcheck.yml
vendored
10
.github/workflows/shellcheck.yml
vendored
|
|
@ -4,19 +4,21 @@ on:
|
|||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
branches: main
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
name: "Shellcheck"
|
||||
name: 'Shellcheck'
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
name: Shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
env:
|
||||
|
|
|
|||
49
.github/workflows/vitest.yml
vendored
Normal file
49
.github/workflows/vitest.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
name: Vitest & ESLint CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
jobs:
|
||||
test:
|
||||
name: Run Vitest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Vitest
|
||||
run: npm run test
|
||||
|
||||
lint:
|
||||
name: Run ESLint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
|
@ -23,41 +23,43 @@ function checkBreakingChangeInBody() {
|
|||
}
|
||||
|
||||
function findTypeIcon() {
|
||||
# get message from 1st param
|
||||
message="$1"
|
||||
|
||||
# declare an icons for each authorized enum-type from `.commitlintrc.js`
|
||||
declare -A icons
|
||||
icons[build]='🤖'
|
||||
icons[chore]='🧹'
|
||||
icons["chore(deps)"]='🧹'
|
||||
icons[config]='🔧'
|
||||
icons[deploy]='🚀'
|
||||
icons[doc]='📚'
|
||||
icons[feat]='✨'
|
||||
icons[fix]='🐛'
|
||||
icons[hotfix]='🚑'
|
||||
icons[i18n]='💬'
|
||||
icons[publish]='📦'
|
||||
icons[refactor]='⚡'
|
||||
icons[revert]='⏪'
|
||||
icons[test]='✅'
|
||||
icons[ui]='🎨'
|
||||
icons[wip]='🚧'
|
||||
icons[WIP]='🚧'
|
||||
if [[ "$message" =~ ^.*!:\ .* ]]; then
|
||||
echo "$boomIcon"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for type in "${!icons[@]}"; do
|
||||
# check if message subject contains breaking change pattern
|
||||
if [[ "$message" =~ ^(.*)(!:){1}(.*)$ ]]; then
|
||||
echo "$boomIcon"
|
||||
return 0
|
||||
# else find corresponding type icon
|
||||
elif [[ "$message" == "$type"* ]]; then
|
||||
echo "${icons[$type]}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
declare -A icons=(
|
||||
[build]='🤖'
|
||||
[chore]='🧹'
|
||||
["chore(deps)"]='🧹'
|
||||
[config]='🔧'
|
||||
[deploy]='🚀'
|
||||
[doc]='📚'
|
||||
[feat]='✨'
|
||||
[fix]='🐛'
|
||||
[hotfix]='🚑'
|
||||
[i18n]='💬'
|
||||
[publish]='📦'
|
||||
[refactor]='⚡'
|
||||
[revert]='⏪'
|
||||
[test]='✅'
|
||||
[ui]='🎨'
|
||||
[wip]='🚧'
|
||||
[WIP]='🚧'
|
||||
[docker]='🐳'
|
||||
)
|
||||
|
||||
commit_type="${message%%:*}"
|
||||
|
||||
icon="${icons[$commit_type]}"
|
||||
if [[ -n "$icon" ]]; then
|
||||
echo "$icon"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# extract original message from the first line of file
|
||||
|
|
|
|||
|
|
@ -1,26 +1,30 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import classes from './QuickCommands.module.css';
|
||||
import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react';
|
||||
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
|
||||
import { lanCommandOption } from '~/helpers/functions';
|
||||
import { WizardEnvType } from '~/types/domain/config.types';
|
||||
|
||||
export default function QuickCommands(props) {
|
||||
////Vars
|
||||
type QuickCommandsProps = {
|
||||
repositoryName: string;
|
||||
wizardEnv?: WizardEnvType;
|
||||
lanCommand?: boolean;
|
||||
};
|
||||
|
||||
export default function QuickCommands(props: QuickCommandsProps) {
|
||||
const wizardEnv = props.wizardEnv;
|
||||
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.lanCommand);
|
||||
|
||||
//State
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
//Functions
|
||||
const handleCopy = async () => {
|
||||
// Asynchronously call copy to clipboard
|
||||
navigator.clipboard
|
||||
.writeText(`ssh://${wizardEnv.UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.repositoryName}`)
|
||||
.writeText(
|
||||
`ssh://${wizardEnv?.UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.repositoryName}`
|
||||
)
|
||||
.then(() => {
|
||||
// If successful, update the isCopied state value
|
||||
setIsCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
|
|
@ -37,7 +41,7 @@ export default function QuickCommands(props) {
|
|||
<div className={classes.copyValid}>Copied !</div>
|
||||
) : (
|
||||
<div className={classes.tooltip}>
|
||||
ssh://{wizardEnv.UNIX_USER}@{FQDN}
|
||||
ssh://{wizardEnv?.UNIX_USER}@{FQDN}
|
||||
{SSH_SERVER_PORT}/./
|
||||
{props.repositoryName}
|
||||
</div>
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
//Lib
|
||||
import { useState } from 'react';
|
||||
import classes from './Repo.module.css';
|
||||
import {
|
||||
|
|
@ -9,29 +8,40 @@ import {
|
|||
IconBellOff,
|
||||
IconLockPlus,
|
||||
} from '@tabler/icons-react';
|
||||
import timestampConverter from '../../helpers/functions/timestampConverter';
|
||||
import StorageBar from '../UI/StorageBar/StorageBar';
|
||||
import QuickCommands from './QuickCommands/QuickCommands';
|
||||
import { Repository, WizardEnvType, Optional } from '~/types';
|
||||
import { fromUnixTime } from 'date-fns';
|
||||
|
||||
export default function Repo(props) {
|
||||
type RepoProps = Omit<Repository, 'unixUser' | 'displayDetails'> & {
|
||||
repoManageEditHandler: () => void;
|
||||
wizardEnv: Optional<WizardEnvType>;
|
||||
};
|
||||
|
||||
export default function Repo(props: RepoProps) {
|
||||
//Load displayDetails from LocalStorage
|
||||
const displayDetailsFromLS = () => {
|
||||
const displayDetailsFromLS = (): boolean => {
|
||||
const key = `displayDetailsRepo${props.id}`;
|
||||
|
||||
try {
|
||||
if (localStorage.getItem('displayDetailsRepo' + props.id) === null) {
|
||||
localStorage.setItem('displayDetailsRepo' + props.id, JSON.stringify(true));
|
||||
return true;
|
||||
} else {
|
||||
return JSON.parse(localStorage.getItem('displayDetailsRepo' + props.id));
|
||||
const storedValue = localStorage.getItem(key);
|
||||
|
||||
if (storedValue === null) {
|
||||
const defaultValue = true;
|
||||
localStorage.setItem(key, JSON.stringify(defaultValue));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const parsedValue = JSON.parse(storedValue);
|
||||
if (typeof parsedValue === 'boolean') {
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
localStorage.removeItem(key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'LocalStorage error, key',
|
||||
'displayDetailsRepo' + props.id,
|
||||
'will be removed. Try again.',
|
||||
'Error message on this key : ',
|
||||
error
|
||||
);
|
||||
localStorage.removeItem('displayDetailsRepo' + props.id);
|
||||
localStorage.removeItem(key);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -39,7 +49,7 @@ export default function Repo(props) {
|
|||
const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS);
|
||||
|
||||
//BUTTON : Display or not repo details for ONE repo
|
||||
const displayDetailsForOneHandler = (boolean) => {
|
||||
const displayDetailsForOneHandler = (boolean: boolean) => {
|
||||
//Update localStorage
|
||||
localStorage.setItem('displayDetailsRepo' + props.id, JSON.stringify(boolean));
|
||||
setDisplayDetails(boolean);
|
||||
|
|
@ -114,7 +124,7 @@ export default function Repo(props) {
|
|||
</th>
|
||||
<th>
|
||||
<div className={classes.lastSave}>
|
||||
{props.lastSave === 0 ? '-' : timestampConverter(props.lastSave)}
|
||||
{props.lastSave === 0 ? '-' : fromUnixTime(props.lastSave).toLocaleString()}
|
||||
</div>
|
||||
</th>
|
||||
<th>#{props.id}</th>
|
||||
|
|
@ -148,7 +158,7 @@ export default function Repo(props) {
|
|||
)}
|
||||
</div>
|
||||
<div className={classes.lastSave}>
|
||||
{props.lastSave === 0 ? null : timestampConverter(props.lastSave)}
|
||||
{props.lastSave === 0 ? null : fromUnixTime(props.lastSave).toLocaleString()}
|
||||
<span style={{ marginLeft: '20px', color: '#637381' }}>#{props.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
//Lib
|
||||
import classes from './CopyButton.module.css';
|
||||
import { useState } from 'react';
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { IconChecks, IconCopy } from '@tabler/icons-react';
|
||||
|
||||
export default function CopyButton(props) {
|
||||
//State
|
||||
type CopyButtonProps = {
|
||||
dataToCopy: string;
|
||||
children?: ReactNode;
|
||||
displayIconConfirmation?: boolean;
|
||||
size?: number;
|
||||
stroke?: number;
|
||||
};
|
||||
|
||||
export default function CopyButton(props: CopyButtonProps) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
//Function
|
||||
const handleCopy = async (data) => {
|
||||
const handleCopy = async (data: string) => {
|
||||
navigator.clipboard
|
||||
.writeText(data)
|
||||
.then(() => {
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
//Lib
|
||||
import classes from './Error.module.css';
|
||||
|
||||
export default function Error(props) {
|
||||
type ErrorProps = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export default function Error(props: ErrorProps) {
|
||||
return <div className={classes.errorMessage}>{props.message}</div>;
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
//Lib
|
||||
import { ReactNode } from 'react';
|
||||
import classes from './Info.module.css';
|
||||
|
||||
export default function Info(props) {
|
||||
type InfoProps = {
|
||||
message: string;
|
||||
color?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export default function Info(props: InfoProps) {
|
||||
return (
|
||||
<div className={classes.infoMessage} style={{ backgroundColor: props.color }}>
|
||||
{props.message}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
//Lib
|
||||
import classes from './Footer.module.css';
|
||||
import packageInfo from '../../../../package.json';
|
||||
import packageInfo from '~/package.json';
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
|
|
@ -27,5 +27,5 @@
|
|||
font-weight: bold;
|
||||
color: #6d4aff;
|
||||
text-shadow: #6d4aff 0px 0px 18px;
|
||||
margin-left: 20px;
|
||||
margin-left: 70px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
//Lib
|
||||
import Image from 'next/image';
|
||||
import classes from './Header.module.css';
|
||||
|
||||
//Components
|
||||
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}>
|
||||
<Image
|
||||
src='/borgwarehouse-logo-violet.svg'
|
||||
alt='BorgWarehouse'
|
||||
width={225}
|
||||
height={40}
|
||||
className={classes.logoImage}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<Nav />
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
//Lib
|
||||
import classes from './Nav.module.css';
|
||||
import { IconUser, IconLogout } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -6,13 +5,10 @@ import { useRouter } from 'next/router';
|
|||
import { useSession, signOut } from 'next-auth/react';
|
||||
|
||||
export default function Nav() {
|
||||
////Var
|
||||
//Get the current route to light the right Item
|
||||
const router = useRouter();
|
||||
const currentRoute = router.pathname;
|
||||
const { status, data } = useSession();
|
||||
|
||||
//Function
|
||||
const onLogoutClickedHandler = async () => {
|
||||
//This bug is open : https://github.com/nextauthjs/next-auth/issues/1542
|
||||
//I put redirect to false and redirect with router.
|
||||
|
|
@ -25,12 +21,12 @@ export default function Nav() {
|
|||
return (
|
||||
<ul className={classes.Nav}>
|
||||
<li style={{ margin: '0px 15px 0px 0px' }} className={classes.account}>
|
||||
<Link href='/account' className={currentRoute === '/account' ? classes.active : null}>
|
||||
<Link href='/account' className={currentRoute === '/account' ? classes.active : undefined}>
|
||||
<div className={classes.user}>
|
||||
<div>
|
||||
<IconUser size={28} />
|
||||
</div>
|
||||
<div className={classes.username}>{status === 'authenticated' && data.user.name}</div>
|
||||
<div className={classes.username}>{status === 'authenticated' && data.user?.name}</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
//Lib
|
||||
import Footer from './Footer/Footer';
|
||||
import Header from './Header/Header';
|
||||
import NavSide from './NavSide/NavSide';
|
||||
import classes from './Layout.module.css';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
function Layout(props) {
|
||||
//Var
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function Layout(props: LayoutProps) {
|
||||
const { status } = useSession();
|
||||
|
||||
if (status === 'authenticated') {
|
||||
|
|
@ -1,21 +1,16 @@
|
|||
//Lib
|
||||
import classes from './NavSide.module.css';
|
||||
import { IconServer, IconSettingsAutomation, IconActivityHeartbeat } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
//Composants
|
||||
|
||||
export default function NavSide() {
|
||||
////Var
|
||||
//Get the current route to light the right Item
|
||||
const router = useRouter();
|
||||
const currentRoute = router.pathname;
|
||||
|
||||
return (
|
||||
<ul className={classes.NavSide}>
|
||||
<li className={classes.NavSideItem}>
|
||||
<Link href='/' className={currentRoute === '/' ? classes.active : null}>
|
||||
<Link href='/' className={currentRoute === '/' ? classes.active : undefined}>
|
||||
<IconServer size={40} />
|
||||
</Link>
|
||||
<span className={classes.tooltip}>Repositories</span>
|
||||
|
|
@ -23,14 +18,17 @@ export default function NavSide() {
|
|||
<li className={classes.NavSideItem}>
|
||||
<Link
|
||||
href='/setup-wizard/1'
|
||||
className={currentRoute === '/setup-wizard/[slug]' ? classes.active : null}
|
||||
className={currentRoute === '/setup-wizard/[slug]' ? classes.active : undefined}
|
||||
>
|
||||
<IconSettingsAutomation size={40} />
|
||||
</Link>
|
||||
<span className={classes.tooltip}>Setup Wizard</span>
|
||||
</li>
|
||||
<li className={classes.NavSideItem}>
|
||||
<Link href='/monitoring' className={currentRoute === '/monitoring' ? classes.active : null}>
|
||||
<Link
|
||||
href='/monitoring'
|
||||
className={currentRoute === '/monitoring' ? classes.active : undefined}
|
||||
>
|
||||
<IconActivityHeartbeat size={40} />
|
||||
</Link>
|
||||
<span className={classes.tooltip}>Monitoring</span>
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
//Lib
|
||||
import classes from './ShimmerRepoList.module.css';
|
||||
|
||||
const LOADING_REPO_COUNT = 5;
|
||||
|
||||
function ShimmerRepoItem() {
|
||||
return <div className={classes.repoIsLoading} />;
|
||||
}
|
||||
|
||||
export default function ShimmerRepoList() {
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
|
|
@ -8,11 +13,9 @@ export default function ShimmerRepoList() {
|
|||
<div className={classes.buttonIsLoading} />
|
||||
</div>
|
||||
<div className={classes.loadingRepoContainer}>
|
||||
<div className={classes.repoIsLoading} />
|
||||
<div className={classes.repoIsLoading} />
|
||||
<div className={classes.repoIsLoading} />
|
||||
<div className={classes.repoIsLoading} />
|
||||
<div className={classes.repoIsLoading} />
|
||||
{Array.from({ length: LOADING_REPO_COUNT }, (_, i) => (
|
||||
<ShimmerRepoItem key={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
//Lib
|
||||
import classes from './StorageBar.module.css';
|
||||
|
||||
export default function StorageBar(props) {
|
||||
//Var
|
||||
type StorageBarProps = {
|
||||
storageUsed: number;
|
||||
storageSize: number;
|
||||
};
|
||||
|
||||
export default function StorageBar(props: StorageBarProps) {
|
||||
//storageUsed is in kB, storageSize is in GB. Round to 1 decimal for %.
|
||||
const storageUsedPercent = (((props.storageUsed / 1024 ** 2) * 100) / props.storageSize).toFixed(
|
||||
1
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
//Lib
|
||||
import classes from './Switch.module.css';
|
||||
|
||||
export default function Switch(props) {
|
||||
return (
|
||||
<>
|
||||
<div className={classes.switchWrapper}>
|
||||
<div className={classes.switch}>
|
||||
<label className={classes.pureMaterialSwitch}>
|
||||
<input
|
||||
checked={props.checked}
|
||||
disabled={props.disabled}
|
||||
type='checkbox'
|
||||
onChange={(e) => props.onChange(e.target.checked)}
|
||||
/>
|
||||
|
||||
<span>{props.switchName}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className={classes.switchDescription}>
|
||||
<span>{props.switchDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,157 +1,84 @@
|
|||
/* Wrapper styles */
|
||||
.switchWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Switch container */
|
||||
.switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.switchDescription {
|
||||
display: flex;
|
||||
margin: 8px 0px 0px 0px;
|
||||
color: #6c737f;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch {
|
||||
z-index: 0;
|
||||
/* Label */
|
||||
.switchLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.87);
|
||||
font-family: var(
|
||||
--pure-material-font,
|
||||
'Roboto',
|
||||
'Segoe UI',
|
||||
BlinkMacSystemFont,
|
||||
system-ui,
|
||||
-apple-system
|
||||
);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
.pureMaterialSwitch > input {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: -8px;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
.switchLabel input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Slider */
|
||||
.switchSlider {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
outline: none;
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 0.3s 0.1s,
|
||||
transform 0.2s 0.1s;
|
||||
height: 20px;
|
||||
background: #ccc;
|
||||
border-radius: 12px;
|
||||
transition: #ccc 0.3s ease;
|
||||
}
|
||||
|
||||
/* Span */
|
||||
.pureMaterialSwitch > span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #494b7a;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
.pureMaterialSwitch > span::before {
|
||||
content: '';
|
||||
float: right;
|
||||
display: inline-block;
|
||||
margin: 5px 0 5px 30px;
|
||||
border-radius: 7px;
|
||||
width: 36px;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Thumb */
|
||||
.pureMaterialSwitch > span::after {
|
||||
.switchSlider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 16px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
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;
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Checked */
|
||||
.pureMaterialSwitch > input:checked {
|
||||
right: -10px;
|
||||
background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
|
||||
/* Checked styles */
|
||||
.switchLabel input:checked + .switchSlider {
|
||||
background: #704dff;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:checked + span::before {
|
||||
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
|
||||
.switchLabel input:checked + .switchSlider::after {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:checked + span::after {
|
||||
background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
|
||||
transform: translateX(16px);
|
||||
/* Disabled styles */
|
||||
.switchLabel input:disabled + .switchSlider {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
/* Active */
|
||||
.pureMaterialSwitch > input:active {
|
||||
opacity: 1;
|
||||
transform: scale(0);
|
||||
transition:
|
||||
transform 0s,
|
||||
opacity 0s;
|
||||
.switchLabel input:disabled + .switchSlider::after {
|
||||
background: #bdbdbd;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:active + span::before {
|
||||
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
|
||||
/* Switch text */
|
||||
.switchText {
|
||||
font-size: 1rem;
|
||||
color: #494b7a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:checked:active + span::before {
|
||||
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
/* Description */
|
||||
.switchDescription {
|
||||
font-size: 0.875rem;
|
||||
color: #6c737f;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
.pureMaterialSwitch > input:disabled + span {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* .pureMaterialSwitch > input:disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:disabled + span {
|
||||
color: rgb(var(--pure-material-onsurface-rgb, 0, 0, 0));
|
||||
opacity: 0.38;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:disabled + span::before {
|
||||
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:checked:disabled + span::before {
|
||||
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
|
||||
} */
|
||||
|
|
|
|||
44
Components/UI/Switch/Switch.tsx
Normal file
44
Components/UI/Switch/Switch.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Optional } from '~/types';
|
||||
import classes from './Switch.module.css';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
|
||||
type SwitchProps = {
|
||||
switchName: string;
|
||||
switchDescription: string;
|
||||
checked: Optional<boolean>;
|
||||
disabled: boolean;
|
||||
loading?: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
export default function Switch(props: SwitchProps) {
|
||||
return (
|
||||
<div className={classes.switchWrapper}>
|
||||
<div className={classes.switch}>
|
||||
<label className={classes.switchLabel}>
|
||||
{props.loading ? (
|
||||
<SpinnerCircularFixed
|
||||
size={24}
|
||||
thickness={120}
|
||||
speed={100}
|
||||
color='#704dff'
|
||||
secondaryColor='#e0dcfc'
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={props.checked || false}
|
||||
disabled={props.disabled}
|
||||
onChange={(e) => props.onChange(e.target.checked)}
|
||||
/>
|
||||
<span className={classes.switchSlider}></span>
|
||||
</>
|
||||
)}
|
||||
<span className={classes.switchText}>{props.switchName}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p className={classes.switchDescription}>{props.switchDescription}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from '../WizardStep1/WizardStep1.module.css';
|
||||
import { IconDeviceDesktopAnalytics, IconTerminal2 } from '@tabler/icons-react';
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from '../WizardStep1/WizardStep1.module.css';
|
||||
import { IconTool, IconAlertCircle } from '@tabler/icons-react';
|
||||
import { IconAlertCircle, IconTool } from '@tabler/icons-react';
|
||||
import CopyButton from '../../UI/CopyButton/CopyButton';
|
||||
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
|
||||
import { WizardStepProps } from '~/types';
|
||||
import classes from '../WizardStep1/WizardStep1.module.css';
|
||||
import { lanCommandOption } from '~/helpers/functions';
|
||||
|
||||
function WizardStep2(props) {
|
||||
////Vars
|
||||
function WizardStep2(props: WizardStepProps) {
|
||||
const wizardEnv = props.wizardEnv;
|
||||
const UNIX_USER = wizardEnv.UNIX_USER;
|
||||
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.
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedOption.lanCommand);
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
|
|
@ -32,10 +30,10 @@ function WizardStep2(props) {
|
|||
borg init -e repokey-blake2 ssh://
|
||||
{UNIX_USER}@{FQDN}
|
||||
{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
{props.selectedRepo?.repositoryName}
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg init -e repokey-blake2 ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
|
||||
dataToCopy={`borg init -e repokey-blake2 ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.note}>
|
||||
|
|
@ -85,10 +83,10 @@ function WizardStep2(props) {
|
|||
ssh://
|
||||
{UNIX_USER}@{FQDN}
|
||||
{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
{props.selectedRepo?.repositoryName}
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
|
||||
dataToCopy={`ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -103,16 +101,18 @@ function WizardStep2(props) {
|
|||
following key's fingerprint when you first connect :
|
||||
<li>
|
||||
<span className={classes.sshPublicKey}>
|
||||
ECDSA : {wizardEnv.SSH_SERVER_FINGERPRINT_ECDSA}
|
||||
ECDSA : {wizardEnv?.SSH_SERVER_FINGERPRINT_ECDSA}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={classes.sshPublicKey}>
|
||||
ED25519 : {wizardEnv.SSH_SERVER_FINGERPRINT_ED25519}
|
||||
ED25519 : {wizardEnv?.SSH_SERVER_FINGERPRINT_ED25519}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={classes.sshPublicKey}>RSA : {wizardEnv.SSH_SERVER_FINGERPRINT_RSA}</span>
|
||||
<span className={classes.sshPublicKey}>
|
||||
RSA : {wizardEnv?.SSH_SERVER_FINGERPRINT_RSA}
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
//Lib
|
||||
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';
|
||||
import { WizardStepProps } from '~/types';
|
||||
import { lanCommandOption } from '~/helpers/functions';
|
||||
|
||||
function WizardStep3(props) {
|
||||
////Vars
|
||||
function WizardStep3(props: WizardStepProps) {
|
||||
const wizardEnv = props.wizardEnv;
|
||||
const UNIX_USER = wizardEnv.UNIX_USER;
|
||||
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.
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedOption.lanCommand);
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
|
|
@ -31,11 +30,11 @@ function WizardStep3(props) {
|
|||
borg create ssh://
|
||||
{UNIX_USER}@{FQDN}
|
||||
{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
{props.selectedRepo?.repositoryName}
|
||||
::archive1 /your/pathToBackup
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg create ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 /your/pathToBackup`}
|
||||
dataToCopy={`borg create ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}::archive1 /your/pathToBackup`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -70,10 +69,10 @@ function WizardStep3(props) {
|
|||
borg check -v --progress ssh://
|
||||
{UNIX_USER}@{FQDN}
|
||||
{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
{props.selectedRepo?.repositoryName}
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg check -v --progress ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
|
||||
dataToCopy={`borg check -v --progress ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}`}
|
||||
/>
|
||||
</div>
|
||||
<li>List the remote archives with :</li>
|
||||
|
|
@ -88,10 +87,10 @@ function WizardStep3(props) {
|
|||
borg list ssh://
|
||||
{UNIX_USER}@{FQDN}
|
||||
{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
{props.selectedRepo?.repositoryName}
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg list ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
|
||||
dataToCopy={`borg list ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}`}
|
||||
/>
|
||||
</div>
|
||||
<li>Download a remote archive with the following command :</li>
|
||||
|
|
@ -106,11 +105,11 @@ function WizardStep3(props) {
|
|||
borg export-tar --tar-filter="gzip -9" ssh://
|
||||
{UNIX_USER}@{FQDN}
|
||||
{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
{props.selectedRepo?.repositoryName}
|
||||
::archive1 archive1.tar.gz
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg export-tar --tar-filter="gzip -9" ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 archive1.tar.gz`}
|
||||
dataToCopy={`borg export-tar --tar-filter="gzip -9" ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}::archive1 archive1.tar.gz`}
|
||||
/>
|
||||
</div>
|
||||
<li>Mount an archive to compare or backup some files without download all the archive :</li>
|
||||
|
|
@ -125,11 +124,11 @@ function WizardStep3(props) {
|
|||
borg mount ssh://
|
||||
{UNIX_USER}@{FQDN}
|
||||
{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
{props.selectedRepo?.repositoryName}
|
||||
::archive1 /tmp/yourMountPoint
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg mount ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 /tmp/yourMountPoint`}
|
||||
dataToCopy={`borg mount ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}::archive1 /tmp/yourMountPoint`}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
//Lib
|
||||
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';
|
||||
import { WizardStepProps } from '~/types';
|
||||
import { lanCommandOption } from '~/helpers/functions';
|
||||
|
||||
function WizardStep4(props) {
|
||||
////Vars
|
||||
function WizardStep4(props: WizardStepProps) {
|
||||
const wizardEnv = props.wizardEnv;
|
||||
const UNIX_USER = wizardEnv.UNIX_USER;
|
||||
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.
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedOption.lanCommand);
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
|
||||
|
||||
const configBorgmatic = `location:
|
||||
# List of source directories to backup.
|
||||
|
|
@ -20,7 +19,7 @@ function WizardStep4(props) {
|
|||
|
||||
repositories:
|
||||
# Paths of local or remote repositories to backup to.
|
||||
- ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}
|
||||
- ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}
|
||||
|
||||
storage:
|
||||
archive_name_format: '{FQDN}-documents-{now}'
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from './WizardStepBar.module.css';
|
||||
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
|
||||
|
||||
function WizardStepBar(props) {
|
||||
////Functions
|
||||
type WizardStepBarProps = {
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
previousStepHandler: () => void;
|
||||
nextStepHandler: () => void;
|
||||
};
|
||||
|
||||
function WizardStepBar(props: WizardStepBarProps) {
|
||||
//Color onClick on a step
|
||||
const colorHandler = (step) => {
|
||||
const colorHandler = (step: number) => {
|
||||
if (step <= props.step) {
|
||||
return classes.active;
|
||||
} else {
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
//Lib
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
|
|
@ -10,16 +9,15 @@ import {
|
|||
} from 'chart.js';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Repository, Optional } from '~/types';
|
||||
|
||||
export default function StorageUsedChartBar() {
|
||||
//States
|
||||
const [data, setData] = useState([]);
|
||||
const [data, setData] = useState<Optional<Array<Repository>>>();
|
||||
|
||||
//LifeCycle
|
||||
useEffect(() => {
|
||||
const dataFetch = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/repo', {
|
||||
const response = await fetch('/api/v1/repositories', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -41,10 +39,10 @@ export default function StorageUsedChartBar() {
|
|||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
position: 'bottom' as const,
|
||||
},
|
||||
title: {
|
||||
position: 'bottom',
|
||||
position: 'bottom' as const,
|
||||
display: true,
|
||||
text: 'Storage used for each repository',
|
||||
},
|
||||
|
|
@ -55,7 +53,7 @@ export default function StorageUsedChartBar() {
|
|||
min: 0,
|
||||
ticks: {
|
||||
// Include a dollar sign in the ticks
|
||||
callback: function (value) {
|
||||
callback: function (value: number | string) {
|
||||
return value + '%';
|
||||
},
|
||||
stepSize: 10,
|
||||
|
|
@ -64,7 +62,7 @@ export default function StorageUsedChartBar() {
|
|||
},
|
||||
};
|
||||
|
||||
const labels = data.map((repo) => repo.alias);
|
||||
const labels = data?.map((repo) => repo.alias);
|
||||
|
||||
const dataChart = {
|
||||
labels,
|
||||
|
|
@ -72,7 +70,7 @@ export default function StorageUsedChartBar() {
|
|||
{
|
||||
label: 'Storage used (%)',
|
||||
//storageUsed is in kB, storageSize is in GB. Round to 1 decimal for %.
|
||||
data: data.map((repo) =>
|
||||
data: data?.map((repo) =>
|
||||
(((repo.storageUsed / 1024 ** 2) * 100) / repo.storageSize).toFixed(1)
|
||||
),
|
||||
backgroundColor: '#704dff',
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
//Lib
|
||||
import classes from './RepoList.module.css';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconPlus } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import { ToastContainer, ToastOptions, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
//Composants
|
||||
import Repo from '../../Components/Repo/Repo';
|
||||
import Repo from '~/Components/Repo/Repo';
|
||||
import RepoManage from '../RepoManage/RepoManage';
|
||||
import ShimmerRepoList from '../../Components/UI/ShimmerRepoList/ShimmerRepoList';
|
||||
import ShimmerRepoList from '~/Components/UI/ShimmerRepoList/ShimmerRepoList';
|
||||
import { Repository, WizardEnvType, Optional } from '~/types';
|
||||
|
||||
export default function RepoList() {
|
||||
////Var
|
||||
const router = useRouter();
|
||||
const { mutate } = useSWRConfig();
|
||||
const toastOptions = {
|
||||
const toastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 8000,
|
||||
hideProgressBar: false,
|
||||
|
|
@ -29,8 +29,8 @@ export default function RepoList() {
|
|||
|
||||
////Datas
|
||||
//Write a fetcher function to wrap the native fetch function and return the result of a call to url in json format
|
||||
const fetcher = async (url) => await fetch(url).then((res) => res.json());
|
||||
const { data, error } = useSWR('/api/repo', fetcher);
|
||||
const fetcher = async (url: string) => await fetch(url).then((res) => res.json());
|
||||
const { data, error } = useSWR('/api/v1/repositories', fetcher);
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
|
|
@ -46,13 +46,14 @@ export default function RepoList() {
|
|||
//Fetch wizardEnv to hydrate Repo components
|
||||
const fetchWizardEnv = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getWizardEnv', {
|
||||
const response = await fetch('/api/v1/account/wizard-env', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setWizardEnv((await response.json()).wizardEnv);
|
||||
const data: WizardEnvType = await response.json();
|
||||
setWizardEnv(data);
|
||||
} catch (error) {
|
||||
console.log('Fetching datas error');
|
||||
}
|
||||
|
|
@ -63,14 +64,14 @@ export default function RepoList() {
|
|||
////States
|
||||
const [displayRepoAdd, setDisplayRepoAdd] = useState(false);
|
||||
const [displayRepoEdit, setDisplayRepoEdit] = useState(false);
|
||||
const [wizardEnv, setWizardEnv] = useState({});
|
||||
const [wizardEnv, setWizardEnv] = useState<Optional<WizardEnvType>>();
|
||||
|
||||
////Functions
|
||||
|
||||
//Firstly, check the availability of data and condition it.
|
||||
if (!data) {
|
||||
//Force mutate after login (force a API GET on /api/repo to load repoList)
|
||||
mutate('/api/repo');
|
||||
//Force mutate after login (force a API GET on /api/v1/repositories to load repoList)
|
||||
mutate('/api/v1/repositories');
|
||||
return <ShimmerRepoList />;
|
||||
}
|
||||
if (error) {
|
||||
|
|
@ -88,7 +89,7 @@ export default function RepoList() {
|
|||
};
|
||||
|
||||
//BUTTON : Display RepoManage component box for EDIT
|
||||
const repoManageEditHandler = (id) => {
|
||||
const manageRepoEditHandler = (id: number) => {
|
||||
router.replace('/manage-repo/edit/' + id);
|
||||
};
|
||||
|
||||
|
|
@ -107,7 +108,7 @@ export default function RepoList() {
|
|||
};
|
||||
|
||||
//Dynamic list of repositories (with a map of Repo components)
|
||||
const renderRepoList = data.repoList.map((repo, index) => {
|
||||
const renderRepoList = data.repoList.map((repo: Repository) => {
|
||||
return (
|
||||
<React.Fragment key={repo.id}>
|
||||
<Repo
|
||||
|
|
@ -124,7 +125,7 @@ export default function RepoList() {
|
|||
comment={repo.comment}
|
||||
lanCommand={repo.lanCommand}
|
||||
appendOnlyMode={repo.appendOnlyMode}
|
||||
repoManageEditHandler={() => repoManageEditHandler(repo.id)}
|
||||
repoManageEditHandler={() => manageRepoEditHandler(repo.id)}
|
||||
wizardEnv={wizardEnv}
|
||||
></Repo>
|
||||
</React.Fragment>
|
||||
|
|
@ -148,12 +149,12 @@ export default function RepoList() {
|
|||
<div className={classes.RepoList}>{renderRepoList}</div>
|
||||
</div>
|
||||
</div>
|
||||
{displayRepoAdd ? (
|
||||
{displayRepoAdd && (
|
||||
<RepoManage mode='add' repoList={data.repoList} closeHandler={closeRepoManageBoxHandler} />
|
||||
) : null}
|
||||
{displayRepoEdit ? (
|
||||
)}
|
||||
{displayRepoEdit && (
|
||||
<RepoManage mode='edit' repoList={data.repoList} closeHandler={closeRepoManageBoxHandler} />
|
||||
) : null}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
.modale {
|
||||
position: fixed;
|
||||
top: 10%;
|
||||
width: 1000px;
|
||||
width: 800px;
|
||||
height: auto;
|
||||
max-width: 75%;
|
||||
max-height: 85%;
|
||||
|
|
@ -24,6 +24,11 @@
|
|||
animation: append-animate 0.3s linear;
|
||||
}
|
||||
|
||||
.modale h2 {
|
||||
margin-top: 0;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
@keyframes append-animate {
|
||||
from {
|
||||
transform: scale(0);
|
||||
|
|
@ -47,89 +52,98 @@
|
|||
|
||||
.repoManageForm {
|
||||
margin: auto;
|
||||
width: 80%;
|
||||
padding: 15px 30px 30px 30px;
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
font-family: Inter, sans-serif;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: #494b7a;
|
||||
margin: 0 auto;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.repoManageForm label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #494b7a;
|
||||
margin-top: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: #374151;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
background-color: #f9fafb;
|
||||
color: #111827;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
.repoManageForm textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.repoManageForm textarea:focus,
|
||||
.repoManageForm input:focus,
|
||||
.repoManageForm textarea:focus,
|
||||
.repoManageForm select:focus {
|
||||
outline: 1px solid #6d4aff;
|
||||
box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
|
||||
border-color: #6d4aff;
|
||||
background-color: #ffffff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(109, 74, 255, 0.3);
|
||||
}
|
||||
|
||||
.repoManageForm .invalid {
|
||||
background: #f3c7c7;
|
||||
border: 1px solid #e45454;
|
||||
outline: 1px solid #ff4a4a;
|
||||
background-color: #fef2f2;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.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);
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.repoManageForm button {
|
||||
display: block;
|
||||
margin: 15px auto;
|
||||
margin: 2rem auto 0 auto;
|
||||
background-color: #6d4aff;
|
||||
color: #ffffff;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.repoManageForm button:hover {
|
||||
display: block;
|
||||
margin: 15px auto;
|
||||
background-color: #5c3dff;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: red;
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: #dc2626;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.optionCommandWrapper {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
color: #494b7a;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.optionCommandWrapper label {
|
||||
|
|
@ -137,15 +151,33 @@
|
|||
}
|
||||
|
||||
.optionCommandWrapper input[type='checkbox'] {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #6d4aff;
|
||||
cursor: pointer;
|
||||
accent-color: #6d4aff;
|
||||
}
|
||||
|
||||
.optionCommandWrapper input[type='checkbox']:focus {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
accent-color: #6d4aff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(109, 74, 255, 0.4);
|
||||
}
|
||||
|
||||
.selectAlert {
|
||||
max-width: 160px;
|
||||
}
|
||||
.selectAlertWrapper label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.selectAlertWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
margin-top: 1.5rem;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* DELETE DIALOG */
|
||||
|
|
@ -254,6 +286,7 @@
|
|||
}
|
||||
|
||||
.littleDeleteButton {
|
||||
margin-top: 10px;
|
||||
border: none;
|
||||
font-weight: 300;
|
||||
color: red;
|
||||
|
|
@ -261,8 +294,3 @@
|
|||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectAlert {
|
||||
margin: auto auto 35px auto;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,47 @@
|
|||
//Lib
|
||||
import classes from './RepoManage.module.css';
|
||||
import { IconAlertCircle, IconX } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast, ToastOptions } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
import Select from 'react-select';
|
||||
import Link from 'next/link';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import { alertOptions } from '../../domain/constants';
|
||||
import { alertOptions, Repository, Optional } from '~/types';
|
||||
|
||||
export default function RepoManage(props) {
|
||||
////Var
|
||||
let targetRepo;
|
||||
type RepoManageProps = {
|
||||
mode: 'add' | 'edit';
|
||||
repoList: Optional<Array<Repository>>;
|
||||
closeHandler: () => void;
|
||||
};
|
||||
|
||||
type DataForm = {
|
||||
alias: string;
|
||||
storageSize: string;
|
||||
sshkey: string;
|
||||
comment: string;
|
||||
alert: { value: Optional<number>; label: string };
|
||||
lanCommand: boolean;
|
||||
appendOnlyMode: boolean;
|
||||
};
|
||||
|
||||
export default function RepoManage(props: RepoManageProps) {
|
||||
const router = useRouter();
|
||||
const targetRepo =
|
||||
props.mode === 'edit' && router.query.slug
|
||||
? props.repoList?.find((repo) => repo.id.toString() === router.query.slug)
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({ mode: 'onChange' });
|
||||
} = useForm<DataForm>({ mode: 'onChange' });
|
||||
|
||||
const toastOptions = {
|
||||
const toastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
|
|
@ -33,53 +51,54 @@ export default function RepoManage(props) {
|
|||
progress: undefined,
|
||||
};
|
||||
|
||||
////State
|
||||
const [deleteDialog, setDeleteDialog] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
////Functions
|
||||
//router.query.slug is undefined for few milliseconds on first render for a direct URL access (https://github.com/vercel/next.js/discussions/11484).
|
||||
//If I call repoManage with edit mode (props), i'm firstly waiting that router.query.slug being available before rendering.
|
||||
if (!router.query.slug && props.mode == 'edit') {
|
||||
return <SpinnerDotted size={30} thickness={100} speed={180} color='rgba(109, 74, 255, 1)' />;
|
||||
} else if (props.mode == 'edit') {
|
||||
for (let element in props.repoList) {
|
||||
if (props.repoList[element].id == router.query.slug) {
|
||||
targetRepo = props.repoList[element];
|
||||
}
|
||||
}
|
||||
//If the ID does not exist > 404
|
||||
if (!targetRepo) {
|
||||
if (props.mode === 'edit') {
|
||||
if (!router.query.slug) {
|
||||
return <SpinnerDotted size={30} thickness={100} speed={180} color='rgba(109, 74, 255, 1)' />;
|
||||
} else if (!targetRepo) {
|
||||
router.push('/404');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Delete a repo
|
||||
const deleteHandler = async () => {
|
||||
const deleteHandler = async (repositoryName?: string) => {
|
||||
if (!repositoryName) {
|
||||
toast.error('Repository name not found', toastOptions);
|
||||
router.replace('/');
|
||||
return;
|
||||
}
|
||||
//API Call for delete
|
||||
fetch('/api/repo/id/' + router.query.slug + '/delete', {
|
||||
await fetch('/api/v1/repositories/' + repositoryName, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
toast.success(
|
||||
'🗑 The repository #' + router.query.slug + ' has been successfully deleted',
|
||||
'🗑 The repository ' + repositoryName + ' has been successfully deleted',
|
||||
toastOptions
|
||||
);
|
||||
router.replace('/');
|
||||
} else {
|
||||
if (response.status == 403)
|
||||
if (response.status == 403) {
|
||||
toast.warning(
|
||||
'🔒 The server is currently protected against repository deletion.',
|
||||
toastOptions
|
||||
);
|
||||
else toast.error('An error has occurred', toastOptions);
|
||||
router.replace('/');
|
||||
console.log('Fail to delete');
|
||||
setIsLoading(false);
|
||||
router.replace('/');
|
||||
} else {
|
||||
const errorMessage = await response.json();
|
||||
toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
|
||||
router.replace('/');
|
||||
console.log('Fail to delete');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
@ -89,48 +108,39 @@ export default function RepoManage(props) {
|
|||
});
|
||||
};
|
||||
|
||||
//Verify that the SSH key is unique
|
||||
const isSSHKeyUnique = async (sshPublicKey) => {
|
||||
let isUnique = true;
|
||||
const isSSHKeyUnique = async (sshPublicKey: string): Promise<boolean> => {
|
||||
try {
|
||||
// Extract the first two columns of the SSH key in the form
|
||||
const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
|
||||
|
||||
// Extract the first two columns of the SSH key in the form
|
||||
const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
|
||||
const response = await fetch('/api/v1/repositories', { method: 'GET' });
|
||||
const data = await response.json();
|
||||
|
||||
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;
|
||||
const conflictingRepo = data.repoList.find((repo: { sshPublicKey: string; id: number }) => {
|
||||
const repoPublicKeyPrefix = repo.sshPublicKey.split(' ').slice(0, 2).join(' ');
|
||||
return (
|
||||
repoPublicKeyPrefix === publicKeyPrefix && (!targetRepo || repo.id !== targetRepo.id)
|
||||
);
|
||||
});
|
||||
return isUnique;
|
||||
|
||||
if (conflictingRepo) {
|
||||
toast.error(
|
||||
`The SSH key is already used in repository #${conflictingRepo.id}. Please use another key or delete the key from the other repository.`,
|
||||
toastOptions
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error('An error has occurred', toastOptions);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
//Form submit Handler for ADD or EDIT a repo
|
||||
const formSubmitHandler = async (dataForm) => {
|
||||
const formSubmitHandler = async (dataForm: DataForm) => {
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setIsLoading(true);
|
||||
//Verify that the SSH key is unique
|
||||
|
|
@ -150,7 +160,7 @@ export default function RepoManage(props) {
|
|||
appendOnlyMode: dataForm.appendOnlyMode,
|
||||
};
|
||||
//POST API to send new repo
|
||||
await fetch('/api/repo/add', {
|
||||
await fetch('/api/v1/repositories', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -163,7 +173,7 @@ export default function RepoManage(props) {
|
|||
router.replace('/');
|
||||
} else {
|
||||
const errorMessage = await response.json();
|
||||
toast.error(`An error has occurred : ${errorMessage.message}`, toastOptions);
|
||||
toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
|
||||
router.replace('/');
|
||||
console.log(`Fail to ${props.mode}`);
|
||||
}
|
||||
|
|
@ -184,7 +194,7 @@ export default function RepoManage(props) {
|
|||
lanCommand: dataForm.lanCommand,
|
||||
appendOnlyMode: dataForm.appendOnlyMode,
|
||||
};
|
||||
await fetch('/api/repo/id/' + router.query.slug + '/edit', {
|
||||
await fetch('/api/v1/repositories/' + targetRepo?.repositoryName, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -194,13 +204,13 @@ export default function RepoManage(props) {
|
|||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
toast.success(
|
||||
'The repository #' + targetRepo.id + ' has been successfully edited !',
|
||||
'The repository ' + targetRepo?.repositoryName + ' has been successfully edited !',
|
||||
toastOptions
|
||||
);
|
||||
router.replace('/');
|
||||
} else {
|
||||
const errorMessage = await response.json();
|
||||
toast.error(`An error has occurred : ${errorMessage.message}`, toastOptions);
|
||||
toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
|
||||
router.replace('/');
|
||||
console.log(`Fail to ${props.mode}`);
|
||||
}
|
||||
|
|
@ -231,14 +241,14 @@ export default function RepoManage(props) {
|
|||
color: 'rgba(99, 115, 129, 0.38)',
|
||||
}}
|
||||
>
|
||||
#{targetRepo.id}
|
||||
#{targetRepo?.id}
|
||||
</span>{' '}
|
||||
?
|
||||
</h1>
|
||||
</div>
|
||||
<div className={classes.deleteDialogMessage}>
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
You are about to permanently delete the repository <b>#{targetRepo.id}</b> and all
|
||||
You are about to permanently delete the repository <b>#{targetRepo?.id}</b> and all
|
||||
the backups it contains.
|
||||
</div>
|
||||
<div>The data will not be recoverable and it will not be possible to go back.</div>
|
||||
|
|
@ -253,7 +263,7 @@ export default function RepoManage(props) {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
deleteHandler();
|
||||
deleteHandler(targetRepo?.repositoryName);
|
||||
setIsLoading(true);
|
||||
}}
|
||||
className={classes.deleteButton}
|
||||
|
|
@ -267,18 +277,18 @@ export default function RepoManage(props) {
|
|||
) : (
|
||||
<div className={classes.formWrapper}>
|
||||
{props.mode == 'edit' && (
|
||||
<h1>
|
||||
<h2>
|
||||
Edit the repository{' '}
|
||||
<span
|
||||
style={{
|
||||
color: 'rgba(99, 115, 129, 0.38)',
|
||||
}}
|
||||
>
|
||||
#{targetRepo.id}
|
||||
#{targetRepo?.id}
|
||||
</span>
|
||||
</h1>
|
||||
</h2>
|
||||
)}
|
||||
{props.mode == 'add' && <h1>Add a repository</h1>}
|
||||
{props.mode == 'add' && <h2>Add a repository</h2>}
|
||||
<form className={classes.repoManageForm} onSubmit={handleSubmit(formSubmitHandler)}>
|
||||
{/* ALIAS */}
|
||||
<label htmlFor='alias'>Alias</label>
|
||||
|
|
@ -286,7 +296,7 @@ export default function RepoManage(props) {
|
|||
className='form-control is-invalid'
|
||||
placeholder='Alias for the repository, e.g."Server 1"'
|
||||
type='text'
|
||||
defaultValue={props.mode == 'edit' ? targetRepo.alias : null}
|
||||
defaultValue={props.mode == 'edit' ? targetRepo?.alias : undefined}
|
||||
{...register('alias', {
|
||||
required: 'An alias is required.',
|
||||
minLength: {
|
||||
|
|
@ -304,15 +314,14 @@ export default function RepoManage(props) {
|
|||
<label htmlFor='sshkey'>SSH public key</label>
|
||||
<textarea
|
||||
placeholder='Public key in OpenSSH format (rsa, ed25519, ed25519-sk)'
|
||||
type='text'
|
||||
defaultValue={props.mode == 'edit' ? targetRepo.sshPublicKey : null}
|
||||
defaultValue={props.mode == 'edit' ? targetRepo?.sshPublicKey : undefined}
|
||||
{...register('sshkey', {
|
||||
required: 'SSH public key is required.',
|
||||
pattern: {
|
||||
value:
|
||||
/^(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?$/,
|
||||
message:
|
||||
'Invalid public key. The SSH key needs to be in OpenSSH format (rsa, ed25519, ed25519-sk)',
|
||||
'Invalid public key. The key needs to be in OpenSSH format (rsa, ed25519, ed25519-sk)',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
|
@ -323,8 +332,9 @@ export default function RepoManage(props) {
|
|||
<label htmlFor='storageSize'>Storage Size (GB)</label>
|
||||
<input
|
||||
type='number'
|
||||
placeholder='1000'
|
||||
min='1'
|
||||
defaultValue={props.mode == 'edit' ? targetRepo.storageSize : null}
|
||||
defaultValue={props.mode == 'edit' ? targetRepo?.storageSize : undefined}
|
||||
{...register('storageSize', {
|
||||
required: 'A storage size is required.',
|
||||
})}
|
||||
|
|
@ -335,9 +345,7 @@ export default function RepoManage(props) {
|
|||
{/* COMMENT */}
|
||||
<label htmlFor='comment'>Comment</label>
|
||||
<textarea
|
||||
type='text'
|
||||
placeholder='Little comment for your repository...'
|
||||
defaultValue={props.mode == 'edit' ? targetRepo.comment : null}
|
||||
defaultValue={props.mode == 'edit' ? targetRepo?.comment : undefined}
|
||||
{...register('comment', {
|
||||
required: false,
|
||||
maxLength: {
|
||||
|
|
@ -353,16 +361,11 @@ export default function RepoManage(props) {
|
|||
<div className={classes.optionCommandWrapper}>
|
||||
<input
|
||||
type='checkbox'
|
||||
name='lanCommand'
|
||||
defaultChecked={props.mode == 'edit' ? targetRepo.lanCommand : false}
|
||||
defaultChecked={props.mode == 'edit' ? targetRepo?.lanCommand : false}
|
||||
{...register('lanCommand')}
|
||||
/>
|
||||
<label htmlFor='lanCommand'>Generates commands for use over LAN.</label>
|
||||
<label htmlFor='lanCommand'>Generates commands for use over LAN</label>
|
||||
<Link
|
||||
style={{
|
||||
alignSelf: 'baseline',
|
||||
marginLeft: '5px',
|
||||
}}
|
||||
href='https://borgwarehouse.com/docs/user-manual/repositories/#generates-commands-for-use-over-lan'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
|
|
@ -374,16 +377,11 @@ export default function RepoManage(props) {
|
|||
<div className={classes.optionCommandWrapper}>
|
||||
<input
|
||||
type='checkbox'
|
||||
name='appendOnlyMode'
|
||||
defaultChecked={props.mode == 'edit' ? targetRepo.appendOnlyMode : false}
|
||||
defaultChecked={props.mode == 'edit' ? targetRepo?.appendOnlyMode : false}
|
||||
{...register('appendOnlyMode')}
|
||||
/>
|
||||
<label htmlFor='appendOnlyMode'>Enable append-only mode.</label>
|
||||
<label htmlFor='appendOnlyMode'>Enable append-only mode</label>
|
||||
<Link
|
||||
style={{
|
||||
alignSelf: 'baseline',
|
||||
marginLeft: '5px',
|
||||
}}
|
||||
href='https://borgwarehouse.com/docs/user-manual/repositories/#append-only-mode'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
|
|
@ -392,50 +390,74 @@ export default function RepoManage(props) {
|
|||
</Link>
|
||||
</div>
|
||||
{/* ALERT */}
|
||||
<label style={{ margin: '25px auto 10px auto' }} htmlFor='alert'>
|
||||
Alert if there is no backup since :
|
||||
</label>
|
||||
<div className={classes.selectAlert}>
|
||||
<Controller
|
||||
name='alert'
|
||||
defaultValue={
|
||||
props.mode == 'edit'
|
||||
? alertOptions.find((x) => x.value === targetRepo.alert) || {
|
||||
value: targetRepo.alert,
|
||||
label: `${targetRepo.alert} seconds (custom)`,
|
||||
}
|
||||
: alertOptions[4]
|
||||
}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
options={alertOptions}
|
||||
isSearchable={false}
|
||||
maxMenuHeight={150}
|
||||
menuPlacement='top'
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
borderRadius: '5px',
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary25: '#c3b6fa',
|
||||
primary: '#6d4aff',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className={classes.selectAlertWrapper}>
|
||||
<label htmlFor='alert'>Alert if there is no backup since :</label>
|
||||
<div className={classes.selectAlert}>
|
||||
<Controller
|
||||
name='alert'
|
||||
defaultValue={
|
||||
props.mode == 'edit'
|
||||
? alertOptions.find((x) => x.value === targetRepo?.alert) || {
|
||||
value: targetRepo?.alert,
|
||||
label: `Custom value (${targetRepo?.alert} seconds)`,
|
||||
}
|
||||
: alertOptions[4]
|
||||
}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
options={alertOptions}
|
||||
isSearchable={false}
|
||||
maxMenuHeight={300}
|
||||
menuPlacement='top'
|
||||
styles={{
|
||||
control: (base) => ({
|
||||
...base,
|
||||
minHeight: '35px',
|
||||
height: '35px',
|
||||
}),
|
||||
valueContainer: (base) => ({
|
||||
...base,
|
||||
height: '35px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
padding: '0 8px',
|
||||
}),
|
||||
input: (base) => ({
|
||||
...base,
|
||||
margin: 0,
|
||||
}),
|
||||
indicatorsContainer: (base) => ({
|
||||
...base,
|
||||
height: '35px',
|
||||
}),
|
||||
}}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
borderRadius: 5,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary25: '#c3b6fa',
|
||||
primary: '#6d4aff',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '8px',
|
||||
marginTop: '2rem',
|
||||
}}
|
||||
>
|
||||
<SpinnerDotted size={30} thickness={150} speed={100} color='#6d4aff' />
|
||||
<SpinnerDotted size={40} thickness={150} speed={100} color='#6d4aff' />
|
||||
</div>
|
||||
) : (
|
||||
<button type='submit' className='defaultButton' disabled={!isValid || isSubmitting}>
|
||||
|
|
@ -1,60 +1,59 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from './SetupWizard.module.css';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Select from 'react-select';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Select, { SingleValue } from 'react-select';
|
||||
import classes from './SetupWizard.module.css';
|
||||
import { Optional, SelectedRepoWizard, Repository, WizardEnvType } from '~/types';
|
||||
|
||||
//Components
|
||||
import WizardStepBar from '../../Components/WizardSteps/WizardStepBar/WizardStepBar';
|
||||
import WizardStep1 from '../../Components/WizardSteps/WizardStep1/WizardStep1';
|
||||
import WizardStep2 from '../../Components/WizardSteps/WizardStep2/WizardStep2';
|
||||
import WizardStep3 from '../../Components/WizardSteps/WizardStep3/WizardStep3';
|
||||
import WizardStep4 from '../../Components/WizardSteps/WizardStep4/WizardStep4';
|
||||
import WizardStepBar from '../../Components/WizardSteps/WizardStepBar/WizardStepBar';
|
||||
|
||||
function SetupWizard(props) {
|
||||
////Var
|
||||
type SetupWizardProps = {
|
||||
step?: number;
|
||||
};
|
||||
|
||||
function SetupWizard(props: SetupWizardProps) {
|
||||
const router = useRouter();
|
||||
|
||||
////States
|
||||
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',
|
||||
});
|
||||
const [repoList, setRepoList] = useState<Optional<Array<Repository>>>();
|
||||
const [repoListIsLoading, setRepoListIsLoading] = useState<boolean>(true);
|
||||
const [step, setStep] = useState<number>(1);
|
||||
const [wizardEnv, setWizardEnv] = useState<Optional<WizardEnvType>>();
|
||||
const [selectedItem, setSelectedItem] = useState<Optional<SelectedRepoWizard>>();
|
||||
|
||||
////LifeCycle
|
||||
//ComponentDidMount
|
||||
useEffect(() => {
|
||||
//retrieve the repository list
|
||||
const repoList = async () => {
|
||||
const fetchRepoList = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/repo', {
|
||||
const response = await fetch('/api/v1/repositories', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setList((await response.json()).repoList);
|
||||
setListIsLoading(false);
|
||||
setRepoList((await response.json()).repoList);
|
||||
setRepoListIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log('Fetching datas error');
|
||||
}
|
||||
};
|
||||
repoList();
|
||||
fetchRepoList();
|
||||
//Fetch wizardEnv to hydrate Wizard' steps
|
||||
const fetchWizardEnv = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getWizardEnv', {
|
||||
const response = await fetch('/api/v1/account/wizard-env', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setWizardEnv((await response.json()).wizardEnv);
|
||||
const data: WizardEnvType = await response.json();
|
||||
setWizardEnv(data);
|
||||
} catch (error) {
|
||||
console.log('Fetching datas error');
|
||||
}
|
||||
|
|
@ -64,47 +63,53 @@ function SetupWizard(props) {
|
|||
//Component did update
|
||||
useEffect(() => {
|
||||
//Go to the step in the URL param when URL change
|
||||
setStep(props.step);
|
||||
props.step && setStep(props.step);
|
||||
}, [props.step]);
|
||||
|
||||
////Functions
|
||||
|
||||
//Options for react-select
|
||||
const options = list.map((repo) => ({
|
||||
const options: Optional<Array<SelectedRepoWizard>> = repoList?.map((repo) => ({
|
||||
label: `${repo.alias} - #${repo.id}`,
|
||||
value: `${repo.alias} - #${repo.id}`,
|
||||
id: repo.id,
|
||||
id: repo.id.toString(),
|
||||
repositoryName: repo.repositoryName,
|
||||
lanCommand: repo.lanCommand,
|
||||
lanCommand: repo.lanCommand ? repo.lanCommand : false,
|
||||
}));
|
||||
|
||||
//Step button (free selection of user)
|
||||
const changeStepHandler = (x) => router.push('/setup-wizard/' + x);
|
||||
const changeStepHandler = (x: number) => router.push('/setup-wizard/' + x.toString());
|
||||
|
||||
//Next Step button
|
||||
const nextStepHandler = () => {
|
||||
if (step < 4) {
|
||||
router.push('/setup-wizard/' + `${Number(step) + 1}`);
|
||||
if (step && step < 4) {
|
||||
router.push('/setup-wizard/' + `${step + 1}`);
|
||||
}
|
||||
};
|
||||
|
||||
//Previous Step button
|
||||
const previousStepHandler = () => {
|
||||
if (step > 1) {
|
||||
router.push('/setup-wizard/' + `${Number(step) - 1}`);
|
||||
if (step && step > 1) {
|
||||
router.push('/setup-wizard/' + `${step - 1}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeSelect = (option: SingleValue<SelectedRepoWizard>) => {
|
||||
if (option) {
|
||||
setSelectedItem(option);
|
||||
} else {
|
||||
setSelectedItem(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
//Change Step with State
|
||||
const wizardStep = (step) => {
|
||||
if (step == 1) {
|
||||
const wizardStep = (step?: number) => {
|
||||
if (!step || step === 1) {
|
||||
return <WizardStep1 />;
|
||||
} else if (step == 2) {
|
||||
return <WizardStep2 selectedOption={selectedOption} wizardEnv={wizardEnv} />;
|
||||
} else if (step == 3) {
|
||||
return <WizardStep3 selectedOption={selectedOption} wizardEnv={wizardEnv} />;
|
||||
} else if (step === 2) {
|
||||
return <WizardStep2 selectedRepo={selectedItem} wizardEnv={wizardEnv} />;
|
||||
} else if (step === 3) {
|
||||
return <WizardStep3 selectedRepo={selectedItem} wizardEnv={wizardEnv} />;
|
||||
} else {
|
||||
return <WizardStep4 selectedOption={selectedOption} wizardEnv={wizardEnv} />;
|
||||
return <WizardStep4 selectedRepo={selectedItem} wizardEnv={wizardEnv} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -118,15 +123,15 @@ function SetupWizard(props) {
|
|||
/>
|
||||
<div className={classes.selectRepo}>
|
||||
<Select
|
||||
onChange={setSelectedOption}
|
||||
isLoading={listIsLoading}
|
||||
isDisabled={listIsLoading}
|
||||
onChange={(item) => onChangeSelect(item)}
|
||||
isLoading={repoListIsLoading}
|
||||
isDisabled={repoListIsLoading}
|
||||
options={options}
|
||||
isSearchable
|
||||
placeholder='Select your repository...'
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
borderRadius: '5px',
|
||||
borderRadius: 5,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary25: '#c3b6fa',
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
//Lib
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Switch from '../../../Components/UI/Switch/Switch';
|
||||
import AppriseURLs from './AppriseURLs/AppriseURLs';
|
||||
import AppriseMode from './AppriseMode/AppriseMode';
|
||||
|
||||
export default function AppriseAlertSettings() {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
//Callback > re-enabled button after notification.
|
||||
onClose: () => setDisabled(false),
|
||||
};
|
||||
|
||||
////State
|
||||
const [checkIsLoading, setCheckIsLoading] = useState(true);
|
||||
const [error, setError] = useState();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [checked, setChecked] = useState();
|
||||
const [testIsLoading, setTestIsLoading] = useState(false);
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
//Initial fetch to get the status of Apprise Alert
|
||||
const getAppriseAlert = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getAppriseAlert', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setChecked((await response.json()).appriseAlert);
|
||||
setCheckIsLoading(false);
|
||||
} catch (error) {
|
||||
setError('Fetching apprise alert setting failed. Contact your administrator.');
|
||||
console.log('Fetching apprise alert setting failed.');
|
||||
setCheckIsLoading(false);
|
||||
}
|
||||
};
|
||||
getAppriseAlert();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Switch to enable/disable Apprise notifications
|
||||
const onChangeSwitchHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Disabled button
|
||||
setDisabled(true);
|
||||
await fetch('/api/account/updateAppriseAlert', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
if (response.ok) {
|
||||
if (data.appriseAlert) {
|
||||
setChecked(!checked);
|
||||
toast.success('Apprise notifications enabled.', toastOptions);
|
||||
} else {
|
||||
setChecked(!checked);
|
||||
toast.success('Apprise notifications disabled.', toastOptions);
|
||||
}
|
||||
} else {
|
||||
setError('Update apprise alert setting failed.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
setDisabled(false);
|
||||
}, 4000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setError('Update Apprise failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
setDisabled(false);
|
||||
}, 4000);
|
||||
});
|
||||
};
|
||||
|
||||
//Send Apprise test notification to services
|
||||
const onSendTestAppriseHandler = async () => {
|
||||
//Loading
|
||||
setTestIsLoading(true);
|
||||
//Remove old error
|
||||
setError();
|
||||
try {
|
||||
const response = await fetch('/api/account/sendTestApprise', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ sendTestApprise: true }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
setTestIsLoading(false);
|
||||
setError(result.message);
|
||||
} else {
|
||||
setTestIsLoading(false);
|
||||
setInfo(true);
|
||||
setTimeout(() => {
|
||||
setInfo(false);
|
||||
}, 4000);
|
||||
}
|
||||
} catch (error) {
|
||||
setTestIsLoading(false);
|
||||
console.log(error);
|
||||
setError('Send notification failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* APPRISE ALERT */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2 style={{ alignSelf: 'baseline' }}>Apprise alert</h2>
|
||||
<Link
|
||||
style={{ alignSelf: 'baseline', marginLeft: '5px' }}
|
||||
href='https://borgwarehouse.com/docs/user-manual/account/#apprise'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink size={16} color='#6c737f' />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
{/* NOTIFY SWITCH */}
|
||||
{checkIsLoading ? (
|
||||
<SpinnerCircularFixed
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<Switch
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
switchName='Notify my Apprise services'
|
||||
switchDescription='You will receive an alert on all your services every 24H if you have a down status.'
|
||||
onChange={(e) => onChangeSwitchHandler({ appriseAlert: e })}
|
||||
/>
|
||||
)}
|
||||
{/* APPRISE SERVICES URLS */}
|
||||
<AppriseURLs />
|
||||
{/* APPRISE MODE SELECTION */}
|
||||
<AppriseMode />
|
||||
{/* APPRISE TEST BUTTON */}
|
||||
{testIsLoading ? (
|
||||
<SpinnerCircularFixed
|
||||
style={{ marginTop: '20px' }}
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
style={{ marginTop: '20px' }}
|
||||
className='defaultButton'
|
||||
onClick={() => onSendTestAppriseHandler()}
|
||||
>
|
||||
Send a test notification
|
||||
</button>
|
||||
)}
|
||||
{info && (
|
||||
<span style={{ marginLeft: '10px', color: '#119300' }}>
|
||||
Notification successfully sent.
|
||||
</span>
|
||||
)}
|
||||
{error && <Error message={error} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast, ToastOptions } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import classes from '../UserSettings.module.css';
|
||||
|
||||
//Components
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import Switch from '~/Components/UI/Switch/Switch';
|
||||
import { useFormStatus } from '~/hooks';
|
||||
import { Optional } from '~/types';
|
||||
import AppriseMode from './AppriseMode/AppriseMode';
|
||||
import AppriseURLs from './AppriseURLs/AppriseURLs';
|
||||
|
||||
type AppriseAlertDataForm = {
|
||||
appriseAlert: boolean;
|
||||
};
|
||||
|
||||
export default function AppriseAlertSettings() {
|
||||
const toastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
};
|
||||
|
||||
const { error, handleError, clearError } = useFormStatus();
|
||||
|
||||
////State
|
||||
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
|
||||
const [isSwitchDisabled, setIsSwitchDisabled] = useState(true);
|
||||
const [isAlertEnabled, setIsAlertEnabled] = useState<Optional<boolean>>(undefined);
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
//Initial fetch to get the status of Apprise Alert
|
||||
const getAppriseAlert = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/notif/apprise/alert', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data: Optional<AppriseAlertDataForm> = await response.json();
|
||||
setIsAlertEnabled(data?.appriseAlert ?? false);
|
||||
setIsSwitchDisabled(false);
|
||||
} catch (error) {
|
||||
setIsSwitchDisabled(true);
|
||||
setIsAlertEnabled(false);
|
||||
handleError('Fetching apprise alert setting failed.');
|
||||
}
|
||||
};
|
||||
getAppriseAlert();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Switch to enable/disable Apprise notifications
|
||||
const onChangeSwitchHandler = async (data: AppriseAlertDataForm) => {
|
||||
clearError();
|
||||
setIsSwitchDisabled(true);
|
||||
await fetch('/api/v1/notif/apprise/alert', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok && typeof data.appriseAlert === 'boolean') {
|
||||
setIsAlertEnabled(data.appriseAlert);
|
||||
toast.success(
|
||||
data.appriseAlert ? 'Apprise notifications enabled' : 'Apprise notifications disabled',
|
||||
toastOptions
|
||||
);
|
||||
} else {
|
||||
handleError('Update apprise alert setting failed.');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
handleError('Update Apprise failed. Contact your administrator.');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSwitchDisabled(false);
|
||||
});
|
||||
};
|
||||
|
||||
//Send Apprise test notification to services
|
||||
const onSendTestAppriseHandler = async () => {
|
||||
clearError();
|
||||
setIsSendingTestNotification(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/notif/apprise/test', {
|
||||
method: 'POST',
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setIsSendingTestNotification(false);
|
||||
handleError(result.message);
|
||||
} else {
|
||||
setIsSendingTestNotification(false);
|
||||
setInfo(true);
|
||||
setTimeout(() => {
|
||||
setInfo(false);
|
||||
}, 4000);
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSendingTestNotification(false);
|
||||
handleError('Send notification failed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* APPRISE ALERT */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2 style={{ alignSelf: 'baseline' }}>Apprise alert</h2>
|
||||
<Link
|
||||
style={{ alignSelf: 'baseline', marginLeft: '5px' }}
|
||||
href='https://borgwarehouse.com/docs/user-manual/account/#apprise'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink size={16} color='#6c737f' />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
<Switch
|
||||
loading={isAlertEnabled === undefined}
|
||||
checked={isAlertEnabled}
|
||||
disabled={isSwitchDisabled}
|
||||
switchName='Notify my Apprise services'
|
||||
switchDescription='You will receive an alert on all your services every 24H if you have a down status.'
|
||||
onChange={(e) => onChangeSwitchHandler({ appriseAlert: e })}
|
||||
/>
|
||||
{isAlertEnabled && (
|
||||
<>
|
||||
<AppriseURLs />
|
||||
<AppriseMode />
|
||||
{isSendingTestNotification ? (
|
||||
<SpinnerCircularFixed
|
||||
style={{ marginTop: '20px' }}
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
style={{ marginTop: '20px' }}
|
||||
className='defaultButton'
|
||||
onClick={() => onSendTestAppriseHandler()}
|
||||
>
|
||||
Send a test notification
|
||||
</button>
|
||||
)}
|
||||
{info && (
|
||||
<span style={{ marginLeft: '10px', color: '#119300' }}>
|
||||
Notification successfully sent.
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{error && <Error message={error} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,44 +1,52 @@
|
|||
//Lib
|
||||
import { useEffect } from 'react';
|
||||
import classes from '../../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Optional, AppriseModeEnum, AppriseModeDTO } from '~/types';
|
||||
|
||||
//Components
|
||||
import Error from '../../../../Components/UI/Error/Error';
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import { useFormStatus } from '~/hooks';
|
||||
|
||||
type AppriseModeDataForm = {
|
||||
appriseMode: string;
|
||||
appriseStatelessURL: string;
|
||||
};
|
||||
|
||||
export default function AppriseMode() {
|
||||
//Var
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({ mode: 'onBlur' });
|
||||
} = useForm<AppriseModeDataForm>({ mode: 'onBlur' });
|
||||
|
||||
////State
|
||||
const [formIsLoading, setFormIsLoading] = useState(false);
|
||||
const [modeFormIsSaved, setModeFormIsSaved] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [displayStatelessURL, setDisplayStatelessURL] = useState(false);
|
||||
const [appriseMode, setAppriseMode] = useState('stateless');
|
||||
const [appriseStatelessURL, setAppriseStatelessURL] = useState();
|
||||
const { isLoading, isSaved, error, setIsLoading, handleSuccess, handleError, clearError } =
|
||||
useFormStatus();
|
||||
|
||||
const [displayStatelessURL, setDisplayStatelessURL] = useState<boolean>(false);
|
||||
const [appriseMode, setAppriseMode] = useState<Optional<AppriseModeEnum>>(
|
||||
AppriseModeEnum.STATELESS
|
||||
);
|
||||
const [appriseStatelessURL, setAppriseStatelessURL] = useState<Optional<string>>();
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
//Initial fetch to get Apprise Mode enabled
|
||||
const getAppriseMode = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getAppriseMode', {
|
||||
const response = await fetch('/api/v1/notif/apprise/mode', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
const { appriseStatelessURL, appriseMode } = await response.json();
|
||||
|
||||
const data: AppriseModeDTO = await response.json();
|
||||
const { appriseStatelessURL, appriseMode } = data;
|
||||
setAppriseMode(appriseMode);
|
||||
if (appriseMode == 'stateless') {
|
||||
|
||||
if (appriseMode == AppriseModeEnum.STATELESS) {
|
||||
setAppriseStatelessURL(appriseStatelessURL);
|
||||
setDisplayStatelessURL(true);
|
||||
}
|
||||
|
|
@ -50,15 +58,12 @@ export default function AppriseMode() {
|
|||
}, []);
|
||||
|
||||
////Functions
|
||||
//Form submit handler to modify Apprise Mode
|
||||
const modeFormSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setFormIsLoading(true);
|
||||
//POST API to update Apprise Mode
|
||||
const modeFormSubmitHandler = async (data: AppriseModeDataForm) => {
|
||||
clearError();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/account/updateAppriseMode', {
|
||||
const response = await fetch('/api/v1/notif/apprise/mode', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -68,20 +73,12 @@ export default function AppriseMode() {
|
|||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setFormIsLoading(false);
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
handleError(result.message);
|
||||
} else {
|
||||
setFormIsLoading(false);
|
||||
setModeFormIsSaved(true);
|
||||
setTimeout(() => setModeFormIsSaved(false), 3000);
|
||||
handleSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
setFormIsLoading(false);
|
||||
setError('Change mode failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
handleError('The Apprise mode change has failed');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -91,7 +88,7 @@ export default function AppriseMode() {
|
|||
<div className={classes.headerFormAppriseUrls}>
|
||||
<div style={{ margin: '0px 10px 0px 0px' }}>Apprise mode</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{formIsLoading && (
|
||||
{isLoading && (
|
||||
<SpinnerCircularFixed
|
||||
size={18}
|
||||
thickness={150}
|
||||
|
|
@ -100,7 +97,7 @@ export default function AppriseMode() {
|
|||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
)}
|
||||
{modeFormIsSaved && (
|
||||
{isSaved && (
|
||||
<div className={classes.formIsSavedMessage}>✅ Apprise mode has been saved.</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -116,7 +113,7 @@ export default function AppriseMode() {
|
|||
value='package'
|
||||
onClick={() => {
|
||||
setDisplayStatelessURL(false);
|
||||
setAppriseMode('package');
|
||||
setAppriseMode(AppriseModeEnum.PACKAGE);
|
||||
}}
|
||||
checked={appriseMode == 'package' ? true : false}
|
||||
/>
|
||||
|
|
@ -131,7 +128,7 @@ export default function AppriseMode() {
|
|||
type='radio'
|
||||
onClick={() => {
|
||||
setDisplayStatelessURL(true);
|
||||
setAppriseMode('stateless');
|
||||
setAppriseMode(AppriseModeEnum.STATELESS);
|
||||
}}
|
||||
checked={appriseMode == 'stateless' ? true : false}
|
||||
/>
|
||||
|
|
@ -1,65 +1,66 @@
|
|||
//Lib
|
||||
import { useEffect } from 'react';
|
||||
import classes from '../../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Optional, AppriseServicesDTO } from '~/types';
|
||||
|
||||
//Components
|
||||
import Error from '../../../../Components/UI/Error/Error';
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import { useFormStatus } from '~/hooks';
|
||||
|
||||
type AppriseURLsDataForm = {
|
||||
appriseURLs: string;
|
||||
};
|
||||
|
||||
export default function AppriseURLs() {
|
||||
//Var
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({ mode: 'onBlur' });
|
||||
} = useForm<AppriseURLsDataForm>({ mode: 'onBlur' });
|
||||
|
||||
////State
|
||||
const [formIsLoading, setFormIsLoading] = useState(false);
|
||||
const [urlsFormIsSaved, setUrlsFormIsSaved] = useState(false);
|
||||
const [appriseServicesList, setAppriseServicesList] = useState();
|
||||
const [error, setError] = useState();
|
||||
const { isLoading, isSaved, error, setIsLoading, handleSuccess, handleError, clearError } =
|
||||
useFormStatus();
|
||||
|
||||
const [appriseServicesList, setAppriseServicesList] = useState<Optional<string>>();
|
||||
const [fetchError, setFetchError] = useState<Optional<boolean>>();
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
//Initial fetch to build the list of Apprise Services enabled
|
||||
const getAppriseServices = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getAppriseServices', {
|
||||
const response = await fetch('/api/v1/notif/apprise/services', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
let servicesArray = (await response.json()).appriseServices;
|
||||
const AppriseServicesListToText = () => {
|
||||
let list = '';
|
||||
for (let service of servicesArray) {
|
||||
list += service + '\n';
|
||||
}
|
||||
return list;
|
||||
};
|
||||
setAppriseServicesList(AppriseServicesListToText());
|
||||
|
||||
const data: AppriseServicesDTO = await response.json();
|
||||
const servicesText = data.appriseServices?.join('\n');
|
||||
setAppriseServicesList(servicesText);
|
||||
setFetchError(false);
|
||||
} catch (error) {
|
||||
console.log('Fetching Apprise services list failed.');
|
||||
setFetchError(true);
|
||||
handleError('Fetching Apprise services list failed.');
|
||||
}
|
||||
};
|
||||
getAppriseServices();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Form submit handler to modify Apprise services
|
||||
const urlsFormSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setFormIsLoading(true);
|
||||
//POST API to update Apprise Services
|
||||
const urlsFormSubmitHandler = async (data: AppriseURLsDataForm) => {
|
||||
clearError();
|
||||
if (fetchError) {
|
||||
handleError('Cannot update Apprise services. Failed to fetch the initial list.');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/account/updateAppriseServices', {
|
||||
const response = await fetch('/api/v1/notif/apprise/services', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -69,20 +70,12 @@ export default function AppriseURLs() {
|
|||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setFormIsLoading(false);
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
handleError(result.message);
|
||||
} else {
|
||||
setFormIsLoading(false);
|
||||
setUrlsFormIsSaved(true);
|
||||
setTimeout(() => setUrlsFormIsSaved(false), 3000);
|
||||
handleSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
setFormIsLoading(false);
|
||||
setError('Failed to update your services. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
handleError('Failed to update your Apprise services.');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -91,9 +84,8 @@ export default function AppriseURLs() {
|
|||
{/* APPRISE SERVICES URLS */}
|
||||
<div className={classes.headerFormAppriseUrls}>
|
||||
<div style={{ marginRight: '10px' }}>Apprise URLs</div>
|
||||
{error && <Error message={error} />}
|
||||
<div style={{ display: 'flex' }}>
|
||||
{formIsLoading && (
|
||||
{isLoading && (
|
||||
<SpinnerCircularFixed
|
||||
size={18}
|
||||
thickness={150}
|
||||
|
|
@ -102,7 +94,7 @@ export default function AppriseURLs() {
|
|||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
)}
|
||||
{urlsFormIsSaved && (
|
||||
{isSaved && (
|
||||
<div className={classes.formIsSavedMessage}>
|
||||
✅ Apprise configuration has been saved.
|
||||
</div>
|
||||
|
|
@ -115,7 +107,6 @@ export default function AppriseURLs() {
|
|||
>
|
||||
<textarea
|
||||
style={{ height: '100px' }}
|
||||
type='text'
|
||||
placeholder={
|
||||
'matrixs://{user}:{password}@{matrixhost}\ndiscord://{WebhookID}/{WebhookToken}\nmmosts://user@hostname/authkey'
|
||||
}
|
||||
|
|
@ -151,6 +142,7 @@ export default function AppriseURLs() {
|
|||
</a>{' '}
|
||||
to send a notification to any service. Only one URL per line.
|
||||
</div>
|
||||
{error && <Error message={error} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
//Lib
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Switch from '../../../Components/UI/Switch/Switch';
|
||||
|
||||
export default function EmailAlertSettings() {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
//Callback > re-enabled button after notification.
|
||||
onClose: () => setDisabled(false),
|
||||
};
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [testIsLoading, setTestIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [checked, setChecked] = useState();
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
const dataFetch = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getEmailAlert', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setChecked((await response.json()).emailAlert);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setError('Fetching email alert setting failed. Contact your administrator.');
|
||||
console.log('Fetching email alert setting failed.');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
dataFetch();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Switch to enable/disable Email notifications
|
||||
const onChangeSwitchHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Disabled button
|
||||
setDisabled(true);
|
||||
await fetch('/api/account/updateEmailAlert', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
if (response.ok) {
|
||||
if (data.emailAlert) {
|
||||
setChecked(!checked);
|
||||
toast.success('Email notification enabled !', toastOptions);
|
||||
} else {
|
||||
setChecked(!checked);
|
||||
toast.success('Email notification disabled !', toastOptions);
|
||||
}
|
||||
} else {
|
||||
setError('Update email alert setting failed.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
setDisabled(false);
|
||||
}, 4000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setError('Update failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
setDisabled(false);
|
||||
}, 4000);
|
||||
});
|
||||
};
|
||||
|
||||
//Send a test notification by email
|
||||
const onSendTestMailHandler = async () => {
|
||||
//Loading
|
||||
setTestIsLoading(true);
|
||||
//Remove old error
|
||||
setError();
|
||||
await fetch('/api/account/sendTestEmail', {
|
||||
method: 'POST',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
setTestIsLoading(false);
|
||||
response
|
||||
.json()
|
||||
.then((data) => {
|
||||
setError(data.message || 'Failed to send the notification.');
|
||||
})
|
||||
.catch(() => {
|
||||
setError('Failed to send the notification.');
|
||||
});
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 10000);
|
||||
} else {
|
||||
setTestIsLoading(false);
|
||||
setInfo(true);
|
||||
setTimeout(() => {
|
||||
setInfo(false);
|
||||
}, 4000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setTestIsLoading(false);
|
||||
setError('Send email failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* EMAIL ALERT */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2 style={{ alignSelf: 'baseline' }}>Email alert</h2>
|
||||
<Link
|
||||
style={{ alignSelf: 'baseline', marginLeft: '5px' }}
|
||||
href='https://borgwarehouse.com/docs/user-manual/account/#alerting'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink size={16} color='#6c737f' />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
{isLoading ? (
|
||||
<SpinnerCircularFixed
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<Switch
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
switchName='Alert me by email'
|
||||
switchDescription='You will receive an alert every 24H if you have a down status.'
|
||||
onChange={(e) => onChangeSwitchHandler({ emailAlert: e })}
|
||||
/>
|
||||
)}
|
||||
{testIsLoading ? (
|
||||
<SpinnerCircularFixed
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<button className='defaultButton' onClick={onSendTestMailHandler}>
|
||||
Send a test mail
|
||||
</button>
|
||||
)}
|
||||
{info && (
|
||||
<span style={{ marginLeft: '10px', color: '#119300' }}>Mail successfully sent.</span>
|
||||
)}
|
||||
{error && <Error message={error} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast, ToastOptions } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { Optional, EmailAlertDTO } from '~/types';
|
||||
|
||||
//Components
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import Switch from '~/Components/UI/Switch/Switch';
|
||||
import { useFormStatus } from '~/hooks';
|
||||
|
||||
export default function EmailAlertSettings() {
|
||||
const toastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
//Callback > re-enabled button after notification.
|
||||
onClose: () => setIsSwitchDisabled(false),
|
||||
};
|
||||
|
||||
const { error, handleError, clearError } = useFormStatus();
|
||||
|
||||
////State
|
||||
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
|
||||
const [isSwitchDisabled, setIsSwitchDisabled] = useState(true);
|
||||
const [isAlertEnabled, setIsAlertEnabled] = useState<Optional<boolean>>(undefined);
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
const dataFetch = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/notif/email/alert', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data: Optional<EmailAlertDTO> = await response.json();
|
||||
setIsAlertEnabled(data?.emailAlert ?? false);
|
||||
setIsSwitchDisabled(false);
|
||||
} catch (error) {
|
||||
setIsSwitchDisabled(true);
|
||||
setIsAlertEnabled(false);
|
||||
handleError('Fetching email alert setting failed');
|
||||
}
|
||||
};
|
||||
dataFetch();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Switch to enable/disable Email notifications
|
||||
const onChangeSwitchHandler = async (data: EmailAlertDTO) => {
|
||||
clearError();
|
||||
setIsSwitchDisabled(true);
|
||||
await fetch('/api/v1/notif/email/alert', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok && typeof data.emailAlert === 'boolean') {
|
||||
setIsAlertEnabled(data.emailAlert);
|
||||
toast.success(
|
||||
data.emailAlert ? 'Email notification enabled !' : 'Email notification disabled !',
|
||||
toastOptions
|
||||
);
|
||||
} else {
|
||||
handleError('Update email alert setting failed.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
handleError('Update email alert setting failed.');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSwitchDisabled(false);
|
||||
});
|
||||
};
|
||||
|
||||
//Send a test notification by email
|
||||
const onSendTestMailHandler = async () => {
|
||||
clearError();
|
||||
setIsSendingTestNotification(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/notif/email/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setIsSendingTestNotification(false);
|
||||
handleError(result.message);
|
||||
} else {
|
||||
setIsSendingTestNotification(false);
|
||||
setInfo(true);
|
||||
setTimeout(() => {
|
||||
setInfo(false);
|
||||
}, 4000);
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSendingTestNotification(false);
|
||||
handleError('Send notification failed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* EMAIL ALERT */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2 style={{ alignSelf: 'baseline' }}>Email alert</h2>
|
||||
<Link
|
||||
style={{ alignSelf: 'baseline', marginLeft: '5px' }}
|
||||
href='https://borgwarehouse.com/docs/user-manual/account/#alerting'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink size={16} color='#6c737f' />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
<Switch
|
||||
loading={isAlertEnabled === undefined}
|
||||
checked={isAlertEnabled}
|
||||
disabled={isSwitchDisabled}
|
||||
switchName='Alert me by email'
|
||||
switchDescription='You will receive an alert every 24H if you have a down status.'
|
||||
onChange={(e) => onChangeSwitchHandler({ emailAlert: e })}
|
||||
/>
|
||||
{isSendingTestNotification ? (
|
||||
<SpinnerCircularFixed
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<button className='defaultButton' onClick={onSendTestMailHandler}>
|
||||
Send a test mail
|
||||
</button>
|
||||
)}
|
||||
{info && (
|
||||
<span style={{ marginLeft: '10px', color: '#119300' }}>Mail successfully sent.</span>
|
||||
)}
|
||||
{error && <Error message={error} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
//Lib
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast, ToastOptions } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
|
|
@ -7,12 +6,13 @@ import { useForm } from 'react-hook-form';
|
|||
import { SpinnerDotted } from 'spinners-react';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Info from '../../../Components/UI/Info/Info';
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import Info from '~/Components/UI/Info/Info';
|
||||
import { useFormStatus } from '~/hooks';
|
||||
import { EmailSettingDTO } from '~/types/api/setting.types';
|
||||
|
||||
export default function EmailSettings(props) {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
export default function EmailSettings(props: EmailSettingDTO) {
|
||||
const toastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 8000,
|
||||
hideProgressBar: false,
|
||||
|
|
@ -27,23 +27,20 @@ export default function EmailSettings(props) {
|
|||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({ mode: 'onChange' });
|
||||
} = useForm<EmailSettingDTO>({ mode: 'onChange' });
|
||||
|
||||
const { isLoading, error, setIsLoading, handleError, clearError } = useFormStatus();
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////Functions
|
||||
//Form submit Handler for ADD a repo
|
||||
const formSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
const formSubmitHandler = async (data: EmailSettingDTO) => {
|
||||
clearError();
|
||||
setIsLoading(true);
|
||||
//POST API to send the new mail address
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/account/updateEmail', {
|
||||
const response = await fetch('/api/v1/account/email', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -53,10 +50,8 @@ export default function EmailSettings(props) {
|
|||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
handleError(result.message);
|
||||
} else {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
|
|
@ -65,9 +60,7 @@ export default function EmailSettings(props) {
|
|||
}
|
||||
} catch (error) {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
setError("Can't update your email. Contact your administrator.");
|
||||
setTimeout(() => setError(), 4000);
|
||||
handleError('Updating your email failed.');
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
|
@ -1,23 +1,26 @@
|
|||
//Lib
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast, ToastOptions } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import timestampConverter from '../../../helpers/functions/timestampConverter';
|
||||
import { fromUnixTime } from 'date-fns';
|
||||
import { IconTrash, IconExternalLink } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { Optional, IntegrationTokenType, TokenPermissionEnum, TokenPermissionsType } from '~/types';
|
||||
import { useFormStatus } from '~/hooks';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import CopyButton from '../../../Components/UI/CopyButton/CopyButton';
|
||||
import Info from '../../../Components/UI/Info/Info';
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import CopyButton from '~/Components/UI/CopyButton/CopyButton';
|
||||
import Info from '~/Components/UI/Info/Info';
|
||||
|
||||
type IntegrationsDataForm = {
|
||||
tokenName: string;
|
||||
};
|
||||
|
||||
export default function Integrations() {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
const toastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
|
|
@ -32,16 +35,27 @@ export default function Integrations() {
|
|||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({ mode: 'onChange' });
|
||||
} = useForm<IntegrationsDataForm>({ mode: 'onChange' });
|
||||
|
||||
const { error, handleError, clearError, setIsLoading, isLoading } = useFormStatus();
|
||||
|
||||
const renderPermissionBadges = (permissions: TokenPermissionsType) => {
|
||||
return Object.entries(permissions)
|
||||
.filter(([, hasPermission]) => hasPermission)
|
||||
.map(([key]) => (
|
||||
<div key={key} className={classes.permissionBadge}>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [tokenList, setTokenList] = useState([]);
|
||||
const [error, setError] = useState();
|
||||
const [lastGeneratedToken, setLastGeneratedToken] = useState();
|
||||
const [deletingToken, setDeletingToken] = useState(null);
|
||||
const [permissions, setPermissions] = useState({
|
||||
const [tokenList, setTokenList] = useState<Array<IntegrationTokenType>>();
|
||||
const [lastGeneratedToken, setLastGeneratedToken] =
|
||||
useState<Optional<{ name: string; value: string }>>();
|
||||
const [deletingToken, setDeletingToken] = useState<Optional<IntegrationTokenType>>(undefined);
|
||||
const [permissions, setPermissions] = useState<TokenPermissionsType>({
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
|
|
@ -50,16 +64,16 @@ export default function Integrations() {
|
|||
|
||||
const fetchTokenList = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/tokenManager', {
|
||||
const response = await fetch('/api/v1/integration/token-manager', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
const tokensArray = await response.json();
|
||||
setTokenList(tokensArray);
|
||||
const data: Array<IntegrationTokenType> = await response.json();
|
||||
setTokenList(data);
|
||||
} catch (error) {
|
||||
console.log('Fetching token list failed.');
|
||||
handleError('Fetching token list failed.');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -72,7 +86,7 @@ export default function Integrations() {
|
|||
const hasNoPermissionSelected = () => {
|
||||
return !Object.values(permissions).some((value) => value);
|
||||
};
|
||||
const togglePermission = (permissionType) => {
|
||||
const togglePermission = (permissionType: TokenPermissionEnum) => {
|
||||
const updatedPermissions = {
|
||||
...permissions,
|
||||
[permissionType]: !permissions[permissionType],
|
||||
|
|
@ -88,39 +102,31 @@ export default function Integrations() {
|
|||
});
|
||||
};
|
||||
|
||||
//Form submit Handler for ADD a new token
|
||||
const formSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
//Form submit handler to ADD a new token
|
||||
const formSubmitHandler = async (data: IntegrationsDataForm) => {
|
||||
clearError();
|
||||
setIsLoading(true);
|
||||
//Generate a UUIDv4
|
||||
const token = uuidv4();
|
||||
setLastGeneratedToken({ name: data.tokenName, value: token });
|
||||
|
||||
// Post API to send the new token integration
|
||||
try {
|
||||
const response = await fetch('/api/account/tokenManager', {
|
||||
const response = await fetch('/api/v1/integration/token-manager', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: data.tokenName,
|
||||
token: token,
|
||||
creation: Math.floor(Date.now() / 1000),
|
||||
expiration: null,
|
||||
permissions: permissions,
|
||||
}),
|
||||
});
|
||||
const result = await response.json();
|
||||
setLastGeneratedToken({ name: data.tokenName, value: result.token });
|
||||
|
||||
if (!response.ok) {
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
resetPermissions();
|
||||
toast.error(result.message, toastOptions);
|
||||
setTimeout(() => setError(), 4000);
|
||||
} else {
|
||||
reset();
|
||||
resetPermissions();
|
||||
|
|
@ -132,16 +138,15 @@ export default function Integrations() {
|
|||
reset();
|
||||
resetPermissions();
|
||||
setIsLoading(false);
|
||||
toast.error("Can't generate your token. Contact your administrator.", toastOptions);
|
||||
setTimeout(() => setError(), 4000);
|
||||
toast.error('Failed to generate a new token', toastOptions);
|
||||
}
|
||||
};
|
||||
|
||||
//Delete token
|
||||
const deleteTokenHandler = async (tokenName) => {
|
||||
const deleteTokenHandler = async (tokenName: string) => {
|
||||
setIsDeleteLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/account/tokenManager', {
|
||||
const response = await fetch('/api/v1/integration/token-manager', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -154,7 +159,6 @@ export default function Integrations() {
|
|||
|
||||
if (!response.ok) {
|
||||
toast.error(result.message, toastOptions);
|
||||
setTimeout(() => setError(), 4000);
|
||||
setIsDeleteLoading(false);
|
||||
} else {
|
||||
fetchTokenList();
|
||||
|
|
@ -163,11 +167,10 @@ export default function Integrations() {
|
|||
}
|
||||
} catch (error) {
|
||||
setIsDeleteLoading(false);
|
||||
toast.error("Can't delete your token. Contact your administrator.", toastOptions);
|
||||
setTimeout(() => setError(), 4000);
|
||||
toast.error('Failed to delete the token', toastOptions);
|
||||
} finally {
|
||||
setIsDeleteLoading(false);
|
||||
setDeletingToken(null);
|
||||
setDeletingToken(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -205,25 +208,25 @@ export default function Integrations() {
|
|||
<div className={classes.permissionsWrapper}>
|
||||
<div
|
||||
className={`${classes.permissionBadge} ${permissions.create ? classes.highlight : ''}`}
|
||||
onClick={() => togglePermission('create')}
|
||||
onClick={() => togglePermission(TokenPermissionEnum.CREATE)}
|
||||
>
|
||||
Create
|
||||
</div>
|
||||
<div
|
||||
className={`${classes.permissionBadge} ${permissions.read ? classes.highlight : ''}`}
|
||||
onClick={() => togglePermission('read')}
|
||||
onClick={() => togglePermission(TokenPermissionEnum.READ)}
|
||||
>
|
||||
Read
|
||||
</div>
|
||||
<div
|
||||
className={`${classes.permissionBadge} ${permissions.update ? classes.highlight : ''}`}
|
||||
onClick={() => togglePermission('update')}
|
||||
onClick={() => togglePermission(TokenPermissionEnum.UPDATE)}
|
||||
>
|
||||
Update
|
||||
</div>
|
||||
<div
|
||||
className={`${classes.permissionBadge} ${permissions.delete ? classes.highlight : ''}`}
|
||||
onClick={() => togglePermission('delete')}
|
||||
onClick={() => togglePermission(TokenPermissionEnum.DELETE)}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
|
|
@ -272,25 +275,19 @@ export default function Integrations() {
|
|||
>
|
||||
<div className={classes.tokenCardHeader}>{token.name}</div>
|
||||
<div className={classes.tokenCardBody}>
|
||||
<p>
|
||||
<div className={classes.tokenInfo}>
|
||||
<strong>Created at:</strong>
|
||||
{timestampConverter(token.creation)}
|
||||
</p>
|
||||
<p>
|
||||
{fromUnixTime(token.creation).toLocaleString()}
|
||||
</div>
|
||||
<div className={classes.tokenInfo}>
|
||||
<strong>Permission:</strong>
|
||||
<div className={classes.permissionBadges}>
|
||||
{Object.keys(token.permissions).map((permission) =>
|
||||
token.permissions[permission] ? (
|
||||
<div key={permission} className={classes.permissionBadge}>
|
||||
{permission.charAt(0).toUpperCase() + permission.slice(1)}
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
{renderPermissionBadges(token.permissions)}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
{lastGeneratedToken && lastGeneratedToken.name === token.name && (
|
||||
<>
|
||||
<p>
|
||||
<div className={classes.tokenInfo}>
|
||||
<strong>Token:</strong>
|
||||
<CopyButton
|
||||
size={22}
|
||||
|
|
@ -299,10 +296,11 @@ export default function Integrations() {
|
|||
>
|
||||
<span>{lastGeneratedToken.value}</span>
|
||||
</CopyButton>
|
||||
</p>
|
||||
<Info color='#3498db'>
|
||||
This token will not be shown again. Please save it.
|
||||
</Info>
|
||||
</div>
|
||||
<Info
|
||||
color='#3498db'
|
||||
message='This token will not be shown again. Please save it.'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{deletingToken && deletingToken.name === token.name && (
|
||||
|
|
@ -320,7 +318,7 @@ export default function Integrations() {
|
|||
{!isDeleteLoading && (
|
||||
<button
|
||||
className={classes.cancelButton}
|
||||
onClick={() => setDeletingToken(null)}
|
||||
onClick={() => setDeletingToken(undefined)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
//Lib
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast, ToastOptions } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
import { useFormStatus } from '~/hooks';
|
||||
import { PasswordSettingDTO } from '~/types';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
|
||||
export default function PasswordSettings(props) {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
export default function PasswordSettings() {
|
||||
const toastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
|
|
@ -26,23 +25,17 @@ export default function PasswordSettings(props) {
|
|||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({ mode: 'onChange' });
|
||||
} = useForm<PasswordSettingDTO>({ mode: 'onChange' });
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const { isLoading, error, setIsLoading, handleError, clearError } = useFormStatus();
|
||||
|
||||
////Functions
|
||||
//Form submit Handler for ADD a repo
|
||||
const formSubmitHandler = async (data) => {
|
||||
console.log(data);
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
const formSubmitHandler = async (data: PasswordSettingDTO) => {
|
||||
clearError();
|
||||
setIsLoading(true);
|
||||
//POST API to send the new and old password
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/account/updatePassword', {
|
||||
const response = await fetch('/api/v1/account/password', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -54,8 +47,7 @@ export default function PasswordSettings(props) {
|
|||
if (!response.ok) {
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
handleError(result.message);
|
||||
} else {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
|
|
@ -64,8 +56,7 @@ export default function PasswordSettings(props) {
|
|||
} catch (error) {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
setError("Can't update your password. Contact your administrator.");
|
||||
setTimeout(() => setError(), 4000);
|
||||
handleError('Failed to update password. Please try again.');
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
//Lib
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from './UserSettings.module.css';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
//Components
|
||||
import EmailSettings from './EmailSettings/EmailSettings';
|
||||
import PasswordSettings from './PasswordSettings/PasswordSettings';
|
||||
import UsernameSettings from './UsernameSettings/UsernameSettings';
|
||||
import EmailAlertSettings from './EmailAlertSettings/EmailAlertSettings';
|
||||
import AppriseAlertSettings from './AppriseAlertSettings/AppriseAlertSettings';
|
||||
import Integrations from './Integrations/Integrations';
|
||||
|
||||
export default function UserSettings(props) {
|
||||
//States
|
||||
const [tab, setTab] = useState('General');
|
||||
const [wizardEnv, setWizardEnv] = useState({});
|
||||
|
||||
//ComponentDidMount
|
||||
useEffect(() => {
|
||||
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();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classes.containerSettings}>
|
||||
<div>
|
||||
<h1
|
||||
style={{
|
||||
color: '#494b7a',
|
||||
textAlign: 'left',
|
||||
marginLeft: '30px',
|
||||
}}
|
||||
>
|
||||
Account{' '}
|
||||
</h1>
|
||||
</div>
|
||||
<div className={classes.tabList}>
|
||||
<button
|
||||
className={tab === 'General' ? classes.tabListButtonActive : classes.tabListButton}
|
||||
onClick={() => setTab('General')}
|
||||
>
|
||||
General
|
||||
</button>
|
||||
<button
|
||||
className={tab === 'Notifications' ? classes.tabListButtonActive : classes.tabListButton}
|
||||
onClick={() => setTab('Notifications')}
|
||||
>
|
||||
Notifications
|
||||
</button>
|
||||
{wizardEnv.DISABLE_INTEGRATIONS !== 'true' && (
|
||||
<button
|
||||
className={tab === 'Integrations' ? classes.tabListButtonActive : classes.tabListButton}
|
||||
onClick={() => setTab('Integrations')}
|
||||
>
|
||||
Integrations
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{tab === 'General' && (
|
||||
<>
|
||||
<PasswordSettings username={props.data.user.name} />
|
||||
<EmailSettings email={props.data.user.email} />
|
||||
<UsernameSettings username={props.data.user.name} />{' '}
|
||||
</>
|
||||
)}
|
||||
{tab === 'Notifications' && (
|
||||
<>
|
||||
<EmailAlertSettings />
|
||||
<AppriseAlertSettings />
|
||||
</>
|
||||
)}
|
||||
{tab === 'Integrations' && (
|
||||
<>
|
||||
<Integrations />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
align-content: baseline;
|
||||
}
|
||||
|
||||
.tokenCardBody p {
|
||||
.tokenInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
|
@ -387,7 +387,7 @@
|
|||
.headerFormAppriseUrls {
|
||||
font-weight: 500;
|
||||
color: #494b7a;
|
||||
margin: 40px 0px 10px 0px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
|
|
|||
99
Containers/UserSettings/UserSettings.tsx
Normal file
99
Containers/UserSettings/UserSettings.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from './UserSettings.module.css';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Session } from 'next-auth';
|
||||
import { Optional, WizardEnvType, SessionStatus } from '~/types';
|
||||
|
||||
// Components
|
||||
import EmailSettings from './EmailSettings/EmailSettings';
|
||||
import PasswordSettings from './PasswordSettings/PasswordSettings';
|
||||
import UsernameSettings from './UsernameSettings/UsernameSettings';
|
||||
import EmailAlertSettings from './EmailAlertSettings/EmailAlertSettings';
|
||||
import AppriseAlertSettings from './AppriseAlertSettings/AppriseAlertSettings';
|
||||
import Integrations from './Integrations/Integrations';
|
||||
|
||||
type UserSettingsProps = {
|
||||
status: SessionStatus;
|
||||
data: Session;
|
||||
};
|
||||
|
||||
export default function UserSettings({ data }: UserSettingsProps) {
|
||||
const [tab, setTab] = useState<'General' | 'Notifications' | 'Integrations'>('General');
|
||||
const [wizardEnv, setWizardEnv] = useState<Optional<WizardEnvType>>(undefined);
|
||||
|
||||
// Fetch wizard environment on mount
|
||||
useEffect(() => {
|
||||
const fetchWizardEnv = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/account/wizard-env');
|
||||
const data: WizardEnvType = await response.json();
|
||||
setWizardEnv(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch wizard environment:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchWizardEnv();
|
||||
}, []);
|
||||
|
||||
// If Integrations tab is selected but disabled, fallback to General
|
||||
useEffect(() => {
|
||||
if (tab === 'Integrations' && wizardEnv?.DISABLE_INTEGRATIONS === 'true') {
|
||||
setTab('General');
|
||||
}
|
||||
}, [wizardEnv, tab]);
|
||||
|
||||
return (
|
||||
<div className={classes.containerSettings}>
|
||||
<h1 style={{ color: '#494b7a', textAlign: 'left', marginLeft: '30px' }}>Account</h1>
|
||||
|
||||
{wizardEnv != undefined && (
|
||||
<>
|
||||
<div className={classes.tabList}>
|
||||
<button
|
||||
className={tab === 'General' ? classes.tabListButtonActive : classes.tabListButton}
|
||||
onClick={() => setTab('General')}
|
||||
>
|
||||
General
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
tab === 'Notifications' ? classes.tabListButtonActive : classes.tabListButton
|
||||
}
|
||||
onClick={() => setTab('Notifications')}
|
||||
>
|
||||
Notifications
|
||||
</button>
|
||||
{wizardEnv.DISABLE_INTEGRATIONS !== 'true' && (
|
||||
<button
|
||||
className={
|
||||
tab === 'Integrations' ? classes.tabListButtonActive : classes.tabListButton
|
||||
}
|
||||
onClick={() => setTab('Integrations')}
|
||||
>
|
||||
Integrations
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tab === 'General' && (
|
||||
<>
|
||||
<PasswordSettings />
|
||||
<EmailSettings email={data.user?.email ?? undefined} />
|
||||
<UsernameSettings username={data.user?.name ?? undefined} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'Notifications' && (
|
||||
<>
|
||||
<EmailAlertSettings />
|
||||
<AppriseAlertSettings />
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'Integrations' && wizardEnv.DISABLE_INTEGRATIONS !== 'true' && <Integrations />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
//Lib
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast, ToastOptions } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
import { useFormStatus } from '~/hooks';
|
||||
import { UsernameSettingDTO } from '~/types';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Info from '../../../Components/UI/Info/Info';
|
||||
import Error from '~/Components/UI/Error/Error';
|
||||
import Info from '~/Components/UI/Info/Info';
|
||||
|
||||
export default function UsernameSettings(props) {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
export default function UsernameSettings(props: UsernameSettingDTO) {
|
||||
const toastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 8000,
|
||||
hideProgressBar: false,
|
||||
|
|
@ -27,23 +27,20 @@ export default function UsernameSettings(props) {
|
|||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({ mode: 'onChange' });
|
||||
} = useForm<UsernameSettingDTO>({ mode: 'onChange' });
|
||||
|
||||
const { isLoading, error, setIsLoading, handleError, clearError } = useFormStatus();
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////Functions
|
||||
//Form submit Handler for ADD a repo
|
||||
const formSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
const formSubmitHandler = async (data: UsernameSettingDTO) => {
|
||||
clearError();
|
||||
setIsLoading(true);
|
||||
//POST API to update the username
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/account/updateUsername', {
|
||||
const response = await fetch('/api/v1/account/username', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
|
|
@ -55,8 +52,7 @@ export default function UsernameSettings(props) {
|
|||
if (!response.ok) {
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
handleError(result.message);
|
||||
} else {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
|
|
@ -66,8 +62,7 @@ export default function UsernameSettings(props) {
|
|||
} catch (error) {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
setError("Can't update your username. Contact your administrator.");
|
||||
setTimeout(() => setError(), 4000);
|
||||
handleError('Failed to update username. Please try again.');
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
|
@ -84,7 +79,7 @@ export default function UsernameSettings(props) {
|
|||
//at the time this code is written to refresh client-side session information
|
||||
//without triggering a logout.
|
||||
//I chose to inform the user to reconnect rather than force logout.
|
||||
<Info message='Please, logout to update your session.' />
|
||||
<Info message='Please, logout to update your session' />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit(formSubmitHandler)}
|
||||
|
|
@ -99,7 +94,7 @@ export default function UsernameSettings(props) {
|
|||
required: 'A username is required.',
|
||||
pattern: {
|
||||
value: /^[a-z]{5,15}$/,
|
||||
message: 'Only a-z characters are allowed.',
|
||||
message: 'Only a-z characters are allowed',
|
||||
},
|
||||
maxLength: {
|
||||
value: 10,
|
||||
|
|
@ -20,7 +20,7 @@ COPY --from=deps /app/node_modules ./node_modules
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN sed -i "s/images:/output: 'standalone',images:/" next.config.js
|
||||
RUN sed -i "s/images:/output: 'standalone',images:/" next.config.ts
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
|
|
|||
12
helpers/functions/__mocks__/apiResponse.ts
Normal file
12
helpers/functions/__mocks__/apiResponse.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const ApiResponse = {
|
||||
success: vi.fn(),
|
||||
badRequest: vi.fn(),
|
||||
unauthorized: vi.fn(),
|
||||
forbidden: vi.fn(),
|
||||
notFound: vi.fn(),
|
||||
methodNotAllowed: vi.fn(),
|
||||
validationError: vi.fn(),
|
||||
serverError: vi.fn(),
|
||||
};
|
||||
|
||||
export default ApiResponse;
|
||||
71
helpers/functions/apiResponse.ts
Normal file
71
helpers/functions/apiResponse.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { NextApiResponse } from 'next';
|
||||
|
||||
const getErrorMessage = (error: unknown): any => {
|
||||
if (error instanceof Error) {
|
||||
const shellError = error as any;
|
||||
|
||||
// Handle shell errors
|
||||
if ('code' in shellError || 'stderr' in shellError || 'stdout' in shellError) {
|
||||
return {
|
||||
code: shellError.code ?? null,
|
||||
cmd: shellError.cmd ?? null,
|
||||
stderr: shellError.stderr ?? null,
|
||||
stdout: shellError.stdout ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (typeof error === 'object' && error !== null && 'code' in error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === 'ENOENT') {
|
||||
return 'No such file or directory';
|
||||
}
|
||||
}
|
||||
|
||||
return 'API error, contact the administrator';
|
||||
};
|
||||
|
||||
export default class ApiResponse {
|
||||
static success<T>(res: NextApiResponse, message = 'Success', data?: T) {
|
||||
res.status(200).json({ status: 200, message, data });
|
||||
}
|
||||
|
||||
static badRequest(res: NextApiResponse, message = 'Bad Request') {
|
||||
res.status(400).json({ status: 400, message });
|
||||
}
|
||||
|
||||
static unauthorized(res: NextApiResponse, message = 'Unauthorized') {
|
||||
res.status(401).json({ status: 401, message });
|
||||
}
|
||||
|
||||
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 methodNotAllowed(res: NextApiResponse, message = 'Method Not Allowed') {
|
||||
res.status(405).json({ status: 405, message });
|
||||
}
|
||||
|
||||
static validationError(res: NextApiResponse, message = 'Validation Error') {
|
||||
res.status(422).json({ status: 422, message });
|
||||
}
|
||||
|
||||
static conflict(res: NextApiResponse, message = 'Conflict') {
|
||||
res.status(409).json({ status: 409, message });
|
||||
}
|
||||
|
||||
static serverError(
|
||||
res: NextApiResponse,
|
||||
error: unknown,
|
||||
fallbackMessage = 'API error, contact the administrator'
|
||||
) {
|
||||
const message = getErrorMessage(error) || fallbackMessage;
|
||||
res.status(500).json({ status: 500, message });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// This function is used to hash user passwords and to verify them with the bcryptjs library
|
||||
//Lib
|
||||
import { hash, compare } from 'bcryptjs';
|
||||
|
||||
export async function hashPassword(password) {
|
||||
return await hash(password, 12);
|
||||
}
|
||||
|
||||
export async function verifyPassword(password, hashedPassword) {
|
||||
return await compare(password, hashedPassword);
|
||||
}
|
||||
4
helpers/functions/index.ts
Normal file
4
helpers/functions/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import lanCommandOption from './lanCommandOption';
|
||||
import isSshPubKeyDuplicate from './isSshPubKeyDuplicate';
|
||||
|
||||
export { lanCommandOption, isSshPubKeyDuplicate };
|
||||
70
helpers/functions/isSshPubKeyDuplicate.test.ts
Normal file
70
helpers/functions/isSshPubKeyDuplicate.test.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import isSshPubKeyDuplicate from './isSshPubKeyDuplicate';
|
||||
import { Optional, Repository } from '~/types';
|
||||
|
||||
describe('isSshPubKeyDuplicate', () => {
|
||||
it('should return true if the SSH public key is duplicated', () => {
|
||||
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
|
||||
const repoList: Array<Optional<Repository>> = [
|
||||
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
|
||||
];
|
||||
|
||||
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the SSH public key is not duplicated', () => {
|
||||
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAdifferentkey user@hostname';
|
||||
const repoList: Array<Optional<Repository>> = [
|
||||
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
|
||||
];
|
||||
|
||||
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw an error if pubKey is missing', () => {
|
||||
const repoList: Array<Optional<Repository>> = [
|
||||
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
|
||||
];
|
||||
|
||||
expect(() => isSshPubKeyDuplicate('', repoList)).toThrow(
|
||||
'Missing or invalid parameters for duplicate SSH public key check.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if repoList is missing', () => {
|
||||
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
|
||||
|
||||
expect(() => isSshPubKeyDuplicate(pubKey, null as any)).toThrow(
|
||||
'Missing or invalid parameters for duplicate SSH public key check.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false if repoList is empty', () => {
|
||||
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
|
||||
const repoList: Array<Optional<Repository>> = [];
|
||||
|
||||
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle repositories with undefined sshPublicKey', () => {
|
||||
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
|
||||
const repoList: Array<Optional<Repository>> = [
|
||||
// @ts-expect-error
|
||||
{ sshPublicKey: undefined } as Repository,
|
||||
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
|
||||
];
|
||||
|
||||
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle repositories with null sshPublicKey', () => {
|
||||
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
|
||||
const repoList: Array<Optional<Repository>> = [
|
||||
// @ts-expect-error
|
||||
{ sshPublicKey: null } as Repository,
|
||||
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAdifferentkey other@host' } as Repository,
|
||||
];
|
||||
|
||||
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { Optional, Repository } from '~/types';
|
||||
|
||||
/**
|
||||
* Checks if the given SSH public key is duplicated in the provided repository list by removing the comment part.
|
||||
*
|
||||
|
|
@ -6,7 +8,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>>
|
||||
): boolean {
|
||||
if (!pubKey || !repoList || !Array.isArray(repoList)) {
|
||||
throw new Error('Missing or invalid parameters for duplicate SSH public key check.');
|
||||
}
|
||||
|
|
@ -16,7 +21,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;
|
||||
});
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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.HIDE_SSH_PORT === 'true' ? '' : ':' + wizardEnv.SSH_SERVER_PORT_LAN;
|
||||
} else {
|
||||
FQDN = wizardEnv.FQDN;
|
||||
SSH_SERVER_PORT = wizardEnv.HIDE_SSH_PORT === 'true' ? '' : ':' + wizardEnv.SSH_SERVER_PORT;
|
||||
}
|
||||
|
||||
return { FQDN, SSH_SERVER_PORT };
|
||||
}
|
||||
75
helpers/functions/lanCommandOption.test.ts
Normal file
75
helpers/functions/lanCommandOption.test.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import lanCommandOption from './lanCommandOption';
|
||||
import { WizardEnvType } from '~/types';
|
||||
|
||||
describe('lanCommandOption', () => {
|
||||
it('should return undefined values when wizardEnv is not provided', () => {
|
||||
const result = lanCommandOption();
|
||||
expect(result).toEqual({ FQDN: undefined, SSH_SERVER_PORT: undefined });
|
||||
});
|
||||
|
||||
it('should return FQDN and SSH_SERVER_PORT from wizardEnv when lanCommand is false', () => {
|
||||
const wizardEnv: Partial<WizardEnvType> = {
|
||||
FQDN: 'example.com',
|
||||
FQDN_LAN: 'lan.example.com',
|
||||
SSH_SERVER_PORT: '22',
|
||||
SSH_SERVER_PORT_LAN: '2222',
|
||||
HIDE_SSH_PORT: 'false',
|
||||
};
|
||||
|
||||
const result = lanCommandOption(wizardEnv, false);
|
||||
expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: ':22' });
|
||||
});
|
||||
|
||||
it('should return FQDN_LAN and SSH_SERVER_PORT_LAN from wizardEnv when lanCommand is true', () => {
|
||||
const wizardEnv: Partial<WizardEnvType> = {
|
||||
FQDN: 'example.com',
|
||||
FQDN_LAN: 'lan.example.com',
|
||||
SSH_SERVER_PORT: '22',
|
||||
SSH_SERVER_PORT_LAN: '2222',
|
||||
HIDE_SSH_PORT: 'false',
|
||||
};
|
||||
|
||||
const result = lanCommandOption(wizardEnv, true);
|
||||
expect(result).toEqual({ FQDN: 'lan.example.com', SSH_SERVER_PORT: ':2222' });
|
||||
});
|
||||
|
||||
it('should return undefined for SSH_SERVER_PORT when HIDE_SSH_PORT is true', () => {
|
||||
const wizardEnv: Partial<WizardEnvType> = {
|
||||
FQDN: 'example.com',
|
||||
FQDN_LAN: 'lan.example.com',
|
||||
SSH_SERVER_PORT: '22',
|
||||
SSH_SERVER_PORT_LAN: '2222',
|
||||
HIDE_SSH_PORT: 'true',
|
||||
};
|
||||
|
||||
const result = lanCommandOption(wizardEnv, false);
|
||||
expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: undefined });
|
||||
});
|
||||
|
||||
it('should fallback to FQDN and should leave ssh server port to undefined for some usages', () => {
|
||||
const wizardEnv: Partial<WizardEnvType> = {
|
||||
FQDN: 'example.com',
|
||||
FQDN_LAN: undefined,
|
||||
SSH_SERVER_PORT: '22',
|
||||
SSH_SERVER_PORT_LAN: undefined,
|
||||
HIDE_SSH_PORT: 'false',
|
||||
};
|
||||
|
||||
const result = lanCommandOption(wizardEnv, true);
|
||||
expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: undefined });
|
||||
});
|
||||
|
||||
it('should handle missing FQDN and SSH_SERVER_PORT gracefully', () => {
|
||||
const wizardEnv: Partial<WizardEnvType> = {
|
||||
FQDN: undefined,
|
||||
FQDN_LAN: 'lan.example.com',
|
||||
SSH_SERVER_PORT: undefined,
|
||||
SSH_SERVER_PORT_LAN: '2222',
|
||||
HIDE_SSH_PORT: 'false',
|
||||
};
|
||||
|
||||
const result = lanCommandOption(wizardEnv, false);
|
||||
expect(result).toEqual({ FQDN: undefined, SSH_SERVER_PORT: undefined });
|
||||
});
|
||||
});
|
||||
24
helpers/functions/lanCommandOption.ts
Normal file
24
helpers/functions/lanCommandOption.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Optional, WizardEnvType } from '~/types';
|
||||
|
||||
export default function lanCommandOption(
|
||||
wizardEnv?: Partial<WizardEnvType>,
|
||||
lanCommand?: boolean
|
||||
): { FQDN: Optional<string>; SSH_SERVER_PORT: Optional<string> } {
|
||||
if (!wizardEnv) {
|
||||
return { FQDN: undefined, SSH_SERVER_PORT: undefined };
|
||||
}
|
||||
|
||||
const { FQDN, FQDN_LAN, SSH_SERVER_PORT, SSH_SERVER_PORT_LAN, HIDE_SSH_PORT } = wizardEnv;
|
||||
|
||||
const isPortHidden = HIDE_SSH_PORT === 'true';
|
||||
|
||||
const selectedFQDN = lanCommand && FQDN_LAN ? FQDN_LAN : FQDN;
|
||||
const selectedPort = lanCommand ? SSH_SERVER_PORT_LAN : SSH_SERVER_PORT;
|
||||
|
||||
const formattedPort = !isPortHidden && selectedPort ? `:${selectedPort}` : undefined;
|
||||
|
||||
return {
|
||||
FQDN: selectedFQDN,
|
||||
SSH_SERVER_PORT: formattedPort,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import nodemailer from 'nodemailer';
|
||||
|
||||
export default function nodemailerSMTP() {
|
||||
const config = {
|
||||
port: parseInt(process.env.MAIL_SMTP_PORT, 10),
|
||||
host: process.env.MAIL_SMTP_HOST,
|
||||
tls: {
|
||||
// false value allow self-signed or invalid TLS certificate
|
||||
rejectUnauthorized: process.env.MAIL_REJECT_SELFSIGNED_TLS === 'false' ? false : true,
|
||||
},
|
||||
};
|
||||
|
||||
const smtpLogin = process.env.MAIL_SMTP_LOGIN || '';
|
||||
const smtpPwd = process.env.MAIL_SMTP_PWD || '';
|
||||
|
||||
// Some SMTP servers doesn't require authentication #364
|
||||
if (smtpLogin) {
|
||||
config.auth = {
|
||||
user: smtpLogin,
|
||||
pass: smtpPwd,
|
||||
};
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(config);
|
||||
return transporter;
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
52
helpers/functions/repositoryNameCheck.test.ts
Normal file
52
helpers/functions/repositoryNameCheck.test.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import repositoryNameCheck from './repositoryNameCheck';
|
||||
|
||||
describe('repositoryNameCheck', () => {
|
||||
it('should return true for a valid 8-character hexadecimal string', () => {
|
||||
expect(repositoryNameCheck('a1b2c3d4')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a string shorter than 8 characters', () => {
|
||||
expect(repositoryNameCheck('a1b2c3')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a string longer than 8 characters', () => {
|
||||
expect(repositoryNameCheck('a1b2c3d4e5')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a string with non-hexadecimal characters', () => {
|
||||
expect(repositoryNameCheck('a1b2c3g4')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an empty string', () => {
|
||||
expect(repositoryNameCheck('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a string with special characters', () => {
|
||||
expect(repositoryNameCheck('a1b2c3d@')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a string with uppercase hexadecimal characters', () => {
|
||||
expect(repositoryNameCheck('A1B2C3D4')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a string with spaces', () => {
|
||||
expect(repositoryNameCheck('a1b2 c3d4')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a non string name', () => {
|
||||
expect(repositoryNameCheck(12345678)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null', () => {
|
||||
expect(repositoryNameCheck(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined', () => {
|
||||
expect(repositoryNameCheck(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for boolean', () => {
|
||||
expect(repositoryNameCheck(true)).toBe(false);
|
||||
});
|
||||
});
|
||||
9
helpers/functions/repositoryNameCheck.ts
Normal file
9
helpers/functions/repositoryNameCheck.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// BorgWarehouse repository name is an 8-character hexadecimal string
|
||||
|
||||
export default function repositoryNameCheck(name: unknown): boolean {
|
||||
if (typeof name !== 'string') {
|
||||
return false;
|
||||
}
|
||||
const repositoryNameRegex = /^[a-f0-9]{8}$/;
|
||||
return repositoryNameRegex.test(name) ? true : false;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
// This function is used to parse the date and time into a human readable format from the timestamp.
|
||||
export default function timestampConverter(UNIX_timestamp) {
|
||||
const a = new Date(UNIX_timestamp * 1000);
|
||||
const year = a.getFullYear();
|
||||
const month = a.getMonth() + 1;
|
||||
const date = a.getDate();
|
||||
const hour = a.getHours();
|
||||
const min = (a.getMinutes() < 10 ? '0' : '') + a.getMinutes();
|
||||
//const sec = a.getSeconds();
|
||||
const time = year + '/' + month + '/' + date + ' ' + hour + ':' + min;
|
||||
return time;
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export default async function tokenController(API_KEY, FROM_IP) {
|
||||
const jsonDirectory = path.join(process.cwd(), 'config');
|
||||
const timestamp = new Date().toISOString();
|
||||
try {
|
||||
if (process.env.DISABLE_INTEGRATIONS === 'true') {
|
||||
console.log(`API auth failed from : ${FROM_IP} [${timestamp}]`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
const users = JSON.parse(usersList);
|
||||
const user = users.find(
|
||||
(user) => Array.isArray(user.tokens) && user.tokens.some((token) => token.token === API_KEY)
|
||||
);
|
||||
if (user) {
|
||||
const token = user.tokens.find((token) => token.token === API_KEY);
|
||||
|
||||
if (token && token.permissions && typeof token.permissions === 'object') {
|
||||
console.log(
|
||||
`API auth success with the token '${token.name}' of user '${user.username}' from : ${FROM_IP} [${timestamp}]`
|
||||
);
|
||||
return token.permissions;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`API auth failed from : ${FROM_IP} [${timestamp}]`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
throw new Error('Error with tokenController');
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ authorized_keys="${home}/.ssh/authorized_keys"
|
|||
|
||||
# Check args
|
||||
if [ "$1" == "" ] || [ "$2" == "" ] || ! [[ "$2" =~ ^[0-9]+$ ]] || [ "$3" != "true" ] && [ "$3" != "false" ]; then
|
||||
echo -n "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]"
|
||||
echo -n "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -41,25 +41,25 @@ fi
|
|||
pattern='(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?'
|
||||
if [[ ! "$1" =~ $pattern ]]
|
||||
then
|
||||
echo -n "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)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
## Check if authorized_keys exists
|
||||
if [ ! -f "${authorized_keys}" ];then
|
||||
echo -n "${authorized_keys} must be present"
|
||||
echo -n "${authorized_keys} must be present" >&2
|
||||
exit 5
|
||||
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"
|
||||
echo -n "SSH pub key already present in authorized_keys" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Check if borgbackup is installed
|
||||
if ! [ -x "$(command -v borg)" ]; then
|
||||
echo -n "You must install borgbackup package."
|
||||
echo -n "You must install borgbackup package." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ authorized_keys="${home}/.ssh/authorized_keys"
|
|||
|
||||
# Check arg
|
||||
if [[ $# -ne 1 || $1 = "" ]]; then
|
||||
echo -n "You must provide a repositoryName in argument."
|
||||
echo -n "You must provide a repositoryName in argument." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ fi
|
|||
# If we receive another pattern there is necessarily a problem.
|
||||
repositoryName=$1
|
||||
if ! [[ "$repositoryName" =~ ^[a-f0-9]{8}$ ]]; then
|
||||
echo "Invalid repository name. Must be an 8-character hex string."
|
||||
echo "Invalid repository name. Must be an 8-character hex string." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ fi
|
|||
|
||||
# Check args
|
||||
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ] || [ "$4" != "true" ] && [ "$4" != "false" ]; then
|
||||
echo -n "This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [Append only mode [true|false]]"
|
||||
echo -n "This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [Append only mode [true|false]]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ fi
|
|||
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 -n "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)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
|
@ -34,13 +34,13 @@ fi
|
|||
# If we receive another pattern there is necessarily a problem.
|
||||
repositoryName=$1
|
||||
if ! [[ "$repositoryName" =~ ^[a-f0-9]{8}$ ]]; then
|
||||
echo "Invalid repository name. Must be an 8-character hex string."
|
||||
echo "Invalid repository name. Must be an 8-character hex string." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# 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"
|
||||
echo -n "No line containing $repositoryName found in authorized_keys" >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ while IFS= read -r line; do
|
|||
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."
|
||||
echo -n "This SSH pub key is already present in authorized_keys on a different line." >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export default function emailTest(mailTo, username, aliasList) {
|
||||
const aliasTemplate = (x) => {
|
||||
import path from 'path';
|
||||
|
||||
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>';
|
||||
|
|
@ -126,7 +128,7 @@ export default function emailTest(mailTo, username, aliasList) {
|
|||
`,
|
||||
attachments: [
|
||||
{
|
||||
path: 'helpers/templates/attachments/alert-icon.png',
|
||||
path: path.join(process.cwd(), 'helpers/templates/attachments/alert-icon.png'),
|
||||
cid: 'alert-icon',
|
||||
},
|
||||
],
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
export default function emailTest(mailTo, username) {
|
||||
import path from 'path';
|
||||
|
||||
export default function emailTest(mailTo: string, username: string) {
|
||||
const template = {
|
||||
from: 'BorgWarehouse' + '<' + process.env.MAIL_SMTP_FROM + '>',
|
||||
to: mailTo,
|
||||
|
|
@ -94,7 +96,7 @@ export default function emailTest(mailTo, username) {
|
|||
`,
|
||||
attachments: [
|
||||
{
|
||||
path: 'helpers/templates/attachments/valid-icon.png',
|
||||
path: path.join(process.cwd(), 'helpers/templates/attachments/valid-icon.png'),
|
||||
cid: 'valid-icon',
|
||||
},
|
||||
],
|
||||
1
hooks/index.ts
Normal file
1
hooks/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './useFormStatus';
|
||||
32
hooks/useFormStatus.ts
Normal file
32
hooks/useFormStatus.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useState } from 'react';
|
||||
import { Optional } from '~/types';
|
||||
|
||||
export function useFormStatus() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
const [error, setError] = useState<Optional<string>>(undefined);
|
||||
|
||||
const handleSuccess = () => {
|
||||
setIsLoading(false);
|
||||
setIsSaved(true);
|
||||
setTimeout(() => setIsSaved(false), 3000);
|
||||
};
|
||||
|
||||
const handleError = (message: string) => {
|
||||
setIsLoading(false);
|
||||
setError(message);
|
||||
setTimeout(() => setError(undefined), 4000);
|
||||
};
|
||||
|
||||
const clearError = () => setError(undefined);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isSaved,
|
||||
error,
|
||||
setIsLoading,
|
||||
handleSuccess,
|
||||
handleError,
|
||||
clearError,
|
||||
};
|
||||
}
|
||||
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
module.exports = {
|
||||
// nextConfig
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
|
|
@ -21,3 +20,5 @@ module.exports = {
|
|||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
14305
package-lock.json
generated
14305
package-lock.json
generated
File diff suppressed because it is too large
Load diff
84
package.json
84
package.json
|
|
@ -1,38 +1,50 @@
|
|||
{
|
||||
"name": "borgwarehouse",
|
||||
"version": "2.4.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"setup": "npm install && npm run setup:hooks",
|
||||
"setup:hooks": "npx husky install",
|
||||
"format": "prettier --write \"{Components,Containers,helpers,pages,styles}/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"chart.js": "^4.4.7",
|
||||
"next": "^15.2.5",
|
||||
"next-auth": "^4.24.10",
|
||||
"nodemailer": "^6.10.0",
|
||||
"react": "^18.3.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-select": "^5.10.1",
|
||||
"react-toastify": "^10.0.6",
|
||||
"spinners-react": "^1.0.10",
|
||||
"swr": "^2.3.3",
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.0",
|
||||
"@commitlint/config-conventional": "^19.8.0",
|
||||
"eslint-config-next": "^15.3.1",
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
"name": "borgwarehouse",
|
||||
"version": "2.4.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"setup": "npm install && npm run setup:hooks",
|
||||
"setup:hooks": "npx husky install",
|
||||
"format": "prettier --write \"{Components,Containers,helpers,pages,styles}/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"chart.js": "^4.4.7",
|
||||
"date-fns": "^4.1.0",
|
||||
"lowdb": "^7.0.1",
|
||||
"next": "^15.2.5",
|
||||
"next-auth": "^4.24.10",
|
||||
"nodemailer": "^6.10.0",
|
||||
"react": "^18.3.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-select": "^5.10.1",
|
||||
"react-toastify": "^10.0.6",
|
||||
"spinners-react": "^1.0.10",
|
||||
"swr": "^2.3.3",
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.0",
|
||||
"@commitlint/config-conventional": "^19.8.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint-config-next": "^15.3.1",
|
||||
"husky": "^9.1.7",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
//Lib
|
||||
import Head from 'next/head';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Error404() {
|
||||
//Var
|
||||
const { status } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
//Lib
|
||||
import '../styles/default.css';
|
||||
import Head from 'next/head';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import { AppProps } from 'next/app';
|
||||
|
||||
//Components
|
||||
import Layout from '../Components/UI/Layout/Layout';
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<SessionProvider session={pageProps.session}>
|
||||
<Layout>
|
||||
|
|
@ -1,19 +1,18 @@
|
|||
//Lib
|
||||
import Head from 'next/head';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { authOptions } from '../../pages/api/auth/[...nextauth]';
|
||||
import { authOptions } from '~/pages/api/auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { SessionStatus } from '~/types';
|
||||
|
||||
//Components
|
||||
import UserSettings from '../../Containers/UserSettings/UserSettings';
|
||||
import UserSettings from '~/Containers/UserSettings/UserSettings';
|
||||
|
||||
export default function Account() {
|
||||
////Var
|
||||
const { status, data } = useSession();
|
||||
|
||||
//Function
|
||||
if (status == 'unauthenticated' || status == 'loading') {
|
||||
if (status == 'unauthenticated' || status == 'loading' || !data) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
return (
|
||||
|
|
@ -22,13 +21,12 @@ export default function Account() {
|
|||
<title>Account - BorgWarehouse</title>
|
||||
</Head>
|
||||
|
||||
<UserSettings status={status} data={data} />
|
||||
<UserSettings status={status as SessionStatus} data={data} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
//Var
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context.req, context.res, authOptions);
|
||||
|
||||
if (!session) {
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
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 {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the appriseAlert bool
|
||||
res.status(200).json({
|
||||
appriseAlert: usersList[userIndex].appriseAlert,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
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 {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the appriseMode object
|
||||
res.status(200).json({
|
||||
appriseMode: usersList[userIndex].appriseMode,
|
||||
appriseStatelessURL: usersList[userIndex].appriseStatelessURL,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
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 {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the appriseServices array
|
||||
res.status(200).json({
|
||||
appriseServices: usersList[userIndex].appriseServices,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
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 {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the emailAlert bool
|
||||
res.status(200).json({
|
||||
emailAlert: usersList[userIndex].emailAlert,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
//Lib
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
//AUTHENTICATION
|
||||
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'),
|
||||
HIDE_SSH_PORT: getEnvVariable('HIDE_SSH_PORT', 'false'),
|
||||
DISABLE_INTEGRATIONS: getEnvVariable('DISABLE_INTEGRATIONS', 'false'),
|
||||
};
|
||||
|
||||
res.status(200).json({ wizardEnv });
|
||||
return;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
//Lib
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
const { exec } = require('child_process');
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'POST') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { sendTestApprise } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : control the data
|
||||
if (sendTestApprise !== true) {
|
||||
res.status(422).json({ message: 'Unexpected data' });
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : if there is no service URLs, throw error
|
||||
if (
|
||||
!usersList[userIndex].appriseServices ||
|
||||
usersList[userIndex].appriseServices.length === 0
|
||||
) {
|
||||
res.status(422).json({
|
||||
message: 'You must provide at least one Apprise URL to send a test.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
////4 : Send the notification to services
|
||||
//Build the URLs service list as a single string
|
||||
let appriseServicesURLs = '';
|
||||
for (let service of usersList[userIndex].appriseServices) {
|
||||
appriseServicesURLs = appriseServicesURLs + service + ' ';
|
||||
}
|
||||
//Mode : package
|
||||
if (usersList[userIndex].appriseMode === 'package') {
|
||||
try {
|
||||
//Check if apprise is installed as local package.
|
||||
exec('apprise -V', (error, stderr, stdout) => {
|
||||
if (error) {
|
||||
console.log(`Error when checking if Apprise is a local package : ${error}`);
|
||||
res.status(500).json({
|
||||
message: 'Apprise is not installed as local package on your server.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send notification via local package.
|
||||
exec(
|
||||
`apprise -v -b "This is a test notification from BorgWarehouse !" ${appriseServicesURLs}`,
|
||||
(error, stderr, stdout) => {
|
||||
if (stderr) {
|
||||
res.status(500).json({
|
||||
message: 'There are some errors : ' + stderr,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
res.status(200).json({
|
||||
message: 'Notifications successfully sent.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({
|
||||
message: 'Error on sending notification. Contact your administrator.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Mode : stateless
|
||||
} else if (usersList[userIndex].appriseMode === 'stateless') {
|
||||
//If stateless URL is empty
|
||||
if (usersList[userIndex].appriseStatelessURL === '') {
|
||||
res.status(500).json({
|
||||
message: 'Please, provide an Apprise stateless API URL.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await fetch(usersList[userIndex].appriseStatelessURL + '/notify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
urls: appriseServicesURLs,
|
||||
body: 'This is a test notification from BorgWarehouse !',
|
||||
}),
|
||||
}).then((response) => {
|
||||
if (response.ok) {
|
||||
res.status(200).json({
|
||||
message: 'Notifications successfully sent.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
console.log(response);
|
||||
res.status(500).json({
|
||||
message: 'There are some errors : ' + response.statusText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
message: 'Error : ' + err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Mode : unknown
|
||||
} else {
|
||||
res.status(422).json({
|
||||
message: 'No Apprise Mode selected or supported.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import nodemailerSMTP from '../../../helpers/functions/nodemailerSMTP';
|
||||
import emailTest from '../../../helpers/templates/emailTest';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'POST') {
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: 'You must be logged in.' });
|
||||
}
|
||||
|
||||
//Create the SMTP Transporter
|
||||
const transporter = nodemailerSMTP();
|
||||
//Mail options
|
||||
const mailData = emailTest(session.user.email, session.user.name);
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailData);
|
||||
console.log(info);
|
||||
return res.status(200).json({
|
||||
message: 'Mail successfully sent',
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return res.status(400).json({
|
||||
message: 'An error occurred while sending the email: ' + err,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'POST') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { name, token, creation, expiration, permissions } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : We check that we receive data for each variable.
|
||||
if (!name || !token || !creation || !permissions) {
|
||||
res.status(400).json({ message: 'A field is missing.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//Control the data
|
||||
const nameRegex = new RegExp('^[a-zA-Z0-9_-]{1,25}$');
|
||||
if (!nameRegex.test(name)) {
|
||||
res.status(400).json({ message: 'Your token name is not valid' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({ message: 'User is incorrect.' });
|
||||
return;
|
||||
}
|
||||
const user = usersList[userIndex];
|
||||
|
||||
//3 : Check that the tokenName or tokenValue already exists
|
||||
const tokenExists =
|
||||
user.tokens && user.tokens.some((existingToken) => existingToken.name === name);
|
||||
if (tokenExists) {
|
||||
res.status(400).json({
|
||||
message: 'A token with this name already exists.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//4 : Add the new token
|
||||
try {
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? {
|
||||
...user,
|
||||
tokens: [
|
||||
...(user.tokens || []),
|
||||
{
|
||||
name,
|
||||
token,
|
||||
creation,
|
||||
expiration,
|
||||
permissions,
|
||||
},
|
||||
],
|
||||
}
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else 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 {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the token list without tokens
|
||||
res.status(200).json([
|
||||
...(usersList[userIndex].tokens && Array.isArray(usersList[userIndex].tokens)
|
||||
? usersList[userIndex].tokens.map((token) => ({
|
||||
name: token.name,
|
||||
creation: token.creation,
|
||||
expiration: token.expiration,
|
||||
permissions: token.permissions,
|
||||
}))
|
||||
: []),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (req.method == 'DELETE') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { name } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : We check that we receive data for each variable.
|
||||
if (!name) {
|
||||
res.status(400).json({ message: 'A field is missing.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({ message: 'User is incorrect.' });
|
||||
return;
|
||||
}
|
||||
const user = usersList[userIndex];
|
||||
|
||||
//Control the data
|
||||
const tokenExists = user.tokens.some((existingToken) => existingToken.name === name);
|
||||
if (!tokenExists) {
|
||||
res.status(400).json({ message: 'Token not found.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Delete the token object if it exists
|
||||
try {
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? {
|
||||
...user,
|
||||
tokens: user.tokens.filter((token) => token.name != name),
|
||||
}
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { appriseAlert } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : control the data
|
||||
if (typeof appriseAlert != 'boolean') {
|
||||
res.status(422).json({ message: 'Unexpected data' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Change the appriseAlert settings
|
||||
try {
|
||||
//Modify the appriseAlert bool for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name ? { ...user, appriseAlert: appriseAlert } : user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { appriseMode, appriseStatelessURL } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : control the data
|
||||
if (appriseMode != 'package' && appriseMode != 'stateless') {
|
||||
res.status(422).json({ message: 'Unexpected data' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Change the appriseMode
|
||||
try {
|
||||
//Modify the appriseMode for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? {
|
||||
...user,
|
||||
appriseMode: appriseMode,
|
||||
appriseStatelessURL: appriseStatelessURL,
|
||||
}
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { appriseURLs } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Update Apprise URLs list
|
||||
try {
|
||||
//Build the services URLs list from form
|
||||
const appriseURLsArray = appriseURLs
|
||||
.replace(/ /g, '')
|
||||
.split('\n')
|
||||
.filter((el) => el != '');
|
||||
|
||||
//Save the list for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? {
|
||||
...user,
|
||||
appriseServices: appriseURLsArray,
|
||||
}
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { email } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : We check that we receive data.
|
||||
if (!email) {
|
||||
//If a variable is empty.
|
||||
res.status(400).json({ message: 'A field is missing.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : control the data
|
||||
const emailRegex = new RegExp(
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
);
|
||||
if (!emailRegex.test(email)) {
|
||||
res.status(400).json({ message: 'Your email is not valid' });
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//4 : Change the email
|
||||
try {
|
||||
//Modify the email for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name ? { ...user, email: email } : user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { emailAlert } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : control the data
|
||||
if (typeof emailAlert != 'boolean') {
|
||||
res.status(422).json({ message: 'Unexpected data' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Change the emailAlert settings
|
||||
try {
|
||||
//Modify the email for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name ? { ...user, emailAlert: emailAlert } : user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
//Lib
|
||||
import { hashPassword, verifyPassword } from '../../../helpers/functions/auth';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { oldPassword, newPassword } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : We check that we receive data for each variable.
|
||||
if (!oldPassword || !newPassword) {
|
||||
//If a variable is empty.
|
||||
res.status(400).json({ message: 'A field is missing.' });
|
||||
return;
|
||||
}
|
||||
//Hash the new password
|
||||
newPassword = await hashPassword(newPassword);
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({ message: 'User is incorrect.' });
|
||||
return;
|
||||
}
|
||||
const user = usersList[userIndex];
|
||||
|
||||
//3 : Check that the old password is correct
|
||||
const isValid = await verifyPassword(oldPassword, user.password);
|
||||
if (!isValid) {
|
||||
res.status(400).json({ message: 'Old password is incorrect.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//4 : Change the password
|
||||
try {
|
||||
//Modify the password for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name ? { ...user, password: newPassword } : user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//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;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { username } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : We check that we receive data.
|
||||
if (!username) {
|
||||
//If a variable is empty.
|
||||
res.status(400).json({ message: 'A field is missing.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : control the data
|
||||
const usernameRegex = new RegExp(/^[a-z]{5,15}$/);
|
||||
if (!usernameRegex.test(username)) {
|
||||
res.status(400).json({
|
||||
message: 'Only a-z characters are allowed (5 to 15 char.)',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Verify that the user of the session exists
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//4 : Change the username
|
||||
try {
|
||||
//Modify the username for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name ? { ...user, username: username } : user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
//Lib
|
||||
import NextAuth from 'next-auth';
|
||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||
import { verifyPassword } from '../../../helpers/functions/auth';
|
||||
import fs from 'fs';
|
||||
import NextAuth, { NextAuthOptions, RequestInternal, User } from 'next-auth';
|
||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||
import path from 'path';
|
||||
import { ConfigService, AuthService } from '~/services';
|
||||
|
||||
const logLogin = async (message, req, success = false) => {
|
||||
const ipAddress = req.headers['x-forwarded-for'] || 'unknown';
|
||||
const logLogin = async (message: string, req: Partial<RequestInternal>, success = false) => {
|
||||
const ipAddress = req.headers?.['x-forwarded-for'] || 'unknown';
|
||||
const timestamp = new Date().toISOString();
|
||||
if (success) {
|
||||
console.log(`Login success from ${ipAddress} with user ${message} [${timestamp}]`);
|
||||
|
|
@ -15,11 +14,22 @@ const logLogin = async (message, req, success = false) => {
|
|||
}
|
||||
};
|
||||
|
||||
interface customUser extends User {
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
////Use if need getServerSideProps and therefore getServerSession
|
||||
export const authOptions = {
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
credentials: {
|
||||
username: { type: 'text' },
|
||||
password: { type: 'password' },
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
if (!credentials) {
|
||||
throw new Error('Missing credentials');
|
||||
}
|
||||
const { username, password } = credentials;
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
|
|
@ -41,34 +51,33 @@ export const authOptions = {
|
|||
])
|
||||
);
|
||||
}
|
||||
let usersList = await fs.promises.readFile(jsonDirectory + '/users.json', 'utf8');
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
const usersList = await ConfigService.getUsersList();
|
||||
|
||||
//Step 1 : does the user exist ?
|
||||
const userIndex = usersList.map((user) => user.username).indexOf(username.toLowerCase());
|
||||
if (userIndex === -1) {
|
||||
await logLogin(`Bad username ${req.body.username}`, req);
|
||||
await logLogin(`Bad username ${req.body?.username}`, req);
|
||||
throw new Error('Incorrect credentials.');
|
||||
}
|
||||
const user = usersList[userIndex];
|
||||
|
||||
//Step 2 : Is the password correct ?
|
||||
const isValid = await verifyPassword(password, user.password);
|
||||
const isValid = await AuthService.verifyPassword(password, user.password);
|
||||
if (!isValid) {
|
||||
await logLogin(`Wrong password for ${req.body.username}`, req);
|
||||
await logLogin(`Wrong password for ${req.body?.username}`, req);
|
||||
throw new Error('Incorrect credentials.');
|
||||
}
|
||||
|
||||
//Success
|
||||
const account = {
|
||||
const account: customUser = {
|
||||
name: user.username,
|
||||
email: user.email,
|
||||
id: user.id,
|
||||
id: user.id.toString(),
|
||||
roles: user.roles,
|
||||
};
|
||||
|
||||
await logLogin(req.body.username, req, true);
|
||||
await logLogin(req.body?.username, req, true);
|
||||
return account;
|
||||
},
|
||||
}),
|
||||
|
|
@ -84,9 +93,9 @@ export const authOptions = {
|
|||
},
|
||||
async session({ session, token }) {
|
||||
// Send properties to the client to access to the token info through session().
|
||||
if (token) {
|
||||
session.user.roles = token.roles;
|
||||
session.user.id = token.id;
|
||||
if (token && session.user) {
|
||||
session.user.roles = token.roles as string[];
|
||||
session.user.id = token.id as string;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +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/getStorageUsed' --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);
|
||||
|
||||
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];
|
||||
|
||||
try {
|
||||
if (req.method == 'POST' && ACTION_KEY === CRONJOB_KEY) {
|
||||
//Check the repoList
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let repoList = await fs.readFile(jsonDirectory + '/repo.json', 'utf8');
|
||||
//Parse the repoList
|
||||
repoList = JSON.parse(repoList);
|
||||
//If repoList is empty we stop here.
|
||||
if (repoList.length === 0) {
|
||||
res.status(200).json({
|
||||
success: 'No repositories to analyse yet.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
////Call the shell : getStorageUsed.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/getStorageUsed.sh`);
|
||||
if (stderr) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'Error on getting storage, contact the administrator.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
//Parse the JSON output of getStorageUsed.sh to use it
|
||||
const storageUsed = JSON.parse(stdout);
|
||||
|
||||
//Rebuild a newRepoList with the storageUsed value updated
|
||||
let newRepoList = repoList;
|
||||
for (let index in newRepoList) {
|
||||
const repoFiltered = storageUsed.filter(
|
||||
(x) => x.name === newRepoList[index].repositoryName
|
||||
);
|
||||
if (repoFiltered.length === 1) {
|
||||
newRepoList[index].storageUsed = repoFiltered[0].size;
|
||||
}
|
||||
}
|
||||
|
||||
//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);
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: 'Storage cron has been executed.',
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({
|
||||
status: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../pages/api/auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { alertOptions } from '../../../domain/constants';
|
||||
import repoHistory from '../../../helpers/functions/repoHistory';
|
||||
import tokenController from '../../../helpers/functions/tokenController';
|
||||
import isSshPubKeyDuplicate from '../../../helpers/functions/isSshPubKeyDuplicate';
|
||||
const util = require('node:util');
|
||||
const exec = util.promisify(require('node:child_process').exec);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ status: 405, message: 'Method Not Allowed' });
|
||||
}
|
||||
|
||||
// AUTHENTICATION
|
||||
const FROM_IP = req.headers['x-forwarded-for'] || 'unknown';
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!(await isAuthenticated(session, authorization, FROM_IP))) {
|
||||
res.status(401).json({ message: 'Invalid API key' });
|
||||
return;
|
||||
}
|
||||
|
||||
// DATA CONTROL
|
||||
const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = req.body;
|
||||
|
||||
if (!isValidRepoData(req.body)) {
|
||||
return res.status(422).json({ message: 'Unexpected data' });
|
||||
}
|
||||
|
||||
try {
|
||||
const repoList = await getRepoList();
|
||||
|
||||
if (isSshKeyConflict(sshPublicKey, repoList)) {
|
||||
return res.status(409).json({
|
||||
message:
|
||||
'The SSH key is already used in another repository. Please use another key or delete the key from the other repository.',
|
||||
});
|
||||
}
|
||||
|
||||
const newRepo = await createNewRepo(repoList, {
|
||||
alias,
|
||||
sshPublicKey,
|
||||
storageSize,
|
||||
comment,
|
||||
alert,
|
||||
lanCommand,
|
||||
appendOnlyMode,
|
||||
});
|
||||
const newRepoList = [newRepo, ...repoList];
|
||||
|
||||
await saveRepoList(newRepoList);
|
||||
|
||||
return res.status(200).json({ id: newRepo.id, repositoryName: newRepo.repositoryName });
|
||||
} catch (error) {
|
||||
handleError(error, res);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Functions
|
||||
// --------------
|
||||
async function isAuthenticated(session, authorization, FROM_IP) {
|
||||
if (session) return true;
|
||||
|
||||
if (authorization) {
|
||||
const API_KEY = authorization.split(' ')[1];
|
||||
const permissions = await tokenController(API_KEY, FROM_IP);
|
||||
return permissions?.create;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isValidRepoData(body) {
|
||||
const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = body;
|
||||
const expectedKeys = [
|
||||
'alias',
|
||||
'sshPublicKey',
|
||||
'storageSize',
|
||||
'comment',
|
||||
'alert',
|
||||
'lanCommand',
|
||||
'appendOnlyMode',
|
||||
];
|
||||
|
||||
const isValidData =
|
||||
typeof alias === 'string' &&
|
||||
alias.trim() !== '' &&
|
||||
typeof sshPublicKey === 'string' &&
|
||||
sshPublicKey.trim() !== '' &&
|
||||
typeof comment === 'string' &&
|
||||
typeof storageSize === 'number' &&
|
||||
Number.isInteger(storageSize) &&
|
||||
storageSize > 0 &&
|
||||
typeof alert === 'number' &&
|
||||
alertOptions.some((option) => option.value === alert) &&
|
||||
typeof lanCommand === 'boolean' &&
|
||||
typeof appendOnlyMode === 'boolean';
|
||||
|
||||
const hasUnexpectedKeys = Object.keys(body).some((key) => !expectedKeys.includes(key));
|
||||
|
||||
return isValidData && !hasUnexpectedKeys;
|
||||
}
|
||||
|
||||
async function getRepoList() {
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
const repoData = await fs.readFile(jsonDirectory + '/repo.json', 'utf8');
|
||||
return JSON.parse(repoData);
|
||||
}
|
||||
|
||||
function isSshKeyConflict(sshPublicKey, repoList) {
|
||||
return typeof sshPublicKey === 'string' && isSshPubKeyDuplicate(sshPublicKey, repoList);
|
||||
}
|
||||
|
||||
async function createNewRepo(
|
||||
repoList,
|
||||
{ alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode }
|
||||
) {
|
||||
const newID = repoList.length > 0 ? Math.max(...repoList.map((repo) => repo.id)) + 1 : 0;
|
||||
|
||||
const newRepo = {
|
||||
id: newID,
|
||||
alias,
|
||||
repositoryName: '',
|
||||
status: false,
|
||||
lastSave: 0,
|
||||
lastStatusAlertSend: Math.floor(Date.now() / 1000),
|
||||
alert,
|
||||
storageSize: Number(storageSize),
|
||||
storageUsed: 0,
|
||||
sshPublicKey,
|
||||
comment,
|
||||
displayDetails: true,
|
||||
lanCommand,
|
||||
appendOnlyMode,
|
||||
};
|
||||
|
||||
const shellsDirectory = path.join(process.cwd(), '/helpers');
|
||||
const { stdout } = await exec(
|
||||
`${shellsDirectory}/shells/createRepo.sh "${newRepo.sshPublicKey}" ${newRepo.storageSize} ${newRepo.appendOnlyMode}`
|
||||
);
|
||||
|
||||
newRepo.repositoryName = stdout.trim();
|
||||
return newRepo;
|
||||
}
|
||||
|
||||
async function saveRepoList(newRepoList) {
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
await repoHistory(newRepoList);
|
||||
await fs.writeFile(jsonDirectory + '/repo.json', JSON.stringify(newRepoList));
|
||||
}
|
||||
|
||||
function handleError(error, res) {
|
||||
console.log(error);
|
||||
if (error.code === 'ENOENT') {
|
||||
res.status(500).json({ message: 'No such file or directory' });
|
||||
} else {
|
||||
res.status(500).json({ message: error.stdout });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import repoHistory from '../../../../../helpers/functions/repoHistory';
|
||||
import tokenController from '../../../../../helpers/functions/tokenController';
|
||||
const util = require('node:util');
|
||||
const exec = util.promisify(require('node:child_process').exec);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'DELETE') {
|
||||
//AUTHENTICATION
|
||||
const FROM_IP = req.headers['x-forwarded-for'] || 'unknown';
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!session && !authorization) {
|
||||
res.status(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!session && authorization) {
|
||||
const API_KEY = authorization.split(' ')[1];
|
||||
const permissions = await tokenController(API_KEY, FROM_IP);
|
||||
if (!permissions) {
|
||||
res.status(401).json({ message: 'Invalid API key' });
|
||||
return;
|
||||
}
|
||||
if (!permissions.delete) {
|
||||
res.status(403).json({ message: 'Insufficient permissions' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: 'Internal Server Error' });
|
||||
return;
|
||||
}
|
||||
|
||||
//If deletion is disabled on the server, return error
|
||||
if (process.env.DISABLE_DELETE_REPO === 'true') {
|
||||
res.status(403).json({
|
||||
message: 'Deletion is disabled on this server',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//console.log('API call (DELETE)');
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let repoList = await fs.readFile(jsonDirectory + '/repo.json', 'utf8');
|
||||
//Parse the repoList
|
||||
repoList = JSON.parse(repoList);
|
||||
|
||||
//Find the ID in the repoList and delete the repo.
|
||||
//NOTE : req.query.slug return a string, so parseInt to use with indexOf.
|
||||
const indexToDelete = repoList.map((repo) => repo.id).indexOf(parseInt(req.query.slug));
|
||||
|
||||
////Call the shell : deleteRepo.sh
|
||||
//Find the absolute path of the shells directory
|
||||
const shellsDirectory = path.join(process.cwd(), '/helpers');
|
||||
//Exec the shell
|
||||
const { stdout, stderr } = await exec(
|
||||
`${shellsDirectory}/shells/deleteRepo.sh ${repoList[indexToDelete].repositoryName}`
|
||||
);
|
||||
if (stderr) {
|
||||
console.log('stderr:', stderr);
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'Error on delete, contact the administrator.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Delete the repo in the repoList
|
||||
if (indexToDelete !== -1) {
|
||||
repoList.splice(indexToDelete, 1);
|
||||
} else {
|
||||
console.log('The index to delete does not exist');
|
||||
res.status(400).json({
|
||||
message: 'This repository does not exist',
|
||||
});
|
||||
return;
|
||||
}
|
||||
//History the repoList
|
||||
await repoHistory(repoList);
|
||||
//Stringify the repoList to write it into the json file.
|
||||
repoList = JSON.stringify(repoList);
|
||||
//Write the new json
|
||||
await fs.writeFile(jsonDirectory + '/repo.json', repoList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
|
||||
res.status(200).json({ message: 'success' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({
|
||||
message: 'Method Not Allowed ',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { alertOptions } from '../../../../../domain/constants';
|
||||
import repoHistory from '../../../../../helpers/functions/repoHistory';
|
||||
import tokenController from '../../../../../helpers/functions/tokenController';
|
||||
import isSshPubKeyDuplicate from '../../../../../helpers/functions/isSshPubKeyDuplicate';
|
||||
const util = require('node:util');
|
||||
const exec = util.promisify(require('node:child_process').exec);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'PATCH') {
|
||||
return res.status(405).json({ status: 405, message: 'Method Not Allowed' });
|
||||
}
|
||||
|
||||
// AUTHENTICATION
|
||||
const FROM_IP = req.headers['x-forwarded-for'] || 'unknown';
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!(await isAuthenticated(session, authorization, FROM_IP))) {
|
||||
res.status(401).json({ message: 'Invalid API key' });
|
||||
return;
|
||||
}
|
||||
|
||||
// DATA CONTROL
|
||||
const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = req.body;
|
||||
|
||||
if (!isValidPatchData(req.body)) {
|
||||
return res.status(422).json({ message: 'Unexpected data' });
|
||||
}
|
||||
|
||||
try {
|
||||
const repoList = await getRepoList();
|
||||
const repoId = parseInt(req.query.slug);
|
||||
const filteredRepoList = repoList.filter((repo) => repo.id !== repoId);
|
||||
|
||||
if (isSshKeyConflict(sshPublicKey, filteredRepoList)) {
|
||||
return res.status(409).json({
|
||||
message:
|
||||
'The SSH key is already used in another repository. Please use another key or delete the key from the other repository.',
|
||||
});
|
||||
}
|
||||
|
||||
const repoIndex = repoList.findIndex((repo) => repo.id === repoId);
|
||||
await updateRepoShell(repoList[repoIndex], sshPublicKey, storageSize, appendOnlyMode);
|
||||
const newRepoList = updateRepoList(repoList, req.query.slug, {
|
||||
alias,
|
||||
sshPublicKey,
|
||||
storageSize,
|
||||
comment,
|
||||
alert,
|
||||
lanCommand,
|
||||
appendOnlyMode,
|
||||
});
|
||||
|
||||
await saveRepoList(newRepoList);
|
||||
return res.status(200).json({ message: 'success' });
|
||||
} catch (error) {
|
||||
handleError(error, res);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Functions
|
||||
// --------------
|
||||
async function isAuthenticated(session, authorization, FROM_IP) {
|
||||
if (session) return true;
|
||||
|
||||
if (authorization) {
|
||||
const API_KEY = authorization.split(' ')[1];
|
||||
const permissions = await tokenController(API_KEY, FROM_IP);
|
||||
return permissions?.update;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isValidPatchData(body) {
|
||||
const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = body;
|
||||
const expectedKeys = [
|
||||
'alias',
|
||||
'sshPublicKey',
|
||||
'storageSize',
|
||||
'comment',
|
||||
'alert',
|
||||
'lanCommand',
|
||||
'appendOnlyMode',
|
||||
];
|
||||
const hasAtLeastOneKey = expectedKeys.some((key) => body.hasOwnProperty(key));
|
||||
const hasUnexpectedKeys = Object.keys(body).some((key) => !expectedKeys.includes(key));
|
||||
|
||||
return (
|
||||
hasAtLeastOneKey &&
|
||||
!hasUnexpectedKeys &&
|
||||
(typeof alias === 'undefined' || (typeof alias === 'string' && alias.trim() !== '')) &&
|
||||
(typeof sshPublicKey === 'undefined' ||
|
||||
(typeof sshPublicKey === 'string' && sshPublicKey.trim() !== '')) &&
|
||||
(typeof comment === 'undefined' || typeof comment === 'string') &&
|
||||
(typeof storageSize === 'undefined' ||
|
||||
(typeof storageSize === 'number' && Number.isInteger(storageSize) && storageSize > 0)) &&
|
||||
(typeof alert === 'undefined' ||
|
||||
(typeof alert === 'number' && alertOptions.some((option) => option.value === alert))) &&
|
||||
(typeof lanCommand === 'undefined' || typeof lanCommand === 'boolean') &&
|
||||
(typeof appendOnlyMode === 'undefined' || typeof appendOnlyMode === 'boolean')
|
||||
);
|
||||
}
|
||||
|
||||
async function getRepoList() {
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
const repoData = await fs.readFile(jsonDirectory + '/repo.json', 'utf8');
|
||||
return JSON.parse(repoData);
|
||||
}
|
||||
|
||||
function isSshKeyConflict(sshPublicKey, repoList) {
|
||||
return typeof sshPublicKey === 'string' && isSshPubKeyDuplicate(sshPublicKey, repoList);
|
||||
}
|
||||
|
||||
async function updateRepoShell(repo, sshPublicKey, storageSize, appendOnlyMode) {
|
||||
const shellsDirectory = path.join(process.cwd(), '/helpers');
|
||||
await exec(
|
||||
`${shellsDirectory}/shells/updateRepo.sh ${repo.repositoryName} "${sshPublicKey ?? repo.sshPublicKey}" ${storageSize ?? repo.storageSize} ${appendOnlyMode ?? repo.appendOnlyMode}`
|
||||
);
|
||||
}
|
||||
|
||||
function updateRepoList(repoList, slug, updates) {
|
||||
return repoList.map((repo) =>
|
||||
repo.id == slug
|
||||
? {
|
||||
...repo,
|
||||
alias: updates.alias ?? repo.alias,
|
||||
sshPublicKey: updates.sshPublicKey ?? repo.sshPublicKey,
|
||||
storageSize:
|
||||
updates.storageSize !== undefined ? Number(updates.storageSize) : repo.storageSize,
|
||||
comment: updates.comment ?? repo.comment,
|
||||
alert: updates.alert ?? repo.alert,
|
||||
lanCommand: updates.lanCommand ?? repo.lanCommand,
|
||||
appendOnlyMode: updates.appendOnlyMode ?? repo.appendOnlyMode,
|
||||
}
|
||||
: repo
|
||||
);
|
||||
}
|
||||
|
||||
async function saveRepoList(newRepoList) {
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
await repoHistory(newRepoList);
|
||||
await fs.writeFile(jsonDirectory + '/repo.json', JSON.stringify(newRepoList));
|
||||
}
|
||||
|
||||
function handleError(error, res) {
|
||||
console.log(error);
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({ message: 'No such file or directory' });
|
||||
} else {
|
||||
res.status(500).json({ message: error.stdout });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import tokenController from '../../../../../helpers/functions/tokenController';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
// AUTHENTICATION
|
||||
const FROM_IP = req.headers['x-forwarded-for'] || 'unknown';
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!session && !authorization) {
|
||||
res.status(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!session && authorization) {
|
||||
const API_KEY = authorization.split(' ')[1];
|
||||
const permissions = await tokenController(API_KEY, FROM_IP);
|
||||
if (!permissions) {
|
||||
res.status(401).json({ message: 'Invalid API key' });
|
||||
return;
|
||||
}
|
||||
if (!permissions.read) {
|
||||
res.status(403).json({ message: 'Insufficient permissions' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: 'Internal Server Error' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
//Read the json data file data.json
|
||||
let repoList = await fs.readFile(jsonDirectory + '/repo.json', 'utf8');
|
||||
//Parse the json data file who has been read
|
||||
repoList = JSON.parse(repoList);
|
||||
//Find the ID (req.query.slug) in RepoList and put the repo in a single object (repo).
|
||||
let repo;
|
||||
for (let element in repoList) {
|
||||
if (repoList[element].id == req.query.slug) {
|
||||
repo = repoList[element];
|
||||
}
|
||||
}
|
||||
//If no repo is found --> 404.
|
||||
if (!repo) {
|
||||
res.status(404).json({
|
||||
message: 'No repository with id #' + req.query.slug,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Send the response and return the repo object --> 200
|
||||
res.status(200).json({ repo });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../pages/api/auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import tokenController from '../../../helpers/functions/tokenController';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === 'GET') {
|
||||
// AUTHENTICATION
|
||||
const FROM_IP = req.headers['x-forwarded-for'] || 'unknown';
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!session && !authorization) {
|
||||
res.status(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!session && authorization) {
|
||||
const API_KEY = authorization.split(' ')[1];
|
||||
const permissions = await tokenController(API_KEY, FROM_IP);
|
||||
if (!permissions) {
|
||||
res.status(401).json({ message: 'Invalid API key' });
|
||||
return;
|
||||
}
|
||||
if (!permissions.read) {
|
||||
res.status(403).json({ message: 'Insufficient permissions' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: 'Internal Server Error' });
|
||||
return;
|
||||
}
|
||||
|
||||
// GET REPO LIST
|
||||
try {
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
//Check if the repo.json file exists and initialize it if not.
|
||||
if (!fs.existsSync(jsonDirectory + '/repo.json')) {
|
||||
fs.writeFileSync(jsonDirectory + '/repo.json', JSON.stringify([]));
|
||||
}
|
||||
//Read the file repo.json
|
||||
let repoList = await fs.promises.readFile(jsonDirectory + '/repo.json', 'utf8');
|
||||
repoList = JSON.parse(repoList);
|
||||
res.status(200).json({ repoList });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
message: 'API error, contact the administrator !',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({
|
||||
message: 'Method Not Allowed ',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
157
pages/api/v1/account/email.test.ts
Normal file
157
pages/api/v1/account/email.test.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { createMocks } from 'node-mocks-http';
|
||||
import handler from '~/pages/api/v1/account/email';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { ConfigService } from '~/services';
|
||||
|
||||
vi.mock('next-auth/next');
|
||||
vi.mock('~/services');
|
||||
|
||||
describe('PUT on email API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(null);
|
||||
|
||||
const { req, res } = createMocks({ method: 'PUT' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
});
|
||||
|
||||
it('should return 405 if method is not PUT', async () => {
|
||||
const { req, res } = createMocks({ method: 'GET' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(405);
|
||||
});
|
||||
|
||||
it('should return 422 if email is not provided', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: {},
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(422);
|
||||
expect(res._getJSONData()).toEqual({ message: 'Unexpected data' });
|
||||
});
|
||||
|
||||
it('should return 400 if user is not found in the users list', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([
|
||||
{ id: 1, username: 'Ada', email: 'ada@example.com', password: '', roles: [] },
|
||||
]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { email: 'new@example.com' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(400);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if email already exists', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([
|
||||
{ id: 1, username: 'Lovelace', email: 'lovelace@example.com', password: '', roles: [] },
|
||||
{ id: 2, username: 'Ada', email: 'new@example.com', password: '', roles: [] },
|
||||
]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { email: 'new@example.com' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(400);
|
||||
expect(res._getJSONData()).toEqual({ message: 'Email already exists' });
|
||||
});
|
||||
|
||||
it('should update the email and return 200 on success', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
const users = [
|
||||
{ id: 1, username: 'Lovelace', email: 'lovelace@example.com', password: '', roles: [] },
|
||||
];
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue(users);
|
||||
vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { email: 'new@example.com' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
|
||||
{ ...users[0], email: 'new@example.com' },
|
||||
]);
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
|
||||
});
|
||||
|
||||
it('should return 500 if there is a file system error', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'ENOENT' });
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { email: 'new@example.com' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(500);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 500 on unknown error', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'UNKNOWN_ERROR' });
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { email: 'new@example.com' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(500);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
});
|
||||
});
|
||||
50
pages/api/v1/account/email.ts
Normal file
50
pages/api/v1/account/email.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { ConfigService } from '~/services';
|
||||
import { authOptions } from '~/pages/api/auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { EmailSettingDTO, ErrorResponse } from '~/types';
|
||||
import ApiResponse from '~/helpers/functions/apiResponse';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest & { body: EmailSettingDTO },
|
||||
res: NextApiResponse<ErrorResponse>
|
||||
) {
|
||||
if (req.method !== 'PUT') {
|
||||
return res.status(405);
|
||||
}
|
||||
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
return res.status(401);
|
||||
}
|
||||
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(422).json({ message: 'Unexpected data' });
|
||||
}
|
||||
|
||||
try {
|
||||
const usersList = await ConfigService.getUsersList();
|
||||
const userIndex = usersList.findIndex((user) => user.username === session.user?.name);
|
||||
|
||||
if (userIndex === -1) {
|
||||
return res.status(400).json({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
}
|
||||
|
||||
if (usersList.some((user) => user.email === email)) {
|
||||
return res.status(400).json({ message: 'Email already exists' });
|
||||
}
|
||||
|
||||
const updatedUsersList = usersList.map((user, index) =>
|
||||
index === userIndex ? { ...user, email } : user
|
||||
);
|
||||
|
||||
await ConfigService.updateUsersList(updatedUsersList);
|
||||
return res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
return ApiResponse.serverError(res, error);
|
||||
}
|
||||
}
|
||||
167
pages/api/v1/account/password.test.ts
Normal file
167
pages/api/v1/account/password.test.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { createMocks } from 'node-mocks-http';
|
||||
import handler from '~/pages/api/v1/account/password';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { ConfigService, AuthService } from '~/services';
|
||||
|
||||
vi.mock('next-auth/next');
|
||||
vi.mock('~/services');
|
||||
|
||||
describe('PUT /api/account/updatePassword', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(null);
|
||||
|
||||
const { req, res } = createMocks({ method: 'PUT' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
});
|
||||
|
||||
it('should return 405 if method is not PUT', async () => {
|
||||
const { req, res } = createMocks({ method: 'GET' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(405);
|
||||
});
|
||||
|
||||
it('should return 422 if oldPassword or newPassword are missing or not strings', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { oldPassword: 1234, newPassword: true },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(422);
|
||||
expect(res._getJSONData()).toEqual({ message: 'Unexpected data' });
|
||||
});
|
||||
|
||||
it('should return 400 if user is not found', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([
|
||||
{ id: 1, username: 'Ada', password: 'hashedpass', roles: [], email: 'ada@example.com' },
|
||||
]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { oldPassword: 'test', newPassword: 'newpass' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(400);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
message: 'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if old password is incorrect', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([
|
||||
{ id: 1, username: 'Lovelace', password: 'hashedpass', roles: [], email: 'love@example.com' },
|
||||
]);
|
||||
|
||||
vi.mocked(AuthService.verifyPassword).mockResolvedValue(false);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { oldPassword: 'wrongpass', newPassword: 'newpass' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(400);
|
||||
expect(res._getJSONData()).toEqual({ message: 'Old password is incorrect.' });
|
||||
});
|
||||
|
||||
it('should update password and return 200 on success', async () => {
|
||||
const oldUser = {
|
||||
id: 1,
|
||||
username: 'Lovelace',
|
||||
password: 'hashedpass',
|
||||
roles: [],
|
||||
email: 'love@example.com',
|
||||
};
|
||||
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([oldUser]);
|
||||
|
||||
vi.mocked(AuthService.verifyPassword).mockResolvedValue(true);
|
||||
vi.mocked(AuthService.hashPassword).mockResolvedValue('newHashedPassword');
|
||||
vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { oldPassword: 'oldpass', newPassword: 'newpass' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(AuthService.verifyPassword).toHaveBeenCalledWith('oldpass', 'hashedpass');
|
||||
expect(AuthService.hashPassword).toHaveBeenCalledWith('newpass');
|
||||
expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
|
||||
{ ...oldUser, password: 'newHashedPassword' },
|
||||
]);
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
|
||||
});
|
||||
|
||||
it('should return 500 if there is a file system error (ENOENT)', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'ENOENT' });
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { oldPassword: 'test', newPassword: 'new' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(500);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 500 on unknown error', async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue({
|
||||
user: { name: 'Lovelace' },
|
||||
});
|
||||
|
||||
vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'SOMETHING_ELSE' });
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'PUT',
|
||||
body: { oldPassword: 'test', newPassword: 'new' },
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(500);
|
||||
expect(res._getJSONData()).toEqual({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue