From 68627b0c92cc05d4bcf069f099666548f5aee712 Mon Sep 17 00:00:00 2001 From: core Date: Mon, 20 Nov 2023 20:07:02 -0500 Subject: [PATCH] worktm --- trifid-api/build.rs | 2 +- .../down.sql | 1 + .../up.sql | 7 ++ trifid-api/src/auth.rs | 96 +++++++++++++++++++ trifid-api/src/config.rs | 16 ++-- trifid-api/src/email.rs | 23 +++-- trifid-api/src/error.rs | 6 +- trifid-api/src/id.rs | 17 +++- trifid-api/src/main.rs | 58 ++++++----- trifid-api/src/models.rs | 33 +++++-- trifid-api/src/response.rs | 39 ++++---- trifid-api/src/routes/mod.rs | 2 +- trifid-api/src/routes/v1/auth/magic_link.rs | 61 ++++++++---- trifid-api/src/routes/v1/auth/mod.rs | 2 +- .../src/routes/v1/auth/verify_magic_link.rs | 59 +++++++++--- trifid-api/src/routes/v1/mod.rs | 3 +- trifid-api/src/routes/v1/signup.rs | 47 ++++++--- .../src/routes/v1/totp_authenticators.rs | 43 +++++++++ trifid-api/src/schema.rs | 11 +++ 19 files changed, 398 insertions(+), 128 deletions(-) create mode 100644 trifid-api/migrations/2023-11-20-034741_create_totp_authenticators/down.sql create mode 100644 trifid-api/migrations/2023-11-20-034741_create_totp_authenticators/up.sql create mode 100644 trifid-api/src/auth.rs create mode 100644 trifid-api/src/routes/v1/totp_authenticators.rs diff --git a/trifid-api/build.rs b/trifid-api/build.rs index 8d90706..aefd807 100644 --- a/trifid-api/build.rs +++ b/trifid-api/build.rs @@ -1,3 +1,3 @@ fn main() { println!("cargo:rerun-if-changed=migrations") -} \ No newline at end of file +} diff --git a/trifid-api/migrations/2023-11-20-034741_create_totp_authenticators/down.sql b/trifid-api/migrations/2023-11-20-034741_create_totp_authenticators/down.sql new file mode 100644 index 0000000..bddd080 --- /dev/null +++ b/trifid-api/migrations/2023-11-20-034741_create_totp_authenticators/down.sql @@ -0,0 +1 @@ +DROP TABLE totp_authenticators; \ No newline at end of file diff --git a/trifid-api/migrations/2023-11-20-034741_create_totp_authenticators/up.sql b/trifid-api/migrations/2023-11-20-034741_create_totp_authenticators/up.sql new file mode 100644 index 0000000..aa7c7fd --- /dev/null +++ b/trifid-api/migrations/2023-11-20-034741_create_totp_authenticators/up.sql @@ -0,0 +1,7 @@ +CREATE TABLE totp_authenticators +( + id VARCHAR NOT NULL PRIMARY KEY, + user_id VARCHAR NOT NULL REFERENCES users(id), + secret VARCHAR NOT NULL, + verified BOOLEAN NOT NULL +); \ No newline at end of file diff --git a/trifid-api/src/auth.rs b/trifid-api/src/auth.rs new file mode 100644 index 0000000..3d650ee --- /dev/null +++ b/trifid-api/src/auth.rs @@ -0,0 +1,96 @@ +use crate::models::SessionToken; + +pub struct AuthInfo { + pub session_token: Option, + pub auth_token: Option<()>, +} + +#[macro_export] +macro_rules! auth { + ($i:expr,$c:expr) => {{ + let authorization_hdr_value = match $i.headers().get("Authorization") { + Some(hdr) => hdr, + None => $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ), + }; + let hdr_value_split = $crate::handle_error!(authorization_hdr_value.to_str()) + .split(' ') + .collect::>(); + if hdr_value_split.len() < 2 { + $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ) + } + let tokens = hdr_value_split[1..].to_vec(); + let mut auth_info = $crate::auth::AuthInfo { + session_token: None, + auth_token: None, + }; + for token in tokens { + if token.starts_with("sess-") { + // handle session token + + use $crate::schema::session_tokens::dsl::*; + + let tokens = $crate::handle_error!( + session_tokens + .filter(id.eq(token)) + .select($crate::models::SessionToken::as_select()) + .load(&mut $c) + .await + ); + let real_token = match tokens.get(0) { + Some(tok) => tok, + None => $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ), + }; + auth_info.session_token = Some(real_token.clone()); + } else if token.starts_with("auth-") { + // parse auth token + todo!() + } + } + auth_info + }}; +} + +#[macro_export] +macro_rules! enforce { + (sess $i:expr) => { + if $i.session_token.is_none() { + $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ) + } + }; + (auth $i:expr) => { + if $i.auth_token.is_none() { + $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ) + } + }; + (sess auth $i:expr) => { + if $i.session_token.is_none() || $i.auth_token.is_none() { + $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ) + } + }; + (auth sess $i:expr) => { + if $i.session_token.is_none() || $i.auth_token.is_none() { + $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ) + } + }; +} diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs index e9ed439..5dc89ce 100644 --- a/trifid-api/src/config.rs +++ b/trifid-api/src/config.rs @@ -1,29 +1,29 @@ -use std::net::IpAddr; use serde::Deserialize; +use std::net::IpAddr; #[derive(Deserialize, Clone)] pub struct Config { pub server: ConfigServer, pub database: ConfigDatabase, pub email: ConfigEmail, - pub tokens: ConfigTokens + pub tokens: ConfigTokens, } #[derive(Deserialize, Clone)] pub struct ConfigServer { pub bind: ConfigServerBind, - pub workers: Option + pub workers: Option, } #[derive(Deserialize, Clone)] pub struct ConfigServerBind { pub ip: IpAddr, - pub port: u16 + pub port: u16, } #[derive(Deserialize, Clone)] pub struct ConfigDatabase { - pub url: String + pub url: String, } #[derive(Deserialize, Clone)] @@ -35,11 +35,11 @@ pub struct ConfigEmail { pub from_name: String, pub from_email: String, pub template: String, - pub starttls: bool + pub starttls: bool, } #[derive(Deserialize, Clone)] pub struct ConfigTokens { pub magic_link_expiry_seconds: u64, - pub session_token_expiry_seconds: u64 -} \ No newline at end of file + pub session_token_expiry_seconds: u64, +} diff --git a/trifid-api/src/email.rs b/trifid-api/src/email.rs index 3ca4212..e840aff 100644 --- a/trifid-api/src/email.rs +++ b/trifid-api/src/email.rs @@ -1,11 +1,18 @@ -use std::error::Error; +use crate::config::Config; use mail_send::mail_builder::MessageBuilder; use mail_send::SmtpClientBuilder; -use crate::config::Config; +use std::error::Error; -pub async fn send_email(token: &str, to_address: &str, config: &Config) -> Result<(), Box> { +pub async fn send_email( + token: &str, + to_address: &str, + config: &Config, +) -> Result<(), Box> { let message = MessageBuilder::new() - .from((config.email.from_name.as_str(), config.email.from_email.as_str())) + .from(( + config.email.from_name.as_str(), + config.email.from_email.as_str(), + )) .to(vec![to_address]) .subject("Trifid - Log In") .text_body(config.email.template.replace("%TOKEN%", token)); @@ -18,8 +25,10 @@ pub async fn send_email(token: &str, to_address: &str, config: &Config) -> Resul SmtpClientBuilder::new(config.email.server.as_str(), config.email.port) .implicit_tls(!config.email.starttls) .credentials((config.email.username.as_str(), password.as_str())) - .connect().await? - .send(message).await?; + .connect() + .await? + .send(message) + .await?; Ok(()) -} \ No newline at end of file +} diff --git a/trifid-api/src/error.rs b/trifid-api/src/error.rs index c1f9cf6..b255659 100644 --- a/trifid-api/src/error.rs +++ b/trifid-api/src/error.rs @@ -1,12 +1,12 @@ -use std::fmt::{Display, Formatter}; use actix_web::error::{JsonPayloadError, PayloadError}; use serde::Serialize; +use std::fmt::{Display, Formatter}; #[derive(Serialize, Debug)] pub struct APIErrorResponse { pub code: String, pub message: String, - pub path: Option + pub path: Option, } impl Display for APIErrorResponse { @@ -110,4 +110,4 @@ impl From<&PayloadError> for APIErrorResponse { }, } } -} \ No newline at end of file +} diff --git a/trifid-api/src/id.rs b/trifid-api/src/id.rs index 85280c2..13fc147 100644 --- a/trifid-api/src/id.rs +++ b/trifid-api/src/id.rs @@ -2,7 +2,8 @@ use rand::Rng; pub const ID_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; pub const ID_RAND_LEN: u32 = 26; -pub const TOKEN_CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; +pub const TOKEN_CHARSET: &[u8] = + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; pub const TOKEN_RAND_LEN: u32 = 43; #[macro_export] @@ -11,13 +12,21 @@ macro_rules! randid { $crate::id::random_with_charset($crate::id::ID_RAND_LEN, $crate::id::ID_CHARSET) }; (id $p:expr) => { - format!("{}-{}", $p, $crate::id::random_with_charset($crate::id::ID_RAND_LEN, $crate::id::ID_CHARSET)) + format!( + "{}-{}", + $p, + $crate::id::random_with_charset($crate::id::ID_RAND_LEN, $crate::id::ID_CHARSET) + ) }; (token) => { random_with_charset($crate::id::TOKEN_RAND_LEN, $crate::id::TOKEN_CHARSET) }; (token $p:expr) => { - format!("{}-{}", $p, $crate::id::random_with_charset($crate::id::TOKEN_RAND_LEN, $crate::id::TOKEN_CHARSET)) + format!( + "{}-{}", + $p, + $crate::id::random_with_charset($crate::id::TOKEN_RAND_LEN, $crate::id::TOKEN_CHARSET) + ) }; } @@ -28,4 +37,4 @@ pub fn random_with_charset(len: u32, charset: &[u8]) -> String { charset[idx] as char }) .collect() -} \ No newline at end of file +} diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index fb3bf3c..47df947 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -1,34 +1,34 @@ -use std::fs; -use std::path::PathBuf; -use actix_web::{App, Error, HttpResponse, HttpServer}; -use actix_web::middleware::Logger; -use actix_web::web::{Data, JsonConfig}; -use diesel::Connection; -use diesel_async::async_connection_wrapper::AsyncConnectionWrapper; -use diesel_async::AsyncPgConnection; -use diesel_async::pooled_connection::AsyncDieselConnectionManager; -use diesel_async::pooled_connection::bb8::Pool; -use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use log::{error, info}; use crate::config::Config; use crate::error::APIErrorResponse; +use actix_web::middleware::Logger; +use actix_web::web::{Data, JsonConfig}; +use actix_web::{App, Error, HttpResponse, HttpServer}; +use diesel::Connection; +use diesel_async::async_connection_wrapper::AsyncConnectionWrapper; +use diesel_async::pooled_connection::bb8::Pool; +use diesel_async::pooled_connection::AsyncDieselConnectionManager; +use diesel_async::AsyncPgConnection; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use log::{error, info}; +use std::fs; +use std::path::PathBuf; pub mod error; #[macro_use] pub mod response; pub mod config; +pub mod models; pub mod routes; pub mod schema; -pub mod models; #[macro_use] pub mod id; +pub mod auth; pub mod email; - #[derive(Clone)] pub struct AppState { pub config: Config, - pub pool: bb8::Pool> + pub pool: bb8::Pool>, } pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); @@ -53,7 +53,11 @@ async fn main() { let config_str = match fs::read_to_string(&config_pathbuf) { Ok(c_str) => c_str, Err(e) => { - error!("Error loading configuration from {}: {}", config_pathbuf.display(), e); + error!( + "Error loading configuration from {}: {}", + config_pathbuf.display(), + e + ); std::process::exit(1); } }; @@ -61,7 +65,11 @@ async fn main() { let config: Config = match toml::from_str(&config_str) { Ok(config) => config, Err(e) => { - error!("Error parsing configuration in {}: {}", config_pathbuf.display(), e); + error!( + "Error parsing configuration in {}: {}", + config_pathbuf.display(), + e + ); std::process::exit(1); } }; @@ -82,7 +90,8 @@ async fn main() { let local_config = config.clone(); let db_url = config.database.url.clone(); - match actix_web::rt::task::spawn_blocking(move || { // Lock block + match actix_web::rt::task::spawn_blocking(move || { + // Lock block let mut conn = match AsyncConnectionWrapper::::establish(&db_url) { Ok(conn) => conn, Err(e) => { @@ -98,7 +107,9 @@ async fn main() { std::process::exit(1); } } - }).await { + }) + .await + { Ok(_) => (), Err(e) => { error!("Error waiting for migrations: {}", e); @@ -106,10 +117,7 @@ async fn main() { } } - let app_state = Data::new(AppState { - config, - pool - }); + let app_state = Data::new(AppState { config, pool }); let server = HttpServer::new(move || { App::new() @@ -128,7 +136,9 @@ async fn main() { .wrap(Logger::default()) .wrap(actix_cors::Cors::permissive()) .app_data(app_state.clone()) - }).bind((local_config.server.bind.ip, local_config.server.bind.port)).unwrap(); + }) + .bind((local_config.server.bind.ip, local_config.server.bind.port)) + .unwrap(); server.run().await.unwrap(); diff --git a/trifid-api/src/models.rs b/trifid-api/src/models.rs index 3bced51..edd45b5 100644 --- a/trifid-api/src/models.rs +++ b/trifid-api/src/models.rs @@ -1,30 +1,47 @@ -use std::time::SystemTime; use diesel::{Associations, Identifiable, Insertable, Queryable, Selectable}; +use std::time::SystemTime; -#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, PartialEq)] +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, PartialEq, Clone)] #[diesel(table_name = crate::schema::users)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct User { pub id: String, - pub email: String + pub email: String, } -#[derive(Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq)] +#[derive( + Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, +)] #[diesel(belongs_to(User))] #[diesel(table_name = crate::schema::magic_links)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct MagicLink { pub id: String, pub user_id: String, - pub expires: SystemTime + pub expires: SystemTime, } -#[derive(Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq)] +#[derive( + Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, +)] #[diesel(belongs_to(User))] #[diesel(table_name = crate::schema::session_tokens)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct SessionToken { pub id: String, pub user_id: String, - pub expires: SystemTime -} \ No newline at end of file + pub expires: SystemTime, +} + +#[derive( + Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, +)] +#[diesel(belongs_to(User))] +#[diesel(table_name = crate::schema::totp_authenticators)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct TotpAuthenticator { + pub id: String, + pub user_id: String, + pub secret: String, + pub verified: bool, +} diff --git a/trifid-api/src/response.rs b/trifid-api/src/response.rs index f1ef24e..858024d 100644 --- a/trifid-api/src/response.rs +++ b/trifid-api/src/response.rs @@ -1,15 +1,15 @@ -use std::fmt::{Debug, Display, Formatter}; +use crate::error::APIErrorResponse; use actix_web::body::BoxBody; +use actix_web::error::JsonPayloadError; use actix_web::http::StatusCode; use actix_web::{HttpRequest, HttpResponse, Responder}; -use actix_web::error::JsonPayloadError; use serde::Serialize; -use crate::error::APIErrorResponse; +use std::fmt::{Debug, Display, Formatter}; #[derive(Debug)] pub enum JsonAPIResponse { Error(StatusCode, APIErrorResponse), - Success(StatusCode, T) + Success(StatusCode, T), } impl Display for JsonAPIResponse { @@ -23,18 +23,14 @@ impl Responder for JsonAPIResponse { fn respond_to(self, _: &HttpRequest) -> HttpResponse { match self { - JsonAPIResponse::Error(c, r) => { - match serde_json::to_string(&r) { - Ok(body) => HttpResponse::build(c).body(body), - Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)) - } - } - JsonAPIResponse::Success(c, b) => { - match serde_json::to_string(&b) { - Ok(body) => HttpResponse::build(c).body(body), - Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)) - } - } + JsonAPIResponse::Error(c, r) => match serde_json::to_string(&r) { + Ok(body) => HttpResponse::build(c).body(body), + Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), + }, + JsonAPIResponse::Success(c, b) => match serde_json::to_string(&b) { + Ok(body) => HttpResponse::build(c).body(body), + Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), + }, } } } @@ -60,7 +56,10 @@ macro_rules! ok { macro_rules! internal_error { ($e:expr) => {{ log::error!("internal error: {}", $e); - $crate::err!(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, $crate::make_err!("ERR_INTERNAL_ERROR", $e)); + $crate::err!( + actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, + $crate::make_err!("ERR_INTERNAL_ERROR", $e) + ); }}; } @@ -91,14 +90,14 @@ macro_rules! make_err { $crate::error::APIErrorResponse { code: $c.to_string(), message: $m.to_string(), - path: Some($p.to_string()) + path: Some($p.to_string()), } }; ($c:expr,$m:expr) => { $crate::error::APIErrorResponse { code: $c.to_string(), message: $m.to_string(), - path: None + path: None, } }; -} \ No newline at end of file +} diff --git a/trifid-api/src/routes/mod.rs b/trifid-api/src/routes/mod.rs index 5dd9fd0..a3a6d96 100644 --- a/trifid-api/src/routes/mod.rs +++ b/trifid-api/src/routes/mod.rs @@ -1 +1 @@ -pub mod v1; \ No newline at end of file +pub mod v1; diff --git a/trifid-api/src/routes/v1/auth/magic_link.rs b/trifid-api/src/routes/v1/auth/magic_link.rs index a1c213e..50d3800 100644 --- a/trifid-api/src/routes/v1/auth/magic_link.rs +++ b/trifid-api/src/routes/v1/auth/magic_link.rs @@ -1,55 +1,76 @@ -use std::time::{Duration, SystemTime}; -use actix_web::post; -use actix_web::http::StatusCode; -use actix_web::web::{Data, Json}; -use serde::{Deserialize, Serialize}; -use crate::{AppState, randid}; +use crate::email::send_email; use crate::models::{MagicLink, User}; use crate::response::JsonAPIResponse; -use diesel::QueryDsl; -use diesel_async::RunQueryDsl; -use crate::email::send_email; -use diesel::SelectableHelper; -use crate::schema::users; use crate::schema::magic_links; +use crate::schema::users; +use crate::{randid, AppState}; +use actix_web::http::StatusCode; +use actix_web::post; +use actix_web::web::{Data, Json}; use diesel::ExpressionMethods; +use diesel::QueryDsl; +use diesel::SelectableHelper; +use diesel_async::RunQueryDsl; +use serde::{Deserialize, Serialize}; +use std::time::{Duration, SystemTime}; #[derive(Deserialize)] pub struct LoginRequest { - pub email: String + pub email: String, } #[derive(Serialize, Debug)] pub struct LoginResponse { pub data: Option<()>, - pub metadata: LoginResponseMetadata + pub metadata: LoginResponseMetadata, } #[derive(Serialize, Debug)] pub struct LoginResponseMetadata {} #[post("/v1/auth/magic-link")] -pub async fn login_req(req: Json, state: Data) -> JsonAPIResponse { +pub async fn login_req( + req: Json, + state: Data, +) -> JsonAPIResponse { let mut conn = handle_error!(state.pool.get().await); - let user_vec = handle_error!(users::dsl::users.filter(users::dsl::email.eq(&req.email)).select(User::as_select()).load(&mut conn).await); + let user_vec = handle_error!( + users::dsl::users + .filter(users::dsl::email.eq(&req.email)) + .select(User::as_select()) + .load(&mut conn) + .await + ); // Difference from DN functionality: Trifid API will not implicitly create accounts let user = match user_vec.get(0) { Some(user) => user, None => { - err!(StatusCode::BAD_REQUEST, make_err!("ERR_INVALID_EMAIL", "does not exist", "email")) + err!( + StatusCode::BAD_REQUEST, + make_err!("ERR_INVALID_EMAIL", "does not exist", "email") + ) } }; let new_magic_link = MagicLink { id: randid!(token "ml"), user_id: user.id.clone(), - expires: SystemTime::now() + Duration::from_secs(state.config.tokens.magic_link_expiry_seconds) + expires: SystemTime::now() + + Duration::from_secs(state.config.tokens.magic_link_expiry_seconds), }; - handle_error!(diesel::insert_into(magic_links::table).values(&new_magic_link).execute(&mut conn).await); + handle_error!( + diesel::insert_into(magic_links::table) + .values(&new_magic_link) + .execute(&mut conn) + .await + ); handle_error!(send_email(&new_magic_link.id, &req.email, &state.config).await); - ok!(LoginResponse { data: None, metadata: LoginResponseMetadata {} }) -} \ No newline at end of file + ok!(LoginResponse { + data: None, + metadata: LoginResponseMetadata {} + }) +} diff --git a/trifid-api/src/routes/v1/auth/mod.rs b/trifid-api/src/routes/v1/auth/mod.rs index aa16fd9..57e96d5 100644 --- a/trifid-api/src/routes/v1/auth/mod.rs +++ b/trifid-api/src/routes/v1/auth/mod.rs @@ -1,2 +1,2 @@ +pub mod magic_link; pub mod verify_magic_link; -pub mod magic_link; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/auth/verify_magic_link.rs b/trifid-api/src/routes/v1/auth/verify_magic_link.rs index 881e456..cf6f2aa 100644 --- a/trifid-api/src/routes/v1/auth/verify_magic_link.rs +++ b/trifid-api/src/routes/v1/auth/verify_magic_link.rs @@ -1,14 +1,14 @@ -use std::time::{Duration, SystemTime}; +use crate::models::{MagicLink, SessionToken}; +use crate::response::JsonAPIResponse; +use crate::schema::session_tokens; +use crate::{randid, AppState}; use actix_web::http::StatusCode; use actix_web::post; use actix_web::web::{Data, Json}; use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; -use serde::{Deserialize, Serialize}; -use crate::{AppState, randid}; -use crate::models::{MagicLink, SessionToken}; -use crate::response::JsonAPIResponse; use diesel_async::RunQueryDsl; -use crate::schema::session_tokens; +use serde::{Deserialize, Serialize}; +use std::time::{Duration, SystemTime}; #[derive(Deserialize)] pub struct VerifyLinkReq { @@ -19,33 +19,56 @@ pub struct VerifyLinkReq { #[derive(Serialize, Debug)] pub struct VerifyLinkResp { pub data: VerifyLinkRespData, - pub metadata: VerifyLinkRespMetadata + pub metadata: VerifyLinkRespMetadata, } #[derive(Serialize, Debug)] pub struct VerifyLinkRespData { #[serde(rename = "sessionToken")] - pub session_token: String + pub session_token: String, } #[derive(Serialize, Debug)] pub struct VerifyLinkRespMetadata {} #[post("/v1/auth/verify-magic-link")] -pub async fn verify_link_req(req: Json, state: Data) -> JsonAPIResponse { +pub async fn verify_link_req( + req: Json, + state: Data, +) -> JsonAPIResponse { use crate::schema::magic_links::dsl::*; let mut conn = handle_error!(state.pool.get().await); - let tokens = handle_error!(magic_links.filter(id.eq(&req.magic_link_token)).select(MagicLink::as_select()).load(&mut conn).await); + let tokens = handle_error!( + magic_links + .filter(id.eq(&req.magic_link_token)) + .select(MagicLink::as_select()) + .load(&mut conn) + .await + ); let token = match tokens.get(0) { Some(token) => token, None => { - err!(StatusCode::BAD_REQUEST, make_err!("ERR_INVALID_MAGIC_LINK_TOKEN", "does not exist (maybe it expired?)", "magicLinkToken")) + err!( + StatusCode::BAD_REQUEST, + make_err!( + "ERR_INVALID_MAGIC_LINK_TOKEN", + "does not exist (maybe it expired?)", + "magicLinkToken" + ) + ) } }; if token.expires < SystemTime::now() { - err!(StatusCode::BAD_REQUEST, make_err!("ERR_INVALID_MAGIC_LINK_TOKEN", "does not exist (maybe it expired?)", "magicLinkToken")) + err!( + StatusCode::BAD_REQUEST, + make_err!( + "ERR_INVALID_MAGIC_LINK_TOKEN", + "does not exist (maybe it expired?)", + "magicLinkToken" + ) + ) } handle_error!(diesel::delete(token).execute(&mut conn).await); @@ -53,10 +76,16 @@ pub async fn verify_link_req(req: Json, state: Data) -> let new_token = SessionToken { id: randid!(token "sess"), user_id: token.user_id.clone(), - expires: SystemTime::now() + Duration::from_secs(state.config.tokens.session_token_expiry_seconds), + expires: SystemTime::now() + + Duration::from_secs(state.config.tokens.session_token_expiry_seconds), }; - handle_error!(diesel::insert_into(session_tokens::table).values(&new_token).execute(&mut conn).await); + handle_error!( + diesel::insert_into(session_tokens::table) + .values(&new_token) + .execute(&mut conn) + .await + ); ok!(VerifyLinkResp { data: VerifyLinkRespData { @@ -64,4 +93,4 @@ pub async fn verify_link_req(req: Json, state: Data) -> }, metadata: VerifyLinkRespMetadata {} }) -} \ No newline at end of file +} diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs index 824934f..40ce087 100644 --- a/trifid-api/src/routes/v1/mod.rs +++ b/trifid-api/src/routes/v1/mod.rs @@ -1,2 +1,3 @@ +pub mod auth; pub mod signup; -pub mod auth; \ No newline at end of file +pub mod totp_authenticators; diff --git a/trifid-api/src/routes/v1/signup.rs b/trifid-api/src/routes/v1/signup.rs index 5a6d46b..c1186c6 100644 --- a/trifid-api/src/routes/v1/signup.rs +++ b/trifid-api/src/routes/v1/signup.rs @@ -1,29 +1,32 @@ -use std::time::{Duration, SystemTime}; -use actix_web::post; -use actix_web::web::{Data, Json}; -use serde::{Deserialize, Serialize}; -use crate::{AppState, randid}; +use crate::email::send_email; use crate::models::{MagicLink, User}; use crate::response::JsonAPIResponse; -use crate::schema::{users, magic_links}; +use crate::schema::{magic_links, users}; +use crate::{randid, AppState}; +use actix_web::post; +use actix_web::web::{Data, Json}; use diesel_async::RunQueryDsl; -use crate::email::send_email; +use serde::{Deserialize, Serialize}; +use std::time::{Duration, SystemTime}; #[derive(Deserialize)] pub struct SignupRequest { - pub email: String + pub email: String, } #[derive(Serialize, Debug)] pub struct SignupResponse { pub data: Option<()>, - pub metadata: SignupResponseMetadata + pub metadata: SignupResponseMetadata, } #[derive(Serialize, Debug)] pub struct SignupResponseMetadata {} #[post("/v1/signup")] -pub async fn signup_req(req: Json, state: Data) -> JsonAPIResponse { +pub async fn signup_req( + req: Json, + state: Data, +) -> JsonAPIResponse { let mut conn = handle_error!(state.pool.get().await); let user_id = randid!(id "user"); @@ -33,16 +36,30 @@ pub async fn signup_req(req: Json, state: Data) -> Json email: req.email.clone(), }; - handle_error!(diesel::insert_into(users::table).values(&new_user).execute(&mut conn).await); + handle_error!( + diesel::insert_into(users::table) + .values(&new_user) + .execute(&mut conn) + .await + ); let new_magic_link = MagicLink { id: randid!(token "ml"), user_id, - expires: SystemTime::now() + Duration::from_secs(state.config.tokens.magic_link_expiry_seconds) + expires: SystemTime::now() + + Duration::from_secs(state.config.tokens.magic_link_expiry_seconds), }; - handle_error!(diesel::insert_into(magic_links::table).values(&new_magic_link).execute(&mut conn).await); + handle_error!( + diesel::insert_into(magic_links::table) + .values(&new_magic_link) + .execute(&mut conn) + .await + ); handle_error!(send_email(&new_magic_link.id, &req.email, &state.config).await); - ok!(SignupResponse { data: None, metadata: SignupResponseMetadata {} }) -} \ No newline at end of file + ok!(SignupResponse { + data: None, + metadata: SignupResponseMetadata {} + }) +} diff --git a/trifid-api/src/routes/v1/totp_authenticators.rs b/trifid-api/src/routes/v1/totp_authenticators.rs new file mode 100644 index 0000000..070e1b6 --- /dev/null +++ b/trifid-api/src/routes/v1/totp_authenticators.rs @@ -0,0 +1,43 @@ +use crate::response::JsonAPIResponse; +use crate::{auth, enforce, AppState}; +use actix_web::web::{Data, Json}; +use actix_web::{post, HttpRequest}; +use diesel::ExpressionMethods; +use diesel::QueryDsl; +use diesel::SelectableHelper; +use diesel_async::RunQueryDsl; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct TotpAuthenticatorReq {} + +#[derive(Serialize, Debug)] +pub struct TotpAuthRespMeta {} + +#[derive(Serialize, Debug)] +pub struct TotpAuthRespData { + #[serde(rename = "totpToken")] + pub totp_token: String, + pub secret: String, + pub url: String, +} + +#[derive(Serialize, Debug)] +pub struct TotpAuthResp { + pub data: TotpAuthRespData, + pub metadata: TotpAuthRespMeta, +} + +#[post("/v1/auth/totp-authenticators")] +pub async fn totp_auth_req( + req: Json, + state: Data, + req_info: HttpRequest, +) -> JsonAPIResponse { + let mut conn = handle_error!(state.pool.get().await); + + let auth_info = auth!(req_info, conn); + enforce!(sess auth_info); + + todo!() +} diff --git a/trifid-api/src/schema.rs b/trifid-api/src/schema.rs index 6436c1b..f308eaa 100644 --- a/trifid-api/src/schema.rs +++ b/trifid-api/src/schema.rs @@ -16,6 +16,15 @@ diesel::table! { } } +diesel::table! { + totp_authenticators (id) { + id -> Varchar, + user_id -> Varchar, + secret -> Varchar, + verified -> Bool, + } +} + diesel::table! { users (id) { id -> Varchar, @@ -25,9 +34,11 @@ diesel::table! { diesel::joinable!(magic_links -> users (user_id)); diesel::joinable!(session_tokens -> users (user_id)); +diesel::joinable!(totp_authenticators -> users (user_id)); diesel::allow_tables_to_appear_in_same_query!( magic_links, session_tokens, + totp_authenticators, users, );