mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
add concept for register flow
This commit is contained in:
parent
4f4ee13556
commit
df85861cf2
11 changed files with 400 additions and 1 deletions
|
@ -8,7 +8,9 @@
|
||||||
"@headlessui/react": "^1.5.0",
|
"@headlessui/react": "^1.5.0",
|
||||||
"@movie-web/providers": "^1.0.4",
|
"@movie-web/providers": "^1.0.4",
|
||||||
"@react-spring/web": "^9.7.1",
|
"@react-spring/web": "^9.7.1",
|
||||||
|
"@scure/bip39": "^1.2.1",
|
||||||
"@sozialhelden/ietf-language-tags": "^5.4.2",
|
"@sozialhelden/ietf-language-tags": "^5.4.2",
|
||||||
|
"@types/node-forge": "^1.3.8",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"core-js": "^3.29.1",
|
"core-js": "^3.29.1",
|
||||||
"dompurify": "^3.0.1",
|
"dompurify": "^3.0.1",
|
||||||
|
@ -19,6 +21,7 @@
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
"i18next": "^22.4.5",
|
"i18next": "^22.4.5",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
"ofetch": "^1.0.0",
|
"ofetch": "^1.0.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
@ -30,6 +33,7 @@
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"subsrt-ts": "^2.1.1",
|
"subsrt-ts": "^2.1.1",
|
||||||
|
"universal-base64url": "^1.1.0",
|
||||||
"unzipit": "^1.4.3",
|
"unzipit": "^1.4.3",
|
||||||
"zustand": "^4.3.9"
|
"zustand": "^4.3.9"
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,9 +23,15 @@ dependencies:
|
||||||
'@react-spring/web':
|
'@react-spring/web':
|
||||||
specifier: ^9.7.1
|
specifier: ^9.7.1
|
||||||
version: 9.7.3(react-dom@17.0.2)(react@17.0.2)
|
version: 9.7.3(react-dom@17.0.2)(react@17.0.2)
|
||||||
|
'@scure/bip39':
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1
|
||||||
'@sozialhelden/ietf-language-tags':
|
'@sozialhelden/ietf-language-tags':
|
||||||
specifier: ^5.4.2
|
specifier: ^5.4.2
|
||||||
version: 5.4.2
|
version: 5.4.2
|
||||||
|
'@types/node-forge':
|
||||||
|
specifier: ^1.3.8
|
||||||
|
version: 1.3.8
|
||||||
classnames:
|
classnames:
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
|
@ -56,6 +62,9 @@ dependencies:
|
||||||
immer:
|
immer:
|
||||||
specifier: ^10.0.2
|
specifier: ^10.0.2
|
||||||
version: 10.0.2
|
version: 10.0.2
|
||||||
|
node-forge:
|
||||||
|
specifier: ^1.3.1
|
||||||
|
version: 1.3.1
|
||||||
ofetch:
|
ofetch:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.3.3
|
version: 1.3.3
|
||||||
|
@ -89,6 +98,9 @@ dependencies:
|
||||||
subsrt-ts:
|
subsrt-ts:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
universal-base64url:
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
unzipit:
|
unzipit:
|
||||||
specifier: ^1.4.3
|
specifier: ^1.4.3
|
||||||
version: 1.4.3
|
version: 1.4.3
|
||||||
|
@ -1889,6 +1901,11 @@ packages:
|
||||||
- encoding
|
- encoding
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@noble/hashes@1.3.2:
|
||||||
|
resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@nodelib/fs.scandir@2.1.5:
|
/@nodelib/fs.scandir@2.1.5:
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
@ -2019,6 +2036,17 @@ packages:
|
||||||
rollup: 2.79.1
|
rollup: 2.79.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@scure/base@1.1.3:
|
||||||
|
resolution: {integrity: sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@scure/bip39@1.2.1:
|
||||||
|
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
|
||||||
|
dependencies:
|
||||||
|
'@noble/hashes': 1.3.2
|
||||||
|
'@scure/base': 1.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@sozialhelden/ietf-language-tags@5.4.2:
|
/@sozialhelden/ietf-language-tags@5.4.2:
|
||||||
resolution: {integrity: sha512-aCN7bVOfX9sBN0EHyWJT14H8bx+VYBo8tdcynai35wgoxKMfVtgEECkQ1gs8nEL6GHGes8lPIfo6AjIch44N3w==}
|
resolution: {integrity: sha512-aCN7bVOfX9sBN0EHyWJT14H8bx+VYBo8tdcynai35wgoxKMfVtgEECkQ1gs8nEL6GHGes8lPIfo6AjIch44N3w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2121,9 +2149,14 @@ packages:
|
||||||
resolution: {integrity: sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==}
|
resolution: {integrity: sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/node-forge@1.3.8:
|
||||||
|
resolution: {integrity: sha512-vGXshY9vim9CJjrpcS5raqSjEfKlJcWy2HNdgUasR66fAnVEYarrf1ULV4nfvpC1nZq/moA9qyqBcu83x+Jlrg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 17.0.45
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/node@17.0.45:
|
/@types/node@17.0.45:
|
||||||
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
|
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/pako@2.0.0:
|
/@types/pako@2.0.0:
|
||||||
resolution: {integrity: sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==}
|
resolution: {integrity: sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==}
|
||||||
|
@ -4733,6 +4766,11 @@ packages:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/node-forge@1.3.1:
|
||||||
|
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||||
|
engines: {node: '>= 6.13.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-releases@2.0.13:
|
/node-releases@2.0.13:
|
||||||
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
|
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -6112,6 +6150,16 @@ packages:
|
||||||
crypto-random-string: 2.0.0
|
crypto-random-string: 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/universal-base64@2.1.0:
|
||||||
|
resolution: {integrity: sha512-WeOkACVnIXJZr/qlv7++Rl1zuZOHN96v2yS5oleUuv8eJOs5j9M5U3xQEIoWqn1OzIuIcgw0fswxWnUVGDfW6g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/universal-base64url@1.1.0:
|
||||||
|
resolution: {integrity: sha512-qWv2+8KCaAWdpqqXwU8W0Yj9pflYDXP37/a3kec6Y4Je7bYzgIfxEVRjZWeLR67be7iot1lGCy5Nuo+xB0fojA==}
|
||||||
|
dependencies:
|
||||||
|
universal-base64: 2.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/universalify@0.2.0:
|
/universalify@0.2.0:
|
||||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
|
|
40
src/backend/accounts/crypto.ts
Normal file
40
src/backend/accounts/crypto.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { generateMnemonic } from "@scure/bip39";
|
||||||
|
import { wordlist } from "@scure/bip39/wordlists/english";
|
||||||
|
import forge from "node-forge";
|
||||||
|
import { encode } from "universal-base64url";
|
||||||
|
|
||||||
|
async function seedFromMnemonic(mnemonic: string) {
|
||||||
|
const md = forge.md.sha256.create();
|
||||||
|
md.update(mnemonic);
|
||||||
|
// TODO this is probably not correct
|
||||||
|
return md.digest().toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function keysFromMenmonic(mnemonic: string) {
|
||||||
|
const seed = await seedFromMnemonic(mnemonic);
|
||||||
|
|
||||||
|
const { privateKey, publicKey } = forge.pki.ed25519.generateKeyPair({
|
||||||
|
seed,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genMnemonic(): string {
|
||||||
|
return generateMnemonic(wordlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signCode(
|
||||||
|
_code: string,
|
||||||
|
_privateKey: forge.pki.ed25519.NativeBuffer
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
// TODO add real signature
|
||||||
|
return new Uint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToBase64Url(bytes: Uint8Array): string {
|
||||||
|
return encode(String.fromCodePoint(...bytes));
|
||||||
|
}
|
13
src/backend/accounts/meta.ts
Normal file
13
src/backend/accounts/meta.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { ofetch } from "ofetch";
|
||||||
|
|
||||||
|
export interface MetaResponse {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
hasCaptcha: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBackendMeta(url: string): Promise<MetaResponse> {
|
||||||
|
return ofetch<MetaResponse>("/meta", {
|
||||||
|
baseURL: url,
|
||||||
|
});
|
||||||
|
}
|
64
src/backend/accounts/register.ts
Normal file
64
src/backend/accounts/register.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { ofetch } from "ofetch";
|
||||||
|
|
||||||
|
import { SessionResponse, UserResponse } from "@/backend/accounts/auth";
|
||||||
|
import { keysFromMenmonic, signCode } from "@/backend/accounts/crypto";
|
||||||
|
|
||||||
|
export interface ChallengeTokenResponse {
|
||||||
|
challenge: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRegisterChallengeToken(
|
||||||
|
url: string,
|
||||||
|
captchaToken?: string
|
||||||
|
): Promise<ChallengeTokenResponse> {
|
||||||
|
return ofetch<ChallengeTokenResponse>("/auth/register/start", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
captchaToken,
|
||||||
|
},
|
||||||
|
baseURL: url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterResponse {
|
||||||
|
user: UserResponse;
|
||||||
|
session: SessionResponse;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterInput {
|
||||||
|
publicKey: string;
|
||||||
|
challenge: {
|
||||||
|
code: string;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
device: string;
|
||||||
|
profile: {
|
||||||
|
colorA: string;
|
||||||
|
colorB: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerAccount(
|
||||||
|
url: string,
|
||||||
|
data: RegisterInput
|
||||||
|
): Promise<RegisterResponse> {
|
||||||
|
return ofetch<RegisterResponse>("/auth/register/complete", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
namespace: "movie-web",
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
baseURL: url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signChallenge(mnemonic: string, challengeCode: string) {
|
||||||
|
const keys = await keysFromMenmonic(mnemonic);
|
||||||
|
const signature = await signCode(challengeCode, keys.privateKey);
|
||||||
|
return {
|
||||||
|
publicKey: keys.publicKey,
|
||||||
|
signature,
|
||||||
|
};
|
||||||
|
}
|
54
src/pages/Register.tsx
Normal file
54
src/pages/Register.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||||
|
import {
|
||||||
|
AccountCreatePart,
|
||||||
|
AccountProfile,
|
||||||
|
} from "@/pages/parts/auth/AccountCreatePart";
|
||||||
|
import { PassphraseGeneratePart } from "@/pages/parts/auth/PassphraseGeneratePart";
|
||||||
|
import { TrustBackendPart } from "@/pages/parts/auth/TrustBackendPart";
|
||||||
|
import { VerifyPassphrase } from "@/pages/parts/auth/VerifyPassphrasePart";
|
||||||
|
|
||||||
|
export function RegisterPage() {
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [mnemonic, setMnemonic] = useState<null | string>(null);
|
||||||
|
const [account, setAccount] = useState<null | AccountProfile>(null);
|
||||||
|
|
||||||
|
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}
|
||||||
|
profile={account}
|
||||||
|
onNext={() => {
|
||||||
|
setStep(4);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{step === 4 ? <p>Success, account now exists</p> : null}
|
||||||
|
</SubPageLayout>
|
||||||
|
);
|
||||||
|
}
|
46
src/pages/parts/auth/AccountCreatePart.tsx
Normal file
46
src/pages/parts/auth/AccountCreatePart.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
||||||
|
|
||||||
|
export interface AccountProfile {
|
||||||
|
device: string;
|
||||||
|
account: string;
|
||||||
|
profile: {
|
||||||
|
colorA: string;
|
||||||
|
colorB: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountCreatePartProps {
|
||||||
|
onNext?: (data: AccountProfile) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
colorB: "#000",
|
||||||
|
icon: "brush",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [account, 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>
|
||||||
|
);
|
||||||
|
}
|
20
src/pages/parts/auth/PassphraseGeneratePart.tsx
Normal file
20
src/pages/parts/auth/PassphraseGeneratePart.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { genMnemonic } from "@/backend/accounts/crypto";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
|
||||||
|
interface PassphraseGeneratePartProps {
|
||||||
|
onNext?: (mnemonic: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) {
|
||||||
|
const mnemonic = useMemo(() => genMnemonic(), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Remeber the following passphrase:</p>
|
||||||
|
<p className="border rounded-xl p-2">{mnemonic}</p>
|
||||||
|
<Button onClick={() => props.onNext?.(mnemonic)}>Next</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
40
src/pages/parts/auth/TrustBackendPart.tsx
Normal file
40
src/pages/parts/auth/TrustBackendPart.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { useAsync } from "react-use";
|
||||||
|
|
||||||
|
import { MetaResponse, getBackendMeta } from "@/backend/accounts/meta";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
|
interface TrustBackendPartProps {
|
||||||
|
onNext?: (meta: MetaResponse) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrustBackendPart(props: TrustBackendPartProps) {
|
||||||
|
const result = useAsync(async () => {
|
||||||
|
const url = conf().BACKEND_URL;
|
||||||
|
return {
|
||||||
|
domain: new URL(url).hostname,
|
||||||
|
data: await getBackendMeta(conf().BACKEND_URL),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (result.loading) return <p>loading...</p>;
|
||||||
|
|
||||||
|
if (result.error || !result.value)
|
||||||
|
return <p>Failed to talk to backend, did you configure it correctly?</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
do you trust{" "}
|
||||||
|
<span className="text-white font-bold">{result.value.domain}</span>
|
||||||
|
</p>
|
||||||
|
<div className="border rounded-xl p-4">
|
||||||
|
<p className="text-white font-bold">{result.value.data.name}</p>
|
||||||
|
{result.value.data.description ? (
|
||||||
|
<p>{result.value.data.description}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => props.onNext?.(result.value.data)}>Next</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
68
src/pages/parts/auth/VerifyPassphrasePart.tsx
Normal file
68
src/pages/parts/auth/VerifyPassphrasePart.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
|
import { bytesToBase64Url } from "@/backend/accounts/crypto";
|
||||||
|
import {
|
||||||
|
getRegisterChallengeToken,
|
||||||
|
registerAccount,
|
||||||
|
signChallenge,
|
||||||
|
} from "@/backend/accounts/register";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
||||||
|
import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
|
interface VerifyPassphraseProps {
|
||||||
|
mnemonic: string | null;
|
||||||
|
profile: AccountProfile | null;
|
||||||
|
onNext?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
||||||
|
const [mnemonic, setMnemonic] = useState("");
|
||||||
|
const setAccount = useAuthStore((s) => s.setAccount);
|
||||||
|
|
||||||
|
const [result, execute] = useAsyncFn(
|
||||||
|
async (inputMnemonic: string) => {
|
||||||
|
if (!props.mnemonic || !props.profile)
|
||||||
|
throw new Error("invalid input data");
|
||||||
|
if (inputMnemonic !== props.mnemonic)
|
||||||
|
throw new Error("Passphrase doesn't match");
|
||||||
|
const url = conf().BACKEND_URL;
|
||||||
|
|
||||||
|
// TODO captcha?
|
||||||
|
const { challenge } = await getRegisterChallengeToken(url);
|
||||||
|
const keys = await signChallenge(inputMnemonic, challenge);
|
||||||
|
const registerResult = await registerAccount(url, {
|
||||||
|
challenge: {
|
||||||
|
code: challenge,
|
||||||
|
signature: bytesToBase64Url(keys.signature),
|
||||||
|
},
|
||||||
|
publicKey: bytesToBase64Url(keys.publicKey),
|
||||||
|
device: props.profile.device,
|
||||||
|
profile: props.profile.profile,
|
||||||
|
});
|
||||||
|
|
||||||
|
setAccount({
|
||||||
|
profile: registerResult.user.profile,
|
||||||
|
sessionId: registerResult.session.id,
|
||||||
|
token: registerResult.token,
|
||||||
|
userId: registerResult.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
props.onNext?.();
|
||||||
|
},
|
||||||
|
[props, setAccount]
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import { DmcaPage } from "@/pages/Dmca";
|
||||||
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
|
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
|
||||||
import { HomePage } from "@/pages/HomePage";
|
import { HomePage } from "@/pages/HomePage";
|
||||||
import { PlayerView } from "@/pages/PlayerView";
|
import { PlayerView } from "@/pages/PlayerView";
|
||||||
|
import { RegisterPage } from "@/pages/Register";
|
||||||
import { SettingsPage } from "@/pages/Settings";
|
import { SettingsPage } from "@/pages/Settings";
|
||||||
import { Layout } from "@/setup/Layout";
|
import { Layout } from "@/setup/Layout";
|
||||||
import { useHistoryListener } from "@/stores/history";
|
import { useHistoryListener } from "@/stores/history";
|
||||||
|
@ -87,6 +88,7 @@ function App() {
|
||||||
</LegacyUrlView>
|
</LegacyUrlView>
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path={["/browse/:query?", "/"]} component={HomePage} />
|
<Route exact path={["/browse/:query?", "/"]} component={HomePage} />
|
||||||
|
<Route exact path="/register" component={RegisterPage} />
|
||||||
<Route exact path="/faq" component={AboutPage} />
|
<Route exact path="/faq" component={AboutPage} />
|
||||||
<Route exact path="/dmca" component={DmcaPage} />
|
<Route exact path="/dmca" component={DmcaPage} />
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue