init
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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
|
||||
}
|
||||
@@ -5,17 +5,50 @@
|
||||
*/
|
||||
|
||||
// Composables
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user