chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests) - Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR imports keep post/product types instead of collapsing to page - Docs: trade-pro theme port spec (R3) and implementation plan Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""One-owner authentication for the SiteForge Docker Web edition.
|
||||
|
||||
Session cookie is signed and HttpOnly. JSON mutations additionally require a
|
||||
session-bound CSRF header; this is intentionally a small-team / single-admin
|
||||
MVP, not a multi-tenant identity provider.
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
|
||||
from fastapi import HTTPException
|
||||
|
||||
COOKIE = 'siteforge_session'
|
||||
MAX_AGE = 8 * 3600
|
||||
SERIALIZER = None
|
||||
PASSWORD_HASH = b''
|
||||
CSRF_KEY = b''
|
||||
COOKIE_SECURE = False
|
||||
FAILURES = defaultdict(list)
|
||||
|
||||
|
||||
def configure():
|
||||
global SERIALIZER, PASSWORD_HASH, CSRF_KEY, COOKIE_SECURE
|
||||
password_file = os.environ.get('SITEFORGE_ADMIN_PASSWORD_FILE', '').strip()
|
||||
if password_file:
|
||||
password = Path(password_file).read_text(encoding='utf-8').strip('\r\n')
|
||||
else:
|
||||
password = os.environ.get('SITEFORGE_ADMIN_PASSWORD', '')
|
||||
secret = os.environ.get('SITEFORGE_SESSION_SECRET', '')
|
||||
weak = {'password', 'admin123456', 'change-me', 'replace-me', 'changeme', '123456789', 'REPLACE_WITH_STRONG_PASSWORD'}
|
||||
if len(password) < 12 or password in weak:
|
||||
raise RuntimeError('请设置至少 12 位的 SITEFORGE_ADMIN_PASSWORD,或配置 SITEFORGE_ADMIN_PASSWORD_FILE')
|
||||
if len(secret) < 32 or secret in weak or secret == 'REPLACE_WITH_64_RANDOM_CHARACTERS':
|
||||
raise RuntimeError('请配置至少 32 位的随机 SITEFORGE_SESSION_SECRET')
|
||||
SERIALIZER = URLSafeTimedSerializer(secret_key=secret, salt='siteforge-web-v02')
|
||||
PASSWORD_HASH = hashlib.sha256(password.encode('utf-8')).digest()
|
||||
CSRF_KEY = hashlib.sha256(('siteforge-csrf-v02:'+secret).encode()).digest()
|
||||
COOKIE_SECURE = os.environ.get('SITEFORGE_COOKIE_SECURE', 'false').lower() in ('1', 'true', 'yes')
|
||||
FAILURES.clear()
|
||||
|
||||
|
||||
def check_password(password):
|
||||
return hmac.compare_digest(hashlib.sha256(password.encode('utf-8')).digest(), PASSWORD_HASH)
|
||||
|
||||
|
||||
def sign_session():
|
||||
session_id = secrets.token_urlsafe(32)
|
||||
return SERIALIZER.dumps({'sub': 'admin', 'sid': session_id, 'v': 2})
|
||||
|
||||
|
||||
def sign_user_session(user_id):
|
||||
return SERIALIZER.dumps({'sub':'user','user_id':user_id,'sid':secrets.token_urlsafe(32),'v':3})
|
||||
|
||||
def read_session(cookie):
|
||||
if not cookie or SERIALIZER is None:
|
||||
return None
|
||||
try:
|
||||
value = SERIALIZER.loads(cookie, max_age=MAX_AGE)
|
||||
if (value.get('sub'),value.get('v')) not in (('admin',2),('user',3)):
|
||||
return None
|
||||
return value
|
||||
except (BadSignature, SignatureExpired, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def csrf(session):
|
||||
return hmac.new(CSRF_KEY, session['sid'].encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def verify_csrf(session, supplied):
|
||||
return bool(supplied) and hmac.compare_digest(csrf(session), supplied)
|
||||
|
||||
|
||||
def check_login_rate(remote):
|
||||
now = time.monotonic()
|
||||
FAILURES[remote] = [t for t in FAILURES[remote] if now-t < 900]
|
||||
if len(FAILURES[remote]) >= 8:
|
||||
raise HTTPException(429, '登录尝试次数过多,请稍后重试')
|
||||
|
||||
|
||||
def note_login_failure(remote):
|
||||
FAILURES[remote].append(time.monotonic())
|
||||
|
||||
|
||||
def note_login_success(remote):
|
||||
FAILURES.pop(remote, None)
|
||||
Reference in New Issue
Block a user