auto-save 2026-05-15 15:15 (~4)
This commit is contained in:
@@ -1,19 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "8547db9",
|
||||
"message": "auto-save 2026-05-13 23:45 (~1)",
|
||||
"ts": "2026-05-13T23:46:07+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "093c924",
|
||||
"message": "auto-save 2026-05-13 23:51 (~1)",
|
||||
"ts": "2026-05-13T23:51:46+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 23:51 (~1)",
|
||||
@@ -3251,6 +3237,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 15:04 (~8)",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-15T15:10:14+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-15 15:10 (~2)",
|
||||
"hash": "8bdb797",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-15T07:14:46Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 4 项未提交变更 · 最近提交:auto-save 2026-05-15 15:10 (~2)",
|
||||
"files_changed": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
112
api/main.py
112
api/main.py
@@ -2,9 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
@@ -16,7 +19,7 @@ from typing import Literal
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import BackgroundTasks, FastAPI, File, HTTPException, UploadFile
|
||||
from fastapi import BackgroundTasks, FastAPI, File, HTTPException, Request, Response, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -91,6 +94,12 @@ VIDEO_MODEL_ALIASES = {
|
||||
}
|
||||
VIDEO_API_BASE_URL = os.getenv("VIDEO_API_BASE_URL", "").strip()
|
||||
VIDEO_API_KEY = os.getenv("VIDEO_API_KEY", "").strip()
|
||||
WEB_AUTH_USERNAME = os.getenv("WEB_AUTH_USERNAME", "").strip()
|
||||
WEB_AUTH_PASSWORD = os.getenv("WEB_AUTH_PASSWORD", "").strip()
|
||||
WEB_AUTH_SESSION_SECRET = os.getenv("WEB_AUTH_SESSION_SECRET", "").strip()
|
||||
WEB_AUTH_COOKIE_NAME = os.getenv("WEB_AUTH_COOKIE_NAME", "skg_marketing_session").strip() or "skg_marketing_session"
|
||||
WEB_AUTH_COOKIE_SECURE = os.getenv("WEB_AUTH_COOKIE_SECURE", "true").strip().lower() not in {"0", "false", "no"}
|
||||
WEB_AUTH_CONFIGURED = bool(WEB_AUTH_USERNAME and WEB_AUTH_PASSWORD and WEB_AUTH_SESSION_SECRET)
|
||||
|
||||
|
||||
def default_video_gateway_paths(base_url: str) -> tuple[str, str, str]:
|
||||
@@ -455,6 +464,12 @@ class Job(BaseModel):
|
||||
error: str = ""
|
||||
|
||||
|
||||
class AuthLoginPayload(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
remember: bool = False
|
||||
|
||||
|
||||
JOBS: dict[str, Job] = {}
|
||||
ANALYZE_QUEUE: list[AnalyzeTask] = []
|
||||
ANALYZE_WORKER_RUNNING = False
|
||||
@@ -462,6 +477,58 @@ AUDIO_WORKERS_RUNNING: set[str] = set()
|
||||
AUDIO_WORKERS_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def ensure_auth_configured() -> None:
|
||||
if not WEB_AUTH_CONFIGURED:
|
||||
raise HTTPException(503, "WEB_AUTH_USERNAME、WEB_AUTH_PASSWORD 或 WEB_AUTH_SESSION_SECRET 未配置")
|
||||
|
||||
|
||||
def _auth_signature(body: str) -> str:
|
||||
return hmac.new(WEB_AUTH_SESSION_SECRET.encode("utf-8"), body.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def _encode_auth_body(payload: dict) -> str:
|
||||
raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
def _decode_auth_body(body: str) -> dict:
|
||||
padded = body + "=" * (-len(body) % 4)
|
||||
raw = base64.urlsafe_b64decode(padded.encode("ascii"))
|
||||
data = json.loads(raw.decode("utf-8"))
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def make_auth_token(username: str, ttl_seconds: int) -> str:
|
||||
body = _encode_auth_body({
|
||||
"u": username,
|
||||
"exp": int(time.time()) + ttl_seconds,
|
||||
"n": secrets.token_hex(8),
|
||||
})
|
||||
return f"{body}.{_auth_signature(body)}"
|
||||
|
||||
|
||||
def verify_auth_token(token: str) -> str | None:
|
||||
if not WEB_AUTH_CONFIGURED or "." not in token:
|
||||
return None
|
||||
body, supplied_sig = token.rsplit(".", 1)
|
||||
if not hmac.compare_digest(_auth_signature(body), supplied_sig):
|
||||
return None
|
||||
try:
|
||||
payload = _decode_auth_body(body)
|
||||
username = str(payload.get("u") or "")
|
||||
expires_at = int(payload.get("exp") or 0)
|
||||
except Exception:
|
||||
return None
|
||||
if username != WEB_AUTH_USERNAME or expires_at < int(time.time()):
|
||||
return None
|
||||
return username
|
||||
|
||||
|
||||
def auth_username_from_request(request: Request) -> str | None:
|
||||
token = request.cookies.get(WEB_AUTH_COOKIE_NAME, "")
|
||||
return verify_auth_token(token)
|
||||
|
||||
|
||||
def job_dir(job_id: str) -> Path:
|
||||
d = JOBS_DIR / job_id
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
@@ -724,6 +791,48 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
|
||||
@app.get("/auth/check")
|
||||
def auth_check(request: Request) -> Response:
|
||||
ensure_auth_configured()
|
||||
if not auth_username_from_request(request):
|
||||
raise HTTPException(401, "unauthorized")
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@app.post("/auth/login")
|
||||
def auth_login(payload: AuthLoginPayload, response: Response) -> dict:
|
||||
ensure_auth_configured()
|
||||
username = payload.username.strip()
|
||||
password = payload.password
|
||||
valid_user = hmac.compare_digest(username, WEB_AUTH_USERNAME)
|
||||
valid_password = hmac.compare_digest(password, WEB_AUTH_PASSWORD)
|
||||
if not (valid_user and valid_password):
|
||||
raise HTTPException(401, "用户名或密码不正确")
|
||||
|
||||
ttl_seconds = 60 * 60 * 24 * 30 if payload.remember else 60 * 60 * 12
|
||||
response.set_cookie(
|
||||
key=WEB_AUTH_COOKIE_NAME,
|
||||
value=make_auth_token(WEB_AUTH_USERNAME, ttl_seconds),
|
||||
max_age=ttl_seconds,
|
||||
httponly=True,
|
||||
secure=WEB_AUTH_COOKIE_SECURE,
|
||||
samesite="lax",
|
||||
path="/",
|
||||
)
|
||||
return {"ok": True, "username": WEB_AUTH_USERNAME}
|
||||
|
||||
|
||||
@app.post("/auth/logout")
|
||||
def auth_logout(response: Response) -> dict:
|
||||
response.delete_cookie(
|
||||
key=WEB_AUTH_COOKIE_NAME,
|
||||
path="/",
|
||||
secure=WEB_AUTH_COOKIE_SECURE,
|
||||
samesite="lax",
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ---------- Pipeline 实现 ----------
|
||||
|
||||
def run(cmd: list[str], cwd: Path | None = None) -> str:
|
||||
@@ -2157,6 +2266,7 @@ def health() -> dict:
|
||||
return {
|
||||
"ok": True,
|
||||
"llm_configured": bool(LLM_API_KEY),
|
||||
"auth_configured": WEB_AUTH_CONFIGURED,
|
||||
"base_url": LLM_BASE_URL or "openai-default",
|
||||
"models": {
|
||||
"asr": ASR_MODEL,
|
||||
|
||||
@@ -4,17 +4,61 @@ server {
|
||||
|
||||
client_max_body_size 2g;
|
||||
|
||||
auth_basic "SKG Marketing Studio";
|
||||
auth_basic_user_file /etc/nginx/auth/.htpasswd;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;
|
||||
|
||||
location = /__auth {
|
||||
internal;
|
||||
proxy_pass http://skg-marketing-api:4291/auth/check;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
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;
|
||||
}
|
||||
|
||||
location = /api/auth/login {
|
||||
proxy_pass http://skg-marketing-api:4291/auth/login;
|
||||
proxy_http_version 1.1;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
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 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_connect_timeout 60s;
|
||||
}
|
||||
|
||||
location = /api/auth/logout {
|
||||
proxy_pass http://skg-marketing-api:4291/auth/logout;
|
||||
proxy_http_version 1.1;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
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 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_connect_timeout 60s;
|
||||
}
|
||||
|
||||
location @api_unauthorized {
|
||||
default_type application/json;
|
||||
return 401 '{"error":"unauthorized"}';
|
||||
}
|
||||
|
||||
location = /api {
|
||||
return 308 /api/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
auth_request /__auth;
|
||||
error_page 401 = @api_unauthorized;
|
||||
proxy_pass http://skg-marketing-api:4291/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_request_buffering off;
|
||||
@@ -28,7 +72,32 @@ server {
|
||||
proxy_connect_timeout 60s;
|
||||
}
|
||||
|
||||
location = /login {
|
||||
return 308 /login/;
|
||||
}
|
||||
|
||||
location /login/ {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /login/index.html;
|
||||
}
|
||||
|
||||
location /_next/ {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~* ^/(icon|apple-icon|favicon|manifest|placeholder).* {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location @login_redirect {
|
||||
return 302 /login/;
|
||||
}
|
||||
|
||||
location / {
|
||||
auth_request /__auth;
|
||||
error_page 401 = @login_redirect;
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ services:
|
||||
container_name: skg-marketing-web
|
||||
depends_on:
|
||||
- api
|
||||
volumes:
|
||||
- ./deploy/.htpasswd:/etc/nginx/auth/.htpasswd:ro
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- skg-marketing-internal
|
||||
|
||||
Reference in New Issue
Block a user