fix(api): add refresh
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user