diff --git a/src/callers/login.rs b/src/callers/login.rs index 443c2ae..2b28ac9 100644 --- a/src/callers/login.rs +++ b/src/callers/login.rs @@ -32,6 +32,11 @@ pub mod request { } } } + + #[derive(Deserialize, utoipa::ToSchema)] + pub struct RefreshTokenRequest { + pub access_token: String, + } } pub mod response { @@ -57,6 +62,12 @@ pub mod response { let _parsed_body: LoginResponse = serde_json::from_slice(&body).unwrap(); todo!("Add code to convert axum::Response to this type"); } + + #[derive(Deserialize, utoipa::ToSchema)] + pub struct RefreshTokenResponse { + pub message: String, + pub data: Vec, + } } /// Endpoint for a user login @@ -337,3 +348,272 @@ pub async fn service_user_login( } } } + +/// Endpoint for service user login +#[utoipa::path( + post, + path = super::endpoints::REFRESH_TOKEN, + request_body( + content = request::RefreshTokenRequest, + description = "Data required refresh token", + content_type = "application/json" + ), + responses( + (status = 201, description = "Service uuser login successful", body = response::RefreshTokenResponse), + (status = 400, description = "Bad data", body = response::RefreshTokenResponse), + (status = 500, description = "Something went wrong", body = response::RefreshTokenResponse) + ) +)] +pub async fn refresh_token( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, +) -> ( + axum::http::StatusCode, + axum::Json, +) { + if payload.access_token.is_empty() { + let reason = String::from("Access token not provided"); + + ( + axum::http::StatusCode::BAD_REQUEST, + axum::Json(response::RefreshTokenResponse { + message: reason, + data: Vec::new(), + }), + ) + } else { + let key = textsender_models::envy::environment::get_secret_key() + .await + .value; + if token_stuff::verify_token(&key, &payload.access_token) { + match token_stuff::extract_id_from_token(&key, &payload.access_token) { + Ok(id) => { + let generate_service_token = + |id, key| -> (Option, Option) { + match token_stuff::create_service_refresh_token(key, id) { + Ok((token, issued)) => (Some(token), Some(issued)), + Err(_err) => (None, None), + } + }; + + let mut response = response::RefreshTokenResponse { + message: String::new(), + data: Vec::new(), + }; + + match repo::user::get_with_id(&pool, &id).await { + Ok(_user) => { + let (refresh_token, issued) = generate_service_token(&id, &key); + match refresh_token { + Some(token) => match issued { + Some(issued_at) => { + response.message = String::from( + super::messages::SUCCESSFUL_MESSAGE, + ); + let lr = textsender_models::token::LoginResult { + user_id: id, + access_token: token, + issued_at, + token_type: String::from( + textsender_models::token::TOKEN_TYPE, + ), + ..Default::default() + }; + response.data.push(lr); + (axum::http::StatusCode::OK, axum::Json(response)) + } + None => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::RefreshTokenResponse { + message: String::from("Issued at not returned"), + data: Vec::new(), + }), + ), + }, + None => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::RefreshTokenResponse { + message: String::from( + "Refresh token not generated", + ), + data: Vec::new(), + }), + ), + } + } + Err(err) => { + println!("Unable to find user, checking service user: {err:?}"); + match repo::service::get(&pool, &id).await { + Ok(_service_user) => { + let (refresh_token, issued) = + generate_service_token(&id, &key); + match refresh_token { + Some(token) => { + match issued { + Some(issued_at) => { + response.message = String::from(super::messages::SUCCESSFUL_MESSAGE); + let lr = textsender_models::token::LoginResult { + user_id: id, + access_token: token, + issued_at, + token_type: String::from( + textsender_models::token::TOKEN_TYPE, + ), + ..Default::default() + }; + response.data.push(lr); + (axum::http::StatusCode::OK, + axum::Json(response)) + } + None => { + (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response::RefreshTokenResponse { + message: String::from("Issued at not returned"), + data: Vec::new() + })) + } + } + } + None => { + (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response::RefreshTokenResponse { + message: String::from("Refresh token not generated"), + data: Vec::new() + })) + } + } + } + Err(err) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::RefreshTokenResponse { + message: err.to_string(), + data: Vec::new(), + }), + ) + } + } + } + } + Err(err) => { + (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response::RefreshTokenResponse { + message: err.to_string(), + data: Vec::new() + })) + } + } + } else { + (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response::RefreshTokenResponse { + message: String::from("Unable to verify token"), + data: Vec::new() + })) + } + /* + match repo::service::exists(&pool, &payload.username).await { + Ok(exists) => { + if !exists { + println!("User does not exists"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::ServiceUserLoginResponse { + message: String::from("Unable to login"), + data: Vec::new(), + }), + ) + } else { + println!("Good to create"); + let service_user = + match repo::service::get_with_username(&pool, &payload.username).await { + Ok(service_user) => service_user, + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::ServiceUserLoginResponse { + message: String::from("Unable to login"), + data: Vec::new(), + }), + ); + } + }; + println!("Service user: {service_user:?}"); + println!("Payload: {:?}", payload.passphrase); + let hashed_password = service_user.passphrase.clone(); + println!("Hash password: {hashed_password:?}"); + + match hashing::verify_password(&payload.passphrase, hashed_password) { + Ok(matches) => { + if matches { + // Create token + println!("Creating token"); + let key = textsender_models::envy::environment::get_secret_key() + .await + .value; + let (token_literal, duration) = + token_stuff::create_token(&key, &service_user.id).unwrap(); + + if token_stuff::verify_token(&key, &token_literal) { + let current_time = time::OffsetDateTime::now_utc(); + let _ = repo::service::update_last_login( + &pool, + &service_user, + ¤t_time, + ) + .await; + + ( + axum::http::StatusCode::OK, + axum::Json(response::ServiceUserLoginResponse { + message: String::from( + super::messages::SUCCESSFUL_MESSAGE, + ), + data: vec![textsender_models::token::LoginResult { + user_id: service_user.id, + access_token: token_literal, + token_type: String::from( + textsender_models::token::TOKEN_TYPE, + ), + issued_at: duration, + ..Default::default() + }], + }), + ) + } else { + eprintln!("Invalid token"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::ServiceUserLoginResponse { + message: String::from("Invalid attempt"), + data: Vec::new(), + }), + ) + } + } else { + eprintln!("No match"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::ServiceUserLoginResponse { + message: String::from("Invalid attempt"), + data: Vec::new(), + }), + ) + } + } + Err(err) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::ServiceUserLoginResponse { + message: err.to_string(), + data: Vec::new(), + }), + ), + } + } + } + Err(err) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::ServiceUserLoginResponse { + message: err.to_string(), + data: Vec::new(), + }), + ), + } + */ + } +} diff --git a/src/lib.rs b/src/lib.rs index af64ea5..0a92e92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,6 +106,10 @@ pub mod init { callers::endpoints::LOGIN_SERVICE_USER, post(callers::login::service_user_login), ) + .route( + callers::endpoints::REFRESH_TOKEN, + post(callers::login::refresh_token), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 1b51e54..f7aeaad 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -41,6 +41,36 @@ pub mod user { } } + pub async fn get_with_id( + pool: &sqlx::PgPool, + id: &uuid::Uuid, + ) -> Result { + match sqlx::query( + r#" + SELECT id, username, password, phone_number, salt_id, firstname, lastname, created, last_login FROM "user" WHERE id = $1 + "#, + ) + .bind(id) + .fetch_optional(pool) + .await { + Ok(r) => match r { + Some(r) => Ok(textsender_models::user::User { + id: r.try_get("id")?, + username: r.try_get("username")?, + password: r.try_get("password")?, + phone_number: r.try_get("phone_number")?, + salt_id: r.try_get("salt_id")?, + firstname: r.try_get("firstname")?, + lastname: r.try_get("lastname")?, + created: r.try_get("created")?, + last_login: r.try_get("last_login")?, + }), + None => Err(sqlx::Error::RowNotFound), + }, + Err(e) => Err(e), + } + } + pub async fn update_last_login( pool: &sqlx::PgPool, user: &textsender_models::user::User, diff --git a/src/repo/service.rs b/src/repo/service.rs index 5617d98..078751e 100644 --- a/src/repo/service.rs +++ b/src/repo/service.rs @@ -49,6 +49,42 @@ pub async fn get_passphrase( } } +pub async fn get( + pool: &sqlx::PgPool, + id: &uuid::Uuid, +) -> Result { + match sqlx::query( + r#"SELECT id, username, passphrase, created, last_login, salt_id FROM "service_user" WHERE id = $1"# + ).bind(id) + .fetch_one(pool).await { + Ok(row) => { + let last_login: Option = match row.try_get("last_login") { + Ok(login) => { + Some(login) + } + Err(err) => { + eprintln!("Error: {err:?}"); + None + } + }; + + let service_user = textsender_models::user::ServiceUser { + id: row.try_get("id")?, + username: row.try_get("username")?, + passphrase: row.try_get("passphrase")?, + created: row.try_get("created")?, + last_login, + salt_id: row.try_get("salt_id")?, + }; + + Ok(service_user) + } + Err(err) => { + Err(err) + } + } +} + pub async fn get_with_username( pool: &sqlx::PgPool, username: &String,