some work

This commit is contained in:
c0repwn3r 2023-05-31 13:24:15 -04:00
parent 3b242290d6
commit 996db62ab2
Signed by: core
GPG Key ID: FDBF740DADDCEECF
11 changed files with 318 additions and 8 deletions

16
Cargo.lock generated
View File

@ -25,6 +25,21 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "actix-cors"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e"
dependencies = [
"actix-utils",
"actix-web",
"derive_more",
"futures-util",
"log",
"once_cell",
"smallvec",
]
[[package]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.3.1" version = "3.3.1"
@ -3522,6 +3537,7 @@ dependencies = [
name = "trifid-api" name = "trifid-api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-cors",
"actix-request-identifier", "actix-request-identifier",
"actix-web", "actix-web",
"aes-gcm", "aes-gcm",

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Logger, logSetup} from "$lib/logger"; import {logDeltaReset, Logger, logSetup} from "$lib/logger";
export let isLoading; export let isLoading;
export let isError; export let isError;
@ -11,6 +11,7 @@
function loadingproclog() { function loadingproclog() {
if (!isLoading) { if (!isLoading) {
logger.info("page loaded - content paint"); logger.info("page loaded - content paint");
logDeltaReset();
} }
} }

View File

@ -1,4 +1,5 @@
import {Logger, logSetup} from "$lib/logger"; import {Logger, logSetup} from "$lib/logger";
import {PUBLIC_BASE_URL} from "$env/static/public";
export enum AuthResult { export enum AuthResult {
Failed = 0, Failed = 0,
@ -13,7 +14,7 @@ export interface SessionInfo {
hasTOTP: boolean hasTOTP: boolean
} }
export interface SessionAuthError { export interface APIError {
code: string, code: string,
message: string message: string
} }
@ -21,7 +22,119 @@ export interface SessionAuthError {
logSetup(); logSetup();
const logger = new Logger("auth.ts"); const logger = new Logger("auth.ts");
export async function isAuthedSession(): Promise<[AuthResult, SessionInfo | SessionAuthError]> { export async function isAuthedSession(): Promise<[AuthResult, SessionInfo | APIError]> {
logger.info('Checking for session authentication'); logger.info('Checking for session authentication');
return [AuthResult.Failed, {code: "asdji", message: "asdioj"}]
if (window.localStorage.getItem("session") === null) {
logger.error('unable to check session: session token not set');
return [AuthResult.Failed, {code: "missing_token", message: "not logged in"}];
}
try {
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
const resp = await fetch(`${PUBLIC_BASE_URL}/v2/whoami`, {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${window.localStorage.getItem("session")}`
}
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error fetching user information: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
return [AuthResult.Successful, JSON.parse(await resp.text()).data.actor]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
export async function isAuthedMFA(): Promise<[AuthResult, SessionInfo | APIError]> {
logger.info('Checking for MFA authentication');
if (window.localStorage.getItem("mfa") === null) {
logger.error('unable to check mfa: mfa token not set');
return [AuthResult.Failed, {code: "missing_token", message: "not logged in"}];
}
const sess = await isAuthedSession();
if (sess[0] !== AuthResult.Successful) {
logger.error('unable to check mfa: session token invalid');
return [AuthResult.Failed, sess[1]];
}
try {
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
const resp = await fetch(`${PUBLIC_BASE_URL}/v2/whoami`, {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${window.localStorage.getItem("session")} ${window.localStorage.getItem("mfa")}`
}
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error fetching user information: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
return [AuthResult.Successful, JSON.parse(await resp.text()).data.actor]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
export async function authSession(email: string): Promise<[AuthResult, null | APIError]> {
logger.info('Sending new session authentication');
try {
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
const resp = await fetch(`${PUBLIC_BASE_URL}/v1/auth/magic-link`, {
'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');
try {
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
const resp = await fetch(`${PUBLIC_BASE_URL}/v1/auth/verify-magic-link`, {
'method': 'POST',
'body': JSON.stringify({
magicLinkToken: ml
}),
'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}];
}
window.localStorage.setItem("session", JSON.parse(await resp.text()).data.sessionToken);
return [AuthResult.Successful, null]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
} }

View File

@ -5,11 +5,36 @@
"linkbody": "Perhaps you meant to visit the {link0}?", "linkbody": "Perhaps you meant to visit the {link0}?",
"linkbody.link0": "admin panel" "linkbody.link0": "admin panel"
}, },
"login": {
"title": "Log in to your account",
"subtitle": "We'll send you an email with a \"magic link\"",
"label": "What is your email?",
"button": "Log in",
"email": "Check your email",
"emailbody": "We sent you an email with a link to complete logging in.",
"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}",
"usermissing": "That user does not exist."
}
},
"ml": {
"header": "Authenticated!",
"body": "Redirecting to admin page...",
"error": {
"notoken": "magic link token missing",
"badtoken": "token is invalid or has expired"
}
},
"common": { "common": {
"title": "{title} | Trifid Web UI", "title": "{title} | Trifid Web UI",
"page": { "page": {
"itworks": "It Works!", "itworks": "It Works!",
"admin": "Admin Panel" "admin": "Admin Panel",
"login": "Login",
"ml": "Verify Magic Link"
} }
} }
} }

View File

@ -38,6 +38,12 @@ export function logSetup() {
logger.info("Logger setup complete"); logger.info("Logger setup complete");
} }
export function logDeltaReset() {
timestampStart = Date.now();
deltaTimestamp = Date.now();
logger.info("delta reset");
}
function log(level: number, module: string, message: string) { function log(level: number, module: string, message: string) {
const log = { const log = {
level: level, level: level,

View File

@ -20,6 +20,6 @@
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}> <LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
<h1>{$t('itworks.header')}</h1> <h1>{$t('itworks.header')}</h1>
<p>{$t('itworks.body')}</p> <p>{$t('itworks.body')}</p>
<!-- eslint-disable-next-line --> <!-- 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>'}})}</p>
</LoadingWrapper> </LoadingWrapper>

View File

@ -4,7 +4,7 @@
import {onMount} from "svelte"; import {onMount} from "svelte";
import {AuthResult, isAuthedSession} from "$lib/auth.ts"; import {AuthResult, isAuthedSession} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger"; import {Logger, logSetup} from "$lib/logger";
import type {SessionAuthError} from "$lib/auth.ts"; import type {APIError} from "$lib/auth.ts";
let loading = true; let loading = true;
let isError = false; let isError = false;
@ -17,7 +17,7 @@
onMount(async () => { onMount(async () => {
let session_load_info = await isAuthedSession(); let session_load_info = await isAuthedSession();
if (session_load_info[0] == AuthResult.Failed) { if (session_load_info[0] == AuthResult.Failed) {
let err = session_load_info[1] as SessionAuthError; let err = session_load_info[1] as APIError;
logger.error(`session load failed: ${err.code} ${err.message}`); logger.error(`session load failed: ${err.code} ${err.message}`);
window.location.href = '/login'; window.location.href = '/login';
return; return;

View File

@ -0,0 +1,92 @@
<script lang="ts">
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {AuthResult, authSession, isAuthedSession} 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("login/+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('login.error.invalidEmail');
hasErrForm = true;
loading = false;
return;
}
let auth_result = await authSession(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_DOES_NOT_EXIST") {
errForm = $t('login.error.usermissing');
} else {
errForm = $t('login.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.login")}})}</title>
</svelte:head>
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
{#if isDone}
<h1>{$t('login.email')}</h1>
<p>{$t('login.emailbody')}</p>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
<p>{@html $t('login.emailbody2', {values:{link0:'<a href="/login">'+$t('login.emailbody2.link0')+'</a>'}})}</p>
{:else}
<h1>{$t('login.title')}</h1>
<h3>{$t('login.subtitle')}</h3>
<form on:submit|preventDefault={onSubmit}>
<label for="email">{$t('login.label')}</label>
<input type="email" bind:value={email} placeholder="john.doe@google.com" id="email"/>
<button>{$t('login.button')}</button>
{#if hasErrForm}
<p>{errForm}</p>
{/if}
</form>
{/if}
</LoadingWrapper>

View File

@ -0,0 +1,54 @@
<script lang="ts">
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {AuthResult, verifyLink} 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("magic-link/+page.svelte");
onMount(async () => {
let url = new URLSearchParams(window.location.search);
if (!url.has("magicLinkToken")) {
logger.error('url does not contain magicLinkToken');
isError = true;
error = $t("ml.error.notoken");
}
let call_result = await verifyLink(url.get("magicLinkToken"));
if (call_result[0] !== AuthResult.Successful) {
let err = (call_result[1] as APIError).code;
if (err === "ERR_UNAUTHORIZED" || err === "ERR_EXPIRED") {
error = $t('ml.error.badtoken');
} else {
error = err;
}
isError = true;
loading = false;
setTimeout(() => window.location.href = "/login", 1000);
} else {
loading = false;
window.location.href = "/admin";
}
})
</script>
<svelte:head>
<title>{$t("common.title", {values: {title: $t("common.page.login")}})}</title>
</svelte:head>
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
<h1>{$t('ml.header')}</h1>
<p>{$t('ml.body')}</p>
</LoadingWrapper>

View File

@ -8,6 +8,7 @@ edition = "2021"
[dependencies] [dependencies]
actix-web = "4" # Web framework actix-web = "4" # Web framework
actix-request-identifier = "4" # Web framework actix-request-identifier = "4" # Web framework
actix-cors = "0.6.4" # Web framework
serde = { version = "1", features = ["derive"] } # Serialization and deserialization serde = { version = "1", features = ["derive"] } # Serialization and deserialization
serde_json = "1.0.95" # Serialization and deserialization (cursors) serde_json = "1.0.95" # Serialization and deserialization (cursors)

View File

@ -23,6 +23,7 @@ use log::{info, Level};
use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use std::error::Error; use std::error::Error;
use std::time::Duration; use std::time::Duration;
use actix_cors::Cors;
use crate::config::CONFIG; use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse}; use crate::error::{APIError, APIErrorsResponse};
@ -75,6 +76,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.wrap(Cors::permissive())
.app_data(data.clone()) .app_data(data.clone())
.app_data( .app_data(
JsonConfig::default() JsonConfig::default()