Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Luisanchez0/modulo_Horario/llms.txt

Use this file to discover all available pages before exploring further.

Módulo Horario ships with sensible defaults, but several controls require explicit configuration before a deployment is production-ready. This page walks through each security domain, explains the expected configuration, and provides a checklist you can work through before going live.
Never commit .env files or hardcode secrets in source code. The repository’s .gitignore excludes .env, but you must verify this is in place before every push.Local development:
# Generate secrets locally
bash setup-secrets.sh

# Confirm .env is not staged
git status  # .env should not appear
Production (AWS Secrets Manager):
aws secretsmanager create-secret \
  --name prod/horario-service \
  --secret-string '{
    "JWT_SECRET": "...",
    "ADMIN_CREATION_KEY": "...",
    "INTERNAL_API_KEY": "..."
  }'
Then reference secrets from Docker Compose using the /run/secrets/ path rather than plain environment variables.Rotation schedule: Rotate all production secrets every 30 days.
NEW_SECRET=$(openssl rand -hex 32)

aws secretsmanager update-secret \
  --secret-id prod/horario-service \
  --secret-string '{"JWT_SECRET": "'"$NEW_SECRET"'"}'

# Redeploy with zero downtime
docker compose up -d --no-deps --build horario-service
If a secret is accidentally committed, revoke it immediately, generate a replacement, rewrite git history if the commit has not reached main, and notify the team. Do not wait.
HTTPS is optional in local development and mandatory in production. Running production traffic over plain HTTP exposes tokens and credentials to network interception.Set up Let’s Encrypt with Nginx:
sudo certbot certonly --standalone -d api.yourdomain.com
Redirect HTTP to HTTPS and add security headers:
server {
    listen 80;
    server_name api.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
The Strict-Transport-Security header tells browsers to always use HTTPS for your domain for one year, even if the user types http://. Add it only after you are certain HTTPS is working correctly.
Rate limits protect the login endpoint from brute-force attacks and protect API endpoints from abuse. Configure these in Nginx for production.Login endpoint — 5 requests per minute per IP:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

location /auth/login {
    limit_req zone=login burst=10 nodelay;
    proxy_pass http://backend;
}
General API endpoints — 100 requests per minute per IP:
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;

location /horarios {
    limit_req zone=api burst=50 nodelay;
    proxy_pass http://backend;
}
When the limit is exceeded, the API returns 429 Too Many Requests.
CORS must be restricted to your actual frontend domain in production. A wildcard origin (*) allows any website to make credentialed requests to your API.
# production settings
CORS_ALLOW_ORIGINS = os.getenv("CORS_ALLOW_ORIGINS", "").split(",")
# Set in .env: CORS_ALLOW_ORIGINS=https://app.yourdomain.com

CORSMiddleware(
    allow_origins=CORS_ALLOW_ORIGINS,
    allow_credentials=False,
    allow_methods=["*"],
    allow_headers=["*"],
)
Never use allow_origins=["*"] in production. It permits any origin to read API responses, bypassing the browser’s same-origin protection.
All request bodies pass through Pydantic schemas before reaching business logic. Invalid input—wrong types, missing required fields, malformed emails, strings that are too short or too long—is rejected automatically with 422 Unprocessable Entity.You do not need to add manual validation guards in route handlers. Rely on schema definitions and add field validators for domain-specific rules:
from pydantic import BaseModel, EmailStr, Field, field_validator

class DocenteCreate(BaseModel):
    nombre: str = Field(..., min_length=2, max_length=255)
    correo: EmailStr
    password: str = Field(..., min_length=8, max_length=128)

    @field_validator('nombre')
    @classmethod
    def nombre_no_vacio(cls, v):
        if not v.strip():
            raise ValueError("Nombre no puede estar vacío")
        return v.strip()
Keep schema definitions as the single source of truth for input rules. Avoid duplicating validation logic in use cases or repositories.
SQLAlchemy’s ORM parameterizes all queries automatically. User-supplied values are passed as bound parameters, never interpolated into the query string.
# Safe — parameterized
horarios = session.query(HorarioModel).filter(
    HorarioModel.docente_id == docente_id
).all()
Never construct queries by string formatting:
# Unsafe — never do this
query = f"SELECT * FROM horarios WHERE docente_id = {docente_id}"
If docente_id were 1; DROP TABLE horarios; --, the string-formatted version would execute the destructive statement. The ORM version would look for a record with that literal string as an ID and find nothing.
React escapes all dynamic values rendered into the DOM. A payload like <script>alert('xss')</script> is rendered as the escaped string &lt;script&gt;alert('xss')&lt;/script&gt;, not as executable HTML.The backend returns JSON, not HTML, so API responses are also safe from injection by default. Do not use dangerouslySetInnerHTML unless you have explicitly sanitized the content first.
Log user IDs and IP addresses for audit trails. Never log passwords, tokens, or secrets.
# Safe — identifiers only
logger.info("Docente login", extra={
    'user_id': docente.id,
    'ip_address': request.client.host,
    'timestamp': datetime.utcnow()
})

logger.error("Auth failed", extra={
    'attempt_count': failed_attempts,
    'ip_address': request.client.host,
    'reason': 'invalid_credentials'
})
# Unsafe — never log these
logger.info(f"Login con password {password}")   # exposes credential
logger.info(f"JWT token: {token}")              # exposes session token
logger.info(f"Secret: {os.getenv('JWT_SECRET')}")  # exposes signing key
In production, write logs to a rotating file (RotatingFileHandler) and use JSON format for compatibility with ELK Stack or similar log aggregation systems.

Pre-deployment checklist

Work through this list before every production deployment. Before development:
  • Run bash setup-secrets.sh to generate local secrets
  • Confirm .env is listed in .gitignore
  • Confirm .env does not appear in git status
Before pushing to GitHub:
  • Run git diff --cached | grep -i "password\|secret\|key" — output should be empty
  • Confirm no .env file is tracked: git status | grep ".env" should return nothing
Before deployment:
  • Regenerate all secrets in AWS Secrets Manager
  • Configure HTTPS in Nginx with a valid Let’s Encrypt certificate
  • Enable rate limiting for /auth/login (5 req/min) and general API routes (100 req/min)
  • Set CORS_ALLOW_ORIGINS to your production domain — never *
  • Configure centralized, structured logging
  • Run a security smoke test against the deployed environment
Weekly in production:
  • Review access logs for anomalies
  • Check rate-limiting alert counts
  • Review failed login attempt trends
  • Verify SSL certificate expiry date
Monthly in production:
  • Rotate all secrets
  • Audit and patch dependencies
  • Conduct a security review
  • Test backup and restore procedures

Build docs developers (and LLMs) love