From 7b3ae6bb6e653d839790fc2d326c9053090f8664 Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Tue, 9 Sep 2025 19:06:16 +0200 Subject: [PATCH] Add provisioning system to server --- server/src/controllers/machines.rs | 107 ++++++++++++++--------------- server/src/main.rs | 6 +- server/src/routes/machines.rs | 19 ++++- server/src/routes/mod.rs | 1 + server/src/utils/models.rs | 19 ++++- 5 files changed, 91 insertions(+), 61 deletions(-) diff --git a/server/src/controllers/machines.rs b/server/src/controllers/machines.rs index 0280539..6512961 100644 --- a/server/src/controllers/machines.rs +++ b/server/src/controllers/machines.rs @@ -1,48 +1,65 @@ -use crate::utils::{error::*, models::*, DbPool}; -use chrono::Utc; +use crate::utils::{base62::Base62, config::ConfigManager, error::*, models::*, DbPool}; +use chrono::{Duration, Utc}; +use rand::{distributions::Alphanumeric, Rng}; 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 { + pub async fn register_machine(pool: &DbPool, user: &User, 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"))?; + let machine_uuid = Uuid::new_v4(); - 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?; + let machine = Self::create_machine(pool, user.id, &machine_uuid, name).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 create_provisioning_code( + 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")); } + + let code: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(5) + .map(char::from) + .collect(); + + let external_url = ConfigManager::get_external_url(pool).await?; + let provisioning_string = format!("52?#{}/{}", external_url, code); + let encoded_code = Base62::encode(&provisioning_string); + let expires_at = Utc::now() + Duration::hours(1); + + sqlx::query( + r#" + INSERT INTO provisioning_codes (machine_id, code, expires_at) + VALUES (?, ?, ?) + "#, + ) + .bind(machine_id) + .bind(&code) + .bind(expires_at) + .execute(pool) + .await?; + + Ok(ProvisioningCodeResponse { + code: encoded_code, + raw_code: code, + expires_at, + }) + } + + pub async fn get_machines_for_user(pool: &DbPool, user: &User) -> AppResult> { + Self::get_machines_by_user_id(pool, user.id).await } pub async fn delete_machine(pool: &DbPool, machine_id: i64, user: &User) -> AppResult<()> { @@ -75,30 +92,6 @@ impl MachinesController { }) } - 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#" @@ -169,7 +162,7 @@ impl MachinesController { ) -> AppResult> { let row = sqlx::query( r#" - SELECT id, user_id, code, created_at, expires_at, used + SELECT id, machine_id, code, created_at, expires_at, used FROM provisioning_codes WHERE code = ? "#, ) @@ -180,7 +173,7 @@ impl MachinesController { if let Some(row) = row { Ok(Some(ProvisioningCode { id: row.get("id"), - user_id: row.get("user_id"), + machine_id: row.get("machine_id"), code: row.get("code"), created_at: row.get("created_at"), expires_at: row.get("expires_at"), diff --git a/server/src/main.rs b/server/src/main.rs index 4a0b5b0..aef6044 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -7,7 +7,7 @@ use axum::{ routing::{delete, get, post, put}, Router, }; -use routes::{accounts, admin, auth as auth_routes, machines, setup}; +use routes::{accounts, admin, auth as auth_routes, config, machines, setup}; use std::path::Path; use tokio::signal; use tower_http::{ @@ -29,7 +29,11 @@ async fn main() -> Result<()> { .route("/admin/users", post(admin::create_user_handler)) .route("/admin/users/{id}", put(admin::update_user_handler)) .route("/admin/users/{id}", delete(admin::delete_user_handler)) + .route("/admin/config", get(config::get_all_configs)) + .route("/admin/config", post(config::set_config)) + .route("/admin/config/{key}", get(config::get_config)) .route("/machines/register", post(machines::register_machine)) + .route("/machines/provisioning-code", post(machines::create_provisioning_code)) .route("/machines", get(machines::get_machines)) .route("/machines/{id}", delete(machines::delete_machine)) .layer(CorsLayer::permissive()) diff --git a/server/src/routes/machines.rs b/server/src/routes/machines.rs index 498fb81..5968fa1 100644 --- a/server/src/routes/machines.rs +++ b/server/src/routes/machines.rs @@ -6,13 +6,13 @@ use axum::{ }; pub async fn register_machine( + auth_user: AuthUser, State(pool): State, Json(request): Json, ) -> Result, AppError> { let machine = MachinesController::register_machine( &pool, - &request.code, - &request.uuid, + &auth_user.user, &request.name, ) .await?; @@ -20,6 +20,21 @@ pub async fn register_machine( Ok(success_response(machine)) } +pub async fn create_provisioning_code( + auth_user: AuthUser, + State(pool): State, + Json(request): Json, +) -> Result, AppError> { + let response = MachinesController::create_provisioning_code( + &pool, + request.machine_id, + &auth_user.user, + ) + .await?; + + Ok(success_response(response)) +} + pub async fn get_machines( auth_user: AuthUser, State(pool): State, diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 842a396..cf0f1b2 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1,5 +1,6 @@ pub mod admin; pub mod auth; +pub mod config; pub mod machines; pub mod setup; pub mod accounts; diff --git a/server/src/utils/models.rs b/server/src/utils/models.rs index bbc2276..41f225d 100644 --- a/server/src/utils/models.rs +++ b/server/src/utils/models.rs @@ -89,15 +89,32 @@ pub struct Machine { #[derive(Debug, Serialize, Deserialize)] pub struct RegisterMachineRequest { + pub name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UseProvisioningCodeRequest { pub code: String, pub uuid: Uuid, pub name: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateProvisioningCodeRequest { + pub machine_id: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ProvisioningCodeResponse { + pub code: String, + pub raw_code: String, + pub expires_at: DateTime, +} + #[derive(Debug, Serialize, Deserialize)] pub struct ProvisioningCode { pub id: i64, - pub user_id: i64, + pub machine_id: i64, pub code: String, pub created_at: DateTime, pub expires_at: DateTime,