mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
225 lines
6.9 KiB
TypeScript
225 lines
6.9 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Form, Field, useFormState, FormApi } from 'react-final-form';
|
|
import { OnChange } from 'react-final-form-listeners';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import Button from '@mui/material/Button';
|
|
import Checkbox from '@mui/material/Checkbox';
|
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
|
|
|
import { CheckboxField, InputField, KnownHosts } from '@app/components';
|
|
import { LoadingState, useKnownHosts, useSettings } from '@app/hooks';
|
|
|
|
import './LoginForm.css';
|
|
|
|
interface LoginFormProps {
|
|
onSubmit: (values: any) => void;
|
|
disableSubmitButton: boolean;
|
|
onResetPassword: () => void;
|
|
}
|
|
|
|
interface LoginFormBodyProps extends LoginFormProps {
|
|
form: FormApi;
|
|
handleSubmit: (event?: React.SyntheticEvent) => void;
|
|
}
|
|
|
|
const LoginFormBody = ({
|
|
form,
|
|
handleSubmit,
|
|
disableSubmitButton,
|
|
onResetPassword,
|
|
}: LoginFormBodyProps) => {
|
|
const { t } = useTranslation();
|
|
const PASSWORD_LABEL = t('Common.label.password');
|
|
const STORED_PASSWORD_LABEL = `* ${t('LoginForm.label.savedPassword')} *`;
|
|
|
|
const settings = useSettings();
|
|
const hosts = useKnownHosts();
|
|
const { values } = useFormState();
|
|
|
|
const selectedHost = hosts.status === LoadingState.READY ? hosts.value?.selectedHost : undefined;
|
|
|
|
const [useStoredPasswordLabel, setUseStoredPasswordLabel] = useState(false);
|
|
const [storedHashInvalidated, setStoredHashInvalidated] = useState(false);
|
|
|
|
const canUseStoredPassword = (remember: boolean, password: string | undefined) =>
|
|
Boolean(remember && selectedHost?.hashedPassword && !password && !storedHashInvalidated);
|
|
|
|
const togglePasswordLabel = (on: boolean) => setUseStoredPasswordLabel(on);
|
|
|
|
// @critical Host-sync must not touch autoConnect — app-level setting, not per-host.
|
|
useEffect(() => {
|
|
if (!selectedHost) {
|
|
return;
|
|
}
|
|
|
|
form.change('userName', selectedHost.userName);
|
|
form.change('password', '');
|
|
form.change('remember', Boolean(selectedHost.remember));
|
|
|
|
setStoredHashInvalidated(false);
|
|
togglePasswordLabel(
|
|
Boolean(selectedHost.remember && selectedHost.hashedPassword)
|
|
);
|
|
}, [selectedHost, form]);
|
|
|
|
useEffect(() => {
|
|
if (settings.status !== LoadingState.READY) {
|
|
return;
|
|
}
|
|
form.change('autoConnect', settings.value?.autoConnect);
|
|
}, [settings, form]);
|
|
|
|
const onUserNameChange = (userName: string | undefined) => {
|
|
const fieldChanged = selectedHost?.userName?.toLowerCase() !== userName?.toLowerCase();
|
|
if (canUseStoredPassword(values.remember, values.password) && fieldChanged) {
|
|
setStoredHashInvalidated(true);
|
|
}
|
|
};
|
|
|
|
const onRememberChange = (checked: boolean) => {
|
|
// @critical Writes form-only, never to persisted setting — "remember" toggle isn't a preference edit.
|
|
if (!checked && values.autoConnect) {
|
|
form.change('autoConnect', false);
|
|
}
|
|
|
|
togglePasswordLabel(canUseStoredPassword(checked, values.password));
|
|
};
|
|
|
|
// @critical Only persist-path for autoConnect; wired to native onChange, not <OnChange>,
|
|
// to avoid leaking form.change() writes into Dexie.
|
|
const onUserToggleAutoConnect = (checked: boolean, fieldOnChange: (v: boolean) => void) => {
|
|
fieldOnChange(checked);
|
|
|
|
if (settings.status === LoadingState.READY) {
|
|
void settings.update({ autoConnect: checked });
|
|
}
|
|
|
|
if (checked && !values.remember) {
|
|
form.change('remember', true);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form className="loginForm" onSubmit={handleSubmit}>
|
|
<div className="loginForm-items">
|
|
<div className="loginForm-item">
|
|
<Field
|
|
label={t('Common.label.username')}
|
|
name="userName"
|
|
component={InputField}
|
|
autoComplete="username"
|
|
/>
|
|
<OnChange name="userName">{onUserNameChange}</OnChange>
|
|
</div>
|
|
<div className="loginForm-item">
|
|
<Field
|
|
label={useStoredPasswordLabel ? STORED_PASSWORD_LABEL : PASSWORD_LABEL}
|
|
onFocus={() => setUseStoredPasswordLabel(false)}
|
|
onBlur={() =>
|
|
togglePasswordLabel(canUseStoredPassword(values.remember, values.password))
|
|
}
|
|
name="password"
|
|
type="password"
|
|
component={InputField}
|
|
autoComplete="new-password"
|
|
/>
|
|
</div>
|
|
<div className="loginForm-actions">
|
|
<Field
|
|
label={t('LoginForm.label.savePassword')}
|
|
name="remember"
|
|
component={CheckboxField}
|
|
/>
|
|
<OnChange name="remember">{onRememberChange}</OnChange>
|
|
|
|
<Button color="primary" onClick={onResetPassword}>
|
|
{t('LoginForm.label.forgot')}
|
|
</Button>
|
|
</div>
|
|
<div className="loginForm-item">
|
|
<Field name="selectedHost" component={KnownHosts} />
|
|
</div>
|
|
<div className="loginForm-actions">
|
|
<Field name="autoConnect" type="checkbox">
|
|
{({ input }) => (
|
|
<FormControlLabel
|
|
className="checkbox-field"
|
|
label={t('LoginForm.label.autoConnect')}
|
|
control={
|
|
<Checkbox
|
|
className="checkbox-field__box"
|
|
checked={!!input.value}
|
|
onChange={(_e, checked) => onUserToggleAutoConnect(checked, input.onChange)}
|
|
color="primary"
|
|
/>
|
|
}
|
|
/>
|
|
)}
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
className="loginForm-submit rounded tall"
|
|
color="primary"
|
|
variant="contained"
|
|
type="submit"
|
|
disabled={disableSubmitButton}
|
|
>
|
|
{t('LoginForm.label.login')}
|
|
</Button>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
const LoginForm = (props: LoginFormProps) => {
|
|
const { t } = useTranslation();
|
|
const knownHosts = useKnownHosts();
|
|
const settings = useSettings();
|
|
|
|
const validate = (values: any) => {
|
|
const errors: any = {};
|
|
|
|
if (!values.userName) {
|
|
errors.userName = t('Common.validation.required');
|
|
}
|
|
if (!values.selectedHost) {
|
|
errors.selectedHost = t('Common.validation.required');
|
|
}
|
|
|
|
return errors;
|
|
};
|
|
|
|
const handleOnSubmit = ({ userName, ...values }: any) => {
|
|
userName = userName?.trim();
|
|
props.onSubmit({ userName, ...values });
|
|
};
|
|
|
|
if (knownHosts.status !== LoadingState.READY || settings.status !== LoadingState.READY) {
|
|
return null;
|
|
}
|
|
|
|
const selectedHost = knownHosts.value?.selectedHost;
|
|
const initialValues = {
|
|
selectedHost,
|
|
userName: selectedHost?.userName ?? '',
|
|
remember: Boolean(selectedHost?.remember),
|
|
autoConnect: Boolean(settings.value?.autoConnect),
|
|
password: '',
|
|
};
|
|
|
|
return (
|
|
<Form
|
|
onSubmit={handleOnSubmit}
|
|
validate={validate}
|
|
initialValues={initialValues}
|
|
keepDirtyOnReinitialize
|
|
>
|
|
{({ handleSubmit, form }) => (
|
|
<LoginFormBody {...props} form={form} handleSubmit={handleSubmit} />
|
|
)}
|
|
</Form>
|
|
);
|
|
};
|
|
|
|
export default LoginForm;
|