worktm
/ build (push) Successful in 46s Details
/ build_x64 (push) Successful in 2m26s Details
/ build_arm64 (push) Successful in 2m36s Details
/ build_win64 (push) Successful in 2m34s Details

This commit is contained in:
core 2023-11-20 20:07:02 -05:00
parent e7e49f9255
commit 68627b0c92
Signed by: core
GPG Key ID: FDBF740DADDCEECF
19 changed files with 398 additions and 128 deletions

View File

@ -1,3 +1,3 @@
fn main() { fn main() {
println!("cargo:rerun-if-changed=migrations") println!("cargo:rerun-if-changed=migrations")
} }

View File

@ -0,0 +1 @@
DROP TABLE totp_authenticators;

View File

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

96
trifid-api/src/auth.rs Normal file
View File

@ -0,0 +1,96 @@
use crate::models::SessionToken;
pub struct AuthInfo {
pub session_token: Option<SessionToken>,
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::<Vec<_>>();
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")
)
}
};
}

View File

@ -1,29 +1,29 @@
use std::net::IpAddr;
use serde::Deserialize; use serde::Deserialize;
use std::net::IpAddr;
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct Config { pub struct Config {
pub server: ConfigServer, pub server: ConfigServer,
pub database: ConfigDatabase, pub database: ConfigDatabase,
pub email: ConfigEmail, pub email: ConfigEmail,
pub tokens: ConfigTokens pub tokens: ConfigTokens,
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct ConfigServer { pub struct ConfigServer {
pub bind: ConfigServerBind, pub bind: ConfigServerBind,
pub workers: Option<usize> pub workers: Option<usize>,
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct ConfigServerBind { pub struct ConfigServerBind {
pub ip: IpAddr, pub ip: IpAddr,
pub port: u16 pub port: u16,
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct ConfigDatabase { pub struct ConfigDatabase {
pub url: String pub url: String,
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
@ -35,11 +35,11 @@ pub struct ConfigEmail {
pub from_name: String, pub from_name: String,
pub from_email: String, pub from_email: String,
pub template: String, pub template: String,
pub starttls: bool pub starttls: bool,
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct ConfigTokens { pub struct ConfigTokens {
pub magic_link_expiry_seconds: u64, pub magic_link_expiry_seconds: u64,
pub session_token_expiry_seconds: u64 pub session_token_expiry_seconds: u64,
} }

View File

@ -1,11 +1,18 @@
use std::error::Error; use crate::config::Config;
use mail_send::mail_builder::MessageBuilder; use mail_send::mail_builder::MessageBuilder;
use mail_send::SmtpClientBuilder; 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<dyn Error>> { pub async fn send_email(
token: &str,
to_address: &str,
config: &Config,
) -> Result<(), Box<dyn Error>> {
let message = MessageBuilder::new() 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]) .to(vec![to_address])
.subject("Trifid - Log In") .subject("Trifid - Log In")
.text_body(config.email.template.replace("%TOKEN%", token)); .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) SmtpClientBuilder::new(config.email.server.as_str(), config.email.port)
.implicit_tls(!config.email.starttls) .implicit_tls(!config.email.starttls)
.credentials((config.email.username.as_str(), password.as_str())) .credentials((config.email.username.as_str(), password.as_str()))
.connect().await? .connect()
.send(message).await?; .await?
.send(message)
.await?;
Ok(()) Ok(())
} }

View File

@ -1,12 +1,12 @@
use std::fmt::{Display, Formatter};
use actix_web::error::{JsonPayloadError, PayloadError}; use actix_web::error::{JsonPayloadError, PayloadError};
use serde::Serialize; use serde::Serialize;
use std::fmt::{Display, Formatter};
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct APIErrorResponse { pub struct APIErrorResponse {
pub code: String, pub code: String,
pub message: String, pub message: String,
pub path: Option<String> pub path: Option<String>,
} }
impl Display for APIErrorResponse { impl Display for APIErrorResponse {
@ -110,4 +110,4 @@ impl From<&PayloadError> for APIErrorResponse {
}, },
} }
} }
} }

View File

@ -2,7 +2,8 @@ use rand::Rng;
pub const ID_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; pub const ID_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
pub const ID_RAND_LEN: u32 = 26; 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; pub const TOKEN_RAND_LEN: u32 = 43;
#[macro_export] #[macro_export]
@ -11,13 +12,21 @@ macro_rules! randid {
$crate::id::random_with_charset($crate::id::ID_RAND_LEN, $crate::id::ID_CHARSET) $crate::id::random_with_charset($crate::id::ID_RAND_LEN, $crate::id::ID_CHARSET)
}; };
(id $p:expr) => { (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) => { (token) => {
random_with_charset($crate::id::TOKEN_RAND_LEN, $crate::id::TOKEN_CHARSET) random_with_charset($crate::id::TOKEN_RAND_LEN, $crate::id::TOKEN_CHARSET)
}; };
(token $p:expr) => { (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 charset[idx] as char
}) })
.collect() .collect()
} }

View File

@ -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::config::Config;
use crate::error::APIErrorResponse; 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; pub mod error;
#[macro_use] #[macro_use]
pub mod response; pub mod response;
pub mod config; pub mod config;
pub mod models;
pub mod routes; pub mod routes;
pub mod schema; pub mod schema;
pub mod models;
#[macro_use] #[macro_use]
pub mod id; pub mod id;
pub mod auth;
pub mod email; pub mod email;
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub config: Config, pub config: Config,
pub pool: bb8::Pool<AsyncDieselConnectionManager<AsyncPgConnection>> pub pool: bb8::Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
} }
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
@ -53,7 +53,11 @@ async fn main() {
let config_str = match fs::read_to_string(&config_pathbuf) { let config_str = match fs::read_to_string(&config_pathbuf) {
Ok(c_str) => c_str, Ok(c_str) => c_str,
Err(e) => { Err(e) => {
error!("Error loading configuration from {}: {}", config_pathbuf.display(), e); error!(
"Error loading configuration from {}: {}",
config_pathbuf.display(),
e
);
std::process::exit(1); std::process::exit(1);
} }
}; };
@ -61,7 +65,11 @@ async fn main() {
let config: Config = match toml::from_str(&config_str) { let config: Config = match toml::from_str(&config_str) {
Ok(config) => config, Ok(config) => config,
Err(e) => { Err(e) => {
error!("Error parsing configuration in {}: {}", config_pathbuf.display(), e); error!(
"Error parsing configuration in {}: {}",
config_pathbuf.display(),
e
);
std::process::exit(1); std::process::exit(1);
} }
}; };
@ -82,7 +90,8 @@ async fn main() {
let local_config = config.clone(); let local_config = config.clone();
let db_url = config.database.url.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::<AsyncPgConnection>::establish(&db_url) { let mut conn = match AsyncConnectionWrapper::<AsyncPgConnection>::establish(&db_url) {
Ok(conn) => conn, Ok(conn) => conn,
Err(e) => { Err(e) => {
@ -98,7 +107,9 @@ async fn main() {
std::process::exit(1); std::process::exit(1);
} }
} }
}).await { })
.await
{
Ok(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
error!("Error waiting for migrations: {}", e); error!("Error waiting for migrations: {}", e);
@ -106,10 +117,7 @@ async fn main() {
} }
} }
let app_state = Data::new(AppState { let app_state = Data::new(AppState { config, pool });
config,
pool
});
let server = HttpServer::new(move || { let server = HttpServer::new(move || {
App::new() App::new()
@ -128,7 +136,9 @@ async fn main() {
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(actix_cors::Cors::permissive()) .wrap(actix_cors::Cors::permissive())
.app_data(app_state.clone()) .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(); server.run().await.unwrap();

View File

@ -1,30 +1,47 @@
use std::time::SystemTime;
use diesel::{Associations, Identifiable, Insertable, Queryable, Selectable}; 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(table_name = crate::schema::users)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct User { pub struct User {
pub id: String, 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(belongs_to(User))]
#[diesel(table_name = crate::schema::magic_links)] #[diesel(table_name = crate::schema::magic_links)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct MagicLink { pub struct MagicLink {
pub id: String, pub id: String,
pub user_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(belongs_to(User))]
#[diesel(table_name = crate::schema::session_tokens)] #[diesel(table_name = crate::schema::session_tokens)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct SessionToken { pub struct SessionToken {
pub id: String, pub id: String,
pub user_id: String, pub user_id: String,
pub expires: SystemTime 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,
}

View File

@ -1,15 +1,15 @@
use std::fmt::{Debug, Display, Formatter}; use crate::error::APIErrorResponse;
use actix_web::body::BoxBody; use actix_web::body::BoxBody;
use actix_web::error::JsonPayloadError;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::{HttpRequest, HttpResponse, Responder}; use actix_web::{HttpRequest, HttpResponse, Responder};
use actix_web::error::JsonPayloadError;
use serde::Serialize; use serde::Serialize;
use crate::error::APIErrorResponse; use std::fmt::{Debug, Display, Formatter};
#[derive(Debug)] #[derive(Debug)]
pub enum JsonAPIResponse<T: Serialize + Debug> { pub enum JsonAPIResponse<T: Serialize + Debug> {
Error(StatusCode, APIErrorResponse), Error(StatusCode, APIErrorResponse),
Success(StatusCode, T) Success(StatusCode, T),
} }
impl<T: Serialize + Debug> Display for JsonAPIResponse<T> { impl<T: Serialize + Debug> Display for JsonAPIResponse<T> {
@ -23,18 +23,14 @@ impl<T: Serialize + Debug> Responder for JsonAPIResponse<T> {
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> { fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
match self { match self {
JsonAPIResponse::Error(c, r) => { JsonAPIResponse::Error(c, r) => match serde_json::to_string(&r) {
match serde_json::to_string(&r) { Ok(body) => HttpResponse::build(c).body(body),
Ok(body) => HttpResponse::build(c).body(body), Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)),
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),
JsonAPIResponse::Success(c, b) => { Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)),
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 { macro_rules! internal_error {
($e:expr) => {{ ($e:expr) => {{
log::error!("internal error: {}", $e); 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 { $crate::error::APIErrorResponse {
code: $c.to_string(), code: $c.to_string(),
message: $m.to_string(), message: $m.to_string(),
path: Some($p.to_string()) path: Some($p.to_string()),
} }
}; };
($c:expr,$m:expr) => { ($c:expr,$m:expr) => {
$crate::error::APIErrorResponse { $crate::error::APIErrorResponse {
code: $c.to_string(), code: $c.to_string(),
message: $m.to_string(), message: $m.to_string(),
path: None path: None,
} }
}; };
} }

View File

@ -1 +1 @@
pub mod v1; pub mod v1;

View File

@ -1,55 +1,76 @@
use std::time::{Duration, SystemTime}; use crate::email::send_email;
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::models::{MagicLink, User}; use crate::models::{MagicLink, User};
use crate::response::JsonAPIResponse; 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::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::ExpressionMethods;
use diesel::QueryDsl;
use diesel::SelectableHelper;
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct LoginRequest { pub struct LoginRequest {
pub email: String pub email: String,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct LoginResponse { pub struct LoginResponse {
pub data: Option<()>, pub data: Option<()>,
pub metadata: LoginResponseMetadata pub metadata: LoginResponseMetadata,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct LoginResponseMetadata {} pub struct LoginResponseMetadata {}
#[post("/v1/auth/magic-link")] #[post("/v1/auth/magic-link")]
pub async fn login_req(req: Json<LoginRequest>, state: Data<AppState>) -> JsonAPIResponse<LoginResponse> { pub async fn login_req(
req: Json<LoginRequest>,
state: Data<AppState>,
) -> JsonAPIResponse<LoginResponse> {
let mut conn = handle_error!(state.pool.get().await); 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 // Difference from DN functionality: Trifid API will not implicitly create accounts
let user = match user_vec.get(0) { let user = match user_vec.get(0) {
Some(user) => user, Some(user) => user,
None => { 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 { let new_magic_link = MagicLink {
id: randid!(token "ml"), id: randid!(token "ml"),
user_id: user.id.clone(), 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); handle_error!(send_email(&new_magic_link.id, &req.email, &state.config).await);
ok!(LoginResponse { data: None, metadata: LoginResponseMetadata {} }) ok!(LoginResponse {
} data: None,
metadata: LoginResponseMetadata {}
})
}

View File

@ -1,2 +1,2 @@
pub mod magic_link;
pub mod verify_magic_link; pub mod verify_magic_link;
pub mod magic_link;

View File

@ -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::http::StatusCode;
use actix_web::post; use actix_web::post;
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; 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 diesel_async::RunQueryDsl;
use crate::schema::session_tokens; use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct VerifyLinkReq { pub struct VerifyLinkReq {
@ -19,33 +19,56 @@ pub struct VerifyLinkReq {
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct VerifyLinkResp { pub struct VerifyLinkResp {
pub data: VerifyLinkRespData, pub data: VerifyLinkRespData,
pub metadata: VerifyLinkRespMetadata pub metadata: VerifyLinkRespMetadata,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct VerifyLinkRespData { pub struct VerifyLinkRespData {
#[serde(rename = "sessionToken")] #[serde(rename = "sessionToken")]
pub session_token: String pub session_token: String,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct VerifyLinkRespMetadata {} pub struct VerifyLinkRespMetadata {}
#[post("/v1/auth/verify-magic-link")] #[post("/v1/auth/verify-magic-link")]
pub async fn verify_link_req(req: Json<VerifyLinkReq>, state: Data<AppState>) -> JsonAPIResponse<VerifyLinkResp> { pub async fn verify_link_req(
req: Json<VerifyLinkReq>,
state: Data<AppState>,
) -> JsonAPIResponse<VerifyLinkResp> {
use crate::schema::magic_links::dsl::*; use crate::schema::magic_links::dsl::*;
let mut conn = handle_error!(state.pool.get().await); 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) { let token = match tokens.get(0) {
Some(token) => token, Some(token) => token,
None => { 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() { 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); handle_error!(diesel::delete(token).execute(&mut conn).await);
@ -53,10 +76,16 @@ pub async fn verify_link_req(req: Json<VerifyLinkReq>, state: Data<AppState>) ->
let new_token = SessionToken { let new_token = SessionToken {
id: randid!(token "sess"), id: randid!(token "sess"),
user_id: token.user_id.clone(), 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 { ok!(VerifyLinkResp {
data: VerifyLinkRespData { data: VerifyLinkRespData {
@ -64,4 +93,4 @@ pub async fn verify_link_req(req: Json<VerifyLinkReq>, state: Data<AppState>) ->
}, },
metadata: VerifyLinkRespMetadata {} metadata: VerifyLinkRespMetadata {}
}) })
} }

View File

@ -1,2 +1,3 @@
pub mod auth;
pub mod signup; pub mod signup;
pub mod auth; pub mod totp_authenticators;

View File

@ -1,29 +1,32 @@
use std::time::{Duration, SystemTime}; use crate::email::send_email;
use actix_web::post;
use actix_web::web::{Data, Json};
use serde::{Deserialize, Serialize};
use crate::{AppState, randid};
use crate::models::{MagicLink, User}; use crate::models::{MagicLink, User};
use crate::response::JsonAPIResponse; 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 diesel_async::RunQueryDsl;
use crate::email::send_email; use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct SignupRequest { pub struct SignupRequest {
pub email: String pub email: String,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct SignupResponse { pub struct SignupResponse {
pub data: Option<()>, pub data: Option<()>,
pub metadata: SignupResponseMetadata pub metadata: SignupResponseMetadata,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct SignupResponseMetadata {} pub struct SignupResponseMetadata {}
#[post("/v1/signup")] #[post("/v1/signup")]
pub async fn signup_req(req: Json<SignupRequest>, state: Data<AppState>) -> JsonAPIResponse<SignupResponse> { pub async fn signup_req(
req: Json<SignupRequest>,
state: Data<AppState>,
) -> JsonAPIResponse<SignupResponse> {
let mut conn = handle_error!(state.pool.get().await); let mut conn = handle_error!(state.pool.get().await);
let user_id = randid!(id "user"); let user_id = randid!(id "user");
@ -33,16 +36,30 @@ pub async fn signup_req(req: Json<SignupRequest>, state: Data<AppState>) -> Json
email: req.email.clone(), 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 { let new_magic_link = MagicLink {
id: randid!(token "ml"), id: randid!(token "ml"),
user_id, 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); handle_error!(send_email(&new_magic_link.id, &req.email, &state.config).await);
ok!(SignupResponse { data: None, metadata: SignupResponseMetadata {} }) ok!(SignupResponse {
} data: None,
metadata: SignupResponseMetadata {}
})
}

View File

@ -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<TotpAuthenticatorReq>,
state: Data<AppState>,
req_info: HttpRequest,
) -> JsonAPIResponse<TotpAuthResp> {
let mut conn = handle_error!(state.pool.get().await);
let auth_info = auth!(req_info, conn);
enforce!(sess auth_info);
todo!()
}

View File

@ -16,6 +16,15 @@ diesel::table! {
} }
} }
diesel::table! {
totp_authenticators (id) {
id -> Varchar,
user_id -> Varchar,
secret -> Varchar,
verified -> Bool,
}
}
diesel::table! { diesel::table! {
users (id) { users (id) {
id -> Varchar, id -> Varchar,
@ -25,9 +34,11 @@ diesel::table! {
diesel::joinable!(magic_links -> users (user_id)); diesel::joinable!(magic_links -> users (user_id));
diesel::joinable!(session_tokens -> 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!( diesel::allow_tables_to_appear_in_same_query!(
magic_links, magic_links,
session_tokens, session_tokens,
totp_authenticators,
users, users,
); );