feat: Implement multi-tenancy with new database migrations, API updates across services, and refactor frontend API calls.
This commit is contained in:
@@ -5,6 +5,7 @@ edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
axum = { workspace = true, features = ["macros"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sqlx.workspace = true
|
||||
|
||||
@@ -6,11 +6,12 @@ use chrono::{Utc, Duration};
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: Uuid,
|
||||
pub org: Uuid,
|
||||
pub exp: i64,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
pub fn create_jwt(user_id: Uuid, role: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
pub fn create_jwt(user_id: Uuid, organization_id: Uuid, role: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let expiration = Utc::now()
|
||||
.checked_add_signed(Duration::hours(24))
|
||||
.expect("valid timestamp")
|
||||
@@ -18,6 +19,7 @@ pub fn create_jwt(user_id: Uuid, role: &str) -> Result<String, jsonwebtoken::err
|
||||
|
||||
let claims = Claims {
|
||||
sub: user_id,
|
||||
org: organization_id,
|
||||
exp: expiration,
|
||||
role: role.to_string(),
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod models;
|
||||
pub mod auth;
|
||||
pub mod utils;
|
||||
pub mod middleware;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::FromRequestParts,
|
||||
http::{request::Parts, Request, StatusCode},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use jsonwebtoken::{decode, DecodingKey, Validation};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::Claims;
|
||||
|
||||
/// Contexto de la organización extraído del JWT.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OrgContext {
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
/// Middleware que valida el token JWT y extrae el `organization_id`.
|
||||
pub async fn org_extractor_middleware<B>(
|
||||
mut req: Request<B>,
|
||||
next: Next<B>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get("authorization")
|
||||
.and_then(|header| header.to_str().ok());
|
||||
|
||||
let token = if let Some(token_str) = auth_header.and_then(|s| s.strip_prefix("Bearer ")) {
|
||||
token_str
|
||||
} else {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
};
|
||||
|
||||
// NOTA: El secreto debe venir de una variable de entorno en producción.
|
||||
let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string());
|
||||
|
||||
let claims = decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(secret.as_ref()),
|
||||
&Validation::default(),
|
||||
)
|
||||
.map_err(|_| StatusCode::UNAUTHORIZED)?
|
||||
.claims;
|
||||
|
||||
// Insertamos el contexto en las extensiones de la petición.
|
||||
req.extensions_mut().insert(OrgContext { id: claims.org });
|
||||
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
|
||||
/// Extractor de Axum para acceder fácilmente al `OrgContext` en los handlers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Org(pub OrgContext);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for Org
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let org_context = parts.extensions.get::<OrgContext>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Contexto de organización no encontrado. ¿El middleware está configurado?",
|
||||
))?;
|
||||
|
||||
Ok(Org(org_context.clone()))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use chrono::{DateTime, Utc};
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Course {
|
||||
pub id: Uuid,
|
||||
pub organization_id: Uuid,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub instructor_id: Uuid,
|
||||
@@ -68,6 +69,7 @@ pub struct UserGrade {
|
||||
pub struct AuditLog {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub organization_id: Option<Uuid>,
|
||||
pub action: String,
|
||||
pub entity_type: String,
|
||||
pub entity_id: Uuid,
|
||||
@@ -91,6 +93,7 @@ pub struct AuditLogResponse {
|
||||
pub struct Enrollment {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub organization_id: Uuid,
|
||||
pub course_id: Uuid,
|
||||
pub enroled_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -98,6 +101,7 @@ pub struct Enrollment {
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Asset {
|
||||
pub id: Uuid,
|
||||
pub organization_id: Uuid,
|
||||
pub filename: String,
|
||||
pub storage_path: String,
|
||||
pub mimetype: String,
|
||||
@@ -108,6 +112,7 @@ pub struct Asset {
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub organization_id: Uuid,
|
||||
pub email: String,
|
||||
pub password_hash: String,
|
||||
pub full_name: String,
|
||||
|
||||
Reference in New Issue
Block a user