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:
Jeremy Letto 2021-11-25 21:12:23 -06:00 committed by GitHub
parent 37879c4255
commit 6ce346af4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1381 additions and 1291 deletions

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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;
}
}

View file

@ -1,5 +0,0 @@
.dialog-title {
display: flex;
justify-content: space-between;
align-items: center;
}

View file

@ -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[] = [

View file

@ -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;
}

View file

@ -1,23 +1,38 @@
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">
const useStyles = makeStyles(theme => ({
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 && (
<div className="inputField-validation">
<div className="InputField-validation">
{
(error &&
<ThemedFieldError className="inputField-error">
<div className="InputField-error">
{error}
<ErrorOutlinedIcon style={{ fontSize: 'small', fontWeight: 'bold' }} />
</ThemedFieldError>
</div>
) ||
(warning && <ThemedFieldWarning className="inputField-warning">{warning}</ThemedFieldWarning>)
(warning && <div className="InputField-warning">{warning}</div>)
}
</div>
) }
@ -35,13 +50,6 @@ const InputField = ({ input, label, name, autoComplete, type, meta: { touched, e
/>
</div>
);
const ThemedFieldError = styled('div')(({ theme }) => ({
color: theme.palette.error.main
}));
const ThemedFieldWarning = styled('div')(({ theme }) => ({
color: theme.palette.warning.main
}));
};
export default InputField;

View file

@ -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;
}

View file

@ -1,77 +1,192 @@
// 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'>
<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>
<Select
id='KnownHosts-select'
labelId='KnownHosts-label'
label='Host'
margin='dense'
value={state.selectedHost}
name='host'
value={hostsState.selectedHost}
fullWidth={true}
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) => (
<MenuItem className='KnownHosts-item' value={index} key={index}>
hostsState.hosts.map((host, index) => (
<MenuItem className='KnownHosts-item' value={host} key={index}>
<div className='KnownHosts-item__label'>
<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>
{ 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' />
</IconButton>
) }
@ -80,6 +195,15 @@ const KnownHosts = ({ onChange }) => {
}
</Select>
</FormControl>
<KnownHostDialog
isOpen={dialogState.open}
host={dialogState.edit}
onRemove={handleDialogRemove}
onSubmit={handleDialogSubmit}
handleClose={closeKnownHostDialog}
/>
</div>
)
};

View file

@ -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';

View file

@ -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>
{

View file

@ -0,0 +1,5 @@
.dialog-title {
display: flex;
justify-content: space-between;
align-items: center;
}

View file

@ -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}>

View 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;
}

View 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;

View 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';

View 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;
}

View 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);

View file

@ -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));

View file

@ -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">

View file

@ -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">

View file

@ -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">

View file

@ -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';

View file

@ -1 +1,3 @@
export * from './useDebounce';
export * from './useAutoConnect';
export * from './useReduxEffect';

View 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];
}

View 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);
}

View file

@ -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)
}

View file

@ -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: {},

View file

@ -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);

View file

@ -16,4 +16,4 @@ export class SetDTO extends Set {
}
};
dexieService.cards.mapToClass(SetDTO);
dexieService.sets.mapToClass(SetDTO);

View 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);

View file

@ -1,4 +1,5 @@
export * from './CardDTO';
export * from './SetDTO';
export * from './SettingDTO';
export * from './TokenDTO';
export * from './HostDTO';

View 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',
});
}

View file

@ -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() {

View file

@ -0,0 +1 @@
export * from './DexieDTOs';

View file

@ -1,2 +1,2 @@
export * from './CardImporterService';
export * from './DexieDTOs';
export * from './dexie';

View file

@ -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

View file

@ -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));
},

View file

@ -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;
}

View file

@ -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',

View file

@ -10,3 +10,4 @@ export * from './routes';
export * from './sort';
export * from './forms';
export * from './message';
export * from './settings';

View file

@ -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,
},
];

View file

@ -0,0 +1,6 @@
export class Setting {
user: string;
autoConnect?: boolean;
}
export const APP_USER = '*app';

View file

@ -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);
}

View file

@ -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);

View file

@ -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,

View file

@ -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);
}

View file

@ -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';
}

View file

@ -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');

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"baseUrl": "src",
"target": "es5",
"target": "es6",
"lib": [
"dom",
"dom.iterable",