feat: fix registration 500, add forgot-password flow, frontend validation
Some checks failed
Build and Deploy / Backend Tests (push) Has been cancelled
Build and Deploy / Frontend Build Check (push) Has been cancelled
Build and Deploy / Build Docker Images (push) Has been cancelled
Build and Deploy / Deploy to Server (push) Has been cancelled

- Fix MissingGreenlet: sync_engine now uses psycopg2 instead of asyncpg
- Fix bcrypt/passlib compat: pin bcrypt<4.1 in requirements
- Fix legacy password_hash NOT NULL: alter column to nullable in migration
- Add frontend password validation (uppercase + lowercase + digit)
- Add forgot-password and reset-password backend endpoints
- Add forgot-password and reset-password frontend pages
- Add email_service.py (SMTP via admin settings)
- Add reset_token/reset_token_expires columns to User model
- Migrate legacy JSON-only users to DB on password reset request
- Mount data/ volume in docker-compose.local.yml for persistence
- Add production deployment config (Dockerfile, nginx, deploy.sh)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-05-01 16:23:51 +02:00
parent 26bd096a06
commit 2f7347b4db
25 changed files with 2765 additions and 64 deletions

View File

@@ -1,7 +1,7 @@
# Document Translation API - Backend Dockerfile
# Multi-stage build for optimized production image
FROM python:3.11-slim as builder
FROM python:3.12-slim AS builder
WORKDIR /app
@@ -22,7 +22,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.11-slim as production
FROM python:3.12-slim AS production
WORKDIR /app

View File

@@ -1,49 +1,55 @@
# Document Translation Frontend - Dockerfile
# Office Translator - Frontend Dockerfile
# Multi-stage build for optimized production
# Build stage
# ---- Build stage ----
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
# Copy package files first (better layer caching)
COPY frontend/package*.json ./
# Install dependencies
RUN npm ci --only=production=false
# Install ALL dependencies (dev needed for build)
RUN npm install
# Copy source code
COPY frontend/ .
# Build arguments for environment
ARG NEXT_PUBLIC_API_URL=http://localhost:8000
# Build arguments
# NEXT_PUBLIC_API_URL: client-side API base (empty = relative URLs via proxy)
# BACKEND_URL: server-side proxy target (must be resolvable from container)
ARG NEXT_PUBLIC_API_URL=
ARG BACKEND_URL=http://backend:8000
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV BACKEND_URL=${BACKEND_URL}
# Build the application
# Build the application (with standalone output)
RUN npm run build
# Production stage
# ---- Production stage ----
FROM node:20-alpine AS production
WORKDIR /app
# Set NODE_ENV before copying
ENV NODE_ENV=production
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# Copy built assets from builder
# Copy standalone output from builder
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Set correct permissions
# Set correct ownership
RUN chown -R nextjs:nodejs /app
USER nextjs
# Environment
ENV NODE_ENV=production \
PORT=3000 \
# Runtime environment
ENV PORT=3000 \
HOSTNAME="0.0.0.0"
EXPOSE 3000

66
docker/frontend/server.js Normal file
View File

@@ -0,0 +1,66 @@
/**
* Custom Next.js standalone server.
* Proxies /api/* requests to BACKEND_URL (resolved at RUNTIME, not build time).
* This is needed because next.config.ts rewrites are baked in at build time.
*/
const { createServer } = require("http");
const { parse } = require("url");
const next = require("next");
const port = parseInt(process.env.PORT || "3000", 10);
const hostname = process.env.HOSTNAME || "0.0.0.0";
const dev = process.env.NODE_ENV !== "production";
const backendUrl = (process.env.BACKEND_URL || "http://127.0.0.1:8000").replace(/\/$/, "");
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer(async (req, res) => {
try {
const parsedUrl = parse(req.url, true);
const { pathname } = parsedUrl;
// Proxy /api/* to backend
if (pathname.startsWith("/api/")) {
const http = require("http");
const targetUrl = new URL(req.url, backendUrl);
const proxyReq = http.request(
targetUrl,
{
method: req.method,
headers: {
...req.headers,
host: targetUrl.host,
},
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
}
);
proxyReq.on("error", (err) => {
console.error(`Proxy error for ${req.url}:`, err.message);
res.writeHead(502);
res.end("Bad Gateway");
});
req.pipe(proxyReq, { end: true });
return;
}
// All other requests -> Next.js
await handle(req, res, parsedUrl);
} catch (err) {
console.error("Error occurred handling", req.url, err);
res.writeHead(500);
res.end("internal server error");
}
}).listen(port, hostname, () => {
console.log(`> Ready on http://${hostname}:${port}`);
console.log(`> API proxy: ${backendUrl}`);
});
});

View File

@@ -109,11 +109,9 @@ server {
proxy_set_header Connection "";
}
# Admin endpoints
# Admin UI -> Frontend (Next.js page)
location /admin {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass http://backend/admin;
proxy_pass http://frontend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -0,0 +1,123 @@
# Office Translator - Local Nginx Config (HTTP only, no SSL)
# Used by: docker-compose.local.yml
# Upstreams (backend, frontend) and rate limit zones are defined in nginx.conf
server {
listen 80;
listen [::]:80;
server_name _;
# File upload size
client_max_body_size 100M;
client_body_timeout 300s;
# Proxy settings
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# API routes -> Backend
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 10;
proxy_pass http://backend;
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;
proxy_set_header Connection "";
# CORS headers
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With, X-API-Key" always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
# File upload / translate endpoint
location /translate {
limit_req zone=upload_limit burst=5 nodelay;
limit_conn conn_limit 5;
proxy_pass http://backend/translate;
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;
proxy_connect_timeout 60s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
}
# Health check endpoint
location /health {
proxy_pass http://backend/health;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# Readiness check
location /ready {
proxy_pass http://backend/ready;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# OpenAPI docs
location /docs {
proxy_pass http://backend/docs;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location /redoc {
proxy_pass http://backend/redoc;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location /openapi.json {
proxy_pass http://backend/openapi.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
# Admin UI -> Frontend (Next.js page)
location /admin {
proxy_pass http://frontend;
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;
}
# Frontend static assets with caching
location /_next/static/ {
proxy_pass http://frontend;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Frontend -> Next.js
location / {
proxy_pass http://frontend;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}