diff --git a/tfweb/src/lib/auth.ts b/tfweb/src/lib/auth.ts index 7074283..ab9a690 100644 --- a/tfweb/src/lib/auth.ts +++ b/tfweb/src/lib/auth.ts @@ -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]> { 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}`); 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] } catch (e) { logger.error(`error making API request: ${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}`}] + } +} diff --git a/tfweb/src/lib/i18n/locales/en.json b/tfweb/src/lib/i18n/locales/en.json index 676fd42..6a19098 100644 --- a/tfweb/src/lib/i18n/locales/en.json +++ b/tfweb/src/lib/i18n/locales/en.json @@ -2,8 +2,9 @@ "itworks": { "header": "It works!", "body": "If you're seeing this page, tfweb is installed and (probably) correctly configured.", - "linkbody": "Perhaps you meant to visit the {link0}?", - "linkbody.link0": "admin panel" + "linkbody": "Perhaps you meant to visit the {link0} or {link1}?", + "linkbody.link0": "admin panel", + "linkbody.link1": "create an account" }, "login": { "title": "Log in to your account", @@ -20,6 +21,22 @@ "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": { "header": "Authenticated!", "body": "Redirecting to admin page...", diff --git a/tfweb/src/routes/+page.svelte b/tfweb/src/routes/+page.svelte index 2bf313c..27f53fb 100644 --- a/tfweb/src/routes/+page.svelte +++ b/tfweb/src/routes/+page.svelte @@ -21,5 +21,5 @@
{$t('itworks.body')}
-{@html $t('itworks.linkbody', {values:{link0:''+$t('itworks.linkbody.link0')+''}})}
+{@html $t('itworks.linkbody', {values:{link0:''+$t('itworks.linkbody.link0')+'',link1:''+$t('itworks.linkbody.link1')+''}})}
diff --git a/tfweb/src/routes/2fasetup/+page.svelte b/tfweb/src/routes/2fasetup/+page.svelte index 32d72cf..15dfa59 100644 --- a/tfweb/src/routes/2fasetup/+page.svelte +++ b/tfweb/src/routes/2fasetup/+page.svelte @@ -2,7 +2,8 @@ import {isLoading, t} from "svelte-i18n"; import LoadingWrapper from "$components/LoadingWrapper.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 {APIError} from "$lib/auth.ts"; import {Logger, logSetup} from "$lib/logger"; @@ -18,6 +19,10 @@ let logger = new Logger("2fasetup/+page.svelte"); let totp_secret = ''; + let otp_url = ''; + let totp_create_token = ''; + + let code = ''; onMount(async () => { let session_load_info = await isAuthedSession(); @@ -54,10 +59,41 @@ 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; }) 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'; } @@ -71,13 +107,13 @@{$t('2fasetup.body')}
{$t('signup.emailbody')}
+ +{@html $t('signup.emailbody2', {values:{link0:''+$t('signup.emailbody2.link0')+''}})}
+ {:else} +