aboutsummaryrefslogtreecommitdiff
path: root/src/auth
diff options
context:
space:
mode:
authorUMTS at Teleco <crt@teleco.ch>2025-12-13 02:48:13 +0100
committerUMTS at Teleco <crt@teleco.ch>2025-12-13 02:48:13 +0100
commite52b8e1c2e110d0feb74feb7905c2ff064b51d55 (patch)
tree3090814e422250e07e72cf1c83241ffd95cf20f7 /src/auth
committing to insanityHEADmaster
Diffstat (limited to 'src/auth')
-rw-r--r--src/auth/mod.rs6
-rw-r--r--src/auth/password.rs56
-rw-r--r--src/auth/pin.rs66
-rw-r--r--src/auth/session.rs129
-rw-r--r--src/auth/token.rs99
5 files changed, 356 insertions, 0 deletions
diff --git a/src/auth/mod.rs b/src/auth/mod.rs
new file mode 100644
index 0000000..65cba10
--- /dev/null
+++ b/src/auth/mod.rs
@@ -0,0 +1,6 @@
+pub mod password;
+pub mod pin;
+pub mod session;
+pub mod token;
+
+pub use session::SessionManager;
diff --git a/src/auth/password.rs b/src/auth/password.rs
new file mode 100644
index 0000000..580ad1f
--- /dev/null
+++ b/src/auth/password.rs
@@ -0,0 +1,56 @@
+// Password authentication module
+use crate::models::{Role, User};
+use anyhow::{Context, Result};
+use bcrypt::verify;
+use sqlx::MySqlPool;
+
+pub async fn authenticate_password(
+ pool: &MySqlPool,
+ username: &str,
+ password: &str,
+) -> Result<Option<(User, Role)>> {
+ // Fetch user from database
+ let user: Option<User> = sqlx::query_as::<_, User>(
+ r#"
+ SELECT id, name, username, password, pin_code, login_string, role_id,
+ email, phone, notes, active, last_login_date, created_date,
+ password_reset_token, password_reset_expiry
+ FROM users
+ WHERE username = ? AND active = TRUE
+ "#,
+ )
+ .bind(username)
+ .fetch_optional(pool)
+ .await
+ .context("Failed to fetch user from database")?;
+
+ if let Some(user) = user {
+ // Verify password
+ let password_valid =
+ verify(password, &user.password).context("Failed to verify password")?;
+
+ if password_valid {
+ // Fetch user's role
+ let role: Role = sqlx::query_as::<_, Role>(
+ "SELECT id, name, power, created_at FROM roles WHERE id = ?",
+ )
+ .bind(user.role_id)
+ .fetch_one(pool)
+ .await
+ .context("Failed to fetch user role")?;
+
+ // Update last login date
+ sqlx::query("UPDATE users SET last_login_date = NOW() WHERE id = ?")
+ .bind(user.id)
+ .execute(pool)
+ .await
+ .context("Failed to update last login date")?;
+
+ Ok(Some((user, role)))
+ } else {
+ Ok(None)
+ }
+ } else {
+ Ok(None)
+ }
+}
diff --git a/src/auth/pin.rs b/src/auth/pin.rs
new file mode 100644
index 0000000..4d1993c
--- /dev/null
+++ b/src/auth/pin.rs
@@ -0,0 +1,66 @@
+// PIN authentication module
+use crate::config::SecurityConfig;
+use crate::models::{Role, User};
+use anyhow::{Context, Result};
+use sqlx::MySqlPool;
+
+pub async fn authenticate_pin(
+ pool: &MySqlPool,
+ username: &str,
+ pin: &str,
+ security_config: &SecurityConfig,
+) -> Result<Option<(User, Role)>> {
+ // Fetch user from database
+ let user: Option<User> = sqlx::query_as::<_, User>(
+ r#"
+ SELECT id, name, username, password, pin_code, login_string, role_id,
+ email, phone, notes, active, last_login_date, created_date,
+ password_reset_token, password_reset_expiry
+ FROM users
+ WHERE username = ? AND active = TRUE AND pin_code IS NOT NULL
+ "#,
+ )
+ .bind(username)
+ .fetch_optional(pool)
+ .await
+ .context("Failed to fetch user from database")?;
+
+ if let Some(user) = user {
+ // Check if user has a PIN set
+ if let Some(user_pin) = &user.pin_code {
+ // Verify PIN - either bcrypt hash or plaintext depending on config
+ let pin_valid = if security_config.hash_pins {
+ bcrypt::verify(pin, user_pin).unwrap_or(false)
+ } else {
+ user_pin == pin
+ };
+
+ if pin_valid {
+ // Fetch user's role
+ let role: Role = sqlx::query_as::<_, Role>(
+ "SELECT id, name, power, created_at FROM roles WHERE id = ?",
+ )
+ .bind(user.role_id)
+ .fetch_one(pool)
+ .await
+ .context("Failed to fetch user role")?;
+
+ // Update last login date
+ sqlx::query("UPDATE users SET last_login_date = NOW() WHERE id = ?")
+ .bind(user.id)
+ .execute(pool)
+ .await
+ .context("Failed to update last login date")?;
+
+ Ok(Some((user, role)))
+ } else {
+ Ok(None)
+ }
+ } else {
+ // User doesn't have a PIN set
+ Ok(None)
+ }
+ } else {
+ Ok(None)
+ }
+}
diff --git a/src/auth/session.rs b/src/auth/session.rs
new file mode 100644
index 0000000..277cef2
--- /dev/null
+++ b/src/auth/session.rs
@@ -0,0 +1,129 @@
+// Session management for SeckelAPI
+use crate::config::Config;
+use crate::models::Session;
+use chrono::{Duration, Utc};
+use std::collections::HashMap;
+use std::sync::{Arc, RwLock};
+use uuid::Uuid;
+
+#[derive(Clone)]
+pub struct SessionManager {
+ sessions: Arc<RwLock<HashMap<String, Session>>>,
+ config: Arc<Config>,
+}
+
+impl SessionManager {
+ pub fn new(config: Arc<Config>) -> Self {
+ Self {
+ sessions: Arc::new(RwLock::new(HashMap::new())),
+ config,
+ }
+ }
+
+ fn get_timeout_for_power(&self, power: i32) -> u64 {
+ self.config.get_session_timeout(power)
+ }
+
+ pub fn create_session(
+ &self,
+ user_id: i32,
+ username: String,
+ role_id: i32,
+ role_name: String,
+ power: i32,
+ ) -> String {
+ let token = Uuid::new_v4().to_string();
+ let now = Utc::now();
+
+ let session = Session {
+ user_id,
+ username,
+ role_id,
+ role_name,
+ power,
+ created_at: now,
+ last_accessed: now,
+ };
+
+ if let Ok(mut sessions) = self.sessions.write() {
+ // Check concurrent session limit for this user
+ let max_sessions = self.config.get_max_concurrent_sessions(power);
+ let user_sessions: Vec<(String, chrono::DateTime<Utc>)> = sessions
+ .iter()
+ .filter(|(_, s)| s.user_id == user_id)
+ .map(|(token, s)| (token.clone(), s.created_at))
+ .collect();
+
+ // If at limit, remove the oldest session
+ if user_sessions.len() >= max_sessions as usize {
+ if let Some(oldest_token) = user_sessions
+ .iter()
+ .min_by_key(|(_, created)| created)
+ .map(|(token, _)| token.clone())
+ {
+ sessions.remove(&oldest_token);
+ }
+ }
+
+ sessions.insert(token.clone(), session);
+ }
+
+ token
+ }
+
+ pub fn get_session(&self, token: &str) -> Option<Session> {
+ let mut session_to_update = None;
+
+ {
+ if let Ok(sessions) = self.sessions.read() {
+ if let Some(session) = sessions.get(token) {
+ let now = Utc::now();
+ let timeout_for_user = self.get_timeout_for_power(session.power);
+ let timeout_duration = Duration::minutes(timeout_for_user as i64);
+
+ // Check if session has expired
+ if now - session.last_accessed > timeout_duration {
+ return None; // Session expired, will be cleaned up later
+ } else {
+ session_to_update = Some(session.clone());
+ }
+ }
+ }
+ }
+
+ if let Some(mut session) = session_to_update {
+ // Update last accessed time only if refresh_on_activity is enabled
+ if self.config.security.refresh_session_on_activity {
+ session.last_accessed = Utc::now();
+
+ if let Ok(mut sessions) = self.sessions.write() {
+ sessions.insert(token.to_string(), session.clone());
+ }
+ }
+
+ Some(session)
+ } else {
+ None
+ }
+ }
+
+ pub fn remove_session(&self, token: &str) -> bool {
+ if let Ok(mut sessions) = self.sessions.write() {
+ sessions.remove(token).is_some()
+ } else {
+ false
+ }
+ }
+
+ pub fn cleanup_expired_sessions(&self) {
+ let now = Utc::now();
+
+ if let Ok(mut sessions) = self.sessions.write() {
+ sessions.retain(|_, session| {
+ let timeout_for_user = self.get_timeout_for_power(session.power);
+ let timeout_duration = Duration::minutes(timeout_for_user as i64);
+ now - session.last_accessed <= timeout_duration
+ });
+ }
+ }
+}
diff --git a/src/auth/token.rs b/src/auth/token.rs
new file mode 100644
index 0000000..17f75e4
--- /dev/null
+++ b/src/auth/token.rs
@@ -0,0 +1,99 @@
+// Token/RFID authentication module
+use crate::config::SecurityConfig;
+use crate::models::{Role, User};
+use anyhow::{Context, Result};
+use sqlx::MySqlPool;
+
+pub async fn authenticate_token(
+ pool: &MySqlPool,
+ login_string: &str,
+ security_config: &SecurityConfig,
+) -> Result<Option<(User, Role)>> {
+ // If hashing is enabled, we can't use WHERE login_string = ? directly
+ // Need to fetch all users and verify hashes
+ if security_config.hash_tokens {
+ // Fetch all active users with login_string set
+ let users: Vec<User> = sqlx::query_as::<_, User>(
+ r#"
+ SELECT id, name, username, password, pin_code, login_string, role_id,
+ email, phone, notes, active, last_login_date, created_date,
+ password_reset_token, password_reset_expiry
+ FROM users
+ WHERE login_string IS NOT NULL AND active = TRUE
+ "#,
+ )
+ .fetch_all(pool)
+ .await
+ .context("Failed to fetch users from database")?;
+
+ // Find matching user by verifying bcrypt hash
+ for user in users {
+ if let Some(ref stored_hash) = user.login_string {
+ if bcrypt::verify(login_string, stored_hash).unwrap_or(false) {
+ // Found matching user
+ return authenticate_user_by_id(pool, user.id).await;
+ }
+ }
+ }
+ Ok(None)
+ } else {
+ // Plaintext comparison - direct database query
+ let user: Option<User> = sqlx::query_as::<_, User>(
+ r#"
+ SELECT id, name, username, password, pin_code, login_string, role_id,
+ email, phone, notes, active, last_login_date, created_date,
+ password_reset_token, password_reset_expiry
+ FROM users
+ WHERE login_string = ? AND active = TRUE
+ "#,
+ )
+ .bind(login_string)
+ .fetch_optional(pool)
+ .await
+ .context("Failed to fetch user from database")?;
+
+ if let Some(user) = user {
+ authenticate_user_by_id(pool, user.id).await
+ } else {
+ Ok(None)
+ }
+ }
+}
+
+async fn authenticate_user_by_id(pool: &MySqlPool, user_id: i32) -> Result<Option<(User, Role)>> {
+ // Fetch user
+ let user: User = sqlx::query_as::<_, User>(
+ r#"
+ SELECT id, name, username, password, pin_code, login_string, role_id,
+ email, phone, notes, active, last_login_date, created_date,
+ password_reset_token, password_reset_expiry
+ FROM users
+ WHERE id = ?
+ "#,
+ )
+ .bind(user_id)
+ .fetch_one(pool)
+ .await
+ .context("Failed to fetch user")?;
+
+ if user.active {
+ // Fetch user's role
+ let role: Role =
+ sqlx::query_as::<_, Role>("SELECT id, name, power, created_at FROM roles WHERE id = ?")
+ .bind(user.role_id)
+ .fetch_one(pool)
+ .await
+ .context("Failed to fetch user role")?;
+
+ // Update last login date
+ sqlx::query("UPDATE users SET last_login_date = NOW() WHERE id = ?")
+ .bind(user.id)
+ .execute(pool)
+ .await
+ .context("Failed to update last login date")?;
+
+ Ok(Some((user, role)))
+ } else {
+ Ok(None)
+ }
+}