mirror of
https://github.com/quantum5/totp.git
synced 2025-04-24 05:31:59 -04:00
Use URL-based state management
This commit is contained in:
parent
51df02bf3f
commit
230326ab9c
|
@ -53,7 +53,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
<p class="card-text">What this tool <strong>can't</strong> do:</p>
|
<p class="card-text">What this tool <strong>can't</strong> do:</p>
|
||||||
<ul class="card-text">
|
<ul class="card-text">
|
||||||
<li>Store your TOTP secrets;</li>
|
<li>Store your TOTP secrets (you can try bookmarking this page with the secret in the URL, but it's not secure);</li>
|
||||||
<li>Act as your general purpose authenticator app; and</li>
|
<li>Act as your general purpose authenticator app; and</li>
|
||||||
<li>Scan TOTP QR codes.</li>
|
<li>Scan TOTP QR codes.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
59
src/App.tsx
59
src/App.tsx
|
@ -3,16 +3,22 @@ import {NumberInput, TextInput} from './Input';
|
||||||
import OTPOutput, {HashAlgorithm} from './OTPOutput';
|
import OTPOutput, {HashAlgorithm} from './OTPOutput';
|
||||||
import Select from './Select';
|
import Select from './Select';
|
||||||
import Collapsible from './Collapsible';
|
import Collapsible from './Collapsible';
|
||||||
import ActionLink from './ActionLink.tsx';
|
import ActionLink from './ActionLink';
|
||||||
|
import {type State, defaults, serializeState, deserializeState} from './state';
|
||||||
|
|
||||||
|
function parseState() {
|
||||||
|
if (window.location.hash.startsWith('#!')) {
|
||||||
|
return deserializeState(window.location.hash.slice(2));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [advanced, setAdvanced] = useState(false);
|
const [advanced, setAdvanced] = useState(false);
|
||||||
|
|
||||||
const [secret, setSecret] = React.useState('');
|
const [state, setState] = React.useState(() => parseState() || defaults);
|
||||||
const [step, setStep] = React.useState(30);
|
const {secret, step, digits, algorithm} = state;
|
||||||
const [offset, setOffset] = React.useState(0);
|
const [offset, setOffset] = React.useState(0);
|
||||||
const [digits, setDigits] = React.useState(6);
|
|
||||||
const [algorithm, setAlgorithm] = React.useState<HashAlgorithm>('sha1');
|
|
||||||
|
|
||||||
const validStep = step > 0;
|
const validStep = step > 0;
|
||||||
const validDigits = digits > 0 && digits <= 10;
|
const validDigits = digits > 0 && digits <= 10;
|
||||||
|
@ -41,12 +47,47 @@ function App() {
|
||||||
setAdvanced(false);
|
setAdvanced(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onReset = React.useCallback(() => {
|
const setSecret = React.useCallback(
|
||||||
setStep(30);
|
(secret: string) => setState((state) => ({...state, secret})),
|
||||||
setDigits(6);
|
[],
|
||||||
setAlgorithm('sha1');
|
);
|
||||||
|
|
||||||
|
const setStep = React.useCallback(
|
||||||
|
(step: number) => setState((state) => ({...state, step})),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setDigits = React.useCallback(
|
||||||
|
(digits: number) => setState((state) => ({...state, digits})),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAlgorithm = React.useCallback(
|
||||||
|
(algorithm: string) => setState((state) => ({...state, algorithm})),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onReset = React.useCallback(
|
||||||
|
() => setState((state) => ({...state, step: 30, digits: 6, algorithm: 'sha1'})),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const value = serializeState(state);
|
||||||
|
console.log(value);
|
||||||
|
history.replaceState(null, '', window.location.pathname + window.location.search + (value && `#!${value}`));
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const onHashChange = React.useCallback(() => {
|
||||||
|
const state = parseState();
|
||||||
|
state && setState(state);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.addEventListener('hashchange', onHashChange);
|
||||||
|
return () => window.removeEventListener('hashchange', onHashChange);
|
||||||
|
}, [onHashChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="totp-app">
|
<div className="totp-app">
|
||||||
<div className="totp-settings">
|
<div className="totp-settings">
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {createDigest} from '@otplib/plugin-crypto-js';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import CopyButton from './CopyButton.tsx';
|
import CopyButton from './CopyButton.tsx';
|
||||||
|
|
||||||
const ALGORITHMS = {
|
export const ALGORITHMS = {
|
||||||
sha1: HashAlgorithms.SHA1,
|
sha1: HashAlgorithms.SHA1,
|
||||||
sha256: HashAlgorithms.SHA256,
|
sha256: HashAlgorithms.SHA256,
|
||||||
sha512: HashAlgorithms.SHA512,
|
sha512: HashAlgorithms.SHA512,
|
||||||
|
|
35
src/state.tsx
Normal file
35
src/state.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import {ALGORITHMS, HashAlgorithm} from './OTPOutput';
|
||||||
|
|
||||||
|
export type State = {
|
||||||
|
secret: string;
|
||||||
|
step: number;
|
||||||
|
digits: number;
|
||||||
|
algorithm: HashAlgorithm;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaults: State = {
|
||||||
|
secret: '',
|
||||||
|
step: 30,
|
||||||
|
digits: 6,
|
||||||
|
algorithm: 'sha1',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function serializeState(state: State): string {
|
||||||
|
const values = ['secret', 'step', 'digits', 'algorithm'].map(
|
||||||
|
(key) => state[key] !== defaults[key] ? encodeURIComponent(state[key]) : '',
|
||||||
|
);
|
||||||
|
while (values[values.length - 1] === '') {
|
||||||
|
values.pop();
|
||||||
|
}
|
||||||
|
return values.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deserializeState(data: string): State {
|
||||||
|
const values = data.split('/').map(decodeURIComponent);
|
||||||
|
return {
|
||||||
|
secret: values[0] || defaults.secret,
|
||||||
|
step: +values[1] || defaults.step,
|
||||||
|
digits: +values[2] || defaults.digits,
|
||||||
|
algorithm: ALGORITHMS[values[3]] !== undefined ? values[3] : defaults.algorithm,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue