fix(api): add refresh

This commit is contained in:
2026-02-10 14:43:48 +08:00
parent a11c742e70
commit 583fd521a2

View File

@@ -25,6 +25,11 @@ struct Code2TokenRequest {
client_secret: String,
}
#[derive(Debug, Deserialize, serde::Serialize)]
struct RefreshTokenRequest {
refresh_token: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Code2TokenData {
@@ -43,7 +48,9 @@ struct AppResponse<T> {
}
pub fn router() -> Router<AppState> {
Router::new().route("/callback", get(sso_callback_handler))
Router::new()
.route("/callback", get(sso_callback_handler))
.route("/refresh", get(refresh_token_handler))
}
fn is_https(headers: &axum::http::HeaderMap) -> bool {
@@ -74,6 +81,115 @@ fn cookie_header(
s
}
#[derive(Debug, Deserialize)]
pub struct RefreshTokenQuery {
pub token: String,
pub next: Option<String>,
}
pub async fn refresh_token_handler(
headers: axum::http::HeaderMap,
Query(q): Query<RefreshTokenQuery>,
) -> Result<axum::response::Response, AppError> {
if q.token.trim().is_empty() {
return Ok(Redirect::temporary("/auth-error?message=missing_token").into_response());
}
let iam_base = std::env::var("IAM_BASE_URL")
.or_else(|_| std::env::var("IAM_SERVICE_BASE_URL"))
.map_err(|_| AppError::ConfigError("IAM_BASE_URL is required".into()))?;
let http = reqwest::Client::new();
let resp = http
.post(format!(
"{}/api/v1/auth/refresh",
iam_base.trim_end_matches('/')
))
.json(&RefreshTokenRequest {
refresh_token: q.token,
})
.send()
.await
.map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?;
let status = resp.status();
// Assuming IAM service returns the same structure as LoginResponse which Code2TokenData roughly matches
// But LoginResponse structure is: access_token, refresh_token, token_type, expires_in.
// Code2TokenData has tenant_id, user_id extra?
// Let's check IAM service LoginResponse definition.
// IAM Service LoginResponse: access_token, refresh_token, token_type, expires_in.
// Wait, Code2TokenData expects tenant_id and user_id.
// Does IAM refresh endpoint return tenant_id and user_id?
// IAM Service LoginResponse struct in src/models.rs (iam-service) DOES NOT have tenant_id/user_id.
// So we cannot reuse Code2TokenData for refresh response parsing if we expect those fields.
// But usually refresh token response just updates access_token (and maybe refresh_token).
// TenantId and UserId should not change. We can keep existing cookies for them if we don't have them.
// But wait, we are setting cookies. If we don't get tenant_id/user_id, we can't set them (or we re-set them if we knew them).
// The previous cookies are still there. We just need to update access_token and refresh_token.
// Let's define a separate struct for Refresh Response if needed.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RefreshResponseData {
access_token: String,
refresh_token: String,
expires_in: usize,
}
let body = resp
.json::<AppResponse<RefreshResponseData>>()
.await
.map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?;
if !status.is_success() || body.code != 0 {
// Refresh failed, redirect to login
let login_url = resolve_front_redirect(q.next); // Actually redirect to front login page or handle error
// If refresh fails, we probably want to redirect to the original requested page so it can trigger login flow,
// OR redirect to auth-error.
// But the middleware calls this. If this returns redirect, the middleware will return redirect.
// If middleware sees error, it should redirect to login.
return Ok(Redirect::temporary("/auth-error?message=refresh_failed").into_response());
}
let Some(data) = body.data else {
return Ok(Redirect::temporary("/auth-error?message=invalid_refresh_response").into_response());
};
let target = resolve_front_redirect(q.next);
let secure = is_https(&headers);
let mut res = Redirect::temporary(&target).into_response();
let refresh_max_age = 30_u64 * 24 * 60 * 60;
res.headers_mut().append(
header::SET_COOKIE,
HeaderValue::from_str(&cookie_header(
"accessToken",
&data.access_token,
secure,
true,
Some(data.expires_in as u64),
))
.map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?,
);
res.headers_mut().append(
header::SET_COOKIE,
HeaderValue::from_str(&cookie_header(
"refreshToken",
&data.refresh_token,
secure,
true,
Some(refresh_max_age),
))
.map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?,
);
// We don't update tenantId/userId as we don't get them from refresh endpoint usually.
// They should persist.
Ok(res)
}
fn resolve_front_redirect(next: Option<String>) -> String {
let base = std::env::var("CMS_FRONT_BASE_URL").ok();