diff --git a/deploy/nginx_172.31.170.47.conf b/deploy/nginx_172.31.170.47.conf index f35539cd..091caba3 100644 --- a/deploy/nginx_172.31.170.47.conf +++ b/deploy/nginx_172.31.170.47.conf @@ -50,9 +50,18 @@ server { proxy_set_header Host $host; } - # ── Legacy install.php redirect ── - location = /install.php { - return 301 /app/install.html; + # ── install.php (PHP-FPM for installer, root = /web/) ── + location ~ ^/install\.php$ { + root /www/wwwroot/api.synaglobal.vip/web; + fastcgi_pass unix:/tmp/php-cgi-82.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + + # ── Installer assets (CSS/JS in /web/) ── + location ~ ^/(css|js|vendor)/ { + root /www/wwwroot/api.synaglobal.vip/web; + try_files $uri =404; } # ── Static assets ── @@ -70,9 +79,9 @@ server { deny all; } - # ── SPA fallback: all paths → index.html ── + # ── Static HTML pages (no SPA — each page is standalone) ── location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ =404; } # ── Logging ── diff --git a/deploy/nginx_api.synaglobal.vip.conf b/deploy/nginx_api.synaglobal.vip.conf new file mode 100644 index 00000000..0a319b75 --- /dev/null +++ b/deploy/nginx_api.synaglobal.vip.conf @@ -0,0 +1,111 @@ +# Nexus v6.0 — Nginx Production Config (HTTPS) +# Domain: api.synaglobal.vip (宝塔面板管理) +# Backend: uvicorn 0.0.0.0:8600 +# Frontend: /www/wwwroot/api.synaglobal.vip/web/app/ (Tailwind CSS v4 + Alpine.js) + +# ── Upstream: Python FastAPI backend ── +upstream nexus_api { + server 127.0.0.1:8600; + keepalive 32; +} + +# ── HTTP → HTTPS redirect ── +server { + listen 80; + server_name api.synaglobal.vip; + return 301 https://$host$request_uri; +} + +# ── HTTPS ── +server { + listen 443 ssl http2; + server_name api.synaglobal.vip; + + # SSL (宝塔面板自动管理证书) + ssl_certificate /www/server/panel/vhost/cert/api.synaglobal.vip/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/api.synaglobal.vip/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # HSTS + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Document root = new Tailwind app directory + root /www/wwwroot/api.synaglobal.vip/web/app; + index index.html; + + # ── install.php (PHP-FPM for installer, root = /web/) ── + location ~ ^/install\.php$ { + root /www/wwwroot/api.synaglobal.vip/web; + fastcgi_pass unix:/tmp/php-cgi-82.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + + # ── Installer assets (CSS/JS in /web/) ── + location ~ ^/(css|js|vendor)/ { + root /www/wwwroot/api.synaglobal.vip/web; + try_files $uri =404; + } + + # ── Python API proxy ── + location /api/ { + proxy_pass http://nexus_api; + 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 300s; + proxy_send_timeout 300s; + proxy_connect_timeout 10s; + } + + # ── WebSocket proxy (alerts + WebSSH terminal) ── + location /ws/ { + proxy_pass http://nexus_api; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # ── Health endpoint ── + location /health { + proxy_pass http://nexus_api; + proxy_set_header Host $host; + } + + # ── Static assets (cache aggressively) ── + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 7d; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # ── Deny sensitive files ── + location ~ /\. { + deny all; + } + location ~ /data/config\.php$ { + deny all; + } + + # ── Static HTML pages (no SPA — each page is standalone) ── + location / { + try_files $uri $uri/ =404; + } + + # ── Logging ── + access_log /www/wwwlogs/api.synaglobal.vip.log; + error_log /www/wwwlogs/api.synaglobal.vip.error.log; + + # ── Upload limit ── + client_max_body_size 100m; +} diff --git a/requirements.txt b/requirements.txt index f0bbf782..570cd425 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ paramiko==3.5.0 asyncssh==2.17.0 PyJWT==2.10.1 bcrypt==4.2.1 +cryptography==44.0.0 pyyaml==6.0.2 rich==13.9.4 redis[hiredis]==5.2.1 \ No newline at end of file diff --git a/server/api/auth.py b/server/api/auth.py index 5548ec25..1334fa0a 100644 --- a/server/api/auth.py +++ b/server/api/auth.py @@ -29,7 +29,7 @@ class RefreshRequest(BaseModel): class LogoutRequest(BaseModel): - admin_id: int + refresh_token: Optional[str] = None class TotpSetupRequest(BaseModel): @@ -85,7 +85,10 @@ async def logout( service: AuthService = Depends(get_auth_service), ): """Invalidate refresh token (client should also discard access token)""" - result = await service.logout(payload.admin_id) + if payload.refresh_token: + result = await service.logout_by_token(payload.refresh_token) + else: + return {"success": True, "message": "已登出"} return result diff --git a/server/application/services/auth_service.py b/server/application/services/auth_service.py index ff0b6193..d11afc79 100644 --- a/server/application/services/auth_service.py +++ b/server/application/services/auth_service.py @@ -148,7 +148,7 @@ class AuthService: return admin async def logout(self, admin_id: int) -> dict: - """Invalidate refresh token""" + """Invalidate refresh token by admin ID""" admin = await self.admin_repo.get_by_id(admin_id) if admin: admin.jwt_refresh_token = None @@ -156,6 +156,16 @@ class AuthService: await self.admin_repo.update(admin) return {"success": True, "message": "已登出"} + async def logout_by_token(self, refresh_token: str) -> dict: + """Invalidate refresh token by token value (used by frontend logout)""" + admin = await self.admin_repo.get_by_refresh_token(refresh_token) + if admin: + admin.jwt_refresh_token = None + admin.jwt_token_expires = None + await self.admin_repo.update(admin) + await self._audit("logout", "admin", admin.id, f"Logout: {admin.username}") + return {"success": True, "message": "已登出"} + async def setup_totp(self, admin_id: int) -> dict: """Generate TOTP secret for an admin user (before enabling)""" import base64 diff --git a/server/domain/models/__init__.py b/server/domain/models/__init__.py index 94f33734..b989f8ce 100644 --- a/server/domain/models/__init__.py +++ b/server/domain/models/__init__.py @@ -13,8 +13,6 @@ from sqlalchemy import ( ) from sqlalchemy.orm import declarative_base, relationship -from cryptography.fernet import Fernet - Base = declarative_base() diff --git a/server/main.py b/server/main.py index 9e6ba929..02d9e944 100644 --- a/server/main.py +++ b/server/main.py @@ -171,8 +171,9 @@ app.add_middleware(DbSessionMiddleware) app.add_middleware( CORSMiddleware, allow_origins=[ - "http://172.31.170.47", # WSL development "https://api.synaglobal.vip", # Production HTTPS + "http://api.synaglobal.vip", # Production HTTP (redirects to HTTPS) + "http://172.31.170.47", # WSL development "http://localhost:8600", # Local dev ], allow_credentials=True, diff --git a/web/app/api.js b/web/app/api.js index c562d274..726de2f3 100644 --- a/web/app/api.js +++ b/web/app/api.js @@ -88,10 +88,11 @@ function doLogout() { const { access, refresh } = _getTokens(); if (access) { try { - navigator.sendBeacon( - API + '/auth/logout', - JSON.stringify({ refresh_token: refresh }) + const blob = new Blob( + [JSON.stringify({ refresh_token: refresh })], + { type: 'application/json' } ); + navigator.sendBeacon(API + '/auth/logout', blob); } catch {} } _logout();