This commit is contained in:
2026-05-23 02:17:37 +02:00
parent fed75d4820
commit 4f0cbe9145
9 changed files with 247 additions and 63 deletions
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts" setup>
</script>
<template>
</template>
<style scoped>
</style>
+28
View File
@@ -0,0 +1,28 @@
import {useAuthStore} from '@/stores/auth'
export async function apiFetch(endpoint: string, options: RequestInit = {}) {
const authStore = useAuthStore()
const baseUrl = '/api' // Votre préfixe configuré dans mod.rs
const headers = new Headers(options.headers)
headers.set('Content-Type', 'application/json')
// On injecte le token s'il existe
if (authStore.token) {
headers.set('Authorization', `Bearer ${authStore.token}`)
}
const response = await fetch(`${baseUrl}${endpoint}`, {
...options,
headers,
})
// Gestion automatique de l'expiration du token (401 Unauthorized)
if (response.status === 401) {
authStore.logout()
// Optionnel : rediriger vers /login
window.location.href = '/login'
}
return response
}
+33
View File
@@ -8,14 +8,47 @@
import {createRouter, createWebHistory} from 'vue-router'
import Index from '@/pages/index.vue'
import {useAuthStore} from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: () => import('@/pages/login.vue'),
},
{
path: '/',
component: Index,
},
{
path: '/admin',
name: 'admin-dashboard',
component: () => import('@/pages/admin/Dashboard.vue'),
meta: {requiresAdmin: true}
},
],
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
const publicPages = ['login', 'register']
const authRequired = !publicPages.includes(to.name as string)
const adminRequired = to.matched.some(record => record.meta.requiresAdmin)
if (authRequired && !authStore.isAuthenticated) {
// Non connecté -> Login
next('/login')
} else if (to.name === 'login' && authStore.isAuthenticated) {
// Déjà connecté -> Accueil
next('/')
} else if (adminRequired && !authStore.isAdmin) {
// Connecté mais pas admin -> Accueil (ou page 403)
next('/')
} else {
next()
}
})
export default router
+52
View File
@@ -0,0 +1,52 @@
import {defineStore} from 'pinia'
interface UserClaims {
user_id: string
username: string
is_admin: boolean // On s'assure que le backend l'envoie ou on le déduit
exp: number
}
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('token') || null as string | null,
user: JSON.parse(localStorage.getItem('user') || 'null') as UserClaims | null,
}),
getters: {
isAuthenticated: (state) => !!state.token,
isAdmin: (state) => state.user?.is_admin || false,
},
actions: {
setToken(token: string) {
this.token = token
localStorage.setItem('token', token)
try {
// Décoder le payload du JWT (2ème partie du string)
const base64Url = token.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
}).join(''))
const decoded = JSON.parse(jsonPayload)
this.user = {
user_id: decoded.user_id,
username: decoded.username,
is_admin: decoded.is_superuser || false, // Vérifiez le nom du champ dans votre Claims Rust
exp: decoded.expire_at
}
localStorage.setItem('user', JSON.stringify(this.user))
} catch (e) {
console.error("Failed to decode token", e)
this.logout()
}
},
logout() {
this.token = null
this.user = null
localStorage.removeItem('token')
localStorage.removeItem('user')
}
}
})
+3
View File
@@ -9,11 +9,13 @@ pub struct Claims {
pub expire_at: usize, // Expiration time
pub created_at: usize, // Issued at
pub username: String,
pub is_superuser: bool, // Ajoutez ce champ
}
pub fn create_jwt(
user_id: Uuid,
username: &str,
is_superuser: bool, // Ajoutez l'argument ici
secret: &str,
expiration_seconds: u64,
) -> Result<String, jsonwebtoken::errors::Error> {
@@ -27,6 +29,7 @@ pub fn create_jwt(
expire_at: (now + expiration_seconds) as usize,
created_at: now as usize,
username: username.to_string(),
is_superuser, // Et ici
};
encode(
+80 -28
View File
@@ -1,28 +1,78 @@
use axum::{extract::State, http::Request, middleware::Next, response::Response};
use axum::{
body::Body,
extract::State,
http::{header, Request},
middleware::Next,
response::{IntoResponse, Response},
};
use std::sync::Arc;
use std::time::Instant;
use tracing::info;
use tracing::{info, Instrument};
use uuid::Uuid;
use super::context::{CurrentUser, RequestContext};
use super::error::HTTPError;
use super::metrics::HttpMetrics;
use crate::auth::token::verify_jwt;
use crate::core::AppState;
pub async fn context_middleware(
State(app_state): State<AppState>,
mut req: Request<axum::body::Body>,
/// Middleware pour collecter les métriques HTTP.
pub async fn metrics_middleware(
State(metrics): State<Arc<HttpMetrics>>,
req: Request<Body>,
next: Next,
) -> Response {
metrics.inc_request();
let started = Instant::now();
let response = next.run(req).await;
let latency = started.elapsed();
metrics.record_response(response.status().as_u16(), latency);
response
}
/// Middleware d'initialisation du contexte de requête et de traçage de base.
pub async fn request_context_middleware(mut req: Request<Body>, next: Next) -> Response {
let request_id = Uuid::new_v4();
let started_at = Instant::now();
// Infos "type Django request"
let method = req.method().clone();
let uri = req.uri().clone();
// Authentification par JWT
// On injecte un contexte initial sans utilisateur
req.extensions_mut().insert(RequestContext {
request_id,
started_at,
method: method.clone(),
uri: uri.clone(),
user: None,
});
let span = tracing::info_span!(
"request",
request_id = %request_id,
method = %method,
uri = %uri,
);
async move {
info!("Incoming request");
next.run(req).await
}
.instrument(span)
.await
}
/// Middleware d'authentification par JWT.
/// Tente d'identifier l'utilisateur et met à jour le RequestContext.
pub async fn auth_middleware(
State(app_state): State<AppState>,
mut req: Request<Body>,
next: Next,
) -> Response {
// Extraction du JWT depuis le header Authorization
let user: Option<CurrentUser> = match req
.headers()
.get(axum::http::header::AUTHORIZATION)
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|auth_header| {
if auth_header.starts_with("Bearer ") {
@@ -44,26 +94,28 @@ pub async fn context_middleware(
None => None,
};
let user_id = user.as_ref().map(|u| u.id);
// Mise à jour du RequestContext existant
if let Some(user) = &user {
info!(user_id = %user.id, username = %user.username, "User identified");
if let Some(ctx) = req.extensions_mut().get_mut::<RequestContext>() {
ctx.user = Some(user.clone());
}
}
// Injecte le contexte dans la requête (espace de stockage partagé)
// C'est ce qui permettra aux extracteurs comme 'CurrentUser' de retrouver ces données plus tard.
req.extensions_mut().insert(RequestContext {
request_id,
started_at,
method: method.clone(),
uri: uri.clone(),
user,
});
info!(
request_id = %request_id,
user_id = ?user_id,
method = %method,
uri = %uri,
"Incoming request"
);
// Passe la requête au reste de la stack
next.run(req).await
}
/// Middleware de sécurité qui impose une authentification.
pub async fn require_auth(req: Request<Body>, next: Next) -> Response {
let authenticated = req
.extensions()
.get::<RequestContext>()
.and_then(|ctx| ctx.user.as_ref())
.is_some();
if authenticated {
next.run(req).await
} else {
HTTPError::Unauthorized.into_response()
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ use axum::Router;
pub mod context;
pub mod error;
pub mod metrics;
mod middleware;
pub mod middleware;
pub mod server;
pub mod validation;
+24 -23
View File
@@ -20,7 +20,7 @@ use crate::core::AppState;
use crate::routes;
use super::metrics::HttpMetrics;
use super::middleware::context_middleware;
use super::middleware;
// ── Erreurs ───────────────────────────────────────────────────────────────────
@@ -44,8 +44,9 @@ pub enum HttpServerError {
/// - Injecte [`AppState`] via le middleware (les handlers l'obtiendront via
/// `State<AppState>` quand ils seront implémentés ; `.with_state()` sera
/// ajouté en conséquence).
/// - Applique [`context_middleware`] (authentification JWT + contexte de
/// requête) sur l'ensemble du router.
/// - Applique [`middleware::request_context_middleware`] (contexte de requête)
/// et [`middleware::auth_middleware`] (authentification JWT) sur l'ensemble
/// du router.
/// - Empile les layers Tower : [`CatchPanicLayer`], [`CorsLayer`] (permissif),
/// [`TraceLayer`].
/// - Collecte des métriques via [`HttpMetrics`] et les reporte périodiquement.
@@ -104,35 +105,35 @@ impl HttpServer {
/// La future se résout lorsqu'un signal de shutdown est reçu ou qu'une
/// erreur I/O fatale survient.
pub async fn run(mut self) -> Result<(), HttpServerError> {
let metrics = self.metrics.clone();
let app_state = self.app_state.clone();
// Construit le router avec state + middleware + layers Tower.
//
// Ordre d'application en axum : chaque `.layer()` enveloppe le service
// courant — le dernier appel devient la couche la plus externe.
// Ordre d'application en axum (LIFO) : le dernier .layer() ajouté est le premier exécuté.
//
// CatchPanicLayer (outermost — intercepte tout panic en dessous)
// └─ CorsLayer (répond aux preflight avant auth)
// └─ TraceLayer (span tracing sur la durée totale)
// └─ context_middleware (JWT + RequestContext + métriques)
// └─ routes (handlers)
// Courant de la requête :
// 1. CatchPanicLayer (le plus externe)
// 2. CorsLayer
// 3. TraceLayer
// 4. metrics_middleware (commence le chrono)
// 5. request_context_middleware (init ID)
// 6. auth_middleware (identifie l'utilisateur)
// 7. Routes (handlers)
let app: Router = routes::router()
.with_state(app_state.clone())
// Innermost : auth JWT, injection contexte, métriques
// Identification de l'utilisateur (optionnelle ici)
.layer(axum_middleware::from_fn_with_state(
app_state.clone(),
move |state, req, next| {
let metrics = metrics.clone();
async move {
metrics.inc_request();
let started = std::time::Instant::now();
let response = context_middleware(state, req, next).await;
let latency = started.elapsed();
metrics.record_response(response.status().as_u16(), latency);
response
}
},
middleware::auth_middleware,
))
// Initialisation du contexte (ID de requête, etc.)
.layer(axum_middleware::from_fn(
middleware::request_context_middleware,
))
// Métriques (mesure de la durée totale de la requête)
.layer(axum_middleware::from_fn_with_state(
app_state.metrics.http.clone(),
middleware::metrics_middleware,
))
// Spans tracing par requête (method, uri, status, latency)
.layer(TraceLayer::new_for_http())
+12 -8
View File
@@ -1,5 +1,6 @@
use crate::http::middleware;
use crate::http::OxRouter;
use axum::Router;
use axum::{middleware as axum_middleware, Router};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
@@ -15,19 +16,22 @@ pub mod server;
pub mod user;
pub fn router() -> OxRouter {
let api_routes = Router::new()
.merge(auth::routes::router())
// Routes nécessitant une authentification
let secure_routes = Router::new()
.merge(server::routes::router())
.merge(category::routes::router())
.merge(channel::routes::router())
.merge(group::routes::router())
.merge(message::routes::router())
.merge(user::routes::router());
.merge(user::routes::router())
.layer(axum_middleware::from_fn(middleware::require_auth));
Router::new()
.nest("/api", api_routes)
// .merge(attachment::routes::router())
.merge(
// Routes publiques (ou gérant leur propre auth)
let api_routes = Router::new()
.merge(secure_routes)
.merge(auth::routes::router());
Router::new().nest("/api", api_routes).merge(
SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", openapi::ApiDoc::openapi()),
)
}