Merge pull request #29 from Ravinou/feat/apprise-integration

Feat/apprise integration
This commit is contained in:
Ravinou 2023-03-15 10:19:36 +01:00 committed by GitHub
commit 1963dfd18f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1556 additions and 102 deletions

View file

@ -0,0 +1,210 @@
//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';
//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>Apprise alert</h2>
</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>
</>
);
}

View file

@ -0,0 +1,173 @@
//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';
//Components
import Error from '../../../../Components/UI/Error/Error';
export default function AppriseMode() {
//Var
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ 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('');
const [appriseStatelessURL, setAppriseStatelessURL] = useState();
////LifeCycle
//Component did mount
useEffect(() => {
//Initial fetch to get Apprise Mode enabled
const getAppriseMode = async () => {
try {
const response = await fetch('/api/account/getAppriseMode', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
const { appriseStatelessURL, appriseMode } =
await response.json();
setAppriseMode(appriseMode);
if (appriseMode == 'stateless') {
setAppriseStatelessURL(appriseStatelessURL);
setDisplayStatelessURL(true);
}
} catch (error) {
console.log('Fetching Apprise Mode failed.');
}
};
getAppriseMode();
}, []);
////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
try {
const response = await fetch('/api/account/updateAppriseMode', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
setFormIsLoading(false);
setError(result.message);
setTimeout(() => setError(), 4000);
} else {
setFormIsLoading(false);
setModeFormIsSaved(true);
setTimeout(() => setModeFormIsSaved(false), 3000);
}
} catch (error) {
setFormIsLoading(false);
setError('Change mode failed. Contact your administrator.');
setTimeout(() => {
setError();
}, 4000);
}
};
return (
<>
{/* APPRISE MODE SELECTION */}
<div className={classes.headerFormAppriseUrls}>
<div style={{ margin: '0px 10px 0px 0px' }}>Apprise mode</div>
<div style={{ display: 'flex' }}>
{formIsLoading && (
<SpinnerCircularFixed
size={18}
thickness={150}
speed={150}
color='#704dff'
secondaryColor='#c3b6fa'
/>
)}
{modeFormIsSaved && (
<div className={classes.formIsSavedMessage}>
Apprise mode has been saved.
</div>
)}
</div>
</div>
{error && <Error message={error} />}
<form
className={classes.bwForm}
onBlur={handleSubmit(modeFormSubmitHandler)}
>
<div className='radio-group'>
<label style={{ marginRight: '50px' }}>
<div style={{ display: 'flex' }}>
<input
{...register('appriseMode')}
type='radio'
value='package'
onClick={() => {
setDisplayStatelessURL(false);
setAppriseMode('package');
}}
checked={
appriseMode == 'package' ? true : false
}
/>
<span>Local package</span>
</div>
</label>
<label>
<div style={{ display: 'flex' }}>
<input
{...register('appriseMode')}
value='stateless'
type='radio'
onClick={() => {
setDisplayStatelessURL(true);
setAppriseMode('stateless');
}}
checked={
appriseMode == 'stateless' ? true : false
}
/>
<span>Stateless API server</span>
</div>
</label>
</div>
{displayStatelessURL && (
<input
type='text'
placeholder='http://localhost:8000'
defaultValue={appriseStatelessURL}
{...register('appriseStatelessURL', {
pattern: {
value: /^(http|https):\/\/.+/g,
message: 'Invalid URL format.',
},
})}
/>
)}
{errors.appriseStatelessURL && (
<small className={classes.errorMessage}>
{errors.appriseStatelessURL.message}
</small>
)}
</form>
</>
);
}

View file

@ -0,0 +1,162 @@
//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';
//Components
import Error from '../../../../Components/UI/Error/Error';
export default function AppriseURLs() {
//Var
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ mode: 'onBlur' });
////State
const [formIsLoading, setFormIsLoading] = useState(false);
const [urlsFormIsSaved, setUrlsFormIsSaved] = useState(false);
const [appriseServicesList, setAppriseServicesList] = useState();
const [error, setError] = useState();
////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',
{
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());
} catch (error) {
console.log('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
try {
const response = await fetch('/api/account/updateAppriseServices', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
setFormIsLoading(false);
setError(result.message);
setTimeout(() => setError(), 4000);
} else {
setFormIsLoading(false);
setUrlsFormIsSaved(true);
setTimeout(() => setUrlsFormIsSaved(false), 3000);
}
} catch (error) {
setFormIsLoading(false);
setError(
'Failed to update your services. Contact your administrator.'
);
setTimeout(() => {
setError();
}, 4000);
}
};
return (
<>
{/* APPRISE SERVICES URLS */}
<div className={classes.headerFormAppriseUrls}>
<div style={{ marginRight: '10px' }}>Apprise URLs</div>
{error && <Error message={error} />}
<div style={{ display: 'flex' }}>
{formIsLoading && (
<SpinnerCircularFixed
size={18}
thickness={150}
speed={150}
color='#704dff'
secondaryColor='#c3b6fa'
/>
)}
{urlsFormIsSaved && (
<div className={classes.formIsSavedMessage}>
Apprise configuration has been saved.
</div>
)}
</div>
</div>
<form
onBlur={handleSubmit(urlsFormSubmitHandler)}
className={classes.bwForm + ' ' + classes.currentSetting}
>
<textarea
style={{ height: '100px' }}
type='text'
placeholder={
'matrixs://{user}:{password}@{matrixhost}\ndiscord://{WebhookID}/{WebhookToken}\nmmosts://user@hostname/authkey'
}
defaultValue={appriseServicesList}
{...register('appriseURLs', {
pattern: {
value: /^.+:\/\/.+$/gm,
message: 'Invalid URLs format.',
},
})}
/>
{errors.appriseURLs && (
<small className={classes.errorMessage}>
{errors.appriseURLs.message}
</small>
)}
</form>
<div
style={{
color: '#6c737f',
fontSize: '0.875rem',
marginBottom: '20px',
}}
>
Use{' '}
<a
style={{
color: '#6d4aff',
textDecoration: 'none',
}}
href='https://github.com/caronc/apprise#supported-notifications'
>
Apprise URLs
</a>{' '}
to send a notification to any service. Only one URL per line.
</div>
</>
);
}

View file

@ -10,7 +10,7 @@ import { SpinnerCircularFixed } from 'spinners-react';
import Error from '../../../Components/UI/Error/Error';
import Switch from '../../../Components/UI/Switch/Switch';
export default function EmailAlertSettings(props) {
export default function EmailAlertSettings() {
//Var
const toastOptions = {
position: 'top-right',
@ -46,64 +46,96 @@ export default function EmailAlertSettings(props) {
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);
const response = await fetch('/api/account/updateEmailAlert', {
await fetch('/api/account/updateEmailAlert', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
setError(result.message);
setTimeout(() => {
setError();
setDisabled(false);
}, 4000);
} else {
if (data.emailAlert) {
setChecked(!checked);
toast.success('Email notification enabled !', toastOptions);
} else {
setChecked(!checked);
toast.success('Email notification disabled !', toastOptions);
}
}
})
.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();
const response = await fetch('/api/account/sendTestEmail', {
await fetch('/api/account/sendTestEmail', {
method: 'POST',
});
const result = await response.json();
if (!response.ok) {
setTestIsLoading(false);
setError(result.message);
} else {
setTestIsLoading(false);
setInfo(true);
setTimeout(() => {
setInfo(false);
}, 4000);
}
})
.then((response) => {
if (!response.ok) {
setTestIsLoading(false);
setError('Failed to send the notification.');
setTimeout(() => {
setError();
}, 4000);
} else {
setTestIsLoading(false);
setInfo(true);
setTimeout(() => {
setInfo(false);
}, 4000);
}
})
.catch((error) => {
setTestIsLoading(false);
console.log(error);
setError('Send email failed. Contact your administrator.');
setTimeout(() => {
setError();
}, 4000);
});
};
return (
@ -111,7 +143,7 @@ export default function EmailAlertSettings(props) {
{/* EMAIL ALERT */}
<div className={classes.containerSetting}>
<div className={classes.settingCategory}>
<h2>Alerting</h2>
<h2>Email alert</h2>
</div>
<div className={classes.setting}>
<div className={classes.bwFormWrapper}>
@ -127,7 +159,7 @@ export default function EmailAlertSettings(props) {
<Switch
checked={checked}
disabled={disabled}
switchName='Email'
switchName='Alert me by email'
switchDescription='You will receive an alert every 24H if you have a down status.'
onChange={(e) =>
onChangeSwitchHandler({ emailAlert: e })

View file

@ -41,26 +41,33 @@ export default function EmailSettings(props) {
setError();
//Loading button on submit to avoid multiple send.
setIsLoading(true);
//POST API to send the new and old password
const response = await fetch('/api/account/updateEmail', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
//POST API to send the new mail address
try {
const response = await fetch('/api/account/updateEmail', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
setIsLoading(false);
if (!response.ok) {
setIsLoading(false);
reset();
setError(result.message);
setTimeout(() => setError(), 4000);
} else {
reset();
setIsLoading(false);
setInfo(true);
toast.success('Email edited !', toastOptions);
}
} catch (error) {
reset();
setError(result.message);
setIsLoading(false);
setError("Can't update your email. Contact your administrator.");
setTimeout(() => setError(), 4000);
} else {
reset();
setIsLoading(false);
setInfo(true);
toast.success('Email edited !', toastOptions);
}
};
return (
@ -107,7 +114,7 @@ export default function EmailSettings(props) {
)}
</p>
<button
className='defaultButton'
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting}
>
{isLoading ? (

View file

@ -42,24 +42,31 @@ export default function PasswordSettings(props) {
//Loading button on submit to avoid multiple send.
setIsLoading(true);
//POST API to send the new and old password
const response = await fetch('/api/account/updatePassword', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
try {
const response = await fetch('/api/account/updatePassword', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
setIsLoading(false);
if (!response.ok) {
setIsLoading(false);
reset();
setError(result.message);
setTimeout(() => setError(), 4000);
} else {
reset();
setIsLoading(false);
toast.success('🔑 Password edited !', toastOptions);
}
} catch (error) {
reset();
setError(result.message);
setIsLoading(false);
setError("Can't update your password. Contact your administrator.");
setTimeout(() => setError(), 4000);
} else {
reset();
setIsLoading(false);
toast.success('🔑 Password edited !', toastOptions);
}
};
return (
@ -106,7 +113,7 @@ export default function PasswordSettings(props) {
)}
</p>
<button
className='defaultButton'
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting}
>
{isLoading ? (

View file

@ -1,28 +1,67 @@
//Lib
import 'react-toastify/dist/ReactToastify.css';
import classes from './UserSettings.module.css';
import { useState } 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';
export default function UserSettings(props) {
//States
const [tab, setTab] = useState('General');
return (
<div className={classes.containerSettings}>
<div>
<h1 style={{ color: '#494b7a', textAlign: 'center' }}>
Welcome{' '}
{props.status === 'authenticated' && props.data.user.name}{' '}
👋
<h1
style={{
color: '#494b7a',
textAlign: 'left',
marginLeft: '30px',
}}
>
Account{' '}
</h1>
</div>
<PasswordSettings username={props.data.user.name} />
<EmailSettings email={props.data.user.email} />
<UsernameSettings username={props.data.user.name} />
<EmailAlertSettings />
<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>
</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 />
</>
)}
</div>
);
}

View file

@ -12,7 +12,7 @@
width: 100%;
margin: 40px 20px 0px 5px;
text-align: left;
padding: 32px 24px;
padding: 28px 24px;
animation: entrance ease-in 0.3s 1 normal none;
border-bottom: 1px solid #e5e7eb;
}
@ -38,6 +38,7 @@
.settingCategory h2 {
color: #494b7a;
margin: 0;
font-size: 1.3em;
}
.setting {
@ -59,6 +60,14 @@
width: 100%;
height: auto;
color: #494b7a;
font-family: var(
--pure-material-font,
'Roboto',
'Segoe UI',
BlinkMacSystemFont,
system-ui,
-apple-system
);
}
.bwFormWrapper p {
@ -69,7 +78,7 @@
display: block;
margin-bottom: 8px;
text-align: center;
margin-top: 20px;
/* margin-top: 20px; */
color: #494b7a;
}
@ -82,17 +91,26 @@
margin: 0;
margin-bottom: 0px;
outline: 0;
padding: 15px;
padding: 10px;
width: 100%;
background-color: #f5f5f5;
border-radius: 5px;
/* color: #1b1340; */
color: #494b7a;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
font-family: (
--pure-material-font,
'Roboto',
'Segoe UI',
BlinkMacSystemFont,
system-ui
);
}
.bwForm textarea {
resize: vertical;
overflow: auto;
white-space: pre;
}
.bwForm textarea:focus,
@ -131,3 +149,102 @@
.currentSetting input::placeholder {
opacity: 1;
}
.headerFormAppriseUrls {
font-weight: 500;
color: #494b7a;
margin: 40px 0px 10px 0px;
display: flex;
padding-right: 5px;
}
.formIsSavedMessage {
color: rgb(0, 164, 0);
animation: entrance 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
font-weight: 300;
}
.tabList {
display: flex;
}
.tabListButton {
color: #494b7a;
padding: 12px 0px;
min-height: 48px;
overflow: hidden;
text-align: center;
flex-direction: column;
font-size: 1em;
font-weight: 500;
line-height: 1.71;
text-transform: none;
align-items: center;
cursor: pointer;
vertical-align: middle;
text-decoration: none;
border: 0;
background-color: transparent;
margin-left: 30px;
border-bottom: 2px solid transparent;
}
.tabListButton:hover {
color: #6d4aff;
border-bottom: 2px solid #6d4aff;
}
.tabListButtonActive {
color: #6d4aff;
border: 0;
border-bottom: 2px solid #6d4aff;
padding: 12px 0px;
min-height: 48px;
overflow: hidden;
text-align: center;
flex-direction: column;
font-size: 1em;
font-weight: 500;
line-height: 1.71;
text-transform: none;
align-items: center;
cursor: pointer;
vertical-align: middle;
text-decoration: none;
background-color: transparent;
margin-left: 30px;
}
.AccountSettingsButton {
border: 0;
padding: 10px 15px;
background-color: #6d4aff;
color: white;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-size: 1em;
}
.AccountSettingsButton:hover {
border: 0;
padding: 10px 15px;
background-color: #4f31ce;
color: white;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-size: 1em;
}
.AccountSettingsButton:active {
border: 0;
padding: 10px 15px;
background-color: #4f31ce;
color: white;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-size: 1em;
transform: scale(0.95);
}

View file

@ -41,26 +41,33 @@ export default function UsernameSettings(props) {
setError();
//Loading button on submit to avoid multiple send.
setIsLoading(true);
//POST API to send the new and old password
const response = await fetch('/api/account/updateUsername', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
//POST API to update the username
try {
const response = await fetch('/api/account/updateUsername', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
setIsLoading(false);
if (!response.ok) {
setIsLoading(false);
reset();
setError(result.message);
setTimeout(() => setError(), 4000);
} else {
reset();
setIsLoading(false);
setInfo(true);
toast.success('Username edited !', toastOptions);
}
} catch (error) {
reset();
setError(result.message);
setIsLoading(false);
setError("Can't update your username. Contact your administrator.");
setTimeout(() => setError(), 4000);
} else {
reset();
setIsLoading(false);
setInfo(true);
toast.success('Username edited !', toastOptions);
}
};
return (
@ -116,7 +123,7 @@ export default function UsernameSettings(props) {
)}
</p>
<button
className='defaultButton'
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting}
>
{isLoading ? (

View file

@ -0,0 +1,63 @@
//Lib
import { promises as fs } from 'fs';
import path from 'path';
import { authOptions } from '../auth/[...nextauth]';
import { unstable_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 unstable_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' });
}
}

View file

@ -0,0 +1,65 @@
//Lib
import { promises as fs } from 'fs';
import path from 'path';
import { authOptions } from '../auth/[...nextauth]';
import { unstable_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 unstable_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' });
}
}

View file

@ -0,0 +1,63 @@
//Lib
import { promises as fs } from 'fs';
import path from 'path';
import { authOptions } from '../auth/[...nextauth]';
import { unstable_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 unstable_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' });
}
}

View file

@ -0,0 +1,163 @@
//Lib
import { authOptions } from '../auth/[...nextauth]';
import { unstable_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 unstable_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.',
});
}
}
}

View file

@ -0,0 +1,82 @@
//Lib
import { promises as fs } from 'fs';
import path from 'path';
import { authOptions } from '../auth/[...nextauth]';
import { unstable_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 unstable_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
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' });
}
}

View file

@ -0,0 +1,86 @@
//Lib
import { promises as fs } from 'fs';
import path from 'path';
import { authOptions } from '../auth/[...nextauth]';
import { unstable_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 unstable_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
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' });
}
}

View file

@ -0,0 +1,85 @@
//Lib
import { promises as fs } from 'fs';
import path from 'path';
import { authOptions } from '../auth/[...nextauth]';
import { unstable_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 unstable_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
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' });
}
}

View file

@ -89,7 +89,7 @@ export default async function handler(req, res) {
return;
}
//// PART 2 : check if there is a repo that need an email alert
//// 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) {
@ -126,7 +126,7 @@ export default async function handler(req, res) {
});
return;
}
//PART 4 : Send the mail alert
//PART 4 : Send the alerts
if (repoListToSendAlert.length > 0) {
// Read user informations
try {
@ -144,6 +144,7 @@ export default async function handler(req, res) {
});
return;
}
////EMAIL
// If the user has enabled email alerts
if (usersList[0].emailAlert) {
//Send mail
@ -169,6 +170,61 @@ export default async function handler(req, res) {
}
});
}
////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

View file

@ -174,3 +174,40 @@ code {
.heart::before {
content: '\f004';
}
/* Radio group and radio button */
.radio-group {
display: flex;
}
.radio-group input[type='radio']:checked + span::before {
background-color: #6d4aff;
box-shadow: inset white 0 0 0 2px;
}
.radio-group span::before {
border: 2px solid #6d4aff;
content: '';
display: flex;
height: 16px;
width: 16px;
margin: 0 5px;
border-radius: 50%;
box-sizing: border-box;
transition: all ease 0.2s;
box-shadow: inset white 0 0 0 10px;
}
.radio-group label {
margin: 0 8px 0 0;
cursor: pointer;
}
.radio-group input[type='radio'] {
opacity: 0;
width: 0;
}
.radio-group span {
display: flex;
align-items: center;
}