feat: add auth login options endpoint

This commit is contained in:
2026-04-21 17:02:55 +08:00
parent c3c5f1acd7
commit d234d27cc0
6 changed files with 184 additions and 5 deletions

View File

@@ -217,7 +217,8 @@
### 当前接口兼容
- [ ] 兼容 `/api/auth/login-options`
- [x] 兼容 `/api/auth/login-options`
交付物:[../docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/login_options.rs](../server-rs/crates/api-server/src/login_options.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
- [x] 兼容 `/api/auth/entry`
交付物:[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
- [x] 兼容 `/api/auth/me`
@@ -250,4 +251,4 @@
- [ ] 手机验证码主链可用
- [ ] 微信登录主链可用
说明:当前按“暂缓执行”处理,不作为当前连续阶段的阻塞项。
- [ ] 所有旧鉴权接口可通过 contract 回归
- [ ] 所有旧鉴权接口可通过 contract 回归

View File

@@ -0,0 +1,102 @@
# `/api/auth/login-options` 登录方式选项设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于冻结 Rust `api-server` 首版 `GET /api/auth/login-options` 的返回 contract、配置来源与当前阶段边界确保前端在登录页读取“当前可用登录方式”时不需要依赖硬编码开关。
## 2. 当前目标
当前阶段只解决一件事:
1.`Axum` 根据服务端配置,返回当前环境启用的登录方式列表。
本阶段明确不包含:
1. 短信或微信登录链路本身是否已经完整落地
2. 对前端返回更细粒度的 provider 配置
3. 第三方登录按钮文案、图标或 UI 布局规则
## 3. 接口 contract
### 3.1 请求
1. 方法:`GET`
2. 路径:`/api/auth/login-options`
3. 鉴权:不需要
4. 请求体:空
### 3.2 成功响应
```json
{
"availableLoginMethods": ["phone", "wechat"]
}
```
字段说明:
1. `availableLoginMethods` 为字符串数组
2. 当前阶段只允许出现:
- `phone`
- `wechat`
### 3.3 返回顺序
返回顺序固定为:
1.`phone`
2.`wechat`
这样可以保证前端按钮顺序稳定,不因配置解析顺序变化而漂移。
## 4. 配置来源
`api-server` 只读取以下布尔配置:
1. `SMS_AUTH_ENABLED`
2. `WECHAT_AUTH_ENABLED`
映射规则固定为:
1. `SMS_AUTH_ENABLED=true` 时返回 `phone`
2. `WECHAT_AUTH_ENABLED=true` 时返回 `wechat`
3. 两者都关闭时返回空数组
## 5. crate 边界
### 5.1 `api-server`
负责:
1. 读取 `AppState.config`
2. 组装 `availableLoginMethods`
3. 返回项目兼容的响应 envelope
### 5.2 `module-auth`
本接口当前阶段不依赖 `module-auth`
### 5.3 前端
负责:
1. 根据 `availableLoginMethods` 决定是否展示手机号 / 微信入口
2. 不再假设某种登录方式一定存在
## 6. 测试要求
至少覆盖:
1. 默认配置下返回空数组
2. 同时启用短信与微信时返回 `["phone", "wechat"]`
## 7. 完成定义
满足以下条件时,本任务视为完成:
1. Rust 已提供 `GET /api/auth/login-options`
2. 响应字段命名与前端约定一致
3. 配置开关可稳定映射到返回数组
4. 文档、任务清单与测试已同步更新

View File

@@ -4,6 +4,7 @@
## 文档列表
- [AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md](./AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md)`/api/auth/login-options` 首版设计,冻结登录方式列表 contract、配置开关来源与返回顺序。
- [AUTH_ME_QUERY_DESIGN_2026-04-21.md](./AUTH_ME_QUERY_DESIGN_2026-04-21.md)`/api/auth/me` 首版查询设计,冻结 Bearer JWT 衔接、`user + availableLoginMethods` 返回 contract以及用户不存在时的 `401` 语义。
- [AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md](./AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md)`/api/auth/logout-all` 全端登出设计,冻结全部 refresh session 吊销、`token_version` 递增、清 cookie 语义与 Rust 首版接口边界。
- [AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](./AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md)`/api/auth/sessions` 会话列表设计,冻结当前设备识别、多端登录字段映射、`clientLabel` 兼容策略与 Rust 首版接口边界。
@@ -43,4 +44,4 @@
## 使用建议
- 做实现选型时,优先看这一组。
- 做阶段排期时,把这一组和 `docs/planning/``docs/prd/` 一起看,更容易判断先后顺序。
- 做阶段排期时,把这一组和 `docs/planning/``docs/prd/` 一起看,更容易判断先后顺序。

View File

@@ -19,6 +19,7 @@ use crate::{
auth_sessions::auth_sessions,
error_middleware::normalize_error_response,
health::health_check,
login_options::auth_login_options,
logout::logout,
logout_all::logout_all,
password_entry::password_entry,
@@ -51,6 +52,10 @@ pub fn build_router(state: AppState) -> Router {
attach_refresh_session_token,
)),
)
.route(
"/api/auth/login-options",
get(auth_login_options),
)
.route(
"/api/auth/me",
get(auth_me).route_layer(middleware::from_fn_with_state(
@@ -438,6 +443,42 @@ mod tests {
assert!(payload["token"].as_str().is_some());
}
#[tokio::test]
async fn auth_login_options_returns_enabled_methods_in_stable_order() {
let config = AppConfig {
sms_auth_enabled: true,
wechat_auth_enabled: true,
..AppConfig::default()
};
let app = build_router(AppState::new(config).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.uri("/api/auth/login-options")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
assert_eq!(
payload["availableLoginMethods"],
serde_json::json!(["phone", "wechat"])
);
}
#[tokio::test]
async fn auth_sessions_returns_multi_device_session_fields() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
@@ -1238,4 +1279,4 @@ mod tests {
.is_some_and(|value| value.contains("Max-Age=0"))
);
}
}
}

View File

@@ -0,0 +1,33 @@
use axum::{
Json,
extract::{Extension, State},
};
use serde::Serialize;
use crate::{api_response::json_success_body, request_context::RequestContext, state::AppState};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthLoginOptionsResponse {
pub available_login_methods: Vec<&'static str>,
}
pub async fn auth_login_options(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
) -> Json<serde_json::Value> {
let mut methods = Vec::new();
if state.config.sms_auth_enabled {
methods.push("phone");
}
if state.config.wechat_auth_enabled {
methods.push("wechat");
}
json_success_body(
Some(&request_context),
AuthLoginOptionsResponse {
available_login_methods: methods,
},
)
}

View File

@@ -9,6 +9,7 @@ mod config;
mod error_middleware;
mod health;
mod http_error;
mod login_options;
mod logout;
mod logout_all;
mod password_entry;
@@ -44,4 +45,4 @@ async fn main() -> Result<(), std::io::Error> {
info!(%bind_address, "api-server 已完成 tracing 初始化并开始监听");
axum::serve(listener, router).await
}
}