diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9ea1680 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +__pycache__ +*.pyc +*.pyo +.pytest_cache +.git +.gitignore +.env +*.md +node_modules +.venv +venv +*.egg-info +.coverage +htmlcov +.mypy_cache diff --git a/.env.example b/.env.example index c6f9346..e4a89fa 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,14 @@ API_SERVER_URL=http://localhost:8000 UPLOAD_DIR=server/uploads MAX_UPLOAD_SIZE_MB=50 +# --- Setup Page --- +SETUP_PASSWORD= # Password per /api/setup, vuoto = disabilitato + +# --- Docker --- +DB_ROOT_PASSWORD=root_password_change_me +NGINX_PORT=80 +NGINX_SSL_PORT=443 + # --- HTTPS (Production) --- # SSL_CERTFILE=/path/to/cert.pem # SSL_KEYFILE=/path/to/key.pem diff --git a/README.md b/README.md new file mode 100644 index 0000000..244c214 --- /dev/null +++ b/README.md @@ -0,0 +1,392 @@ +# TieMeasureFlow by Tielogic + +Sistema di gestione task per misurazioni con calibro manuale. Soluzione tablet-first, multi-ruolo, con statistiche SPC (Statistical Process Control) integrate. + +## Architettura + +``` +┌─────────────┐ ┌──────────────┐ ┌──────────┐ +│ Browser / │────▶│ Nginx │────▶│ MySQL │ +│ Tablet │ │ (Reverse │ │ 8.0 │ +└─────────────┘ │ Proxy) │ └────▲─────┘ + └──┬───────┬───┘ │ + │ │ │ + ┌─────▼──┐ ┌──▼──────┐ │ + │ Flask │ │ FastAPI │───────┘ + │ Client │ │ Server │ + │ :5000 │ │ :8000 │ + └─────────┘ └─────────┘ +``` + +Il sistema è composto da: +- **Flask Client**: Frontend tablet-oriented con interfaccia responsive +- **FastAPI Server**: Backend API REST con autenticazione via API key +- **Nginx**: Reverse proxy per routing unificato +- **MySQL**: Database relazionale per persistenza dati + +## Stack Tecnologico + +### Backend (Server) +- **FastAPI**: Framework asincrono per API REST +- **SQLAlchemy 2.0**: ORM con supporto async/await +- **asyncmy**: Driver MySQL asincrono +- **MySQL 8.0**: Database relazionale +- **Alembic**: Gestione migrazioni database +- **WeasyPrint**: Generazione report PDF da HTML +- **Kaleido**: Export grafici Plotly in formato SVG + +### Frontend (Client) +- **Flask 3.0**: Framework web per rendering server-side +- **Jinja2**: Template engine +- **htmx 2.0**: Interazioni AJAX dichiarative +- **Alpine.js 3.x**: Reattività leggera lato client +- **TailwindCSS 3.x**: Framework CSS utility-first +- **Plotly.js**: Libreria grafici interattivi +- **Fabric.js 6.x**: Editor annotazioni su disegni tecnici +- **Flask-Babel**: Sistema i18n per IT/EN + +### Sicurezza e Auth +- **API Key**: Autenticazione via header X-API-Key +- **Rate Limiting**: Protezione endpoint sensibili +- **CORS**: Configurazione per deployment multi-dominio + +## Quick Start con Docker + +Il metodo raccomandato per installare e avviare TieMeasureFlow è tramite Docker Compose: + +```bash +# 1. Clone del repository +git clone +cd TieMeasureFlow + +# 2. Configurazione ambiente +cp .env.example .env +# Modifica .env con le tue credenziali (DB, password admin, ecc.) + +# 3. Avvio dei servizi +docker compose up -d + +# 4. Setup iniziale +# Apri http://localhost/api/setup nel browser +# Usa SETUP_PASSWORD configurata in .env per: +# - Inizializzare il database +# - Creare l'utente admin +# - (Opzionale) Caricare dati demo + +# 5. Accesso all'applicazione +# Apri http://localhost nel browser +# Login con credenziali admin create nel setup +``` + +L'applicazione sarà disponibile su: +- Frontend: http://localhost +- API Backend: http://localhost/api +- Setup Page: http://localhost/api/setup + +## Setup Manuale (Senza Docker) + +Per chi preferisce installare senza Docker: + +### Requisiti +- Python 3.11 o superiore +- Node.js 18 o superiore +- MySQL 8.0 + +### Installazione + +#### 1. Database MySQL +```bash +# Crea database +mysql -u root -p +CREATE DATABASE tiemeasureflow CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'tielogic'@'localhost' IDENTIFIED BY 'your_password'; +GRANT ALL PRIVILEGES ON tiemeasureflow.* TO 'tielogic'@'localhost'; +FLUSH PRIVILEGES; +``` + +#### 2. Configurazione +```bash +# Copia template configurazione +cp .env.example .env + +# Modifica .env con: +# - Credenziali database MySQL +# - Chiavi segrete +# - Password admin +``` + +#### 3. Server (FastAPI Backend) +```bash +cd server + +# Crea virtual environment +python -m venv venv +source venv/bin/activate # Su Windows: venv\Scripts\activate + +# Installa dipendenze +pip install -r requirements.txt + +# Esegui migrazioni database +alembic upgrade head + +# Avvia server +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +#### 4. Client (Flask Frontend) +```bash +cd client + +# Crea virtual environment +python -m venv venv +source venv/bin/activate # Su Windows: venv\Scripts\activate + +# Installa dipendenze +pip install -r requirements.txt + +# Build TailwindCSS +npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch + +# In un altro terminale: Compila traduzioni +cd client +pybabel compile -d translations + +# Avvia client +flask run --host 0.0.0.0 --port 5000 +``` + +Accesso: +- Frontend: http://localhost:5000 +- Backend API: http://localhost:8000 + +## Pagina di Setup + +TieMeasureFlow include una pagina di setup protetta accessibile su `/api/setup`: + +- **Protezione**: Richiede SETUP_PASSWORD configurata in .env +- **Disabilitazione**: Se SETUP_PASSWORD è vuota/assente, endpoint disabilitato +- **Accesso**: + - Via nginx (produzione): http://localhost/api/setup + - Diretto (sviluppo): http://localhost:8000/api/setup + +### Funzionalità Setup +- **Initialize Database**: Crea tutte le tabelle necessarie +- **Create Admin User**: Crea utente amministratore con credenziali da .env +- **Seed Demo Data**: Carica dati di esempio (ricette, misurazioni, utenti) +- **Reset Database**: Elimina e ricrea tutte le tabelle (attenzione: cancella tutti i dati!) + +## Struttura Progetto + +``` +TieMeasureFlow/ +├── server/ # FastAPI Backend (API REST) +│ ├── main.py # Entry point applicazione +│ ├── config.py # Configurazione da variabili .env +│ ├── database.py # SQLAlchemy async engine e sessioni +│ ├── models/ # ORM models (User, Recipe, Measurement, ecc.) +│ ├── schemas/ # Pydantic schemas per validazione I/O +│ ├── routers/ # API endpoints organizzati per dominio +│ ├── services/ # Business logic (SPC, PDF, auth, ecc.) +│ ├── middleware/ # Auth, logging, rate limiting +│ ├── migrations/ # Alembic migration scripts +│ └── templates/ # Template HTML per setup page +├── client/ # Flask Frontend (UI tablet) +│ ├── app.py # Entry point applicazione +│ ├── blueprints/ # Route blueprints (auth, maker, measurement, metrologist) +│ ├── services/ # API client wrapper per chiamate a FastAPI +│ ├── templates/ # Jinja2 templates per rendering pagine +│ ├── static/ # Asset statici (CSS, JS, immagini) +│ │ ├── css/ # TailwindCSS compiled output +│ │ ├── js/ # Alpine.js components e utility +│ │ └── images/ # Loghi, icone, placeholder +│ └── translations/ # File i18n per IT/EN (Flask-Babel) +├── nginx/ # Configurazione reverse proxy +│ └── nginx.conf # Routing unificato client/server +├── docker-compose.yml # Orchestrazione servizi Docker +├── .env.example # Template variabili d'ambiente +└── docs/ # Documentazione tecnica +``` + +## Ruoli Utente + +TieMeasureFlow supporta tre ruoli principali, combinabili: + +### Maker +Responsabile della creazione e modifica delle ricette di misurazione: +- Caricamento disegni tecnici (PDF) +- Annotazioni grafiche con Fabric.js (linee, frecce, testi, ellissi) +- Definizione task e subtask di misurazione +- Configurazione tolleranze (UTL, UWL, LWL, LTL) +- Gestione versioni immutabili (copy-on-write) + +### MeasurementTec +Operatore che esegue le misurazioni: +- Accesso rapido via barcode scanner +- Interfaccia task-driven per guida passo-passo +- Input valori da calibro USB HID (rilevamento automatico burst) +- Validazione real-time pass/fail su tolleranze +- Salvataggio annotazioni fotografiche + +### Metrologist +Analista qualità con accesso alle statistiche: +- Dashboard SPC con grafici Plotly.js (X-bar, R, Cp, Cpk) +- Filtri multi-dimensionali (ricetta, utente, periodo, stato) +- Export report PDF con WeasyPrint +- Analisi tendenze e capability del processo + +I ruoli sono configurabili in fase di creazione utente e possono essere combinati. Flag `is_admin` separato per gestione sistema. + +## Guida VPS Deployment + +### Requisiti VPS +- 2GB RAM minimo (4GB raccomandati) +- 20GB spazio disco +- Ubuntu 22.04 LTS o superiore +- Accesso SSH con privilegi sudo + +### Installazione Docker + +```bash +# Installa Docker +curl -fsSL https://get.docker.com | sh + +# Aggiungi utente al gruppo docker +sudo usermod -aG docker $USER + +# Installa Docker Compose (se non incluso) +sudo apt update +sudo apt install docker-compose-plugin + +# Ricarica gruppi (oppure logout/login) +newgrp docker +``` + +### Deploy Applicazione + +```bash +# Clone repository +git clone +cd TieMeasureFlow + +# Configura ambiente (IMPORTANTE: usa password sicure!) +cp .env.example .env +nano .env + +# Modifica almeno: +# - MYSQL_ROOT_PASSWORD +# - MYSQL_PASSWORD +# - SECRET_KEY +# - API_KEY_ADMIN +# - SETUP_PASSWORD + +# Avvia servizi +docker compose up -d + +# Verifica stato +docker compose ps +docker compose logs -f +``` + +### Configurazione Firewall + +```bash +# Abilita UFW +sudo ufw allow OpenSSH +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw enable + +# Verifica regole +sudo ufw status +``` + +### Configurazione DNS + +1. Accedi al pannello del tuo provider DNS +2. Crea un record A che punta il tuo dominio all'IP pubblico del VPS: + ``` + Type: A + Name: @ (o www) + Value: + TTL: 3600 + ``` +3. Attendi propagazione DNS (5-30 minuti) + +### SSL con Certbot (HTTPS) + +```bash +# Installa Certbot +sudo apt update +sudo apt install certbot + +# Genera certificati (usa il tuo dominio) +sudo certbot certonly --webroot -w /var/www/certbot -d yourdomain.com -d www.yourdomain.com + +# Decommentare linee SSL in nginx/nginx.conf +nano nginx/nginx.conf +# Rimuovi commenti (#) dalle righe: +# listen 443 ssl http2; +# ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + +# Riavvia nginx +docker compose restart nginx + +# Verifica rinnovo automatico +sudo certbot renew --dry-run +``` + +Accesso produzione: https://yourdomain.com + +## Comandi Utili + +| Comando | Descrizione | +|---------|-------------| +| `docker compose up -d` | Avvia tutti i servizi in background | +| `docker compose down` | Ferma e rimuove tutti i container | +| `docker compose logs -f server` | Segui log del server in tempo reale | +| `docker compose logs -f client` | Segui log del client in tempo reale | +| `docker compose ps` | Mostra stato di tutti i servizi | +| `docker compose exec server alembic upgrade head` | Esegui migrazioni database | +| `docker compose exec server alembic revision --autogenerate -m "description"` | Genera nuova migrazione | +| `docker compose restart nginx` | Riavvia solo nginx | +| `docker compose build --no-cache` | Ricostruisci tutte le immagini senza cache | +| `docker compose exec mysql mysql -u root -p tiemeasureflow` | Accedi alla CLI MySQL | +| `docker compose exec server python -c "from config import settings; print(settings.database_url)"` | Verifica configurazione server | + +## Sviluppo + +### Hot Reload +- Server FastAPI: Uvicorn con `--reload` flag (automatico in docker-compose) +- Client Flask: Flask debug mode (automatico in docker-compose) +- TailwindCSS: `--watch` flag per rebuild automatico + +### Migrazioni Database +```bash +# Genera nuova migrazione +cd server +alembic revision --autogenerate -m "Descrizione modifica" + +# Applica migrazioni +alembic upgrade head + +# Rollback ultima migrazione +alembic downgrade -1 +``` + +### Testing +```bash +# Test server (da implementare) +cd server +pytest + +# Test client (da implementare) +cd client +pytest +``` + +## Licenza + +Proprietary - Tielogic. All rights reserved. + +Questo software è di proprietà esclusiva di Tielogic ed è protetto dalle leggi sul copyright. Non è consentita la distribuzione, modifica o utilizzo senza autorizzazione scritta. diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..a656e62 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.11-slim AS base + +# Installa Node.js per Tailwind CSS build +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt gunicorn + +COPY . . + +# Build Tailwind CSS +RUN npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --minify + +# Compile Flask-Babel translations +RUN pybabel compile -d translations + +EXPOSE 5000 + +CMD ["gunicorn", "--workers", "2", "--bind", "0.0.0.0:5000", "--factory", "app:create_app"] diff --git a/docker-compose.yml b/docker-compose.yml index 19edda2..c1123a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,17 @@ -version: '3.8' - services: mysql: image: mysql:8.0 container_name: tmflow-mysql restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD: root_password_change_me - MYSQL_DATABASE: tiemeasureflow - MYSQL_USER: tmflow - MYSQL_PASSWORD: change_me_in_production - ports: - - "3306:3306" + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root_password_change_me} + MYSQL_DATABASE: ${DB_NAME:-tiemeasureflow} + MYSQL_USER: ${DB_USER:-tmflow} + MYSQL_PASSWORD: ${DB_PASSWORD:-change_me_in_production} volumes: - mysql_data:/var/lib/mysql command: > - --default-authentication-plugin=mysql_native_password + --authentication-policy=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci healthcheck: @@ -23,7 +19,67 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - tmflow-net + + server: + build: + context: ./server + dockerfile: Dockerfile + container_name: tmflow-server + restart: unless-stopped + env_file: + - .env + environment: + DB_HOST: mysql + UPLOAD_DIR: uploads + volumes: + - upload_data:/app/uploads + depends_on: + mysql: + condition: service_healthy + networks: + - tmflow-net + + client: + build: + context: ./client + dockerfile: Dockerfile + container_name: tmflow-client + restart: unless-stopped + env_file: + - .env + environment: + API_SERVER_URL: http://server:8000 + depends_on: + - server + networks: + - tmflow-net + + nginx: + image: nginx:alpine + container_name: tmflow-nginx + restart: unless-stopped + ports: + - "${NGINX_PORT:-80}:80" + - "${NGINX_SSL_PORT:-443}:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro + # Uncomment for SSL: + # - ./certbot/conf:/etc/letsencrypt:ro + # - ./certbot/www:/var/www/certbot:ro + depends_on: + - server + - client + networks: + - tmflow-net volumes: mysql_data: driver: local + upload_data: + driver: local + +networks: + tmflow-net: + driver: bridge diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..c38b266 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,66 @@ +upstream server_backend { + server server:8000; +} + +upstream client_frontend { + server client:5000; +} + +server { + listen 80; + server_name _; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml; + gzip_min_length 256; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Max upload size + client_max_body_size 50M; + + # API requests → FastAPI server + location /api/ { + proxy_pass http://server_backend; + 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; + proxy_read_timeout 120s; + } + + # Upload files served by server + location /uploads/ { + proxy_pass http://server_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # All other requests → Flask client + location / { + proxy_pass http://client_frontend; + 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; + } + + # Static file caching + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ { + proxy_pass http://client_frontend; + proxy_set_header Host $host; + expires 7d; + add_header Cache-Control "public, immutable"; + } + + # SSL configuration (uncomment when using Let's Encrypt) + # listen 443 ssl; + # ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + # ssl_protocols TLSv1.2 TLSv1.3; +} diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..794de71 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.11-slim + +# Installa dipendenze sistema per WeasyPrint +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libcairo2 \ + libgdk-pixbuf-2.0-0 \ + libffi-dev \ + shared-mime-info \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Crea directory uploads +RUN mkdir -p uploads/images uploads/pdfs uploads/logos uploads/reports + +EXPOSE 8000 + +# Entry point: Alembic upgrade + Uvicorn +CMD ["sh", "-c", "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2"] diff --git a/server/config.py b/server/config.py index 29ba580..fa54978 100644 --- a/server/config.py +++ b/server/config.py @@ -31,6 +31,9 @@ class Settings(BaseSettings): ssl_certfile: str | None = None ssl_keyfile: str | None = None + # Setup page (empty = disabled) + setup_password: str | None = None + @property def database_url(self) -> str: """Async MySQL connection string.""" diff --git a/server/main.py b/server/main.py index 8e2184e..9489a9d 100644 --- a/server/main.py +++ b/server/main.py @@ -19,6 +19,7 @@ from routers.files import router as files_router from routers.settings import router as settings_router from routers.reports import router as reports_router from routers.statistics import router as statistics_router +from routers.setup import router as setup_router @asynccontextmanager @@ -69,6 +70,7 @@ app.include_router(files_router) app.include_router(settings_router) app.include_router(statistics_router) app.include_router(reports_router) +app.include_router(setup_router) @app.get("/api/health") diff --git a/server/requirements.txt b/server/requirements.txt index a4fdb7e..ab73cac 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -18,6 +18,9 @@ bcrypt>=4.0.0 pillow>=10.0.0 python-multipart>=0.0.6 +# Templating (setup page) +jinja2>=3.1.0 + # Reports plotly>=5.0.0 kaleido>=0.2.0 diff --git a/server/routers/setup.py b/server/routers/setup.py new file mode 100644 index 0000000..ff79539 --- /dev/null +++ b/server/routers/setup.py @@ -0,0 +1,338 @@ +"""Setup page router - database init, admin creation, demo seed. + +Protected by SETUP_PASSWORD env var. When empty/None, all endpoints return 404. +NOT protected by API Key auth (standalone access). +""" +import secrets +from pathlib import Path + +from fastapi import APIRouter, HTTPException, Request, status +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from pydantic import BaseModel +from sqlalchemy import inspect as sa_inspect + +from config import settings +from database import Base, engine, async_session_factory +from models import ( + User, Recipe, RecipeVersion, RecipeTask, RecipeSubtask, +) +from services.auth_service import hash_password + +router = APIRouter(prefix="/api/setup", tags=["setup"]) + +_templates_dir = Path(__file__).resolve().parent.parent / "templates" +templates = Jinja2Templates(directory=str(_templates_dir)) + + +# --------------------------------------------------------------------------- +# Request schemas +# --------------------------------------------------------------------------- + +class PasswordBody(BaseModel): + password: str + + +class CreateAdminBody(BaseModel): + password: str + admin_username: str + admin_password: str + admin_email: str + admin_display_name: str + + +class ResetDbBody(BaseModel): + password: str + confirm: bool + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _check_setup_enabled() -> None: + """Raise 404 if setup password is not configured.""" + if not settings.setup_password: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + +def _check_password(password: str) -> None: + """Verify the provided password matches the setup password.""" + _check_setup_enabled() + if password != settings.setup_password: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Invalid setup password", + ) + + +# --------------------------------------------------------------------------- +# Endpoints +# --------------------------------------------------------------------------- + +@router.get("", response_class=HTMLResponse) +async def setup_page(request: Request): + """Serve the setup page HTML.""" + _check_setup_enabled() + return templates.TemplateResponse("setup/setup.html", {"request": request}) + + +@router.get("/status") +async def setup_status(): + """Return database table status (no password required, but setup must be enabled).""" + _check_setup_enabled() + async with engine.connect() as conn: + table_names: list[str] = await conn.run_sync( + lambda sync_conn: sa_inspect(sync_conn).get_table_names() + ) + return { + "tables_exist": len(table_names) > 0, + "table_count": len(table_names), + "tables": table_names, + } + + +@router.post("/init-db") +async def init_db(body: PasswordBody): + """Create all database tables.""" + _check_password(body.password) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + return {"status": "ok", "message": "Database tables created successfully"} + + +@router.post("/create-admin") +async def create_admin(body: CreateAdminBody): + """Create an admin user with all roles.""" + _check_password(body.password) + + api_key = secrets.token_hex(32) + + async with async_session_factory() as session: + async with session.begin(): + user = User( + username=body.admin_username, + password_hash=hash_password(body.admin_password), + email=body.admin_email, + display_name=body.admin_display_name, + roles=["Maker", "MeasurementTec", "Metrologist"], + is_admin=True, + api_key=api_key, + ) + session.add(user) + + return { + "status": "ok", + "message": f"Admin user '{body.admin_username}' created", + "api_key": api_key, + } + + +@router.post("/seed") +async def seed_demo_data(body: PasswordBody): + """Seed database with demo users and a sample recipe.""" + _check_password(body.password) + + async with async_session_factory() as session: + async with session.begin(): + # ---- Users -------------------------------------------------------- + users_data = [ + { + "username": "admin", + "password": "admin123", + "display_name": "Administrator", + "email": "admin@tiemeasureflow.local", + "roles": ["Maker", "MeasurementTec", "Metrologist"], + "is_admin": True, + }, + { + "username": "maker1", + "password": "maker123", + "display_name": "Mario Rossi", + "email": "maker1@tiemeasureflow.local", + "roles": ["Maker"], + "is_admin": False, + }, + { + "username": "tec1", + "password": "tec123", + "display_name": "Luca Bianchi", + "email": "tec1@tiemeasureflow.local", + "roles": ["MeasurementTec"], + "is_admin": False, + }, + { + "username": "metro1", + "password": "metro123", + "display_name": "Anna Verdi", + "email": "metro1@tiemeasureflow.local", + "roles": ["Metrologist"], + "is_admin": False, + }, + ] + + created_users: list[User] = [] + for u in users_data: + user = User( + username=u["username"], + password_hash=hash_password(u["password"]), + display_name=u["display_name"], + email=u["email"], + roles=u["roles"], + is_admin=u["is_admin"], + api_key=secrets.token_hex(32), + ) + session.add(user) + created_users.append(user) + + # Flush to get auto-generated user IDs + await session.flush() + + admin_user = created_users[0] + + # ---- Recipe ------------------------------------------------------- + recipe = Recipe( + code="DEMO-001", + name="Demo Measurement Recipe", + description="Sample recipe with shaft and surface measurements for demonstration purposes.", + created_by=admin_user.id, + ) + session.add(recipe) + await session.flush() + + version = RecipeVersion( + recipe_id=recipe.id, + version_number=1, + is_current=True, + created_by=admin_user.id, + change_notes="Initial demo version", + ) + session.add(version) + await session.flush() + + # ---- Task 1: Shaft Diameter ---------------------------------------- + task1 = RecipeTask( + version_id=version.id, + order_index=0, + title="Shaft Diameter Measurement", + directive="Measure the main shaft diameters at the indicated positions.", + description="Use the caliper to measure each diameter. Ensure the shaft is clean and at room temperature.", + ) + session.add(task1) + await session.flush() + + subtasks_1 = [ + RecipeSubtask( + task_id=task1.id, + marker_number=1, + description="D1 - Main shaft diameter", + measurement_type="diameter", + nominal=25.000, + utl=25.050, + uwl=25.030, + lwl=24.970, + ltl=24.950, + unit="mm", + ), + RecipeSubtask( + task_id=task1.id, + marker_number=2, + description="D2 - Bearing seat diameter", + measurement_type="diameter", + nominal=30.000, + utl=30.021, + uwl=30.015, + lwl=29.985, + ltl=29.979, + unit="mm", + ), + RecipeSubtask( + task_id=task1.id, + marker_number=3, + description="D3 - Shoulder diameter", + measurement_type="diameter", + nominal=35.000, + utl=35.039, + uwl=35.025, + lwl=34.975, + ltl=34.961, + unit="mm", + ), + ] + session.add_all(subtasks_1) + + # ---- Task 2: Surface Flatness -------------------------------------- + task2 = RecipeTask( + version_id=version.id, + order_index=1, + title="Surface Flatness Measurement", + directive="Measure surface heights at the marked reference points.", + description="Place the part on the granite surface plate. Zero the indicator at the reference point, then measure each position.", + ) + session.add(task2) + await session.flush() + + subtasks_2 = [ + RecipeSubtask( + task_id=task2.id, + marker_number=1, + description="F1 - Center reference height", + measurement_type="height", + nominal=0.000, + utl=0.020, + uwl=0.010, + lwl=-0.010, + ltl=-0.020, + unit="mm", + ), + RecipeSubtask( + task_id=task2.id, + marker_number=2, + description="F2 - Edge height (left)", + measurement_type="height", + nominal=0.000, + utl=0.030, + uwl=0.015, + lwl=-0.015, + ltl=-0.030, + unit="mm", + ), + RecipeSubtask( + task_id=task2.id, + marker_number=3, + description="F3 - Edge height (right)", + measurement_type="height", + nominal=0.000, + utl=0.030, + uwl=0.015, + lwl=-0.015, + ltl=-0.030, + unit="mm", + ), + ] + session.add_all(subtasks_2) + + return { + "status": "ok", + "message": "Demo data seeded successfully", + "users_created": [u["username"] for u in users_data], + "recipe_created": "DEMO-001", + } + + +@router.post("/reset-db") +async def reset_db(body: ResetDbBody): + """Drop all tables and recreate them. Requires confirm=true.""" + _check_password(body.password) + + if not body.confirm: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="You must set confirm=true to reset the database", + ) + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + await conn.run_sync(Base.metadata.create_all) + + return {"status": "ok", "message": "Database reset successfully"} diff --git a/server/templates/setup/setup.html b/server/templates/setup/setup.html new file mode 100644 index 0000000..38342d8 --- /dev/null +++ b/server/templates/setup/setup.html @@ -0,0 +1,546 @@ + + + + + + TieMeasureFlow — Setup + + + + + +
+ + + + +
+
+
TM
+

TieMeasureFlow

+

System Setup

+
+ +
+

Setup Password

+

+ Enter the SETUP_PASSWORD from your .env file to continue. +

+ + + +
+
+ + + + + + + + + + + +