[release] v0.3.0-unstable2 SMTP

v0.3.0-unstable3

v0.3.0-unstable4

v0.3.0-unstable4
This commit is contained in:
Yann Stepienik 2023-04-30 17:23:48 +01:00
parent 3811e3131e
commit b84ee3e61c
22 changed files with 1359 additions and 256 deletions

View file

@ -5,6 +5,7 @@ if [ $? -ne 0 ]; then
fi
cp -r static build/
cp -r GeoLite2-Country.mmdb build/
cp -r Logo.png build/static/
mkdir build/images
cp client/src/assets/images/icons/cosmos_gray.png build/cosmos_gray.png
cp client/src/assets/images/icons/cosmos_gray.png cosmos_gray.png

View file

@ -1,3 +1,15 @@
## Version 0.3.0
- Set Max nb simulatneous connections per user
- Set Global Max nb simulatneous connections
- Display nickname on invite page
- Reset self-signed certificates when hostnames changes
- Block based on geo-locations
- Block common bots
- DNS challenge for letsencrypt
- Implement 2 FA
- Implement SMTP to Send Email (password reset / invites)
- Show loading on user rows on actions
## Version 0.2.0
- URL UI completely redone from scratch
- Add new "Smart Shield" feature for easier protection without manual adjustments required

View file

@ -100,6 +100,16 @@ function reset2FA(values) {
}))
}
function resetPassword(values) {
return wrap(fetch('/cosmos/api/password-reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values),
}))
}
export {
list,
create,
@ -110,5 +120,6 @@ export {
deleteUser,
new2FA,
check2FA,
reset2FA
reset2FA,
resetPassword,
};

View file

@ -0,0 +1,493 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import { Grid } from '@mui/material';
// From https://bitbucket.org/atlassian/atlaskit-mk-2/raw/4ad0e56649c3e6c973e226b7efaeb28cb240ccb0/packages/core/select/src/data/countries.js
const _countries = [
{ code: 'AD', label: 'Andorra', phone: '376' },
{
code: 'AE',
label: 'United Arab Emirates',
phone: '971',
},
{ code: 'AF', label: 'Afghanistan', phone: '93' },
{
code: 'AG',
label: 'Antigua and Barbuda',
phone: '1-268',
},
{ code: 'AI', label: 'Anguilla', phone: '1-264' },
{ code: 'AL', label: 'Albania', phone: '355' },
{ code: 'AM', label: 'Armenia', phone: '374' },
{ code: 'AO', label: 'Angola', phone: '244' },
{ code: 'AQ', label: 'Antarctica', phone: '672' },
{ code: 'AR', label: 'Argentina', phone: '54' },
{ code: 'AS', label: 'American Samoa', phone: '1-684' },
{ code: 'AT', label: 'Austria', phone: '43' },
{
code: 'AU',
label: 'Australia',
phone: '61',
suggested: true,
},
{ code: 'AW', label: 'Aruba', phone: '297' },
{ code: 'AX', label: 'Alland Islands', phone: '358' },
{ code: 'AZ', label: 'Azerbaijan', phone: '994' },
{
code: 'BA',
label: 'Bosnia and Herzegovina',
phone: '387',
},
{ code: 'BB', label: 'Barbados', phone: '1-246' },
{ code: 'BD', label: 'Bangladesh', phone: '880' },
{ code: 'BE', label: 'Belgium', phone: '32' },
{ code: 'BF', label: 'Burkina Faso', phone: '226' },
{ code: 'BG', label: 'Bulgaria', phone: '359' },
{ code: 'BH', label: 'Bahrain', phone: '973' },
{ code: 'BI', label: 'Burundi', phone: '257' },
{ code: 'BJ', label: 'Benin', phone: '229' },
{ code: 'BL', label: 'Saint Barthelemy', phone: '590' },
{ code: 'BM', label: 'Bermuda', phone: '1-441' },
{ code: 'BN', label: 'Brunei Darussalam', phone: '673' },
{ code: 'BO', label: 'Bolivia', phone: '591' },
{ code: 'BR', label: 'Brazil', phone: '55' },
{ code: 'BS', label: 'Bahamas', phone: '1-242' },
{ code: 'BT', label: 'Bhutan', phone: '975' },
{ code: 'BV', label: 'Bouvet Island', phone: '47' },
{ code: 'BW', label: 'Botswana', phone: '267' },
{ code: 'BY', label: 'Belarus', phone: '375' },
{ code: 'BZ', label: 'Belize', phone: '501' },
{
code: 'CA',
label: 'Canada',
phone: '1',
suggested: true,
},
{
code: 'CC',
label: 'Cocos (Keeling) Islands',
phone: '61',
},
{
code: 'CD',
label: 'Congo, Democratic Republic of the',
phone: '243',
},
{
code: 'CF',
label: 'Central African Republic',
phone: '236',
},
{
code: 'CG',
label: 'Congo, Republic of the',
phone: '242',
},
{ code: 'CH', label: 'Switzerland', phone: '41' },
{ code: 'CI', label: "Cote d'Ivoire", phone: '225' },
{ code: 'CK', label: 'Cook Islands', phone: '682' },
{ code: 'CL', label: 'Chile', phone: '56' },
{ code: 'CM', label: 'Cameroon', phone: '237' },
{ code: 'CN', label: 'China', phone: '86' },
{ code: 'CO', label: 'Colombia', phone: '57' },
{ code: 'CR', label: 'Costa Rica', phone: '506' },
{ code: 'CU', label: 'Cuba', phone: '53' },
{ code: 'CV', label: 'Cape Verde', phone: '238' },
{ code: 'CW', label: 'Curacao', phone: '599' },
{ code: 'CX', label: 'Christmas Island', phone: '61' },
{ code: 'CY', label: 'Cyprus', phone: '357' },
{ code: 'CZ', label: 'Czech Republic', phone: '420' },
{
code: 'DE',
label: 'Germany',
phone: '49',
suggested: true,
},
{ code: 'DJ', label: 'Djibouti', phone: '253' },
{ code: 'DK', label: 'Denmark', phone: '45' },
{ code: 'DM', label: 'Dominica', phone: '1-767' },
{
code: 'DO',
label: 'Dominican Republic',
phone: '1-809',
},
{ code: 'DZ', label: 'Algeria', phone: '213' },
{ code: 'EC', label: 'Ecuador', phone: '593' },
{ code: 'EE', label: 'Estonia', phone: '372' },
{ code: 'EG', label: 'Egypt', phone: '20' },
{ code: 'EH', label: 'Western Sahara', phone: '212' },
{ code: 'ER', label: 'Eritrea', phone: '291' },
{ code: 'ES', label: 'Spain', phone: '34' },
{ code: 'ET', label: 'Ethiopia', phone: '251' },
{ code: 'FI', label: 'Finland', phone: '358' },
{ code: 'FJ', label: 'Fiji', phone: '679' },
{
code: 'FK',
label: 'Falkland Islands (Malvinas)',
phone: '500',
},
{
code: 'FM',
label: 'Micronesia, Federated States of',
phone: '691',
},
{ code: 'FO', label: 'Faroe Islands', phone: '298' },
{
code: 'FR',
label: 'France',
phone: '33',
suggested: true,
},
{ code: 'GA', label: 'Gabon', phone: '241' },
{ code: 'GB', label: 'United Kingdom', phone: '44' },
{ code: 'GD', label: 'Grenada', phone: '1-473' },
{ code: 'GE', label: 'Georgia', phone: '995' },
{ code: 'GF', label: 'French Guiana', phone: '594' },
{ code: 'GG', label: 'Guernsey', phone: '44' },
{ code: 'GH', label: 'Ghana', phone: '233' },
{ code: 'GI', label: 'Gibraltar', phone: '350' },
{ code: 'GL', label: 'Greenland', phone: '299' },
{ code: 'GM', label: 'Gambia', phone: '220' },
{ code: 'GN', label: 'Guinea', phone: '224' },
{ code: 'GP', label: 'Guadeloupe', phone: '590' },
{ code: 'GQ', label: 'Equatorial Guinea', phone: '240' },
{ code: 'GR', label: 'Greece', phone: '30' },
{
code: 'GS',
label: 'South Georgia and the South Sandwich Islands',
phone: '500',
},
{ code: 'GT', label: 'Guatemala', phone: '502' },
{ code: 'GU', label: 'Guam', phone: '1-671' },
{ code: 'GW', label: 'Guinea-Bissau', phone: '245' },
{ code: 'GY', label: 'Guyana', phone: '592' },
{ code: 'HK', label: 'Hong Kong', phone: '852' },
{
code: 'HM',
label: 'Heard Island and McDonald Islands',
phone: '672',
},
{ code: 'HN', label: 'Honduras', phone: '504' },
{ code: 'HR', label: 'Croatia', phone: '385' },
{ code: 'HT', label: 'Haiti', phone: '509' },
{ code: 'HU', label: 'Hungary', phone: '36' },
{ code: 'ID', label: 'Indonesia', phone: '62' },
{ code: 'IE', label: 'Ireland', phone: '353' },
{ code: 'IL', label: 'Israel', phone: '972' },
{ code: 'IM', label: 'Isle of Man', phone: '44' },
{ code: 'IN', label: 'India', phone: '91' },
{
code: 'IO',
label: 'British Indian Ocean Territory',
phone: '246',
},
{ code: 'IQ', label: 'Iraq', phone: '964' },
{
code: 'IR',
label: 'Iran, Islamic Republic of',
phone: '98',
},
{ code: 'IS', label: 'Iceland', phone: '354' },
{ code: 'IT', label: 'Italy', phone: '39' },
{ code: 'JE', label: 'Jersey', phone: '44' },
{ code: 'JM', label: 'Jamaica', phone: '1-876' },
{ code: 'JO', label: 'Jordan', phone: '962' },
{
code: 'JP',
label: 'Japan',
phone: '81',
suggested: true,
},
{ code: 'KE', label: 'Kenya', phone: '254' },
{ code: 'KG', label: 'Kyrgyzstan', phone: '996' },
{ code: 'KH', label: 'Cambodia', phone: '855' },
{ code: 'KI', label: 'Kiribati', phone: '686' },
{ code: 'KM', label: 'Comoros', phone: '269' },
{
code: 'KN',
label: 'Saint Kitts and Nevis',
phone: '1-869',
},
{
code: 'KP',
label: "Korea, Democratic People's Republic of",
phone: '850',
},
{ code: 'KR', label: 'Korea, Republic of', phone: '82' },
{ code: 'KW', label: 'Kuwait', phone: '965' },
{ code: 'KY', label: 'Cayman Islands', phone: '1-345' },
{ code: 'KZ', label: 'Kazakhstan', phone: '7' },
{
code: 'LA',
label: "Lao People's Democratic Republic",
phone: '856',
},
{ code: 'LB', label: 'Lebanon', phone: '961' },
{ code: 'LC', label: 'Saint Lucia', phone: '1-758' },
{ code: 'LI', label: 'Liechtenstein', phone: '423' },
{ code: 'LK', label: 'Sri Lanka', phone: '94' },
{ code: 'LR', label: 'Liberia', phone: '231' },
{ code: 'LS', label: 'Lesotho', phone: '266' },
{ code: 'LT', label: 'Lithuania', phone: '370' },
{ code: 'LU', label: 'Luxembourg', phone: '352' },
{ code: 'LV', label: 'Latvia', phone: '371' },
{ code: 'LY', label: 'Libya', phone: '218' },
{ code: 'MA', label: 'Morocco', phone: '212' },
{ code: 'MC', label: 'Monaco', phone: '377' },
{
code: 'MD',
label: 'Moldova, Republic of',
phone: '373',
},
{ code: 'ME', label: 'Montenegro', phone: '382' },
{
code: 'MF',
label: 'Saint Martin (French part)',
phone: '590',
},
{ code: 'MG', label: 'Madagascar', phone: '261' },
{ code: 'MH', label: 'Marshall Islands', phone: '692' },
{
code: 'MK',
label: 'Macedonia, the Former Yugoslav Republic of',
phone: '389',
},
{ code: 'ML', label: 'Mali', phone: '223' },
{ code: 'MM', label: 'Myanmar', phone: '95' },
{ code: 'MN', label: 'Mongolia', phone: '976' },
{ code: 'MO', label: 'Macao', phone: '853' },
{
code: 'MP',
label: 'Northern Mariana Islands',
phone: '1-670',
},
{ code: 'MQ', label: 'Martinique', phone: '596' },
{ code: 'MR', label: 'Mauritania', phone: '222' },
{ code: 'MS', label: 'Montserrat', phone: '1-664' },
{ code: 'MT', label: 'Malta', phone: '356' },
{ code: 'MU', label: 'Mauritius', phone: '230' },
{ code: 'MV', label: 'Maldives', phone: '960' },
{ code: 'MW', label: 'Malawi', phone: '265' },
{ code: 'MX', label: 'Mexico', phone: '52' },
{ code: 'MY', label: 'Malaysia', phone: '60' },
{ code: 'MZ', label: 'Mozambique', phone: '258' },
{ code: 'NA', label: 'Namibia', phone: '264' },
{ code: 'NC', label: 'New Caledonia', phone: '687' },
{ code: 'NE', label: 'Niger', phone: '227' },
{ code: 'NF', label: 'Norfolk Island', phone: '672' },
{ code: 'NG', label: 'Nigeria', phone: '234' },
{ code: 'NI', label: 'Nicaragua', phone: '505' },
{ code: 'NL', label: 'Netherlands', phone: '31' },
{ code: 'NO', label: 'Norway', phone: '47' },
{ code: 'NP', label: 'Nepal', phone: '977' },
{ code: 'NR', label: 'Nauru', phone: '674' },
{ code: 'NU', label: 'Niue', phone: '683' },
{ code: 'NZ', label: 'New Zealand', phone: '64' },
{ code: 'OM', label: 'Oman', phone: '968' },
{ code: 'PA', label: 'Panama', phone: '507' },
{ code: 'PE', label: 'Peru', phone: '51' },
{ code: 'PF', label: 'French Polynesia', phone: '689' },
{ code: 'PG', label: 'Papua New Guinea', phone: '675' },
{ code: 'PH', label: 'Philippines', phone: '63' },
{ code: 'PK', label: 'Pakistan', phone: '92' },
{ code: 'PL', label: 'Poland', phone: '48' },
{
code: 'PM',
label: 'Saint Pierre and Miquelon',
phone: '508',
},
{ code: 'PN', label: 'Pitcairn', phone: '870' },
{ code: 'PR', label: 'Puerto Rico', phone: '1' },
{
code: 'PS',
label: 'Palestine, State of',
phone: '970',
},
{ code: 'PT', label: 'Portugal', phone: '351' },
{ code: 'PW', label: 'Palau', phone: '680' },
{ code: 'PY', label: 'Paraguay', phone: '595' },
{ code: 'QA', label: 'Qatar', phone: '974' },
{ code: 'RE', label: 'Reunion', phone: '262' },
{ code: 'RO', label: 'Romania', phone: '40' },
{ code: 'RS', label: 'Serbia', phone: '381' },
{ code: 'RU', label: 'Russian Federation', phone: '7' },
{ code: 'RW', label: 'Rwanda', phone: '250' },
{ code: 'SA', label: 'Saudi Arabia', phone: '966' },
{ code: 'SB', label: 'Solomon Islands', phone: '677' },
{ code: 'SC', label: 'Seychelles', phone: '248' },
{ code: 'SD', label: 'Sudan', phone: '249' },
{ code: 'SE', label: 'Sweden', phone: '46' },
{ code: 'SG', label: 'Singapore', phone: '65' },
{ code: 'SH', label: 'Saint Helena', phone: '290' },
{ code: 'SI', label: 'Slovenia', phone: '386' },
{
code: 'SJ',
label: 'Svalbard and Jan Mayen',
phone: '47',
},
{ code: 'SK', label: 'Slovakia', phone: '421' },
{ code: 'SL', label: 'Sierra Leone', phone: '232' },
{ code: 'SM', label: 'San Marino', phone: '378' },
{ code: 'SN', label: 'Senegal', phone: '221' },
{ code: 'SO', label: 'Somalia', phone: '252' },
{ code: 'SR', label: 'Suriname', phone: '597' },
{ code: 'SS', label: 'South Sudan', phone: '211' },
{
code: 'ST',
label: 'Sao Tome and Principe',
phone: '239',
},
{ code: 'SV', label: 'El Salvador', phone: '503' },
{
code: 'SX',
label: 'Sint Maarten (Dutch part)',
phone: '1-721',
},
{
code: 'SY',
label: 'Syrian Arab Republic',
phone: '963',
},
{ code: 'SZ', label: 'Swaziland', phone: '268' },
{
code: 'TC',
label: 'Turks and Caicos Islands',
phone: '1-649',
},
{ code: 'TD', label: 'Chad', phone: '235' },
{
code: 'TF',
label: 'French Southern Territories',
phone: '262',
},
{ code: 'TG', label: 'Togo', phone: '228' },
{ code: 'TH', label: 'Thailand', phone: '66' },
{ code: 'TJ', label: 'Tajikistan', phone: '992' },
{ code: 'TK', label: 'Tokelau', phone: '690' },
{ code: 'TL', label: 'Timor-Leste', phone: '670' },
{ code: 'TM', label: 'Turkmenistan', phone: '993' },
{ code: 'TN', label: 'Tunisia', phone: '216' },
{ code: 'TO', label: 'Tonga', phone: '676' },
{ code: 'TR', label: 'Turkey', phone: '90' },
{
code: 'TT',
label: 'Trinidad and Tobago',
phone: '1-868',
},
{ code: 'TV', label: 'Tuvalu', phone: '688' },
{
code: 'TW',
label: 'Taiwan, Republic of China',
phone: '886',
},
{
code: 'TZ',
label: 'United Republic of Tanzania',
phone: '255',
},
{ code: 'UA', label: 'Ukraine', phone: '380' },
{ code: 'UG', label: 'Uganda', phone: '256' },
{
code: 'US',
label: 'United States',
phone: '1',
suggested: true,
},
{ code: 'UY', label: 'Uruguay', phone: '598' },
{ code: 'UZ', label: 'Uzbekistan', phone: '998' },
{
code: 'VA',
label: 'Holy See (Vatican City State)',
phone: '379',
},
{
code: 'VC',
label: 'Saint Vincent and the Grenadines',
phone: '1-784',
},
{ code: 'VE', label: 'Venezuela', phone: '58' },
{
code: 'VG',
label: 'British Virgin Islands',
phone: '1-284',
},
{
code: 'VI',
label: 'US Virgin Islands',
phone: '1-340',
},
{ code: 'VN', label: 'Vietnam', phone: '84' },
{ code: 'VU', label: 'Vanuatu', phone: '678' },
{ code: 'WF', label: 'Wallis and Futuna', phone: '681' },
{ code: 'WS', label: 'Samoa', phone: '685' },
{ code: 'XK', label: 'Kosovo', phone: '383' },
{ code: 'YE', label: 'Yemen', phone: '967' },
{ code: 'YT', label: 'Mayotte', phone: '262' },
{ code: 'ZA', label: 'South Africa', phone: '27' },
{ code: 'ZM', label: 'Zambia', phone: '260' },
{ code: 'ZW', label: 'Zimbabwe', phone: '263' },
];
const countries = {};
const countriesOptions = [];
_countries.forEach((country) => {
countries[country.code] = country;
countriesOptions.push(country.code);
});
export default function CountrySelect({name, label, formik}) {
return (
<Grid item xs={12}>
<Autocomplete
id={name}
name={name}
multiple
options={countriesOptions}
autoHighlight
value={formik.values[name] || []}
onBlur={formik.handleBlur}
onChange={(event, value) => {
formik.setFieldValue(name, value)
}}
error={Boolean(formik.touched[name] && formik.errors[name])}
getOptionLabel={(option) => <div style={{verticalAlign: 'middle'}}><img
loading="lazy"
width="15"
style={{verticalAlign: 'middle'}}
height="10"
src={`https://flagcdn.com/w20/${option.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${option.toLowerCase()}.png 2x`}
alt=""
/> &nbsp;{countries[option].label}</div>}
renderOption={(props, option) => (
<Box component="li" sx={{ '& > img': { mr: 2, flexShrink: 0 } }} {...props}>
<img
loading="lazy"
width="20"
src={`https://flagcdn.com/w20/${option.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${option.toLowerCase()}.png 2x`}
alt=""
/>
{countries[option].label} ({option.code}) +{countries[option].phone}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label={label}
inputProps={{
...params.inputProps,
autoComplete: 'new-password', // disable autocomplete and autofill
}}
/>
)}
/>
</Grid>
);
}
export {
countries
};

View file

@ -16,7 +16,7 @@ const IsLoggedIn = () => useEffect(() => {
window.location.href = '/ui/newmfa';
}
}
});
})
}, []);
export default IsLoggedIn;

View file

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Link, Link as RouterLink } from 'react-router-dom';
// material-ui
import {
@ -30,6 +30,7 @@ import AnimateButton from '../../../components/@extended/AnimateButton';
// assets
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import { LoadingButton } from '@mui/lab';
// ============================|| FIREBASE - LOGIN ||============================ //
@ -182,10 +183,10 @@ const AuthLogin = () => {
/>
}
label={<Typography variant="h6">Keep me sign in</Typography>}
/>
<Link variant="h6" component={RouterLink} to="" color="text.primary">
/>*/}
<Link variant="h6" component={RouterLink} to="/ui/forgot-password" color="primary">
Forgot Password?
</Link> */}
</Link>
</Stack>
</Grid>
{errors.submit && (
@ -194,10 +195,9 @@ const AuthLogin = () => {
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
<LoadingButton
disableElevation
disabled={isSubmitting}
loading={isSubmitting}
fullWidth
size="large"
type="submit"
@ -205,8 +205,7 @@ const AuthLogin = () => {
color="primary"
>
Login
</Button>
</AnimateButton>
</LoadingButton>
</Grid>
{/* <Grid item xs={12}>
<Divider>

View file

@ -32,6 +32,7 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
// assets
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import { LoadingButton } from '@mui/lab';
// ============================|| FIREBASE - REGISTER ||============================ //
@ -178,10 +179,9 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
<LoadingButton
disableElevation
disabled={isSubmitting}
loading={isSubmitting}
fullWidth
size="large"
type="submit"
@ -191,8 +191,7 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
{
isRegister ? 'Register' : 'Reset Password'
}
</Button>
</AnimateButton>
</LoadingButton>
</Grid>
</Grid>
</form>

View file

@ -0,0 +1,122 @@
import { Link } from 'react-router-dom';
// material-ui
import { Button, FormHelperText, Grid, InputLabel, OutlinedInput, Stack, Typography } from '@mui/material';
// project import
import AuthWrapper from './AuthWrapper';
import { Formik } from 'formik';
// third-party
import * as Yup from 'yup';
import * as API from '../../api';
import { CosmosInputText } from '../config/users/formShortcuts';
import { useState } from 'react';
// ================================|| LOGIN ||================================ //
const ForgotPassword = () => {
const [isSuccess, setIsSuccess] = useState(false);
return (<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Password Reset</Typography>
{/* <Typography component={Link} to="/register" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
Don&apos;t have an account?
</Typography> */}
</Stack>
</Grid>
<Grid item xs={12}>
{!isSuccess && <Formik
initialValues={{
nickname: '',
email: '',
}}
validationSchema={Yup.object().shape({
nickname: Yup.string().max(255).required('Nickname is required'),
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'),
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
API.users.resetPassword(values).then((data) => {
if (data.status == 'error') {
setStatus({ success: false });
setErrors({ submit: 'Unexpected error. Check your infos or try again later.' });
setSubmitting(false);
return;
} else {
setStatus({ success: true });
setSubmitting(false);
setIsSuccess(true);
}
})
} catch (err) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<Grid container spacing={3}>
<CosmosInputText
name="nickname"
label="Nickname"
formik={formik}
/>
<CosmosInputText
name="email"
label="Email"
type="email"
formik={formik}
/>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<Button
disableElevation
disabled={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Reset Password
</Button>
</Grid>
</Grid>
</form>
)}
</Formik>}
{isSuccess && <div>
<Typography variant="h6">Check your email for a link to reset your password. If it doesnt appear within a few minutes, check your spam folder.</Typography>
<br/><br/>
<Button
disableElevation
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
component={Link}
to="/ui/login"
>
Back to login
</Button>
</div>}
</Grid>
</Grid>
</AuthWrapper>
)};
export default ForgotPassword;

View file

@ -28,7 +28,8 @@ import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import { CosmosCheckbox, CosmosInputText, CosmosSelect } from './formShortcuts';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputPassword, CosmosInputText, CosmosSelect } from './formShortcuts';
import CountrySelect, { countries } from '../../../components/countrySelect';
const ConfigManagement = () => {
@ -57,6 +58,7 @@ const ConfigManagement = () => {
MongoDB: config.MongoDB,
LoggingLevel: config.LoggingLevel,
RequireMFA: config.RequireMFA,
GeoBlocking: config.BlockedCountries,
Hostname: config.HTTPConfig.Hostname,
GenerateMissingTLSCert: config.HTTPConfig.GenerateMissingTLSCert,
@ -66,6 +68,14 @@ const ConfigManagement = () => {
SSLEmail: config.HTTPConfig.SSLEmail,
HTTPSCertificateMode: config.HTTPConfig.HTTPSCertificateMode,
DNSChallengeProvider: config.HTTPConfig.DNSChallengeProvider,
Email_Enabled: config.EmailConfig.Enabled,
Email_Host: config.EmailConfig.Host,
Email_Port: config.EmailConfig.Port,
Email_Username: config.EmailConfig.Username,
Email_Password: config.EmailConfig.Password,
Email_From: config.EmailConfig.From,
Email_UseTLS : config.EmailConfig.UseTLS,
}}
validationSchema={Yup.object().shape({
Hostname: Yup.string().max(255).required('Hostname is required'),
@ -79,6 +89,7 @@ const ConfigManagement = () => {
MongoDB: values.MongoDB,
LoggingLevel: values.LoggingLevel,
RequireMFA: values.RequireMFA,
BlockedCountries: values.GeoBlocking,
HTTPConfig: {
...config.HTTPConfig,
Hostname: values.Hostname,
@ -88,6 +99,16 @@ const ConfigManagement = () => {
SSLEmail: values.SSLEmail,
HTTPSCertificateMode: values.HTTPSCertificateMode,
DNSChallengeProvider: values.DNSChallengeProvider,
},
EmailConfig: {
...config.EmailConfig,
Enabled: values.Email_Enabled,
Host: values.Email_Host,
Port: values.Email_Port,
Username: values.Email_Username,
Password: values.Email_Password,
From: values.Email_From,
UseTLS: values.Email_UseTLS,
}
}
@ -116,245 +137,316 @@ const ConfigManagement = () => {
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<MainCard title="General">
<Grid container spacing={3}>
<Grid item xs={12}>
<Alert severity="info">This page allow you to edit the configuration file. Any Environment Variable overwritting configuration won't appear here.</Alert>
</Grid>
<CosmosCheckbox
label="Force Multi-Factor Authentication"
name="RequireMFA"
formik={formik}
helperText="Require MFA for all users"
/>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="MongoDB-login">MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional)</InputLabel>
<OutlinedInput
id="MongoDB-login"
type="password"
value={formik.values.MongoDB}
name="MongoDB"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="MongoDB"
fullWidth
error={Boolean(formik.touched.MongoDB && formik.errors.MongoDB)}
/>
{formik.touched.MongoDB && formik.errors.MongoDB && (
<FormHelperText error id="standard-weight-helper-text-MongoDB-login">
{formik.errors.MongoDB}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="LoggingLevel-login">Level of logging (Default: INFO)</InputLabel>
<TextField
className="px-2 my-2"
variant="outlined"
name="LoggingLevel"
id="LoggingLevel"
select
value={formik.values.LoggingLevel}
onChange={formik.handleChange}
error={
formik.touched.LoggingLevel &&
Boolean(formik.errors.LoggingLevel)
}
helperText={
formik.touched.LoggingLevel && formik.errors.LoggingLevel
}
>
<MenuItem key={"DEBUG"} value={"DEBUG"}>
DEBUG
</MenuItem>
<MenuItem key={"INFO"} value={"INFO"}>
INFO
</MenuItem>
<MenuItem key={"WARNING"} value={"WARNING"}>
WARNING
</MenuItem>
<MenuItem key={"ERROR"} value={"ERROR"}>
ERROR
</MenuItem>
</TextField>
</Stack>
</Grid>
</Grid>
</MainCard>
<br /><br />
<MainCard title="HTTP">
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="Hostname-login">Hostname: This will be used to restrict access to your Cosmos Server (Default: 0.0.0.0)</InputLabel>
<OutlinedInput
id="Hostname-login"
type="text"
value={formik.values.Hostname}
name="Hostname"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="Hostname"
fullWidth
error={Boolean(formik.touched.Hostname && formik.errors.Hostname)}
/>
{formik.touched.Hostname && formik.errors.Hostname && (
<FormHelperText error id="standard-weight-helper-text-Hostname-login">
{formik.errors.Hostname}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPPort-login">HTTP Port (Default: 80)</InputLabel>
<OutlinedInput
id="HTTPPort-login"
type="text"
value={formik.values.HTTPPort}
name="HTTPPort"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="HTTPPort"
fullWidth
error={Boolean(formik.touched.HTTPPort && formik.errors.HTTPPort)}
/>
{formik.touched.HTTPPort && formik.errors.HTTPPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPPort-login">
{formik.errors.HTTPPort}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPSPort-login">HTTPS Port (Default: 443)</InputLabel>
<OutlinedInput
id="HTTPSPort-login"
type="text"
value={formik.values.HTTPSPort}
name="HTTPSPort"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="HTTPSPort"
fullWidth
error={Boolean(formik.touched.HTTPSPort && formik.errors.HTTPSPort)}
/>
{formik.touched.HTTPSPort && formik.errors.HTTPSPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPSPort-login">
{formik.errors.HTTPSPort}
</FormHelperText>
)}
</Stack>
</Grid>
</Grid>
</MainCard>
<br /><br />
<MainCard title="Security Certificates">
<Stack spacing={3}>
<MainCard title="General">
<Grid container spacing={3}>
<Grid item xs={12}>
<Alert severity="info">For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them.</Alert>
<Grid item xs={12}>
<Alert severity="info">This page allow you to edit the configuration file. Any Environment Variable overwritting configuration won't appear here.</Alert>
</Grid>
<CosmosCheckbox
label="Force Multi-Factor Authentication"
name="RequireMFA"
formik={formik}
helperText="Require MFA for all users"
/>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="MongoDB-login">MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional)</InputLabel>
<OutlinedInput
id="MongoDB-login"
type="password"
value={formik.values.MongoDB}
name="MongoDB"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="MongoDB"
fullWidth
error={Boolean(formik.touched.MongoDB && formik.errors.MongoDB)}
/>
{formik.touched.MongoDB && formik.errors.MongoDB && (
<FormHelperText error id="standard-weight-helper-text-MongoDB-login">
{formik.errors.MongoDB}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="LoggingLevel-login">Level of logging (Default: INFO)</InputLabel>
<TextField
className="px-2 my-2"
variant="outlined"
name="LoggingLevel"
id="LoggingLevel"
select
value={formik.values.LoggingLevel}
onChange={formik.handleChange}
error={
formik.touched.LoggingLevel &&
Boolean(formik.errors.LoggingLevel)
}
helperText={
formik.touched.LoggingLevel && formik.errors.LoggingLevel
}
>
<MenuItem key={"DEBUG"} value={"DEBUG"}>
DEBUG
</MenuItem>
<MenuItem key={"INFO"} value={"INFO"}>
INFO
</MenuItem>
<MenuItem key={"WARNING"} value={"WARNING"}>
WARNING
</MenuItem>
<MenuItem key={"ERROR"} value={"ERROR"}>
ERROR
</MenuItem>
</TextField>
</Stack>
</Grid>
</Grid>
</MainCard>
<CosmosSelect
name="HTTPSCertificateMode"
label="HTTPS Certificates"
formik={formik}
options={[
["LETSENCRYPT", "Automatically generate certificates using Let's Encrypt (Recommended)"],
["SELFSIGNED", "Locally self-sign certificates (unsecure)"],
["PROVIDED", "I have my own certificates"],
["DISABLED", "Do not use HTTPS (very unsecure)"],
]}
/>
<MainCard title="HTTP">
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="Hostname-login">Hostname: This will be used to restrict access to your Cosmos Server (Default: 0.0.0.0)</InputLabel>
<OutlinedInput
id="Hostname-login"
type="text"
value={formik.values.Hostname}
name="Hostname"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="Hostname"
fullWidth
error={Boolean(formik.touched.Hostname && formik.errors.Hostname)}
/>
{formik.touched.Hostname && formik.errors.Hostname && (
<FormHelperText error id="standard-weight-helper-text-Hostname-login">
{formik.errors.Hostname}
</FormHelperText>
)}
</Stack>
</Grid>
{
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPPort-login">HTTP Port (Default: 80)</InputLabel>
<OutlinedInput
id="HTTPPort-login"
type="text"
value={formik.values.HTTPPort}
name="HTTPPort"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="HTTPPort"
fullWidth
error={Boolean(formik.touched.HTTPPort && formik.errors.HTTPPort)}
/>
{formik.touched.HTTPPort && formik.errors.HTTPPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPPort-login">
{formik.errors.HTTPPort}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPSPort-login">HTTPS Port (Default: 443)</InputLabel>
<OutlinedInput
id="HTTPSPort-login"
type="text"
value={formik.values.HTTPSPort}
name="HTTPSPort"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="HTTPSPort"
fullWidth
error={Boolean(formik.touched.HTTPSPort && formik.errors.HTTPSPort)}
/>
{formik.touched.HTTPSPort && formik.errors.HTTPSPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPSPort-login">
{formik.errors.HTTPSPort}
</FormHelperText>
)}
</Stack>
</Grid>
</Grid>
</MainCard>
<MainCard title="Emails - SMTP">
<Stack spacing={2}>
<Alert severity="info">This allow you to setup an SMTP server for Cosmos to send emails such as password reset emails and invites.</Alert>
<CosmosCheckbox
label="Enable SMTP"
name="Email_Enabled"
formik={formik}
helperText="Enable SMTP"
/>
{formik.values.Email_Enabled && (<>
<CosmosInputText
name="SSLEmail"
label="Email address for Let's Encrypt"
label="SMTP Host"
name="Email_Host"
formik={formik}
helperText="SMTP Host"
/>
)
}
{
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<CosmosInputText
name="DNSChallengeProvider"
label="DNS provider (if you are using a DNS Challenge)"
label="SMTP Port"
name="Email_Port"
formik={formik}
helperText="SMTP Port"
/>
)
}
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name="GenerateMissingAuthCert"
as={FormControlLabel}
control={<Checkbox size="large" />}
label="Generate missing Authentication Certificates automatically (Default: true)"
<CosmosInputText
label="SMTP Username"
name="Email_Username"
formik={formik}
helperText="SMTP Username"
/>
</Stack>
</Grid>
<CosmosInputPassword
label="SMTP Password"
name="Email_Password"
formik={formik}
helperText="SMTP Password"
noStrength
/>
<CosmosInputText
label="SMTP From"
name="Email_From"
formik={formik}
helperText="SMTP From"
/>
<CosmosCheckbox
label="SMTP Uses TLS"
name="Email_UseTLS"
formik={formik}
helperText="SMTP Uses TLS"
/>
</>)}
</Stack>
</MainCard>
<MainCard title="Security">
<Grid container spacing={3}>
<CosmosFormDivider title='Geo-Blocking' />
<Grid item xs={12}>
<InputLabel htmlFor="GeoBlocking">Geo-Blocking: (Those countries will be blocked from accessing your server)</InputLabel>
</Grid>
<CountrySelect name="GeoBlocking" label="Choose which countries you want to block" formik={formik} />
<Grid item xs={12}>
<Button onClick={() => {
formik.setFieldValue("GeoBlocking", ["CN","RU","TR","BR","BD","IN","NP","PK","LK","VN","ID","IR","IQ","EG","AF","RO",])
}} variant="outlined">Reset to default (most dangerous countries)</Button>
</Grid>
<CosmosFormDivider title='Encryption' />
<Grid item xs={12}>
<Alert severity="info">For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them.</Alert>
</Grid>
<CosmosSelect
name="HTTPSCertificateMode"
label="HTTPS Certificates"
formik={formik}
options={[
["LETSENCRYPT", "Automatically generate certificates using Let's Encrypt (Recommended)"],
["SELFSIGNED", "Locally self-sign certificates (unsecure)"],
["PROVIDED", "I have my own certificates"],
["DISABLED", "Do not use HTTPS (very unsecure)"],
]}
/>
{
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<CosmosInputText
name="SSLEmail"
label="Email address for Let's Encrypt"
formik={formik}
/>
)
}
{
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<CosmosInputText
name="DNSChallengeProvider"
label="DNS provider (if you are using a DNS Challenge)"
formik={formik}
/>
)
}
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name="GenerateMissingAuthCert"
as={FormControlLabel}
control={<Checkbox size="large" />}
label="Generate missing Authentication Certificates automatically (Default: true)"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<h4>Authentication Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.AuthPublicKey}
</pre>
</Stack>
</Grid>
<Grid item xs={12}>
<h4>Root HTTPS Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.TLSCert}
</pre>
</Stack>
</Grid>
</Grid>
</MainCard>
<MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<h4>Authentication Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.AuthPublicKey}
</pre>
</Stack>
<AnimateButton>
<Button
disableElevation
disabled={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button>
</AnimateButton>
</Grid>
<Grid item xs={12}>
<h4>Root HTTPS Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.TLSCert}
</pre>
</Stack>
</Grid>
</Grid>
</MainCard>
<br /><br />
<MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
disableElevation
disabled={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button>
</AnimateButton>
</Grid>
</MainCard>
</MainCard>
</Stack>
</form>
)}
</Formik>

View file

@ -52,7 +52,7 @@ export const CosmosInputText = ({ name, style, multiline, type, placeholder, onC
</Grid>
}
export const CosmosInputPassword = ({ name, type, placeholder, onChange, label, formik }) => {
export const CosmosInputPassword = ({ name, noStrength, type, placeholder, onChange, label, formik }) => {
const [level, setLevel] = React.useState();
const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => {
@ -108,7 +108,7 @@ export const CosmosInputPassword = ({ name, type, placeholder, onChange, label,
</FormHelperText>
)}
<FormControl fullWidth sx={{ mt: 2 }}>
{!noStrength && <FormControl fullWidth sx={{ mt: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Box sx={{ bgcolor: level?.color, width: 85, height: 8, borderRadius: '7px' }} />
@ -119,7 +119,7 @@ export const CosmosInputPassword = ({ name, type, placeholder, onChange, label,
</Typography>
</Grid>
</Grid>
</FormControl>
</FormControl>}
</Stack>
</Grid>
}

View file

@ -30,6 +30,7 @@ const UserManagement = () => {
const [openDeleteForm, setOpenDeleteForm] = React.useState(false);
const [openInviteForm, setOpenInviteForm] = React.useState(false);
const [toAction, setToAction] = React.useState(null);
const [loadingRow, setLoadingRow] = React.useState(null);
const roles = ['Guest', 'User', 'Admin']
@ -39,6 +40,7 @@ const UserManagement = () => {
setIsLoading(true);
API.users.list()
.then(data => {
setLoadingRow(null);
setRows(data.data);
setIsLoading(false);
})
@ -50,30 +52,45 @@ const UserManagement = () => {
function sendlink(nickname, formType) {
API.users.invite({
nickname
nickname,
formType: ""+formType,
})
.then((values) => {
let sendLink = window.location.origin + '/ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
setToAction({...values.data, nickname, sendLink, formType});
setToAction({...values.data, nickname, sendLink, formType, formAction: formType === 2 ? 'invite them to the server' : 'let them reset their password'});
setOpenInviteForm(true);
});
}
return <>
<IsLoggedIn />
{openInviteForm ? <Dialog open={openInviteForm} onClose={() => setOpenInviteForm(false)}>
<IsLoggedIn />
<DialogTitle>Invite User</DialogTitle>
<DialogContent>
<DialogContentText>
Send this link to {toAction.nickname} to invite them to the system:
<div style={{
paddingBottom: '15px',
maxWidth: '350px',
}}>
{toAction.emailWasSent ?
<div>
<strong>An email has been sent</strong> with a link to {toAction.formAction}. Alternatively you can also share the link below:
</div> :
<div>
Send this link to {toAction.nickname} to {toAction.formAction}:
</div>
}
</div>
<div>
<div style={{float: 'left', width: '300px', padding: '5px', background:'rgba(0,0,0,0.15)', whiteSpace: 'nowrap', wordBreak: 'keep-all', overflow: 'auto', fontStyle: 'italic'}}>{toAction.sendLink}</div>
<IconButton size="large" style={{float: 'left'}} aria-label="copy" onClick={
() => {
navigator.clipboard.writeText(toAction.sendLink);
}
}>
<CopyOutlined />
</IconButton><div style={{float: 'left', width: '300px', padding: '5px', background:'rgba(0,0,0,0.15)', whiteSpace: 'nowrap', wordBreak: 'keep-all', overflow: 'auto', fontStyle: 'italic'}}>{toAction.sendLink}</div>
</IconButton>
</div>
</DialogContentText>
</DialogContent>
@ -215,26 +232,34 @@ const UserManagement = () => {
const isRegistered = new Date(r.registeredAt).getTime() > 0;
const inviteExpired = new Date(r.registerKeyExp).getTime() < new Date().getTime();
if (loadingRow === r.nickname) {
return <div style={{textAlign: 'center'}}><CircularProgress /></div>
}
return <>{isRegistered ?
(<Button variant="contained" color="primary" onClick={
() => {
setLoadingRow(r.nickname);
sendlink(r.nickname, 1);
}
}>Send password reset</Button>) :
(<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
() => {
setLoadingRow(r.nickname);
sendlink(r.nickname, 2);
}
} color="primary">Re-Send Invite</Button>)
}
&nbsp;&nbsp;<Button variant="contained" color="error" onClick={
() => {
setLoadingRow(r.nickname);
setToAction(r.nickname);
setOpenDeleteForm(true);
}
}>Delete</Button>
&nbsp;&nbsp;<Button variant="contained" color="error" onClick={
() => {
setLoadingRow(r.nickname);
API.users.reset2FA(r.nickname).then(() => {
refresh();
});

View file

@ -8,6 +8,7 @@ import Logout from '../pages/authentication/Logoff';
import NewInstall from '../pages/newInstall/newInstall';
import {NewMFA, MFALogin} from '../pages/authentication/newMFA';
import ForgotPassword from '../pages/authentication/forgotPassword';
// render - login
const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login')));
@ -43,6 +44,10 @@ const LoginRoutes = {
path: '/ui/loginmfa',
element: <MFALogin />
},
{
path: '/ui/forgot-password',
element: <ForgotPassword />
},
]
};

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.3.0-unstable",
"version": "0.3.0-unstable4",
"description": "",
"main": "test-server.js",
"bugs": {

View file

@ -208,6 +208,7 @@ func StartServer() {
srapi.HandleFunc("/api/invite", user.UserResendInviteLink)
srapi.HandleFunc("/api/me", user.Me)
srapi.HandleFunc("/api/mfa", user.API2FA)
srapi.HandleFunc("/api/password-reset", user.ResetPassword)
srapi.HandleFunc("/api/config", configapi.ConfigRoute)
srapi.HandleFunc("/api/restart", configapi.ConfigApiRestart)

46
src/user/emails.go Normal file
View file

@ -0,0 +1,46 @@
package user
import (
"fmt"
"github.com/azukaar/cosmos-server/src/utils"
)
func SendInviteEmail(nickname string, email string, link string) error {
return utils.SendEmail(
[]string{email},
"Cosmos Invitation for " + nickname,
fmt.Sprintf(`<h1>You have been invited!</h1>
Hello %s, <br>
The admin of a Cosmos Server invited you to join their server. <br>
In order to join, you can click the following link to setup your account: <br>
<a class="button" href="%s">Setup</a> <br><br>
See you soon!! <br>
`, nickname, link))
}
func SendAdminPasswordEmail(nickname string, email string, link string) error {
return utils.SendEmail(
[]string{email},
"Cosmos Password Reset",
fmt.Sprintf(`<h1>Password Reset</h1>
Hello %s, <br>
The admin of a Cosmos Server has sent you a password reset link. <br>
In order to reset your password, you can click the following link and fill in the form: <br>
<a class="button" href="%s">Reset Password</a> <br><br>
See you soon!! <br>
`, nickname, link))
}
func SendPasswordEmail(nickname string, email string, link string) error {
return utils.SendEmail(
[]string{email},
"Cosmos Password Reset",
fmt.Sprintf(`<h1>Password Reset</h1>
Hello %s, <br>
You have requested a password reset. If it wasn't you, please alert your server admin. <br>
If it was you, you can click the following link and fill in the form: <br>
<a class="button" href="%s">Reset Password</a> <br><br>
See you soon!! <br>
`, nickname, link))
}

View file

@ -0,0 +1,98 @@
package user
import (
"net/http"
"encoding/json"
"time"
"math/rand"
"github.com/azukaar/cosmos-server/src/utils"
)
type PasswordResetRequestJSON struct {
Nickname string `validate:"required,min=3,max=32,alphanum"`
Email string `validate:"required,min=3,max=32,alphanum"`
}
func ResetPassword(w http.ResponseWriter, req *http.Request) {
if(req.Method == "POST") {
if !utils.IsEmailEnabled() {
utils.HTTPError(w, "Email is not enabled", http.StatusInternalServerError, "PR007")
return
}
time.Sleep(time.Duration(rand.Float64()*2)*time.Second)
var request PasswordResetRequestJSON
err1 := json.NewDecoder(req.Body).Decode(&request)
if err1 != nil {
utils.Error("PasswordReset: Invalid User Request", err1)
utils.HTTPError(w, "User Send Invite Error", http.StatusInternalServerError, "PR001")
return
}
nickname := utils.Sanitize(request.Nickname)
utils.Debug("Sending password reset to: " + nickname)
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
if errCo != nil {
utils.Error("Database Connect", errCo)
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
return
}
user := utils.User{}
err := c.FindOne(nil, map[string]interface{}{
"Nickname": nickname,
"Email": request.Email,
}).Decode(&user)
if err != nil {
utils.Error("PasswordReset: Error while finding user", err)
utils.HTTPError(w, "User Send Invite Error", http.StatusInternalServerError, "PR001")
return
} else {
RegisterKeyExp := time.Now().Add(time.Hour * 24 * 7)
RegisterKey := utils.GenerateRandomString(48)
utils.Debug(RegisterKey)
utils.Debug(RegisterKeyExp.String())
_, err := c.UpdateOne(nil, map[string]interface{}{
"Nickname": nickname,
}, map[string]interface{}{
"$set": map[string]interface{}{
"RegisterKeyExp": RegisterKeyExp,
"RegisterKey": RegisterKey,
},
})
if err != nil {
utils.Error("PasswordReset: Error while updating user", err)
utils.HTTPError(w, "User Send Invite Error", http.StatusInternalServerError, "PR001")
return
}
utils.Debug("Sending an email to " + user.Email)
url := utils.GetServerURL() + ("ui/register?t=1&nickname="+user.Nickname+"&key=" + RegisterKey)
errEm := SendPasswordEmail(user.Nickname, user.Email, url)
if errEm != nil {
utils.Error("PasswordReset: Error while sending email", errEm)
utils.HTTPError(w, "User Send Invite Error", http.StatusInternalServerError, "PR002")
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
}
} else {
utils.Error("PasswordReset: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -84,7 +84,6 @@ func UserRegister(w http.ResponseWriter, req *http.Request) {
_, err4 := c.UpdateOne(nil, map[string]interface{}{
"Nickname": nickname,
"RegisterKey": registerKey,
"Password": "",
}, map[string]interface{}{
"$set": map[string]interface{}{
"Password": hashedPassword,

View file

@ -11,6 +11,7 @@ import (
type InviteRequestJSON struct {
Nickname string `validate:"required,min=3,max=32,alphanum"`
FormType string
}
func UserResendInviteLink(w http.ResponseWriter, req *http.Request) {
@ -25,7 +26,7 @@ func UserResendInviteLink(w http.ResponseWriter, req *http.Request) {
nickname := utils.Sanitize(request.Nickname)
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
if utils.AdminOnly(w, req) != nil {
return
}
@ -40,8 +41,6 @@ func UserResendInviteLink(w http.ResponseWriter, req *http.Request) {
user := utils.User{}
// TODO: If not logged in as Admin, check email too
err := c.FindOne(nil, map[string]interface{}{
"Nickname": nickname,
}).Decode(&user)
@ -76,13 +75,32 @@ func UserResendInviteLink(w http.ResponseWriter, req *http.Request) {
return
}
// TODO: Only send registerKey if logged in already
emailWasSent := false
if utils.IsEmailEnabled() && user.Email != "" {
utils.Debug("Sending an email to " + user.Email)
url := utils.GetServerURL() + ("ui/register?t="+request.FormType+"&nickname="+user.Nickname+"&key=" + RegisterKey)
var errEm error
if request.FormType == "2" {
errEm = SendInviteEmail(user.Nickname, user.Email, url)
} else {
errEm = SendAdminPasswordEmail(user.Nickname, user.Email, url)
}
if errEm != nil {
utils.Error("UserInvite: Error while sending email", errEm)
}
emailWasSent = true
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": map[string]interface{}{
"registerKey": RegisterKey,
"registerKeyExp": RegisterKeyExp,
"emailWasSent": emailWasSent,
},
})
}

151
src/utils/emails.go Normal file
View file

@ -0,0 +1,151 @@
package utils
import (
"crypto/tls"
"fmt"
"net/smtp"
"strings"
)
var Template = `From: %s
To: %s
Subject: %s
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
}
.container {
max-width: 500px;
margin: auto;
margin-top: 30px;
padding: 20px;
}
.header {
text-align: center;
padding-bottom: 10px;
}
.logo {
width: 130px;
}
.content {
background-color: #f4f4f4;
padding: 20px;
border-radius: 5px;
border-top: 2px solid rgb(171, 71, 188);
}
.footer {
padding-top: 20px;
color: #999;
}
h1 {
color: rgb(171, 71, 188);
}
.button {
background: rgb(171, 71, 188);
color: white !important;
padding: 10px 20px;
border-radius: 5px;
display: inline-block;
margin-top: 15px;
text-decoration: none;
}
.button:hover {
background: rgb(141, 41, 168);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="%s" alt="Logo" class="logo">
</div>
<div class="content">
%s
</div>
<div class="footer">
Sent from: %s
</div>
</div>
</body>
</html>`
func IsEmailEnabled() bool {
config := GetMainConfig()
return config.EmailConfig.Enabled
}
func SendEmail(recipients []string, subject string, body string) error {
config := GetMainConfig()
hostPort := config.EmailConfig.Host + ":" + config.EmailConfig.Port
auth := smtp.PlainAuth("", config.EmailConfig.Username, config.EmailConfig.Password, config.EmailConfig.Host)
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: config.EmailConfig.Host,
}
ServerURL := GetServerURL()
LogoURL := ServerURL + "/ui/assets/Logo.png"
send := func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
c, err := smtp.Dial(addr)
if err != nil {
return err
}
defer c.Close()
if config.EmailConfig.UseTLS {
if err = c.StartTLS(tlsConfig); err != nil {
return err
}
}
if err = c.Auth(a); err != nil {
return err
}
if err = c.Mail(from); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = w.Write(msg)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
msg := []byte(fmt.Sprintf(
Template,
config.EmailConfig.From,
strings.Join(recipients, ","),
subject,
LogoURL,
body,
ServerURL,
))
return send(hostPort, auth, config.EmailConfig.From, recipients, msg)
}

View file

@ -69,7 +69,6 @@ func SetSecurityHeaders(next http.Handler) http.Handler {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("X-Served-By-Cosmos", "1")
w.Header().Set("Referrer-Policy", "no-referrer")
next.ServeHTTP(w, r)
})

View file

@ -79,6 +79,7 @@ type Config struct {
DisableUserManagement bool
NewInstall bool `validate:"boolean"`
HTTPConfig HTTPConfig `validate:"required,dive,required"`
EmailConfig EmailConfig `validate:"required,dive,required"`
DockerConfig DockerConfig
BlockedCountries []string
ServerCountry string
@ -144,3 +145,13 @@ type ProxyRouteConfig struct {
BlockCommonBots bool
BlockAPIAbuse bool
}
type EmailConfig struct {
Enabled bool
Host string
Port string
Username string
Password string
From string
UseTLS bool
}

View file

@ -338,4 +338,25 @@ func StringArrayContains(a []string, b string) bool {
}
}
return false
}
func GetServerURL() string {
ServerURL := ""
if IsHTTPS {
ServerURL += "https://"
} else {
ServerURL += "http://"
}
ServerURL += MainConfig.HTTPConfig.Hostname
if IsHTTPS && MainConfig.HTTPConfig.HTTPSPort != "443" {
ServerURL += ":" + MainConfig.HTTPConfig.HTTPSPort
}
if !IsHTTPS && MainConfig.HTTPConfig.HTTPPort != "80" {
ServerURL += ":" + MainConfig.HTTPConfig.HTTPPort
}
return ServerURL + "/"
}