diff --git a/Cargo.lock b/Cargo.lock
index fa27b04..93025dc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -54,6 +54,15 @@ dependencies = [
"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]]
name = "async-stream"
version = "0.3.3"
@@ -202,6 +211,21 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "cipher"
version = "0.4.3"
@@ -212,6 +236,16 @@ dependencies = [
"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]]
name = "color_quant"
version = "1.1.0"
@@ -238,7 +272,7 @@ dependencies = [
"rand",
"sha2",
"subtle",
- "time",
+ "time 0.3.17",
"version_check",
]
@@ -330,6 +364,50 @@ dependencies = [
"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]]
name = "devise"
version = "0.3.1"
@@ -591,7 +669,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@@ -756,6 +834,30 @@ dependencies = [
"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]]
name = "idna"
version = "0.3.0"
@@ -851,6 +953,15 @@ version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "lock_api"
version = "0.4.9"
@@ -938,7 +1049,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [
"libc",
"log",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.42.0",
]
@@ -1413,7 +1524,7 @@ dependencies = [
"serde_json",
"state",
"tempfile",
- "time",
+ "time 0.3.17",
"tokio",
"tokio-stream",
"tokio-util",
@@ -1460,7 +1571,7 @@ dependencies = [
"smallvec",
"stable-pattern",
"state",
- "time",
+ "time 0.3.17",
"tokio",
"uncased",
]
@@ -1498,6 +1609,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+[[package]]
+name = "scratch"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
+
[[package]]
name = "security-framework"
version = "2.8.2"
@@ -1793,6 +1910,15 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
[[package]]
name = "tfclient"
version = "0.1.0"
@@ -1826,6 +1952,17 @@ dependencies = [
"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]]
name = "time"
version = "0.3.17"
@@ -2069,6 +2206,7 @@ name = "trifid-api"
version = "0.1.0"
dependencies = [
"base64 0.21.0",
+ "chrono",
"dotenvy",
"log",
"paste",
@@ -2141,6 +2279,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
[[package]]
name = "unicode-xid"
version = "0.2.4"
@@ -2231,6 +2375,12 @@ dependencies = [
"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]]
name = "wasi"
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"
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]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
diff --git a/tfweb/.idea/jsLibraryMappings.xml b/tfweb/.idea/jsLibraryMappings.xml
index f837370..703ac1f 100644
--- a/tfweb/.idea/jsLibraryMappings.xml
+++ b/tfweb/.idea/jsLibraryMappings.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/tfweb/.idea/tfweb.iml b/tfweb/.idea/tfweb.iml
index ed165c4..04ff971 100644
--- a/tfweb/.idea/tfweb.iml
+++ b/tfweb/.idea/tfweb.iml
@@ -12,5 +12,6 @@
+
\ No newline at end of file
diff --git a/tfweb/src/app.css b/tfweb/src/app.css
index b5c61c9..eae09d8 100644
--- a/tfweb/src/app.css
+++ b/tfweb/src/app.css
@@ -1,3 +1,14 @@
@tailwind base;
@tailwind components;
@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;
+ }
+}
\ No newline at end of file
diff --git a/tfweb/src/components/QR.svelte b/tfweb/src/components/QR.svelte
new file mode 100644
index 0000000..e53c6b5
--- /dev/null
+++ b/tfweb/src/components/QR.svelte
@@ -0,0 +1,29 @@
+
+
+
+
diff --git a/tfweb/src/lib/auth.ts b/tfweb/src/lib/auth.ts
index a61e169..ab405e2 100644
--- a/tfweb/src/lib/auth.ts
+++ b/tfweb/src/lib/auth.ts
@@ -1,7 +1,7 @@
import {fetch_timeout} from "./util";
import {API_ROOT} from "./config";
import {Logger, logSetup} from "./logger";
-import {getCookie} from "./cookie";
+import {getCookie, setCookie} from "./cookie";
logSetup();
const logger = new Logger("auth.ts");
@@ -11,11 +11,51 @@ export function redact_token(token: string) {
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 {
+ 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]> {
logger.info("Checking session authentication");
const session_token = getCookie("sessionToken");
if (session_token === "") {
logger.error("No session token is present");
+ setCookie("sessionToken", "", -1);
return [false, ""];
}
logger.info(`Session token is ${redact_token(session_token)}`);
@@ -31,6 +71,7 @@ export async function enforce_session(): Promise<[boolean, string]> {
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`session token is invalid: ${rawerror}`);
+ setCookie("sessionToken", "", -1);
return [false, rawerror];
} else {
logger.info("session token OK");
@@ -40,6 +81,7 @@ export async function enforce_session(): Promise<[boolean, string]> {
} catch (e) {
// error in http request
logger.error(`session token is invalid: ${e}`);
+ setCookie("sessionToken", "", -1);
return [false, `${e}`]
}
}
@@ -60,6 +102,7 @@ export async function enforce_auth(): Promise<[boolean, string]> {
const auth_token = getCookie("authToken");
if (auth_token === "") {
logger.error("No auth token is present");
+ setCookie("authToken", "", -1);
return [false, ""];
}
logger.info(`MFA token is ${redact_token(auth_token)}`);
@@ -74,6 +117,7 @@ export async function enforce_auth(): Promise<[boolean, string]> {
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
+ setCookie("authToken", "", -1);
return [false, rawerror];
} else {
// session ok
@@ -81,6 +125,7 @@ export async function enforce_auth(): Promise<[boolean, string]> {
}
} catch (e) {
// error in http request
+ setCookie("authToken", "", -1);
return [false, `${e}`]
}
}
\ No newline at end of file
diff --git a/tfweb/src/lib/i18n/en.json b/tfweb/src/lib/i18n/en.json
index fbea936..ab88f1f 100644
--- a/tfweb/src/lib/i18n/en.json
+++ b/tfweb/src/lib/i18n/en.json
@@ -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.",
"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"
+ }
}
}
\ No newline at end of file
diff --git a/tfweb/src/lib/totp.ts b/tfweb/src/lib/totp.ts
new file mode 100644
index 0000000..aeb3d57
--- /dev/null
+++ b/tfweb/src/lib/totp.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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}`
+ }
+}
\ No newline at end of file
diff --git a/tfweb/src/routes/admin/+page.svelte b/tfweb/src/routes/admin/+page.svelte
index a4eee09..b11f758 100644
--- a/tfweb/src/routes/admin/+page.svelte
+++ b/tfweb/src/routes/admin/+page.svelte
@@ -10,7 +10,6 @@
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
- logger.info(st_result);
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
diff --git a/tfweb/src/routes/auth/mfa/+page.svelte b/tfweb/src/routes/auth/mfa/+page.svelte
index 849ce91..0bfa2d3 100644
--- a/tfweb/src/routes/auth/mfa/+page.svelte
+++ b/tfweb/src/routes/auth/mfa/+page.svelte
@@ -1,8 +1,109 @@
\ No newline at end of file
+
+ 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";
+ }
+
+
+
+
+
+ {#if !isFinished}
+
+
+
{t('mfa.title')}
+ {t('mfa.subtitle')}
+
+
+
+
+ {:else}
+
+
+
{t('mfa.done')}
+ {t('mfa.doneSubtitle')}
+
+ {/if}
+
+
\ No newline at end of file
diff --git a/tfweb/src/routes/auth/mfasetup/+page.svelte b/tfweb/src/routes/auth/mfasetup/+page.svelte
new file mode 100644
index 0000000..674f472
--- /dev/null
+++ b/tfweb/src/routes/auth/mfasetup/+page.svelte
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
{t('mfasetup.title')}
+
{t('mfasetup.subtitle')}
+
+ {#if isLoadingMFA}
+
+
+
{t('mfasetup.loadingmfa')}
+
+ {:else}
+
{t('mfasetup.qrtitle')}
+
+
+
+
{t('mfasetup.secrettitle')}
+
+ {totp_secret.match(/.{1,4}/g).join(" ")}
+
+
+
+ {/if}
+
+
\ No newline at end of file
diff --git a/trifid-api/Cargo.toml b/trifid-api/Cargo.toml
index 9cc94d0..9f9ac6e 100644
--- a/trifid-api/Cargo.toml
+++ b/trifid-api/Cargo.toml
@@ -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"]}
url = { version = "2.3.1", features = ["serde"] }
urlencoding = "2.1.2"
+chrono = "0.4.23"
\ No newline at end of file
diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs
index 6494d47..2d7fa80 100644
--- a/trifid-api/src/main.rs
+++ b/trifid-api/src/main.rs
@@ -116,7 +116,9 @@ async fn main() -> Result<(), Box> {
crate::routes::v1::auth::check_session::check_session,
crate::routes::v1::auth::check_session::check_session_auth,
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![
crate::routes::handler_400,
diff --git a/trifid-api/src/routes/mod.rs b/trifid-api/src/routes/mod.rs
index 5c2f1b3..839d56f 100644
--- a/trifid-api/src/routes/mod.rs
+++ b/trifid-api/src/routes/mod.rs
@@ -1,4 +1,5 @@
pub mod v1;
+pub mod v2;
use rocket::catch;
use serde::{Serialize};
@@ -25,7 +26,7 @@ TODO:
/v1/verify-totp-authenticator [done]
/v1/dnclient
/v2/enroll
- /v2/whoami
+ /v2/whoami [in-progress]
*/
#[derive(Serialize)]
diff --git a/trifid-api/src/routes/v2/mod.rs b/trifid-api/src/routes/v2/mod.rs
new file mode 100644
index 0000000..f511e9f
--- /dev/null
+++ b/trifid-api/src/routes/v2/mod.rs
@@ -0,0 +1 @@
+pub mod whoami;
\ No newline at end of file
diff --git a/trifid-api/src/routes/v2/whoami.rs b/trifid-api/src/routes/v2/whoami.rs
new file mode 100644
index 0000000..27bea05
--- /dev/null
+++ b/trifid-api/src/routes/v2/whoami.rs
@@ -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) -> Result<(ContentType, Json), (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 {},
+ })))
+}
\ No newline at end of file