refactor: rewrite
This commit is contained in:
parent
eef5cd8e02
commit
4dd9013521
File diff suppressed because it is too large
Load Diff
48
Cargo.toml
48
Cargo.toml
|
@ -1,8 +1,40 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"core",
|
||||
"database",
|
||||
"server",
|
||||
"types",
|
||||
]
|
||||
resolver = "2"
|
||||
[package]
|
||||
name = "homedisk"
|
||||
authors = ["MedzikUser <medzik@duck.com>"]
|
||||
homepage = "https://github.com/HomeDisk/cloud"
|
||||
repository = "https://github.com/HomeDisk/cloud"
|
||||
license = "GPL-3.0-or-later"
|
||||
version = "0.0.0-dev"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# Types
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
# Errors
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
# Logger
|
||||
tracing = { version = "0.1", features = ["max_level_debug", "release_max_level_info", "log"] }
|
||||
tracing-subscriber = "0.3"
|
||||
# Backtrace in panic hook
|
||||
backtrace = "0.3"
|
||||
|
||||
# Tokio runtime
|
||||
tokio = { version = "1.20", features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
# Database
|
||||
futures-util = "0.3"
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "sqlite"] }
|
||||
uuid = { version = "1.1", features = ["v4"] }
|
||||
|
||||
# Cryptographic
|
||||
crypto-utils = "0.4"
|
||||
hex = "0.4"
|
||||
|
||||
# Config
|
||||
toml = "0.5"
|
||||
|
||||
# HTTP server
|
||||
axum = { version = "0.6.0-rc.2", features = ["http2", "multipart"] }
|
||||
byte-unit = "4.0.14"
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
name = "homedisk"
|
||||
authors = ["MedzikUser <medzik@duck.com>"]
|
||||
homepage = "https://github.com/HomeDisk/cloud"
|
||||
repository = "https://github.com/HomeDisk/cloud"
|
||||
license = "GPL-3.0-or-later"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "homedisk"
|
||||
path = "./src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.62"
|
||||
better-panic = "0.3.0"
|
||||
tracing-subscriber = "0.3.15"
|
||||
tracing = { version = "0.1.36", features = ["max_level_debug", "release_max_level_info"] }
|
||||
tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"] }
|
||||
homedisk-database = { path = "../database" }
|
||||
homedisk-server = { path = "../server" }
|
||||
homedisk-types = { path = "../types", features = ["config"] }
|
|
@ -1,36 +0,0 @@
|
|||
//! # HomeDisk cloud server
|
||||
//!
|
||||
//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
|
||||
//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
|
||||
//! [total-lines]: https://img.shields.io/tokei/lines/github/MedzikUser/HomeDisk?style=for-the-badge&logo=github&color=fede00
|
||||
//! [code-size]: https://img.shields.io/github/languages/code-size/MedzikUser/HomeDisk?style=for-the-badge&color=c8df52&logo=github
|
||||
//! [ci]: https://img.shields.io/github/workflow/status/MedzikUser/HomeDisk/Rust/main?style=for-the-badge
|
||||
//!
|
||||
//! [home-screenshot]: https://i.imgur.com/x4Glw7w.png
|
||||
//! [login-screenshot]: https://i.imgur.com/KpwY4nb.png
|
||||
//!
|
||||
//! [![github]](https://github.com/MedzikUser/HomeDisk)
|
||||
//! [![docs-rs]](https://homedisk-doc.vercel.app)
|
||||
//! [![total-lines]](https://github.com/MedzikUser/HomeDisk)
|
||||
//! [![code-size]](https://github.com/MedzikUser/HomeDisk)
|
||||
//! [![ci]](https://github.com/MedzikUser/HomeDisk/actions/workflows/rust.yml)
|
||||
//!
|
||||
//! ![home-screenshot]
|
||||
//! ![login-screenshot]
|
||||
//!
|
||||
//! ## 👨💻 Building
|
||||
//!
|
||||
//! First clone the repository: `git clone https://github.com/MedzikUser/HomeDisk.git`
|
||||
//!
|
||||
//! ### Requirements
|
||||
//! - [Rust](https://rust-lang.org)
|
||||
//!
|
||||
//! To build run the command: `cargo build --release`
|
||||
//!
|
||||
//! The compiled binary can be found in `./target/release/homedisk`
|
||||
//!
|
||||
//! ## Configure
|
||||
//!
|
||||
//! Go to [config](homedisk_types::config) module
|
||||
|
||||
#![doc(html_root_url = "https://homedisk-doc.medzik.xyz")]
|
|
@ -1,17 +0,0 @@
|
|||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
// Max Logger Level on debug build
|
||||
#[cfg(debug_assertions)]
|
||||
const MAX_LEVEL: LevelFilter = LevelFilter::DEBUG;
|
||||
|
||||
// Max Logger Level on release build
|
||||
#[cfg(not(debug_assertions))]
|
||||
const MAX_LEVEL: LevelFilter = LevelFilter::INFO;
|
||||
|
||||
pub fn init() {
|
||||
// initialize better_panic
|
||||
better_panic::install();
|
||||
|
||||
// initialize tracing
|
||||
tracing_subscriber::fmt().with_max_level(MAX_LEVEL).init();
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
use std::{fs::File, path::Path};
|
||||
|
||||
use homedisk_database::Database;
|
||||
use homedisk_server::serve_http;
|
||||
use homedisk_types::config::Config;
|
||||
use tracing::{info, warn};
|
||||
|
||||
mod logger;
|
||||
|
||||
pub const DATABASE_FILE: &str = "homedisk.db";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
logger::init();
|
||||
|
||||
let config = Config::parse().expect("Failed to parse configuration file");
|
||||
|
||||
// open database connection
|
||||
let db =
|
||||
// if database file doesn't exists create it
|
||||
if !Path::new(DATABASE_FILE).exists() {
|
||||
warn!("Database file doesn't exists.");
|
||||
info!("Creating database file...");
|
||||
|
||||
// create an empty database file
|
||||
File::create(DATABASE_FILE).expect("Failed to create a database file");
|
||||
|
||||
// open database file
|
||||
let db = Database::open(DATABASE_FILE)
|
||||
.await
|
||||
.expect("Failed to open database file");
|
||||
|
||||
// create tables in the database
|
||||
db.create_tables()
|
||||
.await
|
||||
.expect("Failed to create tables in the database");
|
||||
|
||||
db
|
||||
}
|
||||
// if database file exists
|
||||
else {
|
||||
Database::open(DATABASE_FILE)
|
||||
.await
|
||||
.expect("Failed to open database file")
|
||||
};
|
||||
|
||||
// change the type from Vec<String> to Vec<HeaderValue> so that the http server can correctly detect CORS hosts
|
||||
let origins = config
|
||||
.http
|
||||
.cors
|
||||
.iter()
|
||||
.map(|e| e.parse().expect("parse CORS hosts"))
|
||||
.collect();
|
||||
|
||||
// format host ip and port
|
||||
let host = format!(
|
||||
"{host}:{port}",
|
||||
host = config.http.host,
|
||||
port = config.http.port
|
||||
);
|
||||
|
||||
serve_http(host, origins, db, config)
|
||||
.await
|
||||
.expect("start http server");
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
[package]
|
||||
name = "homedisk-database"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.36"
|
||||
futures-util = "0.3.23"
|
||||
sqlx = { version = "0.6.1", features = ["runtime-tokio-rustls", "sqlite"] }
|
||||
uuid = { version = "1.1.2", features = ["v5"] }
|
||||
homedisk-types = { path = "../types", features = ["database"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"] }
|
|
@ -1,9 +0,0 @@
|
|||
#![doc(html_root_url = "https://homedisk-doc.medzik.xyz")]
|
||||
|
||||
mod sqlite;
|
||||
|
||||
pub use homedisk_types::{
|
||||
database::User,
|
||||
errors::{DatabaseError as Error, DatabaseResult as Result},
|
||||
};
|
||||
pub use sqlite::*;
|
|
@ -1,286 +0,0 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{
|
||||
sqlite::{SqliteConnectOptions, SqliteQueryResult},
|
||||
ConnectOptions, Executor, Row, SqlitePool,
|
||||
};
|
||||
use tracing::{debug, log::LevelFilter};
|
||||
|
||||
use super::{Error, Result, User};
|
||||
|
||||
/// SQL Database
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Database {
|
||||
/// SQLite Connection Pool
|
||||
pub conn: SqlitePool,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Open a SQLite database
|
||||
/// ```no_run
|
||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
||||
/// use homedisk_database::Database;
|
||||
///
|
||||
/// // open database in memory
|
||||
/// Database::open("sqlite::memory:").await?;
|
||||
///
|
||||
/// // open database from file
|
||||
/// Database::open("path/to/database.db").await?;
|
||||
///
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn open(path: &str) -> Result<Self> {
|
||||
debug!("Opening SQLite database");
|
||||
|
||||
// sqlite connection options
|
||||
let mut options = SqliteConnectOptions::from_str(path).map_err(Error::OpenDatabase)?;
|
||||
|
||||
// set log level to Debug
|
||||
options.log_statements(LevelFilter::Debug);
|
||||
|
||||
// create a database pool
|
||||
let conn = SqlitePool::connect_with(options)
|
||||
.await
|
||||
.map_err(Error::ConnectDatabase)?;
|
||||
|
||||
// return `Database`
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
/// Create all required tabled for HomeDisk
|
||||
/// ```
|
||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
||||
/// # let db = homedisk_database::Database::open("sqlite::memory:").await?;
|
||||
/// db.create_tables().await?;
|
||||
///
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn create_tables(&self) -> Result<SqliteQueryResult> {
|
||||
let query = sqlx::query(include_str!("../../tables.sql"));
|
||||
|
||||
self.conn.execute(query).await.map_err(Error::Execute)
|
||||
}
|
||||
|
||||
/// Create a new User
|
||||
/// ```
|
||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
||||
/// # let db = homedisk_database::Database::open("sqlite::memory:").await?;
|
||||
/// # db.create_tables().await?;
|
||||
/// use homedisk_database::User;
|
||||
///
|
||||
/// // create `User` type
|
||||
/// let user = User::new("username", "password");
|
||||
///
|
||||
/// // create a user in database
|
||||
/// db.create_user(&user).await?;
|
||||
///
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn create_user(&self, user: &User) -> Result<SqliteQueryResult> {
|
||||
debug!("Creating user - {}", user.username);
|
||||
|
||||
// insert user to a database
|
||||
let query = sqlx::query("INSERT INTO user (id, username, password) VALUES (?, ?, ?)")
|
||||
.bind(&user.id)
|
||||
.bind(&user.username)
|
||||
.bind(&user.password);
|
||||
|
||||
// execute query and return output
|
||||
self.conn.execute(query).await.map_err(Error::Execute)
|
||||
}
|
||||
|
||||
/// Search for a user
|
||||
/// ```
|
||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
||||
/// # let db = homedisk_database::Database::open("sqlite::memory:").await?;
|
||||
/// # db.create_tables().await?;
|
||||
/// use homedisk_database::User;
|
||||
///
|
||||
/// // create `User` type
|
||||
/// let user = User::new("username", "password");
|
||||
///
|
||||
/// # db.create_user(&user).await?;
|
||||
/// db.find_user(&user).await?;
|
||||
///
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn find_user(&self, user: &User) -> Result<User> {
|
||||
debug!("Searching for a user - {}", user.username);
|
||||
|
||||
// create query request to database
|
||||
let query =
|
||||
sqlx::query_as::<_, User>("SELECT * FROM user WHERE username = ? AND password = ?")
|
||||
.bind(&user.username)
|
||||
.bind(&user.password);
|
||||
|
||||
// fetch query
|
||||
let mut stream = self.conn.fetch(query);
|
||||
|
||||
// get rows from query
|
||||
let row = stream
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(Error::Execute)?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
|
||||
// get `id` row
|
||||
let id = row.try_get("id").map_err(Error::GetRow)?;
|
||||
// get `username` row
|
||||
let username = row.try_get("username").map_err(Error::GetRow)?;
|
||||
// get `password` row
|
||||
let password = row.try_get("password").map_err(Error::GetRow)?;
|
||||
|
||||
// return `User`
|
||||
Ok(User {
|
||||
id,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
/// Search for a user by UUID
|
||||
/// ```
|
||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
||||
/// # let db = homedisk_database::Database::open("sqlite::memory:").await?;
|
||||
/// # db.create_tables().await?;
|
||||
/// use homedisk_database::User;
|
||||
///
|
||||
/// // create `User` type
|
||||
/// let user = User::new("username", "password");
|
||||
///
|
||||
/// # db.create_user(&user).await?;
|
||||
/// db.find_user_by_id(&user.id).await?;
|
||||
///
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn find_user_by_id(&self, id: &str) -> Result<User> {
|
||||
debug!("Searching for a user by UUID - {}", id);
|
||||
|
||||
// create query request to database
|
||||
let query = sqlx::query_as::<_, User>("SELECT * FROM user WHERE id = ?").bind(id);
|
||||
|
||||
// fetch query
|
||||
let mut stream = self.conn.fetch(query);
|
||||
|
||||
// get rows from query
|
||||
let row = stream
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(Error::Execute)?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
|
||||
// get `id` row
|
||||
let id = row.try_get("id").map_err(Error::GetRow)?;
|
||||
// get `username` row
|
||||
let username = row.try_get("username").map_err(Error::GetRow)?;
|
||||
// get `password` row
|
||||
let password = row.try_get("password").map_err(Error::GetRow)?;
|
||||
|
||||
// return `User`
|
||||
Ok(User {
|
||||
id,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Database, User};
|
||||
|
||||
const USERNAME: &str = "medzik";
|
||||
const PASSWORD: &str = "SuperSecretPassword123!";
|
||||
|
||||
/// Utils to open database in tests
|
||||
async fn open_db() -> Database {
|
||||
Database::open("sqlite::memory:").await.expect("open db")
|
||||
}
|
||||
|
||||
/// Utils to create a new user in tests
|
||||
async fn new_user(db: &Database) {
|
||||
// create tables
|
||||
db.create_tables().await.expect("create tables");
|
||||
|
||||
// create new user
|
||||
let user = User::new(USERNAME, PASSWORD);
|
||||
db.create_user(&user).await.expect("create user");
|
||||
}
|
||||
|
||||
/// Test a create user
|
||||
#[tokio::test]
|
||||
async fn create_user() {
|
||||
let db = open_db().await;
|
||||
new_user(&db).await;
|
||||
}
|
||||
|
||||
// Test a search for a user
|
||||
#[tokio::test]
|
||||
async fn find_user() {
|
||||
let db = open_db().await;
|
||||
|
||||
new_user(&db).await;
|
||||
|
||||
let user = User::new(USERNAME, PASSWORD);
|
||||
|
||||
let user = db.find_user(&user).await.unwrap();
|
||||
|
||||
assert_eq!(user.username, USERNAME)
|
||||
}
|
||||
|
||||
// Test a search for a user by id
|
||||
#[tokio::test]
|
||||
async fn find_user_by_id() {
|
||||
let db = open_db().await;
|
||||
new_user(&db).await;
|
||||
|
||||
let user = User::new(USERNAME, PASSWORD);
|
||||
|
||||
let user = db.find_user_by_id(&user.id).await.unwrap();
|
||||
|
||||
assert_eq!(user.username, USERNAME)
|
||||
}
|
||||
|
||||
/// Test a search for a user with an invalid password to see if the user is returned (it shouldn't be)
|
||||
#[tokio::test]
|
||||
async fn find_user_wrong_password() {
|
||||
let db = open_db().await;
|
||||
|
||||
new_user(&db).await;
|
||||
|
||||
let user = User::new(USERNAME, "wrong password 123!");
|
||||
|
||||
let err = db.find_user(&user).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "user not found")
|
||||
}
|
||||
|
||||
/// Test a search for a user who doesn't exist
|
||||
#[tokio::test]
|
||||
async fn find_user_wrong_username() {
|
||||
let db = open_db().await;
|
||||
|
||||
new_user(&db).await;
|
||||
|
||||
let user = User::new("not_exists_user", PASSWORD);
|
||||
|
||||
let err = db.find_user(&user).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "user not found")
|
||||
}
|
||||
|
||||
/// Test a search for a user by UUID who doesn't exist
|
||||
#[tokio::test]
|
||||
async fn find_user_wrong_id() {
|
||||
let db = open_db().await;
|
||||
|
||||
new_user(&db).await;
|
||||
|
||||
let other_user = User::new("not_exists_user", "my secret passphrase");
|
||||
|
||||
let err = db.find_user_by_id(&other_user.id).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "user not found")
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
[package]
|
||||
name = "homedisk-server"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.5.15", features = ["multipart"] }
|
||||
axum-auth = "0.3.0"
|
||||
base64 = "0.13.0"
|
||||
byte-unit = "4.0.14"
|
||||
futures = "0.3.23"
|
||||
hyper = { version = "0.14.20", features = ["full"] }
|
||||
log = "0.4.17"
|
||||
crypto-utils = { version = "0.4.0", features = ["jwt"] }
|
||||
serde = { version = "1.0.144", features = ["derive"] }
|
||||
thiserror = "1.0.32"
|
||||
tower-http = { version = "0.3.4", features = ["full"] }
|
||||
homedisk-database = { path = "../database" }
|
||||
homedisk-types = { path = "../types", features = ["axum"] }
|
|
@ -1,46 +0,0 @@
|
|||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
||||
use homedisk_database::{Database, Error, User};
|
||||
use homedisk_types::{
|
||||
auth::login::{Request, Response},
|
||||
config::Config,
|
||||
errors::{AuthError, ServerError},
|
||||
};
|
||||
|
||||
use crate::middleware::{create_token, validate_json};
|
||||
|
||||
pub async fn handle(
|
||||
Extension(db): Extension<Database>,
|
||||
Extension(config): Extension<Config>,
|
||||
request: Result<Json<Request>, JsonRejection>,
|
||||
) -> Result<Json<Response>, ServerError> {
|
||||
// validate json request
|
||||
let request = validate_json(request)?;
|
||||
|
||||
// create `User` type
|
||||
let user = User::new(&request.username, &request.password);
|
||||
|
||||
// search for a user in database
|
||||
let response = match db.find_user(&user).await {
|
||||
Ok(user) => {
|
||||
// create user token
|
||||
let token = create_token(&user, config.jwt.secret.as_bytes(), config.jwt.expires)?;
|
||||
|
||||
Response {
|
||||
access_token: token,
|
||||
}
|
||||
},
|
||||
|
||||
// error while searching for a user
|
||||
Err(err) => {
|
||||
return match err {
|
||||
// user not found
|
||||
Error::UserNotFound => Err(ServerError::AuthError(AuthError::UserNotFound)),
|
||||
// other error
|
||||
_ => Err(ServerError::AuthError(AuthError::Other(err.to_string()))),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// send response
|
||||
Ok(Json(response))
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
mod login;
|
||||
mod register;
|
||||
mod whoami;
|
||||
|
||||
pub fn app() -> axum::Router {
|
||||
use axum::routing::{get, post};
|
||||
|
||||
axum::Router::new()
|
||||
.route("/login", post(login::handle))
|
||||
.route("/register", post(register::handle))
|
||||
.route("/whoami", get(whoami::handle))
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
use std::fs;
|
||||
|
||||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
||||
use homedisk_database::{Database, User};
|
||||
use homedisk_types::{
|
||||
auth::login::{Request, Response},
|
||||
config::Config,
|
||||
errors::{AuthError, FsError, ServerError},
|
||||
};
|
||||
|
||||
use crate::middleware::{create_token, validate_json};
|
||||
|
||||
pub async fn handle(
|
||||
Extension(db): Extension<Database>,
|
||||
Extension(config): Extension<Config>,
|
||||
request: Result<Json<Request>, JsonRejection>,
|
||||
) -> Result<Json<Response>, ServerError> {
|
||||
// validate json request
|
||||
let request = validate_json(request)?;
|
||||
|
||||
// username must contain at least 4 characters
|
||||
if request.username.len() < 4 {
|
||||
return Err(ServerError::AuthError(AuthError::UsernameTooShort));
|
||||
}
|
||||
|
||||
// username must be less than 25 characters
|
||||
if request.username.len() > 25 {
|
||||
return Err(ServerError::AuthError(AuthError::UsernameTooLong));
|
||||
}
|
||||
|
||||
// password must contain at least 8 characters
|
||||
if request.password.len() < 8 {
|
||||
return Err(ServerError::AuthError(AuthError::PasswordTooShort));
|
||||
}
|
||||
|
||||
// create `User` type and hash password
|
||||
let user = User::new(&request.username, &request.password);
|
||||
|
||||
// create user in the database
|
||||
let response = match db.create_user(&user).await {
|
||||
Ok(_result) => {
|
||||
let token = create_token(&user, config.jwt.secret.as_bytes(), config.jwt.expires)?;
|
||||
|
||||
Response {
|
||||
access_token: token,
|
||||
}
|
||||
},
|
||||
|
||||
// error while searching for a user
|
||||
Err(err) => {
|
||||
// user already exists
|
||||
if err.to_string().contains("UNIQUE constraint failed") {
|
||||
return Err(ServerError::AuthError(AuthError::UserAlreadyExists));
|
||||
}
|
||||
|
||||
// other error
|
||||
return Err(ServerError::AuthError(AuthError::Other(err.to_string())));
|
||||
},
|
||||
};
|
||||
|
||||
// create directory for the user files
|
||||
fs::create_dir_all(&format!("{}/{}", config.storage.path, user.username,))
|
||||
.map_err(|e| ServerError::FsError(FsError::CreateDirectory(e.to_string())))?;
|
||||
|
||||
// send response
|
||||
Ok(Json(response))
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
use axum::{Extension, Json};
|
||||
use axum_auth::AuthBearer;
|
||||
use homedisk_database::{Database, Error};
|
||||
use homedisk_types::{
|
||||
auth::whoami::Response,
|
||||
config::Config,
|
||||
errors::{AuthError, ServerError},
|
||||
};
|
||||
|
||||
use crate::middleware::validate_jwt;
|
||||
|
||||
pub async fn handle(
|
||||
db: Extension<Database>,
|
||||
config: Extension<Config>,
|
||||
AuthBearer(token): AuthBearer,
|
||||
) -> Result<Json<Response>, ServerError> {
|
||||
// validate user token
|
||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
||||
|
||||
// search for a user in database
|
||||
let response = match db.find_user_by_id(&token.claims.sub).await {
|
||||
Ok(user) => Response {
|
||||
username: user.username,
|
||||
},
|
||||
|
||||
// error while searching for a user
|
||||
Err(err) => match err {
|
||||
// user not found
|
||||
Error::UserNotFound => return Err(ServerError::AuthError(AuthError::UserNotFound)),
|
||||
// other error
|
||||
_ => return Err(ServerError::AuthError(AuthError::Other(err.to_string()))),
|
||||
},
|
||||
};
|
||||
|
||||
// send response
|
||||
Ok(Json(response))
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
// HTTP Error
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
// axum::Error
|
||||
#[error("axum error - {0}")]
|
||||
Axum(axum::Error),
|
||||
// hyper::Error
|
||||
#[error("hyper error - {0}")]
|
||||
Hyper(hyper::Error),
|
||||
// std::net::AddrParseError
|
||||
#[error("std::net::AddrParseError - {0}")]
|
||||
AddrParseError(std::net::AddrParseError),
|
||||
}
|
||||
|
||||
/// Custom Result
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<axum::Error> for Error {
|
||||
fn from(err: axum::Error) -> Self {
|
||||
Error::Axum(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for Error {
|
||||
fn from(err: hyper::Error) -> Self {
|
||||
Error::Hyper(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::net::AddrParseError> for Error {
|
||||
fn from(err: std::net::AddrParseError) -> Self {
|
||||
Error::AddrParseError(err)
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
use std::fs;
|
||||
|
||||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
||||
use axum_auth::AuthBearer;
|
||||
use homedisk_database::Database;
|
||||
use homedisk_types::{
|
||||
config::Config,
|
||||
errors::{FsError, ServerError},
|
||||
fs::create_dir::Request,
|
||||
};
|
||||
|
||||
use crate::middleware::{find_user, validate_json, validate_jwt, validate_path};
|
||||
|
||||
pub async fn handle(
|
||||
Extension(db): Extension<Database>,
|
||||
Extension(config): Extension<Config>,
|
||||
AuthBearer(token): AuthBearer,
|
||||
request: Result<Json<Request>, JsonRejection>,
|
||||
) -> Result<(), ServerError> {
|
||||
// validate json request
|
||||
let Json(request) = validate_json(request)?;
|
||||
|
||||
// validate user token
|
||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
||||
|
||||
// validate the `path` can be used
|
||||
validate_path(&request.path)?;
|
||||
|
||||
// search for a user by UUID from a token
|
||||
let user = find_user(&db, &token.claims.sub).await?;
|
||||
|
||||
// directory where the file will be placed
|
||||
let path = format!(
|
||||
"{user_dir}/{req_dir}",
|
||||
user_dir = user.user_dir(&config.storage.path),
|
||||
req_dir = request.path
|
||||
);
|
||||
|
||||
// create directories
|
||||
fs::create_dir_all(path)
|
||||
.map_err(|err| ServerError::FsError(FsError::CreateDirectory(err.to_string())))?;
|
||||
|
||||
// send an empty response
|
||||
Ok(())
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
use std::{fs, path::Path};
|
||||
|
||||
use axum::{extract::Query, Extension};
|
||||
use axum_auth::AuthBearer;
|
||||
use homedisk_database::Database;
|
||||
use homedisk_types::{
|
||||
config::Config,
|
||||
errors::{FsError, ServerError},
|
||||
fs::delete::Request,
|
||||
};
|
||||
|
||||
use crate::middleware::{find_user, validate_jwt, validate_path};
|
||||
|
||||
pub async fn handle(
|
||||
Extension(db): Extension<Database>,
|
||||
Extension(config): Extension<Config>,
|
||||
AuthBearer(token): AuthBearer,
|
||||
query: Query<Request>,
|
||||
) -> Result<(), ServerError> {
|
||||
// validate user token
|
||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
||||
|
||||
// validate the `path` can be used
|
||||
validate_path(&query.path)?;
|
||||
|
||||
// search for a user by UUID from a token
|
||||
let user = find_user(&db, &token.claims.sub).await?;
|
||||
|
||||
// path to the file
|
||||
let path = format!(
|
||||
"{user_dir}/{request_path}",
|
||||
user_dir = user.user_dir(&config.storage.path),
|
||||
request_path = query.path
|
||||
);
|
||||
let path = Path::new(&path);
|
||||
|
||||
// if file does not exist return error
|
||||
if !path.exists() {
|
||||
return Err(ServerError::FsError(FsError::FileDoesNotExist));
|
||||
}
|
||||
|
||||
// delete file
|
||||
if path.is_file() {
|
||||
fs::remove_file(&path)
|
||||
.map_err(|err| ServerError::FsError(FsError::DeleteFile(err.to_string())))?;
|
||||
}
|
||||
// delete directory
|
||||
else if path.is_dir() {
|
||||
fs::remove_dir(&path)
|
||||
.map_err(|err| ServerError::FsError(FsError::DeleteDirectory(err.to_string())))?;
|
||||
}
|
||||
|
||||
// send an empty response
|
||||
Ok(())
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
use std::fs;
|
||||
|
||||
use axum::{extract::Query, Extension};
|
||||
use axum_auth::AuthBearer;
|
||||
use homedisk_database::Database;
|
||||
use homedisk_types::{
|
||||
config::Config,
|
||||
errors::{FsError, ServerError},
|
||||
fs::upload::Pagination,
|
||||
};
|
||||
|
||||
use crate::middleware::{find_user, validate_jwt, validate_path};
|
||||
|
||||
pub async fn handle(
|
||||
Extension(db): Extension<Database>,
|
||||
Extension(config): Extension<Config>,
|
||||
AuthBearer(token): AuthBearer,
|
||||
query: Query<Pagination>,
|
||||
) -> Result<Vec<u8>, ServerError> {
|
||||
// validate user token
|
||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
||||
|
||||
// validate the `path` can be used
|
||||
validate_path(&query.path)?;
|
||||
|
||||
// search for a user by UUID from a token
|
||||
let user = find_user(&db, &token.claims.sub).await?;
|
||||
|
||||
// directory where the file will be placed
|
||||
let path = format!(
|
||||
"{user_dir}/{req_dir}",
|
||||
user_dir = user.user_dir(&config.storage.path),
|
||||
req_dir = query.path
|
||||
);
|
||||
|
||||
// read file content
|
||||
let content =
|
||||
fs::read(path).map_err(|err| ServerError::FsError(FsError::ReadFile(err.to_string())))?;
|
||||
|
||||
// send file content in response
|
||||
Ok(content)
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
use std::{fs, io, path::PathBuf, time::SystemTime};
|
||||
|
||||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
||||
use axum_auth::AuthBearer;
|
||||
use byte_unit::Byte;
|
||||
use homedisk_database::Database;
|
||||
use homedisk_types::{
|
||||
config::Config,
|
||||
errors::{FsError, ServerError},
|
||||
fs::list::{DirInfo, FileInfo, Request, Response},
|
||||
};
|
||||
|
||||
use crate::middleware::{find_user, validate_json, validate_jwt, validate_path};
|
||||
|
||||
/// Get directory size on disk (size of all files in directory).
|
||||
fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
|
||||
fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
|
||||
dir.try_fold(0, |acc, file| {
|
||||
let file = file?;
|
||||
let size = match file.metadata()? {
|
||||
data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
|
||||
data => data.len(),
|
||||
};
|
||||
Ok(acc + size)
|
||||
})
|
||||
}
|
||||
|
||||
dir_size(fs::read_dir(path.into())?)
|
||||
}
|
||||
|
||||
pub async fn handle(
|
||||
Extension(db): Extension<Database>,
|
||||
Extension(config): Extension<Config>,
|
||||
AuthBearer(token): AuthBearer,
|
||||
request: Result<Json<Request>, JsonRejection>,
|
||||
) -> Result<Json<Response>, ServerError> {
|
||||
// validate json request
|
||||
let Json(request) = validate_json::<Request>(request)?;
|
||||
|
||||
// validate user token
|
||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
||||
|
||||
// validate the `path` can be used
|
||||
validate_path(&request.path)?;
|
||||
|
||||
// search for a user by UUID from a token
|
||||
let user = find_user(&db, &token.claims.sub).await?;
|
||||
|
||||
// directory where the file will be placed
|
||||
let path = format!(
|
||||
"{user_dir}/{req_dir}",
|
||||
user_dir = user.user_dir(&config.storage.path),
|
||||
req_dir = request.path
|
||||
);
|
||||
|
||||
// get paths from dir
|
||||
let paths = fs::read_dir(&path)
|
||||
.map_err(|err| ServerError::FsError(FsError::ReadDirectory(err.to_string())))?;
|
||||
|
||||
let mut files = vec![];
|
||||
let mut dirs = vec![];
|
||||
|
||||
for f in paths {
|
||||
// handle Error
|
||||
let f = f.map_err(|err| ServerError::FsError(FsError::Other(err.to_string())))?;
|
||||
|
||||
// get path metadata
|
||||
let metadata = f
|
||||
.metadata()
|
||||
.map_err(|err| ServerError::FsError(FsError::Other(err.to_string())))?;
|
||||
|
||||
// get name of the path
|
||||
let name = f.path().display().to_string().replace(&path, "");
|
||||
|
||||
// if path is directory
|
||||
if metadata.is_dir() {
|
||||
let size = Byte::from_bytes(
|
||||
dir_size(f.path().display().to_string())
|
||||
.map_err(|err| ServerError::FsError(FsError::Other(err.to_string())))?
|
||||
.into(),
|
||||
)
|
||||
.get_appropriate_unit(true)
|
||||
.to_string();
|
||||
|
||||
dirs.push(DirInfo { name, size })
|
||||
}
|
||||
// if path is file
|
||||
else {
|
||||
// get file size in bytes
|
||||
let size = Byte::from_bytes(metadata.len().into())
|
||||
.get_appropriate_unit(true)
|
||||
.to_string();
|
||||
|
||||
// get modification time in unix time format
|
||||
let modified = metadata
|
||||
.modified()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
files.push(FileInfo {
|
||||
name,
|
||||
size,
|
||||
modified,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// send response
|
||||
Ok(Json(Response { files, dirs }))
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
mod create_dir;
|
||||
mod delete;
|
||||
mod download;
|
||||
mod list;
|
||||
mod upload;
|
||||
|
||||
pub fn app() -> axum::Router {
|
||||
use axum::routing::{delete, get, post};
|
||||
|
||||
axum::Router::new()
|
||||
.route("/list", post(list::handle))
|
||||
.route("/upload", post(upload::handle))
|
||||
.route("/delete", delete(delete::handle))
|
||||
.route("/download", get(download::handle))
|
||||
.route("/createdir", post(create_dir::handle))
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
use std::{fs, io::Write, path::Path};
|
||||
|
||||
use axum::{
|
||||
extract::{Multipart, Query},
|
||||
Extension,
|
||||
};
|
||||
use axum_auth::AuthBearer;
|
||||
use futures::TryStreamExt;
|
||||
use homedisk_database::Database;
|
||||
use homedisk_types::{
|
||||
config::Config,
|
||||
errors::{FsError, ServerError},
|
||||
fs::upload::Pagination,
|
||||
};
|
||||
|
||||
use crate::middleware::{find_user, validate_jwt, validate_path};
|
||||
|
||||
pub async fn handle(
|
||||
Extension(db): Extension<Database>,
|
||||
Extension(config): Extension<Config>,
|
||||
AuthBearer(token): AuthBearer,
|
||||
mut multipart: Multipart,
|
||||
query: Query<Pagination>,
|
||||
) -> Result<(), ServerError> {
|
||||
// validate user token
|
||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
||||
|
||||
// validate the `path` can be used
|
||||
validate_path(&query.path)?;
|
||||
|
||||
// search for a user by UUID from a token
|
||||
let user = find_user(&db, &token.claims.sub).await?;
|
||||
|
||||
// path to the file
|
||||
let file_path = format!(
|
||||
"{user_dir}/{request_path}",
|
||||
user_dir = user.user_dir(&config.storage.path),
|
||||
request_path = query.path
|
||||
);
|
||||
let file_path = Path::new(&file_path);
|
||||
|
||||
// check if the file currently exists to avoid overwriting it
|
||||
if file_path.exists() {
|
||||
return Err(ServerError::FsError(FsError::FileAlreadyExists));
|
||||
}
|
||||
|
||||
// create a directory where the file will be placed
|
||||
// e.g. path ==> `/secret/files/images/screenshot.png`
|
||||
// directories up to `{storage dir}/{username}/secret/files/images/` will be created
|
||||
if let Some(prefix) = file_path.parent() {
|
||||
fs::create_dir_all(&prefix)
|
||||
.map_err(|err| ServerError::FsError(FsError::CreateFile(err.to_string())))?
|
||||
}
|
||||
|
||||
// get multipart field
|
||||
let field = multipart
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|_| ServerError::FsError(FsError::MultipartError))?
|
||||
.ok_or(ServerError::FsError(FsError::MultipartError))?;
|
||||
|
||||
// create file
|
||||
let file = std::fs::File::create(&file_path)
|
||||
.map_err(|err| ServerError::FsError(FsError::CreateFile(err.to_string())))?;
|
||||
|
||||
// write file (chunk by chunk)
|
||||
field
|
||||
.try_fold((file, 0u64), |(mut file, written_len), bytes| async move {
|
||||
file.write_all(bytes.as_ref())
|
||||
.expect("failed to write chunk to file");
|
||||
|
||||
Ok((file, written_len + bytes.len() as u64))
|
||||
})
|
||||
.await
|
||||
.map_err(|err| ServerError::FsError(FsError::WriteFile(err.to_string())))?;
|
||||
|
||||
// send an empty response
|
||||
Ok(())
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
#![doc(html_root_url = "https://homedisk-doc.medzik.xyz")]
|
||||
|
||||
mod auth;
|
||||
mod error;
|
||||
mod fs;
|
||||
mod middleware;
|
||||
|
||||
use axum::{http::HeaderValue, routing::get, Extension, Router, Server};
|
||||
use homedisk_database::Database;
|
||||
use homedisk_types::config::Config;
|
||||
use log::{debug, info};
|
||||
use tower_http::cors::{AllowOrigin, CorsLayer};
|
||||
|
||||
/// Handle `/health-check` requests
|
||||
async fn health_check() -> &'static str {
|
||||
"I'm alive!"
|
||||
}
|
||||
|
||||
/// Start HTTP server
|
||||
pub async fn serve_http(
|
||||
host: String,
|
||||
origins: Vec<HeaderValue>,
|
||||
db: Database,
|
||||
config: Config,
|
||||
) -> error::Result<()> {
|
||||
debug!("Starting http server");
|
||||
info!("Website available at: http://{host}");
|
||||
|
||||
// create http Router
|
||||
let app = Router::new()
|
||||
.route("/health-check", get(health_check))
|
||||
.nest("/api/auth", auth::app())
|
||||
.nest("/api/fs", fs::app())
|
||||
.layer(CorsLayer::new().allow_origin(AllowOrigin::list(origins)))
|
||||
.layer(Extension(db))
|
||||
.layer(Extension(config));
|
||||
|
||||
// bind the provided address and serve Router
|
||||
Server::bind(&host.parse()?)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
use crypto_utils::jsonwebtoken::{Token, TokenData};
|
||||
use homedisk_types::errors::{AuthError, ServerError};
|
||||
|
||||
/// Validate user token
|
||||
pub fn validate_jwt(secret: &[u8], token: &str) -> Result<TokenData, ServerError> {
|
||||
match Token::decode(secret, token.to_string()) {
|
||||
// if success return claims
|
||||
Ok(claims) => Ok(claims),
|
||||
// invalid token
|
||||
Err(_) => Err(ServerError::AuthError(AuthError::InvalidToken)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use homedisk_database::User;
|
||||
|
||||
use super::validate_jwt;
|
||||
use crate::middleware::create_token;
|
||||
|
||||
const USERNAME: &str = "username";
|
||||
const PASSWORD: &str = "password";
|
||||
|
||||
const SECRET: &[u8] = b"secret";
|
||||
const INVALID_SECRET: &[u8] = b"invalid secret";
|
||||
|
||||
/// Test a token validation
|
||||
#[test]
|
||||
fn validate_token() {
|
||||
let user = User::new(USERNAME, PASSWORD);
|
||||
|
||||
let token = create_token(&user, SECRET, 1).unwrap();
|
||||
|
||||
validate_jwt(SECRET, &token).unwrap();
|
||||
}
|
||||
|
||||
/// Test a token validation (invalid secret)
|
||||
#[test]
|
||||
fn validate_token_invalid_secret() {
|
||||
let user = User::new(USERNAME, PASSWORD);
|
||||
|
||||
let token = create_token(&user, SECRET, 1).unwrap();
|
||||
|
||||
validate_jwt(INVALID_SECRET, &token).unwrap_err();
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
use crypto_utils::jsonwebtoken::{Claims, Token};
|
||||
use homedisk_database::{Database, User};
|
||||
use homedisk_types::errors::{AuthError, ServerError};
|
||||
|
||||
/// Create user token
|
||||
pub fn create_token(user: &User, secret: &[u8], expires: i64) -> Result<String, ServerError> {
|
||||
let token = Token::new(secret, Claims::new(&user.id, expires))
|
||||
.map_err(|_| ServerError::AuthError(AuthError::TokenGenerate))?;
|
||||
|
||||
Ok(token.encoded)
|
||||
}
|
||||
|
||||
/// Search for a user
|
||||
pub async fn find_user(db: &Database, user_id: &str) -> Result<User, ServerError> {
|
||||
match db.find_user_by_id(user_id).await {
|
||||
// if success return user
|
||||
Ok(user) => Ok(user),
|
||||
// errors
|
||||
Err(err) => match err {
|
||||
// user not found
|
||||
homedisk_database::Error::UserNotFound => {
|
||||
Err(ServerError::AuthError(AuthError::UserNotFound))
|
||||
},
|
||||
// other error
|
||||
_ => Err(ServerError::AuthError(AuthError::Other(err.to_string()))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use homedisk_database::User;
|
||||
|
||||
use super::create_token;
|
||||
|
||||
const SECRET: &[u8] = b"secret";
|
||||
|
||||
const USERNAME: &str = "username";
|
||||
const PASSWORD: &str = "password";
|
||||
|
||||
/// Test a token creation
|
||||
#[test]
|
||||
fn test_create_token() {
|
||||
let secret = SECRET;
|
||||
let user = User::new(USERNAME, PASSWORD);
|
||||
|
||||
create_token(&user, secret, 1).unwrap();
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
mod auth;
|
||||
mod jwt;
|
||||
mod validate_json;
|
||||
mod validate_path;
|
||||
|
||||
pub use auth::*;
|
||||
pub use jwt::*;
|
||||
pub use validate_json::*;
|
||||
pub use validate_path::*;
|
|
@ -1,20 +0,0 @@
|
|||
use axum::{extract::rejection::JsonRejection, Json};
|
||||
use homedisk_types::errors::ServerError;
|
||||
|
||||
/// Validate json request
|
||||
pub fn validate_json<T>(payload: Result<Json<T>, JsonRejection>) -> Result<Json<T>, ServerError> {
|
||||
match payload {
|
||||
// if success return payload
|
||||
Ok(payload) => Ok(payload),
|
||||
// mission json in Content-Type Header
|
||||
Err(JsonRejection::MissingJsonContentType(_)) => Err(ServerError::InvalidContentType),
|
||||
// failed to deserialize json
|
||||
Err(JsonRejection::JsonDataError(_)) => Err(ServerError::JsonDataError),
|
||||
// syntax error in json
|
||||
Err(JsonRejection::JsonSyntaxError(_)) => Err(ServerError::JsonSyntaxError),
|
||||
// failed to extract the request body
|
||||
Err(JsonRejection::BytesRejection(_)) => Err(ServerError::BytesRejection),
|
||||
// other error
|
||||
Err(err) => Err(ServerError::Other(err.to_string())),
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
use homedisk_types::errors::{FsError, ServerError};
|
||||
|
||||
/// Validate path param provided in the request
|
||||
pub fn validate_path(path: &str) -> Result<(), ServerError> {
|
||||
// `path` can't contain `..`
|
||||
// to prevent attack attempts because by using a `..` you can access the previous folder
|
||||
if path.contains("..") {
|
||||
return Err(ServerError::FsError(FsError::ReadDirectory(
|
||||
"the `path` can't contain `..`".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// `path` can't contain `~`
|
||||
// to prevent attack attempts because `~` can get up a directory on `$HOME`
|
||||
if path.contains('~') {
|
||||
return Err(ServerError::FsError(FsError::ReadDirectory(
|
||||
"the `path` can't not contain `~`".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_path() {
|
||||
// Successfully
|
||||
assert!(validate_path("Directory/path/to/test.png").is_ok());
|
||||
assert!(validate_path("/test.png").is_ok()); // `/` doesn't point to the system root
|
||||
assert!(validate_path("./test.png").is_ok());
|
||||
|
||||
// Errors
|
||||
assert!(validate_path("../../test.png").is_err());
|
||||
assert!(validate_path("../test.png").is_err());
|
||||
assert!(validate_path("~/test.png").is_err());
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ pub enum Error {
|
|||
ConnectDatabase(sqlx::Error),
|
||||
#[error("failed to get row: {0}")]
|
||||
GetRow(sqlx::Error),
|
||||
#[error("failed to create all required tables: {0}")]
|
||||
CreateTables(sqlx::Error),
|
||||
#[error("failed to execute the query: {0}")]
|
||||
Execute(sqlx::Error),
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
//! [SQLite database functions](sqlite::Database).
|
||||
|
||||
pub mod error;
|
||||
mod sqlite;
|
||||
mod user;
|
||||
|
||||
pub use sqlite::*;
|
||||
pub use user::*;
|
|
@ -0,0 +1,174 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{
|
||||
sqlite::{SqliteConnectOptions, SqliteQueryResult, SqliteRow},
|
||||
ConnectOptions, Executor, Row, SqlitePool,
|
||||
};
|
||||
use tracing::{debug, info, log::LevelFilter};
|
||||
|
||||
use super::{
|
||||
error::{Error, Result},
|
||||
User,
|
||||
};
|
||||
|
||||
/// SQLite database
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
/// Sqlite connection pool
|
||||
pub pool: SqlitePool,
|
||||
}
|
||||
|
||||
// TODO: Check UUID
|
||||
impl Database {
|
||||
/// Open a SQLite database
|
||||
pub async fn open(path: &str) -> Result<Self> {
|
||||
debug!("Opening SQLite database: {}", path);
|
||||
|
||||
// sqlite connection options
|
||||
let mut options = SqliteConnectOptions::from_str(path).map_err(Error::OpenDatabase)?;
|
||||
|
||||
// set log level to Debug
|
||||
options.log_statements(LevelFilter::Debug);
|
||||
|
||||
// create a database pool
|
||||
let pool = SqlitePool::connect_with(options)
|
||||
.await
|
||||
.map_err(Error::ConnectDatabase)?;
|
||||
|
||||
info!("Connected to database!");
|
||||
|
||||
Ok(Self { pool })
|
||||
}
|
||||
|
||||
/// Create all required tables for HomeDisk.
|
||||
pub async fn create_tables(&self) -> Result<SqliteQueryResult> {
|
||||
self.pool
|
||||
.execute(include_str!("../../tables.sql"))
|
||||
.await
|
||||
.map_err(Error::CreateTables)
|
||||
}
|
||||
|
||||
/// Create new user in the database.
|
||||
pub async fn create_user(&self, user: &User) -> Result<SqliteQueryResult> {
|
||||
debug!("Creating user - {}", user.username);
|
||||
|
||||
// build sql query
|
||||
let query = sqlx::query("INSERT INTO user (id, username, password) VALUES (?, ?, ?)")
|
||||
.bind(&user.id)
|
||||
.bind(&user.username)
|
||||
.bind(&user.password);
|
||||
|
||||
self.pool.execute(query).await.map_err(Error::Execute)
|
||||
}
|
||||
|
||||
/// Search for a user.
|
||||
pub async fn find_user(&self, user: &User) -> Result<User> {
|
||||
debug!("Searching for a user - {}", user.username);
|
||||
|
||||
// create query request to database
|
||||
let query = sqlx::query("SELECT * FROM user WHERE username = ? AND password = ?")
|
||||
.bind(&user.username)
|
||||
.bind(&user.password);
|
||||
|
||||
// fetch query
|
||||
let mut stream = self.pool.fetch(query);
|
||||
|
||||
// get rows from query
|
||||
let row = stream
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(Error::Execute)?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
|
||||
Self::find(row)
|
||||
}
|
||||
|
||||
/// Search for a user by UUID.
|
||||
pub async fn find_user_by_id(&self, id: &str) -> Result<User> {
|
||||
debug!("Searching for a user by UUID - {}", id);
|
||||
|
||||
// create query request to database
|
||||
let query = sqlx::query("SELECT * FROM user WHERE id = ?").bind(id);
|
||||
|
||||
// fetch query
|
||||
let mut stream = self.pool.fetch(query);
|
||||
|
||||
// get rows from query
|
||||
let row = stream
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(Error::Execute)?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
|
||||
Self::find(row)
|
||||
}
|
||||
|
||||
fn find(row: SqliteRow) -> Result<User> {
|
||||
// get `id` row
|
||||
let id = row.try_get("id").map_err(Error::GetRow)?;
|
||||
// get `username` row
|
||||
let username = row.try_get("username").map_err(Error::GetRow)?;
|
||||
// get `password` row
|
||||
let password = row.try_get("password").map_err(Error::GetRow)?;
|
||||
|
||||
Ok(User {
|
||||
id,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const USERNAME: &str = "medzik";
|
||||
const PASSWORD: &str = "SuperSecretPassword123!";
|
||||
|
||||
async fn open_db() -> Database {
|
||||
Database::open("sqlite::memory:")
|
||||
.await
|
||||
.expect("Failed to open database in memory")
|
||||
}
|
||||
|
||||
async fn new_user() -> Database {
|
||||
let db = open_db().await;
|
||||
|
||||
// create tables
|
||||
db.create_tables().await.expect("create tables");
|
||||
|
||||
// create new user
|
||||
let user = User::new(USERNAME, PASSWORD, true);
|
||||
db.create_user(&user).await.expect("create user");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_user() {
|
||||
new_user().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_find_user() {
|
||||
let db = new_user().await;
|
||||
|
||||
let user = db.find_user(&User::new(USERNAME, PASSWORD, false)).await.unwrap();
|
||||
|
||||
assert_eq!(user.username, USERNAME)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_find_user_wrong_password() {
|
||||
let db = new_user().await;
|
||||
|
||||
let err = db
|
||||
.find_user(&User::new(USERNAME, "wrong password 123!", false))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "user not found")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
use crypto_utils::sha::{Algorithm, CryptographicHash};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// SQL user entry
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// The function create a unique UUID and compute SHA-512 hash from salted user password
|
||||
/// and returns the [User] type.
|
||||
///
|
||||
/// **Note**: This function **does not** create a user in database!
|
||||
pub fn new(username: &str, password: &str, gen_uuid: bool) -> Self {
|
||||
// change username to lowercase
|
||||
let username = username.to_lowercase();
|
||||
|
||||
// salting the password
|
||||
let password = format!("{username}${password}");
|
||||
|
||||
// hash the password using SHA-512 algorithm and encode it into String.
|
||||
let password = hex::encode(CryptographicHash::hash(
|
||||
Algorithm::SHA512,
|
||||
password.as_bytes(),
|
||||
));
|
||||
|
||||
// generate UUID
|
||||
let id = if gen_uuid {
|
||||
Uuid::new_v4().to_string()
|
||||
} else {
|
||||
"none".to_string()
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
username,
|
||||
password,
|
||||
}
|
||||
}
|
||||
|
||||
/// The function returns the directory where the user files is located.
|
||||
pub fn user_dir(&self, storage: &str) -> String {
|
||||
format!("{storage}/{username}", username = self.username)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Check if the username is in lowercase.
|
||||
#[test]
|
||||
fn test_username_is_lowercase() {
|
||||
// example user data
|
||||
let username = "MeDZiK";
|
||||
let password = "password";
|
||||
|
||||
// expected username
|
||||
let username_expected = "medzik";
|
||||
|
||||
let user = User::new(username, password, false);
|
||||
|
||||
// username validation
|
||||
assert_eq!(user.username, username_expected)
|
||||
}
|
||||
|
||||
/// Check if the password is hashed.
|
||||
#[test]
|
||||
fn test_password_hashed() {
|
||||
// example user data
|
||||
let username = "username";
|
||||
let password = "password";
|
||||
|
||||
let user = User::new(username, password, false);
|
||||
|
||||
assert_ne!(user.password, password)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_dir() {
|
||||
let storage_dir: &str = "/storage";
|
||||
|
||||
let user = User::new("medzik", "whatever", false);
|
||||
|
||||
let user_dir = user.user_dir(storage_dir);
|
||||
|
||||
assert_eq!(user_dir, "/storage/medzik");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
//! Initialize logger.
|
||||
|
||||
use std::{panic, thread};
|
||||
|
||||
use tracing::{error, level_filters::LevelFilter};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const MAX_LEVEL: LevelFilter = LevelFilter::DEBUG;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const MAX_LEVEL: LevelFilter = LevelFilter::INFO;
|
||||
|
||||
/// Initialize logger (tracing and panic hook).
|
||||
pub fn init() {
|
||||
tracing_subscriber::fmt().with_max_level(MAX_LEVEL).init();
|
||||
|
||||
// catch panic and log them using tracing instead of default output to StdErr
|
||||
panic::set_hook(Box::new(|info| {
|
||||
let thread = thread::current();
|
||||
let thread = thread.name().unwrap_or("unknown");
|
||||
|
||||
let msg = match info.payload().downcast_ref::<&'static str>() {
|
||||
Some(s) => *s,
|
||||
None => match info.payload().downcast_ref::<String>() {
|
||||
Some(s) => &**s,
|
||||
None => "Box<Any>",
|
||||
},
|
||||
};
|
||||
|
||||
let backtrace = backtrace::Backtrace::new();
|
||||
|
||||
match info.location() {
|
||||
Some(location) => {
|
||||
// without backtrace
|
||||
if msg.starts_with("notrace - ") {
|
||||
error!(
|
||||
target: "panic", "thread '{}' panicked at '{}': {}:{}",
|
||||
thread,
|
||||
msg.replace("notrace - ", ""),
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
}
|
||||
// with backtrace
|
||||
else {
|
||||
error!(
|
||||
target: "panic", "thread '{}' panicked at '{}': {}:{}\n{:?}",
|
||||
thread,
|
||||
msg,
|
||||
location.file(),
|
||||
location.line(),
|
||||
backtrace
|
||||
);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
// without backtrace
|
||||
if msg.starts_with("notrace - ") {
|
||||
error!(
|
||||
target: "panic", "thread '{}' panicked at '{}'",
|
||||
thread,
|
||||
msg.replace("notrace - ", ""),
|
||||
);
|
||||
}
|
||||
// with backtrace
|
||||
else {
|
||||
error!(
|
||||
target: "panic", "thread '{}' panicked at '{}'\n{:?}",
|
||||
thread,
|
||||
msg,
|
||||
backtrace
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
//! HomeDisk cloud server
|
||||
//!
|
||||
//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
|
||||
//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
|
||||
//! [total-lines]: https://img.shields.io/tokei/lines/github/MedzikUser/HomeDisk?style=for-the-badge&logo=github&color=fede00
|
||||
//! [code-size]: https://img.shields.io/github/languages/code-size/MedzikUser/HomeDisk?style=for-the-badge&color=c8df52&logo=github
|
||||
//! [ci]: https://img.shields.io/github/workflow/status/MedzikUser/HomeDisk/Rust/main?style=for-the-badge
|
||||
//!
|
||||
//! [home-screenshot]: https://raw.githubusercontent.com/HomeDisk/.github/main/screenshots/home.png
|
||||
//!
|
||||
//! [![github]](https://github.com/MedzikUser/HomeDisk)
|
||||
//! [![docs-rs]](https://homedisk-doc.vercel.app)
|
||||
//! [![total-lines]](https://github.com/MedzikUser/HomeDisk)
|
||||
//! [![code-size]](https://github.com/MedzikUser/HomeDisk)
|
||||
//! [![ci]](https://github.com/MedzikUser/HomeDisk/actions/workflows/rust.yml)
|
||||
//!
|
||||
//! ![home-screenshot]
|
||||
//!
|
||||
//! ## 👨💻 Building
|
||||
//!
|
||||
//! First clone the repository: `git clone https://github.com/MedzikUser/HomeDisk.git`
|
||||
//!
|
||||
//! ### Requirements
|
||||
//! - [Rust](https://rust-lang.org)
|
||||
//!
|
||||
//! To build run the command: `cargo build --release`
|
||||
//!
|
||||
//! The compiled binary can be found in `./target/release/homedisk`
|
||||
//!
|
||||
//! ## Configure
|
||||
//!
|
||||
//! Go to [config] module
|
||||
|
||||
use std::{fs::File, path::Path};
|
||||
|
||||
use config::Config;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::database::Database;
|
||||
|
||||
#[path = "./types/config.rs"]
|
||||
mod config;
|
||||
mod database;
|
||||
mod logger;
|
||||
mod server;
|
||||
|
||||
/// Default SQLite file path for the database.
|
||||
pub const DATABASE_FILE: &str = "homedisk.db";
|
||||
/// Default configuration file.
|
||||
pub const CONFIG_FILE: &str = "config.toml";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
logger::init();
|
||||
|
||||
let config = Config::parse(CONFIG_FILE).expect("notrace - Failed to parse configuration file");
|
||||
|
||||
// open database connection
|
||||
let db =
|
||||
// if database file doesn't exists create it
|
||||
if !Path::new(DATABASE_FILE).exists() {
|
||||
warn!("Database file doesn't exists.");
|
||||
info!("Creating database file...");
|
||||
|
||||
// create an empty database file
|
||||
File::create(DATABASE_FILE).expect("notrace - Failed to create a database file");
|
||||
|
||||
// open database file
|
||||
let db = Database::open(DATABASE_FILE)
|
||||
.await
|
||||
.expect("notrace - Failed to open database file");
|
||||
|
||||
// create tables in the database
|
||||
db.create_tables()
|
||||
.await
|
||||
.expect("notrace - Failed to create tables in the database");
|
||||
|
||||
db
|
||||
}
|
||||
// if database file exists
|
||||
else {
|
||||
Database::open(DATABASE_FILE)
|
||||
.await
|
||||
.expect("notrace - Failed to open database file")
|
||||
};
|
||||
|
||||
server::start_server(config, db)
|
||||
.await
|
||||
.expect("notrace - HTTP Server error");
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
use axum::{Extension, Json};
|
||||
use crypto_utils::jsonwebtoken::{Claims, Token};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
database::{error::Error as DatabaseError, Database, User},
|
||||
server::error::*,
|
||||
};
|
||||
|
||||
pub async fn login(
|
||||
Extension(db): Extension<Database>,
|
||||
Extension(config): Extension<Config>,
|
||||
request: Json<Request>,
|
||||
) -> Result<Json<Response>> {
|
||||
let user = User::new(&request.username, &request.password, false);
|
||||
|
||||
let response = match db.find_user(&user).await {
|
||||
Ok(user) => {
|
||||
let token = Token::new(
|
||||
config.jwt.secret.as_bytes(),
|
||||
Claims::new(&user.id, config.jwt.expires),
|
||||
)
|
||||
.map_err(|_| Error::GenerateToken)?;
|
||||
|
||||
Response {
|
||||
access_token: token.encoded,
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
return match err {
|
||||
DatabaseError::UserNotFound => Err(Error::UserNotFound),
|
||||
_ => Err(Error::Database),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Response {
|
||||
pub access_token: String,
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
mod login;
|
||||
mod register;
|
||||
mod whoami;
|
||||
|
||||
use axum::routing::*;
|
||||
|
||||
pub fn app() -> Router {
|
||||
Router::new()
|
||||
.route("/login", post(login::login))
|
||||
.route("/register", post(register::register))
|
||||
.route("/whoami", get(whoami::whoami))
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
use axum::{Extension, Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
database::{Database, User},
|
||||
server::error::*,
|
||||
};
|
||||
|
||||
pub async fn register(
|
||||
Extension(db): Extension<Database>,
|
||||
request: Json<Request>,
|
||||
) -> Result<Json<Response>> {
|
||||
// username must contain at least 4 characters
|
||||
if request.username.len() < 4 {
|
||||
return Err(Error::UsernameTooShort);
|
||||
}
|
||||
|
||||
// username must be less than 25 characters
|
||||
if request.username.len() > 25 {
|
||||
return Err(Error::UsernameTooLong);
|
||||
}
|
||||
|
||||
// password must contain at least 8 characters
|
||||
if request.password.len() < 8 {
|
||||
return Err(Error::PasswordTooShort);
|
||||
}
|
||||
|
||||
let user = User::new(&request.username, &request.password, true);
|
||||
|
||||
if let Err(err) = db.create_user(&user).await {
|
||||
if err.to_string().contains("UNIQUE constraint failed") {
|
||||
return Err(Error::UserAlreadyExists);
|
||||
}
|
||||
|
||||
return Err(Error::Database);
|
||||
}
|
||||
|
||||
Ok(Json(Response { success: true }))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Response {
|
||||
pub success: bool,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::server::{error::*, utils::token::Token};
|
||||
|
||||
pub async fn whoami(Token(user): Token) -> Result<Json<Response>> {
|
||||
Ok(Json(Response {
|
||||
user: user.username,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Response {
|
||||
pub user: String,
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
use std::{fs, path::Path};
|
||||
|
||||
use axum::{Extension, Json};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
server::{
|
||||
error::*,
|
||||
utils::{
|
||||
path::{validate_path, PathQuery},
|
||||
token::Token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn create_dir(
|
||||
Extension(config): Extension<Config>,
|
||||
path: PathQuery,
|
||||
Token(user): Token,
|
||||
) -> Result<Json<Response>> {
|
||||
let path = validate_path(path)?;
|
||||
|
||||
let path = format!("{}/{}", user.user_dir(&config.storage.path), path);
|
||||
|
||||
if Path::new(&path).exists() {
|
||||
return Err(Error::AlreadyExists);
|
||||
}
|
||||
|
||||
fs::create_dir_all(path).map_err(|_| Error::CreateDirectory)?;
|
||||
|
||||
Ok(Json(Response { success: true }))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Response {
|
||||
pub success: bool,
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
use std::{fs, path::Path};
|
||||
|
||||
use axum::{Extension, Json};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
server::{
|
||||
error::*,
|
||||
utils::{
|
||||
path::{validate_path, PathQuery},
|
||||
token::Token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn delete(
|
||||
Extension(config): Extension<Config>,
|
||||
path: PathQuery,
|
||||
Token(user): Token,
|
||||
) -> Result<Json<Response>> {
|
||||
let path = validate_path(path)?;
|
||||
|
||||
let path = format!("{}/{}", user.user_dir(&config.storage.path), path);
|
||||
let path = Path::new(&path);
|
||||
|
||||
if !path.exists() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
fs::remove_dir(&path).map_err(|_| Error::DeleteDirectory)?;
|
||||
} else {
|
||||
fs::remove_file(&path).map_err(|_| Error::DeleteFile)?;
|
||||
}
|
||||
|
||||
Ok(Json(Response { success: true }))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Response {
|
||||
pub success: bool,
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
use std::{fs, time::SystemTime};
|
||||
|
||||
use axum::{Extension, Json};
|
||||
use byte_unit::Byte;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
server::{
|
||||
error::*,
|
||||
utils::{
|
||||
path::{validate_path, PathQuery},
|
||||
token::Token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn list(
|
||||
Extension(config): Extension<Config>,
|
||||
path: PathQuery,
|
||||
Token(user): Token,
|
||||
) -> Result<Json<Response>> {
|
||||
let path = validate_path(path)?;
|
||||
|
||||
let mut response = Response::default();
|
||||
|
||||
let path = format!("{}/{}", user.user_dir(&config.storage.path), path);
|
||||
|
||||
let paths = fs::read_dir(&path).map_err(|_| Error::FailedReadDirectory)?;
|
||||
|
||||
for dir_entry in paths {
|
||||
let dir_entry = dir_entry.unwrap();
|
||||
|
||||
let metadata = dir_entry.metadata().unwrap();
|
||||
|
||||
let name = dir_entry
|
||||
.path()
|
||||
.display()
|
||||
.to_string()
|
||||
.replace(&format!("{path}/"), "")
|
||||
.replace(&path, "");
|
||||
|
||||
if metadata.is_dir() {
|
||||
// TODO: add size and modification date
|
||||
response.dirs.push(Entry::new(name, 0.to_string(), 0))
|
||||
} else {
|
||||
let size = Byte::from_bytes(metadata.len().into())
|
||||
.get_appropriate_unit(true)
|
||||
.to_string();
|
||||
|
||||
let modified = metadata
|
||||
.modified()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
response.files.push(Entry::new(name, size, modified));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Response {
|
||||
pub dirs: Vec<Entry>,
|
||||
pub files: Vec<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
pub size: String,
|
||||
pub modified: u64,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn new(name: String, size: String, modified: u64) -> Self {
|
||||
Self {
|
||||
name,
|
||||
size,
|
||||
modified,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
mod create_dir;
|
||||
mod delete;
|
||||
mod list;
|
||||
mod upload;
|
||||
|
||||
use axum::routing::*;
|
||||
|
||||
pub fn app() -> Router {
|
||||
Router::new()
|
||||
.route("/list", get(list::list))
|
||||
.route("/createDir", get(create_dir::create_dir))
|
||||
.route("/delete", delete(delete::delete))
|
||||
.route("/upload", post(upload::upload))
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
use std::{fs, io::Write, path::Path};
|
||||
|
||||
use axum::{extract::Multipart, Extension, Json};
|
||||
use futures_util::TryStreamExt;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
server::{
|
||||
error::*,
|
||||
utils::{
|
||||
path::{validate_path, PathQuery},
|
||||
token::Token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn upload(
|
||||
Extension(config): Extension<Config>,
|
||||
path: PathQuery,
|
||||
Token(user): Token,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<Response>> {
|
||||
let path = validate_path(path)?;
|
||||
|
||||
let path = format!("{}/{}", user.user_dir(&config.storage.path), path);
|
||||
let path = Path::new(&path);
|
||||
|
||||
if path.exists() {
|
||||
return Err(Error::AlreadyExists);
|
||||
}
|
||||
|
||||
// create a directory where the file will be placed
|
||||
// e.g. path ==> `/secret/files/images/screenshot.png`
|
||||
// directories up to `{storage dir}/{username}/secret/files/images/` will be created
|
||||
if let Some(prefix) = path.parent() {
|
||||
fs::create_dir_all(&prefix).map_err(|_| Error::CreateDirectory)?
|
||||
}
|
||||
|
||||
let field = multipart
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|_| Error::Multipart)?
|
||||
.ok_or(Error::Multipart)?;
|
||||
|
||||
let file = fs::File::create(path).map_err(|_| Error::CreateFile)?;
|
||||
|
||||
field
|
||||
.try_fold((file, 0), |(mut file, written_len), bytes| async move {
|
||||
file.write_all(bytes.as_ref())
|
||||
.expect("failed to write chunk to file");
|
||||
|
||||
Ok((file, written_len + bytes.len() as u64))
|
||||
})
|
||||
.await
|
||||
.map_err(|_| Error::WriteFile)?;
|
||||
|
||||
Ok(Json(Response { success: true }))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Response {
|
||||
pub success: bool,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
mod auth;
|
||||
mod fs;
|
||||
|
||||
use axum::routing::*;
|
||||
|
||||
pub async fn health() -> &'static str {
|
||||
"I am working!"
|
||||
}
|
||||
|
||||
pub fn app() -> Router {
|
||||
Router::new()
|
||||
.nest("/auth", auth::app())
|
||||
.nest("/fs", fs::app())
|
||||
.route("/health", get(health))
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum Error {
|
||||
// auth error
|
||||
#[error("User not found")]
|
||||
UserNotFound,
|
||||
#[error("User already exists")]
|
||||
UserAlreadyExists,
|
||||
#[error("Failed to generate access token")]
|
||||
GenerateToken,
|
||||
#[error("An error occurred in database request")]
|
||||
Database,
|
||||
#[error("Username is too short")]
|
||||
UsernameTooShort,
|
||||
#[error("Username is too short")]
|
||||
UsernameTooLong,
|
||||
#[error("Password is too short")]
|
||||
PasswordTooShort,
|
||||
#[error("Invalid token")]
|
||||
InvalidToken,
|
||||
#[error("`{0}` header is missing")]
|
||||
MissingHeader(&'static str),
|
||||
#[error("`Authorization` header must be a bearer token")]
|
||||
MissingBearer,
|
||||
// fs error
|
||||
#[error("Invalid `path` query parameter")]
|
||||
InvalidPath,
|
||||
#[error("Failed to read files from directory")]
|
||||
FailedReadDirectory,
|
||||
#[error("Failed to create directory")]
|
||||
CreateDirectory,
|
||||
#[error("Failed to create file")]
|
||||
CreateFile,
|
||||
#[error("File or directory already exists")]
|
||||
AlreadyExists,
|
||||
#[error("File or directory does not exists")]
|
||||
NotFound,
|
||||
#[error("Failed to delete directory (directory is not empty?)")]
|
||||
DeleteDirectory,
|
||||
#[error("Failed to delete file")]
|
||||
DeleteFile,
|
||||
#[error("Multipart error")]
|
||||
Multipart,
|
||||
#[error("Write to file failed")]
|
||||
WriteFile,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
enum ResponseError {
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl axum::response::IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
use axum::http::StatusCode;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
error!("Error: {:?}", self);
|
||||
|
||||
let status = match self {
|
||||
Error::GenerateToken => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::Database => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
};
|
||||
|
||||
let mut response = axum::Json(ResponseError::Error(self.to_string())).into_response();
|
||||
*response.status_mut() = status;
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
@ -0,0 +1,30 @@
|
|||
mod api;
|
||||
pub mod error;
|
||||
pub mod utils;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{routing::get, Extension, Router, Server};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{config::Config, database::Database};
|
||||
|
||||
pub async fn start_server(config: Config, db: Database) -> anyhow::Result<()> {
|
||||
info!(
|
||||
"🚀 Server has launched on http://{}:{}",
|
||||
config.http.host, config.http.port
|
||||
);
|
||||
|
||||
let host = format!("{}:{}", config.http.host, config.http.port);
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/api", api::app())
|
||||
.route("/", get(api::health))
|
||||
.layer(Extension(config))
|
||||
.layer(Extension(db));
|
||||
|
||||
Server::bind(&host.parse()?)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
|
||||
Err(anyhow!("Server unexpected stopped!"))
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod token;
|
||||
pub mod path;
|
|
@ -0,0 +1,29 @@
|
|||
use axum::extract::Query;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::server::error::{Error, Result};
|
||||
|
||||
pub type PathQuery = Query<Path>;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Path {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub fn validate_path(path: PathQuery) -> Result<String> {
|
||||
let path = path.path.clone();
|
||||
|
||||
// `path` can't contain `..`
|
||||
// to prevent attack attempts because by using a `..` you can access the previous folder
|
||||
if path.contains("..") {
|
||||
return Err(Error::InvalidPath);
|
||||
}
|
||||
|
||||
// `path` can't contain `~`
|
||||
// to prevent attack attempts because `~` can get up a directory on `$HOME`
|
||||
if path.contains('~') {
|
||||
return Err(Error::InvalidPath);
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
use axum::{
|
||||
async_trait,
|
||||
extract::FromRequestParts,
|
||||
http::{header::AUTHORIZATION, request::Parts},
|
||||
};
|
||||
use crypto_utils::jsonwebtoken;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
database::{error::Error as DatabaseError, Database, User},
|
||||
server::error::Error,
|
||||
};
|
||||
|
||||
pub struct Token(pub User);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for Token
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Error;
|
||||
|
||||
async fn from_request_parts(req: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let extensions = &req.extensions;
|
||||
|
||||
let config = extensions.get::<Config>().unwrap();
|
||||
let db = extensions.get::<Database>().unwrap();
|
||||
|
||||
// Get authorisation header
|
||||
let authorisation = req
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(Error::MissingHeader("Authorization"))?
|
||||
.to_str()
|
||||
.map_err(|_| Error::InvalidToken)?;
|
||||
|
||||
// Check that its a well-formed bearer
|
||||
let split = authorisation.split_once(' ');
|
||||
|
||||
let token = match split {
|
||||
Some((name, contents)) if name == "Bearer" => contents.to_string(),
|
||||
_ => return Err(Error::MissingBearer),
|
||||
};
|
||||
|
||||
let token = match jsonwebtoken::Token::decode(config.jwt.secret.as_bytes(), token) {
|
||||
Ok(token) => token,
|
||||
Err(_) => return Err(Error::InvalidToken),
|
||||
};
|
||||
|
||||
let user = match db.find_user_by_id(&token.claims.sub).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => match err {
|
||||
DatabaseError::UserNotFound => return Err(Error::UserNotFound),
|
||||
_ => return Err(Error::Database),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Self(user))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//! [Configuration file types](Config).
|
||||
use std::fs;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Settings configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// HTTP Settings.
|
||||
pub http: ConfigHTTP,
|
||||
/// Json Web Token Settings.
|
||||
pub jwt: ConfigJWT,
|
||||
/// Storage Settings.
|
||||
pub storage: ConfigStorage,
|
||||
}
|
||||
|
||||
/// HTTP Settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigHTTP {
|
||||
/// HTTP Host.
|
||||
pub host: String,
|
||||
/// Port HTTP Port.
|
||||
pub port: u16,
|
||||
/// [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) Domains (e.g ["site1.example.com", "site2.example.com"]).
|
||||
pub cors: Vec<String>,
|
||||
}
|
||||
|
||||
/// Json Web Token Settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigJWT {
|
||||
/// JWT Secret string (used to sign tokens).
|
||||
pub secret: String,
|
||||
/// Token expiration time in hours.
|
||||
pub expires: i64,
|
||||
}
|
||||
|
||||
/// Storage Settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigStorage {
|
||||
/// Directory where user files will be stored.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Parse configuration file.
|
||||
pub fn parse(path: &str) -> anyhow::Result<Self> {
|
||||
let config_str = fs::read_to_string(path)?;
|
||||
|
||||
let config = toml::from_str(&config_str)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Test default configuration file.
|
||||
#[test]
|
||||
fn test_config() {
|
||||
Config::parse("./config.toml").expect("Failed to parse configuration file");
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
[package]
|
||||
name = "homedisk-types"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
config = ["toml", "dirs"]
|
||||
database = ["crypto-utils", "hex", "sqlx"]
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0.32"
|
||||
uuid = "1.1.2"
|
||||
anyhow = "1.0.62"
|
||||
serde = { version = "1.0.144", features = ["derive"] }
|
||||
axum = { version = "0.5.15", optional = true }
|
||||
toml = { version = "0.5.9", optional = true }
|
||||
dirs = { version = "4.0.0", optional = true }
|
||||
crypto-utils = { version = "0.4.0", features = ["sha"], optional = true }
|
||||
hex = { version = "0.4.3", optional = true }
|
||||
sqlx = { version = "0.6.1", features = ["sqlite"], optional = true }
|
|
@ -1,16 +0,0 @@
|
|||
//! HTTP `/auth/login` Request and Response types
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP `/auth/login` Request
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// HTTP `/auth/login` Response
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Response {
|
||||
pub access_token: String,
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
//! HTTP `/auth/*` types for Request and Response
|
||||
|
||||
pub mod login;
|
||||
pub mod whoami;
|
|
@ -1,10 +0,0 @@
|
|||
//! HTTP `/auth/whoami` Response type
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP `/auth/whoami` Response
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Response {
|
||||
/// Logged user username
|
||||
pub username: String,
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
//! # Configuration file
|
||||
//!
|
||||
//! Path to a config file is `config.toml` in the work directory.
|
||||
//!
|
||||
//! ## Example config
|
||||
//!
|
||||
//! ```toml
|
||||
//! [http]
|
||||
//! host = "0.0.0.0" # HTTP Host
|
||||
//! port = 8080 # HTTP Port
|
||||
//! cors = ["homedisk.medzik.xyz"] # Domains allowed for CORS
|
||||
//!
|
||||
//! [jwt]
|
||||
//! secret = "secret key used to sign tokens" # JWT Secret string (used to sign tokens)
|
||||
//! expires = 24 # Token expiration time (in hours)
|
||||
//!
|
||||
//! [storage]
|
||||
//! path = "/home/homedisk" # # Directory where user files will be stored
|
||||
//! ```
|
||||
//!
|
||||
//! ### External docs
|
||||
//! - [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
||||
|
||||
mod types;
|
||||
|
||||
pub use types::*;
|
|
@ -1,55 +0,0 @@
|
|||
use std::fs;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Configure HTTP settings
|
||||
pub http: ConfigHTTP,
|
||||
/// Configure Json Web Token settings
|
||||
pub jwt: ConfigJWT,
|
||||
/// Configure storage settings
|
||||
pub storage: ConfigStorage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigHTTP {
|
||||
/// HTTP Host
|
||||
pub host: String,
|
||||
/// Port HTTP Port
|
||||
pub port: u16,
|
||||
/// [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) Domains (e.g ["site1.example.com", "site2.example.com"])
|
||||
pub cors: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigJWT {
|
||||
/// JWT Secret string (used to sign tokens)
|
||||
pub secret: String,
|
||||
/// Token expiration time in hours
|
||||
pub expires: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigStorage {
|
||||
/// Directory where user files will be stored
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "config")]
|
||||
impl Config {
|
||||
/// Parse configuration file.
|
||||
///
|
||||
/// ```no_run
|
||||
/// use homedisk_types::config::Config;
|
||||
///
|
||||
/// let config = Config::parse().unwrap();
|
||||
/// ```
|
||||
pub fn parse() -> anyhow::Result<Self> {
|
||||
let config_str = fs::read_to_string("config.toml")?;
|
||||
|
||||
let config = toml::from_str(&config_str)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
//! Types for a database
|
||||
|
||||
mod user;
|
||||
|
||||
pub use user::*;
|
|
@ -1,138 +0,0 @@
|
|||
use crypto_utils::sha::{Algorithm, CryptographicHash};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// SQL user table
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
/// UUID of the user
|
||||
pub id: String,
|
||||
/// Username
|
||||
pub username: String,
|
||||
/// Encryped user password
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// The function create a unique user UUID and create SHA-512 hash from salted user password
|
||||
/// and returns the [User] type.
|
||||
///
|
||||
/// **Note: This doesn't create a user in the database!**
|
||||
///
|
||||
/// ```
|
||||
/// use homedisk_types::database::User;
|
||||
///
|
||||
/// let user = User::new("medzik", "SuperSecretPassword123!");
|
||||
///
|
||||
/// # assert_eq!(user.username, "medzik")
|
||||
/// ```
|
||||
pub fn new(username: &str, password: &str) -> Self {
|
||||
// change username to lowercase
|
||||
let username = username.to_lowercase();
|
||||
|
||||
// salting the password
|
||||
let password = format!("{username}${password}");
|
||||
|
||||
// hash password using SHA-512 and encode it to String from Vec<u8>
|
||||
let password = hex::encode(CryptographicHash::hash(
|
||||
Algorithm::SHA512,
|
||||
password.as_bytes(),
|
||||
));
|
||||
|
||||
// generate a user UUID
|
||||
let id_sha1 = CryptographicHash::hash(
|
||||
Algorithm::SHA1,
|
||||
(format!("{username}${password}")).as_bytes(),
|
||||
);
|
||||
let id = Uuid::new_v5(&Uuid::NAMESPACE_X500, &id_sha1).to_string();
|
||||
|
||||
// return `User`
|
||||
Self {
|
||||
id,
|
||||
username,
|
||||
password,
|
||||
}
|
||||
}
|
||||
|
||||
/// The function returns the directory where the user file is located.
|
||||
///
|
||||
/// ```
|
||||
/// use homedisk_types::database::User;
|
||||
///
|
||||
/// let user = User::new("medzik", "whatever");
|
||||
///
|
||||
/// let dir = user.user_dir("/storage"); // will return `/storage/medzik`
|
||||
///
|
||||
/// assert_eq!(dir, "/storage/medzik")
|
||||
/// ```
|
||||
pub fn user_dir(&self, storage: &str) -> String {
|
||||
// get a user storage path
|
||||
let path = format!(
|
||||
"{path}/{username}",
|
||||
path = storage,
|
||||
username = self.username,
|
||||
);
|
||||
|
||||
// return user storage path
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crypto_utils::sha::{Algorithm, CryptographicHash};
|
||||
|
||||
use super::User;
|
||||
|
||||
/// Check if the id is reproducable
|
||||
#[test]
|
||||
fn check_id_reproducable() {
|
||||
// example user data
|
||||
let username = "test";
|
||||
let password = "password";
|
||||
|
||||
let user_a = User::new(username, password);
|
||||
let user_b = User::new(username, password);
|
||||
|
||||
assert_eq!(user_a.id, user_b.id)
|
||||
}
|
||||
|
||||
/// Check if the username is in lowercase
|
||||
#[test]
|
||||
fn check_username_is_in_lowercase() {
|
||||
// example user data
|
||||
let username = "mEDZIk";
|
||||
let password = "password";
|
||||
|
||||
// username in lowercase (expected username)
|
||||
let username_expected = "medzik";
|
||||
|
||||
// create a new `User` type
|
||||
let user = User::new(username, password);
|
||||
|
||||
// username validation with expected username
|
||||
assert_eq!(user.username, username_expected)
|
||||
}
|
||||
|
||||
/// Check that the password is a checksum with a salt
|
||||
#[test]
|
||||
fn check_if_password_is_hashed_and_salted() {
|
||||
// example user data
|
||||
let username = "username";
|
||||
let password = "password";
|
||||
|
||||
// create a new `User` type
|
||||
let user = User::new(username, password);
|
||||
|
||||
// expected password salt (string)
|
||||
let password_expected_salt = format!("{username}${password}");
|
||||
|
||||
// expected password (hashed)
|
||||
let password_expected = hex::encode(CryptographicHash::hash(
|
||||
Algorithm::SHA512,
|
||||
password_expected_salt.as_bytes(),
|
||||
));
|
||||
|
||||
// password validation with expected password salt
|
||||
assert_eq!(user.password, password_expected)
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
||||
pub enum Error {
|
||||
#[error("user not found")]
|
||||
UserNotFound,
|
||||
#[error("user already exists")]
|
||||
UserAlreadyExists,
|
||||
#[error("username is too short")]
|
||||
UsernameTooShort,
|
||||
#[error("username is too long")]
|
||||
UsernameTooLong,
|
||||
#[error("password is too short")]
|
||||
PasswordTooShort,
|
||||
#[error("failed to generate jwt token")]
|
||||
TokenGenerate,
|
||||
#[error("invalid jwt token")]
|
||||
InvalidToken,
|
||||
#[error("other error - {0}")]
|
||||
Other(String),
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
||||
pub enum Error {
|
||||
#[error("file doesn't exists")]
|
||||
FileDoesNotExist,
|
||||
#[error("file already exists")]
|
||||
FileAlreadyExists,
|
||||
#[error("unexpected multipart error")]
|
||||
MultipartError,
|
||||
#[error("failed to create create a file - {0}")]
|
||||
CreateFile(String),
|
||||
#[error("failed to create a directory - {0}")]
|
||||
CreateDirectory(String),
|
||||
#[error("failed to delete file: {0}")]
|
||||
DeleteFile(String),
|
||||
#[error("failed to delete directory: {0}")]
|
||||
DeleteDirectory(String),
|
||||
#[error("failed to write content to file: {0}")]
|
||||
WriteFile(String),
|
||||
#[error("failed to decode base64: {0}")]
|
||||
Base64(String),
|
||||
#[error("failed to read directory: {0}")]
|
||||
ReadDirectory(String),
|
||||
#[error("failed to read file content: {0}")]
|
||||
ReadFile(String),
|
||||
#[error("other error - {0}")]
|
||||
Other(String),
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
//! Error types
|
||||
|
||||
mod auth;
|
||||
#[cfg(feature = "database")]
|
||||
mod database;
|
||||
mod fs;
|
||||
mod server;
|
||||
|
||||
pub use auth::Error as AuthError;
|
||||
pub use database::{Error as DatabaseError, Result as DatabaseResult};
|
||||
pub use fs::Error as FsError;
|
||||
pub use server::Error as ServerError;
|
|
@ -1,71 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{AuthError, FsError};
|
||||
|
||||
/// HTTP Server Error
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
||||
#[serde(tag = "error", content = "error_message", rename_all = "kebab-case")]
|
||||
pub enum Error {
|
||||
#[error("auth error: {0}")]
|
||||
AuthError(#[from] AuthError),
|
||||
#[error("fs error: {0}")]
|
||||
FsError(#[from] FsError),
|
||||
#[error("too may requests, please slow down")]
|
||||
TooManyRequests,
|
||||
#[error("invalid Content-Type")]
|
||||
InvalidContentType,
|
||||
#[error("failed to deserialize json")]
|
||||
JsonDataError,
|
||||
#[error("syntax error in json")]
|
||||
JsonSyntaxError,
|
||||
#[error("failed to extract the request body")]
|
||||
BytesRejection,
|
||||
#[error("other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
enum ResponseError {
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn into_response(self) -> ResponseError {
|
||||
ResponseError::Error(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
impl axum::response::IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
use axum::http::StatusCode;
|
||||
|
||||
let status = match self {
|
||||
Self::AuthError(ref err) => match err {
|
||||
AuthError::TokenGenerate => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AuthError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
},
|
||||
Self::FsError(ref err) => match err {
|
||||
FsError::CreateFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
FsError::CreateDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
FsError::DeleteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
FsError::DeleteDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
FsError::WriteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
FsError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
},
|
||||
Self::TooManyRequests => StatusCode::TOO_MANY_REQUESTS,
|
||||
Self::BytesRejection => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
};
|
||||
|
||||
let mut response = axum::Json(self.into_response()).into_response();
|
||||
*response.status_mut() = status;
|
||||
|
||||
response
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
//! HTTP `/fs/createdir` Request and Response types
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP `/fs/createdir` Request
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Request {
|
||||
/// Path to directory wich will be created
|
||||
pub path: String,
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
//! HTTP `/fs/delete` Request type
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP `/fs/delete` Request
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Request {
|
||||
/// Path of file/directory to be delete
|
||||
pub path: String,
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
//! HTTP `/fs/download` Request type
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP `/fs/download` Request
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Request {
|
||||
/// Path of file to be download
|
||||
pub path: String,
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
//! HTTP `/fs/list` Request, Response, FileInfo and DirInfo types
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP `/fs/list` Request
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Request {
|
||||
/// Path to directory
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// HTTP `/fs/list` Response
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Response {
|
||||
/// Vector with files info
|
||||
pub files: Vec<FileInfo>,
|
||||
/// Vector with directories info
|
||||
pub dirs: Vec<DirInfo>,
|
||||
}
|
||||
|
||||
/// Info about a file
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FileInfo {
|
||||
/// File name
|
||||
pub name: String,
|
||||
/// File size
|
||||
pub size: String,
|
||||
/// Latest modification of this file
|
||||
pub modified: u64,
|
||||
}
|
||||
|
||||
/// Info about a directory
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DirInfo {
|
||||
/// Directory name
|
||||
pub name: String,
|
||||
/// Directory size (size of all files in directory)
|
||||
pub size: String,
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
//! HTTP `/fs/*` types for Request and Response
|
||||
|
||||
pub mod create_dir;
|
||||
pub mod delete;
|
||||
pub mod download;
|
||||
pub mod list;
|
||||
pub mod upload;
|
|
@ -1,10 +0,0 @@
|
|||
//! HTTP `/fs/upload` Request and Response types
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP `/fs/upload` Queries
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Pagination {
|
||||
/// Path where the file will be uploaded
|
||||
pub path: String,
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#![doc(html_root_url = "https://homedisk-doc.medzik.xyz")]
|
||||
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
#[cfg(feature = "database")]
|
||||
pub mod database;
|
||||
pub mod errors;
|
||||
pub mod fs;
|
Loading…
Reference in New Issue