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
|
// Composables
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
import Index from '@/pages/index.vue'
|
import Index from '@/pages/index.vue'
|
||||||
|
|
||||||
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('@/pages/login.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Index,
|
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
|
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 expire_at: usize, // Expiration time
|
||||||
pub created_at: usize, // Issued at
|
pub created_at: usize, // Issued at
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
pub is_superuser: bool, // Ajoutez ce champ
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_jwt(
|
pub fn create_jwt(
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
username: &str,
|
username: &str,
|
||||||
|
is_superuser: bool, // Ajoutez l'argument ici
|
||||||
secret: &str,
|
secret: &str,
|
||||||
expiration_seconds: u64,
|
expiration_seconds: u64,
|
||||||
) -> Result<String, jsonwebtoken::errors::Error> {
|
) -> Result<String, jsonwebtoken::errors::Error> {
|
||||||
@@ -27,6 +29,7 @@ pub fn create_jwt(
|
|||||||
expire_at: (now + expiration_seconds) as usize,
|
expire_at: (now + expiration_seconds) as usize,
|
||||||
created_at: now as usize,
|
created_at: now as usize,
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
|
is_superuser, // Et ici
|
||||||
};
|
};
|
||||||
|
|
||||||
encode(
|
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 std::time::Instant;
|
||||||
use tracing::info;
|
use tracing::{info, Instrument};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::context::{CurrentUser, RequestContext};
|
use super::context::{CurrentUser, RequestContext};
|
||||||
|
use super::error::HTTPError;
|
||||||
|
use super::metrics::HttpMetrics;
|
||||||
use crate::auth::token::verify_jwt;
|
use crate::auth::token::verify_jwt;
|
||||||
use crate::core::AppState;
|
use crate::core::AppState;
|
||||||
|
|
||||||
pub async fn context_middleware(
|
/// Middleware pour collecter les métriques HTTP.
|
||||||
State(app_state): State<AppState>,
|
pub async fn metrics_middleware(
|
||||||
mut req: Request<axum::body::Body>,
|
State(metrics): State<Arc<HttpMetrics>>,
|
||||||
|
req: Request<Body>,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Response {
|
) -> 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 request_id = Uuid::new_v4();
|
||||||
let started_at = Instant::now();
|
let started_at = Instant::now();
|
||||||
|
|
||||||
// Infos "type Django request"
|
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
let uri = req.uri().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
|
let user: Option<CurrentUser> = match req
|
||||||
.headers()
|
.headers()
|
||||||
.get(axum::http::header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.and_then(|auth_header| {
|
.and_then(|auth_header| {
|
||||||
if auth_header.starts_with("Bearer ") {
|
if auth_header.starts_with("Bearer ") {
|
||||||
@@ -44,26 +94,28 @@ pub async fn context_middleware(
|
|||||||
None => None,
|
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
|
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 context;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
mod middleware;
|
pub mod middleware;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
|
|
||||||
|
|||||||
+24
-23
@@ -20,7 +20,7 @@ use crate::core::AppState;
|
|||||||
use crate::routes;
|
use crate::routes;
|
||||||
|
|
||||||
use super::metrics::HttpMetrics;
|
use super::metrics::HttpMetrics;
|
||||||
use super::middleware::context_middleware;
|
use super::middleware;
|
||||||
|
|
||||||
// ── Erreurs ───────────────────────────────────────────────────────────────────
|
// ── Erreurs ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -44,8 +44,9 @@ pub enum HttpServerError {
|
|||||||
/// - Injecte [`AppState`] via le middleware (les handlers l'obtiendront via
|
/// - Injecte [`AppState`] via le middleware (les handlers l'obtiendront via
|
||||||
/// `State<AppState>` quand ils seront implémentés ; `.with_state()` sera
|
/// `State<AppState>` quand ils seront implémentés ; `.with_state()` sera
|
||||||
/// ajouté en conséquence).
|
/// ajouté en conséquence).
|
||||||
/// - Applique [`context_middleware`] (authentification JWT + contexte de
|
/// - Applique [`middleware::request_context_middleware`] (contexte de requête)
|
||||||
/// requête) sur l'ensemble du router.
|
/// et [`middleware::auth_middleware`] (authentification JWT) sur l'ensemble
|
||||||
|
/// du router.
|
||||||
/// - Empile les layers Tower : [`CatchPanicLayer`], [`CorsLayer`] (permissif),
|
/// - Empile les layers Tower : [`CatchPanicLayer`], [`CorsLayer`] (permissif),
|
||||||
/// [`TraceLayer`].
|
/// [`TraceLayer`].
|
||||||
/// - Collecte des métriques via [`HttpMetrics`] et les reporte périodiquement.
|
/// - 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
|
/// La future se résout lorsqu'un signal de shutdown est reçu ou qu'une
|
||||||
/// erreur I/O fatale survient.
|
/// erreur I/O fatale survient.
|
||||||
pub async fn run(mut self) -> Result<(), HttpServerError> {
|
pub async fn run(mut self) -> Result<(), HttpServerError> {
|
||||||
let metrics = self.metrics.clone();
|
|
||||||
let app_state = self.app_state.clone();
|
let app_state = self.app_state.clone();
|
||||||
|
|
||||||
// Construit le router avec state + middleware + layers Tower.
|
// Construit le router avec state + middleware + layers Tower.
|
||||||
//
|
//
|
||||||
// Ordre d'application en axum : chaque `.layer()` enveloppe le service
|
// Ordre d'application en axum (LIFO) : le dernier .layer() ajouté est le premier exécuté.
|
||||||
// courant — le dernier appel devient la couche la plus externe.
|
|
||||||
//
|
//
|
||||||
// CatchPanicLayer (outermost — intercepte tout panic en dessous)
|
// Courant de la requête :
|
||||||
// └─ CorsLayer (répond aux preflight avant auth)
|
// 1. CatchPanicLayer (le plus externe)
|
||||||
// └─ TraceLayer (span tracing sur la durée totale)
|
// 2. CorsLayer
|
||||||
// └─ context_middleware (JWT + RequestContext + métriques)
|
// 3. TraceLayer
|
||||||
// └─ routes (handlers)
|
// 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()
|
let app: Router = routes::router()
|
||||||
.with_state(app_state.clone())
|
.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(
|
.layer(axum_middleware::from_fn_with_state(
|
||||||
app_state.clone(),
|
app_state.clone(),
|
||||||
move |state, req, next| {
|
middleware::auth_middleware,
|
||||||
let metrics = metrics.clone();
|
))
|
||||||
async move {
|
// Initialisation du contexte (ID de requête, etc.)
|
||||||
metrics.inc_request();
|
.layer(axum_middleware::from_fn(
|
||||||
let started = std::time::Instant::now();
|
middleware::request_context_middleware,
|
||||||
let response = context_middleware(state, req, next).await;
|
))
|
||||||
let latency = started.elapsed();
|
// Métriques (mesure de la durée totale de la requête)
|
||||||
metrics.record_response(response.status().as_u16(), latency);
|
.layer(axum_middleware::from_fn_with_state(
|
||||||
response
|
app_state.metrics.http.clone(),
|
||||||
}
|
middleware::metrics_middleware,
|
||||||
},
|
|
||||||
))
|
))
|
||||||
// Spans tracing par requête (method, uri, status, latency)
|
// Spans tracing par requête (method, uri, status, latency)
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
|
|||||||
+14
-10
@@ -1,5 +1,6 @@
|
|||||||
|
use crate::http::middleware;
|
||||||
use crate::http::OxRouter;
|
use crate::http::OxRouter;
|
||||||
use axum::Router;
|
use axum::{middleware as axum_middleware, Router};
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
use utoipa_swagger_ui::SwaggerUi;
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
@@ -15,19 +16,22 @@ pub mod server;
|
|||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
pub fn router() -> OxRouter {
|
pub fn router() -> OxRouter {
|
||||||
let api_routes = Router::new()
|
// Routes nécessitant une authentification
|
||||||
.merge(auth::routes::router())
|
let secure_routes = Router::new()
|
||||||
.merge(server::routes::router())
|
.merge(server::routes::router())
|
||||||
.merge(category::routes::router())
|
.merge(category::routes::router())
|
||||||
.merge(channel::routes::router())
|
.merge(channel::routes::router())
|
||||||
.merge(group::routes::router())
|
.merge(group::routes::router())
|
||||||
.merge(message::routes::router())
|
.merge(message::routes::router())
|
||||||
.merge(user::routes::router());
|
.merge(user::routes::router())
|
||||||
|
.layer(axum_middleware::from_fn(middleware::require_auth));
|
||||||
|
|
||||||
Router::new()
|
// Routes publiques (ou gérant leur propre auth)
|
||||||
.nest("/api", api_routes)
|
let api_routes = Router::new()
|
||||||
// .merge(attachment::routes::router())
|
.merge(secure_routes)
|
||||||
.merge(
|
.merge(auth::routes::router());
|
||||||
SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", openapi::ApiDoc::openapi()),
|
|
||||||
)
|
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