worktm
This commit is contained in:
parent
e7e49f9255
commit
68627b0c92
|
@ -0,0 +1 @@
|
|||
DROP TABLE totp_authenticators;
|
|
@ -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
|
||||
);
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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 {}
|
||||
})
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
pub mod verify_magic_link;
|
||||
pub mod magic_link;
|
||||
pub mod verify_magic_link;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pub mod signup;
|
||||
pub mod auth;
|
||||
pub mod signup;
|
||||
pub mod totp_authenticators;
|
||||
|
|
|
@ -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 {}
|
||||
})
|
||||
}
|
|
@ -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!()
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue