diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
index 05e0ace..6df4889 100644
--- a/.idea/sqldialects.xml
+++ b/.idea/sqldialects.xml
@@ -1,7 +1,6 @@
-
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index ed77bf0..fa27b04 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
[[package]]
name = "aead"
version = "0.5.1"
@@ -115,6 +121,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+[[package]]
+name = "base32"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
+
[[package]]
name = "base64"
version = "0.13.1"
@@ -160,6 +172,12 @@ version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+[[package]]
+name = "bytemuck"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393"
+
[[package]]
name = "byteorder"
version = "1.4.3"
@@ -194,6 +212,18 @@ dependencies = [
"inout",
]
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279"
+
[[package]]
name = "cookie"
version = "0.16.2"
@@ -252,6 +282,15 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484"
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "crossbeam-queue"
version = "0.3.8"
@@ -405,6 +444,16 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "flate2"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -717,6 +766,20 @@ dependencies = [
"unicode-normalization",
]
+[[package]]
+name = "image"
+version = "0.24.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "num-rational",
+ "num-traits",
+ "png",
+]
+
[[package]]
name = "indexmap"
version = "1.9.2"
@@ -858,6 +921,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+[[package]]
+name = "miniz_oxide"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+dependencies = [
+ "adler",
+]
+
[[package]]
name = "mio"
version = "0.8.5"
@@ -937,6 +1009,27 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.15"
@@ -1120,6 +1213,18 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+[[package]]
+name = "png"
+version = "0.17.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "flate2",
+ "miniz_oxide",
+]
+
[[package]]
name = "polyval"
version = "0.6.0"
@@ -1160,6 +1265,12 @@ dependencies = [
"yansi",
]
+[[package]]
+name = "qrcodegen"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
+
[[package]]
name = "quote"
version = "1.0.23"
@@ -1866,6 +1977,25 @@ dependencies = [
"toml_datetime",
]
+[[package]]
+name = "totp-rs"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fdd21080b6cf581e0c8fe849626ad627b42af1a0f71ce980244f2d6b1a47836"
+dependencies = [
+ "base32",
+ "base64 0.20.0",
+ "constant_time_eq",
+ "hmac",
+ "image",
+ "qrcodegen",
+ "rand",
+ "sha1",
+ "sha2",
+ "url",
+ "urlencoding",
+]
+
[[package]]
name = "tower-service"
version = "0.3.2"
@@ -1947,6 +2077,10 @@ dependencies = [
"sqlx",
"tokio",
"toml 0.7.1",
+ "totp-rs",
+ "url",
+ "urlencoding",
+ "uuid",
]
[[package]]
@@ -2038,6 +2172,35 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
+
+[[package]]
+name = "uuid"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
+dependencies = [
+ "getrandom",
+ "rand",
+ "uuid-macro-internal",
+]
+
+[[package]]
+name = "uuid-macro-internal"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1b300a878652a387d2a0de915bdae8f1a548f0c6d45e072fe2688794b656cc9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
diff --git a/api/v1/auth/magic-link.txt b/api/v1/auth/magic-link.txt
new file mode 100644
index 0000000..b36e362
--- /dev/null
+++ b/api/v1/auth/magic-link.txt
@@ -0,0 +1,34 @@
+POST /v1/auth/magic-link HTTP/2
+Host: api.defined.net
+Content-Length: 29
+Sec-Ch-Ua: "Chromium";v="109", "Not_A Brand";v="99"
+Accept: application/json
+Content-Type: application/json
+Sec-Ch-Ua-Mobile: ?0
+User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.75 Safari/537.36
+Sec-Ch-Ua-Platform: "Linux"
+Origin: https://admin.defined.net
+Sec-Fetch-Site: same-site
+Sec-Fetch-Mode: cors
+Sec-Fetch-Dest: empty
+Accept-Encoding: gzip, deflate
+Accept-Language: en-US,en;q=0.9
+
+{"email":"core@coredoes.dev"}
+
+HTTP/2 200 OK
+Access-Control-Allow-Credentials: true
+Access-Control-Allow-Origin: https://admin.defined.net
+Access-Control-Expose-Headers: X-Request-Id
+Cache-Control: no-store
+Content-Security-Policy: default-src 'none'
+Content-Type: application/json; charset=utf-8
+Strict-Transport-Security: max-age=31536000; includeSubdomains
+Vary: Origin
+X-Content-Type-Options: nosniff
+X-Frame-Options: DENY
+X-Request-Id: GI4RVV7JQIINE3DDPCYOBAT6TI
+Content-Length: 26
+Date: Sat, 04 Feb 2023 17:45:20 GMT
+
+{"data":{},"metadata":{}}
diff --git a/trifid-api/Cargo.toml b/trifid-api/Cargo.toml
index ecbad68..385f5a1 100644
--- a/trifid-api/Cargo.toml
+++ b/trifid-api/Cargo.toml
@@ -14,4 +14,8 @@ tokio = { version = "1", features = ["full"] }
toml = "0.7.1"
serde = "1.0.152"
dotenvy = "0.15.6"
-paste = "1.0.11"
\ No newline at end of file
+paste = "1.0.11"
+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"
\ No newline at end of file
diff --git a/trifid-api/config.toml b/trifid-api/config.toml
index 5d08077..77a5c77 100644
--- a/trifid-api/config.toml
+++ b/trifid-api/config.toml
@@ -1,2 +1,4 @@
listen_port = 8000
-db_url = "postgres://postgres@localhost/trifidapi"
\ No newline at end of file
+db_url = "postgres://postgres@localhost/trifidapi"
+base = "http://localhost:8000"
+magic_links_valid_for = 86400
\ No newline at end of file
diff --git a/trifid-api/migrations/20230203003134_create_orgs.sql b/trifid-api/migrations/20230203003134_create_orgs.sql
deleted file mode 100644
index 9102aa6..0000000
--- a/trifid-api/migrations/20230203003134_create_orgs.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-CREATE TABLE organizations (
- id SERIAL NOT NULL PRIMARY KEY,
- network_range VARCHAR(18) NOT NULL, -- https://docs.defined.net/guides/choosing-a-cidr/
- org_name VARCHAR(258) NOT NULL
-);
-CREATE INDEX idx_organizations_name ON organizations(org_name);
\ No newline at end of file
diff --git a/trifid-api/migrations/20230203025121_create_users.sql b/trifid-api/migrations/20230203025121_create_users.sql
deleted file mode 100644
index d185325..0000000
--- a/trifid-api/migrations/20230203025121_create_users.sql
+++ /dev/null
@@ -1,10 +0,0 @@
-CREATE TABLE users (
- id SERIAL NOT NULL PRIMARY KEY,
- email VARCHAR(320) NOT NULL,
-
- totp_enabled INTEGER NOT NULL,
- totp_verified INTEGER NOT NULL,
- totp_base32 VARCHAR(1024) NOT NULL,
- totp_auth_url VARCHAR(1024) NOT NULL
-);
-CREATE INDEX idx_users_email ON users(email);
\ No newline at end of file
diff --git a/trifid-api/migrations/20230203025138_create_roles.sql b/trifid-api/migrations/20230203025138_create_roles.sql
deleted file mode 100644
index 904200b..0000000
--- a/trifid-api/migrations/20230203025138_create_roles.sql
+++ /dev/null
@@ -1,10 +0,0 @@
-CREATE TABLE roles (
- id SERIAL NOT NULL PRIMARY KEY,
-
- role_name VARCHAR(128) NOT NULL,
- role_desc VARCHAR(512) NOT NULL,
-
- org_id SERIAL NOT NULL REFERENCES organizations(id)
-);
-CREATE INDEX idx_roles_org_id ON roles(org_id);
--- every org should have an id=0 role which every node is a member of
\ No newline at end of file
diff --git a/trifid-api/migrations/20230203140924_create_firewall_rules.sql b/trifid-api/migrations/20230203140924_create_firewall_rules.sql
deleted file mode 100644
index 06da206..0000000
--- a/trifid-api/migrations/20230203140924_create_firewall_rules.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE TABLE firewall_rules (
- id SERIAL NOT NULL PRIMARY KEY,
-
- allow_role_id SERIAL NOT NULL REFERENCES roles(id), -- all roles: use the organization role itself
-
- protocol INTEGER NOT NULL, -- 0: any, 1: icmp, 2: udp, 3: tcp
- port VARCHAR(256) NOT NULL, -- port range (ignore if protocol==1)
- description VARCHAR(256) NOT NULL -- description
-)
\ No newline at end of file
diff --git a/trifid-api/migrations/20230204174853_create_users.sql b/trifid-api/migrations/20230204174853_create_users.sql
new file mode 100644
index 0000000..9b1ef88
--- /dev/null
+++ b/trifid-api/migrations/20230204174853_create_users.sql
@@ -0,0 +1,13 @@
+CREATE TABLE users (
+ id SERIAL NOT NULL PRIMARY KEY,
+ email VARCHAR(320) NOT NULL UNIQUE,
+ created_on INTEGER NOT NULL,
+
+ banned INTEGER NOT NULL,
+ ban_reason VARCHAR(1024) NOT NULL,
+
+ totp_secret VARCHAR(128) NOT NULL,
+ totp_verified INTEGER NOT NULL,
+ totp_otpurl VARCHAR(3000) NOT NULL
+);
+CREATE INDEX idx_users_email ON users(email);
\ No newline at end of file
diff --git a/trifid-api/migrations/20230204185754_magic_links.sql b/trifid-api/migrations/20230204185754_magic_links.sql
new file mode 100644
index 0000000..501f361
--- /dev/null
+++ b/trifid-api/migrations/20230204185754_magic_links.sql
@@ -0,0 +1,5 @@
+CREATE TABLE magic_links (
+ id VARCHAR(36) NOT NULL PRIMARY KEY UNIQUE,
+ user_id SERIAL NOT NULL REFERENCES users(id),
+ expires_on INTEGER NOT NULL
+);
\ No newline at end of file
diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs
index ce10511..6d0ccdc 100644
--- a/trifid-api/src/config.rs
+++ b/trifid-api/src/config.rs
@@ -1,7 +1,10 @@
use serde::Deserialize;
+use url::Url;
#[derive(Deserialize)]
pub struct TFConfig {
pub listen_port: u16,
- pub db_url: String
+ pub db_url: String,
+ pub base: Url,
+ pub magic_links_valid_for: i64
}
\ No newline at end of file
diff --git a/trifid-api/src/email.rs b/trifid-api/src/email.rs
new file mode 100644
index 0000000..ee9dd80
--- /dev/null
+++ b/trifid-api/src/email.rs
@@ -0,0 +1,14 @@
+use std::error::Error;
+use log::info;
+use sqlx::PgPool;
+use uuid::Uuid;
+use crate::config::TFConfig;
+// https://admin.defined.net/auth/magic-link?email=coredoescode%40gmail.com&token=ml-ckBsgw_5IdK5VYgseBYcoV_v_cQjtdq1re_RhDu_MKg
+pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFConfig) -> Result<(), Box> {
+ let otp = Uuid::new_v4().to_string();
+ let otp_url = format!("{}/{}", config.base, urlencoding::encode(&format!("/auth/magic-link?email={}&token={}", email.clone(), otp.clone())));
+ sqlx::query!("INSERT INTO magic_links (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", otp, id, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + config.config.magic_links_valid_for).await?;
+ // TODO: send email
+ info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for);
+ Ok(())
+}
\ No newline at end of file
diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs
index 92f02cb..ec5e74f 100644
--- a/trifid-api/src/main.rs
+++ b/trifid-api/src/main.rs
@@ -12,7 +12,7 @@ pub mod format;
pub mod util;
pub mod db;
pub mod config;
-
+pub mod email;
pub mod routes;
static MIGRATOR: Migrator = sqlx::migrate!();
@@ -75,29 +75,19 @@ async fn main() -> Result<(), Box> {
info!("[tfapi] building rocket config");
let figment = rocket::Config::figment().merge(("port", config.listen_port));
-/*
-error_handler!(400, "ERR_MALFORMED_REQUEST", "unable to parse the request body, is it properly formatted?");
-error_handler!(401, "ERR_AUTHENTICATION_REQUIRED", "this endpoint requires authentication but it was not provided");
-error_handler!(403, "ERR_UNAUTHORIZED", "authorization was provided but it is expired or invalid");
-error_handler!(404, "ERR_NOT_FOUND", "resource not found");
-error_handler!(405, "ERR_METHOD_NOT_ALLOWED", "method not allowed for this endpoint");
-error_handler!(500, "ERR_QL_QUERY_FAILED", "graphql query timed out");
-error_handler!(501, "ERR_NOT_IMPLEMENTED", "query not supported by this version of graphql");
-error_handler!(502, "ERR_PROXY_ERR", "servers under load, please try again later");
-error_handler!(503, "ERR_SERVER_OVERLOADED", "servers under load, please try again later");
-error_handler!(504, "ERR_PROXY_TIMEOUT", "servers under load, please try again later");
-error_handler!(505, "ERR_CLIENT_UNSUPPORTED", "your version of dnclient is out of date, please update");
- */
let _ = rocket::custom(figment)
.mount("/", routes![
- crate::routes::v1::auth::verify_magic_link::verify_magic_link
+ //crate::routes::v1::auth::verify_magic_link::verify_magic_link
+ crate::routes::v1::signup::signup_request
])
.register("/", catchers![
crate::routes::handler_400,
crate::routes::handler_401,
crate::routes::handler_403,
crate::routes::handler_404,
+ crate::routes::handler_422,
+
crate::routes::handler_500,
crate::routes::handler_501,
crate::routes::handler_502,
diff --git a/trifid-api/src/routes/mod.rs b/trifid-api/src/routes/mod.rs
index 383c6a6..ad95566 100644
--- a/trifid-api/src/routes/mod.rs
+++ b/trifid-api/src/routes/mod.rs
@@ -35,6 +35,8 @@ error_handler!(401, "ERR_AUTHENTICATION_REQUIRED", "this endpoint requires authe
error_handler!(403, "ERR_UNAUTHORIZED", "authorization was provided but it is expired or invalid");
error_handler!(404, "ERR_NOT_FOUND", "resource not found");
error_handler!(405, "ERR_METHOD_NOT_ALLOWED", "method not allowed for this endpoint");
+error_handler!(422, "ERR_MALFORMED_REQUEST", "unable to parse the request body, is it properly formatted?");
+
error_handler!(500, "ERR_QL_QUERY_FAILED", "graphql query timed out");
error_handler!(501, "ERR_NOT_IMPLEMENTED", "query not supported by this version of graphql");
error_handler!(502, "ERR_PROXY_ERR", "servers under load, please try again later");
diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs
index 5696e21..6ce15fd 100644
--- a/trifid-api/src/routes/v1/mod.rs
+++ b/trifid-api/src/routes/v1/mod.rs
@@ -1 +1,2 @@
-pub mod auth;
\ No newline at end of file
+//pub mod auth;
+pub mod signup;
\ No newline at end of file
diff --git a/trifid-api/src/routes/v1/signup.rs b/trifid-api/src/routes/v1/signup.rs
new file mode 100644
index 0000000..b2c6574
--- /dev/null
+++ b/trifid-api/src/routes/v1/signup.rs
@@ -0,0 +1,55 @@
+use rocket::{post, State};
+use rocket::serde::json::Json;
+use serde::{Serialize, Deserialize};
+use std::time::{SystemTime, UNIX_EPOCH};
+use rocket::http::{ContentType, Status};
+use sqlx::PgPool;
+use crate::config::TFConfig;
+use crate::email::send_magic_link;
+
+#[derive(Serialize, Deserialize)]
+#[serde(crate = "rocket::serde")]
+pub struct SignupRequest {
+ pub email: String,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(crate = "rocket::serde")]
+pub struct SignupResponseMetadata {}
+
+#[derive(Serialize, Deserialize)]
+#[serde(crate = "rocket::serde")]
+pub struct SignupResponse {
+ pub data: Option,
+ pub metadata: SignupResponseMetadata,
+}
+/*
+created_on TIMESTAMP NOT NULL,
+
+ banned INTEGER NOT NULL,
+ ban_reason VARCHAR(1024) NOT NULL
+ */
+#[post("/v1/signup", data = "")]
+pub async fn signup_request(req: Json, pool: &State, config: &State) -> Result<(ContentType, Json), (Status, String)> {
+ match sqlx::query!("INSERT INTO users (email, created_on, banned, ban_reason, totp_secret, totp_otpurl, totp_verified) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id ON CONFLICT DO NOTHING;", req.email, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, 0, "", "", "", 0).execute(pool.inner()).await {
+ Ok(_) => (),
+ Err(e) => {
+ return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
+ }
+ };
+
+ // send magic link to email
+ match send_magic_link(req.email.clone(), pool.inner(), config.inner()).await {
+ Ok(_) => (),
+ Err(e) => {
+ return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
+ }
+ };
+
+ // this endpoint doesn't actually ever return an error? it will send you the magic link no matter what
+ // this appears to do the exact same thing as /v1/auth/magic-link, but it doesn't check if you have an account (magic-link does)
+ Ok((ContentType::JSON, Json(SignupResponse {
+ data: None,
+ metadata: SignupResponseMetadata {},
+ })))
+}
\ No newline at end of file
diff --git a/trifid-api/src/util.rs b/trifid-api/src/util.rs
index 74b8d47..1d66dc6 100644
--- a/trifid-api/src/util.rs
+++ b/trifid-api/src/util.rs
@@ -1,4 +1,11 @@
use base64::Engine;
+use totp_rs::Algorithm;
+
+pub const TOTP_ALGORITHM: Algorithm = Algorithm::SHA1;
+pub const TOTP_DIGITS: usize = 6;
+pub const TOTP_SKEW: u8 = 1;
+pub const TOTP_STEP: u64 = 30;
+pub const TOTP_ISSUER: &'static str = "trifidapi";
pub fn base64decode(val: &str) -> Result, base64::DecodeError> {
base64::engine::general_purpose::STANDARD.decode(val)