diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs index 536a5d8..1a8d1d5 100644 --- a/trifid-api/src/config.rs +++ b/trifid-api/src/config.rs @@ -57,7 +57,9 @@ pub struct TrifidConfigServer { #[derive(Serialize, Deserialize, Debug)] pub struct TrifidConfigTokens { #[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 } @@ -65,4 +67,5 @@ fn min_connections_default() -> u32 { 5 } fn time_defaults() -> u64 { 8 } fn sqlx_logging_default() -> bool { true } fn socketaddr_8080() -> SocketAddr { SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([0, 0, 0, 0]), 8080)) } -fn magic_link_expiry_time() -> u64 { 3600 } \ No newline at end of file +fn magic_link_expiry_time() -> u64 { 3600 } // 1 hour +fn session_token_expiry_time() -> u64 { 15780000 } // 6 months \ No newline at end of file diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index ca641df..18cd637 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -63,6 +63,7 @@ async fn main() -> Result<(), Box> { .wrap(RequestIdentifier::with_generator(random_id_no_id)) .service(routes::v1::auth::magic_link::magic_link_request) .service(routes::v1::signup::signup_request) + .service(routes::v1::auth::verify_magic_link::verify_magic_link_request) }).bind(CONFIG.server.bind)?.run().await?; Ok(()) diff --git a/trifid-api/src/routes/v1/auth/mod.rs b/trifid-api/src/routes/v1/auth/mod.rs index 600b7fa..63d2a9f 100644 --- a/trifid-api/src/routes/v1/auth/mod.rs +++ b/trifid-api/src/routes/v1/auth/mod.rs @@ -1 +1,2 @@ -pub mod magic_link; \ No newline at end of file +pub mod magic_link; +pub mod verify_magic_link; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/auth/verify_magic_link.rs b/trifid-api/src/routes/v1/auth/verify_magic_link.rs new file mode 100644 index 0000000..0e08582 --- /dev/null +++ b/trifid-api/src/routes/v1/auth/verify_magic_link.rs @@ -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, req: Json) -> HttpResponse { + let link: Option = 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 {}, + } + ) +} \ No newline at end of file diff --git a/trifid-api/trifid_api_entities/src/entity/mod.rs b/trifid-api/trifid_api_entities/src/entity/mod.rs index 3bd313d..fa37cff 100644 --- a/trifid-api/trifid_api_entities/src/entity/mod.rs +++ b/trifid-api/trifid_api_entities/src/entity/mod.rs @@ -3,4 +3,5 @@ pub mod prelude; pub mod magic_link; +pub mod session_token; pub mod user; diff --git a/trifid-api/trifid_api_entities/src/entity/prelude.rs b/trifid-api/trifid_api_entities/src/entity/prelude.rs index 527e6e0..b5b9410 100644 --- a/trifid-api/trifid_api_entities/src/entity/prelude.rs +++ b/trifid-api/trifid_api_entities/src/entity/prelude.rs @@ -1,4 +1,5 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2 pub use super::magic_link::Entity as MagicLink; +pub use super::session_token::Entity as SessionToken; pub use super::user::Entity as User; diff --git a/trifid-api/trifid_api_entities/src/entity/session_token.rs b/trifid-api/trifid_api_entities/src/entity/session_token.rs new file mode 100644 index 0000000..a648966 --- /dev/null +++ b/trifid-api/trifid_api_entities/src/entity/session_token.rs @@ -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 for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/trifid-api/trifid_api_entities/src/entity/user.rs b/trifid-api/trifid_api_entities/src/entity/user.rs index f7d4230..1ca5914 100644 --- a/trifid-api/trifid_api_entities/src/entity/user.rs +++ b/trifid-api/trifid_api_entities/src/entity/user.rs @@ -15,6 +15,8 @@ pub struct Model { pub enum Relation { #[sea_orm(has_many = "super::magic_link::Entity")] MagicLink, + #[sea_orm(has_many = "super::session_token::Entity")] + SessionToken, } impl Related for Entity { @@ -23,4 +25,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::SessionToken.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/trifid-api/trifid_api_migration/src/lib.rs b/trifid-api/trifid_api_migration/src/lib.rs index 77e9136..5992ac1 100644 --- a/trifid-api/trifid_api_migration/src/lib.rs +++ b/trifid-api/trifid_api_migration/src/lib.rs @@ -4,6 +4,7 @@ pub struct Migrator; pub mod m20230402_162601_create_table_users; pub mod m20230402_183515_create_table_magic_links; +pub mod m20230402_213712_create_table_session_tokens; #[async_trait::async_trait] impl MigratorTrait for Migrator { @@ -11,6 +12,7 @@ impl MigratorTrait for Migrator { vec![ Box::new(m20230402_162601_create_table_users::Migration), Box::new(m20230402_183515_create_table_magic_links::Migration), + Box::new(m20230402_213712_create_table_session_tokens::Migration), ] } } diff --git a/trifid-api/trifid_api_migration/src/m20230402_213712_create_table_session_tokens.rs b/trifid-api/trifid_api_migration/src/m20230402_213712_create_table_session_tokens.rs new file mode 100644 index 0000000..8c51606 --- /dev/null +++ b/trifid-api/trifid_api_migration/src/m20230402_213712_create_table_session_tokens.rs @@ -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 +}