mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-13 08:35:08 +01:00
Add "top" part to large card, create auth input, add captcha things, put rest of auth flow in cards
Co-authored-by: William Oldham <github@binaryoverload.co.uk>
This commit is contained in:
parent
a25b3dee54
commit
a5512b95e5
@ -27,6 +27,7 @@
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-ga4": "^2.0.0",
|
||||
"react-google-recaptcha-v3": "^1.10.1",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-i18next": "^12.1.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
|
@ -80,6 +80,9 @@ dependencies:
|
||||
react-ga4:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.0
|
||||
react-google-recaptcha-v3:
|
||||
specifier: ^1.10.1
|
||||
version: 1.10.1(react-dom@17.0.2)(react@17.0.2)
|
||||
react-helmet-async:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0(react-dom@17.0.2)(react@17.0.2)
|
||||
@ -5187,6 +5190,17 @@ packages:
|
||||
resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==}
|
||||
dev: false
|
||||
|
||||
/react-google-recaptcha-v3@1.10.1(react-dom@17.0.2)(react@17.0.2):
|
||||
resolution: {integrity: sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==}
|
||||
peerDependencies:
|
||||
react: ^16.3 || ^17.0 || ^18.0
|
||||
react-dom: ^17.0 || ^18.0
|
||||
dependencies:
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
dev: false
|
||||
|
||||
/react-helmet-async@1.3.0(react-dom@17.0.2)(react@17.0.2):
|
||||
resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==}
|
||||
peerDependencies:
|
||||
|
@ -6,7 +6,7 @@ import { Icon, Icons } from "./Icon";
|
||||
export function PassphaseDisplay(props: { mnemonic: string }) {
|
||||
const individualWords = props.mnemonic.split(" ");
|
||||
|
||||
const [_, copy] = useCopyToClipboard();
|
||||
const [, copy] = useCopyToClipboard();
|
||||
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
|
||||
|
@ -1,7 +1,17 @@
|
||||
export function LargeCard(props: { children: React.ReactNode }) {
|
||||
export function LargeCard(props: {
|
||||
children: React.ReactNode;
|
||||
top?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl bg-largeCard-background bg-opacity-50 max-w-[600px] mx-auto p-[3rem]">
|
||||
{props.children}
|
||||
<div className="flex flex-col items-center">
|
||||
{props.top ? (
|
||||
<div className="inline-block transform translate-y-1/2">
|
||||
{props.top}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="w-full rounded-xl bg-largeCard-background bg-opacity-50 max-w-[600px] mx-auto p-[3rem]">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export function Input(props: {
|
||||
/>
|
||||
<input
|
||||
placeholder="Search"
|
||||
className="w-full py-2 px-3 pl-[calc(0.75rem+24px)] bg-video-context-inputBg rounded placeholder:text-video-context-inputPlaceholder"
|
||||
className="w-full py-2 px-3 pl-[calc(0.75rem+24px)] focus:outline-none bg-video-context-inputBg rounded placeholder:text-video-context-inputPlaceholder"
|
||||
value={props.value}
|
||||
onInput={(e) => props.onInput(e.currentTarget.value)}
|
||||
/>
|
||||
|
22
src/components/text-inputs/AuthInputBox.tsx
Normal file
22
src/components/text-inputs/AuthInputBox.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { TextInputControl } from "./TextInputControl";
|
||||
|
||||
export function AuthInputBox(props: {
|
||||
value?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (data: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{props.label ? (
|
||||
<p className="font-bold text-white">{props.label}</p>
|
||||
) : null}
|
||||
<TextInputControl
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
placeholder={props.placeholder}
|
||||
className="w-full flex-1 bg-authentication-inputBg px-4 py-3 text-search-text focus:outline-none rounded-lg placeholder:text-gray-700"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -19,6 +19,7 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
export interface RegistrationData {
|
||||
recaptchaToken: string;
|
||||
mnemonic: string;
|
||||
userData: {
|
||||
device: string;
|
||||
@ -89,7 +90,10 @@ export function useAuth() {
|
||||
|
||||
const register = useCallback(
|
||||
async (registerData: RegistrationData) => {
|
||||
const { challenge } = await getRegisterChallengeToken(backendUrl);
|
||||
const { challenge } = await getRegisterChallengeToken(
|
||||
backendUrl,
|
||||
registerData.recaptchaToken
|
||||
);
|
||||
const keys = await keysFromMnemonic(registerData.mnemonic);
|
||||
const signature = await signChallenge(keys, challenge);
|
||||
const registerResult = await registerAccount(backendUrl, {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
|
||||
|
||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||
import {
|
||||
@ -8,47 +9,51 @@ import {
|
||||
import { PassphraseGeneratePart } from "@/pages/parts/auth/PassphraseGeneratePart";
|
||||
import { TrustBackendPart } from "@/pages/parts/auth/TrustBackendPart";
|
||||
import { VerifyPassphrase } from "@/pages/parts/auth/VerifyPassphrasePart";
|
||||
import { conf } from "@/setup/config";
|
||||
|
||||
export function RegisterPage() {
|
||||
const [step, setStep] = useState(0);
|
||||
const [mnemonic, setMnemonic] = useState<null | string>(null);
|
||||
const [account, setAccount] = useState<null | AccountProfile>(null);
|
||||
const reCaptchaKey = conf().RECAPTCHA_SITE_KEY;
|
||||
|
||||
return (
|
||||
<SubPageLayout>
|
||||
{step === 0 ? (
|
||||
<TrustBackendPart
|
||||
onNext={() => {
|
||||
setStep(1);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{step === 1 ? (
|
||||
<PassphraseGeneratePart
|
||||
onNext={(n) => {
|
||||
setMnemonic(n);
|
||||
setStep(2);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{step === 2 ? (
|
||||
<AccountCreatePart
|
||||
onNext={(v) => {
|
||||
setAccount(v);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{step === 3 ? (
|
||||
<VerifyPassphrase
|
||||
mnemonic={mnemonic}
|
||||
userData={account}
|
||||
onNext={() => {
|
||||
setStep(4);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{step === 4 ? <p>Success, account now exists</p> : null}
|
||||
</SubPageLayout>
|
||||
<GoogleReCaptchaProvider reCaptchaKey={reCaptchaKey}>
|
||||
<SubPageLayout>
|
||||
{step === 0 ? (
|
||||
<TrustBackendPart
|
||||
onNext={() => {
|
||||
setStep(1);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{step === 1 ? (
|
||||
<PassphraseGeneratePart
|
||||
onNext={(m) => {
|
||||
setMnemonic(m);
|
||||
setStep(2);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{step === 2 ? (
|
||||
<AccountCreatePart
|
||||
onNext={(a) => {
|
||||
setAccount(a);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{step === 3 ? (
|
||||
<VerifyPassphrase
|
||||
mnemonic={mnemonic}
|
||||
userData={account}
|
||||
onNext={() => {
|
||||
setStep(4);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{step === 4 ? <p>Success, account now exists</p> : null}
|
||||
</SubPageLayout>
|
||||
</GoogleReCaptchaProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import {
|
||||
LargeCard,
|
||||
LargeCardButtons,
|
||||
LargeCardText,
|
||||
} from "@/components/layout/LargeCard";
|
||||
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||
|
||||
export interface AccountProfile {
|
||||
device: string;
|
||||
account: string;
|
||||
profile: {
|
||||
colorA: string;
|
||||
colorB: string;
|
||||
@ -18,13 +23,11 @@ interface AccountCreatePartProps {
|
||||
}
|
||||
|
||||
export function AccountCreatePart(props: AccountCreatePartProps) {
|
||||
const [account, setAccount] = useState("");
|
||||
const [device, setDevice] = useState("");
|
||||
// TODO validate device and account before next step
|
||||
|
||||
const nextStep = useCallback(() => {
|
||||
props.onNext?.({
|
||||
account,
|
||||
device,
|
||||
profile: {
|
||||
colorA: "#fff",
|
||||
@ -32,15 +35,27 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
|
||||
icon: "brush",
|
||||
},
|
||||
});
|
||||
}, [account, device, props]);
|
||||
}, [device, props]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Account name</p>
|
||||
<Input value={account} onInput={setAccount} />
|
||||
<p>Device name</p>
|
||||
<Input value={device} onInput={setDevice} />
|
||||
<Button onClick={() => nextStep()}>Next</Button>
|
||||
</div>
|
||||
<LargeCard>
|
||||
<LargeCardText
|
||||
icon={<Icon icon={Icons.USER} />}
|
||||
title="Account information"
|
||||
>
|
||||
Set up your account.... OR ELSE!
|
||||
</LargeCardText>
|
||||
<AuthInputBox
|
||||
label="Device name"
|
||||
value={device}
|
||||
onChange={setDevice}
|
||||
placeholder="Muad'Dib's Nintendo Switch"
|
||||
/>
|
||||
<LargeCardButtons>
|
||||
<Button theme="purple" onClick={() => nextStep()}>
|
||||
Next
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
</LargeCard>
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,13 @@ import { useAsyncFn } from "react-use";
|
||||
|
||||
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
||||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
import {
|
||||
LargeCard,
|
||||
LargeCardButtons,
|
||||
LargeCardText,
|
||||
} from "@/components/layout/LargeCard";
|
||||
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||
import { useAuth } from "@/hooks/auth/useAuth";
|
||||
|
||||
interface LoginFormPartProps {
|
||||
@ -39,14 +45,38 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>passphrase</p>
|
||||
<Input value={mnemonic} onInput={setMnemonic} />
|
||||
<p>Device name</p>
|
||||
<Input value={device} onInput={setDevice} />
|
||||
{result.loading ? <p>Loading...</p> : null}
|
||||
{result.error ? <p>error: {result.error.toString()}</p> : null}
|
||||
<Button onClick={() => execute(mnemonic, device)}>Login</Button>
|
||||
</div>
|
||||
<LargeCard top={<BrandPill />}>
|
||||
<LargeCardText title="Login to your account">
|
||||
Oh, you're asking for the key to my top-secret lair, also known as
|
||||
The Fortress of Wordsmithery, accessed only by reciting the sacred
|
||||
incantation of the 12-word passphrase!
|
||||
</LargeCardText>
|
||||
<div className="space-y-4">
|
||||
<AuthInputBox
|
||||
label="12-Word Passphrase"
|
||||
value={mnemonic}
|
||||
onChange={setMnemonic}
|
||||
placeholder="Passphrase"
|
||||
/>
|
||||
<AuthInputBox
|
||||
label="Device name"
|
||||
value={device}
|
||||
onChange={setDevice}
|
||||
placeholder="Device"
|
||||
/>
|
||||
{result.loading ? <p>Loading...</p> : null}
|
||||
{result.error && !result.loading ? (
|
||||
<p className="text-authentication-errorText">
|
||||
{result.error.message}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<LargeCardButtons>
|
||||
<Button theme="purple" onClick={() => execute(mnemonic, device)}>
|
||||
LET ME IN!
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
</LargeCard>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { GoogleReCaptcha, useGoogleReCaptcha } from "react-google-recaptcha-v3";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import {
|
||||
LargeCard,
|
||||
LargeCardButtons,
|
||||
LargeCardText,
|
||||
} from "@/components/layout/LargeCard";
|
||||
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||
import { useAuth } from "@/hooks/auth/useAuth";
|
||||
import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
|
||||
|
||||
@ -16,10 +23,17 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
||||
const [mnemonic, setMnemonic] = useState("");
|
||||
const { register, restore } = useAuth();
|
||||
|
||||
const { executeRecaptcha } = useGoogleReCaptcha();
|
||||
|
||||
const [result, execute] = useAsyncFn(
|
||||
async (inputMnemonic: string) => {
|
||||
const recaptchaToken = executeRecaptcha
|
||||
? await executeRecaptcha()
|
||||
: undefined;
|
||||
|
||||
if (!props.mnemonic || !props.userData)
|
||||
throw new Error("invalid input data");
|
||||
throw new Error("Data is not valid");
|
||||
if (!recaptchaToken) throw new Error("ReCaptcha validation failed");
|
||||
if (inputMnemonic !== props.mnemonic)
|
||||
throw new Error("Passphrase doesn't match");
|
||||
|
||||
@ -28,6 +42,7 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
||||
await register({
|
||||
mnemonic: inputMnemonic,
|
||||
userData: props.userData,
|
||||
recaptchaToken,
|
||||
});
|
||||
|
||||
// TODO import (and sort out conflicts)
|
||||
@ -40,12 +55,29 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>verify passphrase</p>
|
||||
<Input value={mnemonic} onInput={setMnemonic} />
|
||||
{result.loading ? <p>Loading...</p> : null}
|
||||
{result.error ? <p>error: {result.error.toString()}</p> : null}
|
||||
<Button onClick={() => execute(mnemonic)}>Register</Button>
|
||||
</div>
|
||||
<LargeCard>
|
||||
<LargeCardText
|
||||
icon={<Icon icon={Icons.CIRCLE_CHECK} />}
|
||||
title="Enter your passphrase"
|
||||
>
|
||||
If you've already lost it, how will you ever be able to take care
|
||||
of a child?
|
||||
</LargeCardText>
|
||||
<AuthInputBox
|
||||
label="Your passphrase"
|
||||
value={mnemonic}
|
||||
onChange={setMnemonic}
|
||||
/>
|
||||
{result.error ? (
|
||||
<p className="mt-3 text-authentication-errorText">
|
||||
{result.error.message}
|
||||
</p>
|
||||
) : null}
|
||||
<LargeCardButtons>
|
||||
<Button theme="purple" onClick={() => execute(mnemonic)}>
|
||||
Register
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
</LargeCard>
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ interface Config {
|
||||
CORS_PROXY_URL: string;
|
||||
NORMAL_ROUTER: boolean;
|
||||
BACKEND_URL: string;
|
||||
RECAPTCHA_SITE_KEY: string;
|
||||
}
|
||||
|
||||
export interface RuntimeConfig {
|
||||
@ -18,6 +19,7 @@ export interface RuntimeConfig {
|
||||
NORMAL_ROUTER: boolean;
|
||||
PROXY_URLS: string[];
|
||||
BACKEND_URL: string;
|
||||
RECAPTCHA_SITE_KEY: string;
|
||||
}
|
||||
|
||||
const env: Record<keyof Config, undefined | string> = {
|
||||
@ -28,6 +30,7 @@ const env: Record<keyof Config, undefined | string> = {
|
||||
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
|
||||
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
|
||||
BACKEND_URL: import.meta.env.VITE_BACKEND_URL,
|
||||
RECAPTCHA_SITE_KEY: import.meta.env.VITE_RECAPTCHA_SITE_KEY,
|
||||
};
|
||||
|
||||
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
|
||||
@ -53,5 +56,6 @@ export function conf(): RuntimeConfig {
|
||||
.split(",")
|
||||
.map((v) => v.trim()),
|
||||
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
|
||||
RECAPTCHA_SITE_KEY: getKey("RECAPTCHA_SITE_KEY"),
|
||||
};
|
||||
}
|
||||
|
@ -210,3 +210,7 @@ input[type=range].styled-slider.slider-progress::-ms-fill-lower {
|
||||
/* For some reason the styles don't get applied without the width */
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.grecaptcha-badge {
|
||||
display: none !important;
|
||||
}
|
@ -120,9 +120,11 @@ module.exports = {
|
||||
// Passphrase
|
||||
authentication: {
|
||||
border: "#393954",
|
||||
inputBg: "#171728",
|
||||
wordBackground: "#171728",
|
||||
copyText: "#58587A",
|
||||
copyTextHover: "#8888AA",
|
||||
errorText: "#DB3D62",
|
||||
},
|
||||
|
||||
// Settings page
|
||||
|
Loading…
Reference in New Issue
Block a user