webui work

This commit is contained in:
c0repwn3r 2023-06-10 16:59:48 -04:00
parent a9d6cb22db
commit f4c0a1718e
Signed by: core
GPG Key ID: FDBF740DADDCEECF
6 changed files with 220 additions and 9 deletions

View File

@ -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}`}]
}
}

View File

@ -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...",

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>