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
1473
webclient/package-lock.json
generated
1473
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",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dexie": "^3.0.3",
|
||||
"final-form": "^4.20.4",
|
||||
"jquery": "^3.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.7.2",
|
||||
"protobufjs": "^6.11.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-final-form": "^6.5.7",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"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 { WebSocketConnectReason, WebSocketOptions } from '../websocket/services/WebSocketService';
|
||||
|
||||
export default class AuthenticationService {
|
||||
static connect(options: WebSocketOptions): void {
|
||||
static login(options: WebSocketConnectOptions): void {
|
||||
SessionCommands.connect(options, WebSocketConnectReason.LOGIN);
|
||||
}
|
||||
|
||||
static register(options: WebSocketOptions): void {
|
||||
static register(options: WebSocketConnectOptions): void {
|
||||
SessionCommands.connect(options, WebSocketConnectReason.REGISTER);
|
||||
}
|
||||
|
||||
static activateAccount(options: WebSocketOptions): void {
|
||||
static activateAccount(options: WebSocketConnectOptions): void {
|
||||
SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT);
|
||||
}
|
||||
|
||||
static resetPasswordRequest(options: WebSocketOptions): void {
|
||||
static resetPasswordRequest(options: WebSocketConnectOptions): void {
|
||||
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_REQUEST);
|
||||
}
|
||||
|
||||
static resetPasswordChallenge(options: WebSocketOptions): void {
|
||||
static resetPasswordChallenge(options: WebSocketConnectOptions): void {
|
||||
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE);
|
||||
}
|
||||
|
||||
static resetPassword(options: WebSocketOptions): void {
|
||||
static resetPassword(options: WebSocketConnectOptions): void {
|
||||
SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET);
|
||||
}
|
||||
|
||||
|
@ -44,4 +43,8 @@ export default class AuthenticationService {
|
|||
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 { AuthenticationService, RoomsService } from 'api';
|
||||
import { CardImportDialog } from 'dialogs';
|
||||
import { Images } from 'images';
|
||||
import { RoomsSelectors, ServerSelectors } from 'store';
|
||||
import { Room, RouteEnum, User } from 'types';
|
||||
|
||||
import './Header.css';
|
||||
|
||||
import CardImportDialog from '../CardImportDialog/CardImportDialog';
|
||||
|
||||
class Header extends Component<HeaderProps> {
|
||||
state: HeaderState;
|
||||
options: string[] = [
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
.inputField {
|
||||
.InputField {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inputField-validation {
|
||||
.InputField-validation {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
@ -10,11 +10,11 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.inputField-error {
|
||||
.InputField-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputField-error svg {
|
||||
.InputField-error svg {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
|
|
@ -1,47 +1,55 @@
|
|||
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 ErrorOutlinedIcon from '@material-ui/icons/ErrorOutlined';
|
||||
|
||||
import './InputField.css';
|
||||
|
||||
const InputField = ({ input, label, name, autoComplete, type, meta: { touched, error, warning } }) => (
|
||||
<div className="inputField">
|
||||
{ touched && (
|
||||
<div className="inputField-validation">
|
||||
{
|
||||
(error &&
|
||||
<ThemedFieldError className="inputField-error">
|
||||
{error}
|
||||
<ErrorOutlinedIcon style={{ fontSize: 'small', fontWeight: 'bold' }} />
|
||||
</ThemedFieldError>
|
||||
) ||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
'& .InputField-error': {
|
||||
color: theme.palette.error.main
|
||||
},
|
||||
|
||||
(warning && <ThemedFieldWarning className="inputField-warning">{warning}</ThemedFieldWarning>)
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
|
||||
<TextField
|
||||
className="rounded"
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
fullWidth={true}
|
||||
label={label}
|
||||
name={name}
|
||||
type={type}
|
||||
autoComplete={autoComplete}
|
||||
{ ...input }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ThemedFieldError = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.error.main
|
||||
'& .InputField-warning': {
|
||||
color: theme.palette.warning.main
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const ThemedFieldWarning = styled('div')(({ theme }) => ({
|
||||
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 && (
|
||||
<div className="InputField-validation">
|
||||
{
|
||||
(error &&
|
||||
<div className="InputField-error">
|
||||
{error}
|
||||
<ErrorOutlinedIcon style={{ fontSize: 'small', fontWeight: 'bold' }} />
|
||||
</div>
|
||||
) ||
|
||||
|
||||
(warning && <div className="InputField-warning">{warning}</div>)
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
|
||||
<TextField
|
||||
className="rounded"
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
fullWidth={true}
|
||||
label={label}
|
||||
name={name}
|
||||
type={type}
|
||||
autoComplete={autoComplete}
|
||||
{ ...input }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputField;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.KnownHosts {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.KnownHosts-item__label {
|
||||
|
@ -15,6 +16,27 @@
|
|||
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 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.MuiSelect-selectMenu .KnownHosts-item__edit {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,85 +1,209 @@
|
|||
// 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 Button from '@material-ui/core/Button';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Check from '@material-ui/icons/Check';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import EditRoundedIcon from '@material-ui/icons/Edit';
|
||||
import ErrorOutlinedIcon from '@material-ui/icons/ErrorOutlined';
|
||||
|
||||
import { KnownHostDialog } from 'dialogs';
|
||||
import { HostDTO } from 'services';
|
||||
import { DefaultHosts, getHostPort } from 'types';
|
||||
import { DefaultHosts, Host, getHostPort } from 'types';
|
||||
|
||||
import './KnownHosts.css';
|
||||
|
||||
const KnownHosts = ({ onChange }) => {
|
||||
const [state, setState] = useState({
|
||||
const useStyles = makeStyles(theme => ({
|
||||
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: [],
|
||||
selectedHost: 0,
|
||||
selectedHost: {} as any,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
HostDTO.getAll().then(async hosts => {
|
||||
if (hosts?.length) {
|
||||
setState(s => ({ ...s, hosts }));
|
||||
} else {
|
||||
setState(s => ({ ...s, hosts: DefaultHosts }));
|
||||
await HostDTO.bulkAdd(DefaultHosts);
|
||||
}
|
||||
});
|
||||
const [dialogState, setDialogState] = useState({
|
||||
open: false,
|
||||
edit: null,
|
||||
});
|
||||
|
||||
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(() => {
|
||||
if (state.hosts.length) {
|
||||
onChange(getHostPort(state.hosts[state.selectedHost]));
|
||||
loadKnownHosts();
|
||||
}, [loadKnownHosts]);
|
||||
|
||||
useEffect(() => {
|
||||
const { hosts, selectedHost } = hostsState;
|
||||
|
||||
if (selectedHost?.id) {
|
||||
updateLastSelectedHost(selectedHost.id).then(() => {
|
||||
onChange(selectedHost);
|
||||
});
|
||||
}
|
||||
}, [state, onChange]);
|
||||
}, [hostsState, onChange]);
|
||||
|
||||
const selectHost = (selectedHost) => {
|
||||
setState(s => ({ ...s, selectedHost }));
|
||||
setHostsState(s => ({ ...s, selectedHost }));
|
||||
};
|
||||
|
||||
const addKnownHost = () => {
|
||||
console.log('KnownHosts->addKnownHost');
|
||||
const openAddKnownHostDialog = () => {
|
||||
setDialogState(s => ({ ...s, open: true, edit: null }));
|
||||
};
|
||||
|
||||
const editKnownHost = (hostIndex) => {
|
||||
console.log('KnownHosts->editKnownHost: ', state.hosts[hostIndex]);
|
||||
const openEditKnownHostDialog = (host: HostDTO) => {
|
||||
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 (
|
||||
<FormControl variant='outlined' className='KnownHosts'>
|
||||
<InputLabel id='KnownHosts-select'>Host</InputLabel>
|
||||
<Select
|
||||
id='KnownHosts-select'
|
||||
labelId='KnownHosts-label'
|
||||
label='Host'
|
||||
margin='dense'
|
||||
value={state.selectedHost}
|
||||
fullWidth={true}
|
||||
onChange={e => selectHost(e.target.value)}
|
||||
>
|
||||
<Button value={state.selectedHost} onClick={addKnownHost}>Add</Button>
|
||||
<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>
|
||||
) ||
|
||||
|
||||
{
|
||||
state.hosts.map((host, index) => (
|
||||
<MenuItem className='KnownHosts-item' value={index} key={index}>
|
||||
<div className='KnownHosts-item__label'>
|
||||
<Check />
|
||||
<span>{host.name} ({ getHostPort(state.hosts[index]).host }:{getHostPort(state.hosts[index]).port})</span>
|
||||
</div>
|
||||
(warning && <div className="KnownHosts-warning">{warning}</div>)
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ host.editable && (
|
||||
<IconButton size='small' color='primary' onClick={() => editKnownHost(index)}>
|
||||
<EditRoundedIcon fontSize='small' />
|
||||
</IconButton>
|
||||
) }
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<InputLabel id='KnownHosts-select'>Host</InputLabel>
|
||||
<Select
|
||||
id='KnownHosts-select'
|
||||
labelId='KnownHosts-label'
|
||||
label='Host'
|
||||
margin='dense'
|
||||
name='host'
|
||||
value={hostsState.selectedHost}
|
||||
fullWidth={true}
|
||||
onChange={e => selectHost(e.target.value)}
|
||||
>
|
||||
<Button value={hostsState.selectedHost} onClick={openAddKnownHostDialog}>
|
||||
<span>Add new host</span>
|
||||
<AddIcon fontSize='small' color='primary' />
|
||||
</Button>
|
||||
|
||||
{
|
||||
hostsState.hosts.map((host, index) => (
|
||||
<MenuItem className='KnownHosts-item' value={host} key={index}>
|
||||
<div className='KnownHosts-item__label'>
|
||||
<Check />
|
||||
<span>{host.name} ({ getHostPort(hostsState.hosts[index]).host }:{getHostPort(hostsState.hosts[index]).port})</span>
|
||||
</div>
|
||||
|
||||
{ host.editable && (
|
||||
<IconButton className='KnownHosts-item__edit' size='small' color='primary' onClick={(e) => {
|
||||
openEditKnownHostDialog(hostsState.hosts[index]);
|
||||
}}>
|
||||
<EditRoundedIcon fontSize='small' />
|
||||
</IconButton>
|
||||
) }
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</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 SelectField } from './SelectField/SelectField';
|
||||
export { default as ScrollToBottomOnChanges } from './ScrollToBottomOnChanges/ScrollToBottomOnChanges';
|
||||
export { default as RegistrationDialog } from './RegistrationDialog/RegistrationDialog';
|
||||
|
||||
// Guards
|
||||
export { default as AuthGuard } from './Guard/AuthGuard';
|
||||
export { default as ModGuard } from './Guard/ModGuard';
|
||||
|
||||
// Dialogs
|
||||
export { default as CardImportDialog } from './CardImportDialog/CardImportDialog';
|
||||
export { default as RequestPasswordResetDialog } from './RequestPasswordResetDialog/RequestPasswordResetDialog';
|
||||
export { default as ResetPasswordDialog } from './ResetPasswordDialog/ResetPasswordDialog';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// eslint-disable-next-line
|
||||
import React, {useState} from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { connect } from 'react-redux';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
@ -13,9 +13,10 @@ import { RequestPasswordResetDialog, ResetPasswordDialog } from 'components';
|
|||
import { LoginForm } from 'forms';
|
||||
import { useReduxEffect } from 'hooks';
|
||||
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 { SessionCommands } from 'websocket';
|
||||
|
||||
import './Login.css';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
|
@ -59,6 +60,7 @@ const Login = ({ state, description }: LoginProps) => {
|
|||
const classes = useStyles();
|
||||
const isConnected = AuthenticationService.isConnected(state);
|
||||
|
||||
const [hostIdToRemember, setHostIdToRemember] = useState(null);
|
||||
const [dialogState, setDialogState] = useState({
|
||||
passwordResetRequestDialog: false,
|
||||
resetPasswordDialog: false
|
||||
|
@ -73,6 +75,15 @@ const Login = ({ state, description }: LoginProps) => {
|
|||
closeResetPasswordDialog();
|
||||
}, 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 = () => {
|
||||
return !isConnected && description?.length;
|
||||
};
|
||||
|
@ -81,6 +92,47 @@ const Login = ({ state, description }: LoginProps) => {
|
|||
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 }) => {
|
||||
if (email) {
|
||||
AuthenticationService.resetPasswordChallenge({ user, email, host, port } as any);
|
||||
|
@ -123,7 +175,7 @@ const Login = ({ state, description }: LoginProps) => {
|
|||
<Typography variant="h1">Login</Typography>
|
||||
<Typography variant="subtitle1">A cross-platform virtual tabletop for multiplayer card games.</Typography>
|
||||
<div className="login-form">
|
||||
<LoginForm onSubmit={AuthenticationService.connect} />
|
||||
<LoginForm onSubmit={onSubmit} />
|
||||
</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 (
|
||||
<Dialog onClose={handleOnClose} open={isOpen}>
|
||||
<DialogTitle disableTypography className="dialog-title">
|
||||
<Typography variant="h6">Import Cards</Typography>
|
||||
<Typography variant="h2">Import Cards</Typography>
|
||||
|
||||
{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
|
||||
import React from "react";
|
||||
import React, { Component, useCallback, useEffect, useState, useRef } from 'react';
|
||||
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 { InputField, KnownHosts } from 'components';
|
||||
// import { ServerDispatch } from "store";
|
||||
import { FormKey } from 'types';
|
||||
import { AuthenticationService } from 'api';
|
||||
import { CheckboxField, InputField, KnownHosts } from 'components';
|
||||
import { useAutoConnect } from 'hooks';
|
||||
import { HostDTO, SettingDTO } from 'services';
|
||||
import { FormKey, APP_USER } from 'types';
|
||||
|
||||
import './LoginForm.css';
|
||||
|
||||
const LoginForm = (props) => {
|
||||
const { dispatch, handleSubmit } = props;
|
||||
const PASSWORD_LABEL = 'Password';
|
||||
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 = () => {
|
||||
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 (
|
||||
<Form className="loginForm" onSubmit={handleSubmit}>
|
||||
<div className="loginForm-items">
|
||||
<div className="loginForm-item">
|
||||
<Field label="Username" name="user" component={InputField} autoComplete="username" />
|
||||
<Form className='loginForm' onSubmit={handleSubmit}>
|
||||
<div className='loginForm-items'>
|
||||
<div className='loginForm-item'>
|
||||
<Field label='Username' name='userName' component={InputField} autoComplete='off' />
|
||||
</div>
|
||||
<div className="loginForm-item">
|
||||
<Field label="Password" name="pass" type="password" component={InputField} autoComplete="current-password" />
|
||||
<div className='loginForm-item'>
|
||||
<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 className="loginForm-actions">
|
||||
<span>Remember Me</span>
|
||||
<Button color="primary" onClick={forgotPassword}>Forgot Password</Button>
|
||||
<div className='loginForm-actions'>
|
||||
<Field label='Save Password' name='remember' component={CheckboxField} onChange={onRememberChange} />
|
||||
<Button color='primary' onClick={forgotPassword}>Forgot Password</Button>
|
||||
</div>
|
||||
<div className="loginForm-item">
|
||||
<KnownHosts onChange={onHostChange} />
|
||||
<div className='loginForm-item'>
|
||||
<Field name='selectedHost' component={KnownHosts} onChange={onHostChange} />
|
||||
</div>
|
||||
<div className='loginForm-actions'>
|
||||
<Field label='Auto Connect' name='autoConnect' component={CheckboxField} onChange={onAutoConnectChange} />
|
||||
</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
|
||||
</Button>
|
||||
</Form>
|
||||
|
@ -55,27 +139,28 @@ const propsMap = {
|
|||
if (!values.user) {
|
||||
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.port) {
|
||||
errors.port = 'Required';
|
||||
|
||||
if (!values.selectedHost) {
|
||||
errors.selectedHost = 'Required';
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
initialValues: {
|
||||
// host: "mtg.tetrarch.co/servatrice",
|
||||
// port: "443"
|
||||
// host: "server.cockatrice.us",
|
||||
// port: "4748"
|
||||
}
|
||||
interface LoginFormProps {
|
||||
form: string;
|
||||
dispatch: Function;
|
||||
submit: Function;
|
||||
handleSubmit: FormSubmitHandler;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(reduxForm(propsMap)(LoginForm));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Form, Field, reduxForm, change } from 'redux-form'
|
||||
|
||||
|
@ -13,15 +13,16 @@ import './RegisterForm.css';
|
|||
const RegisterForm = (props) => {
|
||||
const { dispatch, handleSubmit } = props;
|
||||
|
||||
const onHostChange = ({ host, port }) => {
|
||||
const onHostChange: any = ({ host, port }) => {
|
||||
dispatch(change(FormKey.REGISTER, 'host', host));
|
||||
dispatch(change(FormKey.REGISTER, 'port', port));
|
||||
}
|
||||
|
||||
return (
|
||||
<Form className="registerForm row" onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="leftRegisterForm column" >
|
||||
<div className="registerForm-item">
|
||||
<KnownHosts onChange={onHostChange} />
|
||||
<Field name="selectedHost" component={KnownHosts} onChange={onHostChange} />
|
||||
{ /* Padding is off */ }
|
||||
</div>
|
||||
<div className="registerForm-item">
|
||||
|
|
|
@ -17,7 +17,7 @@ const RequestPasswordResetForm = (props) => {
|
|||
const [errorMessage, setErrorMessage] = 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, 'port', port));
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ const RequestPasswordResetForm = (props) => {
|
|||
</div>
|
||||
) : null}
|
||||
<div className="RequestPasswordResetForm-item">
|
||||
<KnownHosts onChange={onHostChange} />
|
||||
<Field name='selectedHost' component={KnownHosts} onChange={onHostChange} />
|
||||
</div>
|
||||
</div>
|
||||
<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 onHostChange = ({ host, port }) => {
|
||||
const onHostChange: any = ({ host, port }) => {
|
||||
dispatch(change(FormKey.RESET_PASSWORD, 'host', host));
|
||||
dispatch(change(FormKey.RESET_PASSWORD, 'port', port));
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ const ResetPasswordForm = (props) => {
|
|||
<Field label="Password Again" name="passwordAgain" component={InputField} />
|
||||
</div>
|
||||
<div className="ResetPasswordForm-item">
|
||||
<KnownHosts onChange={onHostChange} />
|
||||
<Field name='selectedHost' component={KnownHosts} onChange={onHostChange} />
|
||||
</div>
|
||||
</div>
|
||||
<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 ConnectForm } from './ConnectForm/ConnectForm';
|
||||
export { default as LoginForm } from './LoginForm/LoginForm';
|
||||
export { default as KnownHostForm } from './KnownHostForm/KnownHostForm';
|
||||
export { default as RegisterForm } from './RegisterForm/RegisterForm';
|
||||
export { default as SearchForm } from './SearchForm/SearchForm';
|
||||
export { default as RequestPasswordResetForm } from './RequestPasswordResetForm/RequestPasswordResetForm';
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export * from './useDebounce';
|
||||
export * from './useAutoConnect';
|
||||
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 { useStore } from 'react-redux'
|
||||
import { castArray } from 'lodash'
|
||||
import { AnyAction } from 'redux'
|
||||
import { castArray } from 'lodash'
|
||||
|
||||
export type ReduxEffect = (action: AnyAction) => void
|
||||
|
||||
|
@ -23,25 +23,25 @@ export function useReduxEffect(
|
|||
type: string | string[],
|
||||
deps: DependencyList = [],
|
||||
): void {
|
||||
const currentValue = useRef(null)
|
||||
const store = useStore()
|
||||
const currentValue = useRef(null);
|
||||
const store = useStore();
|
||||
|
||||
const handleChange = (): void => {
|
||||
const state = store.getState()
|
||||
const action = state.action
|
||||
const previousValue = currentValue.current
|
||||
currentValue.current = action.count
|
||||
const state = store.getState();
|
||||
const action = state.action;
|
||||
const previousValue = currentValue.current;
|
||||
currentValue.current = action.count;
|
||||
|
||||
if (
|
||||
previousValue !== action.count &&
|
||||
castArray(type).includes(action.type)
|
||||
) {
|
||||
effect(action)
|
||||
effect(action);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe(handleChange)
|
||||
return (): void => unsubscribe()
|
||||
const unsubscribe = store.subscribe(handleChange);
|
||||
return (): void => unsubscribe();
|
||||
}, deps)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,22 @@ const palette = {
|
|||
dark: '#401C7F',
|
||||
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: {
|
||||
// main: '',
|
||||
// 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: {
|
||||
root: {
|
||||
padding: '8px',
|
||||
|
@ -135,7 +174,10 @@ export const materialTheme = createTheme({
|
|||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
// h2: {},
|
||||
h2: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
// h3: {},
|
||||
// h4: {},
|
||||
// h5: {},
|
||||
|
|
|
@ -8,11 +8,11 @@ export class HostDTO extends Host {
|
|||
return dexieService.hosts.put(this);
|
||||
}
|
||||
|
||||
static add(host: HostDTO): Promise<IndexableType> {
|
||||
static add(host: Host): Promise<IndexableType> {
|
||||
return dexieService.hosts.add(host);
|
||||
}
|
||||
|
||||
static get(id): Promise<HostDTO> {
|
||||
static get(id: number): Promise<HostDTO> {
|
||||
return dexieService.hosts.where('id').equals(id).first();
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,10 @@ export class HostDTO extends Host {
|
|||
static bulkAdd(hosts: Host[]): Promise<IndexableType> {
|
||||
return dexieService.hosts.bulkAdd(hosts);
|
||||
}
|
||||
|
||||
static delete(id: string): Promise<void> {
|
||||
return dexieService.hosts.delete(id);
|
||||
}
|
||||
};
|
||||
|
||||
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 './SetDTO';
|
||||
export * from './SettingDTO';
|
||||
export * from './TokenDTO';
|
||||
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';
|
||||
|
||||
enum Stores {
|
||||
CARDS = 'cards',
|
||||
SETS = 'sets',
|
||||
TOKENS = 'tokens',
|
||||
HOSTS = 'hosts',
|
||||
}
|
||||
|
||||
const StoreKeyIndexes = {
|
||||
[Stores.CARDS]: 'name',
|
||||
[Stores.SETS]: 'code',
|
||||
[Stores.TOKENS]: 'name.value',
|
||||
[Stores.HOSTS]: '++id,name',
|
||||
};
|
||||
import { Stores, schemaV1 } from './DexieSchemas/v1.schema';
|
||||
|
||||
class DexieService {
|
||||
private db: Dexie = new Dexie('Webatrice');
|
||||
|
||||
constructor() {
|
||||
this.db.version(1).stores(StoreKeyIndexes);
|
||||
schemaV1(this.db);
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return this.db.table(Stores.SETTINGS);
|
||||
}
|
||||
|
||||
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 './DexieDTOs';
|
||||
export * from './dexie';
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import { Type } from 'protobufjs';
|
||||
import { WebSocketConnectOptions } from 'types';
|
||||
|
||||
import { Types } from './server.types';
|
||||
|
||||
export const Actions = {
|
||||
clearStore: () => ({
|
||||
type: Types.CLEAR_STORE
|
||||
}),
|
||||
loginSuccessful: (options: WebSocketConnectOptions) => ({
|
||||
type: Types.LOGIN_SUCCESSFUL,
|
||||
options
|
||||
}),
|
||||
connectionClosed: reason => ({
|
||||
type: Types.CONNECTION_CLOSED,
|
||||
reason
|
||||
|
|
|
@ -6,6 +6,9 @@ export const Dispatch = {
|
|||
clearStore: () => {
|
||||
store.dispatch(Actions.clearStore());
|
||||
},
|
||||
loginSuccessful: options => {
|
||||
store.dispatch(Actions.loginSuccessful(options));
|
||||
},
|
||||
connectionClosed: reason => {
|
||||
store.dispatch(Actions.connectionClosed(reason));
|
||||
},
|
||||
|
|
|
@ -3,24 +3,26 @@ import { Log, SortBy, User, UserSortField } from 'types';
|
|||
export interface ServerConnectParams {
|
||||
host: string;
|
||||
port: string;
|
||||
user: string;
|
||||
pass: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ServerRegisterParams {
|
||||
host: string;
|
||||
port: string;
|
||||
user: string;
|
||||
pass: string;
|
||||
passAgain: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
email: string;
|
||||
country: string;
|
||||
realName: string;
|
||||
}
|
||||
|
||||
export interface RequestPasswordSaltParams {
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export interface ForgotPasswordParams {
|
||||
user: string;
|
||||
clientid: string;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export interface ForgotPasswordChallengeParams extends ForgotPasswordParams {
|
||||
|
@ -33,8 +35,7 @@ export interface ForgotPasswordResetParams extends ForgotPasswordParams {
|
|||
}
|
||||
|
||||
export interface AccountActivationParams extends ServerRegisterParams {
|
||||
activationCode: string;
|
||||
clientid: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface ServerState {
|
||||
|
@ -68,7 +69,3 @@ export interface ServerStateLogs {
|
|||
export interface ServerStateSortUsersBy extends SortBy {
|
||||
field: UserSortField
|
||||
}
|
||||
|
||||
export interface RequestPasswordSaltParams {
|
||||
user: string;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export const Types = {
|
||||
CLEAR_STORE: '[Server] Clear Store',
|
||||
LOGIN_SUCCESSFUL: '[Server] Login Successful',
|
||||
CONNECTION_CLOSED: '[Server] Connection Closed',
|
||||
SERVER_MESSAGE: '[Server] Server Message',
|
||||
UPDATE_BUDDY_LIST: '[Server] Update Buddy List',
|
||||
|
|
|
@ -10,3 +10,4 @@ export * from './routes';
|
|||
export * from './sort';
|
||||
export * from './forms';
|
||||
export * from './message';
|
||||
export * from './settings';
|
||||
|
|
|
@ -12,6 +12,29 @@ export enum StatusEnum {
|
|||
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 {
|
||||
id?: number;
|
||||
name: string;
|
||||
|
@ -20,6 +43,10 @@ export class Host {
|
|||
localHost?: string;
|
||||
localPort?: string;
|
||||
editable: boolean;
|
||||
lastSelected?: boolean;
|
||||
userName?: string;
|
||||
hashedPassword?: string;
|
||||
remember?: boolean;
|
||||
}
|
||||
|
||||
export const DefaultHosts: Host[] = [
|
||||
|
@ -40,7 +67,7 @@ export const DefaultHosts: Host[] = [
|
|||
{
|
||||
name: 'Tetrarch',
|
||||
host: 'mtg.tetrarch.co/servatrice',
|
||||
port: '4748',
|
||||
port: '443',
|
||||
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 { WebSocketOptions, WebSocketService } from './services/WebSocketService';
|
||||
import { WebSocketService } from './services/WebSocketService';
|
||||
|
||||
import { RoomPersistence, SessionPersistence } from './persistence';
|
||||
|
||||
|
@ -30,11 +30,12 @@ export class WebClient {
|
|||
]
|
||||
};
|
||||
|
||||
public options: WebSocketOptions = {
|
||||
public options: WebSocketConnectOptions = {
|
||||
host: '',
|
||||
port: '',
|
||||
user: '',
|
||||
pass: '',
|
||||
userName: '',
|
||||
password: '',
|
||||
hashedPassword: '',
|
||||
newPassword: '',
|
||||
email: '',
|
||||
clientid: null,
|
||||
|
@ -43,6 +44,8 @@ export class WebClient {
|
|||
keepalive: 5000
|
||||
};
|
||||
|
||||
public connectionAttemptMade = false;
|
||||
|
||||
constructor() {
|
||||
this.socket.message$.subscribe((message: MessageEvent) => {
|
||||
this.protobuf.handleMessageEvent(message);
|
||||
|
@ -55,7 +58,8 @@ export class WebClient {
|
|||
console.log(this);
|
||||
}
|
||||
|
||||
public connect(options: WebSocketOptions) {
|
||||
public connect(options: WebSocketConnectOptions) {
|
||||
this.connectionAttemptMade = true;
|
||||
this.options = { ...this.options, ...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 webClient from '../WebClient';
|
||||
import { guid, hashPassword } from '../utils';
|
||||
import { WebSocketConnectReason, WebSocketOptions } from '../services/WebSocketService';
|
||||
import {
|
||||
AccountActivationParams,
|
||||
ForgotPasswordChallengeParams,
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
import NormalizeService from '../utils/NormalizeService';
|
||||
|
||||
export class SessionCommands {
|
||||
static connect(options: WebSocketOptions, reason: WebSocketConnectReason): void {
|
||||
static connect(options: WebSocketConnectOptions, reason: WebSocketConnectReason): void {
|
||||
switch (reason) {
|
||||
case WebSocketConnectReason.LOGIN:
|
||||
case WebSocketConnectReason.REGISTER:
|
||||
|
@ -38,16 +38,18 @@ export class SessionCommands {
|
|||
}
|
||||
|
||||
static login(passwordSalt?: string): void {
|
||||
const { userName, password, hashedPassword } = webClient.options;
|
||||
|
||||
const loginConfig: any = {
|
||||
...webClient.clientConfig,
|
||||
userName: webClient.options.user,
|
||||
clientid: guid()
|
||||
clientid: 'webatrice',
|
||||
userName,
|
||||
};
|
||||
|
||||
if (passwordSalt) {
|
||||
loginConfig.hashedPassword = hashPassword(passwordSalt, webClient.options.pass);
|
||||
loginConfig.hashedPassword = hashedPassword || hashPassword(passwordSalt, password);
|
||||
} else {
|
||||
loginConfig.password = webClient.options.pass;
|
||||
loginConfig.password = password;
|
||||
}
|
||||
|
||||
const CmdLogin = webClient.protobuf.controller.Command_Login.create(loginConfig);
|
||||
|
@ -65,11 +67,13 @@ export class SessionCommands {
|
|||
SessionPersistence.updateBuddyList(buddyList);
|
||||
SessionPersistence.updateIgnoreList(ignoreList);
|
||||
SessionPersistence.updateUser(userInfo);
|
||||
SessionPersistence.loginSuccessful(loginConfig);
|
||||
|
||||
SessionCommands.listUsers();
|
||||
SessionCommands.listRooms();
|
||||
|
||||
SessionCommands.updateStatus(StatusEnum.LOGGED_IN, 'Logged in.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -117,11 +121,11 @@ export class SessionCommands {
|
|||
}
|
||||
|
||||
static requestPasswordSalt(): void {
|
||||
const options = webClient.options as unknown as RequestPasswordSaltParams;
|
||||
const { userName } = webClient.options as unknown as RequestPasswordSaltParams;
|
||||
|
||||
const registerConfig = {
|
||||
...webClient.clientConfig,
|
||||
userName: options.user,
|
||||
userName,
|
||||
};
|
||||
|
||||
const CmdRequestPasswordSalt = webClient.protobuf.controller.Command_RequestPasswordSalt.create(registerConfig);
|
||||
|
@ -132,35 +136,35 @@ export class SessionCommands {
|
|||
|
||||
webClient.protobuf.sendSessionCommand(sc, raw => {
|
||||
switch (raw.responseCode) {
|
||||
case webClient.protobuf.controller.Response.ResponseCode.RespOk:
|
||||
case webClient.protobuf.controller.Response.ResponseCode.RespOk: {
|
||||
const passwordSalt = raw['.Response_PasswordSalt.ext'].passwordSalt;
|
||||
SessionCommands.login(passwordSalt);
|
||||
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.disconnect();
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason');
|
||||
SessionCommands.disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 = {
|
||||
...webClient.clientConfig,
|
||||
userName: options.user,
|
||||
password: options.pass,
|
||||
email: options.email,
|
||||
country: options.country,
|
||||
realName: options.realName,
|
||||
clientid: 'webatrice'
|
||||
clientid: 'webatrice',
|
||||
userName,
|
||||
password,
|
||||
email,
|
||||
country,
|
||||
realName,
|
||||
};
|
||||
|
||||
const CmdRegister = webClient.protobuf.controller.Command_Register.create(registerConfig);
|
||||
|
@ -222,13 +226,13 @@ export class SessionCommands {
|
|||
};
|
||||
|
||||
static activateAccount(): void {
|
||||
const options = webClient.options as unknown as AccountActivationParams;
|
||||
const { userName, token } = webClient.options as unknown as AccountActivationParams;
|
||||
|
||||
const accountActivationConfig = {
|
||||
...webClient.clientConfig,
|
||||
userName: options.user,
|
||||
clientid: options.clientid,
|
||||
token: options.activationCode
|
||||
clientid: 'webatrice',
|
||||
userName,
|
||||
token,
|
||||
};
|
||||
|
||||
const CmdActivate = webClient.protobuf.controller.Command_Activate.create(accountActivationConfig);
|
||||
|
@ -249,12 +253,12 @@ export class SessionCommands {
|
|||
}
|
||||
|
||||
static resetPasswordRequest(): void {
|
||||
const options = webClient.options as unknown as ForgotPasswordParams;
|
||||
const { userName } = webClient.options as unknown as ForgotPasswordParams;
|
||||
|
||||
const forgotPasswordConfig = {
|
||||
...webClient.clientConfig,
|
||||
userName: options.user,
|
||||
clientid: options.clientid
|
||||
clientid: 'webatrice',
|
||||
userName,
|
||||
};
|
||||
|
||||
const CmdForgotPasswordRequest = webClient.protobuf.controller.Command_ForgotPasswordRequest.create(forgotPasswordConfig);
|
||||
|
@ -284,13 +288,13 @@ export class SessionCommands {
|
|||
}
|
||||
|
||||
static resetPasswordChallenge(): void {
|
||||
const options = webClient.options as unknown as ForgotPasswordChallengeParams;
|
||||
const { userName, email } = webClient.options as unknown as ForgotPasswordChallengeParams;
|
||||
|
||||
const forgotPasswordChallengeConfig = {
|
||||
...webClient.clientConfig,
|
||||
userName: options.user,
|
||||
clientid: options.clientid,
|
||||
email: options.email
|
||||
clientid: 'webatrice',
|
||||
userName,
|
||||
email,
|
||||
};
|
||||
|
||||
const CmdForgotPasswordChallenge = webClient.protobuf.controller.Command_ForgotPasswordChallenge.create(forgotPasswordChallengeConfig);
|
||||
|
@ -313,14 +317,14 @@ export class SessionCommands {
|
|||
}
|
||||
|
||||
static resetPassword(): void {
|
||||
const options = webClient.options as unknown as ForgotPasswordResetParams;
|
||||
const { userName, token, newPassword } = webClient.options as unknown as ForgotPasswordResetParams;
|
||||
|
||||
const forgotPasswordResetConfig = {
|
||||
...webClient.clientConfig,
|
||||
userName: options.user,
|
||||
clientid: options.clientid,
|
||||
token: options.token,
|
||||
newPassword: options.newPassword
|
||||
clientid: 'webatrice',
|
||||
userName,
|
||||
token,
|
||||
newPassword,
|
||||
};
|
||||
|
||||
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 { RoomPersistence, SessionPersistence } from '../persistence';
|
||||
import { ProtobufEvents } from '../services/ProtobufService';
|
||||
import webClient from '../WebClient';
|
||||
import { WebSocketConnectReason } from '../services/WebSocketService';
|
||||
|
||||
export const SessionEvents: ProtobufEvents = {
|
||||
'.Event_AddToList.ext': addToList,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ServerDispatch } from 'store';
|
||||
import { Log, StatusEnum, User } from 'types';
|
||||
import { Log, StatusEnum, User, WebSocketConnectOptions } from 'types';
|
||||
|
||||
import { sanitizeHtml } from 'websocket/utils';
|
||||
import NormalizeService from '../utils/NormalizeService';
|
||||
|
@ -9,6 +9,10 @@ export class SessionPersistence {
|
|||
ServerDispatch.clearStore();
|
||||
}
|
||||
|
||||
static loginSuccessful(options: WebSocketConnectOptions) {
|
||||
ServerDispatch.loginSuccessful(options);
|
||||
}
|
||||
|
||||
static connectionClosed(reason: number) {
|
||||
ServerDispatch.connectionClosed(reason);
|
||||
}
|
||||
|
|
|
@ -1,32 +1,10 @@
|
|||
import { Subject } from 'rxjs';
|
||||
|
||||
import { ServerStatus, StatusEnum } from 'types';
|
||||
import { ServerStatus, StatusEnum, WebSocketConnectOptions } from 'types';
|
||||
|
||||
import { KeepAliveService } from './KeepAliveService';
|
||||
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 {
|
||||
private socket: WebSocket;
|
||||
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') {
|
||||
protocol = 'ws';
|
||||
}
|
||||
|
|
|
@ -17,13 +17,13 @@ export function sanitizeHtml(msg: string): string {
|
|||
}
|
||||
|
||||
function enforceTagWhitelist($el: JQuery<HTMLElement>, tags: string): void {
|
||||
$el.find('*').not(tags).each(() => {
|
||||
$el.find('*').not(tags).each(function enforceTag() {
|
||||
$(this).replaceWith(this.innerHTML);
|
||||
});
|
||||
}
|
||||
|
||||
function enforceAttrWhitelist($el: JQuery<HTMLElement>, attrs: string[]): void {
|
||||
$el.find('*').each(() => {
|
||||
$el.find('*').each(function enforceAttribute() {
|
||||
const attributes = this.attributes;
|
||||
let i = attributes.length;
|
||||
while (i--) {
|
||||
|
@ -36,7 +36,7 @@ function enforceAttrWhitelist($el: JQuery<HTMLElement>, attrs: string[]): void {
|
|||
}
|
||||
|
||||
function enforceHrefWhitelist($el: JQuery<HTMLElement>, hrefs: string[]): void {
|
||||
$el.find('[href]').each(() => {
|
||||
$el.find('[href]').each(function enforceHref() {
|
||||
const $_el = $(this);
|
||||
const attributeValue = $_el.attr('href');
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
|
|
Loading…
Reference in a new issue