Include require auth middleware and login route

This commit is contained in:
GW_MC
2025-12-18 18:25:57 +08:00
parent b0c11c7c67
commit ccd8bc7aa1
15 changed files with 326 additions and 39 deletions

View File

@@ -1,13 +1,21 @@
mod auth;
mod health;
mod openapi;
mod restricted;
use std::sync::Arc;
use crate::routes::AppState;
pub use self::openapi::ApiDoc;
use axum::{Router, response::IntoResponse, routing::any};
pub fn get_api_router() -> Router {
pub fn get_api_router(state: Arc<AppState>) -> Router {
Router::new()
.nest("/health", health::get_health_router())
.merge(auth::get_basic_auth_router(state.clone()))
.merge(restricted::get_restricted_router(state.clone()))
// explicit fallback for unmatched API routes
.route("/{*wildcard}", any(api_fallback_handler))
}

View File

@@ -0,0 +1,16 @@
pub mod login;
use std::sync::Arc;
use axum::{
Router,
routing::{get, post},
};
use crate::routes::AppState;
pub fn get_basic_auth_router(state: Arc<AppState>) -> Router {
Router::new()
.route("/login", post(login::login))
.with_state(state)
}

View File

@@ -0,0 +1,98 @@
use std::sync::Arc;
use axum::{
Json,
body::Body,
extract::State,
http::{StatusCode, header::SET_COOKIE},
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, from_value};
use tracing::{error, warn};
use crate::routes::{AppState, api::openapi::tag::AUTH_TAG};
/// Login request payload
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct LoginRequest {
username: String,
password: String,
}
/// Login endpoint
///
/// Authenticates a user and returns a JWT in an HttpOnly cookie.
#[utoipa::path(
post,
path = "/api/auth/login",
request_body = LoginRequest,
responses(
(status = 200, description = "User authenticated successfully", body = ()),
(status = 401, description = "Authentication failed"),
(status = 500, description = "Internal server error"),
),
tag = AUTH_TAG,
)]
pub async fn login(State(state): State<Arc<AppState>>, Json(payload): Json<Value>) -> Response {
let login_request: LoginRequest = match from_value(payload) {
Ok(req) => req,
Err(e) => {
warn!("Invalid login request: {}", e);
return (StatusCode::BAD_REQUEST).into_response();
}
};
let user_id = match state
.service
.auth_state
.strategy
.password
.authenticate(&login_request.username, &login_request.password, None)
.await
{
Ok(user_id) => user_id,
Err(e) => {
warn!(
"Authentication failed for user {}: {}",
login_request.username, e
);
return (StatusCode::UNAUTHORIZED).into_response();
}
};
let (jwt, claims) = match state
.service
.auth_state
.authentication
.generate_jwt(user_id, 3600)
.await
{
Ok(token) => token,
Err(e) => {
error!("Error generating JWT for user {}: {}", user_id, e);
return (StatusCode::INTERNAL_SERVER_ERROR).into_response();
}
};
let response_builder = Response::builder()
.status(StatusCode::OK)
// add jwt as cookie
.header(
SET_COOKIE,
format!(
"token={}; HttpOnly; Path=/; Max-Age={}; SameSite=Strict;",
jwt,
claims.exp - claims.iat
),
)
.body(Body::from(()));
match response_builder {
Ok(resp) => resp,
Err(e) => {
error!("Error building response: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR).into_response();
}
}
}

View File

@@ -1,18 +1,22 @@
pub mod tag {
/// Health tag constant
pub const HEALTH_TAG: &str = "Health";
pub const AUTH_TAG: &str = "Authentication";
}
#[derive(utoipa::OpenApi)]
#[openapi(
paths(
crate::routes::api::health::info::get_health_info
crate::routes::api::health::info::get_health_info,
crate::routes::api::auth::login::login
),
components(
schemas(crate::routes::api::health::info::HealthInfo) // Register any schemas used in your paths
schemas(crate::routes::api::health::info::HealthInfo), // Register any schemas used in your paths
schemas(crate::routes::api::auth::login::LoginRequest)
),
tags(
(name = tag::HEALTH_TAG, description = "Health information API")
(name = tag::HEALTH_TAG, description = "Health information API"),
(name = tag::AUTH_TAG, description = "Authentication API")
)
)]
pub struct ApiDoc;

View File

@@ -0,0 +1,15 @@
use std::sync::Arc;
use axum::{Router, routing::get};
use crate::{middlewares::require_auth::require_auth, routes::AppState};
pub fn get_restricted_router(state: Arc<AppState>) -> Router {
Router::new()
//
//
.layer(axum::middleware::from_fn_with_state(
state.clone(),
require_auth,
))
}