diff --git a/Cargo.toml b/Cargo.toml index 939d7c7..75049b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ path = "src/bin/main.rs" name = "route96" [features] -default = ["nip96", "blossom", "analytics", "ranges"] +default = ["nip96", "blossom", "analytics", "ranges", "react-ui"] media-compression = ["dep:ffmpeg-rs-raw", "dep:libc"] labels = ["nip96", "dep:candle-core", "dep:candle-nn", "dep:candle-transformers"] nip96 = ["media-compression"] @@ -25,6 +25,7 @@ torrent-v2 = [] analytics = [] void-cat-redirects = ["dep:sqlx-postgres"] ranges = ["dep:http-range-header"] +react-ui = [] [dependencies] log = "0.4.21" diff --git a/src/db.rs b/src/db.rs index 7538027..097b759 100644 --- a/src/db.rs +++ b/src/db.rs @@ -51,6 +51,12 @@ impl FileLabel { } } +#[derive(Clone, FromRow, Serialize)] +pub struct UserStats { + pub file_count: u64, + pub total_size: u64, +} + #[derive(Clone)] pub struct Database { pub(crate) pool: sqlx::pool::Pool, @@ -88,6 +94,19 @@ impl Database { .await } + pub async fn get_user_stats(&self, id: u64) -> Result { + sqlx::query_as( + "select cast(count(user_uploads.file) as unsigned integer) as file_count, \ + cast(sum(uploads.size) as unsigned integer) as total_size \ + from user_uploads,uploads \ + where user_uploads.user_id = ? \ + and user_uploads.file = uploads.id", + ) + .bind(id) + .fetch_one(&self.pool) + .await + } + pub async fn get_user_id(&self, pubkey: &Vec) -> Result { sqlx::query("select id from users where pubkey = ?") .bind(pubkey) @@ -167,6 +186,14 @@ impl Database { Ok(()) } + pub async fn delete_all_file_owner(&self, file: &Vec) -> Result<(), Error> { + sqlx::query("delete from user_uploads where file = ?") + .bind(file) + .execute(&self.pool) + .await?; + Ok(()) + } + pub async fn delete_file(&self, file: &Vec) -> Result<(), Error> { sqlx::query("delete from uploads where id = ?") .bind(file) diff --git a/src/routes/admin.rs b/src/routes/admin.rs index d7619e3..9ba9332 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,5 +1,5 @@ use crate::auth::nip98::Nip98Auth; -use crate::db::{Database, FileUpload, User}; +use crate::db::{Database, FileUpload}; use crate::routes::{Nip94Event, PagedResult}; use crate::settings::Settings; use rocket::serde::json::Json; @@ -48,11 +48,30 @@ impl AdminResponse { } } +#[derive(Serialize)] +pub struct SelfUser { + pub is_admin: bool, + pub file_count: u64, + pub total_size: u64, +} + #[rocket::get("/self")] -async fn admin_get_self(auth: Nip98Auth, db: &State) -> AdminResponse { +async fn admin_get_self(auth: Nip98Auth, db: &State) -> AdminResponse { let pubkey_vec = auth.event.pubkey.to_bytes().to_vec(); match db.get_user(&pubkey_vec).await { - Ok(user) => AdminResponse::success(user), + Ok(user) => { + let s = match db.get_user_stats(user.id).await { + Ok(r) => r, + Err(e) => { + return AdminResponse::error(&format!("Failed to load user stats: {}", e)) + } + }; + AdminResponse::success(SelfUser { + is_admin: user.is_admin, + file_count: s.file_count, + total_size: s.total_size, + }) + } Err(_) => AdminResponse::error("User not found"), } } @@ -66,7 +85,7 @@ async fn admin_list_files( settings: &State, ) -> AdminResponse> { let pubkey_vec = auth.event.pubkey.to_bytes().to_vec(); - let server_count = count.min(5_000).max(1); + let server_count = count.clamp(1, 5_000); let user = match db.get_user(&pubkey_vec).await { Ok(user) => user, diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 680afa5..6819b8d 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -253,22 +253,34 @@ async fn delete_file( } if let Ok(Some(_info)) = db.get_file(&id).await { let pubkey_vec = auth.pubkey.to_bytes().to_vec(); + let auth_user = db.get_user(&pubkey_vec).await?; let owners = db.get_file_owners(&id).await?; - - let this_owner = match owners.iter().find(|o| o.pubkey.eq(&pubkey_vec)) { - Some(o) => o, - None => return Err(Error::msg("You dont own this file, you cannot delete it")), - }; - if let Err(e) = db.delete_file_owner(&id, this_owner.id).await { - return Err(Error::msg(format!("Failed to delete (db): {}", e))); - } - // only 1 owner was left, delete file completely - if owners.len() == 1 { + if auth_user.is_admin { + if let Err(e) = db.delete_all_file_owner(&id).await { + return Err(Error::msg(format!("Failed to delete (db): {}", e))); + } if let Err(e) = db.delete_file(&id).await { return Err(Error::msg(format!("Failed to delete (fs): {}", e))); } if let Err(e) = tokio::fs::remove_file(fs.get(&id)).await { - return Err(Error::msg(format!("Failed to delete (fs): {}", e))); + warn!("Failed to delete (fs): {}", e); + } + } else { + let this_owner = match owners.iter().find(|o| o.pubkey.eq(&pubkey_vec)) { + Some(o) => o, + None => return Err(Error::msg("You dont own this file, you cannot delete it")), + }; + if let Err(e) = db.delete_file_owner(&id, this_owner.id).await { + return Err(Error::msg(format!("Failed to delete (db): {}", e))); + } + // only 1 owner was left, delete file completely + if owners.len() == 1 { + if let Err(e) = db.delete_file(&id).await { + return Err(Error::msg(format!("Failed to delete (fs): {}", e))); + } + if let Err(e) = tokio::fs::remove_file(fs.get(&id)).await { + warn!("Failed to delete (fs): {}", e); + } } } Ok(()) @@ -279,10 +291,12 @@ async fn delete_file( #[rocket::get("/")] pub async fn root() -> Result { - #[cfg(debug_assertions)] - let index = "./index.html"; - #[cfg(not(debug_assertions))] + #[cfg(all(debug_assertions, feature = "react-ui"))] + let index = "./ui_src/dist/index.html"; + #[cfg(all(not(debug_assertions), feature = "react-ui"))] let index = "./ui/index.html"; + #[cfg(not(feature = "react-ui"))] + let index = "./index.html"; if let Ok(f) = NamedFile::open(index).await { Ok(f) } else { diff --git a/ui_src/package.json b/ui_src/package.json index bd4ea27..19ca088 100644 --- a/ui_src/package.json +++ b/ui_src/package.json @@ -14,6 +14,7 @@ "@snort/shared": "^1.0.17", "@snort/system": "^1.5.1", "@snort/system-react": "^1.5.1", + "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/ui_src/src/components/button.tsx b/ui_src/src/components/button.tsx index f7ad037..d67b47e 100644 --- a/ui_src/src/components/button.tsx +++ b/ui_src/src/components/button.tsx @@ -22,7 +22,7 @@ export default function Button({ } return ( - - - + + +
+ {!listedFiles && + } + + {self &&
+
Uploads: {self.file_count.toLocaleString()}
+
Total Size: {FormatBytes(self.total_size)}
+
} {listedFiles && ( setListedPage(x)} + onDelete={async (x) => { + await deleteFile(x); + await listUploads(listedPage); + }} /> )} {self?.is_admin && ( <> +

Admin File List:

{adminListedFiles && ( setAdminListedPage(x)} + onDelete={async (x) => { + await deleteFile(x); + await listAllUploads(adminListedPage); + } + } /> )} diff --git a/ui_src/tsconfig.app.tsbuildinfo b/ui_src/tsconfig.app.tsbuildinfo index 5928181..50e8f83 100644 --- a/ui_src/tsconfig.app.tsbuildinfo +++ b/ui_src/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/const.ts","./src/login.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/button.tsx","./src/components/profile.tsx","./src/hooks/login.ts","./src/hooks/publisher.ts","./src/upload/blossom.ts","./src/upload/index.ts","./src/upload/nip96.ts","./src/views/files.tsx","./src/views/header.tsx","./src/views/upload.tsx"],"version":"5.6.2"} \ No newline at end of file +{"root":["./src/App.tsx","./src/const.ts","./src/login.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/button.tsx","./src/components/profile.tsx","./src/hooks/login.ts","./src/hooks/publisher.ts","./src/upload/admin.ts","./src/upload/blossom.ts","./src/upload/index.ts","./src/upload/nip96.ts","./src/views/files.tsx","./src/views/header.tsx","./src/views/upload.tsx"],"version":"5.6.2"} \ No newline at end of file diff --git a/ui_src/yarn.lock b/ui_src/yarn.lock index 136946d..a4e2f18 100644 --- a/ui_src/yarn.lock +++ b/ui_src/yarn.lock @@ -1455,6 +1455,13 @@ __metadata: languageName: node linkType: hard +"classnames@npm:^2.5.1": + version: 2.5.1 + resolution: "classnames@npm:2.5.1" + checksum: 10c0/afff4f77e62cea2d79c39962980bf316bacb0d7c49e13a21adaadb9221e1c6b9d3cdb829d8bb1b23c406f4e740507f37e1dcf506f7e3b7113d17c5bab787aa69 + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -3721,6 +3728,7 @@ __metadata: "@types/react-dom": "npm:^18.3.0" "@vitejs/plugin-react": "npm:^4.3.1" autoprefixer: "npm:^10.4.20" + classnames: "npm:^2.5.1" eslint: "npm:^9.9.0" eslint-plugin-react-hooks: "npm:^5.1.0-rc.0" eslint-plugin-react-refresh: "npm:^0.4.9"