Mejora de fromts

This commit is contained in:
2026-03-03 11:25:37 -07:00
parent 49dfb3ef24
commit f10b15d91b
19 changed files with 180 additions and 2115 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ from app.services.audit_service import AuditService
from app.services.token_service import TokenService
from app.api.deps import oauth2_scheme, get_current_user
from app.core.cache import cache, cache_key
from app.core.limiter import limiter
# Nombres de cookie por tipo de usuario
CLIENT_ROLES = {"CLIENT_ADMIN", "CLIENT_USER"}
@@ -46,6 +47,7 @@ settings = get_settings()
# ===================================
@router.post("/login", response_model=LoginResponse)
@limiter.limit("10/minute")
async def login(
login_data: LoginRequest,
request: Request,
@@ -707,7 +709,9 @@ _RESET_KEY_PREFIX = "pwd_reset:"
@router.post("/forgot-password", status_code=status.HTTP_200_OK)
@limiter.limit("5/minute")
async def forgot_password(
request: Request,
data: ForgotPasswordRequest,
db: AsyncSession = Depends(get_db),
):
@@ -774,7 +778,9 @@ async def forgot_password(
@router.post("/reset-password", status_code=status.HTTP_200_OK)
@limiter.limit("5/minute")
async def reset_password(
request: Request,
data: ResetPasswordRequest,
db: AsyncSession = Depends(get_db),
):

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ Configuración centralizada usando Pydantic Settings v2
"""
from functools import lru_cache
from typing import List, Optional
from pydantic_settings import BaseSettings
from pydantic import field_validator, Field
import os
@@ -60,8 +59,8 @@ class Settings(BaseSettings):
# ===================================
SMTP_HOST: str = Field(default="localhost")
SMTP_PORT: int = Field(default=587)
SMTP_USER: Optional[str] = Field(default=None)
SMTP_PASSWORD: Optional[str] = Field(default=None)
SMTP_USER: str | None = Field(default=None)
SMTP_PASSWORD: str | None = Field(default=None)
SMTP_USE_TLS: bool = Field(default=True)
SMTP_USE_SSL: bool = Field(default=False)
@@ -79,7 +78,7 @@ class Settings(BaseSettings):
UPLOAD_PATH: str = Field(default="/app/uploads")
@property
def ALLOWED_FILE_EXTENSIONS(self) -> List[str]:
def ALLOWED_FILE_EXTENSIONS(self) -> list[str]:
"""Parse the comma-separated file extensions."""
return [ext.strip().lower() for ext in self.ALLOWED_FILE_EXTENSIONS_STR.split(",")]
@@ -102,7 +101,7 @@ class Settings(BaseSettings):
# ===================================
LOG_LEVEL: str = Field(default="INFO")
LOG_FORMAT: str = Field(default="json")
LOG_FILE: Optional[str] = Field(default=None)
LOG_FILE: str | None = Field(default=None)
# ===================================
# FRONTEND URLS

View File

@@ -0,0 +1,20 @@
"""
Rate Limiter - ServiceManagerWeb
Configura slowapi con Redis como storage backend.
Respeta settings.RATE_LIMIT_ENABLED: si está desactivado usa memoria
y el limiter queda en modo noop (enabled=False).
"""
from slowapi import Limiter
from slowapi.util import get_remote_address
from app.core.config import get_settings
settings = get_settings()
limiter = Limiter(
key_func=get_remote_address,
storage_uri=settings.REDIS_URL if settings.RATE_LIMIT_ENABLED else "memory://",
enabled=settings.RATE_LIMIT_ENABLED,
)

View File

@@ -9,6 +9,9 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import JSONResponse
from contextlib import asynccontextmanager
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
import structlog
import time
import uuid
@@ -31,6 +34,7 @@ from app.api.v1.router import api_router
from app.middleware.tenant import TenantMiddleware
from app.middleware.correlation_id import CorrelationIDMiddleware
from app.core.cache import cache
from app.core.limiter import limiter
settings = get_settings()
setup_logging()
@@ -70,6 +74,11 @@ app = FastAPI(
openapi_url=f"/{settings.API_VERSION}/openapi.json"
)
# SlowAPI rate limiting
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)
# ===================================
# MIDDLEWARE
# ===================================

View File

@@ -0,0 +1,43 @@
"""add_ticket_indexes
Revision ID: b7c8d9e0f1a2
Revises: fix_client_timestamps
Create Date: 2026-03-03 00:00:00.000000
Agrega índices a la tabla tickets para optimizar queries frecuentes:
- idx_tickets_status → filtros por estado
- idx_tickets_priority → filtros por prioridad
- idx_tickets_assigned_to → tickets por agente asignado
- idx_tickets_tenant_status → compuesto multi-tenant (tenant_id, status)
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = 'b7c8d9e0f1a2'
down_revision = 'fix_client_timestamps'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets (status)"
)
op.execute(
"CREATE INDEX IF NOT EXISTS idx_tickets_priority ON tickets (priority)"
)
op.execute(
"CREATE INDEX IF NOT EXISTS idx_tickets_assigned_to "
"ON tickets (assigned_to) WHERE assigned_to IS NOT NULL"
)
op.execute(
"CREATE INDEX IF NOT EXISTS idx_tickets_tenant_status "
"ON tickets (tenant_id, status)"
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS idx_tickets_tenant_status")
op.execute("DROP INDEX IF EXISTS idx_tickets_assigned_to")
op.execute("DROP INDEX IF EXISTS idx_tickets_priority")
op.execute("DROP INDEX IF EXISTS idx_tickets_status")

View File

@@ -31,6 +31,7 @@ pyotp==2.9.0 # TOTP/2FA support
# ===================================
celery==5.3.4
redis==5.0.1
slowapi==0.1.9 # Rate limiting middleware
# ===================================
# EMAIL

11
frontend-client/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import type { User } from '$lib/stores/auth';
declare global {
namespace App {
interface Locals {
user: User | null;
}
}
}
export {};

View File

@@ -0,0 +1,19 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const cookie = event.request.headers.get('cookie') ?? '';
if (cookie) {
try {
const apiUrl = process.env.PUBLIC_API_URL ?? 'http://backend:8000';
const response = await fetch(`${apiUrl}/v1/auth/me`, {
headers: { cookie, 'X-App': 'client' }
});
event.locals.user = response.ok ? await response.json() : null;
} catch {
event.locals.user = null;
}
} else {
event.locals.user = null;
}
return resolve(event);
};

View File

@@ -128,6 +128,11 @@ function createAuthStore() {
update(state => ({ ...state, user }));
},
// Set user from SSR pre-load (no fetch required)
setUser: (user: User) => {
set({ user, token: null, isAuthenticated: true, isLoading: false });
},
// Set loading state
setLoading: (isLoading: boolean) => {
update(state => ({ ...state, isLoading }));

View File

@@ -0,0 +1,7 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = ({ locals }) => {
return {
user: locals.user ?? null
};
};

View File

@@ -9,10 +9,14 @@
import { browser } from '$app/environment';
import '../app.css';
export let data;
let mounted = false;
onMount(async () => {
await auth.init();
onMount(() => {
if (data.user && !$auth.isAuthenticated) {
auth.setUser(data.user);
}
mounted = true;
});

11
frontend-internal/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import type { InternalUser } from '$lib/stores/auth';
declare global {
namespace App {
interface Locals {
user: InternalUser | null;
}
}
}
export {};

View File

@@ -0,0 +1,19 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const cookie = event.request.headers.get('cookie') ?? '';
if (cookie) {
try {
const apiUrl = process.env.PUBLIC_API_URL ?? 'http://backend:8000';
const response = await fetch(`${apiUrl}/v1/auth/me`, {
headers: { cookie, 'X-App': 'internal' }
});
event.locals.user = response.ok ? await response.json() : null;
} catch {
event.locals.user = null;
}
} else {
event.locals.user = null;
}
return resolve(event);
};

View File

@@ -190,6 +190,11 @@ function createAuthStore() {
update(state => ({ ...state, user }));
},
// Set user from SSR pre-load (no fetch required)
setUser: (user: InternalUser) => {
set({ user, token: null, refreshToken: null, isAuthenticated: true, isLoading: false });
},
// Set loading state
setLoading: (isLoading: boolean) => {
update(state => ({ ...state, isLoading }));

View File

@@ -0,0 +1,7 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = ({ locals }) => {
return {
user: locals.user ?? null
};
};

View File

@@ -10,11 +10,15 @@
import { browser } from '$app/environment';
import '../app.css';
export let data;
let sidebarOpen = false;
let mounted = false;
onMount(async () => {
await auth.init();
onMount(() => {
if (data.user && !$auth.isAuthenticated) {
auth.setUser(data.user);
}
mounted = true;
});