Compare commits
3 Commits
17643d54ea
...
22d8d5414f
| Author | SHA1 | Date | |
|---|---|---|---|
| 22d8d5414f | |||
| 72075b7b71 | |||
| f372e7bd99 |
@ -15,8 +15,12 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run migrations and start server
|
||||
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"]
|
||||
# Run migrations and start server (--no-server-header suppresses uvicorn version disclosure)
|
||||
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --no-server-header"]
|
||||
|
||||
@ -18,6 +18,9 @@ class Settings(BaseSettings):
|
||||
# TOTP issuer name shown in authenticator apps
|
||||
TOTP_ISSUER: str = "UMBRA"
|
||||
|
||||
# CORS allowed origins (comma-separated)
|
||||
CORS_ORIGINS: str = "http://localhost:5173"
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
|
||||
@ -4,6 +4,7 @@ from contextlib import asynccontextmanager
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
from app.config import settings
|
||||
from app.database import engine
|
||||
from app.routers import auth, todos, events, calendars, reminders, projects, people, locations, settings as settings_router, dashboard, weather, event_templates
|
||||
from app.routers import totp
|
||||
@ -32,20 +33,25 @@ async def lifespan(app: FastAPI):
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
_is_dev = settings.ENVIRONMENT == "development"
|
||||
|
||||
app = FastAPI(
|
||||
title="UMBRA API",
|
||||
description="Backend API for UMBRA application",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
lifespan=lifespan,
|
||||
docs_url="/docs" if _is_dev else None,
|
||||
redoc_url="/redoc" if _is_dev else None,
|
||||
openapi_url="/openapi.json" if _is_dev else None,
|
||||
)
|
||||
|
||||
# CORS configuration for development
|
||||
# CORS configuration
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173"],
|
||||
allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
allow_headers=["Content-Type", "Authorization", "Cookie", "X-Requested-With"],
|
||||
)
|
||||
|
||||
# Include routers with /api prefix
|
||||
|
||||
@ -409,14 +409,22 @@ async def verify_password(
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
data: ChangePasswordRequest,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Change the current user's password. Requires old password verification."""
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
_check_ip_rate_limit(client_ip)
|
||||
await _check_account_lockout(current_user)
|
||||
|
||||
valid, _ = verify_password_with_upgrade(data.old_password, current_user.password_hash)
|
||||
if not valid:
|
||||
_record_ip_failure(client_ip)
|
||||
await _record_failed_login(db, current_user)
|
||||
raise HTTPException(status_code=401, detail="Invalid current password")
|
||||
|
||||
_failed_attempts.pop(client_ip, None)
|
||||
current_user.password_hash = hash_password(data.new_password)
|
||||
await db.commit()
|
||||
|
||||
|
||||
@ -14,8 +14,6 @@ services:
|
||||
backend:
|
||||
build: ./backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file: .env
|
||||
depends_on:
|
||||
db:
|
||||
@ -30,7 +28,7 @@ services:
|
||||
build: ./frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "80:8080"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
|
||||
@ -15,8 +15,8 @@ COPY . .
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
# Production stage — unprivileged nginx (runs as non-root, listens on 8080)
|
||||
FROM nginxinc/nginx-unprivileged:alpine
|
||||
|
||||
# Copy built files from build stage
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
@ -24,8 +24,8 @@ COPY --from=build /app/dist /usr/share/nginx/html
|
||||
# Copy nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
# Expose port 8080 (unprivileged)
|
||||
EXPOSE 8080
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@ -1,15 +1,75 @@
|
||||
# Rate limiting zones (before server block)
|
||||
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=10r/m;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Suppress nginx version in Server header
|
||||
server_tokens off;
|
||||
|
||||
# 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)
|
||||
location ~ /\.(?!well-known) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Rate-limited auth endpoints
|
||||
location /api/auth/login {
|
||||
limit_req zone=auth_limit burst=5 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/auth/verify-password {
|
||||
limit_req zone=auth_limit burst=5 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/auth/totp-verify {
|
||||
limit_req zone=auth_limit burst=5 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/auth/change-password {
|
||||
limit_req zone=auth_limit burst=5 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api {
|
||||
proxy_pass http://backend:8000;
|
||||
@ -35,12 +95,12 @@ server {
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
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'; img-src 'self' data:; font-src 'self';" 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;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
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'; img-src 'self' data:; font-src 'self';" 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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user