Initial public release v0.1.0

This commit is contained in:
thakares committed 2026-06-21 20:05:29 +05:30
commit 6c39e0bfbf
87 files changed
+10867

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
use axum::{
extract::{ConnectInfo, FromRequestParts},
http::request::Parts,
};
use std::net::SocketAddr;
/// Request context for audit logging — captures IP and User-Agent.
///
/// Handlers include this extractor to forward client metadata to the audit log
/// without threading raw request headers through the call stack.
#[derive(Debug, Clone, Default)]
pub struct AuditContext {
pub ip_address: Option<String>,
pub user_agent: Option<String>,
}
impl<S> FromRequestParts<S> for AuditContext
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// Prefer X-Forwarded-For (set by reverse proxies like Nginx)
let ip_address = parts
.headers
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.split(',').next())
.map(|s| s.trim().to_string())
.or_else(|| {
// Fall back to direct peer address (requires ConnectInfo extension)
parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.map(|ci| ci.0.ip().to_string())
});
let user_agent = parts
.headers
.get(axum::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
Ok(AuditContext {
ip_address,
user_agent,
})
}
}
+96
View File
@@ -0,0 +1,96 @@
use axum::{
extract::{FromRef, FromRequestParts},
http::request::Parts,
};
use axum_extra::extract::CookieJar;
use crate::{
db::models::User,
db::repository::users as user_repo,
error::AppError,
security::{sessions, tokens},
state::AppState,
};
/// Describes how the current request was authenticated.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthMethod {
Session,
Token,
}
/// Axum extractor that resolves the authenticated user from either a session
/// cookie or a Bearer token in the Authorization header.
///
/// Handlers that need an authenticated user simply include `auth: AuthUser`
/// in their parameter list.
#[derive(Debug, Clone)]
pub struct AuthUser {
pub user: User,
pub method: AuthMethod,
/// Session ID — populated when `method == Session`, used for logout.
pub session_id: Option<String>,
}
impl<S> FromRequestParts<S> for AuthUser
where
AppState: FromRef<S>,
S: Send + Sync,
{
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, AppError> {
let app_state = AppState::from_ref(state);
// 1. Try session cookie first
let jar = CookieJar::from_headers(&parts.headers);
if let Some(cookie) = jar.get(sessions::SESSION_COOKIE) {
let raw = cookie.value();
if let Some(session) =
sessions::validate_session(&app_state.pool, raw, &app_state.config.security).await?
{
let user = user_repo::find_by_id(&app_state.pool, &session.user_id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::Unauthorized)?;
if !user.is_active() {
return Err(AppError::Unauthorized);
}
return Ok(AuthUser {
user,
method: AuthMethod::Session,
session_id: Some(session.id),
});
}
}
// 2. Try Bearer token in Authorization header
if let Some(auth_header) = parts.headers.get(axum::http::header::AUTHORIZATION) {
if let Ok(value) = auth_header.to_str() {
if let Some(raw) = value.strip_prefix("Bearer ") {
if let Some(token) = tokens::validate_token(&app_state.pool, raw.trim()).await?
{
let user = user_repo::find_by_id(&app_state.pool, &token.user_id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::Unauthorized)?;
if !user.is_active() {
return Err(AppError::Unauthorized);
}
return Ok(AuthUser {
user,
method: AuthMethod::Token,
session_id: None,
});
}
}
}
}
Err(AppError::Unauthorized)
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod audit;
pub mod auth;
pub mod permissions;
+12
View File
@@ -0,0 +1,12 @@
use sqlx::SqlitePool;
use crate::{error::AppError, identity::permissions};
/// Enforce that the calling user has the given permission.
///
/// Alias for `permissions::require_permission` — imported in handlers for
/// readability: `require(pool, user_id, "users:create").await?`
#[inline]
pub async fn require(pool: &SqlitePool, user_id: &str, permission: &str) -> Result<(), AppError> {
permissions::require_permission(pool, user_id, permission).await
}