fix(api): perbaiki keamanan auth & WebSocket, tambah rate limiting

Security fixes hasil audit:
- fix(auth): refresh token kini memakai claim typ=refresh; access token
  tidak bisa dipakai sebagai refresh token (sebelumnya bisa — eskalasi
  masa berlaku 1 jam -> 7 hari)
- fix(api): layer JWT hanya melindungi route /sessions dan /chat;
  /auth/login, /auth/register, /auth/refresh, /health kini publik
  (sebelumnya semua route 401-lock, API tidak bisa dipakai sama sekali)
- fix(ws): endpoint /ws kini memverifikasi token ZESDEX_WS_TOKEN via
  query param jika env diset (mencegah pemakaian LLM proxy terbuka)
- feat(api): rate limiting login/register/refresh (20 request / 10 menit
  per client IP) memakai RateLimiter yang tadinya dead code
- test(jwt): tambah unit test token type access vs refresh + expired
This commit is contained in:
asepharyana
2026-08-27 22:04:32 +07:00
parent 7f64423615
commit 6db00b2266
7 changed files with 219 additions and 25 deletions
+20 -13
View File
@@ -56,25 +56,34 @@ pub fn build_router(state: ApiState) -> Router {
// CORS layer — permissive for local daemon / development use
let cors = CorsLayer::permissive();
// JWT auth middleware — validates Bearer tokens on all API routes.
// Health and auth endpoints (login/register/refresh) are also
// protected; adjust route ordering or add an allow-list inside the
// middleware if public access is needed.
// JWT auth middleware — protects session/chat routes.
// Auth (login/register/refresh) and health endpoints stay public.
let jwt_auth = middleware::auth::JwtAuthLayer::new(shared_state.clone());
// Combine all sub-routers under a versioned prefix
Router::new()
.nest("/api/v1", api_v1_router())
.nest("/api/v1/auth", auth_router())
.nest("/api/v1/health", health_router())
.nest(
"/api/v1",
protected_router().layer(jwt_auth),
)
.layer(cors)
.layer(jwt_auth)
.with_state(shared_state)
}
/// Version 1 API sub-router.
///
/// Groups all resource routes under `/api/v1/*`.
fn api_v1_router() -> Router<Arc<ApiState>> {
use handlers::{auth, chat, conversations, health, sessions};
/// Auth + health sub-routers — publicly accessible (no JWT required).
fn auth_router() -> Router<Arc<ApiState>> {
handlers::auth::router()
}
fn health_router() -> Router<Arc<ApiState>> {
Router::new().route("/", axum::routing::get(handlers::health::health))
}
/// Protected sub-router — sessions + chat, guarded by JWT auth layer.
fn protected_router() -> Router<Arc<ApiState>> {
use handlers::{chat, conversations, sessions};
// Sessions router combines session CRUD + nested conversations
let sessions_router = Router::new()
@@ -96,8 +105,6 @@ fn api_v1_router() -> Router<Arc<ApiState>> {
);
Router::new()
.route("/health", axum::routing::get(health::health))
.nest("/auth", auth::router())
.nest("/sessions", sessions_router)
.nest("/chat", chat::router())
}