This commit is contained in:
2026-07-16 18:43:24 +02:00
parent ae0176b49a
commit a04c3cd8e2
5 changed files with 69 additions and 15 deletions
+3
View File
@@ -27,3 +27,6 @@ frontend/node_modules/
/target /target
.antigravitycli/ .antigravitycli/
.sqlx/
backend/.sqlx/
dist/
+51 -12
View File
@@ -4,14 +4,15 @@ use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_ha
use axum::{ use axum::{
Json, Json,
extract::{Query, State}, extract::{Query, State},
http::StatusCode, http::{StatusCode, header::SET_COOKIE},
response::{IntoResponse, Redirect}, response::{Html, IntoResponse, Response},
}; };
use jsonwebtoken::{EncodingKey, Header};
use rand::{RngCore, thread_rng}; use rand::{RngCore, thread_rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::Row; use sqlx::Row;
use crate::AppState; use crate::{AppState, cookie::jwt::encode_token};
#[derive(Serialize)] #[derive(Serialize)]
pub struct QrCodeResponse { pub struct QrCodeResponse {
@@ -57,12 +58,15 @@ pub async fn generate_qr_token(
pub async fn validate_qr_token( pub async fn validate_qr_token(
State(data): State<Arc<AppState>>, State(data): State<Arc<AppState>>,
Query(params): Query<AuthQuery>, Query(params): Query<AuthQuery>,
) -> Result<impl IntoResponse, StatusCode> { ) -> Response {
println!("Incoming token: {}", params.token); let active_tokens_res = sqlx::query("SELECT user_id, token_hash FROM qr_codes")
let active_tokens = sqlx::query("SELECT user_id, token_hash FROM qr_codes")
.fetch_all(&data.db) .fetch_all(&data.db)
.await .await;
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let active_tokens = match active_tokens_res {
Ok(tokens) => tokens,
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
};
let mut authenticated_user_id: Option<i16> = None; let mut authenticated_user_id: Option<i16> = None;
let argon2 = Argon2::default(); let argon2 = Argon2::default();
@@ -82,10 +86,45 @@ pub async fn validate_qr_token(
} }
} }
if let Some(_user_id) = authenticated_user_id { if let Some(user_id) = authenticated_user_id {
let redirect_url = format!("{}/", data.env.origin.trim_end_matches('/')); let base_origin = data.env.origin.trim_end_matches('/');
Ok(Redirect::to(&redirect_url))
let header = Header::default();
let id_str = user_id.to_string();
let secret_bytes = data.env.token_secret.as_bytes();
let encoding_key = EncodingKey::from_secret(secret_bytes);
let token = encode_token(&header, id_str, &encoding_key);
let cookie_value = format!(
"token={}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600",
token
);
let redirect_html = format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url={}/" />
<script type="text/javascript">
window.location.href = "{}/"
</script>
<title>Redirecting...</title>
</head>
<body>
<p>Authenticated! Redirecting you to the dashboard...</p>
</body>
</html>"#,
base_origin, base_origin
);
let mut response = Html(redirect_html).into_response();
if let Ok(header_value) = axum::http::HeaderValue::from_str(&cookie_value) {
response.headers_mut().insert(SET_COOKIE, header_value);
}
response
} else { } else {
Err(StatusCode::UNAUTHORIZED) StatusCode::UNAUTHORIZED.into_response()
} }
} }
+13 -1
View File
@@ -1,7 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{ use axum::{
Router, middleware, Router,
http::StatusCode,
middleware,
response::IntoResponse,
routing::{get, post}, routing::{get, post},
}; };
@@ -18,6 +21,14 @@ use crate::{
}, },
}; };
async fn debug_fallback_handler(uri: axum::http::Uri) -> impl IntoResponse {
println!(
"[BACKEND DEBUG] Axum Fallback triggered! Unmatched request path: {}",
uri
);
(StatusCode::NOT_FOUND, "Backend route not found")
}
/// Creates the complete router with all API endpoints. /// Creates the complete router with all API endpoints.
/// ///
/// The router is organized in layers for proper middleware application. Uses [`AppState`] /// The router is organized in layers for proper middleware application. Uses [`AppState`]
@@ -90,5 +101,6 @@ pub fn create_router(state: Arc<AppState>) -> Router {
Router::new() Router::new()
.merge(protected_routes) .merge(protected_routes)
.merge(public_routes) .merge(public_routes)
.fallback(debug_fallback_handler)
.with_state(state) .with_state(state)
} }
+1 -1
View File
@@ -5,7 +5,7 @@
<link data-trunk rel="rust" data-bin="bin" /> <link data-trunk rel="rust" data-bin="bin" />
<link data-trunk rel="scss" href="src/styles/main.scss" /> <link data-trunk rel="scss" href="src/styles/main.scss" />
<link data-trunk rel="icon" href="src/assets/favicon.ico" type="image/x-icon" /> <link data-trunk rel="icon" href="src/assets/favicon.ico" type="image/x-icon" />
<link data-trunk rel="copy-dir" href="src/assets" /> <link data-trunk rel="copy-dir" href="src/assets" data-target-path="assets" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Yew App</title> <title>Yew App</title>
</head> </head>
+1 -1
View File
@@ -136,6 +136,6 @@ pub fn denied_component() -> Html {
#[component(Icon)] #[component(Icon)]
pub fn icon_component() -> Html { pub fn icon_component() -> Html {
html! { html! {
<img src="assets/csg.png" class="csg-icon" /> <img src="/assets/csg.png" class="csg-icon" />
} }
} }