test
All checks were successful
/ upload (release) Successful in 25s

This commit is contained in:
pavel 2026-02-24 21:37:18 +01:00
commit d703501b78
12 changed files with 4062 additions and 72 deletions

View file

@ -13,7 +13,6 @@ use uuid::Uuid;
use crate::{AppState, db};
const SESSION_COOKIE: &str = "chattz_session";
const OAUTH_STATE_COOKIE: &str = "chattz_oauth_state";
const SESSION_TTL_SECS: u64 = 60 * 60 * 24 * 7;
@ -65,7 +64,13 @@ struct ErrorBody<'a> {
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(self.status, Json(ErrorBody { error: &self.message })).into_response()
(
self.status,
Json(ErrorBody {
error: &self.message,
}),
)
.into_response()
}
}
@ -84,9 +89,13 @@ where
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app = AppState::from_ref(state);
let token = read_cookie(parts, SESSION_COOKIE).ok_or_else(|| ApiError::unauthorized("missing session"))?;
let token = read_bearer_token(parts)
.or_else(|| read_query_token(parts))
.ok_or_else(|| ApiError::unauthorized("missing jwt token"))?;
let user_id = verify_session(&token, &app.settings.session_secret)
.map_err(|_| ApiError::unauthorized("invalid session"))?;
.map_err(|_| ApiError::unauthorized("invalid token"))?;
let exists = db::user_exists(&app.db, user_id)
.await
@ -129,7 +138,7 @@ pub fn read_oauth_state_from_headers(headers: &axum::http::HeaderMap) -> Option<
})
}
pub fn new_session_cookie(user_id: Uuid, secret: &str, secure: bool) -> Result<String> {
pub fn new_jwt_token(user_id: Uuid, secret: &str) -> Result<String> {
let now = now_ts();
let claims = SessionClaims {
sub: user_id.to_string(),
@ -144,20 +153,7 @@ pub fn new_session_cookie(user_id: Uuid, secret: &str, secure: bool) -> Result<S
)
.context("failed to encode session token")?;
Ok(format!(
"{name}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={ttl}{secure_flag}",
name = SESSION_COOKIE,
ttl = SESSION_TTL_SECS,
secure_flag = if secure { "; Secure" } else { "" }
))
}
pub fn clear_session_cookie(secure: bool) -> String {
format!(
"{name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0{secure_flag}",
name = SESSION_COOKIE,
secure_flag = if secure { "; Secure" } else { "" }
)
Ok(token)
}
fn verify_session(token: &str, secret: &str) -> Result<Uuid> {
@ -183,14 +179,32 @@ pub fn validate_oauth_state(expected_cookie: Option<String>, query_state: &str)
Ok(())
}
fn read_cookie(parts: &Parts, name: &str) -> Option<String> {
let raw = parts.headers.get(axum::http::header::COOKIE)?.to_str().ok()?;
raw.split(';').find_map(|pair| {
let mut kv = pair.trim().splitn(2, '=');
fn read_bearer_token(parts: &Parts) -> Option<String> {
let raw = parts
.headers
.get(axum::http::header::AUTHORIZATION)?
.to_str()
.ok()?;
if raw.starts_with("Bearer ") {
Some(raw["Bearer ".len()..].trim().to_string())
} else {
None
}
}
fn read_query_token(parts: &Parts) -> Option<String> {
let query = parts.uri.query()?;
// Simple query param parsing without pulling in url::Url overhead
for pair in query.split('&') {
let mut kv = pair.splitn(2, '=');
let key = kv.next()?;
let value = kv.next()?;
(key == name).then(|| value.to_string())
})
if key == "token" {
return Some(value.to_string());
}
}
None
}
fn now_ts() -> u64 {

View file

@ -213,20 +213,10 @@ async fn auth_callback(
.await
.map_err(|e| ApiError::internal(&format!("failed to persist user: {e}")))?;
let session_cookie = auth::new_session_cookie(
user.id,
&state.settings.session_secret,
state.settings.cookie_secure,
)
.map_err(|e| ApiError::internal(&e.to_string()))?;
let jwt_token = auth::new_jwt_token(user.id, &state.settings.session_secret)
.map_err(|e| ApiError::internal(&e.to_string()))?;
let mut headers = HeaderMap::new();
headers.append(
header::SET_COOKIE,
session_cookie
.parse()
.map_err(|_| ApiError::internal("failed to set session cookie"))?,
);
headers.append(
header::SET_COOKIE,
auth::clear_oauth_state_cookie(state.settings.cookie_secure)
@ -234,19 +224,11 @@ async fn auth_callback(
.map_err(|_| ApiError::internal("failed to clear oauth state cookie"))?,
);
Ok((headers, Redirect::to("/")))
Ok((headers, Redirect::to(&format!("/?token={}", jwt_token))))
}
async fn auth_logout(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
let mut headers = HeaderMap::new();
headers.append(
header::SET_COOKIE,
auth::clear_session_cookie(state.settings.cookie_secure)
.parse()
.map_err(|_| ApiError::internal("failed to clear session cookie"))?,
);
Ok((StatusCode::NO_CONTENT, headers))
async fn auth_logout() -> Result<impl IntoResponse, ApiError> {
Ok(StatusCode::NO_CONTENT)
}
#[derive(Serialize)]