/v1/auth/verify-magic-link
This commit is contained in:
parent
fcadd98701
commit
1453509dc4
|
@ -57,7 +57,9 @@ pub struct TrifidConfigServer {
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct TrifidConfigTokens {
|
pub struct TrifidConfigTokens {
|
||||||
#[serde(default = "magic_link_expiry_time")]
|
#[serde(default = "magic_link_expiry_time")]
|
||||||
pub magic_link_expiry_time_seconds: u64
|
pub magic_link_expiry_time_seconds: u64,
|
||||||
|
#[serde(default = "session_token_expiry_time")]
|
||||||
|
pub session_token_expiry_time_seconds: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
fn max_connections_default() -> u32 { 100 }
|
fn max_connections_default() -> u32 { 100 }
|
||||||
|
@ -65,4 +67,5 @@ fn min_connections_default() -> u32 { 5 }
|
||||||
fn time_defaults() -> u64 { 8 }
|
fn time_defaults() -> u64 { 8 }
|
||||||
fn sqlx_logging_default() -> bool { true }
|
fn sqlx_logging_default() -> bool { true }
|
||||||
fn socketaddr_8080() -> SocketAddr { SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([0, 0, 0, 0]), 8080)) }
|
fn socketaddr_8080() -> SocketAddr { SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([0, 0, 0, 0]), 8080)) }
|
||||||
fn magic_link_expiry_time() -> u64 { 3600 }
|
fn magic_link_expiry_time() -> u64 { 3600 } // 1 hour
|
||||||
|
fn session_token_expiry_time() -> u64 { 15780000 } // 6 months
|
|
@ -63,6 +63,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
.wrap(RequestIdentifier::with_generator(random_id_no_id))
|
.wrap(RequestIdentifier::with_generator(random_id_no_id))
|
||||||
.service(routes::v1::auth::magic_link::magic_link_request)
|
.service(routes::v1::auth::magic_link::magic_link_request)
|
||||||
.service(routes::v1::signup::signup_request)
|
.service(routes::v1::signup::signup_request)
|
||||||
|
.service(routes::v1::auth::verify_magic_link::verify_magic_link_request)
|
||||||
}).bind(CONFIG.server.bind)?.run().await?;
|
}).bind(CONFIG.server.bind)?.run().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
pub mod magic_link;
|
pub mod magic_link;
|
||||||
|
pub mod verify_magic_link;
|
|
@ -0,0 +1,113 @@
|
||||||
|
use actix_web::{HttpResponse, post};
|
||||||
|
use actix_web::web::{Data, Json};
|
||||||
|
use log::error;
|
||||||
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use crate::AppState;
|
||||||
|
use trifid_api_entities::entity::magic_link;
|
||||||
|
use trifid_api_entities::entity::magic_link::Model;
|
||||||
|
use trifid_api_entities::entity::session_token;
|
||||||
|
use crate::config::CONFIG;
|
||||||
|
use crate::error::{APIError, APIErrorsResponse};
|
||||||
|
use crate::timers::{expired, expires_in_seconds};
|
||||||
|
use crate::tokens::random_token;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct VerifyMagicLinkRequest {
|
||||||
|
#[serde(rename = "magicLinkToken")]
|
||||||
|
pub magic_link_token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct VerifyMagicLinkResponse {
|
||||||
|
pub data: VerifyMagicLinkResponseData,
|
||||||
|
pub metadata: VerifyMagicLinkResponseMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct VerifyMagicLinkResponseData {
|
||||||
|
#[serde(rename = "sessionToken")]
|
||||||
|
pub session_token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct VerifyMagicLinkResponseMetadata {}
|
||||||
|
|
||||||
|
#[post("/v1/auth/verify-magic-link")]
|
||||||
|
pub async fn verify_magic_link_request(db: Data<AppState>, req: Json<VerifyMagicLinkRequest>) -> HttpResponse {
|
||||||
|
let link: Option<Model> = match magic_link::Entity::find().filter(magic_link::Column::Id.eq(&req.magic_link_token)).one(&db.conn).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
error!("database error: {}", e);
|
||||||
|
return HttpResponse::InternalServerError().json(APIErrorsResponse {
|
||||||
|
errors: vec![
|
||||||
|
APIError {
|
||||||
|
code: "ERR_DB_ERROR".to_string(),
|
||||||
|
message: "There was an error with the database request, please try again later.".to_string(),
|
||||||
|
path: None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let link = match link {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
return HttpResponse::Unauthorized().json(APIErrorsResponse {
|
||||||
|
errors: vec![
|
||||||
|
APIError {
|
||||||
|
code: "ERR_UNAUTHORIZED".to_string(),
|
||||||
|
message: "Unauthorized".to_string(),
|
||||||
|
path: None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !expired(link.expires_on as u64) {
|
||||||
|
return HttpResponse::Unauthorized().json(APIErrorsResponse {
|
||||||
|
errors: vec![
|
||||||
|
APIError {
|
||||||
|
code: "ERR_EXPIRED".to_string(),
|
||||||
|
message: "Magic link token expired".to_string(),
|
||||||
|
path: None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let model = session_token::Model {
|
||||||
|
id: random_token("sess"),
|
||||||
|
user: link.user,
|
||||||
|
expires_on: expires_in_seconds(CONFIG.tokens.session_token_expiry_time_seconds) as i64,
|
||||||
|
};
|
||||||
|
let token = model.id.clone();
|
||||||
|
let active_model = model.into_active_model();
|
||||||
|
|
||||||
|
match active_model.insert(&db.conn).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
error!("database error: {}", e);
|
||||||
|
return HttpResponse::InternalServerError().json(APIErrorsResponse {
|
||||||
|
errors: vec![
|
||||||
|
APIError {
|
||||||
|
code: "ERR_DB_ERROR".to_string(),
|
||||||
|
message: "There was an error with the database request, please try again later.".to_string(),
|
||||||
|
path: None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(
|
||||||
|
VerifyMagicLinkResponse {
|
||||||
|
data: VerifyMagicLinkResponseData {
|
||||||
|
session_token: token,
|
||||||
|
},
|
||||||
|
metadata: VerifyMagicLinkResponseMetadata {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,4 +3,5 @@
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
pub mod magic_link;
|
pub mod magic_link;
|
||||||
|
pub mod session_token;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
|
||||||
|
|
||||||
pub use super::magic_link::Entity as MagicLink;
|
pub use super::magic_link::Entity as MagicLink;
|
||||||
|
pub use super::session_token::Entity as SessionToken;
|
||||||
pub use super::user::Entity as User;
|
pub use super::user::Entity as User;
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "session_token")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: String,
|
||||||
|
pub user: String,
|
||||||
|
pub expires_on: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::user::Entity",
|
||||||
|
from = "Column::User",
|
||||||
|
to = "super::user::Column::Id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::User.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -15,6 +15,8 @@ pub struct Model {
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(has_many = "super::magic_link::Entity")]
|
#[sea_orm(has_many = "super::magic_link::Entity")]
|
||||||
MagicLink,
|
MagicLink,
|
||||||
|
#[sea_orm(has_many = "super::session_token::Entity")]
|
||||||
|
SessionToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::magic_link::Entity> for Entity {
|
impl Related<super::magic_link::Entity> for Entity {
|
||||||
|
@ -23,4 +25,10 @@ impl Related<super::magic_link::Entity> for Entity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::session_token::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::SessionToken.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub struct Migrator;
|
||||||
|
|
||||||
pub mod m20230402_162601_create_table_users;
|
pub mod m20230402_162601_create_table_users;
|
||||||
pub mod m20230402_183515_create_table_magic_links;
|
pub mod m20230402_183515_create_table_magic_links;
|
||||||
|
pub mod m20230402_213712_create_table_session_tokens;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigratorTrait for Migrator {
|
impl MigratorTrait for Migrator {
|
||||||
|
@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
|
||||||
vec![
|
vec![
|
||||||
Box::new(m20230402_162601_create_table_users::Migration),
|
Box::new(m20230402_162601_create_table_users::Migration),
|
||||||
Box::new(m20230402_183515_create_table_magic_links::Migration),
|
Box::new(m20230402_183515_create_table_magic_links::Migration),
|
||||||
|
Box::new(m20230402_213712_create_table_session_tokens::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
use crate::m20230402_162601_create_table_users::User;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(SessionToken::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(ColumnDef::new(SessionToken::Id).string().not_null().primary_key())
|
||||||
|
.col(ColumnDef::new(SessionToken::User).string().not_null())
|
||||||
|
.col(ColumnDef::new(SessionToken::ExpiresOn).big_integer().not_null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.from(SessionToken::Table, SessionToken::User)
|
||||||
|
.to(User::Table, User::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.to_owned()
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager.drop_table(Table::drop().table(SessionToken::Table).to_owned()).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Learn more at https://docs.rs/sea-query#iden
|
||||||
|
#[derive(Iden)]
|
||||||
|
pub enum SessionToken {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
User,
|
||||||
|
ExpiresOn
|
||||||
|
}
|
Loading…
Reference in New Issue