From 453ae9ceeafa0a903d65c17c2c75cacaf4f60bdd Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Mon, 8 Sep 2025 21:17:09 +0200 Subject: [PATCH] Create controllers functions --- server/src/controllers/auth.rs | 106 +++++++++++++++ server/src/controllers/machines.rs | 208 +++++++++++++++++++++++++++++ server/src/controllers/mod.rs | 3 + server/src/controllers/users.rs | 202 ++++++++++++++++++++++++++++ 4 files changed, 519 insertions(+) create mode 100644 server/src/controllers/auth.rs create mode 100644 server/src/controllers/machines.rs create mode 100644 server/src/controllers/mod.rs create mode 100644 server/src/controllers/users.rs diff --git a/server/src/controllers/auth.rs b/server/src/controllers/auth.rs new file mode 100644 index 0000000..8df29b6 --- /dev/null +++ b/server/src/controllers/auth.rs @@ -0,0 +1,106 @@ +use crate::controllers::users::UsersController; +use crate::utils::{error::*, models::*, DbPool}; +use chrono::{Duration, Utc}; +use sqlx::Row; +use uuid::Uuid; + +pub struct AuthController; + +impl AuthController { + pub async fn login(pool: &DbPool, username: &str, password: &str) -> AppResult { + let user = UsersController::verify_user_credentials(pool, username, password).await?; + + let session = Self::create_session(pool, user.id).await?; + + Ok(LoginResponse { + token: session.token, + role: user.role, + }) + } + + pub async fn logout(pool: &DbPool, user_id: i64) -> AppResult<()> { + sqlx::query("DELETE FROM sessions WHERE user_id = ?") + .bind(user_id) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn create_session(pool: &DbPool, user_id: i64) -> AppResult { + let token = Self::generate_session_token(); + let expires_at = Utc::now() + Duration::days(30); + let result = sqlx::query( + r#" + INSERT INTO sessions (user_id, token, expires_at) + VALUES (?, ?, ?) + "#, + ) + .bind(user_id) + .bind(&token) + .bind(expires_at) + .execute(pool) + .await?; + + Ok(Session { + id: result.last_insert_rowid(), + user_id, + token, + created_at: Utc::now(), + expires_at, + }) + } + + pub async fn get_session_by_token(pool: &DbPool, token: &str) -> AppResult> { + let row = sqlx::query( + r#" + SELECT id, user_id, token, created_at, expires_at + FROM sessions WHERE token = ? AND expires_at > datetime('now') + "#, + ) + .bind(token) + .fetch_optional(pool) + .await?; + + if let Some(row) = row { + Ok(Some(Session { + id: row.get("id"), + user_id: row.get("user_id"), + token: row.get("token"), + created_at: row.get("created_at"), + expires_at: row.get("expires_at"), + })) + } else { + Ok(None) + } + } + + pub async fn authenticate_user(pool: &DbPool, token: &str) -> AppResult { + let session = Self::get_session_by_token(pool, token) + .await? + .ok_or_else(|| auth_error("Invalid or expired session"))?; + + let user = UsersController::get_user_by_id(pool, session.user_id).await?; + Ok(user) + } + + pub async fn cleanup_expired_sessions(pool: &DbPool) -> AppResult<()> { + sqlx::query("DELETE FROM sessions WHERE expires_at <= datetime('now')") + .execute(pool) + .await?; + Ok(()) + } + + fn generate_session_token() -> String { + Uuid::new_v4().to_string() + } + + #[allow(dead_code)] + pub async fn delete_session_by_token(pool: &DbPool, token: &str) -> AppResult<()> { + sqlx::query("DELETE FROM sessions WHERE token = ?") + .bind(token) + .execute(pool) + .await?; + Ok(()) + } +} diff --git a/server/src/controllers/machines.rs b/server/src/controllers/machines.rs new file mode 100644 index 0000000..0280539 --- /dev/null +++ b/server/src/controllers/machines.rs @@ -0,0 +1,208 @@ +use crate::utils::{error::*, models::*, DbPool}; +use chrono::Utc; +use sqlx::Row; +use uuid::Uuid; + +pub struct MachinesController; + +impl MachinesController { + pub async fn register_machine( + pool: &DbPool, + code: &str, + uuid: &Uuid, + name: &str, + ) -> AppResult { + Self::validate_machine_input(name)?; + + let provisioning_code = Self::get_provisioning_code(pool, code) + .await? + .ok_or_else(|| validation_error("Invalid provisioning code"))?; + + if provisioning_code.used { + return Err(validation_error("Provisioning code already used")); + } + + if provisioning_code.expires_at < Utc::now() { + return Err(validation_error("Provisioning code expired")); + } + + if Self::machine_exists_by_uuid(pool, uuid).await? { + return Err(conflict_error("Machine with this UUID already exists")); + } + + let machine = Self::create_machine(pool, provisioning_code.user_id, uuid, name).await?; + + Self::mark_provisioning_code_used(pool, code).await?; + + Ok(machine) + } + + pub async fn get_machines_for_user(pool: &DbPool, user: &User) -> AppResult> { + if user.role == UserRole::Admin { + Self::get_all_machines(pool).await + } else { + Self::get_machines_by_user_id(pool, user.id).await + } + } + + pub async fn delete_machine(pool: &DbPool, machine_id: i64, user: &User) -> AppResult<()> { + let machine = Self::get_machine_by_id(pool, machine_id).await?; + + if user.role != UserRole::Admin && machine.user_id != user.id { + return Err(forbidden_error("Access denied")); + } + + Self::delete_machine_by_id(pool, machine_id).await + } + + pub async fn get_machine_by_id(pool: &DbPool, id: i64) -> AppResult { + let row = sqlx::query( + r#" + SELECT id, user_id, uuid, name, created_at + FROM machines WHERE id = ? + "#, + ) + .bind(id) + .fetch_one(pool) + .await?; + + Ok(Machine { + id: row.get("id"), + user_id: row.get("user_id"), + uuid: Uuid::parse_str(&row.get::("uuid")).unwrap(), + name: row.get("name"), + created_at: row.get("created_at"), + }) + } + + async fn get_all_machines(pool: &DbPool) -> AppResult> { + let rows = sqlx::query( + r#" + SELECT id, user_id, uuid, name, created_at + FROM machines ORDER BY created_at DESC + "#, + ) + .fetch_all(pool) + .await?; + + let mut machines = Vec::new(); + for row in rows { + machines.push(Machine { + id: row.get("id"), + user_id: row.get("user_id"), + uuid: Uuid::parse_str(&row.get::("uuid")).unwrap(), + name: row.get("name"), + created_at: row.get("created_at"), + }); + } + + Ok(machines) + } + + async fn get_machines_by_user_id(pool: &DbPool, user_id: i64) -> AppResult> { + let rows = sqlx::query( + r#" + SELECT id, user_id, uuid, name, created_at + FROM machines WHERE user_id = ? ORDER BY created_at DESC + "#, + ) + .bind(user_id) + .fetch_all(pool) + .await?; + + let mut machines = Vec::new(); + for row in rows { + machines.push(Machine { + id: row.get("id"), + user_id: row.get("user_id"), + uuid: Uuid::parse_str(&row.get::("uuid")).unwrap(), + name: row.get("name"), + created_at: row.get("created_at"), + }); + } + + Ok(machines) + } + + async fn create_machine( + pool: &DbPool, + user_id: i64, + uuid: &Uuid, + name: &str, + ) -> AppResult { + let result = sqlx::query( + r#" + INSERT INTO machines (user_id, uuid, name) + VALUES (?, ?, ?) + "#, + ) + .bind(user_id) + .bind(uuid.to_string()) + .bind(name) + .execute(pool) + .await?; + + Self::get_machine_by_id(pool, result.last_insert_rowid()).await + } + + async fn machine_exists_by_uuid(pool: &DbPool, uuid: &Uuid) -> AppResult { + let row = sqlx::query("SELECT COUNT(*) as count FROM machines WHERE uuid = ?") + .bind(uuid.to_string()) + .fetch_one(pool) + .await?; + + let count: i64 = row.get("count"); + Ok(count > 0) + } + + async fn delete_machine_by_id(pool: &DbPool, id: i64) -> AppResult<()> { + sqlx::query("DELETE FROM machines WHERE id = ?") + .bind(id) + .execute(pool) + .await?; + Ok(()) + } + + async fn get_provisioning_code( + pool: &DbPool, + code: &str, + ) -> AppResult> { + let row = sqlx::query( + r#" + SELECT id, user_id, code, created_at, expires_at, used + FROM provisioning_codes WHERE code = ? + "#, + ) + .bind(code) + .fetch_optional(pool) + .await?; + + if let Some(row) = row { + Ok(Some(ProvisioningCode { + id: row.get("id"), + user_id: row.get("user_id"), + code: row.get("code"), + created_at: row.get("created_at"), + expires_at: row.get("expires_at"), + used: row.get("used"), + })) + } else { + Ok(None) + } + } + + async fn mark_provisioning_code_used(pool: &DbPool, code: &str) -> AppResult<()> { + sqlx::query("UPDATE provisioning_codes SET used = 1 WHERE code = ?") + .bind(code) + .execute(pool) + .await?; + Ok(()) + } + + fn validate_machine_input(name: &str) -> AppResult<()> { + if name.trim().is_empty() { + return Err(validation_error("Machine name cannot be empty")); + } + Ok(()) + } +} diff --git a/server/src/controllers/mod.rs b/server/src/controllers/mod.rs new file mode 100644 index 0000000..c0f47b9 --- /dev/null +++ b/server/src/controllers/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod machines; +pub mod users; diff --git a/server/src/controllers/users.rs b/server/src/controllers/users.rs new file mode 100644 index 0000000..150bf5f --- /dev/null +++ b/server/src/controllers/users.rs @@ -0,0 +1,202 @@ +use crate::utils::{error::*, models::*, DbPool}; +use bcrypt::{hash, verify, DEFAULT_COST}; +use sqlx::Row; +use std::str::FromStr; + +pub struct UsersController; + +impl UsersController { + pub async fn create_user( + pool: &DbPool, + username: &str, + password: &str, + role: UserRole, + storage_limit_gb: i64, + ) -> AppResult { + Self::validate_user_input(username, password)?; + + let password_hash = hash(password, DEFAULT_COST)?; + + let result = sqlx::query( + r#" + INSERT INTO users (username, password_hash, role, storage_limit_gb) + VALUES (?, ?, ?, ?) + "#, + ) + .bind(username) + .bind(&password_hash) + .bind(role.to_string()) + .bind(storage_limit_gb) + .execute(pool) + .await?; + + Self::get_user_by_id(pool, result.last_insert_rowid()).await + } + + pub async fn get_user_by_username(pool: &DbPool, username: &str) -> AppResult> { + let row = sqlx::query( + r#" + SELECT id, username, password_hash, role, storage_limit_gb, created_at + FROM users WHERE username = ? + "#, + ) + .bind(username) + .fetch_optional(pool) + .await?; + + if let Some(row) = row { + Ok(Some(User { + id: row.get("id"), + username: row.get("username"), + password_hash: row.get("password_hash"), + role: UserRole::from_str(&row.get::("role")).unwrap(), + storage_limit_gb: row.get("storage_limit_gb"), + created_at: row.get("created_at"), + })) + } else { + Ok(None) + } + } + + pub async fn get_user_by_id(pool: &DbPool, id: i64) -> AppResult { + let row = sqlx::query( + r#" + SELECT id, username, password_hash, role, storage_limit_gb, created_at + FROM users WHERE id = ? + "#, + ) + .bind(id) + .fetch_one(pool) + .await?; + + Ok(User { + id: row.get("id"), + username: row.get("username"), + password_hash: row.get("password_hash"), + role: UserRole::from_str(&row.get::("role")).unwrap(), + storage_limit_gb: row.get("storage_limit_gb"), + created_at: row.get("created_at"), + }) + } + + pub async fn get_all_users(pool: &DbPool) -> AppResult> { + let rows = sqlx::query( + r#" + SELECT id, username, password_hash, role, storage_limit_gb, created_at + FROM users ORDER BY created_at DESC + "#, + ) + .fetch_all(pool) + .await?; + + let mut users = Vec::new(); + for row in rows { + users.push(User { + id: row.get("id"), + username: row.get("username"), + password_hash: row.get("password_hash"), + role: UserRole::from_str(&row.get::("role")).unwrap(), + storage_limit_gb: row.get("storage_limit_gb"), + created_at: row.get("created_at"), + }); + } + + Ok(users) + } + + pub async fn update_user( + pool: &DbPool, + id: i64, + request: UpdateUserRequest, + ) -> AppResult { + let current_user = Self::get_user_by_id(pool, id).await?; + + let username = request.username.clone().unwrap_or(current_user.username); + let role = request.role.unwrap_or(current_user.role); + let storage_limit_gb = request + .storage_limit_gb + .unwrap_or(current_user.storage_limit_gb); + + if request.username.is_some() || request.password.is_some() { + Self::validate_user_input( + &username, + &request.password.as_deref().unwrap_or("validpassword"), + )?; + } + + if let Some(password) = request.password { + let password_hash = hash(&password, DEFAULT_COST)?; + sqlx::query( + r#" + UPDATE users + SET username = ?, password_hash = ?, role = ?, storage_limit_gb = ? + WHERE id = ? + "#, + ) + .bind(&username) + .bind(&password_hash) + .bind(role.to_string()) + .bind(storage_limit_gb) + .bind(id) + .execute(pool) + .await?; + } else { + sqlx::query( + r#" + UPDATE users + SET username = ?, role = ?, storage_limit_gb = ? + WHERE id = ? + "#, + ) + .bind(&username) + .bind(role.to_string()) + .bind(storage_limit_gb) + .bind(id) + .execute(pool) + .await?; + } + + Self::get_user_by_id(pool, id).await + } + + pub async fn delete_user(pool: &DbPool, id: i64) -> AppResult<()> { + Self::get_user_by_id(pool, id).await?; + + sqlx::query("DELETE FROM users WHERE id = ?") + .bind(id) + .execute(pool) + .await?; + Ok(()) + } + + pub async fn verify_user_credentials( + pool: &DbPool, + username: &str, + password: &str, + ) -> AppResult { + let user = Self::get_user_by_username(pool, username) + .await? + .ok_or_else(|| auth_error("Invalid credentials"))?; + + let is_valid = verify(password, &user.password_hash)?; + if !is_valid { + return Err(auth_error("Invalid credentials")); + } + + Ok(user) + } + + fn validate_user_input(username: &str, password: &str) -> AppResult<()> { + if username.trim().is_empty() { + return Err(validation_error("Username cannot be empty")); + } + + if password.len() < 8 { + return Err(validation_error( + "Password must be at least 8 characters long", + )); + } + + Ok(()) + } +}