feat: 添加后台登录验证码、记住密码功能,优化媒体资源与前端规范

- 新增后台登录图形验证码功能,完善登录安全防护
- 新增登录rememberMe参数,控制Refresh Token的会话持久化策略
- 实现OSS私有桶媒体URL自动签名,统一处理图片资源的临时访问签名
- 新增素材库数据库表与上传API,规范媒体资源管理流程
- 统一前端UI图标使用@element-plus/icons-vue,重构布局图标组件
- 登录页新增验证码输入、刷新功能,添加账号记忆与记住密码逻辑
- 更新全套文档,补充API契约、技术决策记录与集成流程说明
- 修复多个业务页面的图标展示问题,新增认证流程相关测试用例
This commit is contained in:
duanshuwen committed 2026-08-27 07:50:59 +08:00
1 parent 2c8c327de7
commit 6245159e7c
35 files changed
+914 -79

No files matched your search

+85 -3
View File
@@ -76,6 +76,32 @@ def test_in_memory_store_supports_login_limit_and_permission_cache():
assert store.get_permission_context("admin-1") == {"permissions": ["admin:read"]}
def test_in_memory_captcha_is_single_use():
store = InMemoryAdminSessionStore()
store.create_captcha("captcha-1", "ABCD", 120)
assert store.consume_captcha("captcha-1", "ABCD") is True
assert store.consume_captcha("captcha-1", "ABCD") is False
def test_admin_captcha_endpoint_returns_graphical_challenge():
store = InMemoryAdminSessionStore()
app = create_app()
app.dependency_overrides[get_admin_session_store] = lambda: store
try:
response = TestClient(app).get("/api/admin/auth/captcha")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
data = response.json()["data"]
assert data["captchaEnabled"] is True
assert data["captchaId"]
assert data["image"].startswith("data:image/svg+xml;base64,")
assert data["expiresIn"] == 120
def test_admin_access_token_contains_scoped_session_claims():
user = AdminUser(
id="admin-1",
@@ -109,12 +135,20 @@ def test_admin_login_keeps_legacy_fields_and_sets_http_only_refresh_cookie():
)
app = create_app()
app.dependency_overrides[get_db] = lambda: LoginDb(user)
app.dependency_overrides[get_admin_session_store] = lambda: InMemoryAdminSessionStore()
store = InMemoryAdminSessionStore()
store.create_captcha("captcha-login", "ABCD", 120)
app.dependency_overrides[get_admin_session_store] = lambda: store
try:
response = TestClient(app).post(
"/api/admin/auth/login",
json={"email": "admin@example.com", "password": "ChangeMe123!"},
json={
"email": "admin@example.com",
"password": "ChangeMe123!",
"captchaId": "captcha-login",
"captchaCode": "ABCD",
"rememberMe": False,
},
)
finally:
app.dependency_overrides.clear()
@@ -127,6 +161,7 @@ def test_admin_login_keeps_legacy_fields_and_sets_http_only_refresh_cookie():
cookie = response.headers["set-cookie"]
assert "HttpOnly" in cookie
assert "Path=/api/admin/auth" in cookie
assert "Max-Age=" not in cookie
def test_refresh_rotates_access_token_and_logout_revokes_it():
@@ -145,9 +180,16 @@ def test_refresh_rotates_access_token_and_logout_revokes_it():
try:
client = TestClient(app)
store.create_captcha("captcha-refresh", "ABCD", 120)
login_response = client.post(
"/api/admin/auth/login",
json={"email": "admin@example.com", "password": "ChangeMe123!"},
json={
"email": "admin@example.com",
"password": "ChangeMe123!",
"captchaId": "captcha-refresh",
"captchaCode": "ABCD",
"rememberMe": False,
},
)
old_token = login_response.json()["data"]["accessToken"]
@@ -155,6 +197,7 @@ def test_refresh_rotates_access_token_and_logout_revokes_it():
new_token = refresh_response.json()["data"]["accessToken"]
assert refresh_response.status_code == 200
assert "Max-Age=" not in refresh_response.headers["set-cookie"]
assert new_token != old_token
assert client.get("/api/admin/me", headers={"Authorization": f"Bearer {old_token}"}).status_code == 401
assert client.get("/api/admin/me", headers={"Authorization": f"Bearer {new_token}"}).status_code == 200
@@ -166,6 +209,45 @@ def test_refresh_rotates_access_token_and_logout_revokes_it():
app.dependency_overrides.clear()
def test_remember_me_keeps_persistent_cookie_after_refresh_rotation():
user = AdminUser(
id="admin-1",
email="admin@example.com",
name="Admin",
role="admin",
passwordHash=hash_password("ChangeMe123!", rounds=4),
isActive=True,
)
store = InMemoryAdminSessionStore()
app = create_app()
app.dependency_overrides[get_db] = lambda: LoginDb(user)
app.dependency_overrides[get_admin_session_store] = lambda: store
try:
client = TestClient(app)
store.create_captcha("captcha-remember", "ABCD", 120)
login_response = client.post(
"/api/admin/auth/login",
json={
"email": "admin@example.com",
"password": "ChangeMe123!",
"captchaId": "captcha-remember",
"captchaCode": "ABCD",
"rememberMe": True,
},
)
assert login_response.status_code == 200
assert "Max-Age=" in login_response.headers["set-cookie"]
refresh_response = client.post("/api/admin/auth/refresh")
assert refresh_response.status_code == 200
assert "Max-Age=" in refresh_response.headers["set-cookie"]
finally:
app.dependency_overrides.clear()
def test_rbac_models_define_normalized_association_tables():
assert AdminRole.__tablename__ == "AdminRole"
assert AdminMenu.__tablename__ == "AdminMenu"