Skip to content

Commit

Permalink
refactor(api): [torrust#183] Axum API, user context, login
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Jun 13, 2023
1 parent a341e38 commit 3f639b3
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub async fn registration_handler(
) -> ServiceResult<impl Responder> {
let conn_info = req.connection_info().clone();
// todo: check if `base_url` option was define in settings `net->base_url`.
// It should have priority over request headers.
// It should have priority over request he
let api_base_url = format!("{}://{}", conn_info.scheme(), conn_info.host());

let _user_id = app_data
Expand Down
10 changes: 10 additions & 0 deletions src/web/api/v1/contexts/user/forms.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
use serde::{Deserialize, Serialize};

// Registration

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RegistrationForm {
pub username: String,
pub email: Option<String>,
pub password: String,
pub confirm_password: String,
}

// Authentication

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LoginForm {
pub login: String, // todo: rename to `username` after finishing Axum API migration.
pub password: String,
}
28 changes: 26 additions & 2 deletions src/web/api/v1/contexts/user/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ use axum::extract::{self, Host, Path, State};
use axum::Json;
use serde::Deserialize;

use super::forms::RegistrationForm;
use super::responses::{self, NewUser};
use super::forms::{LoginForm, RegistrationForm};
use super::responses::{self, NewUser, TokenResponse};
use crate::common::AppData;
use crate::errors::ServiceError;
use crate::web::api::v1::responses::OkResponse;

// Registration

/// It handles the registration of a new user.
///
/// # Errors
Expand Down Expand Up @@ -51,6 +53,28 @@ pub async fn email_verification_handler(State(app_data): State<Arc<AppData>>, Pa
}
}

// Authentication

/// It handles the user login.
///
/// # Errors
///
/// It returns an error if the user could not be registered.
#[allow(clippy::unused_async)]
pub async fn login_handler(
State(app_data): State<Arc<AppData>>,
extract::Json(login_form): extract::Json<LoginForm>,
) -> Result<Json<OkResponse<TokenResponse>>, ServiceError> {
match app_data
.authentication_service
.login(&login_form.login, &login_form.password)
.await
{
Ok((token, user_compact)) => Ok(responses::logged_in_user(token, user_compact)),
Err(error) => Err(error),
}
}

/// It returns the base API URL without the port. For example: `http://localhost`.
fn api_base_url(host: &str) -> String {
// HTTPS is not supported yet.
Expand Down
24 changes: 23 additions & 1 deletion src/web/api/v1/contexts/user/responses.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use axum::Json;
use serde::{Deserialize, Serialize};

use crate::models::user::UserId;
use crate::models::user::{UserCompact, UserId};
use crate::web::api::v1::responses::OkResponse;

// Registration

#[derive(Serialize, Deserialize, Debug)]
pub struct NewUser {
pub user_id: UserId,
Expand All @@ -15,3 +17,23 @@ pub fn added_user(user_id: i64) -> Json<OkResponse<NewUser>> {
data: NewUser { user_id },
})
}

// Authentication

#[derive(Serialize, Deserialize, Debug)]
pub struct TokenResponse {
pub token: String,
pub username: String,
pub admin: bool,
}

/// Response after successfully log in a user.
pub fn logged_in_user(token: String, user_compact: UserCompact) -> Json<OkResponse<TokenResponse>> {
Json(OkResponse {
data: TokenResponse {
token,
username: user_compact.username,
admin: user_compact.administrator,
},
})
}
14 changes: 12 additions & 2 deletions src/web/api/v1/contexts/user/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@ use std::sync::Arc;
use axum::routing::{get, post};
use axum::Router;

use super::handlers::{email_verification_handler, registration_handler};
use super::handlers::{email_verification_handler, login_handler, registration_handler};
use crate::common::AppData;

/// Routes for the [`user`](crate::web::api::v1::contexts::user) API context.
pub fn router(app_data: Arc<AppData>) -> Router {
Router::new()
// Registration
.route("/register", post(registration_handler).with_state(app_data.clone()))
.route("/email/verify/:token", get(email_verification_handler).with_state(app_data))
// code-review: should this be part of the REST API?
// - This endpoint should only verify the email.
// - There should be an independent service (web app) serving the email verification page.
// The wep app can user this endpoint to verify the email and render the page accordingly.
.route(
"/email/verify/:token",
get(email_verification_handler).with_state(app_data.clone()),
)
// Authentication
.route("/login", post(login_handler).with_state(app_data))
}
83 changes: 65 additions & 18 deletions tests/e2e/contexts/user/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,32 +193,79 @@ mod banned_user_list {
}

mod with_axum_implementation {
use std::env;

use torrust_index_backend::web::api;
mod registration {
use std::env;

use crate::common::client::Client;
use crate::common::contexts::user::asserts::assert_added_user_response;
use crate::common::contexts::user::fixtures::random_user_registration_form;
use crate::e2e::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL;
use crate::e2e::environment::TestEnv;
use torrust_index_backend::web::api;

#[tokio::test]
async fn it_should_allow_a_guest_user_to_register() {
let mut env = TestEnv::new();
env.start(api::Implementation::Axum).await;
use crate::common::client::Client;
use crate::common::contexts::user::asserts::assert_added_user_response;
use crate::common::contexts::user::fixtures::random_user_registration_form;
use crate::e2e::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL;
use crate::e2e::environment::TestEnv;

#[tokio::test]
async fn it_should_allow_a_guest_user_to_register() {
let mut env = TestEnv::new();
env.start(api::Implementation::Axum).await;

if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() {
println!("Skipped");
return;
}

let client = Client::unauthenticated(&env.server_socket_addr().unwrap());

let form = random_user_registration_form();

let response = client.register_user(form).await;

if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() {
println!("Skipped");
return;
assert_added_user_response(&response);
}
}

let client = Client::unauthenticated(&env.server_socket_addr().unwrap());
mod authentication {
use std::env;

use torrust_index_backend::web::api;

use crate::common::client::Client;
use crate::common::contexts::user::forms::LoginForm;
use crate::common::contexts::user::responses::SuccessfulLoginResponse;
use crate::e2e::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL;
use crate::e2e::contexts::user::steps::new_registered_user;
use crate::e2e::environment::TestEnv;

#[tokio::test]
async fn it_should_allow_a_registered_user_to_login() {
let mut env = TestEnv::new();
env.start(api::Implementation::Axum).await;

let form = random_user_registration_form();
if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() {
println!("Skipped");
return;
}

let response = client.register_user(form).await;
let client = Client::unauthenticated(&env.server_socket_addr().unwrap());

assert_added_user_response(&response);
let registered_user = new_registered_user(&env).await;

let response = client
.login_user(LoginForm {
login: registered_user.username.clone(),
password: registered_user.password.clone(),
})
.await;

let res: SuccessfulLoginResponse = serde_json::from_str(&response.body).unwrap();
let logged_in_user = res.data;

assert_eq!(logged_in_user.username, registered_user.username);
if let Some(content_type) = &response.content_type {
assert_eq!(content_type, "application/json");
}
assert_eq!(response.status, 200);
}
}
}

0 comments on commit 3f639b3

Please sign in to comment.