feat: add password entry auth flow

This commit is contained in:
2026-04-21 14:50:42 +08:00
parent 5675c40119
commit c23088539e
18 changed files with 1146 additions and 25 deletions

View File

@@ -17,6 +17,7 @@ use crate::{
},
error_middleware::normalize_error_response,
health::health_check,
password_entry::password_entry,
request_context::{attach_request_context, resolve_request_id},
response_headers::propagate_request_id_header,
state::AppState,
@@ -49,6 +50,7 @@ pub fn build_router(state: AppState) -> Router {
"/api/assets/direct-upload-tickets",
post(create_direct_upload_ticket),
)
.route("/api/auth/entry", post(password_entry))
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
.layer(middleware::from_fn(normalize_error_response))
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
@@ -329,4 +331,179 @@ mod tests {
Value::Number(serde_json::Number::from(10))
);
}
#[tokio::test]
async fn password_entry_creates_user_and_sets_refresh_cookie() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
assert!(
response
.headers()
.get("set-cookie")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.contains("genarrative_refresh_session="))
);
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["user"]["username"],
Value::String("guest_001".to_string())
);
assert!(payload["token"].as_str().is_some());
}
#[tokio::test]
async fn password_entry_reuses_same_user_for_same_credentials() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let first_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("first request should succeed");
let first_body = first_response
.into_body()
.collect()
.await
.expect("first body should collect")
.to_bytes();
let first_payload: Value =
serde_json::from_slice(&first_body).expect("first payload should be json");
let second_response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("second request should succeed");
let second_body = second_response
.into_body()
.collect()
.await
.expect("second body should collect")
.to_bytes();
let second_payload: Value =
serde_json::from_slice(&second_body).expect("second payload should be json");
assert_eq!(first_payload["user"]["id"], second_payload["user"]["id"]);
}
#[tokio::test]
async fn password_entry_rejects_wrong_password() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
app.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("seed request should succeed");
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret999"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn password_entry_rejects_invalid_username() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "无效用户",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
}