Skip to content

Commit 03dba5c

Browse files
committed
Merge #196: Axum API: user context
d3b5b15 refactor(api): [#183] Axum API, user context, ban user (Jose Celano) 9564dec refactor(api): [#183] Axum API, user context, renew JWT (Jose Celano) b15616c refactor(api): [#183] Axum API, user context, verify JWT (Jose Celano) 3f639b3 refactor(api): [#183] Axum API, user context, login (Jose Celano) a341e38 refactor(api): [#183] Axum API, user context, email verification (Jose Celano) 91522f4 feat: add cargo dependency tower-http (Jose Celano) 79682a5 refactor(api): [#183] Axum API, user context, registration (Jose Celano) Pull request description: API migration to Axum for `user` context. Top commit has no ACKs. Tree-SHA512: efd396039246739428c9b80ff942b8672bcf115a5d4dcac1ef18fb960de1d2dfd8d86bf1d490b6d6b8294f0fd9d3f92b9f89814c781d632a8533074cccca08b0
2 parents 9f8832b + d3b5b15 commit 03dba5c

File tree

32 files changed

+804
-114
lines changed

32 files changed

+804
-114
lines changed

.github/workflows/develop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ jobs:
3131
run: cargo llvm-cov nextest
3232
- name: E2E Tests
3333
run: ./docker/bin/run-e2e-tests.sh
34+
env:
35+
TORRUST_IDX_BACK_E2E_EXCLUDE_AXUM_IMPL: "true"

Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ thiserror = "1.0"
4848
binascii = "0.1"
4949
axum = "0.6.18"
5050
hyper = "0.14.26"
51+
tower-http = { version = "0.4.0", features = ["cors"]}
5152

5253
[dev-dependencies]
5354
rand = "0.8"

src/auth.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ use actix_web::HttpRequest;
55
use crate::errors::ServiceError;
66
use crate::models::user::{UserClaims, UserCompact, UserId};
77
use crate::services::authentication::JsonWebToken;
8+
use crate::web::api::v1::extractors::bearer_token::BearerToken;
9+
10+
// todo: refactor this after finishing migration to Axum.
11+
// - Extract service to handle Json Web Tokens: `new`, `sign_jwt`, `verify_jwt`.
12+
// - Move the rest to `src/web/api/v1/auth.rs`. It's a helper for Axum handlers
13+
// to get user id from request.
814

915
pub struct Authentication {
1016
json_web_token: Arc<JsonWebToken>,
@@ -30,13 +36,25 @@ impl Authentication {
3036
self.json_web_token.verify(token).await
3137
}
3238

33-
/// Get Claims from Request
39+
// Begin ActixWeb
40+
41+
/// Get User id from `ActixWeb` Request
42+
///
43+
/// # Errors
44+
///
45+
/// This function will return an error if it can get claims from the request
46+
pub async fn get_user_id_from_actix_web_request(&self, req: &HttpRequest) -> Result<UserId, ServiceError> {
47+
let claims = self.get_claims_from_actix_web_request(req).await?;
48+
Ok(claims.user.user_id)
49+
}
50+
51+
/// Get Claims from `ActixWeb` Request
3452
///
3553
/// # Errors
3654
///
37-
/// This function will return an `ServiceError::TokenNotFound` if `HeaderValue` is `None`
38-
/// This function will pass through the `ServiceError::TokenInvalid` if unable to verify the JWT.
39-
pub async fn get_claims_from_request(&self, req: &HttpRequest) -> Result<UserClaims, ServiceError> {
55+
/// - Return an `ServiceError::TokenNotFound` if `HeaderValue` is `None`.
56+
/// - Pass through the `ServiceError::TokenInvalid` if unable to verify the JWT.
57+
async fn get_claims_from_actix_web_request(&self, req: &HttpRequest) -> Result<UserClaims, ServiceError> {
4058
match req.headers().get("Authorization") {
4159
Some(auth) => {
4260
let split: Vec<&str> = auth
@@ -55,13 +73,37 @@ impl Authentication {
5573
}
5674
}
5775

58-
/// Get User id from Request
76+
// End ActixWeb
77+
78+
// Begin Axum
79+
80+
/// Get User id from bearer token
5981
///
6082
/// # Errors
6183
///
6284
/// This function will return an error if it can get claims from the request
63-
pub async fn get_user_id_from_request(&self, req: &HttpRequest) -> Result<UserId, ServiceError> {
64-
let claims = self.get_claims_from_request(req).await?;
85+
pub async fn get_user_id_from_bearer_token(&self, maybe_token: &Option<BearerToken>) -> Result<UserId, ServiceError> {
86+
let claims = self.get_claims_from_bearer_token(maybe_token).await?;
6587
Ok(claims.user.user_id)
6688
}
89+
90+
/// Get Claims from bearer token
91+
///
92+
/// # Errors
93+
///
94+
/// This function will:
95+
///
96+
/// - Return an `ServiceError::TokenNotFound` if `HeaderValue` is `None`.
97+
/// - Pass through the `ServiceError::TokenInvalid` if unable to verify the JWT.
98+
async fn get_claims_from_bearer_token(&self, maybe_token: &Option<BearerToken>) -> Result<UserClaims, ServiceError> {
99+
match maybe_token {
100+
Some(token) => match self.verify_jwt(&token.value()).await {
101+
Ok(claims) => Ok(claims),
102+
Err(e) => Err(e),
103+
},
104+
None => Err(ServiceError::TokenNotFound),
105+
}
106+
}
107+
108+
// End Axum
67109
}

src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,12 @@ impl Configuration {
422422

423423
settings_lock.website.name.clone()
424424
}
425+
426+
pub async fn get_api_base_url(&self) -> Option<String> {
427+
let settings_lock = self.settings.read().await;
428+
429+
settings_lock.net.base_url.clone()
430+
}
425431
}
426432

427433
/// The public backend configuration.

src/errors.rs

Lines changed: 69 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -146,49 +146,7 @@ pub struct ErrorToResponse {
146146

147147
impl ResponseError for ServiceError {
148148
fn status_code(&self) -> StatusCode {
149-
#[allow(clippy::match_same_arms)]
150-
match self {
151-
ServiceError::ClosedForRegistration => StatusCode::FORBIDDEN,
152-
ServiceError::EmailInvalid => StatusCode::BAD_REQUEST,
153-
ServiceError::NotAUrl => StatusCode::BAD_REQUEST,
154-
ServiceError::WrongPasswordOrUsername => StatusCode::FORBIDDEN,
155-
ServiceError::UsernameNotFound => StatusCode::NOT_FOUND,
156-
ServiceError::UserNotFound => StatusCode::NOT_FOUND,
157-
ServiceError::AccountNotFound => StatusCode::NOT_FOUND,
158-
ServiceError::ProfanityError => StatusCode::BAD_REQUEST,
159-
ServiceError::BlacklistError => StatusCode::BAD_REQUEST,
160-
ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST,
161-
ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST,
162-
ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST,
163-
ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST,
164-
ServiceError::UsernameTaken => StatusCode::BAD_REQUEST,
165-
ServiceError::UsernameInvalid => StatusCode::BAD_REQUEST,
166-
ServiceError::EmailTaken => StatusCode::BAD_REQUEST,
167-
ServiceError::EmailNotVerified => StatusCode::FORBIDDEN,
168-
ServiceError::TokenNotFound => StatusCode::UNAUTHORIZED,
169-
ServiceError::TokenExpired => StatusCode::UNAUTHORIZED,
170-
ServiceError::TokenInvalid => StatusCode::UNAUTHORIZED,
171-
ServiceError::TorrentNotFound => StatusCode::BAD_REQUEST,
172-
ServiceError::InvalidTorrentFile => StatusCode::BAD_REQUEST,
173-
ServiceError::InvalidTorrentPiecesLength => StatusCode::BAD_REQUEST,
174-
ServiceError::InvalidFileType => StatusCode::BAD_REQUEST,
175-
ServiceError::BadRequest => StatusCode::BAD_REQUEST,
176-
ServiceError::InvalidCategory => StatusCode::BAD_REQUEST,
177-
ServiceError::InvalidTag => StatusCode::BAD_REQUEST,
178-
ServiceError::Unauthorized => StatusCode::FORBIDDEN,
179-
ServiceError::InfoHashAlreadyExists => StatusCode::BAD_REQUEST,
180-
ServiceError::TorrentTitleAlreadyExists => StatusCode::BAD_REQUEST,
181-
ServiceError::TrackerOffline => StatusCode::INTERNAL_SERVER_ERROR,
182-
ServiceError::CategoryAlreadyExists => StatusCode::BAD_REQUEST,
183-
ServiceError::TagAlreadyExists => StatusCode::BAD_REQUEST,
184-
ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
185-
ServiceError::EmailMissing => StatusCode::NOT_FOUND,
186-
ServiceError::FailedToSendVerificationEmail => StatusCode::INTERNAL_SERVER_ERROR,
187-
ServiceError::WhitelistingError => StatusCode::INTERNAL_SERVER_ERROR,
188-
ServiceError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
189-
ServiceError::CategoryNotFound => StatusCode::NOT_FOUND,
190-
ServiceError::TagNotFound => StatusCode::NOT_FOUND,
191-
}
149+
http_status_code_for_service_error(self)
192150
}
193151

194152
fn error_response(&self) -> HttpResponse {
@@ -220,22 +178,7 @@ impl From<sqlx::Error> for ServiceError {
220178

221179
impl From<database::Error> for ServiceError {
222180
fn from(e: database::Error) -> Self {
223-
#[allow(clippy::match_same_arms)]
224-
match e {
225-
database::Error::Error => ServiceError::InternalServerError,
226-
database::Error::ErrorWithText(_) => ServiceError::InternalServerError,
227-
database::Error::UsernameTaken => ServiceError::UsernameTaken,
228-
database::Error::EmailTaken => ServiceError::EmailTaken,
229-
database::Error::UserNotFound => ServiceError::UserNotFound,
230-
database::Error::CategoryAlreadyExists => ServiceError::CategoryAlreadyExists,
231-
database::Error::CategoryNotFound => ServiceError::InvalidCategory,
232-
database::Error::TagAlreadyExists => ServiceError::TagAlreadyExists,
233-
database::Error::TagNotFound => ServiceError::InvalidTag,
234-
database::Error::TorrentNotFound => ServiceError::TorrentNotFound,
235-
database::Error::TorrentAlreadyExists => ServiceError::InfoHashAlreadyExists,
236-
database::Error::TorrentTitleAlreadyExists => ServiceError::TorrentTitleAlreadyExists,
237-
database::Error::UnrecognizedDatabaseDriver => ServiceError::InternalServerError,
238-
}
181+
map_database_error_to_service_error(&e)
239182
}
240183
}
241184

@@ -266,3 +209,70 @@ impl From<serde_json::Error> for ServiceError {
266209
ServiceError::InternalServerError
267210
}
268211
}
212+
213+
#[must_use]
214+
pub fn http_status_code_for_service_error(error: &ServiceError) -> StatusCode {
215+
#[allow(clippy::match_same_arms)]
216+
match error {
217+
ServiceError::ClosedForRegistration => StatusCode::FORBIDDEN,
218+
ServiceError::EmailInvalid => StatusCode::BAD_REQUEST,
219+
ServiceError::NotAUrl => StatusCode::BAD_REQUEST,
220+
ServiceError::WrongPasswordOrUsername => StatusCode::FORBIDDEN,
221+
ServiceError::UsernameNotFound => StatusCode::NOT_FOUND,
222+
ServiceError::UserNotFound => StatusCode::NOT_FOUND,
223+
ServiceError::AccountNotFound => StatusCode::NOT_FOUND,
224+
ServiceError::ProfanityError => StatusCode::BAD_REQUEST,
225+
ServiceError::BlacklistError => StatusCode::BAD_REQUEST,
226+
ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST,
227+
ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST,
228+
ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST,
229+
ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST,
230+
ServiceError::UsernameTaken => StatusCode::BAD_REQUEST,
231+
ServiceError::UsernameInvalid => StatusCode::BAD_REQUEST,
232+
ServiceError::EmailTaken => StatusCode::BAD_REQUEST,
233+
ServiceError::EmailNotVerified => StatusCode::FORBIDDEN,
234+
ServiceError::TokenNotFound => StatusCode::UNAUTHORIZED,
235+
ServiceError::TokenExpired => StatusCode::UNAUTHORIZED,
236+
ServiceError::TokenInvalid => StatusCode::UNAUTHORIZED,
237+
ServiceError::TorrentNotFound => StatusCode::BAD_REQUEST,
238+
ServiceError::InvalidTorrentFile => StatusCode::BAD_REQUEST,
239+
ServiceError::InvalidTorrentPiecesLength => StatusCode::BAD_REQUEST,
240+
ServiceError::InvalidFileType => StatusCode::BAD_REQUEST,
241+
ServiceError::BadRequest => StatusCode::BAD_REQUEST,
242+
ServiceError::InvalidCategory => StatusCode::BAD_REQUEST,
243+
ServiceError::InvalidTag => StatusCode::BAD_REQUEST,
244+
ServiceError::Unauthorized => StatusCode::FORBIDDEN,
245+
ServiceError::InfoHashAlreadyExists => StatusCode::BAD_REQUEST,
246+
ServiceError::TorrentTitleAlreadyExists => StatusCode::BAD_REQUEST,
247+
ServiceError::TrackerOffline => StatusCode::INTERNAL_SERVER_ERROR,
248+
ServiceError::CategoryAlreadyExists => StatusCode::BAD_REQUEST,
249+
ServiceError::TagAlreadyExists => StatusCode::BAD_REQUEST,
250+
ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
251+
ServiceError::EmailMissing => StatusCode::NOT_FOUND,
252+
ServiceError::FailedToSendVerificationEmail => StatusCode::INTERNAL_SERVER_ERROR,
253+
ServiceError::WhitelistingError => StatusCode::INTERNAL_SERVER_ERROR,
254+
ServiceError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
255+
ServiceError::CategoryNotFound => StatusCode::NOT_FOUND,
256+
ServiceError::TagNotFound => StatusCode::NOT_FOUND,
257+
}
258+
}
259+
260+
#[must_use]
261+
pub fn map_database_error_to_service_error(error: &database::Error) -> ServiceError {
262+
#[allow(clippy::match_same_arms)]
263+
match error {
264+
database::Error::Error => ServiceError::InternalServerError,
265+
database::Error::ErrorWithText(_) => ServiceError::InternalServerError,
266+
database::Error::UsernameTaken => ServiceError::UsernameTaken,
267+
database::Error::EmailTaken => ServiceError::EmailTaken,
268+
database::Error::UserNotFound => ServiceError::UserNotFound,
269+
database::Error::CategoryAlreadyExists => ServiceError::CategoryAlreadyExists,
270+
database::Error::CategoryNotFound => ServiceError::InvalidCategory,
271+
database::Error::TagAlreadyExists => ServiceError::TagAlreadyExists,
272+
database::Error::TagNotFound => ServiceError::InvalidTag,
273+
database::Error::TorrentNotFound => ServiceError::TorrentNotFound,
274+
database::Error::TorrentAlreadyExists => ServiceError::InfoHashAlreadyExists,
275+
database::Error::TorrentTitleAlreadyExists => ServiceError::TorrentTitleAlreadyExists,
276+
database::Error::UnrecognizedDatabaseDriver => ServiceError::InternalServerError,
277+
}
278+
}

src/routes/category.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub struct Category {
4141
/// This function will return an error if unable to get user.
4242
/// This function will return an error if unable to insert into the database the new category.
4343
pub async fn add(req: HttpRequest, payload: web::Json<Category>, app_data: WebAppData) -> ServiceResult<impl Responder> {
44-
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
44+
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;
4545

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

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

64-
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
64+
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;
6565

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

src/routes/proxy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub fn init(cfg: &mut web::ServiceConfig) {
2121
///
2222
/// This function will return `Ok` only for now.
2323
pub async fn get_proxy_image(req: HttpRequest, app_data: WebAppData, path: web::Path<String>) -> ServiceResult<impl Responder> {
24-
let user_id = app_data.auth.get_user_id_from_request(&req).await.ok();
24+
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await.ok();
2525

2626
match user_id {
2727
Some(user_id) => {

src/routes/settings.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub fn init(cfg: &mut web::ServiceConfig) {
2525
///
2626
/// This function will return an error if unable to get user from database.
2727
pub async fn get_all_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
28-
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
28+
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;
2929

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

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

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

src/routes/tag.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub struct Create {
4444
/// * Get the compact user from the user id.
4545
/// * Add the new tag to the database.
4646
pub async fn create(req: HttpRequest, payload: web::Json<Create>, app_data: WebAppData) -> ServiceResult<impl Responder> {
47-
let user_id = app_data.auth.get_user_id_from_request(&req).await?;
47+
let user_id = app_data.auth.get_user_id_from_actix_web_request(&req).await?;
4848

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

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

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

0 commit comments

Comments
 (0)