Webatrice: KnownHosts component (#4456)
* refactor dexie services for future schema updates Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
parent
37879c4255
commit
6ce346af4a
54 changed files with 1381 additions and 1291 deletions
1507
webclient/package-lock.json
generated
1507
webclient/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,12 +8,14 @@
|
||||||
"@material-ui/styles": "^4.11.4",
|
"@material-ui/styles": "^4.11.4",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dexie": "^3.0.3",
|
"dexie": "^3.0.3",
|
||||||
|
"final-form": "^4.20.4",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"protobufjs": "^6.11.2",
|
"protobufjs": "^6.11.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-final-form": "^6.5.7",
|
||||||
"react-redux": "^7.2.6",
|
"react-redux": "^7.2.6",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
|
|
|
@ -1,29 +1,28 @@
|
||||||
import { StatusEnum, User } from 'types';
|
import { StatusEnum, User, WebSocketConnectReason, WebSocketConnectOptions } from 'types';
|
||||||
import { SessionCommands, webClient } from 'websocket';
|
import { SessionCommands, webClient } from 'websocket';
|
||||||
import { WebSocketConnectReason, WebSocketOptions } from '../websocket/services/WebSocketService';
|
|
||||||
|
|
||||||
export default class AuthenticationService {
|
export default class AuthenticationService {
|
||||||
static connect(options: WebSocketOptions): void {
|
static login(options: WebSocketConnectOptions): void {
|
||||||
SessionCommands.connect(options, WebSocketConnectReason.LOGIN);
|
SessionCommands.connect(options, WebSocketConnectReason.LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
static register(options: WebSocketOptions): void {
|
static register(options: WebSocketConnectOptions): void {
|
||||||
SessionCommands.connect(options, WebSocketConnectReason.REGISTER);
|
SessionCommands.connect(options, WebSocketConnectReason.REGISTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
static activateAccount(options: WebSocketOptions): void {
|
static activateAccount(options: WebSocketConnectOptions): void {
|
||||||
SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT);
|
SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
static resetPasswordRequest(options: WebSocketOptions): void {
|
static resetPasswordRequest(options: WebSocketConnectOptions): void {
|
||||||
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_REQUEST);
|
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
static resetPasswordChallenge(options: WebSocketOptions): void {
|
static resetPasswordChallenge(options: WebSocketConnectOptions): void {
|
||||||
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE);
|
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
static resetPassword(options: WebSocketOptions): void {
|
static resetPassword(options: WebSocketConnectOptions): void {
|
||||||
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET);
|
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,4 +43,8 @@ export default class AuthenticationService {
|
||||||
static isAdmin() {
|
static isAdmin() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static connectionAttemptMade() {
|
||||||
|
return webClient.connectionAttemptMade;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
.dialog-title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
|
@ -13,14 +13,13 @@ import MenuRoundedIcon from '@material-ui/icons/MenuRounded';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { AuthenticationService, RoomsService } from 'api';
|
import { AuthenticationService, RoomsService } from 'api';
|
||||||
|
import { CardImportDialog } from 'dialogs';
|
||||||
import { Images } from 'images';
|
import { Images } from 'images';
|
||||||
import { RoomsSelectors, ServerSelectors } from 'store';
|
import { RoomsSelectors, ServerSelectors } from 'store';
|
||||||
import { Room, RouteEnum, User } from 'types';
|
import { Room, RouteEnum, User } from 'types';
|
||||||
|
|
||||||
import './Header.css';
|
import './Header.css';
|
||||||
|
|
||||||
import CardImportDialog from '../CardImportDialog/CardImportDialog';
|
|
||||||
|
|
||||||
class Header extends Component<HeaderProps> {
|
class Header extends Component<HeaderProps> {
|
||||||
state: HeaderState;
|
state: HeaderState;
|
||||||
options: string[] = [
|
options: string[] = [
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
.inputField {
|
.InputField {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField-validation {
|
.InputField-validation {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -10,11 +10,11 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField-error {
|
.InputField-error {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField-error svg {
|
.InputField-error svg {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,38 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { styled } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import TextField from '@material-ui/core/TextField';
|
import TextField from '@material-ui/core/TextField';
|
||||||
import ErrorOutlinedIcon from '@material-ui/icons/ErrorOutlined';
|
import ErrorOutlinedIcon from '@material-ui/icons/ErrorOutlined';
|
||||||
|
|
||||||
import './InputField.css';
|
import './InputField.css';
|
||||||
|
|
||||||
const InputField = ({ input, label, name, autoComplete, type, meta: { touched, error, warning } }) => (
|
const useStyles = makeStyles(theme => ({
|
||||||
<div className="inputField">
|
root: {
|
||||||
|
'& .InputField-error': {
|
||||||
|
color: theme.palette.error.main
|
||||||
|
},
|
||||||
|
|
||||||
|
'& .InputField-warning': {
|
||||||
|
color: theme.palette.warning.main
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const InputField = ({ input, label, name, autoComplete, type, meta: { touched, error, warning } }) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'InputField ' + classes.root}>
|
||||||
{ touched && (
|
{ touched && (
|
||||||
<div className="inputField-validation">
|
<div className="InputField-validation">
|
||||||
{
|
{
|
||||||
(error &&
|
(error &&
|
||||||
<ThemedFieldError className="inputField-error">
|
<div className="InputField-error">
|
||||||
{error}
|
{error}
|
||||||
<ErrorOutlinedIcon style={{ fontSize: 'small', fontWeight: 'bold' }} />
|
<ErrorOutlinedIcon style={{ fontSize: 'small', fontWeight: 'bold' }} />
|
||||||
</ThemedFieldError>
|
</div>
|
||||||
) ||
|
) ||
|
||||||
|
|
||||||
(warning && <ThemedFieldWarning className="inputField-warning">{warning}</ThemedFieldWarning>)
|
(warning && <div className="InputField-warning">{warning}</div>)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
) }
|
) }
|
||||||
|
@ -34,14 +49,7 @@ const InputField = ({ input, label, name, autoComplete, type, meta: { touched, e
|
||||||
{ ...input }
|
{ ...input }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
const ThemedFieldError = styled('div')(({ theme }) => ({
|
|
||||||
color: theme.palette.error.main
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ThemedFieldWarning = styled('div')(({ theme }) => ({
|
|
||||||
color: theme.palette.warning.main
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default InputField;
|
export default InputField;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
.KnownHosts {
|
.KnownHosts {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.KnownHosts-item__label {
|
.KnownHosts-item__label {
|
||||||
|
@ -15,6 +16,27 @@
|
||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.KnownHosts-validation {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.KnownHosts-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.KnownHosts-error svg {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.Mui-selected .KnownHosts-item__label svg {
|
.Mui-selected .KnownHosts-item__label svg {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MuiSelect-selectMenu .KnownHosts-item__edit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -1,77 +1,192 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Select, MenuItem } from '@material-ui/core';
|
import { Select, MenuItem } from '@material-ui/core';
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import FormControl from '@material-ui/core/FormControl';
|
import FormControl from '@material-ui/core/FormControl';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
import InputLabel from '@material-ui/core/InputLabel';
|
import InputLabel from '@material-ui/core/InputLabel';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import Check from '@material-ui/icons/Check';
|
import Check from '@material-ui/icons/Check';
|
||||||
|
import AddIcon from '@material-ui/icons/Add';
|
||||||
import EditRoundedIcon from '@material-ui/icons/Edit';
|
import EditRoundedIcon from '@material-ui/icons/Edit';
|
||||||
|
import ErrorOutlinedIcon from '@material-ui/icons/ErrorOutlined';
|
||||||
|
|
||||||
|
import { KnownHostDialog } from 'dialogs';
|
||||||
import { HostDTO } from 'services';
|
import { HostDTO } from 'services';
|
||||||
import { DefaultHosts, getHostPort } from 'types';
|
import { DefaultHosts, Host, getHostPort } from 'types';
|
||||||
|
|
||||||
import './KnownHosts.css';
|
import './KnownHosts.css';
|
||||||
|
|
||||||
const KnownHosts = ({ onChange }) => {
|
const useStyles = makeStyles(theme => ({
|
||||||
const [state, setState] = useState({
|
root: {
|
||||||
|
'& .KnownHosts-error': {
|
||||||
|
color: theme.palette.error.main
|
||||||
|
},
|
||||||
|
|
||||||
|
'& .KnownHosts-warning': {
|
||||||
|
color: theme.palette.warning.main
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const KnownHosts = ({ input: { onChange }, meta: { touched, error, warning } }) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [hostsState, setHostsState] = useState({
|
||||||
hosts: [],
|
hosts: [],
|
||||||
selectedHost: 0,
|
selectedHost: {} as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const [dialogState, setDialogState] = useState({
|
||||||
HostDTO.getAll().then(async hosts => {
|
open: false,
|
||||||
if (hosts?.length) {
|
edit: null,
|
||||||
setState(s => ({ ...s, hosts }));
|
|
||||||
} else {
|
|
||||||
setState(s => ({ ...s, hosts: DefaultHosts }));
|
|
||||||
await HostDTO.bulkAdd(DefaultHosts);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loadKnownHosts = useCallback(async () => {
|
||||||
|
const hosts = await HostDTO.getAll();
|
||||||
|
|
||||||
|
if (!hosts?.length) {
|
||||||
|
// @TODO: find a better pattern to seeding default data in indexedDB
|
||||||
|
await HostDTO.bulkAdd(DefaultHosts);
|
||||||
|
loadKnownHosts();
|
||||||
|
} else {
|
||||||
|
const selectedHost = hosts.find(({ lastSelected }) => lastSelected) || hosts[0];
|
||||||
|
setHostsState(s => ({ ...s, hosts, selectedHost }));
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.hosts.length) {
|
loadKnownHosts();
|
||||||
onChange(getHostPort(state.hosts[state.selectedHost]));
|
}, [loadKnownHosts]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { hosts, selectedHost } = hostsState;
|
||||||
|
|
||||||
|
if (selectedHost?.id) {
|
||||||
|
updateLastSelectedHost(selectedHost.id).then(() => {
|
||||||
|
onChange(selectedHost);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [state, onChange]);
|
}, [hostsState, onChange]);
|
||||||
|
|
||||||
const selectHost = (selectedHost) => {
|
const selectHost = (selectedHost) => {
|
||||||
setState(s => ({ ...s, selectedHost }));
|
setHostsState(s => ({ ...s, selectedHost }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const addKnownHost = () => {
|
const openAddKnownHostDialog = () => {
|
||||||
console.log('KnownHosts->addKnownHost');
|
setDialogState(s => ({ ...s, open: true, edit: null }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const editKnownHost = (hostIndex) => {
|
const openEditKnownHostDialog = (host: HostDTO) => {
|
||||||
console.log('KnownHosts->editKnownHost: ', state.hosts[hostIndex]);
|
setDialogState(s => ({ ...s, open: true, edit: host }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeKnownHostDialog = () => {
|
||||||
|
setDialogState(s => ({ ...s, open: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDialogRemove = async ({ id }) => {
|
||||||
|
setHostsState(s => ({
|
||||||
|
...s,
|
||||||
|
hosts: s.hosts.filter(host => host.id !== id),
|
||||||
|
selectedHost: s.selectedHost.id === id ? s.hosts[0] : s.selectedHost,
|
||||||
|
}));
|
||||||
|
|
||||||
|
closeKnownHostDialog();
|
||||||
|
HostDTO.delete(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogSubmit = async ({ id, name, host, port }) => {
|
||||||
|
if (id) {
|
||||||
|
const hostDTO = await HostDTO.get(id);
|
||||||
|
hostDTO.name = name;
|
||||||
|
hostDTO.host = host;
|
||||||
|
hostDTO.port = port;
|
||||||
|
await hostDTO.save();
|
||||||
|
|
||||||
|
setHostsState(s => ({
|
||||||
|
...s,
|
||||||
|
hosts: s.hosts.map(h => h.id === id ? hostDTO : h),
|
||||||
|
selectedHost: hostDTO
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const newHost: Host = { name, host, port, editable: true };
|
||||||
|
newHost.id = await HostDTO.add(newHost) as number;
|
||||||
|
|
||||||
|
setHostsState(s => ({
|
||||||
|
...s,
|
||||||
|
hosts: [...s.hosts, newHost],
|
||||||
|
selectedHost: newHost,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
closeKnownHostDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLastSelectedHost = (hostId): Promise<any[]> => {
|
||||||
|
return HostDTO.getAll().then(hosts =>
|
||||||
|
hosts.map(async host => {
|
||||||
|
if (host.id === hostId) {
|
||||||
|
host.lastSelected = true;
|
||||||
|
return await host.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.lastSelected) {
|
||||||
|
host.lastSelected = false;
|
||||||
|
return await host.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return host;
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl variant='outlined' className='KnownHosts'>
|
<div>
|
||||||
|
<FormControl variant='outlined' className={'KnownHosts ' + classes.root}>
|
||||||
|
{ touched && (
|
||||||
|
<div className="KnownHosts-validation">
|
||||||
|
{
|
||||||
|
(error &&
|
||||||
|
<div className="KnownHosts-error">
|
||||||
|
{error}
|
||||||
|
<ErrorOutlinedIcon style={{ fontSize: 'small', fontWeight: 'bold' }} />
|
||||||
|
</div>
|
||||||
|
) ||
|
||||||
|
|
||||||
|
(warning && <div className="KnownHosts-warning">{warning}</div>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
|
||||||
<InputLabel id='KnownHosts-select'>Host</InputLabel>
|
<InputLabel id='KnownHosts-select'>Host</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
id='KnownHosts-select'
|
id='KnownHosts-select'
|
||||||
labelId='KnownHosts-label'
|
labelId='KnownHosts-label'
|
||||||
label='Host'
|
label='Host'
|
||||||
margin='dense'
|
margin='dense'
|
||||||
value={state.selectedHost}
|
name='host'
|
||||||
|
value={hostsState.selectedHost}
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
onChange={e => selectHost(e.target.value)}
|
onChange={e => selectHost(e.target.value)}
|
||||||
>
|
>
|
||||||
<Button value={state.selectedHost} onClick={addKnownHost}>Add</Button>
|
<Button value={hostsState.selectedHost} onClick={openAddKnownHostDialog}>
|
||||||
|
<span>Add new host</span>
|
||||||
|
<AddIcon fontSize='small' color='primary' />
|
||||||
|
</Button>
|
||||||
|
|
||||||
{
|
{
|
||||||
state.hosts.map((host, index) => (
|
hostsState.hosts.map((host, index) => (
|
||||||
<MenuItem className='KnownHosts-item' value={index} key={index}>
|
<MenuItem className='KnownHosts-item' value={host} key={index}>
|
||||||
<div className='KnownHosts-item__label'>
|
<div className='KnownHosts-item__label'>
|
||||||
<Check />
|
<Check />
|
||||||
<span>{host.name} ({ getHostPort(state.hosts[index]).host }:{getHostPort(state.hosts[index]).port})</span>
|
<span>{host.name} ({ getHostPort(hostsState.hosts[index]).host }:{getHostPort(hostsState.hosts[index]).port})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ host.editable && (
|
{ host.editable && (
|
||||||
<IconButton size='small' color='primary' onClick={() => editKnownHost(index)}>
|
<IconButton className='KnownHosts-item__edit' size='small' color='primary' onClick={(e) => {
|
||||||
|
openEditKnownHostDialog(hostsState.hosts[index]);
|
||||||
|
}}>
|
||||||
<EditRoundedIcon fontSize='small' />
|
<EditRoundedIcon fontSize='small' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
) }
|
) }
|
||||||
|
@ -80,6 +195,15 @@ const KnownHosts = ({ onChange }) => {
|
||||||
}
|
}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<KnownHostDialog
|
||||||
|
isOpen={dialogState.open}
|
||||||
|
host={dialogState.edit}
|
||||||
|
onRemove={handleDialogRemove}
|
||||||
|
onSubmit={handleDialogSubmit}
|
||||||
|
handleClose={closeKnownHostDialog}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,11 @@ export { default as ThreePaneLayout } from './ThreePaneLayout/ThreePaneLayout';
|
||||||
export { default as CheckboxField } from './CheckboxField/CheckboxField';
|
export { default as CheckboxField } from './CheckboxField/CheckboxField';
|
||||||
export { default as SelectField } from './SelectField/SelectField';
|
export { default as SelectField } from './SelectField/SelectField';
|
||||||
export { default as ScrollToBottomOnChanges } from './ScrollToBottomOnChanges/ScrollToBottomOnChanges';
|
export { default as ScrollToBottomOnChanges } from './ScrollToBottomOnChanges/ScrollToBottomOnChanges';
|
||||||
export { default as RegistrationDialog } from './RegistrationDialog/RegistrationDialog';
|
|
||||||
|
|
||||||
// Guards
|
// Guards
|
||||||
export { default as AuthGuard } from './Guard/AuthGuard';
|
export { default as AuthGuard } from './Guard/AuthGuard';
|
||||||
export { default as ModGuard } from './Guard/ModGuard';
|
export { default as ModGuard } from './Guard/ModGuard';
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
export { default as CardImportDialog } from './CardImportDialog/CardImportDialog';
|
|
||||||
export { default as RequestPasswordResetDialog } from './RequestPasswordResetDialog/RequestPasswordResetDialog';
|
export { default as RequestPasswordResetDialog } from './RequestPasswordResetDialog/RequestPasswordResetDialog';
|
||||||
export { default as ResetPasswordDialog } from './ResetPasswordDialog/ResetPasswordDialog';
|
export { default as ResetPasswordDialog } from './ResetPasswordDialog/ResetPasswordDialog';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, {useState} from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
@ -13,9 +13,10 @@ import { RequestPasswordResetDialog, ResetPasswordDialog } from 'components';
|
||||||
import { LoginForm } from 'forms';
|
import { LoginForm } from 'forms';
|
||||||
import { useReduxEffect } from 'hooks';
|
import { useReduxEffect } from 'hooks';
|
||||||
import { Images } from 'images';
|
import { Images } from 'images';
|
||||||
import { RouteEnum, StatusEnum } from 'types';
|
import { HostDTO } from 'services';
|
||||||
|
import { RouteEnum, WebSocketConnectOptions, getHostPort } from 'types';
|
||||||
import { ServerSelectors, ServerTypes } from 'store';
|
import { ServerSelectors, ServerTypes } from 'store';
|
||||||
import { SessionCommands } from 'websocket';
|
|
||||||
import './Login.css';
|
import './Login.css';
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
|
@ -59,6 +60,7 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const isConnected = AuthenticationService.isConnected(state);
|
const isConnected = AuthenticationService.isConnected(state);
|
||||||
|
|
||||||
|
const [hostIdToRemember, setHostIdToRemember] = useState(null);
|
||||||
const [dialogState, setDialogState] = useState({
|
const [dialogState, setDialogState] = useState({
|
||||||
passwordResetRequestDialog: false,
|
passwordResetRequestDialog: false,
|
||||||
resetPasswordDialog: false
|
resetPasswordDialog: false
|
||||||
|
@ -73,6 +75,15 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
closeResetPasswordDialog();
|
closeResetPasswordDialog();
|
||||||
}, ServerTypes.RESET_PASSWORD_SUCCESS, []);
|
}, ServerTypes.RESET_PASSWORD_SUCCESS, []);
|
||||||
|
|
||||||
|
useReduxEffect(({ options: { hashedPassword } }) => {
|
||||||
|
if (hostIdToRemember) {
|
||||||
|
HostDTO.get(hostIdToRemember).then(host => {
|
||||||
|
host.hashedPassword = hashedPassword;
|
||||||
|
host.save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ServerTypes.LOGIN_SUCCESSFUL, [hostIdToRemember]);
|
||||||
|
|
||||||
const showDescription = () => {
|
const showDescription = () => {
|
||||||
return !isConnected && description?.length;
|
return !isConnected && description?.length;
|
||||||
};
|
};
|
||||||
|
@ -81,6 +92,47 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
console.log('Login.createAccount->openForgotPasswordDialog');
|
console.log('Login.createAccount->openForgotPasswordDialog');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSubmit = useCallback((loginForm) => {
|
||||||
|
const {
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
selectedHost,
|
||||||
|
selectedHost: {
|
||||||
|
id: hostId,
|
||||||
|
hashedPassword
|
||||||
|
},
|
||||||
|
remember
|
||||||
|
} = loginForm;
|
||||||
|
|
||||||
|
updateHost(loginForm);
|
||||||
|
|
||||||
|
if (remember) {
|
||||||
|
setHostIdToRemember(hostId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: WebSocketConnectOptions = {
|
||||||
|
...getHostPort(selectedHost),
|
||||||
|
userName,
|
||||||
|
password
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
options.hashedPassword = hashedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationService.login(options as WebSocketConnectOptions);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateHost = ({ selectedHost, userName, hashedPassword, remember }) => {
|
||||||
|
HostDTO.get(selectedHost.id).then(hostDTO => {
|
||||||
|
hostDTO.remember = remember;
|
||||||
|
hostDTO.userName = remember ? userName : null;
|
||||||
|
hostDTO.hashedPassword = remember ? hashedPassword : null;
|
||||||
|
|
||||||
|
hostDTO.save();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleRequestPasswordResetDialogSubmit = async ({ user, email, host, port }) => {
|
const handleRequestPasswordResetDialogSubmit = async ({ user, email, host, port }) => {
|
||||||
if (email) {
|
if (email) {
|
||||||
AuthenticationService.resetPasswordChallenge({ user, email, host, port } as any);
|
AuthenticationService.resetPasswordChallenge({ user, email, host, port } as any);
|
||||||
|
@ -123,7 +175,7 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
<Typography variant="h1">Login</Typography>
|
<Typography variant="h1">Login</Typography>
|
||||||
<Typography variant="subtitle1">A cross-platform virtual tabletop for multiplayer card games.</Typography>
|
<Typography variant="subtitle1">A cross-platform virtual tabletop for multiplayer card games.</Typography>
|
||||||
<div className="login-form">
|
<div className="login-form">
|
||||||
<LoginForm onSubmit={AuthenticationService.connect} />
|
<LoginForm onSubmit={onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.dialog-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ const CardImportDialog = ({ classes, handleClose, isOpen }: any) => {
|
||||||
return (
|
return (
|
||||||
<Dialog onClose={handleOnClose} open={isOpen}>
|
<Dialog onClose={handleOnClose} open={isOpen}>
|
||||||
<DialogTitle disableTypography className="dialog-title">
|
<DialogTitle disableTypography className="dialog-title">
|
||||||
<Typography variant="h6">Import Cards</Typography>
|
<Typography variant="h2">Import Cards</Typography>
|
||||||
|
|
||||||
{handleOnClose ? (
|
{handleOnClose ? (
|
||||||
<IconButton onClick={handleOnClose}>
|
<IconButton onClick={handleOnClose}>
|
26
webclient/src/dialogs/KnownHostDialog/KnownHostDialog.css
Normal file
26
webclient/src/dialogs/KnownHostDialog/KnownHostDialog.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
.KnownHostDialog {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.KnownHostDialog .MuiDialog-paper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title__wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title__label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content__subtitle.MuiTypography-root {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
55
webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx
Normal file
55
webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import AddIcon from '@material-ui/icons/Add';
|
||||||
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
|
||||||
|
import { KnownHostForm } from 'forms';
|
||||||
|
|
||||||
|
import './KnownHostDialog.css';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
root: {
|
||||||
|
'& .dialog-title__wrapper': {
|
||||||
|
borderColor: theme.palette.grey[300]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const KnownHostDialog = ({ handleClose, onRemove, onSubmit, isOpen, host }: any) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const handleOnClose = () => {
|
||||||
|
if (handleClose) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog className={'KnownHostDialog ' + classes.root} onClose={handleOnClose} open={isOpen}>
|
||||||
|
<DialogTitle disableTypography className='dialog-title'>
|
||||||
|
<div className='dialog-title__wrapper'>
|
||||||
|
<Typography variant='h2'>{ host ? 'Edit' : 'Add' } Known Host</Typography>
|
||||||
|
|
||||||
|
{handleClose ? (
|
||||||
|
<IconButton onClick={handleClose}>
|
||||||
|
<CloseIcon fontSize='large' />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent className='dialog-content'>
|
||||||
|
<Typography className='dialog-content__subtitle' variant='subtitle1'>
|
||||||
|
Adding a new host allows you to connect to different servers. Enter the details below to your host list.
|
||||||
|
</Typography>
|
||||||
|
<KnownHostForm onRemove={onRemove} onSubmit={onSubmit} host={host}></KnownHostForm>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KnownHostDialog;
|
3
webclient/src/dialogs/index.ts
Normal file
3
webclient/src/dialogs/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as CardImportDialog } from './CardImportDialog/CardImportDialog';
|
||||||
|
export { default as KnownHostDialog } from './KnownHostDialog/KnownHostDialog';
|
||||||
|
export { default as RegistrationDialog } from './RegistrationDialog/RegistrationDialog';
|
21
webclient/src/forms/KnownHostForm/KnownHostForm.css
Normal file
21
webclient/src/forms/KnownHostForm/KnownHostForm.css
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
.KnownHostForm {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.KnownHostForm-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.KnownHostForm-submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.KnownHostForm-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 5px 0 10px;
|
||||||
|
color: red;
|
||||||
|
}
|
83
webclient/src/forms/KnownHostForm/KnownHostForm.tsx
Normal file
83
webclient/src/forms/KnownHostForm/KnownHostForm.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// eslint-disable-next-line
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Form, Field } from 'react-final-form'
|
||||||
|
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import AnchorLink from '@material-ui/core/Link';
|
||||||
|
|
||||||
|
import { InputField } from 'components';
|
||||||
|
|
||||||
|
import './KnownHostForm.css';
|
||||||
|
|
||||||
|
function KnownHostForm({ host, onRemove, onSubmit }) {
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
initialValues={{
|
||||||
|
id: host?.id,
|
||||||
|
name: host?.name,
|
||||||
|
host: host?.host,
|
||||||
|
port: host?.port,
|
||||||
|
}}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
validate={values => {
|
||||||
|
const errors: any = {};
|
||||||
|
|
||||||
|
if (!values.name) {
|
||||||
|
errors.name = 'Required'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.host) {
|
||||||
|
errors.host = 'Required'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.port) {
|
||||||
|
errors.port = 'Required'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(errors).length) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ handleSubmit }) => (
|
||||||
|
<form className="KnownHostForm" onSubmit={handleSubmit}>
|
||||||
|
<div className="KnownHostForm-item">
|
||||||
|
<Field label="Host Name" name="name" component={InputField} />
|
||||||
|
</div>
|
||||||
|
<div className="KnownHostForm-item">
|
||||||
|
<Field label="Host Address" name="host" component={InputField} />
|
||||||
|
</div>
|
||||||
|
<div className="KnownHostForm-item">
|
||||||
|
<Field label="Port" name="port" type="number" component={InputField} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className="KnownHostForm-submit" color="primary" variant="contained" type="submit">
|
||||||
|
{host ? 'Save Changes' : 'Add Host' }
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="KnownHostForm-actions">
|
||||||
|
<div className="KnownHostForm-actions__delete">
|
||||||
|
{ host && (
|
||||||
|
<Button color="inherit" onClick={() => !confirmDelete ? setConfirmDelete(true) : onRemove(host)}>
|
||||||
|
{ !confirmDelete ? 'Delete' : 'Are you sure?' }
|
||||||
|
</Button>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
<AnchorLink href='https://github.com/Cockatrice/Cockatrice/wiki/Public-Servers' target='_blank'>
|
||||||
|
Need help adding a new host?
|
||||||
|
</AnchorLink>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) }
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(KnownHostForm);
|
|
@ -1,46 +1,130 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React from "react";
|
import React, { Component, useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Form, Field, reduxForm, change } from 'redux-form'
|
import { Form, Field, reduxForm, change, FormSubmitHandler } from 'redux-form'
|
||||||
|
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
|
|
||||||
import { InputField, KnownHosts } from 'components';
|
import { AuthenticationService } from 'api';
|
||||||
// import { ServerDispatch } from "store";
|
import { CheckboxField, InputField, KnownHosts } from 'components';
|
||||||
import { FormKey } from 'types';
|
import { useAutoConnect } from 'hooks';
|
||||||
|
import { HostDTO, SettingDTO } from 'services';
|
||||||
|
import { FormKey, APP_USER } from 'types';
|
||||||
|
|
||||||
import './LoginForm.css';
|
import './LoginForm.css';
|
||||||
|
|
||||||
const LoginForm = (props) => {
|
const PASSWORD_LABEL = 'Password';
|
||||||
const { dispatch, handleSubmit } = props;
|
const STORED_PASSWORD_LABEL = '* SAVED *';
|
||||||
|
|
||||||
|
const LoginForm: any = ({ dispatch, form, submit, handleSubmit }: LoginFormProps) => {
|
||||||
|
const password: any = useRef();
|
||||||
|
const [host, setHost] = useState(null);
|
||||||
|
const [remember, setRemember] = useState(false);
|
||||||
|
const [passwordLabel, setPasswordLabel] = useState(PASSWORD_LABEL);
|
||||||
|
const [hasStoredPassword, useStoredPassword] = useState(false);
|
||||||
|
|
||||||
|
const [autoConnect, setAutoConnect] = useAutoConnect(() => {
|
||||||
|
dispatch(change(form, 'autoConnect', autoConnect));
|
||||||
|
|
||||||
|
if (autoConnect && !remember) {
|
||||||
|
setRemember(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
SettingDTO.get(APP_USER).then((userSetting: SettingDTO) => {
|
||||||
|
if (userSetting?.autoConnect && !AuthenticationService.connectionAttemptMade()) {
|
||||||
|
HostDTO.getAll().then(hosts => {
|
||||||
|
let lastSelectedHost = hosts.find(({ lastSelected }) => lastSelected);
|
||||||
|
|
||||||
|
if (lastSelectedHost?.remember && lastSelectedHost?.hashedPassword) {
|
||||||
|
dispatch(change(form, 'selectedHost', lastSelectedHost));
|
||||||
|
dispatch(change(form, 'userName', lastSelectedHost.userName));
|
||||||
|
dispatch(change(form, 'remember', true));
|
||||||
|
setPasswordLabel(STORED_PASSWORD_LABEL);
|
||||||
|
dispatch(submit);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [submit, dispatch, form]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(change(form, 'remember', remember));
|
||||||
|
|
||||||
|
if (!remember) {
|
||||||
|
setAutoConnect(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!remember) {
|
||||||
|
useStoredPassword(false);
|
||||||
|
setPasswordLabel(PASSWORD_LABEL);
|
||||||
|
} else if (host?.hashedPassword) {
|
||||||
|
useStoredPassword(true);
|
||||||
|
setPasswordLabel(STORED_PASSWORD_LABEL);
|
||||||
|
}
|
||||||
|
}, [remember, dispatch, form]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!host) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(change(form, 'userName', host.userName));
|
||||||
|
dispatch(change(form, 'password', ''));
|
||||||
|
|
||||||
|
setRemember(host.remember);
|
||||||
|
setAutoConnect(host.remember && autoConnect);
|
||||||
|
|
||||||
|
if (host.remember && host.hashedPassword) {
|
||||||
|
// TODO: check if this causes a double render (maybe try combined state)
|
||||||
|
// try deriving useStoredPassword
|
||||||
|
useStoredPassword(true);
|
||||||
|
setPasswordLabel(STORED_PASSWORD_LABEL);
|
||||||
|
} else {
|
||||||
|
useStoredPassword(false);
|
||||||
|
setPasswordLabel(PASSWORD_LABEL);
|
||||||
|
}
|
||||||
|
}, [host, dispatch, form]);
|
||||||
|
|
||||||
|
const onRememberChange = event => setRemember(event.target.checked);
|
||||||
|
const onAutoConnectChange = event => setAutoConnect(event.target.checked);
|
||||||
|
const onHostChange = h => setHost(h);
|
||||||
|
|
||||||
const forgotPassword = () => {
|
const forgotPassword = () => {
|
||||||
console.log('Show recover password dialog, then AuthService.forgotPasswordRequest');
|
console.log('Show recover password dialog, then AuthService.forgotPasswordRequest');
|
||||||
};
|
};
|
||||||
|
|
||||||
const onHostChange = ({ host, port }) => {
|
|
||||||
dispatch(change(FormKey.LOGIN, 'host', host));
|
|
||||||
dispatch(change(FormKey.LOGIN, 'port', port));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className="loginForm" onSubmit={handleSubmit}>
|
<Form className='loginForm' onSubmit={handleSubmit}>
|
||||||
<div className="loginForm-items">
|
<div className='loginForm-items'>
|
||||||
<div className="loginForm-item">
|
<div className='loginForm-item'>
|
||||||
<Field label="Username" name="user" component={InputField} autoComplete="username" />
|
<Field label='Username' name='userName' component={InputField} autoComplete='off' />
|
||||||
</div>
|
</div>
|
||||||
<div className="loginForm-item">
|
<div className='loginForm-item'>
|
||||||
<Field label="Password" name="pass" type="password" component={InputField} autoComplete="current-password" />
|
<Field
|
||||||
|
label={passwordLabel}
|
||||||
|
ref={password}
|
||||||
|
onFocus={() => setPasswordLabel(PASSWORD_LABEL)}
|
||||||
|
onBlur={() => !password.current.value && hasStoredPassword && setPasswordLabel(STORED_PASSWORD_LABEL)}
|
||||||
|
name='password'
|
||||||
|
type='password'
|
||||||
|
component={InputField}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="loginForm-actions">
|
<div className='loginForm-actions'>
|
||||||
<span>Remember Me</span>
|
<Field label='Save Password' name='remember' component={CheckboxField} onChange={onRememberChange} />
|
||||||
<Button color="primary" onClick={forgotPassword}>Forgot Password</Button>
|
<Button color='primary' onClick={forgotPassword}>Forgot Password</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="loginForm-item">
|
<div className='loginForm-item'>
|
||||||
<KnownHosts onChange={onHostChange} />
|
<Field name='selectedHost' component={KnownHosts} onChange={onHostChange} />
|
||||||
|
</div>
|
||||||
|
<div className='loginForm-actions'>
|
||||||
|
<Field label='Auto Connect' name='autoConnect' component={CheckboxField} onChange={onAutoConnectChange} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="loginForm-submit rounded tall" color="primary" variant="contained" type="submit">
|
<Button className='loginForm-submit rounded tall' color='primary' variant='contained' type='submit'>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -55,27 +139,28 @@ const propsMap = {
|
||||||
if (!values.user) {
|
if (!values.user) {
|
||||||
errors.user = 'Required';
|
errors.user = 'Required';
|
||||||
}
|
}
|
||||||
if (!values.pass) {
|
|
||||||
errors.pass = 'Required';
|
if (!values.password && !values.selectedHost?.hashedPassword) {
|
||||||
|
errors.password = 'Required';
|
||||||
}
|
}
|
||||||
if (!values.host) {
|
|
||||||
errors.host = 'Required';
|
if (!values.selectedHost) {
|
||||||
}
|
errors.selectedHost = 'Required';
|
||||||
if (!values.port) {
|
|
||||||
errors.port = 'Required';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = () => ({
|
interface LoginFormProps {
|
||||||
initialValues: {
|
form: string;
|
||||||
// host: "mtg.tetrarch.co/servatrice",
|
dispatch: Function;
|
||||||
// port: "443"
|
submit: Function;
|
||||||
// host: "server.cockatrice.us",
|
handleSubmit: FormSubmitHandler;
|
||||||
// port: "4748"
|
}
|
||||||
}
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(reduxForm(propsMap)(LoginForm));
|
export default connect(mapStateToProps)(reduxForm(propsMap)(LoginForm));
|
||||||
|
|
|
@ -13,15 +13,16 @@ import './RegisterForm.css';
|
||||||
const RegisterForm = (props) => {
|
const RegisterForm = (props) => {
|
||||||
const { dispatch, handleSubmit } = props;
|
const { dispatch, handleSubmit } = props;
|
||||||
|
|
||||||
const onHostChange = ({ host, port }) => {
|
const onHostChange: any = ({ host, port }) => {
|
||||||
dispatch(change(FormKey.REGISTER, 'host', host));
|
dispatch(change(FormKey.REGISTER, 'host', host));
|
||||||
dispatch(change(FormKey.REGISTER, 'port', port));
|
dispatch(change(FormKey.REGISTER, 'port', port));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className="registerForm row" onSubmit={handleSubmit} autoComplete="off">
|
<Form className="registerForm row" onSubmit={handleSubmit} autoComplete="off">
|
||||||
<div className="leftRegisterForm column" >
|
<div className="leftRegisterForm column" >
|
||||||
<div className="registerForm-item">
|
<div className="registerForm-item">
|
||||||
<KnownHosts onChange={onHostChange} />
|
<Field name="selectedHost" component={KnownHosts} onChange={onHostChange} />
|
||||||
{ /* Padding is off */ }
|
{ /* Padding is off */ }
|
||||||
</div>
|
</div>
|
||||||
<div className="registerForm-item">
|
<div className="registerForm-item">
|
||||||
|
|
|
@ -17,7 +17,7 @@ const RequestPasswordResetForm = (props) => {
|
||||||
const [errorMessage, setErrorMessage] = useState(false);
|
const [errorMessage, setErrorMessage] = useState(false);
|
||||||
const [isMFA, setIsMFA] = useState(false);
|
const [isMFA, setIsMFA] = useState(false);
|
||||||
|
|
||||||
const onHostChange = ({ host, port }) => {
|
const onHostChange: any = ({ host, port }) => {
|
||||||
dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'host', host));
|
dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'host', host));
|
||||||
dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'port', port));
|
dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'port', port));
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ const RequestPasswordResetForm = (props) => {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="RequestPasswordResetForm-item">
|
<div className="RequestPasswordResetForm-item">
|
||||||
<KnownHosts onChange={onHostChange} />
|
<Field name='selectedHost' component={KnownHosts} onChange={onHostChange} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="RequestPasswordResetForm-submit rounded tall" color="primary" variant="contained" type="submit">
|
<Button className="RequestPasswordResetForm-submit rounded tall" color="primary" variant="contained" type="submit">
|
||||||
|
|
|
@ -18,7 +18,7 @@ const ResetPasswordForm = (props) => {
|
||||||
const [errorMessage, setErrorMessage] = useState(false);
|
const [errorMessage, setErrorMessage] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
const onHostChange = ({ host, port }) => {
|
const onHostChange: any = ({ host, port }) => {
|
||||||
dispatch(change(FormKey.RESET_PASSWORD, 'host', host));
|
dispatch(change(FormKey.RESET_PASSWORD, 'host', host));
|
||||||
dispatch(change(FormKey.RESET_PASSWORD, 'port', port));
|
dispatch(change(FormKey.RESET_PASSWORD, 'port', port));
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ const ResetPasswordForm = (props) => {
|
||||||
<Field label="Password Again" name="passwordAgain" component={InputField} />
|
<Field label="Password Again" name="passwordAgain" component={InputField} />
|
||||||
</div>
|
</div>
|
||||||
<div className="ResetPasswordForm-item">
|
<div className="ResetPasswordForm-item">
|
||||||
<KnownHosts onChange={onHostChange} />
|
<Field name='selectedHost' component={KnownHosts} onChange={onHostChange} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="ResetPasswordForm-submit rounded tall" color="primary" variant="contained" type="submit">
|
<Button className="ResetPasswordForm-submit rounded tall" color="primary" variant="contained" type="submit">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export { default as CardImportForm } from './CardImportForm/CardImportForm';
|
export { default as CardImportForm } from './CardImportForm/CardImportForm';
|
||||||
export { default as ConnectForm } from './ConnectForm/ConnectForm';
|
export { default as ConnectForm } from './ConnectForm/ConnectForm';
|
||||||
export { default as LoginForm } from './LoginForm/LoginForm';
|
export { default as LoginForm } from './LoginForm/LoginForm';
|
||||||
|
export { default as KnownHostForm } from './KnownHostForm/KnownHostForm';
|
||||||
export { default as RegisterForm } from './RegisterForm/RegisterForm';
|
export { default as RegisterForm } from './RegisterForm/RegisterForm';
|
||||||
export { default as SearchForm } from './SearchForm/SearchForm';
|
export { default as SearchForm } from './SearchForm/SearchForm';
|
||||||
export { default as RequestPasswordResetForm } from './RequestPasswordResetForm/RequestPasswordResetForm';
|
export { default as RequestPasswordResetForm } from './RequestPasswordResetForm/RequestPasswordResetForm';
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
|
export * from './useDebounce';
|
||||||
|
export * from './useAutoConnect';
|
||||||
export * from './useReduxEffect';
|
export * from './useReduxEffect';
|
||||||
|
|
40
webclient/src/hooks/useAutoConnect.ts
Normal file
40
webclient/src/hooks/useAutoConnect.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { debounce, DebouncedFunc } from 'lodash';
|
||||||
|
|
||||||
|
import { SettingDTO } from 'services';
|
||||||
|
import { APP_USER } from 'types';
|
||||||
|
|
||||||
|
type OnChange = () => void;
|
||||||
|
|
||||||
|
export function useAutoConnect(onChange: OnChange) {
|
||||||
|
const [setting, setSetting] = useState(undefined);
|
||||||
|
const [autoConnect, setAutoConnect] = useState(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
SettingDTO.get(APP_USER).then((setting: SettingDTO) => {
|
||||||
|
if (!setting) {
|
||||||
|
setting = new SettingDTO(APP_USER);
|
||||||
|
setting.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSetting(setting);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (setting) {
|
||||||
|
setAutoConnect(setting.autoConnect);
|
||||||
|
}
|
||||||
|
}, [setting]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (setting) {
|
||||||
|
setting.autoConnect = autoConnect;
|
||||||
|
setting.save();
|
||||||
|
|
||||||
|
onChange();
|
||||||
|
}
|
||||||
|
}, [setting, autoConnect]);
|
||||||
|
|
||||||
|
return [autoConnect, setAutoConnect];
|
||||||
|
}
|
13
webclient/src/hooks/useDebounce.ts
Normal file
13
webclient/src/hooks/useDebounce.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { debounce, DebouncedFunc } from 'lodash';
|
||||||
|
|
||||||
|
type UseDebounceType = (...args: any) => any;
|
||||||
|
const DEBOUNCE_DELAY = 250;
|
||||||
|
|
||||||
|
export function useDebounce<T extends UseDebounceType>(
|
||||||
|
fn: T,
|
||||||
|
deps: any[] = [],
|
||||||
|
timeout: number = DEBOUNCE_DELAY
|
||||||
|
): DebouncedFunc<T> {
|
||||||
|
return useCallback(debounce(fn, timeout), deps);
|
||||||
|
}
|
|
@ -6,8 +6,8 @@ File is adapted from https://github.com/Qeepsake/use-redux-effect under MIT Lice
|
||||||
|
|
||||||
import { useRef, useEffect, DependencyList } from 'react'
|
import { useRef, useEffect, DependencyList } from 'react'
|
||||||
import { useStore } from 'react-redux'
|
import { useStore } from 'react-redux'
|
||||||
import { castArray } from 'lodash'
|
|
||||||
import { AnyAction } from 'redux'
|
import { AnyAction } from 'redux'
|
||||||
|
import { castArray } from 'lodash'
|
||||||
|
|
||||||
export type ReduxEffect = (action: AnyAction) => void
|
export type ReduxEffect = (action: AnyAction) => void
|
||||||
|
|
||||||
|
@ -23,25 +23,25 @@ export function useReduxEffect(
|
||||||
type: string | string[],
|
type: string | string[],
|
||||||
deps: DependencyList = [],
|
deps: DependencyList = [],
|
||||||
): void {
|
): void {
|
||||||
const currentValue = useRef(null)
|
const currentValue = useRef(null);
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
|
|
||||||
const handleChange = (): void => {
|
const handleChange = (): void => {
|
||||||
const state = store.getState()
|
const state = store.getState();
|
||||||
const action = state.action
|
const action = state.action;
|
||||||
const previousValue = currentValue.current
|
const previousValue = currentValue.current;
|
||||||
currentValue.current = action.count
|
currentValue.current = action.count;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
previousValue !== action.count &&
|
previousValue !== action.count &&
|
||||||
castArray(type).includes(action.type)
|
castArray(type).includes(action.type)
|
||||||
) {
|
) {
|
||||||
effect(action)
|
effect(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = store.subscribe(handleChange)
|
const unsubscribe = store.subscribe(handleChange);
|
||||||
return (): void => unsubscribe()
|
return (): void => unsubscribe();
|
||||||
}, deps)
|
}, deps)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,22 @@ const palette = {
|
||||||
dark: '#401C7F',
|
dark: '#401C7F',
|
||||||
contrastText: '#FFFFFF',
|
contrastText: '#FFFFFF',
|
||||||
},
|
},
|
||||||
|
grey: {
|
||||||
|
50: '#fafafa',
|
||||||
|
100: '#f5f5f5',
|
||||||
|
200: '#eeeeee',
|
||||||
|
300: '#e0e0e0',
|
||||||
|
400: '#bdbdbd',
|
||||||
|
500: '#9e9e9e',
|
||||||
|
600: '#757575',
|
||||||
|
700: '#616161',
|
||||||
|
800: '#424242',
|
||||||
|
900: '#212121',
|
||||||
|
A100: '#d5d5d5',
|
||||||
|
A200: '#aaaaaa',
|
||||||
|
A400: '#303030',
|
||||||
|
A700: '#616161',
|
||||||
|
},
|
||||||
// secondary: {
|
// secondary: {
|
||||||
// main: '',
|
// main: '',
|
||||||
// light: '',
|
// light: '',
|
||||||
|
@ -67,6 +83,29 @@ export const materialTheme = createTheme({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
MuiCheckbox: {
|
||||||
|
root: {
|
||||||
|
'& .MuiSvgIcon-root': {
|
||||||
|
width: '.75em',
|
||||||
|
height: '.75em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
MuiFormControlLabel: {
|
||||||
|
label: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
MuiLink: {
|
||||||
|
root: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
MuiList: {
|
MuiList: {
|
||||||
root: {
|
root: {
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
|
@ -135,7 +174,10 @@ export const materialTheme = createTheme({
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
// h2: {},
|
h2: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
// h3: {},
|
// h3: {},
|
||||||
// h4: {},
|
// h4: {},
|
||||||
// h5: {},
|
// h5: {},
|
||||||
|
|
|
@ -8,11 +8,11 @@ export class HostDTO extends Host {
|
||||||
return dexieService.hosts.put(this);
|
return dexieService.hosts.put(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static add(host: HostDTO): Promise<IndexableType> {
|
static add(host: Host): Promise<IndexableType> {
|
||||||
return dexieService.hosts.add(host);
|
return dexieService.hosts.add(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get(id): Promise<HostDTO> {
|
static get(id: number): Promise<HostDTO> {
|
||||||
return dexieService.hosts.where('id').equals(id).first();
|
return dexieService.hosts.where('id').equals(id).first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,10 @@ export class HostDTO extends Host {
|
||||||
static bulkAdd(hosts: Host[]): Promise<IndexableType> {
|
static bulkAdd(hosts: Host[]): Promise<IndexableType> {
|
||||||
return dexieService.hosts.bulkAdd(hosts);
|
return dexieService.hosts.bulkAdd(hosts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static delete(id: string): Promise<void> {
|
||||||
|
return dexieService.hosts.delete(id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
dexieService.hosts.mapToClass(HostDTO);
|
dexieService.hosts.mapToClass(HostDTO);
|
|
@ -16,4 +16,4 @@ export class SetDTO extends Set {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
dexieService.cards.mapToClass(SetDTO);
|
dexieService.sets.mapToClass(SetDTO);
|
22
webclient/src/services/dexie/DexieDTOs/SettingDTO.ts
Normal file
22
webclient/src/services/dexie/DexieDTOs/SettingDTO.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Setting } from 'types';
|
||||||
|
|
||||||
|
import { dexieService } from '../DexieService';
|
||||||
|
|
||||||
|
export class SettingDTO extends Setting {
|
||||||
|
constructor(user) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.user = user;
|
||||||
|
this.autoConnect = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
return dexieService.settings.put(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get(user) {
|
||||||
|
return dexieService.settings.where('user').equalsIgnoreCase(user).first();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dexieService.settings.mapToClass(SettingDTO);
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './CardDTO';
|
export * from './CardDTO';
|
||||||
export * from './SetDTO';
|
export * from './SetDTO';
|
||||||
|
export * from './SettingDTO';
|
||||||
export * from './TokenDTO';
|
export * from './TokenDTO';
|
||||||
export * from './HostDTO';
|
export * from './HostDTO';
|
17
webclient/src/services/dexie/DexieSchemas/v1.schema.ts
Normal file
17
webclient/src/services/dexie/DexieSchemas/v1.schema.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export enum Stores {
|
||||||
|
SETTINGS = 'settings',
|
||||||
|
CARDS = 'cards',
|
||||||
|
SETS = 'sets',
|
||||||
|
TOKENS = 'tokens',
|
||||||
|
HOSTS = 'hosts',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const schemaV1 = (db) => {
|
||||||
|
db.version(1).stores({
|
||||||
|
[Stores.CARDS]: 'name',
|
||||||
|
[Stores.SETS]: 'code',
|
||||||
|
[Stores.SETTINGS]: 'user',
|
||||||
|
[Stores.TOKENS]: 'name.value',
|
||||||
|
[Stores.HOSTS]: '++id',
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,24 +1,16 @@
|
||||||
import Dexie from 'dexie';
|
import Dexie from 'dexie';
|
||||||
|
|
||||||
enum Stores {
|
import { Stores, schemaV1 } from './DexieSchemas/v1.schema';
|
||||||
CARDS = 'cards',
|
|
||||||
SETS = 'sets',
|
|
||||||
TOKENS = 'tokens',
|
|
||||||
HOSTS = 'hosts',
|
|
||||||
}
|
|
||||||
|
|
||||||
const StoreKeyIndexes = {
|
|
||||||
[Stores.CARDS]: 'name',
|
|
||||||
[Stores.SETS]: 'code',
|
|
||||||
[Stores.TOKENS]: 'name.value',
|
|
||||||
[Stores.HOSTS]: '++id,name',
|
|
||||||
};
|
|
||||||
|
|
||||||
class DexieService {
|
class DexieService {
|
||||||
private db: Dexie = new Dexie('Webatrice');
|
private db: Dexie = new Dexie('Webatrice');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.db.version(1).stores(StoreKeyIndexes);
|
schemaV1(this.db);
|
||||||
|
}
|
||||||
|
|
||||||
|
get settings() {
|
||||||
|
return this.db.table(Stores.SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
get cards() {
|
get cards() {
|
1
webclient/src/services/dexie/index.ts
Normal file
1
webclient/src/services/dexie/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './DexieDTOs';
|
|
@ -1,2 +1,2 @@
|
||||||
export * from './CardImporterService';
|
export * from './CardImporterService';
|
||||||
export * from './DexieDTOs';
|
export * from './dexie';
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import { Type } from 'protobufjs';
|
import { Type } from 'protobufjs';
|
||||||
|
import { WebSocketConnectOptions } from 'types';
|
||||||
|
|
||||||
import { Types } from './server.types';
|
import { Types } from './server.types';
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
clearStore: () => ({
|
clearStore: () => ({
|
||||||
type: Types.CLEAR_STORE
|
type: Types.CLEAR_STORE
|
||||||
}),
|
}),
|
||||||
|
loginSuccessful: (options: WebSocketConnectOptions) => ({
|
||||||
|
type: Types.LOGIN_SUCCESSFUL,
|
||||||
|
options
|
||||||
|
}),
|
||||||
connectionClosed: reason => ({
|
connectionClosed: reason => ({
|
||||||
type: Types.CONNECTION_CLOSED,
|
type: Types.CONNECTION_CLOSED,
|
||||||
reason
|
reason
|
||||||
|
|
|
@ -6,6 +6,9 @@ export const Dispatch = {
|
||||||
clearStore: () => {
|
clearStore: () => {
|
||||||
store.dispatch(Actions.clearStore());
|
store.dispatch(Actions.clearStore());
|
||||||
},
|
},
|
||||||
|
loginSuccessful: options => {
|
||||||
|
store.dispatch(Actions.loginSuccessful(options));
|
||||||
|
},
|
||||||
connectionClosed: reason => {
|
connectionClosed: reason => {
|
||||||
store.dispatch(Actions.connectionClosed(reason));
|
store.dispatch(Actions.connectionClosed(reason));
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,24 +3,26 @@ import { Log, SortBy, User, UserSortField } from 'types';
|
||||||
export interface ServerConnectParams {
|
export interface ServerConnectParams {
|
||||||
host: string;
|
host: string;
|
||||||
port: string;
|
port: string;
|
||||||
user: string;
|
userName: string;
|
||||||
pass: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerRegisterParams {
|
export interface ServerRegisterParams {
|
||||||
host: string;
|
host: string;
|
||||||
port: string;
|
port: string;
|
||||||
user: string;
|
userName: string;
|
||||||
pass: string;
|
password: string;
|
||||||
passAgain: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
country: string;
|
country: string;
|
||||||
realName: string;
|
realName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RequestPasswordSaltParams {
|
||||||
|
userName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ForgotPasswordParams {
|
export interface ForgotPasswordParams {
|
||||||
user: string;
|
userName: string;
|
||||||
clientid: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ForgotPasswordChallengeParams extends ForgotPasswordParams {
|
export interface ForgotPasswordChallengeParams extends ForgotPasswordParams {
|
||||||
|
@ -33,8 +35,7 @@ export interface ForgotPasswordResetParams extends ForgotPasswordParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccountActivationParams extends ServerRegisterParams {
|
export interface AccountActivationParams extends ServerRegisterParams {
|
||||||
activationCode: string;
|
token: string;
|
||||||
clientid: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerState {
|
export interface ServerState {
|
||||||
|
@ -68,7 +69,3 @@ export interface ServerStateLogs {
|
||||||
export interface ServerStateSortUsersBy extends SortBy {
|
export interface ServerStateSortUsersBy extends SortBy {
|
||||||
field: UserSortField
|
field: UserSortField
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestPasswordSaltParams {
|
|
||||||
user: string;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const Types = {
|
export const Types = {
|
||||||
CLEAR_STORE: '[Server] Clear Store',
|
CLEAR_STORE: '[Server] Clear Store',
|
||||||
|
LOGIN_SUCCESSFUL: '[Server] Login Successful',
|
||||||
CONNECTION_CLOSED: '[Server] Connection Closed',
|
CONNECTION_CLOSED: '[Server] Connection Closed',
|
||||||
SERVER_MESSAGE: '[Server] Server Message',
|
SERVER_MESSAGE: '[Server] Server Message',
|
||||||
UPDATE_BUDDY_LIST: '[Server] Update Buddy List',
|
UPDATE_BUDDY_LIST: '[Server] Update Buddy List',
|
||||||
|
|
|
@ -10,3 +10,4 @@ export * from './routes';
|
||||||
export * from './sort';
|
export * from './sort';
|
||||||
export * from './forms';
|
export * from './forms';
|
||||||
export * from './message';
|
export * from './message';
|
||||||
|
export * from './settings';
|
||||||
|
|
|
@ -12,6 +12,29 @@ export enum StatusEnum {
|
||||||
DISCONNECTING = 99
|
DISCONNECTING = 99
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WebSocketConnectOptions {
|
||||||
|
host?: string;
|
||||||
|
port?: string;
|
||||||
|
userName?: string;
|
||||||
|
password?: string;
|
||||||
|
hashedPassword?: string;
|
||||||
|
newPassword?: string;
|
||||||
|
email?: string;
|
||||||
|
autojoinrooms?: boolean;
|
||||||
|
keepalive?: number;
|
||||||
|
clientid?: string;
|
||||||
|
reason?: WebSocketConnectReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WebSocketConnectReason {
|
||||||
|
LOGIN,
|
||||||
|
REGISTER,
|
||||||
|
ACTIVATE_ACCOUNT,
|
||||||
|
PASSWORD_RESET_REQUEST,
|
||||||
|
PASSWORD_RESET_CHALLENGE,
|
||||||
|
PASSWORD_RESET
|
||||||
|
}
|
||||||
|
|
||||||
export class Host {
|
export class Host {
|
||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -20,6 +43,10 @@ export class Host {
|
||||||
localHost?: string;
|
localHost?: string;
|
||||||
localPort?: string;
|
localPort?: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
lastSelected?: boolean;
|
||||||
|
userName?: string;
|
||||||
|
hashedPassword?: string;
|
||||||
|
remember?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultHosts: Host[] = [
|
export const DefaultHosts: Host[] = [
|
||||||
|
@ -40,7 +67,7 @@ export const DefaultHosts: Host[] = [
|
||||||
{
|
{
|
||||||
name: 'Tetrarch',
|
name: 'Tetrarch',
|
||||||
host: 'mtg.tetrarch.co/servatrice',
|
host: 'mtg.tetrarch.co/servatrice',
|
||||||
port: '4748',
|
port: '443',
|
||||||
editable: false,
|
editable: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
6
webclient/src/types/settings.ts
Normal file
6
webclient/src/types/settings.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export class Setting {
|
||||||
|
user: string;
|
||||||
|
autoConnect?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APP_USER = '*app';
|
|
@ -1,7 +1,7 @@
|
||||||
import { ServerStatus, StatusEnum } from 'types';
|
import { ServerStatus, StatusEnum, WebSocketConnectOptions } from 'types';
|
||||||
|
|
||||||
import { ProtobufService } from './services/ProtobufService';
|
import { ProtobufService } from './services/ProtobufService';
|
||||||
import { WebSocketOptions, WebSocketService } from './services/WebSocketService';
|
import { WebSocketService } from './services/WebSocketService';
|
||||||
|
|
||||||
import { RoomPersistence, SessionPersistence } from './persistence';
|
import { RoomPersistence, SessionPersistence } from './persistence';
|
||||||
|
|
||||||
|
@ -30,11 +30,12 @@ export class WebClient {
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
public options: WebSocketOptions = {
|
public options: WebSocketConnectOptions = {
|
||||||
host: '',
|
host: '',
|
||||||
port: '',
|
port: '',
|
||||||
user: '',
|
userName: '',
|
||||||
pass: '',
|
password: '',
|
||||||
|
hashedPassword: '',
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
email: '',
|
email: '',
|
||||||
clientid: null,
|
clientid: null,
|
||||||
|
@ -43,6 +44,8 @@ export class WebClient {
|
||||||
keepalive: 5000
|
keepalive: 5000
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public connectionAttemptMade = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.socket.message$.subscribe((message: MessageEvent) => {
|
this.socket.message$.subscribe((message: MessageEvent) => {
|
||||||
this.protobuf.handleMessageEvent(message);
|
this.protobuf.handleMessageEvent(message);
|
||||||
|
@ -55,7 +58,8 @@ export class WebClient {
|
||||||
console.log(this);
|
console.log(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public connect(options: WebSocketOptions) {
|
public connect(options: WebSocketConnectOptions) {
|
||||||
|
this.connectionAttemptMade = true;
|
||||||
this.options = { ...this.options, ...options };
|
this.options = { ...this.options, ...options };
|
||||||
this.socket.connect(this.options);
|
this.socket.connect(this.options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { StatusEnum } from 'types';
|
import { HostDTO } from 'services';
|
||||||
|
import { StatusEnum, WebSocketConnectReason, WebSocketConnectOptions } from 'types';
|
||||||
|
|
||||||
import { RoomPersistence, SessionPersistence } from '../persistence';
|
import { RoomPersistence, SessionPersistence } from '../persistence';
|
||||||
import webClient from '../WebClient';
|
import webClient from '../WebClient';
|
||||||
import { guid, hashPassword } from '../utils';
|
import { guid, hashPassword } from '../utils';
|
||||||
import { WebSocketConnectReason, WebSocketOptions } from '../services/WebSocketService';
|
|
||||||
import {
|
import {
|
||||||
AccountActivationParams,
|
AccountActivationParams,
|
||||||
ForgotPasswordChallengeParams,
|
ForgotPasswordChallengeParams,
|
||||||
|
@ -15,7 +15,7 @@ import {
|
||||||
import NormalizeService from '../utils/NormalizeService';
|
import NormalizeService from '../utils/NormalizeService';
|
||||||
|
|
||||||
export class SessionCommands {
|
export class SessionCommands {
|
||||||
static connect(options: WebSocketOptions, reason: WebSocketConnectReason): void {
|
static connect(options: WebSocketConnectOptions, reason: WebSocketConnectReason): void {
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case WebSocketConnectReason.LOGIN:
|
case WebSocketConnectReason.LOGIN:
|
||||||
case WebSocketConnectReason.REGISTER:
|
case WebSocketConnectReason.REGISTER:
|
||||||
|
@ -38,16 +38,18 @@ export class SessionCommands {
|
||||||
}
|
}
|
||||||
|
|
||||||
static login(passwordSalt?: string): void {
|
static login(passwordSalt?: string): void {
|
||||||
|
const { userName, password, hashedPassword } = webClient.options;
|
||||||
|
|
||||||
const loginConfig: any = {
|
const loginConfig: any = {
|
||||||
...webClient.clientConfig,
|
...webClient.clientConfig,
|
||||||
userName: webClient.options.user,
|
clientid: 'webatrice',
|
||||||
clientid: guid()
|
userName,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (passwordSalt) {
|
if (passwordSalt) {
|
||||||
loginConfig.hashedPassword = hashPassword(passwordSalt, webClient.options.pass);
|
loginConfig.hashedPassword = hashedPassword || hashPassword(passwordSalt, password);
|
||||||
} else {
|
} else {
|
||||||
loginConfig.password = webClient.options.pass;
|
loginConfig.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CmdLogin = webClient.protobuf.controller.Command_Login.create(loginConfig);
|
const CmdLogin = webClient.protobuf.controller.Command_Login.create(loginConfig);
|
||||||
|
@ -65,11 +67,13 @@ export class SessionCommands {
|
||||||
SessionPersistence.updateBuddyList(buddyList);
|
SessionPersistence.updateBuddyList(buddyList);
|
||||||
SessionPersistence.updateIgnoreList(ignoreList);
|
SessionPersistence.updateIgnoreList(ignoreList);
|
||||||
SessionPersistence.updateUser(userInfo);
|
SessionPersistence.updateUser(userInfo);
|
||||||
|
SessionPersistence.loginSuccessful(loginConfig);
|
||||||
|
|
||||||
SessionCommands.listUsers();
|
SessionCommands.listUsers();
|
||||||
SessionCommands.listRooms();
|
SessionCommands.listRooms();
|
||||||
|
|
||||||
SessionCommands.updateStatus(StatusEnum.LOGGED_IN, 'Logged in.');
|
SessionCommands.updateStatus(StatusEnum.LOGGED_IN, 'Logged in.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,11 +121,11 @@ export class SessionCommands {
|
||||||
}
|
}
|
||||||
|
|
||||||
static requestPasswordSalt(): void {
|
static requestPasswordSalt(): void {
|
||||||
const options = webClient.options as unknown as RequestPasswordSaltParams;
|
const { userName } = webClient.options as unknown as RequestPasswordSaltParams;
|
||||||
|
|
||||||
const registerConfig = {
|
const registerConfig = {
|
||||||
...webClient.clientConfig,
|
...webClient.clientConfig,
|
||||||
userName: options.user,
|
userName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CmdRequestPasswordSalt = webClient.protobuf.controller.Command_RequestPasswordSalt.create(registerConfig);
|
const CmdRequestPasswordSalt = webClient.protobuf.controller.Command_RequestPasswordSalt.create(registerConfig);
|
||||||
|
@ -132,35 +136,35 @@ export class SessionCommands {
|
||||||
|
|
||||||
webClient.protobuf.sendSessionCommand(sc, raw => {
|
webClient.protobuf.sendSessionCommand(sc, raw => {
|
||||||
switch (raw.responseCode) {
|
switch (raw.responseCode) {
|
||||||
case webClient.protobuf.controller.Response.ResponseCode.RespOk:
|
case webClient.protobuf.controller.Response.ResponseCode.RespOk: {
|
||||||
const passwordSalt = raw['.Response_PasswordSalt.ext'].passwordSalt;
|
const passwordSalt = raw['.Response_PasswordSalt.ext'].passwordSalt;
|
||||||
SessionCommands.login(passwordSalt);
|
SessionCommands.login(passwordSalt);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationRequired:
|
case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationRequired: {
|
||||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Login failed: incorrect username or password');
|
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Login failed: incorrect username or password');
|
||||||
SessionCommands.disconnect();
|
SessionCommands.disconnect();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default: {
|
||||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason');
|
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason');
|
||||||
SessionCommands.disconnect();
|
SessionCommands.disconnect();
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static register(): void {
|
static register(): void {
|
||||||
const options = webClient.options as unknown as ServerRegisterParams;
|
const { userName, password, email, country, realName } = webClient.options as unknown as ServerRegisterParams;
|
||||||
|
|
||||||
const registerConfig = {
|
const registerConfig = {
|
||||||
...webClient.clientConfig,
|
...webClient.clientConfig,
|
||||||
userName: options.user,
|
clientid: 'webatrice',
|
||||||
password: options.pass,
|
userName,
|
||||||
email: options.email,
|
password,
|
||||||
country: options.country,
|
email,
|
||||||
realName: options.realName,
|
country,
|
||||||
clientid: 'webatrice'
|
realName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CmdRegister = webClient.protobuf.controller.Command_Register.create(registerConfig);
|
const CmdRegister = webClient.protobuf.controller.Command_Register.create(registerConfig);
|
||||||
|
@ -222,13 +226,13 @@ export class SessionCommands {
|
||||||
};
|
};
|
||||||
|
|
||||||
static activateAccount(): void {
|
static activateAccount(): void {
|
||||||
const options = webClient.options as unknown as AccountActivationParams;
|
const { userName, token } = webClient.options as unknown as AccountActivationParams;
|
||||||
|
|
||||||
const accountActivationConfig = {
|
const accountActivationConfig = {
|
||||||
...webClient.clientConfig,
|
...webClient.clientConfig,
|
||||||
userName: options.user,
|
clientid: 'webatrice',
|
||||||
clientid: options.clientid,
|
userName,
|
||||||
token: options.activationCode
|
token,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CmdActivate = webClient.protobuf.controller.Command_Activate.create(accountActivationConfig);
|
const CmdActivate = webClient.protobuf.controller.Command_Activate.create(accountActivationConfig);
|
||||||
|
@ -249,12 +253,12 @@ export class SessionCommands {
|
||||||
}
|
}
|
||||||
|
|
||||||
static resetPasswordRequest(): void {
|
static resetPasswordRequest(): void {
|
||||||
const options = webClient.options as unknown as ForgotPasswordParams;
|
const { userName } = webClient.options as unknown as ForgotPasswordParams;
|
||||||
|
|
||||||
const forgotPasswordConfig = {
|
const forgotPasswordConfig = {
|
||||||
...webClient.clientConfig,
|
...webClient.clientConfig,
|
||||||
userName: options.user,
|
clientid: 'webatrice',
|
||||||
clientid: options.clientid
|
userName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CmdForgotPasswordRequest = webClient.protobuf.controller.Command_ForgotPasswordRequest.create(forgotPasswordConfig);
|
const CmdForgotPasswordRequest = webClient.protobuf.controller.Command_ForgotPasswordRequest.create(forgotPasswordConfig);
|
||||||
|
@ -284,13 +288,13 @@ export class SessionCommands {
|
||||||
}
|
}
|
||||||
|
|
||||||
static resetPasswordChallenge(): void {
|
static resetPasswordChallenge(): void {
|
||||||
const options = webClient.options as unknown as ForgotPasswordChallengeParams;
|
const { userName, email } = webClient.options as unknown as ForgotPasswordChallengeParams;
|
||||||
|
|
||||||
const forgotPasswordChallengeConfig = {
|
const forgotPasswordChallengeConfig = {
|
||||||
...webClient.clientConfig,
|
...webClient.clientConfig,
|
||||||
userName: options.user,
|
clientid: 'webatrice',
|
||||||
clientid: options.clientid,
|
userName,
|
||||||
email: options.email
|
email,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CmdForgotPasswordChallenge = webClient.protobuf.controller.Command_ForgotPasswordChallenge.create(forgotPasswordChallengeConfig);
|
const CmdForgotPasswordChallenge = webClient.protobuf.controller.Command_ForgotPasswordChallenge.create(forgotPasswordChallengeConfig);
|
||||||
|
@ -313,14 +317,14 @@ export class SessionCommands {
|
||||||
}
|
}
|
||||||
|
|
||||||
static resetPassword(): void {
|
static resetPassword(): void {
|
||||||
const options = webClient.options as unknown as ForgotPasswordResetParams;
|
const { userName, token, newPassword } = webClient.options as unknown as ForgotPasswordResetParams;
|
||||||
|
|
||||||
const forgotPasswordResetConfig = {
|
const forgotPasswordResetConfig = {
|
||||||
...webClient.clientConfig,
|
...webClient.clientConfig,
|
||||||
userName: options.user,
|
clientid: 'webatrice',
|
||||||
clientid: options.clientid,
|
userName,
|
||||||
token: options.token,
|
token,
|
||||||
newPassword: options.newPassword
|
newPassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CmdForgotPasswordReset = webClient.protobuf.controller.Command_ForgotPasswordReset.create(forgotPasswordResetConfig);
|
const CmdForgotPasswordReset = webClient.protobuf.controller.Command_ForgotPasswordReset.create(forgotPasswordResetConfig);
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Room, StatusEnum, User } from 'types';
|
import { Room, StatusEnum, User, WebSocketConnectReason } from 'types';
|
||||||
|
|
||||||
import { SessionCommands } from '../commands';
|
import { SessionCommands } from '../commands';
|
||||||
import { RoomPersistence, SessionPersistence } from '../persistence';
|
import { RoomPersistence, SessionPersistence } from '../persistence';
|
||||||
import { ProtobufEvents } from '../services/ProtobufService';
|
import { ProtobufEvents } from '../services/ProtobufService';
|
||||||
import webClient from '../WebClient';
|
import webClient from '../WebClient';
|
||||||
import { WebSocketConnectReason } from '../services/WebSocketService';
|
|
||||||
|
|
||||||
export const SessionEvents: ProtobufEvents = {
|
export const SessionEvents: ProtobufEvents = {
|
||||||
'.Event_AddToList.ext': addToList,
|
'.Event_AddToList.ext': addToList,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ServerDispatch } from 'store';
|
import { ServerDispatch } from 'store';
|
||||||
import { Log, StatusEnum, User } from 'types';
|
import { Log, StatusEnum, User, WebSocketConnectOptions } from 'types';
|
||||||
|
|
||||||
import { sanitizeHtml } from 'websocket/utils';
|
import { sanitizeHtml } from 'websocket/utils';
|
||||||
import NormalizeService from '../utils/NormalizeService';
|
import NormalizeService from '../utils/NormalizeService';
|
||||||
|
@ -9,6 +9,10 @@ export class SessionPersistence {
|
||||||
ServerDispatch.clearStore();
|
ServerDispatch.clearStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static loginSuccessful(options: WebSocketConnectOptions) {
|
||||||
|
ServerDispatch.loginSuccessful(options);
|
||||||
|
}
|
||||||
|
|
||||||
static connectionClosed(reason: number) {
|
static connectionClosed(reason: number) {
|
||||||
ServerDispatch.connectionClosed(reason);
|
ServerDispatch.connectionClosed(reason);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,10 @@
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
import { ServerStatus, StatusEnum } from 'types';
|
import { ServerStatus, StatusEnum, WebSocketConnectOptions } from 'types';
|
||||||
|
|
||||||
import { KeepAliveService } from './KeepAliveService';
|
import { KeepAliveService } from './KeepAliveService';
|
||||||
import { WebClient } from '../WebClient';
|
import { WebClient } from '../WebClient';
|
||||||
|
|
||||||
export interface WebSocketOptions {
|
|
||||||
host: string;
|
|
||||||
port: string;
|
|
||||||
user: string;
|
|
||||||
pass: string;
|
|
||||||
newPassword: string;
|
|
||||||
email: string;
|
|
||||||
autojoinrooms: boolean;
|
|
||||||
keepalive: number;
|
|
||||||
clientid: string;
|
|
||||||
reason: WebSocketConnectReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WebSocketConnectReason {
|
|
||||||
LOGIN,
|
|
||||||
REGISTER,
|
|
||||||
ACTIVATE_ACCOUNT,
|
|
||||||
PASSWORD_RESET_REQUEST,
|
|
||||||
PASSWORD_RESET_CHALLENGE,
|
|
||||||
PASSWORD_RESET
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WebSocketService {
|
export class WebSocketService {
|
||||||
private socket: WebSocket;
|
private socket: WebSocket;
|
||||||
private webClient: WebClient;
|
private webClient: WebClient;
|
||||||
|
@ -48,7 +26,7 @@ export class WebSocketService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public connect(options: WebSocketOptions, protocol: string = 'wss'): void {
|
public connect(options: WebSocketConnectOptions, protocol: string = 'wss'): void {
|
||||||
if (window.location.hostname === 'localhost') {
|
if (window.location.hostname === 'localhost') {
|
||||||
protocol = 'ws';
|
protocol = 'ws';
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,13 @@ export function sanitizeHtml(msg: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function enforceTagWhitelist($el: JQuery<HTMLElement>, tags: string): void {
|
function enforceTagWhitelist($el: JQuery<HTMLElement>, tags: string): void {
|
||||||
$el.find('*').not(tags).each(() => {
|
$el.find('*').not(tags).each(function enforceTag() {
|
||||||
$(this).replaceWith(this.innerHTML);
|
$(this).replaceWith(this.innerHTML);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function enforceAttrWhitelist($el: JQuery<HTMLElement>, attrs: string[]): void {
|
function enforceAttrWhitelist($el: JQuery<HTMLElement>, attrs: string[]): void {
|
||||||
$el.find('*').each(() => {
|
$el.find('*').each(function enforceAttribute() {
|
||||||
const attributes = this.attributes;
|
const attributes = this.attributes;
|
||||||
let i = attributes.length;
|
let i = attributes.length;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
|
@ -36,7 +36,7 @@ function enforceAttrWhitelist($el: JQuery<HTMLElement>, attrs: string[]): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function enforceHrefWhitelist($el: JQuery<HTMLElement>, hrefs: string[]): void {
|
function enforceHrefWhitelist($el: JQuery<HTMLElement>, hrefs: string[]): void {
|
||||||
$el.find('[href]').each(() => {
|
$el.find('[href]').each(function enforceHref() {
|
||||||
const $_el = $(this);
|
const $_el = $(this);
|
||||||
const attributeValue = $_el.attr('href');
|
const attributeValue = $_el.attr('href');
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "src",
|
"baseUrl": "src",
|
||||||
"target": "es5",
|
"target": "es6",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
|
|
Loading…
Reference in a new issue