Skip to content

Commit

Permalink
refactor(api): [#183] Axum API, user context, ban user
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Jun 14, 2023
1 parent 9564dec commit d3b5b15
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 24 deletions.
56 changes: 49 additions & 7 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ use actix_web::HttpRequest;
use crate::errors::ServiceError;
use crate::models::user::{UserClaims, UserCompact, UserId};
use crate::services::authentication::JsonWebToken;
use crate::web::api::v1::extractors::bearer_token::BearerToken;

// todo: refactor this after finishing migration to Axum.
// - Extract service to handle Json Web Tokens: `new`, `sign_jwt`, `verify_jwt`.
// - Move the rest to `src/web/api/v1/auth.rs`. It's a helper for Axum handlers
// to get user id from request.

pub struct Authentication {
json_web_token: Arc<JsonWebToken>,
Expand All @@ -30,13 +36,25 @@ impl Authentication {
self.json_web_token.verify(token).await
}

/// Get Claims from Request
// Begin ActixWeb

/// Get User id from `ActixWeb` Request
///
/// # Errors
///
/// This function will return an error if it can get claims from the request
pub async fn get_user_id_from_actix_web_request(&self, req: &HttpRequest) -> Result<UserId, ServiceError> {
let claims = self.get_claims_from_actix_web_request(req).await?;
Ok(claims.user.user_id)
}

/// Get Claims from `ActixWeb` Request
///
/// # Errors
///
/// This function will return an `ServiceError::TokenNotFound` if `HeaderValue` is `None`
/// This function will pass through the `ServiceError::TokenInvalid` if unable to verify the JWT.
pub async fn get_claims_from_request(&self, req: &HttpRequest) -> Result<UserClaims, ServiceError> {
/// - Return an `ServiceError::TokenNotFound` if `HeaderValue` is `None`.
/// - Pass through the `ServiceError::TokenInvalid` if unable to verify the JWT.
async fn get_claims_from_actix_web_request(&self, req: &HttpRequest) -> Result<UserClaims, ServiceError> {
match req.headers().get("Authorization") {
Some(auth) => {
let split: Vec<&str> = auth
Expand All @@ -55,13 +73,37 @@ impl Authentication {
}
}

/// Get User id from Request
// End ActixWeb

// Begin Axum

/// Get User id from bearer token
///
/// # Errors
///
/// This function will return an error if it can get claims from the request
pub async fn get_user_id_from_request(&self, req: &HttpRequest) -> Result<UserId, ServiceError> {
let claims = self.get_claims_from_request(req).await?;
pub async fn get_user_id_from_bearer_token(&self, maybe_token: &Option<BearerToken>) -> Result<UserId, ServiceError> {
let claims = self.get_claims_from_bearer_token(maybe_token).await?;
Ok(claims.user.user_id)
}

/// Get Claims from bearer token
///
/// # Errors
///
/// This function will:
///
/// - Return an `ServiceError::TokenNotFound` if `HeaderValue` is `None`.
/// - Pass through the `ServiceError::TokenInvalid` if unable to verify the JWT.
async fn get_claims_from_bearer_token(&self, maybe_token: &Option<BearerToken>) -> Result<UserClaims, ServiceError> {
match maybe_token {
Some(token) => match self.verify_jwt(&token.value()).await {
Ok(claims) => Ok(claims),
Err(e) => Err(e),
},
None => Err(ServiceError::TokenNotFound),
}
}

// End Axum
}
4 changes: 2 additions & 2 deletions src/routes/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct Category {
/// This function will return an error if unable to get user.
/// This function will return an error if unable to insert into the database the new category.
pub async fn add(req: HttpRequest, payload: web::Json<Category>, app_data: WebAppData) -> ServiceResult<impl Responder> {
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

let _category_id = app_data.category_service.add_category(&payload.name, &user_id).await?;

Expand All @@ -61,7 +61,7 @@ pub async fn delete(req: HttpRequest, payload: web::Json<Category>, app_data: We
// And we should use the ID instead of the name, because the name could change
// or we could add support for multiple languages.

let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

app_data.category_service.delete_category(&payload.name, &user_id).await?;

Expand Down
2 changes: 1 addition & 1 deletion src/routes/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub fn init(cfg: &mut web::ServiceConfig) {
///
/// This function will return `Ok` only for now.
pub async fn get_proxy_image(req: HttpRequest, app_data: WebAppData, path: web::Path<String>) -> ServiceResult<impl Responder> {
let user_id = app_data.auth.get_user_id_from_request(&req).await.ok();
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await.ok();

match user_id {
Some(user_id) => {
Expand Down
4 changes: 2 additions & 2 deletions src/routes/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub fn init(cfg: &mut web::ServiceConfig) {
///
/// This function will return an error if unable to get user from database.
pub async fn get_all_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

let all_settings = app_data.settings_service.get_all(&user_id).await?;

Expand All @@ -46,7 +46,7 @@ pub async fn update_handler(
payload: web::Json<config::TorrustBackend>,
app_data: WebAppData,
) -> ServiceResult<impl Responder> {
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

let new_settings = app_data.settings_service.update_all(payload.into_inner(), &user_id).await?;

Expand Down
4 changes: 2 additions & 2 deletions src/routes/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub struct Create {
/// * Get the compact user from the user id.
/// * Add the new tag to the database.
pub async fn create(req: HttpRequest, payload: web::Json<Create>, app_data: WebAppData) -> ServiceResult<impl Responder> {
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

app_data.tag_service.add_tag(&payload.name, &user_id).await?;

Expand All @@ -68,7 +68,7 @@ pub struct Delete {
/// * Get the compact user from the user id.
/// * Delete the tag from the database.
pub async fn delete(req: HttpRequest, payload: web::Json<Delete>, app_data: WebAppData) -> ServiceResult<impl Responder> {
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

app_data.tag_service.delete_tag(&payload.tag_id, &user_id).await?;

Expand Down
10 changes: 5 additions & 5 deletions src/routes/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub struct Update {
/// This function will return an error if there was a problem uploading the
/// torrent.
pub async fn upload_torrent_handler(req: HttpRequest, payload: Multipart, app_data: WebAppData) -> ServiceResult<impl Responder> {
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

let torrent_request = get_torrent_request_from_payload(payload).await?;

Expand All @@ -99,7 +99,7 @@ pub async fn upload_torrent_handler(req: HttpRequest, payload: Multipart, app_da
/// Returns `ServiceError::BadRequest` if the torrent info-hash is invalid.
pub async fn download_torrent_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
let info_hash = get_torrent_info_hash_from_request(&req)?;
let user_id = app_data.auth.get_user_id_from_request(&req).await.ok();
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await.ok();

let torrent = app_data.torrent_service.get_torrent(&info_hash, user_id).await?;

Expand All @@ -115,7 +115,7 @@ pub async fn download_torrent_handler(req: HttpRequest, app_data: WebAppData) ->
/// This function will return an error if unable to get torrent info.
pub async fn get_torrent_info_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
let info_hash = get_torrent_info_hash_from_request(&req)?;
let user_id = app_data.auth.get_user_id_from_request(&req).await.ok();
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await.ok();

let torrent_response = app_data.torrent_service.get_torrent_info(&info_hash, user_id).await?;

Expand All @@ -137,7 +137,7 @@ pub async fn update_torrent_info_handler(
app_data: WebAppData,
) -> ServiceResult<impl Responder> {
let info_hash = get_torrent_info_hash_from_request(&req)?;
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

let torrent_response = app_data
.torrent_service
Expand All @@ -158,7 +158,7 @@ pub async fn update_torrent_info_handler(
/// * Delete the torrent.
pub async fn delete_torrent_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
let info_hash = get_torrent_info_hash_from_request(&req)?;
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;

let deleted_torrent_response = app_data.torrent_service.delete_torrent(&info_hash, &user_id).await?;

Expand Down
2 changes: 1 addition & 1 deletion src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ pub async fn email_verification_handler(req: HttpRequest, app_data: WebAppData)
///
/// This function will return if the user could not be banned.
pub async fn ban_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;
let to_be_banned_username = req.match_info().get("user").ok_or(ServiceError::InternalServerError)?;

app_data.ban_service.ban_user(to_be_banned_username, &user_id).await?;
Expand Down
13 changes: 13 additions & 0 deletions src/web/api/v1/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,16 @@
//! "data": "new category"
//! }
//! ```

use hyper::http::HeaderValue;

/// Parses the token from the `Authorization` header.
pub fn parse_token(authorization: &HeaderValue) -> String {
let split: Vec<&str> = authorization
.to_str()
.expect("variable `auth` contains data that is not visible ASCII chars.")
.split("Bearer")
.collect();
let token = split[1].trim();
token.to_string()
}
30 changes: 30 additions & 0 deletions src/web/api/v1/contexts/user/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use super::forms::{JsonWebToken, LoginForm, RegistrationForm};
use super::responses::{self, NewUser, TokenResponse};
use crate::common::AppData;
use crate::errors::ServiceError;
use crate::web::api::v1::extractors::bearer_token::Extract;
use crate::web::api::v1::responses::OkResponse;

// Registration
Expand Down Expand Up @@ -99,6 +100,9 @@ pub async fn verify_token_handler(
}
}

#[derive(Deserialize)]
pub struct UsernameParam(pub String);

/// It renews the JWT.
///
/// # Errors
Expand All @@ -118,6 +122,32 @@ pub async fn renew_token_handler(
}
}

/// It bans a user from the index.
///
/// # Errors
///
/// This function will return if:
///
/// - The JWT provided by the banning authority was not valid.
/// - The user could not be banned: it does not exist, etcetera.
#[allow(clippy::unused_async)]
pub async fn ban_handler(
State(app_data): State<Arc<AppData>>,
Path(to_be_banned_username): Path<UsernameParam>,
Extract(maybe_bearer_token): Extract,
) -> Result<Json<OkResponse<String>>, ServiceError> {
// todo: add reason and `date_expiry` parameters to request

let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

match app_data.ban_service.ban_user(&to_be_banned_username.0, &user_id).await {
Ok(_) => Ok(axum::Json(OkResponse {
data: format!("Banned user: {}", to_be_banned_username.0),
})),
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
9 changes: 6 additions & 3 deletions src/web/api/v1/contexts/user/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::user).
use std::sync::Arc;

use axum::routing::{get, post};
use axum::routing::{delete, get, post};
use axum::Router;

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

Expand All @@ -27,5 +27,8 @@ pub fn router(app_data: Arc<AppData>) -> Router {
// Authentication
.route("/login", post(login_handler).with_state(app_data.clone()))
.route("/token/verify", post(verify_token_handler).with_state(app_data.clone()))
.route("/token/renew", post(renew_token_handler).with_state(app_data))
.route("/token/renew", post(renew_token_handler).with_state(app_data.clone()))
// User ban
// code-review: should not this be a POST method? We add the user to the blacklist. We do not delete the user.
.route("/ban/:user", delete(ban_handler).with_state(app_data))
}
36 changes: 36 additions & 0 deletions src/web/api/v1/extractors/bearer_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use axum::async_trait;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::response::Response;
use serde::Deserialize;

use crate::web::api::v1::auth::parse_token;

pub struct Extract(pub Option<BearerToken>);

#[derive(Deserialize, Debug)]
pub struct BearerToken(String);

impl BearerToken {
#[must_use]
pub fn value(&self) -> String {
self.0.clone()
}
}

#[async_trait]
impl<S> FromRequestParts<S> for Extract
where
S: Send + Sync,
{
type Rejection = Response;

async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let header = parts.headers.get("Authorization");

match header {
Some(header_value) => Ok(Extract(Some(BearerToken(parse_token(header_value))))),
None => Ok(Extract(None)),
}
}
}
1 change: 1 addition & 0 deletions src/web/api/v1/extractors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod bearer_token;
1 change: 1 addition & 0 deletions src/web/api/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
//! information.
pub mod auth;
pub mod contexts;
pub mod extractors;
pub mod responses;
pub mod routes;
14 changes: 13 additions & 1 deletion tests/common/contexts/user/asserts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::forms::RegistrationForm;
use super::responses::LoggedInUserData;
use crate::common::asserts::assert_json_ok;
use crate::common::contexts::user::responses::{
AddedUserResponse, SuccessfulLoginResponse, TokenRenewalData, TokenRenewalResponse, TokenVerifiedResponse,
AddedUserResponse, BannedUserResponse, SuccessfulLoginResponse, TokenRenewalData, TokenRenewalResponse, TokenVerifiedResponse,
};
use crate::common::responses::TextResponse;

Expand Down Expand Up @@ -47,3 +47,15 @@ pub fn assert_token_renewal_response(response: &TextResponse, logged_in_user: &L

assert_json_ok(response);
}

pub fn assert_banned_user_response(response: &TextResponse, registered_user: &RegistrationForm) {
let banned_user_response: BannedUserResponse = serde_json::from_str(&response.body)
.unwrap_or_else(|_| panic!("response {:#?} should be a BannedUserResponse", response.body));

assert_eq!(
banned_user_response.data,
format!("Banned user: {}", registered_user.username)
);

assert_json_ok(response);
}
Loading

0 comments on commit d3b5b15

Please sign in to comment.