webui work
This commit is contained in:
parent
a9d6cb22db
commit
f4c0a1718e
|
@ -110,6 +110,32 @@ export async function authSession(email: string): Promise<[AuthResult, null | AP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function signup(email: string): Promise<[AuthResult, null | APIError]> {
|
||||||
|
logger.info('sending signup');
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
|
||||||
|
const resp = await fetch(`${PUBLIC_BASE_URL}/v1/signup`, {
|
||||||
|
'method': 'POST',
|
||||||
|
'body': JSON.stringify({
|
||||||
|
email: email
|
||||||
|
}),
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const rawerror = JSON.parse(await resp.text()).errors[0];
|
||||||
|
logger.error(`error sending authentication: ${rawerror.message}`);
|
||||||
|
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
|
||||||
|
}
|
||||||
|
return [AuthResult.Successful, null]
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`error making API request: ${e}`);
|
||||||
|
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function verifyLink(ml: string): Promise<[AuthResult, null | APIError]> {
|
export async function verifyLink(ml: string): Promise<[AuthResult, null | APIError]> {
|
||||||
logger.info('checking magic link authentication');
|
logger.info('checking magic link authentication');
|
||||||
|
|
||||||
|
@ -164,10 +190,41 @@ export async function createTotp(token: string): Promise<[AuthResult, TOTPCreate
|
||||||
logger.error(`error sending totp create: ${rawerror.message}`);
|
logger.error(`error sending totp create: ${rawerror.message}`);
|
||||||
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
|
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
|
||||||
}
|
}
|
||||||
logger.info(`call success, ${await resp.text()}`)
|
|
||||||
return [AuthResult.Successful, JSON.parse(await resp.text()).data]
|
return [AuthResult.Successful, JSON.parse(await resp.text()).data]
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`error making API request: ${e}`);
|
logger.error(`error making API request: ${e}`);
|
||||||
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
|
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyTotp(token: string, totpToken: string, totpCode: string): Promise<[AuthResult, undefined | APIError]> {
|
||||||
|
logger.info('verifying totp authenticator');
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
|
||||||
|
const resp = await fetch(`${PUBLIC_BASE_URL}/v1/totp-authenticators`, {
|
||||||
|
'method': 'POST',
|
||||||
|
'body': JSON.stringify({
|
||||||
|
totpToken: totpToken,
|
||||||
|
code: totpCode
|
||||||
|
}),
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
logger.error('call returned error code');
|
||||||
|
const rawerror = JSON.parse(await resp.text()).errors[0];
|
||||||
|
logger.error(`error sending totp create: ${rawerror.message}`);
|
||||||
|
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem("mfa", JSON.parse(await resp.text()).data.authToken);
|
||||||
|
|
||||||
|
return [AuthResult.Successful, undefined]
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`error making API request: ${e}`);
|
||||||
|
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
"itworks": {
|
"itworks": {
|
||||||
"header": "It works!",
|
"header": "It works!",
|
||||||
"body": "If you're seeing this page, tfweb is installed and (probably) correctly configured.",
|
"body": "If you're seeing this page, tfweb is installed and (probably) correctly configured.",
|
||||||
"linkbody": "Perhaps you meant to visit the {link0}?",
|
"linkbody": "Perhaps you meant to visit the {link0} or {link1}?",
|
||||||
"linkbody.link0": "admin panel"
|
"linkbody.link0": "admin panel",
|
||||||
|
"linkbody.link1": "create an account"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Log in to your account",
|
"title": "Log in to your account",
|
||||||
|
@ -20,6 +21,22 @@
|
||||||
"usermissing": "That user does not exist."
|
"usermissing": "That user does not exist."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"signup": {
|
||||||
|
"title": "Create an account",
|
||||||
|
"subtitle": "We'll send you an email with a \"magic link\"",
|
||||||
|
"label": "What is your email?",
|
||||||
|
"button": "Create account",
|
||||||
|
"email": "Check your email",
|
||||||
|
"emailbody": "We sent you an email with a link to complete signing up.",
|
||||||
|
"emailbody2": "Didn't work? Check your junk inbox or click {link0} to try again.",
|
||||||
|
"emailbody2.link0": "here",
|
||||||
|
"error": {
|
||||||
|
"invalidEmail": "That email address isn't valid. Try again.",
|
||||||
|
"generic": "There was an error logging you in. Try again or contact support with the error code {err}",
|
||||||
|
"userexists": "That user already exists. Try {link0}?",
|
||||||
|
"userexists.link0": "logging in"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ml": {
|
"ml": {
|
||||||
"header": "Authenticated!",
|
"header": "Authenticated!",
|
||||||
"body": "Redirecting to admin page...",
|
"body": "Redirecting to admin page...",
|
||||||
|
|
|
@ -21,5 +21,5 @@
|
||||||
<h1>{$t('itworks.header')}</h1>
|
<h1>{$t('itworks.header')}</h1>
|
||||||
<p>{$t('itworks.body')}</p>
|
<p>{$t('itworks.body')}</p>
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
<p>{@html $t('itworks.linkbody', {values:{link0:'<a href="/admin">'+$t('itworks.linkbody.link0')+'</a>'}})}</p>
|
<p>{@html $t('itworks.linkbody', {values:{link0:'<a href="/admin">'+$t('itworks.linkbody.link0')+'</a>',link1:'<a href="/signup">'+$t('itworks.linkbody.link1')+'</a>'}})}</p>
|
||||||
</LoadingWrapper>
|
</LoadingWrapper>
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
import {isLoading, t} from "svelte-i18n";
|
import {isLoading, t} from "svelte-i18n";
|
||||||
import LoadingWrapper from "$components/LoadingWrapper.svelte";
|
import LoadingWrapper from "$components/LoadingWrapper.svelte";
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import {AuthResult, authSession, createTotp, isAuthedSession} from "$lib/auth.ts";
|
import {AuthResult, authSession, createTotp, isAuthedSession, verifyTotp} from "$lib/auth.ts";
|
||||||
|
import type {TOTPCreateInfo} from "$lib/auth.ts";
|
||||||
import type {SessionInfo} from "$lib/auth.ts";
|
import type {SessionInfo} from "$lib/auth.ts";
|
||||||
import type {APIError} from "$lib/auth.ts";
|
import type {APIError} from "$lib/auth.ts";
|
||||||
import {Logger, logSetup} from "$lib/logger";
|
import {Logger, logSetup} from "$lib/logger";
|
||||||
|
@ -18,6 +19,10 @@
|
||||||
let logger = new Logger("2fasetup/+page.svelte");
|
let logger = new Logger("2fasetup/+page.svelte");
|
||||||
|
|
||||||
let totp_secret = '';
|
let totp_secret = '';
|
||||||
|
let otp_url = '';
|
||||||
|
let totp_create_token = '';
|
||||||
|
|
||||||
|
let code = '';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let session_load_info = await isAuthedSession();
|
let session_load_info = await isAuthedSession();
|
||||||
|
@ -54,10 +59,41 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('setting secret and otpurl');
|
||||||
|
|
||||||
|
totp_secret = (create_res[1] as TOTPCreateInfo).secret;
|
||||||
|
otp_url = (create_res[1] as TOTPCreateInfo).url;
|
||||||
|
totp_create_token = (create_res[1] as TOTPCreateInfo).totpToken;
|
||||||
|
|
||||||
|
logger.info(totp_secret);
|
||||||
|
logger.info(otp_url);
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
})
|
})
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
|
let create_res = await verifyTotp(window.localStorage.getItem("session"), totp_create_token, code);
|
||||||
|
|
||||||
|
if (create_res[0] == AuthResult.Failed) {
|
||||||
|
logger.error(`totp auth fail`);
|
||||||
|
isError = true;
|
||||||
|
let err = create_res[1] as APIError;
|
||||||
|
|
||||||
|
let etext = err.code;
|
||||||
|
|
||||||
|
if (etext === "api_call_failed") {
|
||||||
|
etext = $t('2fasetup.error.api');
|
||||||
|
} else if (etext === "ERR_ALREADY_HAS_TOTP") {
|
||||||
|
window.location.href = '/2fa';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = $t('2fasetup.error.generic', {values:{err:etext}});
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = '/admin';
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -71,13 +107,13 @@
|
||||||
<p>{$t('2fasetup.body')}</p>
|
<p>{$t('2fasetup.body')}</p>
|
||||||
<h4>{$t('2fasetup.scan')}</h4>
|
<h4>{$t('2fasetup.scan')}</h4>
|
||||||
<div>
|
<div>
|
||||||
<QrCode value="abcd1234"/>
|
<QrCode bind:value={otp_url}/>
|
||||||
</div>
|
</div>
|
||||||
<h4>{$t('2fasetup.code')}</h4>
|
<h4>{$t('2fasetup.code')}</h4>
|
||||||
<div><span>totp secret hete</span></div>
|
<div><span>{totp_secret}</span></div>
|
||||||
<form on:submit|preventDefault={onSubmit}>
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
<label for="code">{$t('2fasetup.verify')}</label>
|
<label for="code">{$t('2fasetup.verify')}</label>
|
||||||
<input type="number" min="0" max="999999" id="code" />
|
<input bind:value={code} type="number" min="0" max="999999" id="code" />
|
||||||
<button>{$t('2fasetup.button')}</button>
|
<button>{$t('2fasetup.button')}</button>
|
||||||
</form>
|
</form>
|
||||||
</LoadingWrapper>
|
</LoadingWrapper>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import {isLoading, t} from "svelte-i18n";
|
import {isLoading, t} from "svelte-i18n";
|
||||||
import LoadingWrapper from "$components/LoadingWrapper.svelte";
|
import LoadingWrapper from "$components/LoadingWrapper.svelte";
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import {AuthResult, isAuthedSession} from "$lib/auth.ts";
|
import {AuthResult, isAuthedMFA, isAuthedSession} from "$lib/auth.ts";
|
||||||
import {Logger, logSetup} from "$lib/logger";
|
import {Logger, logSetup} from "$lib/logger";
|
||||||
import type {APIError} from "$lib/auth.ts";
|
import type {APIError} from "$lib/auth.ts";
|
||||||
|
|
||||||
|
@ -23,6 +23,14 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mfa_load_info = await isAuthedMFA();
|
||||||
|
if (mfa_load_info[0] == AuthResult.Failed) {
|
||||||
|
let err = mfa_load_info[1] as APIError;
|
||||||
|
logger.error(`mfa load failed: ${err.code} ${err.message}`);
|
||||||
|
window.location.href = '/2fa';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {isLoading, t} from "svelte-i18n";
|
||||||
|
import LoadingWrapper from "$components/LoadingWrapper.svelte";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import {AuthResult, authSession, isAuthedSession, signup} from "$lib/auth.ts";
|
||||||
|
import type {APIError} from "$lib/auth.ts";
|
||||||
|
import {Logger, logSetup} from "$lib/logger";
|
||||||
|
|
||||||
|
let loading = true;
|
||||||
|
let isError = false;
|
||||||
|
let error = '';
|
||||||
|
$: currentlyLoading = $isLoading || loading;
|
||||||
|
|
||||||
|
logSetup();
|
||||||
|
let logger = new Logger("signup/+page.svelte");
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let session_load_info = await isAuthedSession();
|
||||||
|
if (session_load_info[0] != AuthResult.Failed) {
|
||||||
|
logger.error(`session load success, the user is already logged in`);
|
||||||
|
window.location.href = '/2fa';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
let email = '';
|
||||||
|
let email_regexp = new RegExp(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
|
||||||
|
|
||||||
|
let hasErrForm = false;
|
||||||
|
let errForm = '';
|
||||||
|
|
||||||
|
let isDone = false;
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
if (!email_regexp.test(email)) {
|
||||||
|
errForm = $t('signup.error.invalidEmail');
|
||||||
|
hasErrForm = true;
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_result = await signup(email);
|
||||||
|
|
||||||
|
if (auth_result[0] === AuthResult.Failed) {
|
||||||
|
|
||||||
|
hasErrForm = true;
|
||||||
|
|
||||||
|
// parse the error and filter out common ones for more specific error messages
|
||||||
|
let err = auth_result[1] as APIError;
|
||||||
|
|
||||||
|
if (err.code == "ERR_USER_EXISTS") {
|
||||||
|
errForm = $t('signup.error.userexists', {values:{link0:'<a href="/login">'+$t('signup.error.userexists.link0')+'</a>'}});
|
||||||
|
} else {
|
||||||
|
errForm = $t('signup.error.generic', {values: {err: (auth_result[1] as APIError).code}});
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDone = true;
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t("common.title", {values: {title: $t("common.page.signup")}})}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
|
||||||
|
{#if isDone}
|
||||||
|
<h1>{$t('signup.email')}</h1>
|
||||||
|
<p>{$t('signup.emailbody')}</p>
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
<p>{@html $t('signup.emailbody2', {values:{link0:'<a href="/signup">'+$t('signup.emailbody2.link0')+'</a>'}})}</p>
|
||||||
|
{:else}
|
||||||
|
<h1>{$t('signup.title')}</h1>
|
||||||
|
<h3>{$t('signup.subtitle')}</h3>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="email">{$t('signup.label')}</label>
|
||||||
|
<input type="email" bind:value={email} placeholder="john.doe@google.com" id="email"/>
|
||||||
|
<button>{$t('signup.button')}</button>
|
||||||
|
{#if hasErrForm}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
<p>{@html errForm}</p>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</LoadingWrapper>
|
Loading…
Reference in New Issue