Tweaks for the desktop client
This commit is contained in:
@@ -67,10 +67,11 @@ pub async fn get_permissions(
|
||||
}
|
||||
|
||||
pub async fn get_name(file_id: Uuid, pool: &Pool) -> sqlx::Result<Option<String>> {
|
||||
let record = sqlx::query!("SELECT file_name FROM files WHERE file_id = $1", file_id)
|
||||
let name = sqlx::query!("SELECT file_name FROM files WHERE file_id = $1", file_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(record.map(|record| record.file_name))
|
||||
.await?
|
||||
.map(|record| record.file_name);
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
pub async fn delete(file_id: Uuid, pool: &Pool) -> sqlx::Result<bool> {
|
||||
|
@@ -89,10 +89,24 @@ pub async fn name_exists(parent_folder_id: Uuid, name: &str, pool: &Pool) -> sql
|
||||
|
||||
/// Creates a folder in the database. Do not use this function to create the ROOT folder
|
||||
pub async fn insert(parent_folder_id: Uuid, folder_name: &str, pool: &Pool) -> sqlx::Result<Uuid> {
|
||||
sqlx::query_file!("sql/create_folder.sql", parent_folder_id, folder_name)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map(|record| record.folder_id)
|
||||
let folder_id = Uuid::now_v7();
|
||||
let owner_id = get_by_id(parent_folder_id, pool)
|
||||
.await?
|
||||
.ok_or(sqlx::Error::RowNotFound)?
|
||||
.owner_id;
|
||||
let result = sqlx::query_file!(
|
||||
"sql/create_folder.sql",
|
||||
parent_folder_id,
|
||||
owner_id,
|
||||
folder_name,
|
||||
folder_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(sqlx::Error::RowNotFound);
|
||||
}
|
||||
Ok(folder_id)
|
||||
}
|
||||
|
||||
pub fn delete(folder_id: Uuid, pool: &Pool) -> impl Stream<Item = sqlx::Result<Uuid>> + '_ {
|
||||
|
@@ -1,8 +1,11 @@
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use db::folder::FolderWithoutParentId;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(sqlx::Type, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[sqlx(type_name = "permission")]
|
||||
#[sqlx(rename_all = "lowercase")]
|
||||
pub enum PermissionRaw {
|
||||
@@ -120,10 +123,10 @@ pub async fn insert(
|
||||
pub async fn get_all_for_folder(
|
||||
folder_id: Uuid,
|
||||
pool: &Pool,
|
||||
) -> sqlx::Result<HashMap<String, PermissionRaw>> {
|
||||
) -> sqlx::Result<HashMap<i32, PermissionRaw>> {
|
||||
sqlx::query_file!("sql/get_all_permissions_for_folder.sql", folder_id)
|
||||
.fetch(pool)
|
||||
.map_ok(|record| (record.username, record.permission_type))
|
||||
.map_ok(|record| (record.user_id, record.permission_type))
|
||||
.try_collect()
|
||||
.await
|
||||
}
|
||||
@@ -135,10 +138,16 @@ pub async fn delete_for_folder(folder_id: Uuid, user_id: i32, pool: &Pool) -> sq
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub async fn get_top_level_permitted_folders(user_id: i32, pool: &Pool) -> sqlx::Result<Vec<Uuid>> {
|
||||
sqlx::query_file!("sql/get_top_level_folder.sql", user_id)
|
||||
.fetch(pool)
|
||||
.map_ok(|record| record.folder_id)
|
||||
.try_collect()
|
||||
.await
|
||||
pub async fn get_top_level_permitted_folders(
|
||||
user_id: i32,
|
||||
pool: &Pool,
|
||||
) -> sqlx::Result<Vec<FolderWithoutParentId>> {
|
||||
sqlx::query_file_as!(
|
||||
FolderWithoutParentId,
|
||||
"sql/get_top_level_folder.sql",
|
||||
user_id
|
||||
)
|
||||
.fetch(pool)
|
||||
.try_collect()
|
||||
.await
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ pub async fn create_user(
|
||||
pool: &Pool,
|
||||
) -> sqlx::Result<Option<i32>> {
|
||||
let Some(record) = sqlx::query!(
|
||||
"INSERT INTO users(username, email, hashed_password) VALUES ($1, $2, $3) RETURNING user_id",
|
||||
"INSERT INTO users(username, email, hashed_password) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING RETURNING user_id",
|
||||
user_name,
|
||||
user_email,
|
||||
hashed_password
|
||||
@@ -80,7 +80,7 @@ pub async fn get(user_id: i32, pool: &Pool) -> sqlx::Result<Option<UserInfo>> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Gets the hashed password field by either the email or th username
|
||||
/// Gets the hashed password field by either the email or the username
|
||||
pub async fn get_hash(search_string: &str, pool: &Pool) -> sqlx::Result<Option<(i32, Vec<u8>)>> {
|
||||
let record = sqlx::query!(
|
||||
"SELECT user_id, hashed_password FROM users WHERE username = $1 OR email = $1",
|
||||
|
@@ -14,15 +14,9 @@ pub async fn delete(
|
||||
.await
|
||||
.can_write_guard()?;
|
||||
|
||||
let deleted = db::file::delete(params.file_id, &state.pool)
|
||||
db::file::delete(params.file_id, &state.pool)
|
||||
.await
|
||||
.handle_internal("Error deleting the file")?;
|
||||
if !deleted {
|
||||
return Err(GeneralError::message(
|
||||
StatusCode::NOT_FOUND,
|
||||
"Item not found",
|
||||
)); // Will not happen most of the time due to can write guard
|
||||
}
|
||||
|
||||
state
|
||||
.storage
|
||||
|
@@ -8,7 +8,7 @@ pub struct Params {
|
||||
pub async fn delete(
|
||||
State(state): State<AppState>,
|
||||
claims: Claims,
|
||||
Json(params): Json<Params>,
|
||||
Query(params): Query<Params>,
|
||||
) -> GeneralResult<()> {
|
||||
let root = db::folder::get_root(claims.user_id, &state.pool)
|
||||
.await
|
||||
|
@@ -1,3 +1,4 @@
|
||||
use db::{file::FileWithoutParentId, folder::FolderWithoutParentId};
|
||||
use tokio::try_join;
|
||||
|
||||
use super::list::Params;
|
||||
@@ -6,13 +7,13 @@ use crate::prelude::*;
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct FolderStructure {
|
||||
#[serde(flatten)]
|
||||
folder_base: db::folder::FolderWithoutParentId,
|
||||
folder_base: FolderWithoutParentId,
|
||||
folders: Vec<FolderStructure>,
|
||||
files: Vec<db::file::FileWithoutParentId>,
|
||||
files: Vec<FileWithoutParentId>,
|
||||
}
|
||||
|
||||
impl From<db::folder::FolderWithoutParentId> for FolderStructure {
|
||||
fn from(value: db::folder::FolderWithoutParentId) -> Self {
|
||||
impl From<FolderWithoutParentId> for FolderStructure {
|
||||
fn from(value: FolderWithoutParentId) -> Self {
|
||||
FolderStructure {
|
||||
folder_base: value,
|
||||
folders: Vec::new(),
|
||||
|
@@ -9,7 +9,7 @@ pub struct Params {
|
||||
pub async fn delete(
|
||||
State(pool): State<Pool>,
|
||||
claims: Claims,
|
||||
Json(params): Json<Params>,
|
||||
Query(params): Query<Params>,
|
||||
) -> GeneralResult<StatusCode> {
|
||||
if params.user_id != claims.user_id {
|
||||
db::folder::get_permissions(params.folder_id, claims.user_id, &pool)
|
||||
|
@@ -13,13 +13,13 @@ pub async fn get(
|
||||
State(pool): State<Pool>,
|
||||
Query(params): Query<Params>,
|
||||
claims: Claims,
|
||||
) -> GeneralResult<Json<HashMap<String, PermissionRaw>>> {
|
||||
) -> GeneralResult<Json<HashMap<i32, PermissionRaw>>> {
|
||||
db::folder::get_permissions(params.folder_id, claims.user_id, &pool)
|
||||
.await
|
||||
.can_manage_guard()?;
|
||||
|
||||
let permissions = db::permissions::get_all_for_folder(params.folder_id, &pool)
|
||||
db::permissions::get_all_for_folder(params.folder_id, &pool)
|
||||
.await
|
||||
.handle_internal("Error getting permissions")?;
|
||||
Ok(Json(permissions))
|
||||
.handle_internal("Error getting permissions")
|
||||
.map(Json)
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
use db::folder::FolderWithoutParentId;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub async fn get_top_level(
|
||||
State(pool): State<Pool>,
|
||||
claims: Claims,
|
||||
) -> GeneralResult<Json<Vec<Uuid>>> {
|
||||
let folders = db::permissions::get_top_level_permitted_folders(claims.user_id, &pool)
|
||||
) -> GeneralResult<Json<Vec<FolderWithoutParentId>>> {
|
||||
db::permissions::get_top_level_permitted_folders(claims.user_id, &pool)
|
||||
.await
|
||||
.handle_internal("Error reading from the database")?;
|
||||
Ok(Json(folders))
|
||||
.handle_internal("Error reading from the database")
|
||||
.map(Json)
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ pub async fn set(
|
||||
if params.folder_id == root {
|
||||
return Err(GeneralError::message(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Cannot delete the root folder",
|
||||
"Cannot set permissions for the root folder",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -26,6 +26,13 @@ pub async fn set(
|
||||
.await
|
||||
.can_manage_guard()?;
|
||||
|
||||
if params.user_id == claims.user_id {
|
||||
return Err(GeneralError::message(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Cannot set your own permissions",
|
||||
));
|
||||
}
|
||||
|
||||
let folder_info = db::folder::get_by_id(params.folder_id, &pool)
|
||||
.await
|
||||
.handle_internal("Error getting folder info")?
|
||||
@@ -33,7 +40,7 @@ pub async fn set(
|
||||
if folder_info.owner_id == params.user_id {
|
||||
return Err(GeneralError::message(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Cannot set permissions of the folder owner",
|
||||
"Cannot set permissions of the folder's owner",
|
||||
));
|
||||
}
|
||||
|
||||
|
@@ -8,11 +8,11 @@ pub struct Params {
|
||||
type Response = GeneralResult<Json<db::users::UserInfo>>;
|
||||
|
||||
pub async fn get(State(pool): State<Pool>, Query(params): Query<Params>) -> Response {
|
||||
let info = db::users::get(params.user_id, &pool)
|
||||
db::users::get(params.user_id, &pool)
|
||||
.await
|
||||
.handle_internal("Error getting the user")?
|
||||
.handle(StatusCode::NOT_FOUND, "User not found")?;
|
||||
Ok(Json(info))
|
||||
.handle(StatusCode::NOT_FOUND, "User not found")
|
||||
.map(Json)
|
||||
}
|
||||
|
||||
pub async fn current(state: State<Pool>, claims: Claims) -> Response {
|
||||
|
@@ -4,7 +4,7 @@ use crate::prelude::*;
|
||||
|
||||
#[derive(Deserialize, Debug, Validate)]
|
||||
pub struct Params {
|
||||
#[validate(email)]
|
||||
#[validate(length(min = 3, max = 10))]
|
||||
username: String,
|
||||
#[validate(email)]
|
||||
email: String,
|
||||
@@ -15,7 +15,7 @@ pub async fn put(
|
||||
claims: Claims,
|
||||
Json(params): Json<Params>,
|
||||
) -> GeneralResult<Json<db::users::UserInfo>> {
|
||||
params.validate().handle_validation()?;
|
||||
params.validate()?;
|
||||
db::users::update(claims.user_id, ¶ms.username, ¶ms.email, &pool)
|
||||
.await
|
||||
.handle_internal("Error updating the user")
|
||||
|
@@ -48,7 +48,7 @@ pub async fn register(
|
||||
State(pool): State<Pool>,
|
||||
Form(params): Form<Params>,
|
||||
) -> GeneralResult<Json<Token>> {
|
||||
params.validate().handle_validation()?;
|
||||
params.validate()?;
|
||||
|
||||
let password = HashedBytes::hash_bytes(params.password.as_bytes()).as_bytes();
|
||||
let id = db::users::create_user(¶ms.username, ¶ms.email, &password, &pool)
|
||||
|
@@ -41,6 +41,12 @@ impl IntoResponse for GeneralError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<validator::ValidationErrors> for GeneralError {
|
||||
fn from(value: validator::ValidationErrors) -> Self {
|
||||
GeneralError::message(StatusCode::BAD_REQUEST, value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type GeneralResult<T> = Result<T, GeneralError>;
|
||||
|
||||
pub trait ErrorHandlingExt<T>
|
||||
@@ -92,19 +98,6 @@ pub trait ItemNotFoundExt<T> {
|
||||
|
||||
impl<T> ItemNotFoundExt<T> for Option<T> {
|
||||
fn item_not_found(self) -> GeneralResult<T> {
|
||||
self.ok_or(GeneralError::const_message(
|
||||
StatusCode::NOT_FOUND,
|
||||
"Item not found",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ValidationExt<T> {
|
||||
fn handle_validation(self) -> GeneralResult<T>;
|
||||
}
|
||||
|
||||
impl<T> ValidationExt<T> for Result<T, validator::ValidationErrors> {
|
||||
fn handle_validation(self) -> GeneralResult<T> {
|
||||
self.map_err(|err| GeneralError::message(StatusCode::BAD_REQUEST, err.to_string()))
|
||||
self.handle(StatusCode::NOT_FOUND, "Item not found")
|
||||
}
|
||||
}
|
||||
|
30
src/main.rs
30
src/main.rs
@@ -10,7 +10,7 @@ use std::{env, net::Ipv4Addr};
|
||||
use auth::HashedBytes;
|
||||
use axum::{extract::FromRef, routing::post, Router};
|
||||
use file_storage::FileStorage;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::{net::TcpListener, signal};
|
||||
|
||||
type Pool = sqlx::postgres::PgPool;
|
||||
|
||||
@@ -66,11 +66,37 @@ async fn main() -> anyhow::Result<()> {
|
||||
let addr = (Ipv4Addr::UNSPECIFIED, 3000);
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
|
||||
axum::serve(listener, router).await?;
|
||||
axum::serve(listener, router)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install Ctrl+C handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
() = ctrl_c => {},
|
||||
() = terminate => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn app(state: AppState) -> Router {
|
||||
use axum::{http::header, routing::get};
|
||||
use endpoints::{
|
||||
|
@@ -1,10 +1,7 @@
|
||||
pub(crate) use crate::{
|
||||
auth::Claims,
|
||||
db::{self, permissions::PermissionExt as _},
|
||||
errors::{
|
||||
ErrorHandlingExt as _, GeneralError, GeneralResult, ItemNotFoundExt as _,
|
||||
ValidationExt as _,
|
||||
},
|
||||
errors::{ErrorHandlingExt as _, GeneralError, GeneralResult, ItemNotFoundExt as _},
|
||||
AppState, Pool,
|
||||
};
|
||||
pub use axum::{
|
||||
|
Reference in New Issue
Block a user