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 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,
|
||||||
}
|
}
|
|
@ -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(())
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
JsonAPIResponse::Success(c, b) => {
|
|
||||||
match serde_json::to_string(&b) {
|
|
||||||
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)),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -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 {}
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -1,2 +1,2 @@
|
||||||
pub mod verify_magic_link;
|
|
||||||
pub mod 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::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 {
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod signup;
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod signup;
|
||||||
|
pub mod totp_authenticators;
|
||||||
|
|
|
@ -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 {}
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -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! {
|
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,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue