Skip to content

Commit 9564dec

Browse files
committed
refactor(api): [#183] Axum API, user context, renew JWT
1 parent b15616c commit 9564dec

File tree

5 files changed

+94
-7
lines changed

5 files changed

+94
-7
lines changed

src/web/api/v1/contexts/user/handlers.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ pub async fn email_verification_handler(State(app_data): State<Arc<AppData>>, Pa
5959
///
6060
/// # Errors
6161
///
62-
/// It returns an error if the user could not be registered.
62+
/// It returns an error if:
63+
///
64+
/// - Unable to verify the supplied payload as a valid JWT.
65+
/// - The JWT is not invalid or expired.
6366
#[allow(clippy::unused_async)]
6467
pub async fn login_handler(
6568
State(app_data): State<Arc<AppData>>,
@@ -96,6 +99,25 @@ pub async fn verify_token_handler(
9699
}
97100
}
98101

102+
/// It renews the JWT.
103+
///
104+
/// # Errors
105+
///
106+
/// It returns an error if:
107+
///
108+
/// - Unable to parse the supplied payload as a valid JWT.
109+
/// - The JWT is not invalid or expired.
110+
#[allow(clippy::unused_async)]
111+
pub async fn renew_token_handler(
112+
State(app_data): State<Arc<AppData>>,
113+
extract::Json(token): extract::Json<JsonWebToken>,
114+
) -> Result<Json<OkResponse<TokenResponse>>, ServiceError> {
115+
match app_data.authentication_service.renew_token(&token.token).await {
116+
Ok((token, user_compact)) => Ok(responses::renewed_token(token, user_compact)),
117+
Err(error) => Err(error),
118+
}
119+
}
120+
99121
/// It returns the base API URL without the port. For example: `http://localhost`.
100122
fn api_base_url(host: &str) -> String {
101123
// HTTPS is not supported yet.

src/web/api/v1/contexts/user/responses.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub struct TokenResponse {
2727
pub admin: bool,
2828
}
2929

30-
/// Response after successfully log in a user.
30+
/// Response after successfully logging in a user.
3131
pub fn logged_in_user(token: String, user_compact: UserCompact) -> Json<OkResponse<TokenResponse>> {
3232
Json(OkResponse {
3333
data: TokenResponse {
@@ -37,3 +37,14 @@ pub fn logged_in_user(token: String, user_compact: UserCompact) -> Json<OkRespon
3737
},
3838
})
3939
}
40+
41+
/// Response after successfully renewing a JWT.
42+
pub fn renewed_token(token: String, user_compact: UserCompact) -> Json<OkResponse<TokenResponse>> {
43+
Json(OkResponse {
44+
data: TokenResponse {
45+
token,
46+
username: user_compact.username,
47+
admin: user_compact.administrator,
48+
},
49+
})
50+
}

src/web/api/v1/contexts/user/routes.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use std::sync::Arc;
66
use axum::routing::{get, post};
77
use axum::Router;
88

9-
use super::handlers::{email_verification_handler, login_handler, registration_handler, verify_token_handler};
9+
use super::handlers::{
10+
email_verification_handler, login_handler, registration_handler, renew_token_handler, verify_token_handler,
11+
};
1012
use crate::common::AppData;
1113

1214
/// Routes for the [`user`](crate::web::api::v1::contexts::user) API context.
@@ -24,5 +26,6 @@ pub fn router(app_data: Arc<AppData>) -> Router {
2426
)
2527
// Authentication
2628
.route("/login", post(login_handler).with_state(app_data.clone()))
27-
.route("/token/verify", post(verify_token_handler).with_state(app_data))
29+
.route("/token/verify", post(verify_token_handler).with_state(app_data.clone()))
30+
.route("/token/renew", post(renew_token_handler).with_state(app_data))
2831
}

tests/common/contexts/user/asserts.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use super::forms::RegistrationForm;
2+
use super::responses::LoggedInUserData;
23
use crate::common::asserts::assert_json_ok;
3-
use crate::common::contexts::user::responses::{AddedUserResponse, SuccessfulLoginResponse, TokenVerifiedResponse};
4+
use crate::common::contexts::user::responses::{
5+
AddedUserResponse, SuccessfulLoginResponse, TokenRenewalData, TokenRenewalResponse, TokenVerifiedResponse,
6+
};
47
use crate::common::responses::TextResponse;
58

69
pub fn assert_added_user_response(response: &TextResponse) {
@@ -28,3 +31,19 @@ pub fn assert_token_verified_response(response: &TextResponse) {
2831

2932
assert_json_ok(response);
3033
}
34+
35+
pub fn assert_token_renewal_response(response: &TextResponse, logged_in_user: &LoggedInUserData) {
36+
let token_renewal_response: TokenRenewalResponse = serde_json::from_str(&response.body)
37+
.unwrap_or_else(|_| panic!("response {:#?} should be a TokenRenewalResponse", response.body));
38+
39+
assert_eq!(
40+
token_renewal_response.data,
41+
TokenRenewalData {
42+
token: logged_in_user.token.clone(),
43+
username: logged_in_user.username.clone(),
44+
admin: logged_in_user.admin,
45+
}
46+
);
47+
48+
assert_json_ok(response);
49+
}

tests/e2e/contexts/user/contract.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,10 @@ mod with_axum_implementation {
231231
use torrust_index_backend::web::api;
232232

233233
use crate::common::client::Client;
234-
use crate::common::contexts::user::asserts::{assert_successful_login_response, assert_token_verified_response};
235-
use crate::common::contexts::user::forms::{LoginForm, TokenVerificationForm};
234+
use crate::common::contexts::user::asserts::{
235+
assert_successful_login_response, assert_token_renewal_response, assert_token_verified_response,
236+
};
237+
use crate::common::contexts::user::forms::{LoginForm, TokenRenewalForm, TokenVerificationForm};
236238
use crate::e2e::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL;
237239
use crate::e2e::contexts::user::steps::{new_logged_in_user, new_registered_user};
238240
use crate::e2e::environment::TestEnv;
@@ -265,6 +267,12 @@ mod with_axum_implementation {
265267
async fn it_should_allow_a_logged_in_user_to_verify_an_authentication_token() {
266268
let mut env = TestEnv::new();
267269
env.start(api::Implementation::Axum).await;
270+
271+
if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() {
272+
println!("Skipped");
273+
return;
274+
}
275+
268276
let client = Client::unauthenticated(&env.server_socket_addr().unwrap());
269277

270278
let logged_in_user = new_logged_in_user(&env).await;
@@ -277,5 +285,29 @@ mod with_axum_implementation {
277285

278286
assert_token_verified_response(&response);
279287
}
288+
289+
#[tokio::test]
290+
async fn it_should_not_allow_a_logged_in_user_to_renew_an_authentication_token_which_is_still_valid_for_more_than_one_week(
291+
) {
292+
let mut env = TestEnv::new();
293+
env.start(api::Implementation::Axum).await;
294+
295+
if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() {
296+
println!("Skipped");
297+
return;
298+
}
299+
300+
let logged_in_user = new_logged_in_user(&env).await;
301+
302+
let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_user.token);
303+
304+
let response = client
305+
.renew_token(TokenRenewalForm {
306+
token: logged_in_user.token.clone(),
307+
})
308+
.await;
309+
310+
assert_token_renewal_response(&response, &logged_in_user);
311+
}
280312
}
281313
}

0 commit comments

Comments
 (0)