Compare commits
1 commit
master
...
v0.3.0-uns
Author | SHA1 | Date | |
---|---|---|---|
![]() |
858dfa70e1 |
21 changed files with 1349 additions and 256 deletions
1
build.sh
1
build.sh
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
493
client/src/components/countrySelect.jsx
Normal file
493
client/src/components/countrySelect.jsx
Normal 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=""
|
||||
/> {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
|
||||
};
|
|
@ -16,7 +16,7 @@ const IsLoggedIn = () => useEffect(() => {
|
|||
window.location.href = '/ui/newmfa';
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}, []);
|
||||
|
||||
export default IsLoggedIn;
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
122
client/src/pages/authentication/forgotPassword.jsx
Normal file
122
client/src/pages/authentication/forgotPassword.jsx
Normal 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'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 doesn’t 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;
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>)
|
||||
}
|
||||
<Button variant="contained" color="error" onClick={
|
||||
() => {
|
||||
setLoadingRow(r.nickname);
|
||||
setToAction(r.nickname);
|
||||
setOpenDeleteForm(true);
|
||||
}
|
||||
}>Delete</Button>
|
||||
<Button variant="contained" color="error" onClick={
|
||||
() => {
|
||||
setLoadingRow(r.nickname);
|
||||
API.users.reset2FA(r.nickname).then(() => {
|
||||
refresh();
|
||||
});
|
||||
|
|
|
@ -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 />
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.3.0-unstable",
|
||||
"version": "0.3.0-unstable2",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
|
|
@ -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
46
src/user/emails.go
Normal 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))
|
||||
}
|
98
src/user/password_reset.go
Normal file
98
src/user/password_reset.go
Normal 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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
153
src/utils/emails.go
Normal file
153
src/utils/emails.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
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"
|
||||
|
||||
LogoURL = "https://yann-server.com/ui/assets/cosmos-a35d8a5e.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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 + "/"
|
||||
}
|
Loading…
Add table
Reference in a new issue