* build(deps): bump axum from 0.7.9 to 0.8.8 Bumps [axum](https://github.com/tokio-rs/axum) from 0.7.9 to 0.8.8. - [Release notes](https://github.com/tokio-rs/axum/releases) - [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.7.9...axum-v0.8.8) --- updated-dependencies: - dependency-name: axum dependency-version: 0.8.8 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * fix(deps): adapt code for axum 0.8 and tower-cookies 0.11 - Bump tower-cookies 0.10 → 0.11 (requires axum-core 0.5 / axum 0.8) - Remove axum::async_trait usage (axum 0.8 uses native async traits) - Migrate route path params from :param to {param} syntax * fix(deps): correct axum and tower version constraints Dependabot updated the lockfile for axum 0.8 but left the Cargo.toml constraint at "0.7", causing CI to resolve back to 0.7.9. It also incorrectly downgraded tower from "0.5" to "0.4", creating a duplicate tower version in the lockfile and trait mismatches with axum 0.8. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Seager <jon@sgrs.uk>
133 lines
4.1 KiB
Rust
133 lines
4.1 KiB
Rust
use axum::{
|
|
extract::{FromRequestParts, Request},
|
|
http::{StatusCode, header, request::Parts},
|
|
};
|
|
use tower_cookies::Cookies;
|
|
use tracing::warn;
|
|
|
|
use crate::application::state::AppState;
|
|
use crate::domain::users::User;
|
|
use crate::infrastructure::auth::hash_token;
|
|
|
|
const SESSION_COOKIE_NAME: &str = "brewlog_session";
|
|
|
|
/// Extension type to carry authenticated user through request handlers
|
|
#[derive(Debug, Clone)]
|
|
pub struct AuthenticatedUser(pub User);
|
|
|
|
impl FromRequestParts<AppState> for AuthenticatedUser {
|
|
type Rejection = StatusCode;
|
|
|
|
async fn from_request_parts(
|
|
parts: &mut Parts,
|
|
state: &AppState,
|
|
) -> Result<Self, Self::Rejection> {
|
|
// Try to get from extensions first (if middleware already set it)
|
|
if let Some(user) = parts.extensions.get::<AuthenticatedUser>() {
|
|
return Ok(user.clone());
|
|
}
|
|
|
|
// Try to authenticate via session cookie first
|
|
if let Ok(cookies) = Cookies::from_request_parts(parts, state).await
|
|
&& let Some(user) = authenticate_via_session(state, &cookies).await
|
|
{
|
|
return Ok(AuthenticatedUser(user));
|
|
}
|
|
|
|
// Fall back to Bearer token authentication
|
|
let auth_header = parts
|
|
.headers
|
|
.get(header::AUTHORIZATION)
|
|
.ok_or(StatusCode::UNAUTHORIZED)?;
|
|
|
|
let auth_str = auth_header.to_str().map_err(|err| {
|
|
warn!(error = %err, "authorization header contains invalid characters");
|
|
StatusCode::UNAUTHORIZED
|
|
})?;
|
|
|
|
// Check for "Bearer <token>" format
|
|
let token = auth_str
|
|
.strip_prefix("Bearer ")
|
|
.ok_or(StatusCode::UNAUTHORIZED)?;
|
|
|
|
// Hash the token to look it up in the database
|
|
let token_hash = hash_token(token);
|
|
|
|
// Look up the token
|
|
let token_record = state
|
|
.token_repo
|
|
.get_by_token_hash(&token_hash)
|
|
.await
|
|
.map_err(|err| {
|
|
warn!(error = %err, "bearer token lookup failed");
|
|
StatusCode::UNAUTHORIZED
|
|
})?;
|
|
|
|
// Check if token is revoked
|
|
if token_record.is_revoked() {
|
|
return Err(StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
// Update last used timestamp (fire and forget)
|
|
let token_repo = state.token_repo.clone();
|
|
let token_id = token_record.id;
|
|
tokio::spawn(async move {
|
|
if let Err(err) = token_repo.update_last_used(token_id).await {
|
|
warn!(error = %err, %token_id, "failed to update token last_used");
|
|
}
|
|
});
|
|
|
|
// Get the user
|
|
let user = state
|
|
.user_repo
|
|
.get(token_record.user_id)
|
|
.await
|
|
.map_err(|err| {
|
|
warn!(error = %err, user_id = %token_record.user_id, "user lookup failed for valid token");
|
|
StatusCode::UNAUTHORIZED
|
|
})?;
|
|
|
|
Ok(AuthenticatedUser(user))
|
|
}
|
|
}
|
|
|
|
/// Authenticate via session cookie
|
|
async fn authenticate_via_session(state: &AppState, cookies: &Cookies) -> Option<User> {
|
|
let cookie = cookies.get(SESSION_COOKIE_NAME)?;
|
|
let session_token = cookie.value();
|
|
let session_token_hash = hash_token(session_token);
|
|
|
|
// Check if session exists and is valid
|
|
let session = match state
|
|
.session_repo
|
|
.get_by_token_hash(&session_token_hash)
|
|
.await
|
|
{
|
|
Ok(s) => s,
|
|
Err(err) => {
|
|
warn!(error = %err, "session lookup failed during authentication");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
if session.is_expired() {
|
|
return None;
|
|
}
|
|
|
|
// Get the user
|
|
match state.user_repo.get(session.user_id).await {
|
|
Ok(user) => Some(user),
|
|
Err(err) => {
|
|
warn!(error = %err, user_id = %session.user_id, "user lookup failed for valid session");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper to extract authenticated user from request extensions
|
|
pub fn get_authenticated_user(request: &Request) -> Option<&User> {
|
|
request
|
|
.extensions()
|
|
.get::<AuthenticatedUser>()
|
|
.map(|auth| &auth.0)
|
|
}
|