disable register page
This commit is contained in:
parent
c8aa1e016c
commit
0f7a60c2ff
20 changed files with 188 additions and 302 deletions
|
|
@ -17,6 +17,11 @@ GRAFANA_PASSWORD=change-this-password-in-production
|
||||||
CELERY_BROKER_URL=redis://redis:6379/0
|
CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
|
|
||||||
|
# Registration
|
||||||
|
# Set to "false" to disable user registration. For prod, build the image with:
|
||||||
|
# docker build --build-arg REGISTRATION_ENABLED=false -t your-image .
|
||||||
|
REGISTRATION_ENABLED=true
|
||||||
|
|
||||||
# MinIO Configuration (Object Storage)
|
# MinIO Configuration (Object Storage)
|
||||||
# MinIO server stays hidden - Flask proxies all requests
|
# MinIO server stays hidden - Flask proxies all requests
|
||||||
# MINIO_ENDPOINT: Internal Docker network address (for server-to-server communication)
|
# MINIO_ENDPOINT: Internal Docker network address (for server-to-server communication)
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gcc \
|
|
||||||
postgresql-client \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Python dependencies
|
|
||||||
COPY requirements/ requirements/
|
|
||||||
RUN pip install --no-cache-dir -r requirements/prod.txt
|
|
||||||
|
|
||||||
# Copy application code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Create non-root user
|
|
||||||
RUN useradd -m appuser && chown -R appuser:appuser /app
|
|
||||||
USER appuser
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:8000/health/ || exit 1
|
|
||||||
|
|
||||||
# Run with gunicorn
|
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "wsgi:app"]
|
|
||||||
|
|
@ -16,6 +16,7 @@ class Config:
|
||||||
MAX_FAILED_LOGIN_ATTEMPTS = int(os.environ.get("MAX_FAILED_LOGIN_ATTEMPTS", "5"))
|
MAX_FAILED_LOGIN_ATTEMPTS = int(os.environ.get("MAX_FAILED_LOGIN_ATTEMPTS", "5"))
|
||||||
ACCOUNT_LOCK_DURATION_MINUTES = int(os.environ.get("ACCOUNT_LOCK_DURATION_MINUTES", "30"))
|
ACCOUNT_LOCK_DURATION_MINUTES = int(os.environ.get("ACCOUNT_LOCK_DURATION_MINUTES", "30"))
|
||||||
LOCKOUT_STRATEGY = os.environ.get("LOCKOUT_STRATEGY", "timeout") # "timeout" | "permanent"
|
LOCKOUT_STRATEGY = os.environ.get("LOCKOUT_STRATEGY", "timeout") # "timeout" | "permanent"
|
||||||
|
REGISTRATION_ENABLED = os.environ.get("REGISTRATION_ENABLED", "true").lower() == "true"
|
||||||
|
|
||||||
# Celery Configuration
|
# Celery Configuration
|
||||||
CELERY = {
|
CELERY = {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class LoginHistory(db.Model):
|
||||||
"ip_address": self.ip_address,
|
"ip_address": self.ip_address,
|
||||||
"user_agent": self.user_agent,
|
"user_agent": self.user_agent,
|
||||||
"success": self.success,
|
"success": self.success,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": self.created_at if self.created_at else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""Auth and user management API routes"""
|
"""Auth and user management API routes"""
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, current_app, jsonify, request
|
||||||
from flask_jwt_extended import (
|
from flask_jwt_extended import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
create_refresh_token,
|
create_refresh_token,
|
||||||
|
|
@ -24,6 +24,9 @@ api_bp = Blueprint("api", __name__)
|
||||||
@api_bp.route("/auth/register", methods=["POST"])
|
@api_bp.route("/auth/register", methods=["POST"])
|
||||||
def register():
|
def register():
|
||||||
"""Register a new user"""
|
"""Register a new user"""
|
||||||
|
if not current_app.config.get("REGISTRATION_ENABLED", True):
|
||||||
|
return jsonify({"error": "Not found"}), 404
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
|
|
|
||||||
36
backend/gunicorn.conf.py
Normal file
36
backend/gunicorn.conf.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""Gunicorn configuration.
|
||||||
|
|
||||||
|
All Gunicorn settings live here instead of the Dockerfile CMD so they can be
|
||||||
|
tweaked without rebuilding the image, and so the access-log format/timezone are
|
||||||
|
defined in one place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Server ----
|
||||||
|
bind = "0.0.0.0:8000"
|
||||||
|
workers = 2
|
||||||
|
|
||||||
|
# ---- Logging ----
|
||||||
|
accesslog = "-"
|
||||||
|
errorlog = "-"
|
||||||
|
capture_output = True
|
||||||
|
loglevel = "info"
|
||||||
|
|
||||||
|
# %(h)s = real client IP. Gunicorn resolves X-Forwarded-For based on its built-in
|
||||||
|
# proxy trust logic (the same mechanism already validated behind Traefik).
|
||||||
|
# %(t)s = overridden by CustomLogger -> [2026-08-09 20:41:00 +0300]
|
||||||
|
access_log_format = '%(h)s - - [%(t)s] "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||||
|
logger_class = "gunicorn_logger.CustomLogger"
|
||||||
|
|
||||||
|
# logger_class = CustomLogger
|
||||||
|
|
||||||
|
# ---- Timezone for Python logging / error logs ----
|
||||||
|
# Default to UTC unless TZ is set (e.g. TZ=Africa/Nairobi at docker run time).
|
||||||
|
os.environ.setdefault("TZ", os.environ.get("TZ", "UTC"))
|
||||||
|
try:
|
||||||
|
time.tzset()
|
||||||
|
except AttributeError: # non-Unix
|
||||||
|
pass
|
||||||
29
backend/gunicorn_logger.py
Normal file
29
backend/gunicorn_logger.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Custom Gunicorn logger.
|
||||||
|
|
||||||
|
Overrides the default timestamp atom `%(t)s` in access logs from Gunicorn's
|
||||||
|
default format `[09/Aug/2026:17:42:48 +0000]` to an ISO-like local-time format
|
||||||
|
`[2026-08-09 20:41:00 +0300]`, honoring the TZ environment variable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from gunicorn.glogging import Logger
|
||||||
|
|
||||||
|
TIMEZONE = os.environ.get("TZ", "UTC")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLogger(Logger):
|
||||||
|
"""Gunicorn logger that emits timezone-aware ISO timestamps."""
|
||||||
|
|
||||||
|
def atoms(self, resp, req, environ, request_time):
|
||||||
|
"""Build the log atoms, overriding the time atom with a custom format."""
|
||||||
|
atoms = super().atoms(resp, req, environ, request_time)
|
||||||
|
try:
|
||||||
|
now = datetime.now(ZoneInfo(TIMEZONE))
|
||||||
|
except Exception:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
# [09/Aug/2026:17:42:48 +0000] -> [2026-08-09 20:41:00 +0300]
|
||||||
|
atoms["t"] = f"[{now.strftime('%Y-%m-%d %H:%M:%S %z')}]"
|
||||||
|
return atoms
|
||||||
|
|
@ -82,6 +82,23 @@ class TestAuthRoutes:
|
||||||
data = response.get_json()
|
data = response.get_json()
|
||||||
assert "already exists" in data["error"].lower()
|
assert "already exists" in data["error"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.auth
|
||||||
|
def test_register_disabled(self, app, client, monkeypatch):
|
||||||
|
"""Test registration returns 404 when disabled via config"""
|
||||||
|
monkeypatch.setitem(app.config, "REGISTRATION_ENABLED", False)
|
||||||
|
response = client.post(
|
||||||
|
"/api/auth/register",
|
||||||
|
json={
|
||||||
|
"email": "disabled@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"confirm_password": "password123",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
data = response.get_json()
|
||||||
|
assert data["error"] == "Not found"
|
||||||
|
|
||||||
@pytest.mark.auth
|
@pytest.mark.auth
|
||||||
def test_login_success(self, client, regular_user):
|
def test_login_success(self, client, regular_user):
|
||||||
"""Test successful login"""
|
"""Test successful login"""
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import os
|
import os
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
env = os.environ.get('FLASK_ENV', 'dev')
|
env = os.environ.get('FLASK_ENV', 'dev')
|
||||||
app = create_app(env)
|
app = create_app(env)
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=8000)
|
app.run(host='0.0.0.0', port=8000)
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: crafting-shop-backend
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
environment:
|
|
||||||
- FLASK_ENV=${FLASK_ENV:-prod}
|
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
|
||||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
volumes:
|
|
||||||
- backend-data:/app/instance
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: crafting-shop-frontend
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: crafting-shop-postgres
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-crafting}
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-crafting_shop}
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: crafting-shop-redis
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
minio:
|
|
||||||
image: minio/minio:latest
|
|
||||||
container_name: crafting-shop-minio
|
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
- "9001:9001"
|
|
||||||
environment:
|
|
||||||
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-minioadmin}
|
|
||||||
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-minioadmin}
|
|
||||||
volumes:
|
|
||||||
- minio-data:/data
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 20s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
celery_worker:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: crafting-shop-celery-worker
|
|
||||||
command: celery -A celery_worker worker --loglevel=info --concurrency=4
|
|
||||||
environment:
|
|
||||||
- FLASK_ENV=${FLASK_ENV:-prod}
|
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
|
||||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- postgres
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "celery", "-A", "celery_worker", "inspect", "ping", "-d", "celery@$$HOSTNAME"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
celery_beat:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: crafting-shop-celery-beat
|
|
||||||
command: celery -A celery_worker beat --loglevel=info
|
|
||||||
environment:
|
|
||||||
- FLASK_ENV=${FLASK_ENV:-prod}
|
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
|
||||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- postgres
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- celery-beat-data:/app/celerybeat
|
|
||||||
|
|
||||||
flower:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: crafting-shop-flower
|
|
||||||
command: celery -A celery_worker flower --port=5555
|
|
||||||
ports:
|
|
||||||
- "5555:5555"
|
|
||||||
environment:
|
|
||||||
- FLASK_ENV=${FLASK_ENV:-prod}
|
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
|
||||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- celery_worker
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
prometheus:
|
|
||||||
image: prom/prometheus:latest
|
|
||||||
container_name: crafting-shop-prometheus
|
|
||||||
ports:
|
|
||||||
- "9090:9090"
|
|
||||||
volumes:
|
|
||||||
- ./infrastructure/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
|
|
||||||
- prometheus-data:/prometheus
|
|
||||||
command:
|
|
||||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
|
||||||
- '--storage.tsdb.path=/prometheus'
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana:latest
|
|
||||||
container_name: crafting-shop-grafana
|
|
||||||
ports:
|
|
||||||
- "3001:3000"
|
|
||||||
environment:
|
|
||||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
|
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
|
|
||||||
volumes:
|
|
||||||
- grafana-data:/var/lib/grafana
|
|
||||||
networks:
|
|
||||||
- crafting-shop-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
redis-data:
|
|
||||||
prometheus-data:
|
|
||||||
grafana-data:
|
|
||||||
backend-data:
|
|
||||||
celery-beat-data:
|
|
||||||
minio-data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
crafting-shop-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
FROM node:18-alpine AS frontend-build
|
FROM node:18-alpine AS frontend-build
|
||||||
|
|
||||||
|
# Build-time configurable: pass --build-arg REGISTRATION_ENABLED=false to disable registration
|
||||||
|
ARG REGISTRATION_ENABLED=true
|
||||||
|
ENV VITE_REGISTRATION_ENABLED=$REGISTRATION_ENABLED
|
||||||
|
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
# Copy package files first for better caching
|
# Copy package files first for better caching
|
||||||
|
|
@ -20,6 +24,10 @@ FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Build-time configurable for the backend: pass --build-arg REGISTRATION_ENABLED=false to disable registration
|
||||||
|
ARG REGISTRATION_ENABLED=true
|
||||||
|
ENV REGISTRATION_ENABLED=$REGISTRATION_ENABLED
|
||||||
|
|
||||||
# Install system dependencies (if needed for python packages)
|
# Install system dependencies (if needed for python packages)
|
||||||
# RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*
|
# RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
@ -41,10 +49,13 @@ USER appuser
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Run with Gunicorn (Production WSGI Server)
|
# # Run with Gunicorn (Production WSGI Server)
|
||||||
# --bind 0.0.0.0 makes it accessible outside the container
|
# # --bind 0.0.0.0 makes it accessible outside the container
|
||||||
# --access-logfile - sends access logs to stdout
|
# # --access-logfile - sends access logs to stdout
|
||||||
# --error-logfile - sends error logs to stderr
|
# # --error-logfile - sends error logs to stderr
|
||||||
# --capture-output ensures all output is captured
|
# # --capture-output ensures all output is captured
|
||||||
# --log-level info sets the logging level
|
# # --log-level info sets the logging level
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--access-logfile", "-", "--error-logfile", "-", "--capture-output", "--log-level", "info", "wsgi:app"]
|
# CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--access-logfile", "-", "--access-logformat", "%({x-forwarded-for}i)s - - [%(t)s] \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\"", "--error-logfile", "-", "--capture-output", "--log-level", "info", "wsgi:app"]
|
||||||
|
|
||||||
|
ENV TZ=Africa/Nairobi
|
||||||
|
CMD ["gunicorn", "-c", "gunicorn.conf.py", "wsgi:app"]
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Add real IP logging to Nginx access logs
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://backend:5000;
|
|
||||||
|
|
||||||
# Forward the real IP
|
|
||||||
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;
|
|
||||||
|
|
||||||
# Add these headers too
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /health {
|
|
||||||
proxy_pass http://backend:5000;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+text text/javascript;
|
|
||||||
}
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Navbar } from './components/Navbar';
|
||||||
import { Home } from './pages/Home';
|
import { Home } from './pages/Home';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import { Register } from './pages/Register';
|
import { Register } from './pages/Register';
|
||||||
|
import { REGISTRATION_ENABLED } from './config/registration';
|
||||||
import Profile from './pages/Profile';
|
import Profile from './pages/Profile';
|
||||||
import { ProtectedRoute } from './components/ProtectedRoute';
|
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||||
import { Boards } from './pages/Boards';
|
import { Boards } from './pages/Boards';
|
||||||
|
|
@ -26,6 +27,7 @@ import CreateWiki from './pages/CreateWiki';
|
||||||
import { WikiDetail } from './pages/WikiDetail';
|
import { WikiDetail } from './pages/WikiDetail';
|
||||||
import { EditWiki } from './pages/EditWiki';
|
import { EditWiki } from './pages/EditWiki';
|
||||||
import { CardDetail } from './pages/CardDetail';
|
import { CardDetail } from './pages/CardDetail';
|
||||||
|
import { NotFound } from './pages/NotFound';
|
||||||
import { BoardDetailLayout } from './components/BoardDetailLayout';
|
import { BoardDetailLayout } from './components/BoardDetailLayout';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
|
@ -59,7 +61,10 @@ const App = () => {
|
||||||
/>
|
/>
|
||||||
<Route path="/home" element={<Home />} />
|
<Route path="/home" element={<Home />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route
|
||||||
|
path="/register"
|
||||||
|
element={REGISTRATION_ENABLED ? <Register /> : <Navigate to="/login" replace />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/profile"
|
path="/profile"
|
||||||
element={
|
element={
|
||||||
|
|
@ -196,6 +201,7 @@ const App = () => {
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
{/* Order matters for Z-Index: Loader (70) > Toast (60) > Modal (50) */}
|
{/* Order matters for Z-Index: Loader (70) > Toast (60) > Modal (50) */}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useState } from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import { TaskboardLogo } from './TaskboardLogo';
|
import { TaskboardLogo } from './TaskboardLogo';
|
||||||
|
import { REGISTRATION_ENABLED } from '../config/registration';
|
||||||
import MenuIcon from './icons/MenuIcon';
|
import MenuIcon from './icons/MenuIcon';
|
||||||
import CloseIcon from './icons/CloseIcon';
|
import CloseIcon from './icons/CloseIcon';
|
||||||
import UserIcon from './icons/UserIcon';
|
import UserIcon from './icons/UserIcon';
|
||||||
|
|
@ -69,12 +70,14 @@ export function Navbar() {
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
{REGISTRATION_ENABLED && (
|
||||||
to="/register"
|
<Link
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
|
to="/register"
|
||||||
>
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
|
||||||
Register
|
>
|
||||||
</Link>
|
Register
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,13 +139,15 @@ export function Navbar() {
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
{REGISTRATION_ENABLED && (
|
||||||
to="/register"
|
<Link
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white block px-3 py-2 rounded-md text-base font-medium"
|
to="/register"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
className="bg-blue-600 hover:bg-blue-700 text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
>
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
Register
|
>
|
||||||
</Link>
|
Register
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
5
frontend/src/config/registration.ts
Normal file
5
frontend/src/config/registration.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Inlined at build time by Vite via import.meta.env.
|
||||||
|
// Unset (local dev / no build arg) → registration is enabled (backward compatible).
|
||||||
|
// Set to "false" via docker build --build-arg REGISTRATION_ENABLED=false → registration disabled.
|
||||||
|
export const REGISTRATION_ENABLED =
|
||||||
|
import.meta.env.VITE_REGISTRATION_ENABLED !== 'false';
|
||||||
|
|
@ -3,7 +3,7 @@ import { useApi } from './useApi';
|
||||||
import { useLoader } from '../context/loaders/useLoader';
|
import { useLoader } from '../context/loaders/useLoader';
|
||||||
import { useToast } from '../context/toasts/useToast';
|
import { useToast } from '../context/toasts/useToast';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { User } from '../types';
|
import { RegisterData, User } from '../types';
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -58,13 +58,7 @@ export function useAuth() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegister = async (userData: {
|
const handleRegister = async (userData: RegisterData) => {
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
username: string;
|
|
||||||
first_name?: string;
|
|
||||||
last_name?: string;
|
|
||||||
}) => {
|
|
||||||
try {
|
try {
|
||||||
const response = await withLoader(() => registerApi(userData), 'Creating account...');
|
const response = await withLoader(() => registerApi(userData), 'Creating account...');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useLoader } from '../context/loaders/useLoader';
|
||||||
import { useToast } from '../context/toasts/useToast';
|
import { useToast } from '../context/toasts/useToast';
|
||||||
import { Card } from '../types/kanban';
|
import { Card } from '../types/kanban';
|
||||||
|
|
||||||
export function useCardMutations(boardId: number, onUpdate: () => void) {
|
export function useCardMutations(_boardId: number, onUpdate: () => void) {
|
||||||
const { createCard, updateCard, deleteCard } = useApi();
|
const { createCard, updateCard, deleteCard } = useApi();
|
||||||
const { withLoader } = useLoader();
|
const { withLoader } = useLoader();
|
||||||
const { addNotification } = useToast();
|
const { addNotification } = useToast();
|
||||||
|
|
@ -69,7 +69,7 @@ export function useCardMutations(boardId: number, onUpdate: () => void) {
|
||||||
|
|
||||||
const moveCard = async (
|
const moveCard = async (
|
||||||
card: Card,
|
card: Card,
|
||||||
fromListId: number,
|
_fromListId: number,
|
||||||
toListId: number,
|
toListId: number,
|
||||||
newPosition: number
|
newPosition: number
|
||||||
) => {
|
) => {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
|
import { REGISTRATION_ENABLED } from '../config/registration';
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
email: z.string().min(1, 'Email is required').email('Invalid email address'),
|
email: z.string().min(1, 'Email is required').email('Invalid email address'),
|
||||||
|
|
@ -81,12 +82,14 @@ export default function Login() {
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="mt-6 text-center text-gray-400">
|
{REGISTRATION_ENABLED && (
|
||||||
Don‘t have an account?
|
<p className="mt-6 text-center text-gray-400">
|
||||||
<Link to="/register" className="ml-2 text-blue-400 hover:text-blue-300">
|
Don‘t have an account?
|
||||||
Register
|
<Link to="/register" className="ml-2 text-blue-400 hover:text-blue-300">
|
||||||
</Link>
|
Register
|
||||||
</p>
|
</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
frontend/src/pages/NotFound.tsx
Normal file
30
frontend/src/pages/NotFound.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useDocumentTitle } from "../hooks/useDocumentTitle";
|
||||||
|
import { TaskboardLogo } from "../components/TaskboardLogo";
|
||||||
|
|
||||||
|
export function NotFound() {
|
||||||
|
useDocumentTitle("Page Not Found");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 p-8 mx-auto w-full max-w-7xl">
|
||||||
|
<div className="text-center py-24">
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<TaskboardLogo className="h-16 w-auto" />
|
||||||
|
</div>
|
||||||
|
<p className="text-7xl font-bold text-gray-600 mb-4">404</p>
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-4">Page Not Found</h1>
|
||||||
|
<p className="text-lg text-gray-400 mb-8 max-w-md mx-auto">
|
||||||
|
The page you are looking for doesn't exist or may have been moved.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="bg-gray-700 hover:bg-gray-600 text-white font-medium py-2.5 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Go Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
Loading…
Reference in a new issue