This commit is contained in:
aaron 2026-06-08 08:15:27 +08:00
parent ce7a203f3d
commit ca021d541e
5 changed files with 35 additions and 13 deletions

View File

@ -11,6 +11,7 @@ from app.web.shared import (
SendCodeRequest, SendCodeRequest,
VerifyEmailRequest, VerifyEmailRequest,
auth_error, auth_error,
has_active_subscription,
require_user, require_user,
) )
@ -108,6 +109,8 @@ async def api_auth_login(req: LoginRequest, request: Request = None):
async def api_auth_me(altcoin_session: str = Cookie(default="")): async def api_auth_me(altcoin_session: str = Cookie(default="")):
user = require_user(altcoin_session) user = require_user(altcoin_session)
sub = auth_db.get_current_subscription(user["id"]) sub = auth_db.get_current_subscription(user["id"])
if not user.get("local_debug") and not has_active_subscription(user):
raise HTTPException(status_code=402, detail="订阅已过期或未开通,请先开通订阅")
return {"ok": True, "user": user, "subscription": sub, "subscription_active": bool(sub)} return {"ok": True, "user": user, "subscription": sub, "subscription_active": bool(sub)}

View File

@ -148,10 +148,10 @@ def subscription_redirect():
def has_active_subscription(user) -> bool: def has_active_subscription(user) -> bool:
if is_local_request():
return True
if not user: if not user:
return False return False
if user.get("local_debug"):
return True
try: try:
if auth_db.is_user_admin(user["id"]): if auth_db.is_user_admin(user["id"]):
return True return True

View File

@ -266,7 +266,6 @@ a { color: inherit; text-decoration: none; }
<a class="sidebar-link {% if active_nav == 'subscription' %}active{% endif %}" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a> <a class="sidebar-link {% if active_nav == 'subscription' %}active{% endif %}" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link {% if active_nav == 'referral' %}active{% endif %}" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>邀请</a> <a class="sidebar-link {% if active_nav == 'referral' %}active{% endif %}" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>邀请</a>
<div class="sidebar-section-label admin-link" style="display:none">管理员菜单</div> <div class="sidebar-section-label admin-link" style="display:none">管理员菜单</div>
<a class="sidebar-link admin-link {% if active_nav == 'operations' %}active{% endif %}" href="/operations" target="_blank" rel="noopener" style="display:none"><svg class="link-icon"><use href="#svg-operations"/></svg>运行大屏</a>
<a class="sidebar-link admin-link {% if active_nav == 'paper_trading' %}active{% endif %}" href="/paper-trading" style="display:none"><svg class="link-icon"><use href="#svg-paper"/></svg>策略交易</a> <a class="sidebar-link admin-link {% if active_nav == 'paper_trading' %}active{% endif %}" href="/paper-trading" style="display:none"><svg class="link-icon"><use href="#svg-paper"/></svg>策略交易</a>
<a class="sidebar-link admin-link {% if active_nav == 'live_trading' %}active{% endif %}" href="/live-trading" style="display:none"><svg class="link-icon"><use href="#svg-shield"/></svg>实盘控制台</a> <a class="sidebar-link admin-link {% if active_nav == 'live_trading' %}active{% endif %}" href="/live-trading" style="display:none"><svg class="link-icon"><use href="#svg-shield"/></svg>实盘控制台</a>
<a class="sidebar-link admin-link {% if active_nav == 'review_center' %}active{% endif %}" href="/review-center" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>复盘中心</a> <a class="sidebar-link admin-link {% if active_nav == 'review_center' %}active{% endif %}" href="/review-center" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>复盘中心</a>
@ -328,6 +327,8 @@ window.addEventListener('orientationchange', function(){ setTimeout(setAppViewpo
async function loadUser() { async function loadUser() {
try { try {
var resp = await fetch(API + '/api/auth/me'); var resp = await fetch(API + '/api/auth/me');
if (resp.status === 401) { window.location.href = '/auth?tab=login'; return; }
if (resp.status === 402) { window.location.href = '/subscription?expired=1'; return; }
if (!resp.ok) return; if (!resp.ok) return;
var data = await resp.json(); var data = await resp.json();
currentUser = data.user; currentUser = data.user;

View File

@ -342,6 +342,7 @@ def test_sidebar_keeps_engineering_pages_in_admin_menu(temp_db):
html = resp.text html = resp.text
assert "机会中心" in html assert "机会中心" in html
assert "诊断中心" in html assert "诊断中心" in html
assert 'href="/operations"' not in html
assert 'href="/llm-insights"' not in html assert 'href="/llm-insights"' not in html
assert 'href="/data-export"' not in html assert 'href="/data-export"' not in html
assert 'href="/strategy"' not in html assert 'href="/strategy"' not in html

View File

@ -223,12 +223,11 @@ def test_auth_page_hides_internal_requirements_and_has_modern_member_copy(temp_a
assert forbidden not in html assert forbidden not in html
for expected in [ for expected in [
"提前发现机会,别在强信号后追高",
"登录或开启免费体验",
"创建账号", "创建账号",
"会员登录", "会员登录",
"前往订阅中心", "邮箱验证码",
"AlphaX Agent Crypto", "发送验证码",
"AlphaX Agent",
]: ]:
assert expected in html assert expected in html
@ -251,17 +250,35 @@ def test_subscription_page_owns_trial_and_plan_flow(temp_auth_db):
assert "USDT 订阅已预留表结构" not in html assert "USDT 订阅已预留表结构" not in html
def test_app_shell_returns_200_for_all_users(temp_auth_db): def test_app_shell_requires_active_subscription_for_real_users(temp_auth_db):
"""v2: /app 是纯壳页,不校验登录/订阅(鉴权由 JS 调用 /api/auth/me 完成)"""
client = TestClient(web_server.app) client = TestClient(web_server.app)
# 未登录也能拿到壳页JS自己判断跳转
assert client.get("/app").status_code == 200
assert "AlphaX Agent Crypto" in client.get("/app").text
# 登录用户也一样
reg = auth_db.register_user("alice@example.com", "StrongPass123") reg = auth_db.register_user("alice@example.com", "StrongPass123")
auth_db.verify_email("alice@example.com", reg["verification_code"]) auth_db.verify_email("alice@example.com", reg["verification_code"])
login = auth_db.login_user("alice@example.com", "StrongPass123") login = auth_db.login_user("alice@example.com", "StrongPass123")
token = login["token"] token = login["token"]
client.cookies.set("altcoin_session", token) client.cookies.set("altcoin_session", token)
expired_at = (datetime.now() - timedelta(days=1)).isoformat(timespec="seconds")
conn = auth_db.get_conn()
conn.execute(
"""
INSERT INTO user_subscription (user_id, plan_code, start_at, end_at, status, source, order_id, created_at, updated_at)
VALUES (%s, 'free_trial_1m', %s, %s, 'active', 'test', 0, %s, %s)
""",
(login["user"]["id"], (datetime.now() - timedelta(days=31)).isoformat(timespec="seconds"), expired_at, expired_at, expired_at),
)
conn.commit()
conn.close()
page = client.get("/app", follow_redirects=False)
me = client.get("/api/auth/me")
assert page.status_code == 302
assert page.headers["location"] == "/subscription?expired=1"
assert me.status_code == 402
sub = auth_db.claim_free_trial(login["user"]["id"])
assert sub["status"] == "active"
assert client.get("/app").status_code == 200 assert client.get("/app").status_code == 200
assert client.get("/api/auth/me").status_code == 200