disable register page
This commit is contained in:
parent
c8aa1e016c
commit
045435ffe6
12 changed files with 77 additions and 213 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
# 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/*
|
||||
|
||||
|
|
@ -47,4 +55,4 @@ EXPOSE 8000
|
|||
# --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"]
|
||||
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"]
|
||||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
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,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‘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
1
frontend/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
Loading…
Reference in a new issue