some unfinished database work - /v1/signup works now

This commit is contained in:
core 2023-02-04 18:44:51 -05:00
parent a6ea23c32d
commit c3990486b8
Signed by: core
GPG Key ID: FDBF740DADDCEECF
18 changed files with 312 additions and 55 deletions

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/trifid-api/migrations/20230203025121_create_users.sql" dialect="GenericSQL" />
<file url="PROJECT" dialect="PostgreSQL" /> <file url="PROJECT" dialect="PostgreSQL" />
</component> </component>
</project> </project>

163
Cargo.lock generated
View File

@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "aead" name = "aead"
version = "0.5.1" version = "0.5.1"
@ -115,6 +121,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 = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base32"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.1" version = "0.13.1"
@ -160,6 +172,12 @@ version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "bytemuck"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.4.3" version = "1.4.3"
@ -194,6 +212,18 @@ dependencies = [
"inout", "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]] [[package]]
name = "cookie" name = "cookie"
version = "0.16.2" version = "0.16.2"
@ -252,6 +282,15 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" 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]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.8" version = "0.3.8"
@ -405,6 +444,16 @@ dependencies = [
"version_check", "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]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -717,6 +766,20 @@ dependencies = [
"unicode-normalization", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.2" version = "1.9.2"
@ -858,6 +921,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 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]] [[package]]
name = "mio" name = "mio"
version = "0.8.5" version = "0.8.5"
@ -937,6 +1009,27 @@ dependencies = [
"winapi", "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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.15" version = "0.2.15"
@ -1120,6 +1213,18 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 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]] [[package]]
name = "polyval" name = "polyval"
version = "0.6.0" version = "0.6.0"
@ -1160,6 +1265,12 @@ dependencies = [
"yansi", "yansi",
] ]
[[package]]
name = "qrcodegen"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.23" version = "1.0.23"
@ -1866,6 +1977,25 @@ dependencies = [
"toml_datetime", "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]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.2"
@ -1947,6 +2077,10 @@ dependencies = [
"sqlx", "sqlx",
"tokio", "tokio",
"toml 0.7.1", "toml 0.7.1",
"totp-rs",
"url",
"urlencoding",
"uuid",
] ]
[[package]] [[package]]
@ -2038,6 +2172,35 @@ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
"percent-encoding", "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]] [[package]]

View File

@ -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":{}}

View File

@ -14,4 +14,8 @@ tokio = { version = "1", features = ["full"] }
toml = "0.7.1" toml = "0.7.1"
serde = "1.0.152" serde = "1.0.152"
dotenvy = "0.15.6" dotenvy = "0.15.6"
paste = "1.0.11" 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"

View File

@ -1,2 +1,4 @@
listen_port = 8000 listen_port = 8000
db_url = "postgres://postgres@localhost/trifidapi" db_url = "postgres://postgres@localhost/trifidapi"
base = "http://localhost:8000"
magic_links_valid_for = 86400

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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
)

View File

@ -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);

View File

@ -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
);

View File

@ -1,7 +1,10 @@
use serde::Deserialize; use serde::Deserialize;
use url::Url;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct TFConfig { pub struct TFConfig {
pub listen_port: u16, pub listen_port: u16,
pub db_url: String pub db_url: String,
pub base: Url,
pub magic_links_valid_for: i64
} }

14
trifid-api/src/email.rs Normal file
View File

@ -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<dyn Error>> {
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(())
}

View File

@ -12,7 +12,7 @@ pub mod format;
pub mod util; pub mod util;
pub mod db; pub mod db;
pub mod config; pub mod config;
pub mod email;
pub mod routes; pub mod routes;
static MIGRATOR: Migrator = sqlx::migrate!(); static MIGRATOR: Migrator = sqlx::migrate!();
@ -75,29 +75,19 @@ async fn main() -> Result<(), Box<dyn Error>> {
info!("[tfapi] building rocket config"); info!("[tfapi] building rocket config");
let figment = rocket::Config::figment().merge(("port", config.listen_port)); 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) let _ = rocket::custom(figment)
.mount("/", routes![ .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![ .register("/", catchers![
crate::routes::handler_400, crate::routes::handler_400,
crate::routes::handler_401, crate::routes::handler_401,
crate::routes::handler_403, crate::routes::handler_403,
crate::routes::handler_404, crate::routes::handler_404,
crate::routes::handler_422,
crate::routes::handler_500, crate::routes::handler_500,
crate::routes::handler_501, crate::routes::handler_501,
crate::routes::handler_502, crate::routes::handler_502,

View File

@ -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!(403, "ERR_UNAUTHORIZED", "authorization was provided but it is expired or invalid");
error_handler!(404, "ERR_NOT_FOUND", "resource not found"); error_handler!(404, "ERR_NOT_FOUND", "resource not found");
error_handler!(405, "ERR_METHOD_NOT_ALLOWED", "method not allowed for this endpoint"); 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!(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!(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!(502, "ERR_PROXY_ERR", "servers under load, please try again later");

View File

@ -1 +1,2 @@
pub mod auth; //pub mod auth;
pub mod signup;

View File

@ -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<String>,
pub metadata: SignupResponseMetadata,
}
/*
created_on TIMESTAMP NOT NULL,
banned INTEGER NOT NULL,
ban_reason VARCHAR(1024) NOT NULL
*/
#[post("/v1/signup", data = "<req>")]
pub async fn signup_request(req: Json<SignupRequest>, pool: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<SignupResponse>), (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 {},
})))
}

View File

@ -1,4 +1,11 @@
use base64::Engine; 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<Vec<u8>, base64::DecodeError> { pub fn base64decode(val: &str) -> Result<Vec<u8>, base64::DecodeError> {
base64::engine::general_purpose::STANDARD.decode(val) base64::engine::general_purpose::STANDARD.decode(val)