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

@ -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 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<usize>
pub workers: Option<usize>,
}
#[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
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::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()
.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(())
}

View File

@ -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<String>
pub path: Option<String>,
}
impl Display for APIErrorResponse {

View File

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

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::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<AsyncDieselConnectionManager<AsyncPgConnection>>
pub pool: bb8::Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
}
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::<AsyncPgConnection>::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();

View File

@ -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
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::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<T: Serialize + Debug> {
Error(StatusCode, APIErrorResponse),
Success(StatusCode, T)
Success(StatusCode, 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> {
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,
}
};
}

View File

@ -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<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 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 {} })
ok!(LoginResponse {
data: None,
metadata: LoginResponseMetadata {}
})
}

View File

@ -1,2 +1,2 @@
pub mod verify_magic_link;
pub mod magic_link;
pub mod verify_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::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<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::*;
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<VerifyLinkReq>, state: Data<AppState>) ->
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 {

View File

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

View File

@ -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<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 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(),
};
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 {} })
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! {
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,
);