Browse Source

Add route to create first admin

Lee Morgan 3 weeks ago
parent
commit
552ca18752

+ 134 - 5
Cargo.lock

@@ -234,6 +234,18 @@ dependencies = [
  "alloc-no-stdlib",
 ]
 
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures 0.2.17",
+ "password-hash",
+]
+
 [[package]]
 name = "awc"
 version = "3.8.2"
@@ -273,12 +285,36 @@ version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
 [[package]]
 name = "bitflags"
 version = "2.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
 
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.12.1"
@@ -355,7 +391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
 dependencies = [
  "cfg-if",
- "cpufeatures",
+ "cpufeatures 0.3.0",
  "rand_core 0.10.1",
 ]
 
@@ -385,6 +421,15 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "cpufeatures"
 version = "0.3.0"
@@ -403,6 +448,16 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
 [[package]]
 name = "crypto-common"
 version = "0.2.2"
@@ -441,15 +496,26 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
 [[package]]
 name = "digest"
 version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
 dependencies = [
- "block-buffer",
+ "block-buffer 0.12.1",
  "const-oid",
- "crypto-common",
+ "crypto-common 0.2.2",
 ]
 
 [[package]]
@@ -574,6 +640,27 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.3.4"
@@ -623,6 +710,7 @@ version = "0.1.0"
 dependencies = [
  "actix-codec",
  "actix-web",
+ "argon2",
  "awc",
  "dotenvy",
  "futures-util",
@@ -630,6 +718,7 @@ dependencies = [
  "serde_json",
  "tokio",
  "uuid",
+ "validator",
 ]
 
 [[package]]
@@ -951,6 +1040,17 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.3.2"
@@ -1054,6 +1154,15 @@ dependencies = [
  "rand_core 0.9.5",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
 [[package]]
 name = "rand_core"
 version = "0.9.5"
@@ -1208,8 +1317,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
 dependencies = [
  "cfg-if",
- "cpufeatures",
- "digest",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
 ]
 
 [[package]]
@@ -1272,6 +1381,12 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
 
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
 [[package]]
 name = "syn"
 version = "2.0.119"
@@ -1473,6 +1588,20 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "validator"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d68c6633c483df6780cc5277a417c7c2d1bceee2649d06c8ab6b0fd2dd3c81"
+dependencies = [
+ "idna",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+]
+
 [[package]]
 name = "version_check"
 version = "0.9.5"

+ 2 - 0
Cargo.toml

@@ -6,6 +6,7 @@ edition = "2024"
 [dependencies]
 actix-codec = "0.5.2"
 actix-web = "4.14.0"
+argon2 = { version = "0.5.3", features = ["std"]}
 awc = "3.8.2"
 dotenvy = "0.15.7"
 futures-util = "0.3.33"
@@ -13,6 +14,7 @@ serde = { version = "1.0.229", features = ["derive"]}
 serde_json = "1.0.151"
 tokio = { version="1.53.1", features = ["macros", "rt-multi-thread", "sync", "time"]}
 uuid = { version = "1.24.0", features = ["v4", "serde"]}
+validator = "0.21.0"
 
 [profile.release]
 opt-level = 3

+ 0 - 6
src/controllers/create_first_admin.rs

@@ -1,6 +0,0 @@
-use actix_web::{HttpResponse, web, post};
-
-#[post("/admin/first")]
-pub async fn route(
-    
-)

+ 63 - 0
src/controllers/first_admin.rs

@@ -0,0 +1,63 @@
+use actix_web::{HttpResponse, web, post};
+use validator::ValidateEmail;
+use uuid::Uuid;
+use serde::Deserialize;
+use std::sync::Mutex;
+use crate::{
+    users::{User, UserAccess},
+    logic::hash_password
+};
+
+#[derive(Deserialize)]
+struct Body {
+    name: String,
+    email: String,
+    password: String,
+    confirm_password: String
+}
+
+#[post("/admin/first")]
+pub async fn route(
+    web::Json(body): web::Json<Body>,
+    users: web::Data<Mutex<Vec<User>>>
+) -> HttpResponse {
+    let b = match validate_body(body) {
+        Ok(b) => b,
+        Err(e) => { return HttpResponse::BadRequest().body(e.to_string())}
+    };
+
+    let new_user = User {
+        id: Uuid::new_v4(),
+        name: b.name,
+        email: b.email,
+        pass_hash: match hash_password(&b.password) {
+            Ok(p) => p,
+            Err(_) => { return HttpResponse::InternalServerError().finish() }
+        },
+        access: UserAccess::Admin
+    };
+
+    {
+        let mut guard = users.lock().unwrap();
+        guard.push(new_user);
+        User::write(guard.to_vec());
+    }
+
+    HttpResponse::Ok().body("success")
+}
+
+fn validate_body(body: Body) -> Result<Body, Box<dyn std::error::Error>> {
+    if body.email.validate_email() {
+        return Err("Invalid email".into());
+    }
+
+    if body.password != body.confirm_password {
+        return Err("Passwords do not match".into())
+    }
+
+    if body.password.len() < 12 {
+        return Err("Password must contain at least 12 characters".into());
+    }
+
+    Ok(body)
+}

+ 1 - 0
src/controllers/mod.rs

@@ -1 +1,2 @@
 pub mod home;
+pub mod first_admin;

+ 17 - 0
src/logic/hash_password.rs

@@ -0,0 +1,17 @@
+use argon2::{
+    Argon2,
+    password_hash::{
+        SaltString,
+        PasswordHasher,
+        rand_core::OsRng
+    }
+};
+
+pub fn hash_password(pass: &String) -> Result<String, Box<dyn std::error::Error>> {
+    let salt = SaltString::generate(&mut OsRng);
+    let argon2 = Argon2::default();
+    match argon2.hash_password(pass.as_bytes(), &salt) {
+        Ok(h) => Ok(h.to_string()),
+        Err(e) => Err(Box::new(e))
+    }
+}

+ 3 - 0
src/logic/mod.rs

@@ -0,0 +1,3 @@
+pub mod hash_password;
+
+pub use hash_password::hash_password;

+ 1 - 0
src/main.rs

@@ -7,6 +7,7 @@ mod routes;
 mod controllers;
 mod websocket;
 mod users;
+mod logic;
 
 #[actix_web::main]
 async fn main() -> Result<(), std::io::Error> {

+ 4 - 1
src/routes.rs

@@ -1,8 +1,11 @@
 use actix_web::web::ServiceConfig;
 use crate::controllers::{
-    home
+    home,
+    first_admin
 };
 
 pub fn config(cfg: &mut ServiceConfig) {
     cfg.service(home::route);
+    
+    cfg.service(first_admin::route);
 }

+ 21 - 5
src/users.rs

@@ -2,24 +2,40 @@ use uuid::Uuid;
 use serde::{Serialize, Deserialize};
 use std::{fs, io::Write, path::Path};
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct User {
     pub id: Uuid,
     pub name: String,
     pub email: String,
     pub pass_hash: String,
-    pub devices: Vec<Device>
+    pub access: UserAccess
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub enum UserAccess {
+    Admin,
+    Standard {
+        devices: Vec<Device>,
+        areas: Vec<Area>
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct Device {
     id: String,
     device_type: DeviceType,
     name: String
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-pub enum DeviceType{
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Area {
+    id: String,
+    device_type: DeviceType,
+    name: String
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub enum DeviceType {
     Switch
 }