diff --git a/api/src/guard/candidate_jwt.rs b/api/src/guard/candidate_jwt.rs deleted file mode 100644 index 25d9486..0000000 --- a/api/src/guard/candidate_jwt.rs +++ /dev/null @@ -1,33 +0,0 @@ -use rocket::http::Status; -use rocket::outcome::Outcome; -use rocket::request::{FromRequest, Request}; - -use portfolio_core::token::candidate_token::CandidateToken; -use portfolio_core::token::decode_candidate_token; - -pub struct TokenRequest(CandidateToken); - -impl TokenRequest { - pub fn to_token(self) -> CandidateToken { - self.0 - } -} - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for TokenRequest { - type Error = Status; - async fn from_request(req: &'r Request<'_>) -> Outcome { - if let Some(auth) = req.headers().get_one("Authorization") { - let auth_string = auth.to_string(); - if auth_string.starts_with("Bearer") { - let token = auth_string[6..auth_string.len()].trim(); - let token_data = decode_candidate_token(token.to_string()); - - if token_data.is_ok() { - return Outcome::Success(TokenRequest(token_data.ok().unwrap().claims)); - } - } - } - return Outcome::Failure((Status::Unauthorized, Status::Unauthorized)); - } -} diff --git a/api/src/guard/candidate_refresh_token.rs b/api/src/guard/candidate_refresh_token.rs new file mode 100644 index 0000000..dee824d --- /dev/null +++ b/api/src/guard/candidate_refresh_token.rs @@ -0,0 +1,27 @@ +use portfolio_core::sea_orm::prelude::Uuid; +use rocket::http::Status; +use rocket::outcome::Outcome; +use rocket::request::{FromRequest, Request}; + + +pub struct UUIDCookie(Uuid); + +impl UUIDCookie { + pub fn value(self) -> Uuid { + self.0 + } +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for UUIDCookie { + type Error = Status; + async fn from_request(req: &'r Request<'_>) -> Outcome { + let session_id = req.cookies().get("id").unwrap().name_value().1; + println!("session_id: {}", session_id); + + match Uuid::parse_str(&session_id) { + Ok(uuid) => Outcome::Success(UUIDCookie(uuid)), + Err(_) => return Outcome::Failure((Status::BadRequest, Status::BadRequest)), + } + } +} diff --git a/api/src/guard/mod.rs b/api/src/guard/mod.rs index f2318e8..bdf6937 100644 --- a/api/src/guard/mod.rs +++ b/api/src/guard/mod.rs @@ -1 +1 @@ -pub mod candidate_jwt; \ No newline at end of file +pub mod candidate_refresh_token; \ No newline at end of file diff --git a/api/src/lib.rs b/api/src/lib.rs index b4d16c9..98821d4 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,7 +1,6 @@ #[macro_use] extern crate rocket; -use guard::candidate_jwt::TokenRequest; use portfolio_core::error::ServiceError; use portfolio_core::services::candidate_service::CandidateService; use requests::LoginRequest; @@ -27,6 +26,8 @@ pub use entity::candidate::Entity as Candidate; use portfolio_core::crypto::random_8_char_string; +use crate::guard::candidate_refresh_token::UUIDCookie; + fn custom_err_from_service_err(service_err: ServiceError) -> Custom { Custom(Status::from_code(service_err.0.code).unwrap_or_default(), service_err.1.to_string()) } @@ -45,38 +46,39 @@ async fn create(conn: Connection<'_, Db>, post_form: Json) -> Ok(plain_text_password) } +#[get("/whoami")] +async fn validate(conn: Connection<'_, Db>, uuid_cookie: Result) -> Result> { + let db = conn.into_inner(); + let user = CandidateService::auth_user_session(db, uuid_cookie.ok().unwrap().value()).await; + + + match user { + Ok(user) => Ok(user.application.to_string()), + Err(err) => Err(custom_err_from_service_err(err)) + } +} + #[post("/login", data = "")] async fn login(conn: Connection<'_, Db>, login_form: Json) -> Result> { let db = conn.into_inner(); println!("{} {}", login_form.application_id, login_form.password); - let jwt = CandidateService::login(db, - login_form.application_id, - login_form.password.to_owned()).await; + let session_token = CandidateService::new_session(db, + login_form.application_id, + login_form.password.to_string() + ).await; - if jwt.is_ok() { + if session_token.is_ok() { return Ok( - jwt.ok().unwrap() + session_token.ok().unwrap() ); } else { return Err( - custom_err_from_service_err(jwt.err().unwrap()) + custom_err_from_service_err(session_token.err().unwrap()) ) } } -#[get("/whoami")] -async fn whoami(conn: Connection<'_, Db>, token_req: Result) -> Result> { - let db = conn.into_inner(); - let token = token_req.ok().unwrap().to_token(); - let user = CandidateService::authenticate_candidate(db, token).await; - - match user { - Ok(user) => Ok(format!("{} {}", user.name.unwrap(), user.surname.unwrap())), - Err(e) => Err(custom_err_from_service_err(e)), - } -} - #[get("/hello")] async fn hello() -> &'static str { "Hello, world!" @@ -94,7 +96,7 @@ async fn start() -> Result<(), rocket::Error> { .attach(Db::init()) .attach(AdHoc::try_on_ignite("Migrations", run_migrations)) //.mount("/", FileServer::from(relative!("/static"))) - .mount("/", routes![create, login, hello, whoami]) + .mount("/", routes![create, login, hello, validate]) .register("/", catchers![]) .launch() .await diff --git a/core/src/crypto.rs b/core/src/crypto.rs index d6ded82..9ee678e 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -461,4 +461,4 @@ mod tests { PASSWORD ); } -} +} \ No newline at end of file diff --git a/core/src/error.rs b/core/src/error.rs index b2093a7..d8afda9 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -4,6 +4,9 @@ pub struct Status { pub const INVALID_CREDENTIALS_ERROR: ServiceError = ServiceError(Status { code: 401 }, "Invalid credentials"); +pub const EXPIRED_SESSION_ERROR: ServiceError = ServiceError(Status { code: 401 }, + "Session expired, please login again"); + pub const JWT_ERROR: ServiceError = ServiceError(Status { code: 500 }, "Error while encoding JWT"); @@ -13,6 +16,9 @@ pub const USER_NOT_FOUND_ERROR: ServiceError = ServiceError(Status { code: 404 } pub const DB_ERROR: ServiceError = ServiceError(Status { code: 500 }, "Database error"); -pub const USER_NOT_FOUND_BY_JWT_ID: ServiceError = ServiceError(Status { code: 500 }, // User got somehow +pub const USER_NOT_FOUND_BY_JWT_ID: ServiceError = ServiceError(Status { code: 500 }, // User got somehow deleted + "User not found, please contact technical support"); // Shouldn't ever happen + +pub const USER_NOT_FOUND_BY_SESSION_ID: ServiceError = ServiceError(Status { code: 500 }, // User got somehow deleted "User not found, please contact technical support"); // Shouldn't ever happen pub struct ServiceError<'a>(pub Status, pub &'a str); \ No newline at end of file diff --git a/core/src/lib.rs b/core/src/lib.rs index 7da383a..1773217 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,7 +1,6 @@ mod mutation; mod query; pub mod crypto; -pub mod token; pub mod filetype; pub mod services; pub mod error; diff --git a/core/src/mutation.rs b/core/src/mutation.rs index 069f83f..8e8ccc8 100644 --- a/core/src/mutation.rs +++ b/core/src/mutation.rs @@ -1,5 +1,6 @@ -use ::entity::{candidate}; -use sea_orm::*; +use chrono::{Utc, Duration}; +use ::entity::{candidate, session}; +use sea_orm::{*, prelude::Uuid}; use crate::crypto::hash_password; pub struct Mutation; @@ -24,4 +25,33 @@ impl Mutation { .insert(db) .await } + + + pub async fn insert_session( + db: &DbConn, + user_id: i32, + random_uuid: Uuid, + ) -> Result { + session::ActiveModel { + id: Set(random_uuid), + user_id: Set(user_id), + ip_address: Set("127.0.0.1".to_string()), + created_at: Set(Utc::now().naive_local()), + expires_at: Set(Utc::now().naive_local().checked_add_signed(Duration::days(1)).unwrap()), + } + .insert(db) + .await + } + + pub async fn delete_session( + db: &DbConn, + session_id: Uuid + ) -> Result { + session::ActiveModel { + id: Set(session_id), + ..Default::default() + } + .delete(db) + .await + } } diff --git a/core/src/query.rs b/core/src/query.rs index a4e4236..4a356e9 100644 --- a/core/src/query.rs +++ b/core/src/query.rs @@ -1,5 +1,7 @@ use ::entity::{candidate, candidate::Entity as Candidate}; +use ::entity::{session, session::Entity as Session}; use sea_orm::*; +use sea_orm::prelude::Uuid; pub struct Query; @@ -7,6 +9,18 @@ impl Query { pub async fn find_candidate_by_id(db: &DbConn, id: i32) -> Result, DbErr> { Candidate::find_by_id(id).one(db).await } + + pub async fn find_session_by_uuid(db: &DbConn, uuid: Uuid) -> Result, DbErr> { + Session::find_by_id(uuid).one(db).await + } + + // find session by user id + pub async fn find_session_by_user_id(db: &DbConn, user_id: i32) -> Result, DbErr> { + Session::find() + .filter(session::Column::UserId.eq(user_id)) + .one(db) + .await + } } #[cfg(test)] diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index cd72b45..09606dc 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,36 +1,63 @@ +use chrono::Duration; use entity::candidate; -use sea_orm::DatabaseConnection; +use sea_orm::{DatabaseConnection, prelude::Uuid, ModelTrait}; -use crate::{crypto, Query, token::{generate_candidate_token, candidate_token::CandidateToken}, error::{ServiceError, USER_NOT_FOUND_ERROR, INVALID_CREDENTIALS_ERROR, DB_ERROR, USER_NOT_FOUND_BY_JWT_ID}}; +use crate::{crypto::{self}, Query, error::{ServiceError, USER_NOT_FOUND_ERROR, INVALID_CREDENTIALS_ERROR, DB_ERROR, USER_NOT_FOUND_BY_JWT_ID, USER_NOT_FOUND_BY_SESSION_ID}, Mutation}; pub struct CandidateService; impl CandidateService { - - pub async fn login(db: &DatabaseConnection, id: i32, password: String) -> Result { - let candidate = match Query::find_candidate_by_id(db, id).await { + pub async fn new_session(db: &DatabaseConnection, user_id: i32, password: String) -> Result { + let candidate = match Query::find_candidate_by_id(db, user_id).await { Ok(candidate) => match candidate { Some(candidate) => candidate, None => return Err(USER_NOT_FOUND_ERROR) }, Err(_) => {return Err(DB_ERROR)} }; - - - let valid = crypto::verify_password(password,candidate.code.clone()).await - .expect("Invalid password"); - - if !valid { - return Err(INVALID_CREDENTIALS_ERROR) + + // compare passwords + match crypto::verify_password(password,candidate.code.clone()).await { + Ok(valid) => { + if !valid { + return Err(INVALID_CREDENTIALS_ERROR) + } + }, + Err(_) => {return Err(INVALID_CREDENTIALS_ERROR)} } - let jwt = generate_candidate_token(candidate); // TODO better error handling - Ok(jwt) - + // TODO delete old sessions? + + // user is authenticated, generate a session + let random_uuid: Uuid = Uuid::new_v4(); + + let session = match Mutation::insert_session(db, user_id, random_uuid).await { + Ok(session) => session, + Err(_) => return Err(DB_ERROR) + }; + + Ok(session.id.to_string()) } - pub async fn authenticate_candidate(db: &DatabaseConnection, token: CandidateToken) -> Result { - let candidate = match Query::find_candidate_by_id(db, token.application_id).await { + pub async fn auth_user_session(db: &DatabaseConnection, uuid: Uuid) -> Result { + let session = match Query::find_session_by_uuid(db, uuid).await { + Ok(session) => match session { + Some(session) => session, + None => return Err(USER_NOT_FOUND_BY_SESSION_ID) + }, + Err(_) => {return Err(DB_ERROR)} + }; + + let limit = session.created_at.checked_add_signed(Duration::days(1)).unwrap(); + let now = chrono::Utc::now().naive_utc(); + // check if session is expired + if now > limit { + // delete session + Mutation::delete_session(db, session.id).await.unwrap(); + return Err(USER_NOT_FOUND_BY_SESSION_ID) + } + + let candidate = match session.find_related(candidate::Entity).one(db).await { Ok(candidate) => match candidate { Some(candidate) => candidate, None => return Err(USER_NOT_FOUND_BY_JWT_ID) @@ -39,7 +66,7 @@ impl CandidateService { }; Ok(candidate) - } + } } @@ -47,19 +74,23 @@ impl CandidateService { #[cfg(test)] mod tests { use entity::candidate; - use sea_orm::{DbConn, Database, sea_query::TableCreateStatement, DbBackend, Schema, ConnectionTrait}; + use sea_orm::{DbConn, Database, sea_query::TableCreateStatement, DbBackend, Schema, ConnectionTrait, prelude::Uuid}; use serde_json::json; - use crate::{crypto, Mutation, services::candidate_service::CandidateService, token}; + use crate::{crypto, Mutation, services::candidate_service::CandidateService}; #[cfg(test)] async fn get_memory_sqlite_connection() -> DbConn { + use entity::session; + let base_url = "sqlite::memory:"; let db: DbConn = Database::connect(base_url).await.unwrap(); let schema = Schema::new(DbBackend::Sqlite); let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); + let stmt2: TableCreateStatement = schema.create_table_from_entity(session::Entity); db.execute(db.get_database_backend().build(&stmt)).await.unwrap(); + db.execute(db.get_database_backend().build(&stmt2)).await.unwrap(); db } @@ -79,22 +110,46 @@ mod tests { assert_ne!(candidate.code, SECRET.to_string()); assert!(crypto::verify_password(SECRET.to_string(), candidate.code).await.ok().unwrap()); } - - + #[tokio::test] - async fn test_candidate_jwt() { + async fn test_candidate_session_correct_password() { let db = &get_memory_sqlite_connection().await; - + let form = serde_json::from_value(json!({ "application": 5555555, })).unwrap(); - - let candidate = Mutation::create_candidate(&db, form, &"Tajny_kod".to_string()).await.unwrap(); - - let jwt = CandidateService::login(db, 5555555, "Tajny_kod".to_string()).await.ok().unwrap(); - - let claims = token::decode_candidate_token(jwt).ok().unwrap().claims; - - assert_eq!(claims.application_id, candidate.application); + + Mutation::create_candidate(&db, form, &"Tajny_kod".to_string()).await.unwrap(); + + // correct password + let session = CandidateService::new_session( + db, + 5555555, + "Tajny_kod".to_string() + ) + .await.ok().unwrap(); + // println!("{}", session.err().unwrap().1); + + assert!( + CandidateService::auth_user_session(db, Uuid::parse_str(&session).unwrap()) + .await + .is_ok() + ); + } + + #[tokio::test] + async fn test_candidate_session_incorrect_password() { + let db = &get_memory_sqlite_connection().await; + + let form = serde_json::from_value(json!({ + "application": 5555555, + })).unwrap(); + + let candidate_form = Mutation::create_candidate(&db, form, &"Tajny_kod".to_string()).await.unwrap(); + + // incorrect password + assert!( + CandidateService::new_session(db, candidate_form.application, "Spatny_kod".to_string()).await.is_err() + ); } } \ No newline at end of file diff --git a/core/src/token/admin_token.rs b/core/src/token/admin_token.rs deleted file mode 100644 index 73b7a5f..0000000 --- a/core/src/token/admin_token.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Serialize, Deserialize}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct AdminToken { - // issued at - pub iat: i64, - // expiration - pub exp: i64, -} \ No newline at end of file diff --git a/core/src/token/candidate_token.rs b/core/src/token/candidate_token.rs deleted file mode 100644 index 7014965..0000000 --- a/core/src/token/candidate_token.rs +++ /dev/null @@ -1,26 +0,0 @@ -use chrono::Utc; -use serde::{Serialize, Deserialize}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct CandidateToken { - // issued at - pub iat: i64, - // expiration - pub exp: i64, - pub application_id: i32, - pub name: String, - pub surname: String, -} - -impl CandidateToken { - pub fn generate(application_id: i32, name: String, surname: String) -> Self { - let now = Utc::now().timestamp(); - CandidateToken { - iat: now, - exp: now + 60 * 60, // 1 hour for now - application_id, - name, - surname, - } - } -} \ No newline at end of file diff --git a/core/src/token/mod.rs b/core/src/token/mod.rs deleted file mode 100644 index 370f28f..0000000 --- a/core/src/token/mod.rs +++ /dev/null @@ -1,98 +0,0 @@ -pub mod admin_token; -pub mod candidate_token; - -use chrono::Utc; - -use entity::{admin, candidate}; -use jsonwebtoken::errors::Result; -use jsonwebtoken::TokenData; -use jsonwebtoken::{DecodingKey, EncodingKey}; -use jsonwebtoken::{Header, Validation}; - -use admin_token::AdminToken; -use candidate_token::CandidateToken; -use serde::Deserialize; - -const ONE_WEEK: i64 = 60 * 60 * 24 * 7; - -pub fn generate_candidate_token(candidate: candidate::Model) -> String { - let now = Utc::now().timestamp(); - let payload = CandidateToken { - iat: now, - exp: now + ONE_WEEK, - application_id: candidate.application, - name: candidate.name.unwrap_or_else(|| "".into()), - surname: candidate.surname.unwrap_or_else(|| "".into()), - }; - - jsonwebtoken::encode( - &Header::default(), - &payload, - &EncodingKey::from_secret(include_bytes!("secret.key")), - ) - .unwrap() -} - -pub fn generate_admin_token(_admin: admin::Model) -> String { - let now = Utc::now().timestamp(); - let payload = AdminToken { - iat: now, - exp: now + ONE_WEEK, - }; - - jsonwebtoken::encode( - &Header::default(), - &payload, - &EncodingKey::from_secret(include_bytes!("secret.key")), - ) - .unwrap() -} - -pub fn decode_token Deserialize<'a>>(token: String) -> Result> { - jsonwebtoken::decode::( - &token, - &DecodingKey::from_secret(include_bytes!("secret.key")), - &Validation::default(), - ) -} - -pub fn decode_candidate_token(token: String) -> Result> { - decode_token(token) -} - -pub fn decode_admin_token(token: String) -> Result> { - decode_token(token) -} - - -#[test] -fn test_encode_decode_token() { - let candidate_model = candidate::Model { - application: 101204, - code: "random_code".to_string(), - birth_surname: None, - birthplace: None, - birthdate: None, - address: None, - telephone: None, - citizenship: None, - sex: None, - study: None, - personal_identification_number: None, - personal_identification_number_hash: None, - public_key: "None".to_owned(), - private_key: "None".to_owned(), - created_at: Utc::now().naive_local(), - updated_at: Utc::now().naive_local(), - name: Some("Uplnej".to_string()), - surname: Some("Magor".to_string()), - email: Some("email.uchazece@centrum.cz".to_string()), - }; - - let jwt = generate_candidate_token(candidate_model.clone()); - - let decoded = decode_candidate_token(jwt).unwrap(); - let token_claims = decoded.claims; - assert_eq!(candidate_model.name.unwrap(), token_claims.name); - assert_eq!(candidate_model.surname.unwrap(), token_claims.surname); -} \ No newline at end of file diff --git a/core/src/token/secret.key b/core/src/token/secret.key deleted file mode 100644 index 3602361..0000000 --- a/core/src/token/secret.key +++ /dev/null @@ -1 +0,0 @@ -temp \ No newline at end of file diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index d9199ce..4fb441e 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -37,6 +37,15 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::session::Entity")] + Session, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Session.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/session.rs b/entity/src/session.rs index 2df872b..3fdde8c 100644 --- a/entity/src/session.rs +++ b/entity/src/session.rs @@ -7,13 +7,28 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, - pub hashed_token: String, pub user_id: i32, + pub ip_address: String, pub created_at: DateTime, - pub updated_at: DateTime, + pub expires_at: DateTime, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::candidate::Entity", + from = "Column::UserId", + to = "super::candidate::Column::Application", + on_update = "Cascade", + on_delete = "Cascade" + )] + Candidate, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Candidate.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/migration/src/lib.rs b/migration/src/lib.rs index adbef73..a8f21b1 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -5,6 +5,7 @@ mod m20221024_121621_create_candidate; mod m20221024_124701_create_parent; mod m20221024_134454_fill_admin; mod m20221025_154422_create_session; +mod m20221027_194728_session_create_user_fk; pub struct Migrator; @@ -17,6 +18,7 @@ impl MigratorTrait for Migrator { Box::new(m20221024_124701_create_parent::Migration), Box::new(m20221024_134454_fill_admin::Migration::default()), Box::new(m20221025_154422_create_session::Migration), + Box::new(m20221027_194728_session_create_user_fk::Migration), ] } } diff --git a/migration/src/m20221024_121621_create_candidate.rs b/migration/src/m20221024_121621_create_candidate.rs index 974a93f..d11282d 100644 --- a/migration/src/m20221024_121621_create_candidate.rs +++ b/migration/src/m20221024_121621_create_candidate.rs @@ -50,7 +50,7 @@ impl MigrationTrait for Migration { /// Learn more at https://docs.rs/sea-query#iden #[derive(Iden)] -enum Candidate { +pub enum Candidate { Table, Application, Code, diff --git a/migration/src/m20221025_154422_create_session.rs b/migration/src/m20221025_154422_create_session.rs index d7931f0..9c084de 100644 --- a/migration/src/m20221025_154422_create_session.rs +++ b/migration/src/m20221025_154422_create_session.rs @@ -18,10 +18,10 @@ impl MigrationTrait for Migration { .unique_key() .primary_key(), ) - .col(ColumnDef::new(Session::HashedToken).string().not_null()) .col(ColumnDef::new(Session::UserId).integer().not_null()) + .col(ColumnDef::new(Session::IpAddress).string().not_null()) .col(ColumnDef::new(Session::CreatedAt).date_time().not_null()) - .col(ColumnDef::new(Session::UpdatedAt).date_time().not_null()) + .col(ColumnDef::new(Session::ExpiresAt).date_time().not_null()) .to_owned(), ) .await @@ -36,11 +36,11 @@ impl MigrationTrait for Migration { /// Learn more at https://docs.rs/sea-query#iden #[derive(Iden)] -enum Session { +pub enum Session { Table, Id, - HashedToken, UserId, + IpAddress, CreatedAt, - UpdatedAt + ExpiresAt, } diff --git a/migration/src/m20221027_194728_session_create_user_fk.rs b/migration/src/m20221027_194728_session_create_user_fk.rs new file mode 100644 index 0000000..248fe2d --- /dev/null +++ b/migration/src/m20221027_194728_session_create_user_fk.rs @@ -0,0 +1,26 @@ +use sea_orm_migration::prelude::*; + +use crate::{m20221025_154422_create_session::Session, m20221024_121621_create_candidate::Candidate}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.create_foreign_key(ForeignKey::create() + .name("user_fk") + .from(Session::Table, Session::UserId) + .to(Candidate::Table, Candidate::Application) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) + .to_owned()).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.drop_foreign_key(ForeignKey::drop() + .name("user_fk") + .table(Session::Table) + .to_owned()).await + } +} \ No newline at end of file