Compare commits
8 Commits
30a383d511
...
6a104c38ec
Author | SHA1 | Date | |
---|---|---|---|
6a104c38ec
|
|||
870be26ba2
|
|||
a2bf22548c
|
|||
a31b1db03e
|
|||
453ae9ceea
|
|||
f03a6935d5
|
|||
f31d10b6e1
|
|||
33e7cc2d59
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,3 +12,5 @@ target/
|
|||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
server/data
|
2375
server/Cargo.lock
generated
Normal file
2375
server/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
axum = "0.8.4"
|
||||||
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
|
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
bcrypt = "0.17.1"
|
||||||
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
tower-http = { version = "0.6.6", features = ["cors"] }
|
||||||
|
anyhow = "1.0"
|
106
server/src/controllers/auth.rs
Normal file
106
server/src/controllers/auth.rs
Normal file
@@ -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<LoginResponse> {
|
||||||
|
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<Session> {
|
||||||
|
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<Option<Session>> {
|
||||||
|
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<User> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
208
server/src/controllers/machines.rs
Normal file
208
server/src/controllers/machines.rs
Normal file
@@ -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<Machine> {
|
||||||
|
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<Vec<Machine>> {
|
||||||
|
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<Machine> {
|
||||||
|
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::<String, _>("uuid")).unwrap(),
|
||||||
|
name: row.get("name"),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_machines(pool: &DbPool) -> AppResult<Vec<Machine>> {
|
||||||
|
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::<String, _>("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<Vec<Machine>> {
|
||||||
|
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::<String, _>("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<Machine> {
|
||||||
|
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<bool> {
|
||||||
|
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<Option<ProvisioningCode>> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
3
server/src/controllers/mod.rs
Normal file
3
server/src/controllers/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod auth;
|
||||||
|
pub mod machines;
|
||||||
|
pub mod users;
|
202
server/src/controllers/users.rs
Normal file
202
server/src/controllers/users.rs
Normal file
@@ -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<User> {
|
||||||
|
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<Option<User>> {
|
||||||
|
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::<String, _>("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<User> {
|
||||||
|
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::<String, _>("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<Vec<User>> {
|
||||||
|
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::<String, _>("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<User> {
|
||||||
|
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<User> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,44 @@
|
|||||||
fn main() {
|
mod controllers;
|
||||||
println!("Hello, world!");
|
mod routes;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use utils::init_database;
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::{
|
||||||
|
routing::{delete, get, post, put},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use routes::{admin, auth as auth_routes, machines, setup};
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let pool = init_database().await?;
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/setup/status", get(setup::get_setup_status))
|
||||||
|
.route("/setup/init", post(setup::init_setup))
|
||||||
|
|
||||||
|
.route("/auth/login", post(auth_routes::login))
|
||||||
|
.route("/auth/logout", post(auth_routes::logout))
|
||||||
|
|
||||||
|
.route("/admin/users", get(admin::get_users))
|
||||||
|
.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("/machines/register", post(machines::register_machine))
|
||||||
|
.route("/machines", get(machines::get_machines))
|
||||||
|
.route("/machines/{id}", delete(machines::delete_machine))
|
||||||
|
|
||||||
|
.layer(CorsLayer::permissive())
|
||||||
|
|
||||||
|
.with_state(pool);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8379").await?;
|
||||||
|
println!("Server running on http://0.0.0.0:8379");
|
||||||
|
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
53
server/src/routes/admin.rs
Normal file
53
server/src/routes/admin.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use crate::controllers::users::UsersController;
|
||||||
|
use crate::utils::{auth::*, error::*, models::*, DbPool};
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Json,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn get_users(
|
||||||
|
_admin: AdminUser,
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
) -> Result<Json<Vec<User>>, AppError> {
|
||||||
|
let users = UsersController::get_all_users(&pool).await?;
|
||||||
|
Ok(success_response(users))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_user_handler(
|
||||||
|
_admin: AdminUser,
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
Json(request): Json<CreateUserRequest>,
|
||||||
|
) -> Result<Json<User>, AppError> {
|
||||||
|
let role = request.role.unwrap_or(UserRole::User);
|
||||||
|
let storage_limit_gb = request.storage_limit_gb.unwrap_or(0);
|
||||||
|
|
||||||
|
let user = UsersController::create_user(
|
||||||
|
&pool,
|
||||||
|
&request.username,
|
||||||
|
&request.password,
|
||||||
|
role,
|
||||||
|
storage_limit_gb,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(success_response(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_handler(
|
||||||
|
_admin: AdminUser,
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
Path(user_id): Path<i64>,
|
||||||
|
Json(request): Json<UpdateUserRequest>,
|
||||||
|
) -> Result<Json<User>, AppError> {
|
||||||
|
let updated_user = UsersController::update_user(&pool, user_id, request).await?;
|
||||||
|
Ok(success_response(updated_user))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user_handler(
|
||||||
|
_admin: AdminUser,
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
Path(user_id): Path<i64>,
|
||||||
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
|
UsersController::delete_user(&pool, user_id).await?;
|
||||||
|
Ok(success_message("User deleted successfully"))
|
||||||
|
}
|
19
server/src/routes/auth.rs
Normal file
19
server/src/routes/auth.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use crate::utils::{auth::*, error::*, models::*, DbPool};
|
||||||
|
use crate::controllers::auth::AuthController;
|
||||||
|
use axum::{extract::State, response::Json};
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
Json(request): Json<LoginRequest>,
|
||||||
|
) -> Result<Json<LoginResponse>, AppError> {
|
||||||
|
let response = AuthController::login(&pool, &request.username, &request.password).await?;
|
||||||
|
Ok(success_response(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout(
|
||||||
|
auth_user: AuthUser,
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
|
AuthController::logout(&pool, auth_user.user.id).await?;
|
||||||
|
Ok(success_message("Logged out successfully"))
|
||||||
|
}
|
38
server/src/routes/machines.rs
Normal file
38
server/src/routes/machines.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::utils::{auth::*, error::*, models::*, DbPool};
|
||||||
|
use crate::controllers::machines::MachinesController;
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Json,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn register_machine(
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
Json(request): Json<RegisterMachineRequest>,
|
||||||
|
) -> Result<Json<Machine>, AppError> {
|
||||||
|
let machine = MachinesController::register_machine(
|
||||||
|
&pool,
|
||||||
|
&request.code,
|
||||||
|
&request.uuid,
|
||||||
|
&request.name,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(success_response(machine))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_machines(
|
||||||
|
auth_user: AuthUser,
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
) -> Result<Json<Vec<Machine>>, AppError> {
|
||||||
|
let machines = MachinesController::get_machines_for_user(&pool, &auth_user.user).await?;
|
||||||
|
Ok(success_response(machines))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_machine(
|
||||||
|
auth_user: AuthUser,
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
Path(machine_id): Path<i64>,
|
||||||
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
|
MachinesController::delete_machine(&pool, machine_id, &auth_user.user).await?;
|
||||||
|
Ok(success_message("Machine deleted successfully"))
|
||||||
|
}
|
4
server/src/routes/mod.rs
Normal file
4
server/src/routes/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod admin;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod machines;
|
||||||
|
pub mod setup;
|
32
server/src/routes/setup.rs
Normal file
32
server/src/routes/setup.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::controllers::users::UsersController;
|
||||||
|
use crate::utils::{database::*, error::*, models::*};
|
||||||
|
use axum::{extract::State, response::Json};
|
||||||
|
|
||||||
|
pub async fn get_setup_status(
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
) -> Result<Json<SetupStatusResponse>, AppError> {
|
||||||
|
let first_user_exists = check_first_user_exists(&pool).await?;
|
||||||
|
Ok(success_response(SetupStatusResponse { first_user_exists }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_setup(
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
Json(request): Json<InitSetupRequest>,
|
||||||
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
|
let first_user_exists = check_first_user_exists(&pool).await?;
|
||||||
|
|
||||||
|
if first_user_exists {
|
||||||
|
return Err(validation_error("Setup already completed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
UsersController::create_user(
|
||||||
|
&pool,
|
||||||
|
&request.username,
|
||||||
|
&request.password,
|
||||||
|
UserRole::Admin,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(success_message("Setup completed successfully"))
|
||||||
|
}
|
67
server/src/utils/auth.rs
Normal file
67
server/src/utils/auth.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use crate::controllers::auth::AuthController;
|
||||||
|
use crate::utils::{models::*, DbPool};
|
||||||
|
use axum::{
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::{header::AUTHORIZATION, request::Parts, StatusCode},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AuthUser {
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> FromRequestParts<S> for AuthUser
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = StatusCode;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let auth_header = parts
|
||||||
|
.headers
|
||||||
|
.get(AUTHORIZATION)
|
||||||
|
.and_then(|header| header.to_str().ok())
|
||||||
|
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||||
|
|
||||||
|
if !auth_header.starts_with("Bearer ") {
|
||||||
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = &auth_header[7..];
|
||||||
|
let pool = parts
|
||||||
|
.extensions
|
||||||
|
.get::<DbPool>()
|
||||||
|
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let user = AuthController::authenticate_user(pool, token)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
||||||
|
|
||||||
|
Ok(AuthUser { user })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminUser {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> FromRequestParts<S> for AdminUser
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = StatusCode;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let auth_user = AuthUser::from_request_parts(parts, _state).await?;
|
||||||
|
|
||||||
|
if auth_user.user.role != UserRole::Admin {
|
||||||
|
return Err(StatusCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AdminUser {
|
||||||
|
user: auth_user.user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
111
server/src/utils/database.rs
Normal file
111
server/src/utils/database.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use crate::utils::{ensure_data_directories, get_database_path, AppResult};
|
||||||
|
use sqlx::{sqlite::SqlitePool, Pool, Row, Sqlite};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub type DbPool = Pool<Sqlite>;
|
||||||
|
|
||||||
|
pub async fn init_database() -> AppResult<DbPool> {
|
||||||
|
ensure_data_directories()?;
|
||||||
|
|
||||||
|
let db_path = get_database_path()?;
|
||||||
|
|
||||||
|
if !Path::new(&db_path).exists() {
|
||||||
|
std::fs::File::create(&db_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let database_url = format!("sqlite://{}", db_path);
|
||||||
|
|
||||||
|
let pool = SqlitePool::connect(&database_url).await?;
|
||||||
|
|
||||||
|
run_migrations(&pool).await?;
|
||||||
|
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_migrations(pool: &DbPool) -> AppResult<()> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
role TEXT CHECK(role IN ('admin','user')) NOT NULL,
|
||||||
|
storage_limit_gb INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token TEXT UNIQUE NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS machines (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
uuid TEXT UNIQUE NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS provisioning_codes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
code TEXT UNIQUE NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
used BOOLEAN DEFAULT 0,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS snapshots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
machine_id INTEGER NOT NULL,
|
||||||
|
snapshot_hash TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY(machine_id) REFERENCES machines(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_first_user_exists(pool: &DbPool) -> AppResult<bool> {
|
||||||
|
let row = sqlx::query("SELECT COUNT(*) as count FROM users")
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let count: i64 = row.get("count");
|
||||||
|
Ok(count > 0)
|
||||||
|
}
|
76
server/src/utils/db_path.rs
Normal file
76
server/src/utils/db_path.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use crate::utils::error::{internal_error, AppResult};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
pub fn get_database_path() -> AppResult<String> {
|
||||||
|
let db_dir = "data/db";
|
||||||
|
let db_path = format!("{}/arkendro.db", db_dir);
|
||||||
|
|
||||||
|
if let Err(e) = fs::create_dir_all(db_dir) {
|
||||||
|
return Err(internal_error(&format!(
|
||||||
|
"Failed to create database directory: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(db_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_data_directories() -> AppResult<()> {
|
||||||
|
let directories = ["data", "data/db", "data/backups", "data/logs"];
|
||||||
|
|
||||||
|
for dir in directories.iter() {
|
||||||
|
if let Err(e) = fs::create_dir_all(dir) {
|
||||||
|
return Err(internal_error(&format!(
|
||||||
|
"Failed to create directory '{}': {}",
|
||||||
|
dir, e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_data_path(filename: &str) -> String {
|
||||||
|
format!("data/{}", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_backup_path(filename: &str) -> String {
|
||||||
|
format!("data/backups/{}", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_log_path(filename: &str) -> String {
|
||||||
|
format!("data/logs/{}", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_database_path_creation() {
|
||||||
|
let _ = fs::remove_dir_all("data");
|
||||||
|
|
||||||
|
let db_path = get_database_path().expect("Should create database path");
|
||||||
|
assert_eq!(db_path, "data/db/arkendro.db");
|
||||||
|
|
||||||
|
assert!(Path::new("data/db").exists());
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all("data");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_data_directories() {
|
||||||
|
let _ = fs::remove_dir_all("data");
|
||||||
|
|
||||||
|
ensure_data_directories().expect("Should create all directories");
|
||||||
|
|
||||||
|
assert!(Path::new("data").exists());
|
||||||
|
assert!(Path::new("data/db").exists());
|
||||||
|
assert!(Path::new("data/backups").exists());
|
||||||
|
assert!(Path::new("data/logs").exists());
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all("data");
|
||||||
|
}
|
||||||
|
}
|
142
server/src/utils/error.rs
Normal file
142
server/src/utils/error.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Json, Response},
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppError {
|
||||||
|
DatabaseError(String),
|
||||||
|
ValidationError(String),
|
||||||
|
AuthenticationError(String),
|
||||||
|
AuthorizationError(String),
|
||||||
|
NotFoundError(String),
|
||||||
|
ConflictError(String),
|
||||||
|
InternalError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AppError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
AppError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
|
||||||
|
AppError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
|
||||||
|
AppError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
|
||||||
|
AppError::AuthorizationError(msg) => write!(f, "Authorization error: {}", msg),
|
||||||
|
AppError::NotFoundError(msg) => write!(f, "Not found: {}", msg),
|
||||||
|
AppError::ConflictError(msg) => write!(f, "Conflict: {}", msg),
|
||||||
|
AppError::InternalError(msg) => write!(f, "Internal error: {}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for AppError {}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, error_message) = match self {
|
||||||
|
AppError::DatabaseError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Database error"),
|
||||||
|
AppError::ValidationError(ref msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
|
||||||
|
AppError::AuthenticationError(ref msg) => (StatusCode::UNAUTHORIZED, msg.as_str()),
|
||||||
|
AppError::AuthorizationError(ref msg) => (StatusCode::FORBIDDEN, msg.as_str()),
|
||||||
|
AppError::NotFoundError(ref msg) => (StatusCode::NOT_FOUND, msg.as_str()),
|
||||||
|
AppError::ConflictError(ref msg) => (StatusCode::CONFLICT, msg.as_str()),
|
||||||
|
AppError::InternalError(_) => {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = Json(json!({
|
||||||
|
"error": error_message
|
||||||
|
}));
|
||||||
|
|
||||||
|
(status, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<anyhow::Error> for AppError {
|
||||||
|
fn from(err: anyhow::Error) -> Self {
|
||||||
|
if let Some(sqlx_err) = err.downcast_ref::<sqlx::Error>() {
|
||||||
|
match sqlx_err {
|
||||||
|
sqlx::Error::RowNotFound => {
|
||||||
|
AppError::NotFoundError("Resource not found".to_string())
|
||||||
|
}
|
||||||
|
sqlx::Error::Database(db_err) => {
|
||||||
|
if db_err.message().contains("UNIQUE constraint failed") {
|
||||||
|
AppError::ConflictError("Resource already exists".to_string())
|
||||||
|
} else {
|
||||||
|
AppError::DatabaseError(db_err.message().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => AppError::DatabaseError("Database operation failed".to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AppError::InternalError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bcrypt::BcryptError> for AppError {
|
||||||
|
fn from(_: bcrypt::BcryptError) -> Self {
|
||||||
|
AppError::InternalError("Password hashing error".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for AppError {
|
||||||
|
fn from(err: sqlx::Error) -> Self {
|
||||||
|
match err {
|
||||||
|
sqlx::Error::RowNotFound => AppError::NotFoundError("Resource not found".to_string()),
|
||||||
|
sqlx::Error::Database(db_err) => {
|
||||||
|
if db_err.message().contains("UNIQUE constraint failed") {
|
||||||
|
AppError::ConflictError("Resource already exists".to_string())
|
||||||
|
} else {
|
||||||
|
AppError::DatabaseError(db_err.message().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => AppError::DatabaseError("Database operation failed".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for AppError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
AppError::InternalError(format!("IO error: {}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type AppResult<T> = Result<T, AppError>;
|
||||||
|
|
||||||
|
pub fn validation_error(msg: &str) -> AppError {
|
||||||
|
AppError::ValidationError(msg.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auth_error(msg: &str) -> AppError {
|
||||||
|
AppError::AuthenticationError(msg.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forbidden_error(msg: &str) -> AppError {
|
||||||
|
AppError::AuthorizationError(msg.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn not_found_error(msg: &str) -> AppError {
|
||||||
|
AppError::NotFoundError(msg.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn conflict_error(msg: &str) -> AppError {
|
||||||
|
AppError::ConflictError(msg.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn internal_error(msg: &str) -> AppError {
|
||||||
|
AppError::InternalError(msg.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn success_response<T>(data: T) -> Json<T>
|
||||||
|
where
|
||||||
|
T: serde::Serialize,
|
||||||
|
{
|
||||||
|
Json(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn success_message(msg: &str) -> Json<serde_json::Value> {
|
||||||
|
Json(json!({ "message": msg }))
|
||||||
|
}
|
9
server/src/utils/mod.rs
Normal file
9
server/src/utils/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
pub mod auth;
|
||||||
|
pub mod database;
|
||||||
|
pub mod db_path;
|
||||||
|
pub mod error;
|
||||||
|
pub mod models;
|
||||||
|
|
||||||
|
pub use database::*;
|
||||||
|
pub use db_path::*;
|
||||||
|
pub use error::*;
|
137
server/src/utils/models.rs
Normal file
137
server/src/utils/models.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: i64,
|
||||||
|
pub username: String,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub password_hash: String,
|
||||||
|
pub role: UserRole,
|
||||||
|
pub storage_limit_gb: i64,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum UserRole {
|
||||||
|
Admin,
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for UserRole {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
UserRole::Admin => write!(f, "admin"),
|
||||||
|
UserRole::User => write!(f, "user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for UserRole {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"admin" => Ok(UserRole::Admin),
|
||||||
|
"user" => Ok(UserRole::User),
|
||||||
|
_ => Err("Invalid role"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CreateUserRequest {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub role: Option<UserRole>,
|
||||||
|
pub storage_limit_gb: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UpdateUserRequest {
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub password: Option<String>,
|
||||||
|
pub role: Option<UserRole>,
|
||||||
|
pub storage_limit_gb: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Session {
|
||||||
|
pub id: i64,
|
||||||
|
pub user_id: i64,
|
||||||
|
pub token: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
pub token: String,
|
||||||
|
pub role: UserRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Machine {
|
||||||
|
pub id: i64,
|
||||||
|
pub user_id: i64,
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RegisterMachineRequest {
|
||||||
|
pub code: String,
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ProvisioningCode {
|
||||||
|
pub id: i64,
|
||||||
|
pub user_id: i64,
|
||||||
|
pub code: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
|
pub used: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Snapshot {
|
||||||
|
pub id: i64,
|
||||||
|
pub machine_id: i64,
|
||||||
|
pub snapshot_hash: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SetupStatusResponse {
|
||||||
|
pub first_user_exists: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct InitSetupRequest {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorResponse {
|
||||||
|
pub fn new(message: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
error: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user