Files
standardnotes-app-web/app/assets/javascripts/components/AccountMenu/Authentication.tsx

390 lines
13 KiB
TypeScript

import { confirmDialog } from '@Services/alertService';
import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
STRING_GENERATING_LOGIN_KEYS,
STRING_GENERATING_REGISTER_KEYS,
STRING_NON_MATCHING_PASSWORDS
} from '@/strings';
import { JSXInternal } from 'preact/src/jsx';
import TargetedEvent = JSXInternal.TargetedEvent;
import TargetedKeyboardEvent = JSXInternal.TargetedKeyboardEvent;
import { WebApplication } from '@/ui_models/application';
import { useEffect, useRef, useState } from 'preact/hooks';
import TargetedMouseEvent = JSXInternal.TargetedMouseEvent;
import { User } from '@standardnotes/snjs/dist/@types/services/api/responses';
import { observer } from 'mobx-react-lite';
import { AppState } from '@/ui_models/app_state';
type Props = {
application: WebApplication;
appState: AppState;
closeAccountMenu: () => void;
notesAndTagsCount: number;
user: User | undefined;
}
const Authentication = observer(({
application,
appState,
closeAccountMenu,
notesAndTagsCount,
user
}: Props) => {
const [showAdvanced, setShowAdvanced] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState<string>('');
const [status, setStatus] = useState<string | undefined>(undefined);
const [isEmailFocused, setIsEmailFocused] = useState(false);
const [isEphemeral, setIsEphemeral] = useState(false);
const [isStrictSignIn, setIsStrictSignIn] = useState(false);
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
const {
server,
showLogin,
showRegister,
setShowLogin,
setShowRegister,
setServer
} = appState.accountMenu;
useEffect(() => {
if (isEmailFocused) {
emailInputRef.current.focus();
setIsEmailFocused(false);
}
}, [isEmailFocused]);
// Reset password and confirmation fields when hiding the form
useEffect(() => {
if (!showLogin && !showRegister) {
setPassword('');
setPasswordConfirmation('');
}
}, [showLogin, showRegister]);
const handleHostInputChange = (event: TargetedEvent<HTMLInputElement>) => {
const { value } = event.target as HTMLInputElement;
setServer(value);
application.setHost(value);
};
const emailInputRef = useRef<HTMLInputElement>();
const passwordInputRef = useRef<HTMLInputElement>();
const passwordConfirmationInputRef = useRef<HTMLInputElement>();
const handleSignInClick = () => {
setShowLogin(true);
setIsEmailFocused(true);
};
const handleRegisterClick = () => {
setShowRegister(true);
setIsEmailFocused(true);
};
const blurAuthFields = () => {
emailInputRef.current.blur();
passwordInputRef.current.blur();
passwordConfirmationInputRef.current?.blur();
};
const login = async () => {
setStatus(STRING_GENERATING_LOGIN_KEYS);
setIsAuthenticating(true);
const response = await application.signIn(
email,
password,
isStrictSignIn,
isEphemeral,
shouldMergeLocal
);
const error = response.error;
if (!error) {
setIsAuthenticating(false);
setPassword('');
setShowLogin(false);
closeAccountMenu();
return;
}
setShowLogin(true);
setStatus(undefined);
setPassword('');
if (error.message) {
await application.alertService.alert(error.message);
}
setIsAuthenticating(false);
};
const register = async () => {
if (passwordConfirmation !== password) {
application.alertService.alert(STRING_NON_MATCHING_PASSWORDS);
return;
}
setStatus(STRING_GENERATING_REGISTER_KEYS);
setIsAuthenticating(true);
const response = await application.register(
email,
password,
isEphemeral,
shouldMergeLocal
);
const error = response.error;
if (error) {
setStatus(undefined);
setIsAuthenticating(false);
application.alertService.alert(error.message);
} else {
setIsAuthenticating(false);
setShowRegister(false);
closeAccountMenu();
}
};
const handleAuthFormSubmit = (event:
TargetedEvent<HTMLFormElement> |
TargetedMouseEvent<HTMLButtonElement> |
TargetedKeyboardEvent<HTMLButtonElement>
) => {
event.preventDefault();
if (!email || !password) {
return;
}
blurAuthFields();
if (showLogin) {
login();
} else {
register();
}
};
const handleKeyPressKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
handleAuthFormSubmit(event as TargetedKeyboardEvent<HTMLButtonElement>);
}
};
const handlePasswordChange = (event: TargetedEvent<HTMLInputElement>) => {
const { value } = event.target as HTMLInputElement;
setPassword(value);
};
const handleEmailChange = (event: TargetedEvent<HTMLInputElement>) => {
const { value } = event.target as HTMLInputElement;
setEmail(value);
};
const handlePasswordConfirmationChange = (event: TargetedEvent<HTMLInputElement>) => {
const { value } = event.target as HTMLInputElement;
setPasswordConfirmation(value);
};
const handleMergeLocalData = async (event: TargetedEvent<HTMLInputElement>) => {
const { checked } = event.target as HTMLInputElement;
setShouldMergeLocal(checked);
if (!checked) {
const confirmResult = await confirmDialog({
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
confirmButtonStyle: 'danger'
});
setShouldMergeLocal(!confirmResult);
}
};
return (
<>
{!user && !showLogin && !showRegister && (
<div className="sk-panel-section sk-panel-hero">
<div className="sk-panel-row">
<div className="sk-h1">Sign in or register to enable sync and end-to-end encryption.</div>
</div>
<div className="flex my-1">
<button
className="sn-button info flex-grow text-base py-3 mr-1.5"
onClick={handleSignInClick}
>
Sign In
</button>
<button
className="sn-button info flex-grow text-base py-3 ml-1.5"
onClick={handleRegisterClick}
>
Register
</button>
</div>
<div className="sk-panel-row sk-p">
Standard Notes is free on every platform, and comes
standard with sync and encryption.
</div>
</div>
)}
{(showLogin || showRegister) && (
<div className="sk-panel-section">
<div className="sk-panel-section-title">
{showLogin ? 'Sign In' : 'Register'}
</div>
<form className="sk-panel-form" onSubmit={handleAuthFormSubmit} noValidate>
<div className="sk-panel-section">
<input
className="sk-input contrast"
name="email"
type="email"
value={email}
onChange={handleEmailChange}
placeholder="Email"
required
spellcheck={false}
ref={emailInputRef}
/>
<input
className="sk-input contrast"
name="password"
type="password"
value={password}
onChange={handlePasswordChange}
placeholder="Password"
required
onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown}
ref={passwordInputRef}
/>
{showRegister &&
<input
className="sk-input contrast"
name="password_conf"
type="password"
placeholder="Confirm Password"
required
onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown}
value={passwordConfirmation}
onChange={handlePasswordConfirmationChange}
ref={passwordConfirmationInputRef}
/>}
<div className="sk-panel-row" />
<button
type="button"
className="sn-button small info"
onClick={() => {
setShowAdvanced(!showAdvanced);
}}>
Advanced Options
</button>
</div>
{showAdvanced && (
<div className="sk-notification unpadded contrast advanced-options sk-panel-row">
<div className="sk-panel-column stretch">
<div className="sk-notification-title sk-panel-row padded-row">
Advanced Options
</div>
<div className="bordered-row padded-row">
<label className="sk-label">Sync Server Domain</label>
<input
className="sk-input sk-base"
name="server"
placeholder="Server URL"
onChange={handleHostInputChange}
value={server}
required
/>
</div>
{showLogin && (
<label className="sk-label padded-row sk-panel-row justify-left">
<div className="sk-horizontal-group tight cursor-pointer">
<input
className="sk-input"
type="checkbox"
checked={isStrictSignIn}
onChange={() => setIsStrictSignIn(prevState => !prevState)}
/>
<p className="sk-p">Use strict sign in</p>
<span>
<a className="info"
href="https://standardnotes.org/help/security" rel="noopener"
target="_blank"
>
(Learn more)
</a>
</span>
</div>
</label>
)}
</div>
</div>
)}
{!isAuthenticating && (
<div className="sk-panel-section form-submit">
<button className="sn-button info text-base py-3 text-center" type="submit"
disabled={isAuthenticating}>
{showLogin ? 'Sign In' : 'Register'}
</button>
</div>
)}
{showRegister && (
<div className="sk-notification neutral">
<div className="sk-notification-title">No Password Reset.</div>
<div className="sk-notification-text">
Because your notes are encrypted using your password,
Standard Notes does not have a password reset option.
You cannot forget your password.
</div>
</div>
)}
{status && (
<div className="sk-panel-section no-bottom-pad">
<div className="sk-horizontal-group">
<div className="sk-spinner small neutral" />
<div className="sk-label">{status}</div>
</div>
</div>
)}
{!isAuthenticating && (
<div className="sk-panel-section no-bottom-pad">
<label className="sk-panel-row justify-left">
<div className="sk-horizontal-group tight cursor-pointer">
<input
type="checkbox"
checked={!isEphemeral}
onChange={() => setIsEphemeral(prevState => !prevState)}
/>
<p className="sk-p">Stay signed in</p>
</div>
</label>
{notesAndTagsCount > 0 && (
<label className="sk-panel-row justify-left">
<div className="sk-horizontal-group tight cursor-pointer">
<input
type="checkbox"
checked={shouldMergeLocal}
onChange={handleMergeLocalData}
/>
<p className="sk-p">Merge local data ({notesAndTagsCount}) notes and tags</p>
</div>
</label>
)}
</div>
)}
</form>
</div>
)}</>
);
});
export default Authentication;