Compare commits
3 Commits
ccfc5e151a
...
c986028f51
| Author | SHA1 | Date | |
|---|---|---|---|
| c986028f51 | |||
| ab7e4a7c7e | |||
| 7721bf5cec |
@ -18,6 +18,9 @@ ENVIRONMENT=development
|
||||
# CORS allowed origins (comma-separated, default: http://localhost:5173)
|
||||
# CORS_ORIGINS=https://umbra.example.com
|
||||
|
||||
# Public URL for ntfy notification click links (default: http://localhost)
|
||||
# UMBRA_URL=https://umbra.example.com
|
||||
|
||||
# Timezone (applied to backend + db containers via env_file)
|
||||
TZ=Australia/Perth
|
||||
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
# ── Build stage: compile C extensions ──────────────────────────────────
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||
|
||||
# ── Runtime stage: lean production image ───────────────────────────────
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Copy pre-built Python packages from builder
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
@ -19,7 +25,6 @@ COPY . .
|
||||
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run migrations and start server
|
||||
|
||||
@ -23,6 +23,12 @@ class Settings(BaseSettings):
|
||||
# CORS allowed origins (comma-separated)
|
||||
CORS_ORIGINS: str = "http://localhost:5173"
|
||||
|
||||
# Public-facing URL used in ntfy notification click links
|
||||
UMBRA_URL: str = "http://localhost"
|
||||
|
||||
# Concurrent session limit per user (oldest evicted when exceeded)
|
||||
MAX_SESSIONS_PER_USER: int = 10
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
|
||||
@ -35,7 +35,7 @@ from app.services.ntfy_templates import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
UMBRA_URL = "http://10.0.69.35"
|
||||
from app.config import settings as app_settings
|
||||
|
||||
|
||||
# ── Dedup helpers ─────────────────────────────────────────────────────────────
|
||||
@ -92,7 +92,7 @@ async def _dispatch_reminders(db: AsyncSession, settings: Settings, now: datetim
|
||||
)
|
||||
sent = await send_ntfy_notification(
|
||||
settings=settings,
|
||||
click_url=UMBRA_URL,
|
||||
click_url=app_settings.UMBRA_URL,
|
||||
**payload,
|
||||
)
|
||||
if sent:
|
||||
@ -144,7 +144,7 @@ async def _dispatch_events(db: AsyncSession, settings: Settings, now: datetime,
|
||||
)
|
||||
sent = await send_ntfy_notification(
|
||||
settings=settings,
|
||||
click_url=UMBRA_URL,
|
||||
click_url=app_settings.UMBRA_URL,
|
||||
**payload,
|
||||
)
|
||||
if sent:
|
||||
@ -184,7 +184,7 @@ async def _dispatch_todos(db: AsyncSession, settings: Settings, today, sent_keys
|
||||
)
|
||||
sent = await send_ntfy_notification(
|
||||
settings=settings,
|
||||
click_url=UMBRA_URL,
|
||||
click_url=app_settings.UMBRA_URL,
|
||||
**payload,
|
||||
)
|
||||
if sent:
|
||||
@ -223,7 +223,7 @@ async def _dispatch_projects(db: AsyncSession, settings: Settings, today, sent_k
|
||||
)
|
||||
sent = await send_ntfy_notification(
|
||||
settings=settings,
|
||||
click_url=UMBRA_URL,
|
||||
click_url=app_settings.UMBRA_URL,
|
||||
**payload,
|
||||
)
|
||||
if sent:
|
||||
|
||||
@ -770,7 +770,9 @@ async def get_audit_log(
|
||||
)
|
||||
|
||||
if action:
|
||||
base_q = base_q.where(AuditLog.action.like(f"{action}%"))
|
||||
# Escape LIKE metacharacters so user input is treated literally
|
||||
safe_action = action.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
base_q = base_q.where(AuditLog.action.like(f"{safe_action}%", escape="\\"))
|
||||
if target_user_id is not None:
|
||||
base_q = base_q.where(AuditLog.target_user_id == target_user_id)
|
||||
|
||||
|
||||
@ -66,6 +66,7 @@ def _set_session_cookie(response: Response, token: str) -> None:
|
||||
secure=app_settings.COOKIE_SECURE,
|
||||
max_age=app_settings.SESSION_MAX_AGE_DAYS * 86400,
|
||||
samesite="lax",
|
||||
path="/",
|
||||
)
|
||||
|
||||
|
||||
@ -219,6 +220,26 @@ async def _create_db_session(
|
||||
)
|
||||
db.add(db_session)
|
||||
await db.flush()
|
||||
|
||||
# Enforce concurrent session limit: revoke oldest sessions beyond the cap
|
||||
active_sessions = (
|
||||
await db.execute(
|
||||
select(UserSession)
|
||||
.where(
|
||||
UserSession.user_id == user.id,
|
||||
UserSession.revoked == False, # noqa: E712
|
||||
UserSession.expires_at > datetime.now(),
|
||||
)
|
||||
.order_by(UserSession.created_at.asc())
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
max_sessions = app_settings.MAX_SESSIONS_PER_USER
|
||||
if len(active_sessions) > max_sessions:
|
||||
for old_session in active_sessions[: len(active_sessions) - max_sessions]:
|
||||
old_session.revoked = True
|
||||
await db.flush()
|
||||
|
||||
token = create_session_token(user.id, session_id)
|
||||
return session_id, token
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import settings as app_settings
|
||||
from app.models.settings import Settings
|
||||
from app.models.user import User
|
||||
from app.schemas.settings import SettingsUpdate, SettingsResponse
|
||||
@ -108,7 +109,7 @@ async def test_ntfy(
|
||||
message="If you see this, your ntfy integration is working correctly.",
|
||||
tags=["white_check_mark"],
|
||||
priority=3,
|
||||
click_url="http://10.0.69.35",
|
||||
click_url=app_settings.UMBRA_URL,
|
||||
)
|
||||
|
||||
if not success:
|
||||
|
||||
@ -21,15 +21,26 @@ server {
|
||||
# Suppress nginx version in Server header
|
||||
server_tokens off;
|
||||
|
||||
# ── Real client IP restoration (PT-01) ────────────────────────────
|
||||
# Pangolin (TLS-terminating reverse proxy) connects via Docker bridge.
|
||||
# Restore the real client IP from X-Forwarded-For so that limit_req_zone
|
||||
# (which keys on $binary_remote_addr) throttles per-client, not per-proxy.
|
||||
# Safe to trust all sources: nginx is only reachable via Docker networking,
|
||||
# never directly internet-facing. Tighten if deployment model changes.
|
||||
set_real_ip_from 0.0.0.0/0;
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
|
||||
|
||||
# Block dotfiles (except .well-known for ACME/Let's Encrypt)
|
||||
# Block dotfiles (except .well-known for ACME/Let's Encrypt) (PT-04)
|
||||
location ~ /\.(?!well-known) {
|
||||
return 404;
|
||||
default_type application/json;
|
||||
return 404 '{"detail":"Not Found"}';
|
||||
}
|
||||
|
||||
# Rate-limited auth endpoints (keep in sync with proxy-params.conf)
|
||||
@ -104,6 +115,7 @@ server {
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
@ -111,4 +123,5 @@ server {
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user