full 2fa flow
This commit is contained in:
parent
0c5656efb6
commit
5bdcac5e89
|
@ -54,6 +54,15 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
@ -202,6 +211,21 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"time 0.1.45",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
@ -212,6 +236,16 @@ dependencies = [
|
||||||
"inout",
|
"inout",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codespan-reporting"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||||
|
dependencies = [
|
||||||
|
"termcolor",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -238,7 +272,7 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"time",
|
"time 0.3.17",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -330,6 +364,50 @@ dependencies = [
|
||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx"
|
||||||
|
version = "1.0.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cxxbridge-flags",
|
||||||
|
"cxxbridge-macro",
|
||||||
|
"link-cplusplus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-build"
|
||||||
|
version = "1.0.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"codespan-reporting",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"scratch",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-flags"
|
||||||
|
version = "1.0.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-macro"
|
||||||
|
version = "1.0.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "devise"
|
name = "devise"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -591,7 +669,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -756,6 +834,30 @@ dependencies = [
|
||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.53"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||||
|
dependencies = [
|
||||||
|
"cxx",
|
||||||
|
"cxx-build",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -851,6 +953,15 @@ version = "0.2.139"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "link-cplusplus"
|
||||||
|
version = "1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
@ -938,7 +1049,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.42.0",
|
"windows-sys 0.42.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1413,7 +1524,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"state",
|
"state",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"time",
|
"time 0.3.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -1460,7 +1571,7 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"stable-pattern",
|
"stable-pattern",
|
||||||
"state",
|
"state",
|
||||||
"time",
|
"time 0.3.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uncased",
|
"uncased",
|
||||||
]
|
]
|
||||||
|
@ -1498,6 +1609,12 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scratch"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
|
@ -1793,6 +1910,15 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tfclient"
|
name = "tfclient"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1826,6 +1952,17 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
@ -2069,6 +2206,7 @@ name = "trifid-api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.0",
|
"base64 0.21.0",
|
||||||
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"log",
|
"log",
|
||||||
"paste",
|
"paste",
|
||||||
|
@ -2141,6 +2279,12 @@ version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
@ -2231,6 +2375,12 @@ dependencies = [
|
||||||
"try-lock",
|
"try-lock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
@ -2327,6 +2477,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="JavaScriptLibraryMappings">
|
<component name="JavaScriptLibraryMappings">
|
||||||
<file url="PROJECT" libraries="{all, flag-icons}" />
|
<file url="PROJECT" libraries="{all, flag-icons, qrcode}" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -12,5 +12,6 @@
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" name="flag-icons" level="application" />
|
<orderEntry type="library" name="flag-icons" level="application" />
|
||||||
<orderEntry type="library" name="all" level="application" />
|
<orderEntry type="library" name="all" level="application" />
|
||||||
|
<orderEntry type="library" name="qrcode" level="application" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
|
@ -1,3 +1,14 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
input[type=number].appearance-none::-webkit-inner-spin-button,
|
||||||
|
input[type=number].appearance-none::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
input[type=number].appearance-none {
|
||||||
|
-moz-appearance:textfield;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let codeValue;
|
||||||
|
|
||||||
|
let qrcode;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
|
||||||
|
let script = document.createElement('script');
|
||||||
|
script.src = "https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"
|
||||||
|
document.head.append(script);
|
||||||
|
|
||||||
|
script.onload = function() {
|
||||||
|
|
||||||
|
qrcode = new QRCode("qrcode", {
|
||||||
|
text: codeValue,
|
||||||
|
colorDark : "#000000",
|
||||||
|
colorLight : "#ffffff",
|
||||||
|
correctLevel : QRCode.CorrectLevel.H
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="qrcode" class="w-max"></div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {fetch_timeout} from "./util";
|
import {fetch_timeout} from "./util";
|
||||||
import {API_ROOT} from "./config";
|
import {API_ROOT} from "./config";
|
||||||
import {Logger, logSetup} from "./logger";
|
import {Logger, logSetup} from "./logger";
|
||||||
import {getCookie} from "./cookie";
|
import {getCookie, setCookie} from "./cookie";
|
||||||
|
|
||||||
logSetup();
|
logSetup();
|
||||||
const logger = new Logger("auth.ts");
|
const logger = new Logger("auth.ts");
|
||||||
|
@ -11,11 +11,51 @@ export function redact_token(token: string) {
|
||||||
return token.substring(0, 5) + stars;
|
return token.substring(0, 5) + stars;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
data: UserData,
|
||||||
|
metadata: object
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserData {
|
||||||
|
actorType: string,
|
||||||
|
actor: Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Actor {
|
||||||
|
id: string,
|
||||||
|
organizationID: string,
|
||||||
|
email: string,
|
||||||
|
createdAt: string,
|
||||||
|
hasTOTPAuthenticator: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_user_info(api_key: string): Promise<UserInfo | string> {
|
||||||
|
logger.info("Asking server for user information");
|
||||||
|
try {
|
||||||
|
const resp = await fetch_timeout(`${API_ROOT}/v2/whoami`, {
|
||||||
|
'method': 'GET',
|
||||||
|
'headers': {
|
||||||
|
'Authorization': `Bearer ${api_key}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
|
logger.error(`error fetching user information: ${rawerror}`);
|
||||||
|
return rawerror;
|
||||||
|
}
|
||||||
|
return JSON.parse(await resp.text()) as UserInfo;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Error fetching userinfo: ${e}`);
|
||||||
|
return `${e}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function enforce_session(): Promise<[boolean, string]> {
|
export async function enforce_session(): Promise<[boolean, string]> {
|
||||||
logger.info("Checking session authentication");
|
logger.info("Checking session authentication");
|
||||||
const session_token = getCookie("sessionToken");
|
const session_token = getCookie("sessionToken");
|
||||||
if (session_token === "") {
|
if (session_token === "") {
|
||||||
logger.error("No session token is present");
|
logger.error("No session token is present");
|
||||||
|
setCookie("sessionToken", "", -1);
|
||||||
return [false, ""];
|
return [false, ""];
|
||||||
}
|
}
|
||||||
logger.info(`Session token is ${redact_token(session_token)}`);
|
logger.info(`Session token is ${redact_token(session_token)}`);
|
||||||
|
@ -31,6 +71,7 @@ export async function enforce_session(): Promise<[boolean, string]> {
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
logger.error(`session token is invalid: ${rawerror}`);
|
logger.error(`session token is invalid: ${rawerror}`);
|
||||||
|
setCookie("sessionToken", "", -1);
|
||||||
return [false, rawerror];
|
return [false, rawerror];
|
||||||
} else {
|
} else {
|
||||||
logger.info("session token OK");
|
logger.info("session token OK");
|
||||||
|
@ -40,6 +81,7 @@ export async function enforce_session(): Promise<[boolean, string]> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// error in http request
|
// error in http request
|
||||||
logger.error(`session token is invalid: ${e}`);
|
logger.error(`session token is invalid: ${e}`);
|
||||||
|
setCookie("sessionToken", "", -1);
|
||||||
return [false, `${e}`]
|
return [false, `${e}`]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,6 +102,7 @@ export async function enforce_auth(): Promise<[boolean, string]> {
|
||||||
const auth_token = getCookie("authToken");
|
const auth_token = getCookie("authToken");
|
||||||
if (auth_token === "") {
|
if (auth_token === "") {
|
||||||
logger.error("No auth token is present");
|
logger.error("No auth token is present");
|
||||||
|
setCookie("authToken", "", -1);
|
||||||
return [false, ""];
|
return [false, ""];
|
||||||
}
|
}
|
||||||
logger.info(`MFA token is ${redact_token(auth_token)}`);
|
logger.info(`MFA token is ${redact_token(auth_token)}`);
|
||||||
|
@ -74,6 +117,7 @@ export async function enforce_auth(): Promise<[boolean, string]> {
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
|
setCookie("authToken", "", -1);
|
||||||
return [false, rawerror];
|
return [false, rawerror];
|
||||||
} else {
|
} else {
|
||||||
// session ok
|
// session ok
|
||||||
|
@ -81,6 +125,7 @@ export async function enforce_auth(): Promise<[boolean, string]> {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// error in http request
|
// error in http request
|
||||||
|
setCookie("authToken", "", -1);
|
||||||
return [false, `${e}`]
|
return [false, `${e}`]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -47,5 +47,27 @@
|
||||||
"unable to parse the request body, is it properly formatted?": "There was an error processing your request, please try again later.",
|
"unable to parse the request body, is it properly formatted?": "There was an error processing your request, please try again later.",
|
||||||
"this token is invalid - no rows returned by a query that expected to return at least one row": "This token is invalid or has expired."
|
"this token is invalid - no rows returned by a query that expected to return at least one row": "This token is invalid or has expired."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"mfa": {
|
||||||
|
"title": "Two-factor authentication",
|
||||||
|
"subtitle": "Enter the code displayed on your authenticator app",
|
||||||
|
"actionButtonText": "Check code",
|
||||||
|
"apierror": {
|
||||||
|
"invalid TOTP code (maybe it expired?)": "Incorrect 2FA code"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"mfasetup": {
|
||||||
|
"title": "Protect your account",
|
||||||
|
"subtitle": "2FA is required for all trifid accounts. Protect your account with any TOTP-compatible authenticator app.",
|
||||||
|
"qrtitle": "Scan the QR code with your authenticator app.",
|
||||||
|
"secrettitle": "Or, copy this code into your authenticator app.",
|
||||||
|
"verifytitle": "Enter the code shown on your authenticator app",
|
||||||
|
"loadingmfa": "Hang on while we load your account...",
|
||||||
|
"actionButtonText": "Add authenticator",
|
||||||
|
"apierror": {
|
||||||
|
"Invalid TOTP code": "Incorrect 2FA code"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
import {fetch_timeout} from "./util";
|
||||||
|
import {Logger, logSetup} from "./logger";
|
||||||
|
import {API_ROOT} from "./config";
|
||||||
|
|
||||||
|
const logger = new Logger("totp.ts");
|
||||||
|
logSetup();
|
||||||
|
|
||||||
|
export interface TOTPSetupDetails {
|
||||||
|
totpToken: string,
|
||||||
|
secret: string,
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startTotpSetup(api_key: string): Promise<TOTPSetupDetails | string> {
|
||||||
|
logger.info("Starting TOTP setup");
|
||||||
|
try {
|
||||||
|
const resp = await fetch_timeout(`${API_ROOT}/v1/totp-authenticators`, {
|
||||||
|
'method': 'POST',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${api_key}`
|
||||||
|
},
|
||||||
|
'body': "{}"
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
|
logger.error(`API returned error setting up TOTP: ${rawerror}`);
|
||||||
|
return rawerror;
|
||||||
|
}
|
||||||
|
logger.info('Initiated TOTP setup successfully');
|
||||||
|
return (await resp.json()).data as TOTPSetupDetails;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Error while trying to setup TOTP: ${e}`);
|
||||||
|
return `${e}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TOTPToken {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function finishTOTPSetup(api_key: string, token: string, code: string): Promise<TOTPToken | string> {
|
||||||
|
logger.info("Finishing up TOTP setup");
|
||||||
|
try {
|
||||||
|
const resp = await fetch_timeout(`${API_ROOT}/v1/verify-totp-authenticator`, {
|
||||||
|
'method': 'POST',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${api_key}`
|
||||||
|
},
|
||||||
|
'body': `{"totpToken":"${token}","code":"${code}"}`
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
|
logger.error(`API returned error finishing up TOTP: ${rawerror}`);
|
||||||
|
return rawerror;
|
||||||
|
}
|
||||||
|
logger.info('Finished TOTP setup! Auth token issued');
|
||||||
|
return {
|
||||||
|
token: (await resp.json()).data.authToken
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Error while trying to finish TOTP: ${e}`);
|
||||||
|
return `${e}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateTOTP(api_key: string, code: string): Promise<TOTPToken | string> {
|
||||||
|
logger.info("Validating 2fa code");
|
||||||
|
try {
|
||||||
|
const resp = await fetch_timeout(`${API_ROOT}/v1/auth/totp`, {
|
||||||
|
'method': 'POST',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${api_key}`
|
||||||
|
},
|
||||||
|
'body': `{"code":"${code}"}`
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
|
logger.error(`API returned error verifying TOTP: ${rawerror}`);
|
||||||
|
return rawerror;
|
||||||
|
}
|
||||||
|
logger.info('auth token issued');
|
||||||
|
return {
|
||||||
|
token: (await resp.json()).data.authToken
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Error while trying to validate TOTP: ${e}`);
|
||||||
|
return `${e}`
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let st_result = await enforce_session();
|
let st_result = await enforce_session();
|
||||||
if (!st_result[0]) {
|
if (!st_result[0]) {
|
||||||
logger.info(st_result);
|
|
||||||
// Session token is invalid. redirect to login
|
// Session token is invalid. redirect to login
|
||||||
window.location = "/auth/login";
|
window.location = "/auth/login";
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,8 +1,109 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import {enforce_session} from "../../../lib/auth";
|
import {enforce_session} from "$lib/auth";
|
||||||
|
import {Logger, logSetup} from "$lib/logger";
|
||||||
|
import {get_user_info} from "$lib/auth";
|
||||||
|
import type {UserInfo} from "$lib/auth";
|
||||||
|
import {t} from "$lib/i18n";
|
||||||
|
import {validateTOTP} from "$lib/totp";
|
||||||
|
import {setCookie} from "$lib/cookie";
|
||||||
|
import {enforce_auth} from "$lib/auth";
|
||||||
|
|
||||||
|
let logger = new Logger("auth/mfa/+page.svelte");
|
||||||
|
logSetup();
|
||||||
|
|
||||||
|
let isFinished = false;
|
||||||
|
let isloading = false;
|
||||||
|
let api_token = "";
|
||||||
|
let mfa_token = "";
|
||||||
|
let hasError = false;
|
||||||
|
let error = "";
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let st_res = await enforce_session();
|
let st_res = await enforce_session();
|
||||||
|
if (!st_res[0]) {
|
||||||
|
// session token is invalid
|
||||||
|
// Session token is invalid. redirect to login
|
||||||
|
logger.info("Invalid session token, redirecting to login");
|
||||||
|
window.location = "/auth/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api_token = st_res[1];
|
||||||
|
|
||||||
|
let at_res = await enforce_auth();
|
||||||
|
if (at_res[0]) {
|
||||||
|
logger.info("User already TOTP authenticated");
|
||||||
|
window.location = "/admin";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: UserInfo | string = await get_user_info(api_token);
|
||||||
|
if (typeof user === "string") {
|
||||||
|
logger.error(`Unable to get user info: ${user}`);
|
||||||
|
hasError = true;
|
||||||
|
error = user;
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
|
async function tryMFACode() {
|
||||||
|
isloading = true;
|
||||||
|
logger.info(`Submitting 2FA verify with code ${mfa_token}`);
|
||||||
|
let resp = await validateTOTP(api_token, mfa_token);
|
||||||
|
if (typeof resp === "string") {
|
||||||
|
logger.error(`Unable to validate TOTP token: ${resp}`);
|
||||||
|
hasError = true;
|
||||||
|
isloading = false;
|
||||||
|
error = t(`mfa.apierror.${resp}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// set cookie
|
||||||
|
setCookie("authToken", resp.token, 86400 * 365);
|
||||||
|
window.location = "/admin";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
{#if !isFinished}
|
||||||
|
<!-- Title -->
|
||||||
|
<div>
|
||||||
|
<h1 class="font-semibold text-2xl">{t('mfa.title')}</h1>
|
||||||
|
<h2 class="ftext-sm">{t('mfa.subtitle')}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- The actual form -->
|
||||||
|
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={tryMFACode}>
|
||||||
|
<div class="-space-y-px rounded-md shadow-sm">
|
||||||
|
<label for="mfa_token" class="sr-only">{t('mfa.prompt')}</label>
|
||||||
|
<input bind:value={mfa_token} type="number" maxlength="6" id="mfa_token"
|
||||||
|
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
|
||||||
|
{#if hasError}
|
||||||
|
<span class="text-red-600 text-sm">{error}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 -space-y-px rounded-md shadow-sm place-content-center">
|
||||||
|
{#if !isloading}
|
||||||
|
{t('mfa.actionButtonText')}
|
||||||
|
{:else}
|
||||||
|
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||||
|
stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<!-- Title -->
|
||||||
|
<div>
|
||||||
|
<h1 class="font-semibold text-2xl">{t('mfa.done')}</h1>
|
||||||
|
<h2 class="ftext-sm">{t('mfa.doneSubtitle')}</h2>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,148 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import {enforce_session} from "$lib/auth";
|
||||||
|
import {Logger, logSetup} from "$lib/logger";
|
||||||
|
import {get_user_info} from "$lib/auth";
|
||||||
|
import type {UserInfo} from "$lib/auth";
|
||||||
|
import {t} from "$lib/i18n";
|
||||||
|
import {startTotpSetup} from "$lib/totp";
|
||||||
|
import {browser} from "$app/environment";
|
||||||
|
import QR from "../../../components/QR.svelte";
|
||||||
|
import {finishTOTPSetup} from "$lib/totp";
|
||||||
|
import {setCookie} from "$lib/cookie";
|
||||||
|
|
||||||
|
let logger = new Logger("auth/mfa/+page.svelte");
|
||||||
|
logSetup();
|
||||||
|
|
||||||
|
let api_token = "";
|
||||||
|
let isLoadingMFA = true;
|
||||||
|
|
||||||
|
let hasError = false;
|
||||||
|
let error = "";
|
||||||
|
|
||||||
|
let totp_setup_token = "";
|
||||||
|
let totp_otpurl = "";
|
||||||
|
let totp_secret = "";
|
||||||
|
let mfa_token = "";
|
||||||
|
|
||||||
|
let isloading = false;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let st_res = await enforce_session();
|
||||||
|
if (!st_res[0]) {
|
||||||
|
// session token is invalid
|
||||||
|
// Session token is invalid. redirect to mfa
|
||||||
|
logger.info("Invalid session token, redirecting to mfa");
|
||||||
|
window.location = "/auth/mfa";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api_token = st_res[1];
|
||||||
|
let user: UserInfo | string = await get_user_info(api_token);
|
||||||
|
if (typeof user === "string") {
|
||||||
|
logger.error(`Unable to get user info: ${user}`);
|
||||||
|
hasError = true;
|
||||||
|
error = user;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.data.actor.hasTOTPAuthenticator) {
|
||||||
|
logger.info("User already has mfa set up, redirecting");
|
||||||
|
window.location = "/auth/mfa";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = await startTotpSetup(api_token);
|
||||||
|
if (typeof resp === "string") {
|
||||||
|
logger.error(`Unable to create TOTP token: ${resp}`);
|
||||||
|
hasError = true;
|
||||||
|
error = t(`mfasetup.apierror.${resp}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
totp_setup_token = resp.totpToken;
|
||||||
|
totp_secret = resp.secret;
|
||||||
|
totp_otpurl = resp.url;
|
||||||
|
|
||||||
|
isLoadingMFA = false;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateInput(evt) {
|
||||||
|
if (evt.which < 48 || evt.which > 57) {
|
||||||
|
evt.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryMFACode() {
|
||||||
|
isloading = true;
|
||||||
|
logger.info(`Submitting 2FA verify with code ${mfa_token}`);
|
||||||
|
let resp = await finishTOTPSetup(api_token, totp_setup_token, mfa_token);
|
||||||
|
if (typeof resp === "string") {
|
||||||
|
logger.error(`Unable to validate TOTP token: ${resp}`);
|
||||||
|
hasError = true;
|
||||||
|
isloading = false;
|
||||||
|
error = t(`mfasetup.apierror.${resp}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// set cookie
|
||||||
|
setCookie("authToken", resp.token);
|
||||||
|
window.location = "/admin";
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<sveltekit:head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/davidshimjs/qrcodejs/qrcode.min.js"></script>
|
||||||
|
</sveltekit:head>
|
||||||
|
|
||||||
|
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<h1 class="font-semibold text-2xl">{t('mfasetup.title')}</h1>
|
||||||
|
<h2 class="text-sm">{t('mfasetup.subtitle')}</h2>
|
||||||
|
|
||||||
|
{#if isLoadingMFA}
|
||||||
|
<div class="mt-5">
|
||||||
|
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||||
|
stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-xs inline-block">{t('mfasetup.loadingmfa')}</h3>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<h4 class="text-sm mt-5 font-semibold ">{t('mfasetup.qrtitle')}</h4>
|
||||||
|
<div class="mt-2 dark:bg-slate-600 bg-slate-200 rounded aspect-square p-4 w-min">
|
||||||
|
<QR codeValue="{totp_otpurl}"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-sm mt-5">{t('mfasetup.secrettitle')}</h4>
|
||||||
|
<span class="font-mono text-md mt-4 block">
|
||||||
|
{totp_secret.match(/.{1,4}/g).join(" ")}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={tryMFACode}>
|
||||||
|
<div class="-space-y-px rounded-md shadow-sm">
|
||||||
|
<input on:keypress={validateInput} bind:value={mfa_token} type="number" id="mfa_token" placeholder="{t('mfasetup.verifytitle')}"
|
||||||
|
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
|
||||||
|
{#if hasError}
|
||||||
|
<span class="text-red-600 text-sm">{error}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="-space-y-px bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 rounded-md shadow-sm place-content-center">
|
||||||
|
{#if !isloading}
|
||||||
|
{t('mfasetup.actionButtonText')}
|
||||||
|
{:else}
|
||||||
|
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||||
|
stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -19,3 +19,4 @@ totp-rs = { version = "4.2.0", features = ["qr", "otpauth", "gen_secret"]}
|
||||||
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics"]}
|
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics"]}
|
||||||
url = { version = "2.3.1", features = ["serde"] }
|
url = { version = "2.3.1", features = ["serde"] }
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
|
chrono = "0.4.23"
|
|
@ -116,7 +116,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
crate::routes::v1::auth::check_session::check_session,
|
crate::routes::v1::auth::check_session::check_session,
|
||||||
crate::routes::v1::auth::check_session::check_session_auth,
|
crate::routes::v1::auth::check_session::check_session_auth,
|
||||||
crate::routes::v1::auth::check_session::options,
|
crate::routes::v1::auth::check_session::options,
|
||||||
crate::routes::v1::auth::check_session::options_auth
|
crate::routes::v1::auth::check_session::options_auth,
|
||||||
|
crate::routes::v2::whoami::whoami_request,
|
||||||
|
crate::routes::v2::whoami::options
|
||||||
])
|
])
|
||||||
.register("/", catchers![
|
.register("/", catchers![
|
||||||
crate::routes::handler_400,
|
crate::routes::handler_400,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod v1;
|
pub mod v1;
|
||||||
|
pub mod v2;
|
||||||
|
|
||||||
use rocket::catch;
|
use rocket::catch;
|
||||||
use serde::{Serialize};
|
use serde::{Serialize};
|
||||||
|
@ -25,7 +26,7 @@ TODO:
|
||||||
/v1/verify-totp-authenticator [done]
|
/v1/verify-totp-authenticator [done]
|
||||||
/v1/dnclient
|
/v1/dnclient
|
||||||
/v2/enroll
|
/v2/enroll
|
||||||
/v2/whoami
|
/v2/whoami [in-progress]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod whoami;
|
|
@ -0,0 +1,61 @@
|
||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use rocket::{options, get, State};
|
||||||
|
use rocket::http::{ContentType, Status};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use crate::auth::PartialUserInfo;
|
||||||
|
use crate::tokens::user_has_totp;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WhoamiMetadata {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WhoamiActor {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "organizationID")]
|
||||||
|
pub organization_id: String,
|
||||||
|
pub email: String,
|
||||||
|
#[serde(rename = "createdAt")]
|
||||||
|
pub created_at: String,
|
||||||
|
#[serde(rename = "hasTOTPAuthenticator")]
|
||||||
|
pub has_totpauthenticator: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WhoamiData {
|
||||||
|
#[serde(rename = "actorType")]
|
||||||
|
pub actor_type: String,
|
||||||
|
pub actor: WhoamiActor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WhoamiResponse {
|
||||||
|
pub data: WhoamiData,
|
||||||
|
pub metadata: WhoamiMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[options("/v2/whoami")]
|
||||||
|
pub fn options() -> &'static str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/v2/whoami")]
|
||||||
|
pub async fn whoami_request(user: PartialUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<WhoamiResponse>), (Status, String)> {
|
||||||
|
Ok((ContentType::JSON, Json(WhoamiResponse {
|
||||||
|
data: WhoamiData {
|
||||||
|
actor_type: "user".to_string(),
|
||||||
|
actor: WhoamiActor {
|
||||||
|
id: user.user_id.to_string(),
|
||||||
|
organization_id: "TEMP_ORG_BECAUSE_THAT_ISNT_IMPLEMENTED_YET".to_string(),
|
||||||
|
email: user.email,
|
||||||
|
created_at: NaiveDateTime::from_timestamp_opt(user.created_at, 0).unwrap().and_local_timezone(Utc).unwrap().to_rfc3339(),
|
||||||
|
has_totpauthenticator: match user_has_totp(user.user_id, db.inner()).await {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DBERROR", "an error occured trying to verify your user", e)))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
metadata: WhoamiMetadata {},
|
||||||
|
})))
|
||||||
|
}
|
Loading…
Reference in New Issue