disable register page

This commit is contained in:
david 2026-08-09 15:49:45 +03:00
parent c8aa1e016c
commit 2d5e3032e0
14 changed files with 148 additions and 219 deletions

View file

@ -17,6 +17,11 @@ GRAFANA_PASSWORD=change-this-password-in-production
CELERY_BROKER_URL=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 server stays hidden - Flask proxies all requests
# MINIO_ENDPOINT: Internal Docker network address (for server-to-server communication)

View file

@ -16,6 +16,7 @@ class Config:
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"))
LOCKOUT_STRATEGY = os.environ.get("LOCKOUT_STRATEGY", "timeout") # "timeout" | "permanent"
REGISTRATION_ENABLED = os.environ.get("REGISTRATION_ENABLED", "true").lower() == "true"
# Celery Configuration
CELERY = {

View file

@ -1,6 +1,6 @@
"""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 (
create_access_token,
create_refresh_token,
@ -24,6 +24,9 @@ api_bp = Blueprint("api", __name__)
@api_bp.route("/auth/register", methods=["POST"])
def register():
"""Register a new user"""
if not current_app.config.get("REGISTRATION_ENABLED", True):
return jsonify({"error": "Not found"}), 404
try:
data = request.get_json()

33
backend/gunicorn.conf.py Normal file
View file

@ -0,0 +1,33 @@
"""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"
# ---- 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

View 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

View file

@ -82,6 +82,23 @@ class TestAuthRoutes:
data = response.get_json()
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
def test_login_success(self, client, regular_user):
"""Test successful login"""

View file

@ -1,8 +1,11 @@
import os
from app import create_app
from werkzeug.middleware.proxy_fix import ProxyFix
env = os.environ.get('FLASK_ENV', 'dev')
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__':
app.run(host='0.0.0.0', port=8000)

View file

@ -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

View file

@ -3,6 +3,10 @@
# ---------------------------------------------------------
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
# Copy package files first for better caching
@ -20,6 +24,10 @@ FROM python:3.11-slim
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)
# RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*
@ -41,10 +49,13 @@ USER appuser
# Expose port
EXPOSE 8000
# Run with Gunicorn (Production WSGI Server)
# --bind 0.0.0.0 makes it accessible outside the container
# --access-logfile - sends access logs to stdout
# --error-logfile - sends error logs to stderr
# --capture-output ensures all output is captured
# --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"]
# # Run with Gunicorn (Production WSGI Server)
# # --bind 0.0.0.0 makes it accessible outside the container
# # --access-logfile - sends access logs to stdout
# # --error-logfile - sends error logs to stderr
# # --capture-output ensures all output is captured
# # --log-level info sets the logging level
# 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"]

View file

@ -11,6 +11,7 @@ import { Navbar } from './components/Navbar';
import { Home } from './pages/Home';
import Login from './pages/Login';
import { Register } from './pages/Register';
import { REGISTRATION_ENABLED } from './config/registration';
import Profile from './pages/Profile';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Boards } from './pages/Boards';
@ -59,7 +60,10 @@ const App = () => {
/>
<Route path="/home" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/register"
element={REGISTRATION_ENABLED ? <Register /> : <Navigate to="/login" replace />}
/>
<Route
path="/profile"
element={

View file

@ -3,6 +3,7 @@ import { useState } from 'react';
import { useApp } from '../context/AppContext';
import { useAuth } from '../hooks/useAuth';
import { TaskboardLogo } from './TaskboardLogo';
import { REGISTRATION_ENABLED } from '../config/registration';
import MenuIcon from './icons/MenuIcon';
import CloseIcon from './icons/CloseIcon';
import UserIcon from './icons/UserIcon';
@ -69,12 +70,14 @@ export function Navbar() {
>
Login
</Link>
{REGISTRATION_ENABLED && (
<Link
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>
)}
</>
)}
</div>
@ -136,6 +139,7 @@ export function Navbar() {
>
Login
</Link>
{REGISTRATION_ENABLED && (
<Link
to="/register"
className="bg-blue-600 hover:bg-blue-700 text-white block px-3 py-2 rounded-md text-base font-medium"
@ -143,6 +147,7 @@ export function Navbar() {
>
Register
</Link>
)}
</>
)}
</div>

View 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';

View file

@ -3,6 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Link, useSearchParams } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
import { REGISTRATION_ENABLED } from '../config/registration';
const loginSchema = z.object({
email: z.string().min(1, 'Email is required').email('Invalid email address'),
@ -81,12 +82,14 @@ export default function Login() {
</button>
</form>
{REGISTRATION_ENABLED && (
<p className="mt-6 text-center text-gray-400">
Don&lsquo;t have an account?
<Link to="/register" className="ml-2 text-blue-400 hover:text-blue-300">
Register
</Link>
</p>
)}
</div>
);
}

1
frontend/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />