This commit is contained in:
2026-07-16 18:01:58 +02:00
parent b62bba0173
commit ae0176b49a
13 changed files with 371 additions and 8 deletions
+2
View File
@@ -17,3 +17,5 @@ argon2 = "0.5.3"
time = "0.3.47"
tower = "0.5.3"
tower-http = { version = "0.6.8", features = ["cors"] }
rand = '0.8'
hex = '0.4'
@@ -0,0 +1 @@
DROP TABLE qr_codes;
@@ -0,0 +1,8 @@
CREATE TABLE qr_codes (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id SMALLINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_qr_token_hash ON qr_codes(token_hash);
+1
View File
@@ -3,4 +3,5 @@
//! It serves as a central point for managing the logic that responds to various
//! HTTP requests, categorizing handlers by their domain (e.g., authentication, tickets).
pub mod auth;
pub mod qr;
pub mod ticket;
+91
View File
@@ -0,0 +1,91 @@
use std::sync::Arc;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Redirect},
};
use rand::{RngCore, thread_rng};
use serde::{Deserialize, Serialize};
use sqlx::Row;
use crate::AppState;
#[derive(Serialize)]
pub struct QrCodeResponse {
pub raw_token: String,
}
#[derive(Serialize, Deserialize)]
pub struct AuthQuery {
pub token: String,
}
#[derive(Deserialize)]
pub struct GenerateQrRequest {
pub user_id: i16,
}
pub async fn generate_qr_token(
State(data): State<Arc<AppState>>,
Json(payload): Json<GenerateQrRequest>,
) -> Result<impl IntoResponse, StatusCode> {
let mut token_bytes = [0u8; 32];
thread_rng().fill_bytes(&mut token_bytes);
let raw_token = hex::encode(token_bytes);
let salt = SaltString::generate(&mut thread_rng());
let argon2 = Argon2::default();
let token_hash = argon2
.hash_password(raw_token.as_bytes(), &salt)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.to_string();
sqlx::query("INSERT INTO qr_codes (user_id, token_hash) VALUES ($1, $2)")
.bind(payload.user_id)
.bind(token_hash)
.execute(&data.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok((StatusCode::CREATED, Json(QrCodeResponse { raw_token })))
}
pub async fn validate_qr_token(
State(data): State<Arc<AppState>>,
Query(params): Query<AuthQuery>,
) -> Result<impl IntoResponse, StatusCode> {
println!("Incoming token: {}", params.token);
let active_tokens = sqlx::query("SELECT user_id, token_hash FROM qr_codes")
.fetch_all(&data.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut authenticated_user_id: Option<i16> = None;
let argon2 = Argon2::default();
for record in active_tokens {
let user_id: i16 = record.get("user_id");
let token_hash: String = record.get("token_hash");
if let Ok(parsed_hash) = PasswordHash::new(&token_hash) {
if argon2
.verify_password(params.token.as_bytes(), &parsed_hash)
.is_ok()
{
authenticated_user_id = Some(user_id);
break;
}
}
}
if let Some(_user_id) = authenticated_user_id {
let redirect_url = format!("{}/", data.env.origin.trim_end_matches('/'));
Ok(Redirect::to(&redirect_url))
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
+10 -4
View File
@@ -10,9 +10,10 @@ use crate::{
cookie::validation::{validate_admin, validate_token},
handlers::{
auth::{
check_admin_exists, create_user, delete_user, get_current_user, get_user_by_id, get_users, login, logout,
setup_initial_admin, update_user,
check_admin_exists, create_user, delete_user, get_current_user, get_user_by_id,
get_users, login, logout, setup_initial_admin, update_user,
},
qr::{generate_qr_token, validate_qr_token},
ticket::{create_ticket, delete_ticket, edit_ticket, get_ticket_by_id, get_tickets},
},
};
@@ -63,6 +64,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
"/api/users/{id}",
get(get_user_by_id).delete(delete_user).patch(update_user),
)
.route("/api/generate_qr", post(generate_qr_token))
.layer(middleware::from_fn_with_state(
state.clone(),
validate_admin,
@@ -79,10 +81,14 @@ pub fn create_router(state: Arc<AppState>) -> Router {
validate_token,
));
Router::new()
.merge(protected_routes)
let public_routes = Router::new()
.route("/api/login", post(login))
.route("/api/check-admin", get(check_admin_exists))
.route("/api/setup-admin", post(setup_initial_admin))
.route("/api/qr_login", get(validate_qr_token));
Router::new()
.merge(protected_routes)
.merge(public_routes)
.with_state(state)
}