use std::time::Duration; use axum::extract::State; use axum::http::{HeaderMap, StatusCode}; use axum::Json; use chrono::Utc; use rand::Rng; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::auth::jwt; use crate::auth::middleware::AuthUser; use crate::error::AppError; use crate::models::event::Event; use crate::models::session::Session; use crate::models::user::{User, UserRole}; use crate::services::rate_limiter::client_ip; use crate::state::AppState; #[derive(Deserialize)] pub struct JoinRequest { pub display_name: String, } #[derive(Serialize)] pub struct JoinResponse { pub jwt: String, pub pin: String, pub user_id: Uuid, pub is_new: bool, } pub async fn join( State(state): State, headers: HeaderMap, Json(body): Json, ) -> Result<(StatusCode, Json), AppError> { let ip = client_ip(&headers, "unknown"); if !state.rate_limiter.check(format!("join:{ip}"), 5, Duration::from_secs(60)) { return Err(AppError::TooManyRequests( "Zu viele Anfragen. Bitte warte kurz und versuche es erneut.".into(), )); } let display_name = body.display_name.trim(); if display_name.is_empty() || display_name.len() > 50 { return Err(AppError::BadRequest( "Name muss zwischen 1 und 50 Zeichen lang sein.".into(), )); } let event = Event::find_or_create( &state.pool, &state.config.event_slug, &state.config.event_name, ) .await?; // Generate a 4-digit PIN let pin: String = format!("{:04}", rand::rng().random_range(0..10000u32)); let pin_hash = bcrypt::hash(&pin, 12).map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?; let user = User::create(&state.pool, event.id, display_name, &pin_hash).await?; let token = jwt::create_token( user.id, event.id, user.role.clone(), &state.config.jwt_secret, state.config.session_expiry_days, ) .map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?; let token_hash = jwt::hash_token(&token); let expires_at = Utc::now() + chrono::Duration::days(state.config.session_expiry_days); Session::create(&state.pool, user.id, &token_hash, expires_at).await?; Ok(( StatusCode::CREATED, Json(JoinResponse { jwt: token, pin, user_id: user.id, is_new: true, }), )) } #[derive(Deserialize)] pub struct RecoverRequest { pub display_name: String, pub pin: String, } #[derive(Serialize)] pub struct RecoverResponse { pub jwt: String, pub user_id: Uuid, } pub async fn recover( State(state): State, Json(body): Json, ) -> Result, AppError> { let display_name = body.display_name.trim(); let event = Event::find_by_slug(&state.pool, &state.config.event_slug) .await? .ok_or_else(|| AppError::NotFound("Event nicht gefunden.".into()))?; let users = User::find_by_event_and_name(&state.pool, event.id, display_name).await?; if users.is_empty() { return Err(AppError::NotFound( "Kein Benutzer mit diesem Namen gefunden.".into(), )); } for user in &users { // Check PIN lockout if let Some(locked_until) = user.pin_locked_until { if Utc::now() < locked_until { return Err(AppError::TooManyRequests( "Zu viele Versuche. Bitte warte 15 Minuten.".into(), )); } } let pin_matches = bcrypt::verify(&body.pin, &user.recovery_pin_hash) .unwrap_or(false); if pin_matches { // Reset failed attempts on success User::reset_pin_attempts(&state.pool, user.id).await?; let token = jwt::create_token( user.id, event.id, user.role.clone(), &state.config.jwt_secret, state.config.session_expiry_days, ) .map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?; let token_hash = jwt::hash_token(&token); let expires_at = Utc::now() + chrono::Duration::days(state.config.session_expiry_days); Session::create(&state.pool, user.id, &token_hash, expires_at).await?; return Ok(Json(RecoverResponse { jwt: token, user_id: user.id, })); } // Wrong PIN — increment failure count let attempts = User::increment_failed_pin(&state.pool, user.id).await?; if attempts >= 3 { let lockout = Utc::now() + chrono::Duration::minutes(15); User::lock_pin(&state.pool, user.id, lockout).await?; } } Err(AppError::Unauthorized("PIN ist falsch.".into())) } #[derive(Deserialize)] pub struct AdminLoginRequest { pub password: String, } #[derive(Serialize)] pub struct AdminLoginResponse { pub jwt: String, } pub async fn admin_login( State(state): State, Json(body): Json, ) -> Result, AppError> { if state.config.admin_password_hash.is_empty() { return Err(AppError::Forbidden( "Admin-Login ist nicht konfiguriert.".into(), )); } let valid = bcrypt::verify(&body.password, &state.config.admin_password_hash) .unwrap_or(false); if !valid { return Err(AppError::Unauthorized("Falsches Passwort.".into())); } let event = Event::find_or_create( &state.pool, &state.config.event_slug, &state.config.event_name, ) .await?; // Find or create the admin user for this event let admin_name = "Admin"; let users = User::find_by_event_and_name(&state.pool, event.id, admin_name).await?; let admin_user = if let Some(u) = users.into_iter().find(|u| u.role == UserRole::Admin) { u } else { // Create admin user with a dummy PIN (admin authenticates via password) let dummy_hash = bcrypt::hash("0000", 4) .map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?; let user = User::create(&state.pool, event.id, admin_name, &dummy_hash).await?; sqlx::query("UPDATE \"user\" SET role = 'admin' WHERE id = $1") .bind(user.id) .execute(&state.pool) .await?; User::find_by_id(&state.pool, user.id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("admin user creation failed")))? }; let token = jwt::create_token( admin_user.id, event.id, UserRole::Admin, &state.config.jwt_secret, 1, // Admin sessions expire after 1 day ) .map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?; let token_hash = jwt::hash_token(&token); let expires_at = Utc::now() + chrono::Duration::days(1); Session::create(&state.pool, admin_user.id, &token_hash, expires_at).await?; Ok(Json(AdminLoginResponse { jwt: token })) } pub async fn logout( State(state): State, auth: AuthUser, ) -> Result { Session::delete_by_token_hash(&state.pool, &auth.token_hash).await?; Ok(StatusCode::NO_CONTENT) }