From af9887e651af855c114b60b9583901f7139ab5f1 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 20 Oct 2025 00:38:35 -0400 Subject: [PATCH] fix(admin): modal bugs; chore(api): update ReDoc SRI; docs(openapi): add annotations + spec --- CHANGELOG.md | 31 + README.md | 2 +- openapi.json.dist | 5098 +++++++++++-------- public/api.php | 2 +- public/api/addUser.php | 34 + public/api/admin/acl/getGrants.php | 23 + public/api/admin/acl/saveGrants.php | 22 + public/api/admin/getConfig.php | 24 + public/api/admin/readMetadata.php | 29 + public/api/admin/updateConfig.php | 39 + public/api/auth/auth.php | 46 + public/api/auth/checkAuth.php | 29 + public/api/auth/login_basic.php | 26 + public/api/auth/logout.php | 22 + public/api/auth/token.php | 23 + public/api/changePassword.php | 38 + public/api/file/copyFiles.php | 30 + public/api/file/createFile.php | 24 + public/api/file/createShareLink.php | 36 + public/api/file/deleteFiles.php | 28 + public/api/file/deleteShareLink.php | 21 + public/api/file/deleteTrashFiles.php | 30 + public/api/file/download.php | 28 + public/api/file/downloadZip.php | 35 + public/api/file/extractZip.php | 25 + public/api/file/getFileList.php | 17 + public/api/file/getFileTag.php | 11 + public/api/file/getShareLinks.php | 13 + public/api/file/getTrashItems.php | 14 + public/api/file/moveFiles.php | 14 + public/api/file/renameFile.php | 26 + public/api/file/restoreFiles.php | 22 + public/api/file/saveFile.php | 26 + public/api/file/saveFileTag.php | 28 + public/api/file/share.php | 26 + public/api/folder/capabilities.php | 52 + public/api/folder/createFolder.php | 30 + public/api/folder/createShareFolderLink.php | 36 + public/api/folder/deleteFolder.php | 24 + public/api/folder/deleteShareFolderLink.php | 24 + public/api/folder/downloadSharedFile.php | 24 + public/api/folder/getFolderList.php | 32 + public/api/folder/getShareFolderLinks.php | 15 + public/api/folder/renameFolder.php | 25 + public/api/folder/shareFolder.php | 20 + public/api/folder/uploadToSharedFolder.php | 27 + public/api/getUserPermissions.php | 19 + public/api/getUsers.php | 26 + public/api/profile/getCurrentUser.php | 25 + public/api/profile/uploadPicture.php | 51 + public/api/removeUser.php | 36 + public/api/totp_disable.php | 26 + public/api/totp_recover.php | 40 + public/api/totp_saveCode.php | 30 + public/api/totp_setup.php | 25 + public/api/totp_verify.php | 37 + public/api/updateUserPanel.php | 36 + public/api/updateUserPermissions.php | 46 + public/api/upload/removeChunks.php | 29 + public/api/upload/upload.php | 79 + public/css/styles.css | 5 +- public/js/adminPanel.js | 277 +- src/controllers/AdminController.php | 104 +- src/controllers/AuthController.php | 149 - src/controllers/UploadController.php | 102 +- src/controllers/UserController.php | 381 -- src/openapi/Components.php | 168 + 67 files changed, 5072 insertions(+), 2870 deletions(-) create mode 100644 src/openapi/Components.php diff --git a/CHANGELOG.md b/CHANGELOG.md index fb8fbd2..0312adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## Changes 10/19/2025 (v1.5.2) + +fix(admin): modal bugs; chore(api): update ReDoc SRI; docs(openapi): add annotations + spec + +- adminPanel.js + - Fix modal open/close reliability and stacking order + - Prevent background scroll while modal is open + - Tidy focus/keyboard handling for better UX + +- style.css + - Polish styles for Folder Access + Users views (spacing, tables, badges) + - Improve responsiveness and visual consistency + +- api.php + - Update Redoc SRI hash and pin to the current bundle URL + +- OpenAPI + - Add/refresh inline @OA annotations across endpoints + - Introduce src/openapi/Components.php with base Info/Server, + common responses, and shared components + - Regenerate and commit openapi.json.dist + +- public/js/adminPanel.js +- public/css/style.css +- public/api.php +- src/openapi/Components.php +- openapi.json.dist +- public/api/** (annotated endpoints) + +--- + ## Changes 10/19/2025 (v1.5.1) fix(config/ui): serve safe public config to non-admins; init early; gate trash UI to admins; dynamic title; demo toast (closes #56) diff --git a/README.md b/README.md index ee864eb..16542d9 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Upload, organize, and share files or folders through a sleek web interface. **Fi [![Demo](https://img.shields.io/badge/demo-live-brightgreen)](https://demo.filerise.net) **Demo credentials:** `demo` / `demo` -Curious about the UI? **Check out the live demo:** (login with username “demo” and password “demo”). *The demo is read-only for security*. Explore the interface, switch themes, preview files, and see FileRise in action! +Curious about the UI? **Check out the live demo:** (login with username “demo” and password “demo”). **The demo is read-only for security.** Explore the interface, switch themes, preview files, and see FileRise in action! --- diff --git a/openapi.json.dist b/openapi.json.dist index 840f6c7..80d8443 100644 --- a/openapi.json.dist +++ b/openapi.json.dist @@ -2,153 +2,44 @@ "openapi": "3.0.0", "info": { "title": "FileRise API", - "description": "A lightweight self-hosted file manager API", - "version": "1.0.0" + "version": "1.5.2" }, + "servers": [ + { + "url": "/", + "description": "Same-origin server" + } + ], "paths": { - "/api/admin": {}, - "/api/auth": {}, - "/api/file": {}, - "/api/folder": {}, - "/api/upload": {}, - "/api": {}, - "/api/admin/getConfig.php": { - "get": { + "/api/addUser.php": { + "post": { "tags": [ - "Admin" + "Users" ], - "summary": "Retrieve admin configuration", - "description": "Returns the admin configuration settings, decrypting the configuration file and providing default values if not set.", - "operationId": "getAdminConfig", - "responses": { - "200": { - "description": "Configuration retrieved successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "header_title": { - "type": "string", - "example": "FileRise" - }, - "oidc": { - "properties": { - "providerUrl": { - "type": "string", - "example": "https://your-oidc-provider.com" - }, - "clientId": { - "type": "string", - "example": "YOUR_CLIENT_ID" - }, - "clientSecret": { - "type": "string", - "example": "YOUR_CLIENT_SECRET" - }, - "redirectUri": { - "type": "string", - "example": "https://yourdomain.com/auth.php?oidc=callback" - } - }, - "type": "object" - }, - "loginOptions": { - "properties": { - "disableFormLogin": { - "type": "boolean", - "example": false - }, - "disableBasicAuth": { - "type": "boolean", - "example": false - }, - "disableOIDCLogin": { - "type": "boolean", - "example": false - } - }, - "type": "object" - }, - "globalOtpauthUrl": { - "type": "string", - "example": "" - } - }, - "type": "object" - } - } - } - }, - "500": { - "description": "Failed to decrypt configuration or server error" - } - } - } - }, - "/api/admin/updateConfig.php": { - "put": { - "tags": [ - "Admin" - ], - "summary": "Update admin configuration", - "description": "Updates the admin configuration settings. Requires admin privileges and a valid CSRF token.", - "operationId": "updateAdminConfig", + "summary": "Add a new user", + "description": "Adds a new user to the system. In setup mode, the new user is automatically made admin.", + "operationId": "addUser", "requestBody": { "required": true, "content": { "application/json": { "schema": { "required": [ - "header_title", - "oidc", - "loginOptions" + "username", + "password" ], "properties": { - "header_title": { + "username": { "type": "string", - "example": "FileRise" + "example": "johndoe" }, - "oidc": { - "properties": { - "providerUrl": { - "type": "string", - "example": "https://your-oidc-provider.com" - }, - "clientId": { - "type": "string", - "example": "YOUR_CLIENT_ID" - }, - "clientSecret": { - "type": "string", - "example": "YOUR_CLIENT_SECRET" - }, - "redirectUri": { - "type": "string", - "example": "https://yourdomain.com/api/auth/auth.php?oidc=callback" - } - }, - "type": "object" - }, - "loginOptions": { - "properties": { - "disableFormLogin": { - "type": "boolean", - "example": false - }, - "disableBasicAuth": { - "type": "boolean", - "example": false - }, - "disableOIDCLogin": { - "type": "boolean", - "example": false - } - }, - "type": "object" - }, - "globalOtpauthUrl": { + "password": { "type": "string", - "example": "" + "example": "securepassword" + }, + "isAdmin": { + "type": "boolean", + "example": true } }, "type": "object" @@ -158,14 +49,14 @@ }, "responses": { "200": { - "description": "Configuration updated successfully", + "description": "User added successfully", "content": { "application/json": { "schema": { "properties": { "success": { "type": "string", - "example": "Configuration updated successfully." + "example": "User added successfully" } }, "type": "object" @@ -174,17 +65,561 @@ } }, "400": { - "description": "Bad Request (e.g., invalid input, incomplete OIDC configuration)" + "description": "Bad Request" }, - "403": { - "description": "Unauthorized (user not admin or invalid CSRF token)" - }, - "500": { - "description": "Server error (failed to write configuration file)" + "401": { + "description": "Unauthorized" } } } }, + "/api/admin/acl/getGrants.php": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get per-folder grants for a user (admin-only)", + "description": "Returns the explicit ACL grants for the specified user across all known folders. Booleans mean: `view` = full view, `viewOwn` = view own uploads only, `upload` = write (upload/edit/delete), `manage` = owner, `share` = can create share links.", + "operationId": "adminGetUserFolderGrants", + "parameters": [ + { + "name": "user", + "in": "query", + "description": "Username to inspect.", + "required": true, + "schema": { + "type": "string", + "minLength": 1 + } + } + ], + "responses": { + "200": { + "description": "Map of folder → grants for the requested user.", + "content": { + "application/json": { + "schema": { + "required": [ + "grants" + ], + "properties": { + "grants": { + "type": "object", + "example": { + "root": { + "view": true, + "viewOwn": false, + "upload": true, + "manage": false, + "share": true + }, + "projects/acme": { + "view": true, + "viewOwn": false, + "upload": true, + "manage": true, + "share": true + }, + "teams/ops": { + "view": false, + "viewOwn": true, + "upload": true, + "manage": false, + "share": false + } + }, + "additionalProperties": { + "required": [ + "view", + "viewOwn", + "upload", + "manage", + "share" + ], + "properties": { + "view": { + "description": "Full view of all files in the folder.", + "type": "boolean", + "example": true + }, + "viewOwn": { + "description": "View only files uploaded by the user.", + "type": "boolean", + "example": false + }, + "upload": { + "description": "Write (upload/edit/rename/move/delete).", + "type": "boolean", + "example": true + }, + "manage": { + "description": "Is folder owner (implicit full rights).", + "type": "boolean", + "example": false + }, + "share": { + "description": "Can create share links.", + "type": "boolean", + "example": true + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "Invalid user (missing or fails validation)." + }, + "401": { + "description": "Unauthorized (not logged in or not admin)." + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/admin/acl/saveGrants.php": { + "post": { + "tags": [ + "Admin" + ], + "summary": "Save per-folder grants for one or many users (admin-only)", + "description": "Accepts either a single-user payload `{user, grants}` or a batch payload `{changes:[{user, grants},...]}`. Booleans map to: `view`=full view, `viewOwn`=view only own uploads, `upload`=write (upload/edit/rename/move/delete), `manage`=owner, `share`=can create share links.", + "operationId": "adminSaveUserFolderGrants", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "description": "CSRF token bound to the current session.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "required": [ + "user", + "grants" + ], + "properties": { + "user": { + "type": "string", + "example": "alice" + }, + "grants": { + "description": "Map of folder → flags.", + "type": "object", + "example": { + "root": { + "view": true, + "viewOwn": false, + "upload": true, + "manage": false, + "share": true + }, + "projects/acme": { + "view": true, + "viewOwn": false, + "upload": true, + "manage": true, + "share": true + } + }, + "additionalProperties": { + "required": [ + "view", + "viewOwn", + "upload", + "manage", + "share" + ], + "properties": { + "view": { + "type": "boolean" + }, + "viewOwn": { + "type": "boolean" + }, + "upload": { + "type": "boolean" + }, + "manage": { + "type": "boolean" + }, + "share": { + "type": "boolean" + } + }, + "type": "object" + } + } + }, + "type": "object" + }, + { + "required": [ + "changes" + ], + "properties": { + "changes": { + "type": "array", + "items": { + "required": [ + "user", + "grants" + ], + "properties": { + "user": { + "type": "string", + "example": "bob" + }, + "grants": { + "type": "object", + "example": { + "teams/ops": { + "view": false, + "viewOwn": true, + "upload": true, + "manage": false, + "share": false + } + }, + "additionalProperties": { + "required": [ + "view", + "viewOwn", + "upload", + "manage", + "share" + ], + "properties": { + "view": { + "type": "boolean" + }, + "viewOwn": { + "type": "boolean" + }, + "upload": { + "type": "boolean" + }, + "manage": { + "type": "boolean" + }, + "share": { + "type": "boolean" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + }, + "type": "object", + "example": { + "changes": [ + { + "user": "alice", + "grants": { + "root": { + "view": true, + "viewOwn": false, + "upload": true, + "manage": false, + "share": true + } + } + }, + { + "user": "bob", + "grants": { + "teams/ops": { + "view": false, + "viewOwn": true, + "upload": true, + "manage": false, + "share": false + } + } + } + ] + } + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Grants saved.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "description": "Single-user result (whatever ACL::applyUserGrantsAtomic returns).", + "type": "object", + "example": { + "updated": { + "root": { + "view": true, + "viewOwn": false, + "upload": true, + "manage": false, + "share": true + } + } + }, + "additionalProperties": true + }, + { + "properties": { + "ok": { + "type": "boolean", + "example": true + }, + "updated": { + "description": "Per-user results from ACL::applyUserGrantsAtomic.", + "type": "object", + "additionalProperties": { + "type": "object" + } + } + }, + "type": "object", + "example": { + "ok": true, + "updated": { + "alice": { + "root": { + "view": true, + "viewOwn": false, + "upload": true, + "manage": false, + "share": true + } + }, + "bob": { + "teams/ops": { + "view": false, + "viewOwn": true, + "upload": true, + "manage": false, + "share": false + } + } + } + } + } + ] + } + } + } + }, + "400": { + "description": "Invalid JSON / invalid user / invalid payload shape." + }, + "401": { + "description": "Unauthorized (not logged in or not admin)." + }, + "403": { + "description": "Invalid CSRF token." + }, + "500": { + "description": "Failed to save grants." + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/admin/getConfig.php": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get UI configuration", + "description": "Returns a public subset for everyone; authenticated admins receive additional loginOptions fields.", + "operationId": "getAdminConfig", + "responses": { + "200": { + "description": "Configuration loaded", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AdminGetConfigPublic" + }, + { + "$ref": "#/components/schemas/AdminGetConfigAdmin" + } + ] + } + } + } + }, + "500": { + "description": "Server error" + } + } + } + }, + "/api/admin/readMetadata.php": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Read share metadata (admin-only)", + "description": "Returns the requested metadata JSON (either `share_links.json` or `share_folder_links.json`). Expired entries are pruned before returning. If the file does not exist, this returns `{}` with 200.", + "operationId": "adminReadMetadata", + "parameters": [ + { + "name": "file", + "in": "query", + "description": "Which metadata file to read.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "share_links.json", + "share_folder_links.json" + ] + } + } + ], + "responses": { + "200": { + "description": "Cleaned metadata map keyed by token.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "description": "File share links (token → record).", + "type": "object", + "additionalProperties": { + "required": [ + "folder", + "file", + "expires" + ], + "properties": { + "folder": { + "type": "string", + "example": "team" + }, + "file": { + "type": "string", + "example": "report.pdf" + }, + "expires": { + "description": "Unix timestamp (seconds).", + "type": "integer", + "format": "int64", + "example": 1704067200 + }, + "password": { + "description": "Hashed password if set.", + "type": "string", + "example": "$2y$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "nullable": true + }, + "createdBy": { + "type": "string", + "example": "alice", + "nullable": true + } + }, + "type": "object" + } + }, + { + "description": "Folder share links (token → record).", + "type": "object", + "additionalProperties": { + "required": [ + "folder", + "expires" + ], + "properties": { + "folder": { + "type": "string", + "example": "team/sub" + }, + "expires": { + "description": "Unix timestamp (seconds).", + "type": "integer", + "format": "int64", + "example": 1704067200 + }, + "password": { + "description": "Hashed password if set.", + "type": "string", + "example": "$2y$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "nullable": true + }, + "allowUpload": { + "description": "Whether uploads are allowed on the shared folder.", + "type": "integer", + "enum": [ + 0, + 1 + ], + "example": 1 + }, + "createdBy": { + "type": "string", + "example": "alice", + "nullable": true + } + }, + "type": "object" + } + } + ] + } + } + } + }, + "400": { + "description": "Missing `file` parameter." + }, + "403": { + "description": "Forbidden (not admin or invalid filename)." + }, + "500": { + "description": "Corrupted JSON on disk." + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, "/api/auth/auth.php": { "post": { "tags": [ @@ -310,38 +745,6 @@ } } }, - "/api/auth/token.php": { - "get": { - "tags": [ - "Auth" - ], - "summary": "Retrieve CSRF token and share URL", - "description": "Returns the current CSRF token along with the configured share URL.", - "operationId": "getToken", - "responses": { - "200": { - "description": "CSRF token and share URL", - "content": { - "application/json": { - "schema": { - "properties": { - "csrf_token": { - "type": "string", - "example": "0123456789abcdef..." - }, - "share_url": { - "type": "string", - "example": "https://yourdomain.com/share.php" - } - }, - "type": "object" - } - } - } - } - } - } - }, "/api/auth/login_basic.php": { "get": { "tags": [ @@ -391,1832 +794,34 @@ } } }, - "/api/file/copyFiles.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Copy files between folders", - "description": "Copies files from a source folder to a destination folder. It validates folder names, handles file renaming if a conflict exists, and updates metadata accordingly.", - "operationId": "copyFiles", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "source", - "destination", - "files" - ], - "properties": { - "source": { - "type": "string", - "example": "root" - }, - "destination": { - "type": "string", - "example": "Documents" - }, - "files": { - "type": "array", - "items": { - "type": "string", - "example": "example.pdf" - } - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Files copied successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "Files copied successfully" - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid request or input" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or read-only permission" - } - } - } - }, - "/api/file/deleteFiles.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Delete files (move to trash)", - "description": "Moves the specified files from the given folder to the trash and updates metadata accordingly.", - "operationId": "deleteFiles", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "files" - ], - "properties": { - "folder": { - "type": "string", - "example": "Documents" - }, - "files": { - "type": "array", - "items": { - "type": "string", - "example": "example.pdf" - } - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Files moved to Trash successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "Files moved to Trash: file1.pdf, file2.doc" - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid request" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or permission denied" - } - } - } - }, - "/api/file/moveFiles.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Move files between folders", - "description": "Moves files from a source folder to a destination folder, updating metadata accordingly.", - "operationId": "moveFiles", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "source", - "destination", - "files" - ], - "properties": { - "source": { - "type": "string", - "example": "root" - }, - "destination": { - "type": "string", - "example": "Archives" - }, - "files": { - "type": "array", - "items": { - "type": "string", - "example": "report.pdf" - } - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Files moved successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "Files moved successfully" - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid request or input" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or permission denied" - } - } - } - }, - "/api/file/renameFile.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Rename a file", - "description": "Renames a file within a specified folder and updates folder metadata. If a file with the new name exists, a unique name is generated.", - "operationId": "renameFile", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "folder", - "oldName", - "newName" - ], - "properties": { - "folder": { - "type": "string", - "example": "Documents" - }, - "oldName": { - "type": "string", - "example": "oldfile.pdf" - }, - "newName": { - "type": "string", - "example": "newfile.pdf" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "File renamed successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "File renamed successfully" - }, - "newName": { - "type": "string", - "example": "newfile.pdf" - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid input" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or permission denied" - } - } - } - }, - "/api/file/saveFile.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Save a file", - "description": "Saves file content to disk in a specified folder and updates metadata accordingly.", - "operationId": "saveFile", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "fileName", - "content" - ], - "properties": { - "fileName": { - "type": "string", - "example": "document.txt" - }, - "content": { - "type": "string", - "example": "File content here" - }, - "folder": { - "type": "string", - "example": "Documents" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "File saved successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "File saved successfully" - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid request data" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or read-only permission" - } - } - } - }, - "/api/file/download.php": { + "/api/auth/token.php": { "get": { "tags": [ - "Files" - ], - "summary": "Download a file", - "description": "Downloads a file from a specified folder. The file is served inline for images or as an attachment for other types.", - "operationId": "downloadFile", - "parameters": [ - { - "name": "file", - "in": "query", - "description": "The name of the file to download", - "required": true, - "schema": { - "type": "string", - "example": "example.pdf" - } - }, - { - "name": "folder", - "in": "query", - "description": "The folder in which the file is located. Defaults to root.", - "required": false, - "schema": { - "type": "string", - "example": "Documents" - } - } + "Auth" ], + "summary": "Retrieve CSRF token and share URL", + "description": "Returns the current CSRF token along with the configured share URL.", + "operationId": "getToken", "responses": { "200": { - "description": "File downloaded successfully" - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Access forbidden" - }, - "404": { - "description": "File not found" - }, - "500": { - "description": "Server error" - } - } - } - }, - "/api/file/downloadZip.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Download a ZIP archive of selected files", - "description": "Creates a ZIP archive of the specified files in a folder and serves it for download.", - "operationId": "downloadZip", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "folder", - "files" - ], - "properties": { - "folder": { - "type": "string", - "example": "Documents" - }, - "files": { - "type": "array", - "items": { - "type": "string", - "example": "example.pdf" - } - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "ZIP archive created and served", - "content": { - "application/zip": {} - } - }, - "400": { - "description": "Bad request or invalid input" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token" - }, - "500": { - "description": "Server error" - } - } - } - }, - "/api/file/extractZip.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Extract ZIP files", - "description": "Extracts ZIP archives from a specified folder and updates metadata. Returns a list of extracted files.", - "operationId": "extractZip", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "folder", - "files" - ], - "properties": { - "folder": { - "type": "string", - "example": "Documents" - }, - "files": { - "type": "array", - "items": { - "type": "string", - "example": "archive.zip" - } - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "ZIP files extracted successfully", + "description": "CSRF token and share URL", "content": { "application/json": { "schema": { "properties": { - "success": { - "type": "boolean", - "example": true + "csrf_token": { + "type": "string", + "example": "0123456789abcdef..." }, - "extractedFiles": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid input" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token" - } - } - } - }, - "/api/file/share.php": { - "get": { - "tags": [ - "Files" - ], - "summary": "Access a shared file", - "description": "Serves a shared file based on a share token. If the file is password protected and no password is provided, a password entry form is returned.", - "operationId": "shareFile", - "parameters": [ - { - "name": "token", - "in": "query", - "description": "The share token", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "pass", - "in": "query", - "description": "The password for the share if required", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "File served or password form rendered", - "content": { - "application/octet-stream": {} - } - }, - "400": { - "description": "Missing token or invalid request" - }, - "403": { - "description": "Link expired, invalid password, or forbidden access" - }, - "404": { - "description": "Share link or file not found" - } - } - } - }, - "/api/file/createShareLink.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Create a share link for a file", - "description": "Generates a secure share link token for a specific file with an optional password protection and expiration time.", - "operationId": "createShareLink", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "folder", - "file" - ], - "properties": { - "folder": { - "type": "string", - "example": "Documents" - }, - "file": { - "type": "string", - "example": "report.pdf" - }, - "expirationMinutes": { - "type": "integer", - "example": 60 - }, - "password": { - "type": "string", - "example": "secret" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Share link created successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "token": { + "share_url": { "type": "string", - "example": "a1b2c3d4e5f6..." - }, - "expires": { - "type": "integer", - "example": 1621234567 + "example": "https://yourdomain.com/share.php" } }, "type": "object" } } } - }, - "400": { - "description": "Invalid request data" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Read-only users are not allowed to create share links" - } - } - } - }, - "/api/file/getTrashItems.php": { - "get": { - "tags": [ - "Files" - ], - "summary": "Get trash items", - "description": "Retrieves a list of files that have been moved to Trash, enriched with metadata such as who deleted them and when.", - "operationId": "getTrashItems", - "responses": { - "200": { - "description": "Trash items retrieved successfully", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object" - } - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/file/restoreFiles.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Restore trashed files", - "description": "Restores files from Trash based on provided trash file identifiers and updates metadata.", - "operationId": "restoreFiles", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "files" - ], - "properties": { - "files": { - "type": "array", - "items": { - "type": "string", - "example": "trashedFile_1623456789.zip" - } - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Files restored successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "Items restored: file1, file2" - }, - "restored": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid request" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token" - } - } - } - }, - "/api/file/deleteTrashFiles.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Delete trash files", - "description": "Deletes trash items based on provided trash file identifiers from the trash metadata and removes the files from disk.", - "operationId": "deleteTrashFiles", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "required": [ - "deleteAll" - ], - "properties": { - "deleteAll": { - "type": "boolean", - "example": true - } - }, - "type": "object" - }, - { - "required": [ - "files" - ], - "properties": { - "files": { - "type": "array", - "items": { - "type": "string", - "example": "trashedfile_1234567890" - } - } - }, - "type": "object" - } - ] - } - } - } - }, - "responses": { - "200": { - "description": "Trash items deleted successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "deleted": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid input" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token" - } - } - } - }, - "/api/file/getFileTag.php": { - "get": { - "tags": [ - "Files" - ], - "summary": "Retrieve file tags", - "description": "Retrieves tags from the createdTags.json metadata file.", - "operationId": "getFileTags", - "responses": { - "200": { - "description": "File tags retrieved successfully", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object" - } - } - } - } - } - } - } - }, - "/api/file/saveFileTag.php": { - "post": { - "tags": [ - "Files" - ], - "summary": "Save file tags", - "description": "Saves tag data for a specified file and updates global tag data. For folder-specific tags, saves to the folder's metadata file.", - "operationId": "saveFileTag", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "file", - "tags" - ], - "properties": { - "file": { - "type": "string", - "example": "document.txt" - }, - "folder": { - "type": "string", - "example": "Documents" - }, - "tags": { - "type": "array", - "items": { - "properties": { - "name": { - "type": "string", - "example": "Important" - }, - "color": { - "type": "string", - "example": "#FF0000" - } - }, - "type": "object" - } - }, - "deleteGlobal": { - "type": "boolean", - "example": false - }, - "tagToDelete": { - "type": "string", - "example": "OldTag" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Tag data saved successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "Tag data saved successfully." - }, - "globalTags": { - "type": "array", - "items": { - "type": "object" - } - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid request data" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or insufficient permissions" - } - } - } - }, - "/api/file/getFileList.php": { - "get": { - "tags": [ - "Files" - ], - "summary": "Get file list", - "description": "Retrieves a list of files from a specified folder along with global tags and metadata.", - "operationId": "getFileList", - "parameters": [ - { - "name": "folder", - "in": "query", - "description": "Folder name (defaults to 'root')", - "required": false, - "schema": { - "type": "string", - "example": "Documents" - } - } - ], - "responses": { - "200": { - "description": "File list retrieved successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "files": { - "type": "array", - "items": { - "type": "object" - } - }, - "globalTags": { - "type": "array", - "items": { - "type": "object" - } - } - }, - "type": "object" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/folder/createFolder.php": { - "post": { - "tags": [ - "Folders" - ], - "summary": "Create a new folder", - "description": "Creates a new folder in the upload directory (under an optional parent) and creates an associated empty metadata file.", - "operationId": "createFolder", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "folderName" - ], - "properties": { - "folderName": { - "type": "string", - "example": "NewFolder" - }, - "parent": { - "type": "string", - "example": "Documents" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Folder created successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "boolean", - "example": true - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Bad Request (e.g., invalid folder name)" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or permission denied" - } - } - } - }, - "/api/folder/deleteFolder.php": { - "post": { - "tags": [ - "Folders" - ], - "summary": "Delete an empty folder", - "description": "Deletes a specified folder if it is empty and not the root folder, and also removes its metadata file.", - "operationId": "deleteFolder", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "folder" - ], - "properties": { - "folder": { - "type": "string", - "example": "Documents/Subfolder" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Folder deleted successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "boolean", - "example": true - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Bad Request (e.g., invalid folder name or folder not empty)" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or permission denied" - } - } - } - }, - "/api/folder/renameFolder.php": { - "post": { - "tags": [ - "Folders" - ], - "summary": "Rename a folder", - "description": "Renames an existing folder and updates its associated metadata files.", - "operationId": "renameFolder", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "oldFolder", - "newFolder" - ], - "properties": { - "oldFolder": { - "type": "string", - "example": "Documents/OldFolder" - }, - "newFolder": { - "type": "string", - "example": "Documents/NewFolder" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Folder renamed successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "boolean", - "example": true - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid folder names or folder does not exist" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token or permission denied" - } - } - } - }, - "/api/folder/getFolderList.php": { - "get": { - "tags": [ - "Folders" - ], - "summary": "Get list of folders", - "description": "Retrieves the list of folders in the upload directory, including file counts and metadata file names for each folder.", - "operationId": "getFolderList", - "parameters": [ - { - "name": "folder", - "in": "query", - "description": "Optional folder name to filter the listing", - "required": false, - "schema": { - "type": "string", - "example": "Documents" - } - } - ], - "responses": { - "200": { - "description": "Folder list retrieved successfully", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object" - } - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "400": { - "description": "Bad request" - } - } - } - }, - "/api/folder/shareFolder.php": { - "get": { - "tags": [ - "Folders" - ], - "summary": "Display a shared folder", - "description": "Renders an HTML view of a shared folder's contents. Supports password protection, file listing with pagination, and an upload container if uploads are allowed.", - "operationId": "shareFolder", - "parameters": [ - { - "name": "token", - "in": "query", - "description": "The share token for the folder", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "pass", - "in": "query", - "description": "The password if the folder is protected", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "page", - "in": "query", - "description": "Page number for pagination", - "required": false, - "schema": { - "type": "integer", - "example": 1 - } - } - ], - "responses": { - "200": { - "description": "Shared folder displayed", - "content": { - "text/html": {} - } - }, - "400": { - "description": "Invalid request" - }, - "403": { - "description": "Access forbidden (expired link or invalid password)" - }, - "404": { - "description": "Share folder not found" - } - } - } - }, - "/api/folder/createShareFolderLink.php": { - "post": { - "tags": [ - "Folders" - ], - "summary": "Create a share link for a folder", - "description": "Generates a secure share link for a folder along with optional password protection and upload settings.", - "operationId": "createShareFolderLink", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "folder" - ], - "properties": { - "folder": { - "type": "string", - "example": "Documents" - }, - "expirationMinutes": { - "type": "integer", - "example": 60 - }, - "password": { - "type": "string", - "example": "secret" - }, - "allowUpload": { - "type": "integer", - "example": 1 - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Share link created successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "token": { - "type": "string", - "example": "a1b2c3d4..." - }, - "expires": { - "type": "integer", - "example": 1623456789 - }, - "link": { - "type": "string", - "example": "https://yourdomain.com/api/folder/shareFolder.php?token=..." - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid input" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Read-only users are not allowed to create share links" - } - } - } - }, - "/api/folder/downloadSharedFile.php": { - "get": { - "tags": [ - "Folders" - ], - "summary": "Download a file from a shared folder", - "description": "Retrieves and serves a file from a shared folder based on a share token.", - "operationId": "downloadSharedFile", - "parameters": [ - { - "name": "token", - "in": "query", - "description": "The share folder token", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "file", - "in": "query", - "description": "The filename to download", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "File served successfully", - "content": { - "application/octet-stream": {} - } - }, - "400": { - "description": "Bad Request (missing parameters, invalid file name, etc.)" - }, - "403": { - "description": "Access forbidden (e.g., expired share link)" - }, - "404": { - "description": "File not found" - } - } - } - }, - "/api/folder/uploadToSharedFolder.php": { - "post": { - "tags": [ - "Folders" - ], - "summary": "Upload a file to a shared folder", - "description": "Handles file upload to a shared folder using a share token. Validates file size, extension, and uploads the file to the shared folder, updating metadata accordingly.", - "operationId": "uploadToSharedFolder", - "requestBody": { - "description": "Multipart form data containing the share token and file to upload.", - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "token", - "fileToUpload" - ], - "properties": { - "token": { - "type": "string" - }, - "fileToUpload": { - "type": "string", - "format": "binary" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "302": { - "description": "Redirects to the shared folder page on success." - }, - "400": { - "description": "Bad Request (missing token, file upload error, file type/size not allowed)" - }, - "403": { - "description": "Forbidden (share link expired or uploads not allowed)" - }, - "500": { - "description": "Server error during file move" - } - } - } - }, - "/api/upload/upload.php": { - "post": { - "tags": [ - "Uploads" - ], - "summary": "Handle file upload", - "description": "Handles file uploads for both chunked and non-chunked (full) uploads. Validates CSRF, user authentication, and permissions, and processes file uploads accordingly. On success, returns a JSON status for chunked uploads or redirects for full uploads.", - "operationId": "handleUpload", - "requestBody": { - "description": "Multipart form data for file upload. For chunked uploads, include fields like 'resumableChunkNumber', 'resumableTotalChunks', 'resumableIdentifier', 'resumableFilename', etc.", - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "token", - "fileToUpload" - ], - "properties": { - "token": { - "description": "Share token or upload token.", - "type": "string" - }, - "fileToUpload": { - "description": "The file to upload.", - "type": "string", - "format": "binary" - }, - "resumableChunkNumber": { - "description": "Chunk number for chunked uploads.", - "type": "integer" - }, - "resumableTotalChunks": { - "description": "Total number of chunks.", - "type": "integer" - }, - "resumableFilename": { - "description": "Original filename.", - "type": "string" - }, - "folder": { - "description": "Target folder (default 'root').", - "type": "string" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "File uploaded successfully (or chunk uploaded status).", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "File uploaded successfully" - }, - "newFilename": { - "type": "string", - "example": "5f2d7c123a_example.png" - }, - "status": { - "type": "string", - "example": "chunk uploaded" - } - }, - "type": "object" - } - } - } - }, - "302": { - "description": "Redirection on full upload success." - }, - "400": { - "description": "Bad Request (e.g., missing file, invalid parameters)" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden (e.g., invalid CSRF token, upload disabled)" - }, - "500": { - "description": "Server error during file processing" - } - } - } - }, - "/api/upload/removeChunks.php": { - "post": { - "tags": [ - "Uploads" - ], - "summary": "Remove chunked upload temporary directory", - "description": "Removes the temporary directory used for chunked uploads, given a folder name matching the expected resumable pattern.", - "operationId": "removeChunks", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "folder" - ], - "properties": { - "folder": { - "type": "string", - "example": "resumable_myupload123" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Temporary folder removed successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Temporary folder removed." - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Invalid input (e.g., missing folder or invalid folder name)" - }, - "403": { - "description": "Invalid CSRF token" - } - } - } - }, - "/api/getUsers.php": { - "get": { - "tags": [ - "Users" - ], - "summary": "Retrieve a list of users", - "description": "Returns a JSON array of users. Only available to authenticated admin users.", - "operationId": "getUsers", - "responses": { - "200": { - "description": "Successful response with an array of users", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "properties": { - "username": { - "type": "string", - "example": "johndoe" - }, - "role": { - "type": "string", - "example": "admin" - } - }, - "type": "object" - } - } - } - } - }, - "401": { - "description": "Unauthorized: the user is not authenticated or is not an admin" - } - } - } - }, - "/api/addUser.php": { - "post": { - "tags": [ - "Users" - ], - "summary": "Add a new user", - "description": "Adds a new user to the system. In setup mode, the new user is automatically made admin.", - "operationId": "addUser", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "username", - "password" - ], - "properties": { - "username": { - "type": "string", - "example": "johndoe" - }, - "password": { - "type": "string", - "example": "securepassword" - }, - "isAdmin": { - "type": "boolean", - "example": true - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "User added successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "User added successfully" - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/removeUser.php": { - "delete": { - "tags": [ - "Users" - ], - "summary": "Remove a user", - "description": "Removes the specified user from the system. Cannot remove the currently logged-in user.", - "operationId": "removeUser", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "username" - ], - "properties": { - "username": { - "type": "string", - "example": "johndoe" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "User removed successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "User removed successfully" - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token" - } - } - } - }, - "/api/getUserPermissions.php": { - "get": { - "tags": [ - "Users" - ], - "summary": "Retrieve user permissions", - "description": "Returns the permissions for the current user, or all permissions if the user is an admin.", - "operationId": "getUserPermissions", - "responses": { - "200": { - "description": "Successful response with user permissions", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/updateUserPermissions.php": { - "put": { - "tags": [ - "Users" - ], - "summary": "Update user permissions", - "description": "Updates permissions for users. Only available to authenticated admin users.", - "operationId": "updateUserPermissions", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "permissions" - ], - "properties": { - "permissions": { - "type": "array", - "items": { - "properties": { - "username": { - "type": "string", - "example": "johndoe" - }, - "folderOnly": { - "type": "boolean", - "example": true - }, - "readOnly": { - "type": "boolean", - "example": false - }, - "disableUpload": { - "type": "boolean", - "example": false - } - }, - "type": "object" - } - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "User permissions updated successfully", - "content": { - "application/json": { - "schema": { - "properties": { - "success": { - "type": "string", - "example": "User permissions updated successfully." - } - }, - "type": "object" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Invalid CSRF token" - }, - "400": { - "description": "Bad Request" } } } @@ -2287,26 +892,53 @@ } } }, - "/api/updateUserPanel.php": { - "put": { + "/api/file/copyFiles.php": { + "post": { "tags": [ - "Users" + "Files" + ], + "summary": "Copy files between folders", + "description": "Requires read access on source and write access on destination. Enforces folder scope and ownership.", + "operationId": "copyFiles", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "description": "CSRF token from the current session", + "required": true, + "schema": { + "type": "string" + } + } ], - "summary": "Update user panel settings", - "description": "Updates user panel settings by disabling TOTP when not enabled. Accessible to authenticated users.", - "operationId": "updateUserPanel", "requestBody": { "required": true, "content": { "application/json": { "schema": { "required": [ - "totp_enabled" + "source", + "destination", + "files" ], "properties": { - "totp_enabled": { - "type": "boolean", - "example": false + "source": { + "type": "string", + "example": "root" + }, + "destination": { + "type": "string", + "example": "userA/projects" + }, + "files": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "report.pdf", + "notes.txt" + ] } }, "type": "object" @@ -2316,14 +948,152 @@ }, "responses": { "200": { - "description": "User panel updated successfully", + "description": "Copy result (model-defined)" + }, + "400": { + "description": "Invalid request or folder name" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/createFile.php": { + "post": { + "tags": [ + "Files" + ], + "summary": "Create an empty file", + "description": "Requires write access on the target folder. Enforces folder-only scope.", + "operationId": "createFile", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder", + "name" + ], + "properties": { + "folder": { + "type": "string", + "example": "root" + }, + "name": { + "type": "string", + "example": "new.txt" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Creation result (model-defined)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/createShareLink.php": { + "post": { + "tags": [ + "Shares" + ], + "summary": "Create a share link for a file", + "description": "Requires share permission on the folder. Non-admins must own the file unless bypassOwnership.", + "operationId": "createShareLink", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder", + "file" + ], + "properties": { + "folder": { + "type": "string", + "example": "root" + }, + "file": { + "type": "string", + "example": "invoice.pdf" + }, + "expirationValue": { + "type": "integer", + "example": 60 + }, + "expirationUnit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days" + ], + "example": "minutes" + }, + "password": { + "type": "string", + "example": "" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Share link created", "content": { "application/json": { "schema": { "properties": { - "success": { + "token": { "type": "string", - "example": "User panel updated: TOTP disabled" + "example": "abc123" + }, + "url": { + "type": "string", + "example": "/api/file/share.php?token=abc123" + }, + "expires": { + "type": "integer", + "example": 1700000000 } }, "type": "object" @@ -2331,14 +1101,1857 @@ } } }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/deleteFiles.php": { + "post": { + "tags": [ + "Files" + ], + "summary": "Delete files to Trash", + "description": "Requires write access on the folder and (for non-admins) ownership of the files.", + "operationId": "deleteFiles", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder", + "files" + ], + "properties": { + "folder": { + "type": "string", + "example": "root" + }, + "files": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "old.docx", + "draft.md" + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Delete result (model-defined)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/deleteShareLink.php": { + "post": { + "tags": [ + "Shares" + ], + "summary": "Delete a share link by token", + "description": "Deletes a share token. NOTE: Current implementation does not require authentication.", + "operationId": "deleteShareLink", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "example": "abc123" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Deletion result (success or not found)" + } + } + } + }, + "/api/file/deleteTrashFiles.php": { + "post": { + "tags": [ + "Trash" + ], + "summary": "Permanently delete Trash items (admin only)", + "operationId": "deleteTrashFiles", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "required": [ + "deleteAll" + ], + "properties": { + "deleteAll": { + "type": "boolean", + "example": true + } + }, + "type": "object" + }, + { + "required": [ + "files" + ], + "properties": { + "files": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "trash/abc", + "trash/def" + ] + } + }, + "type": "object" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Deletion result (model-defined)" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Admin only" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/download.php": { + "get": { + "tags": [ + "Files" + ], + "summary": "Download a file", + "description": "Requires view access (or own-only with ownership). Streams the file with appropriate Content-Type.", + "operationId": "downloadFile", + "parameters": [ + { + "name": "folder", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "root" + }, + { + "name": "file", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "photo.jpg" + } + ], + "responses": { + "200": { + "description": "Binary file", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Invalid folder/file" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not found" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/downloadZip.php": { + "post": { + "tags": [ + "Files" + ], + "summary": "Download multiple files as a ZIP", + "description": "Requires view access (or own-only with ownership). May be gated by account flag.", + "operationId": "downloadZip", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder", + "files" + ], + "properties": { + "folder": { + "type": "string", + "example": "root" + }, + "files": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "a.jpg", + "b.png" + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "ZIP archive", + "content": { + "application/zip": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/extractZip.php": { + "post": { + "tags": [ + "Files" + ], + "summary": "Extract ZIP file(s) into a folder", + "description": "Requires write access on the target folder.", + "operationId": "extractZip", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder", + "files" + ], + "properties": { + "folder": { + "type": "string", + "example": "root" + }, + "files": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "archive.zip" + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Extraction result (model-defined)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/getFileList.php": { + "get": { + "tags": [ + "Files" + ], + "summary": "List files in a folder", + "description": "Requires view access (full) or read_own (own-only results).", + "operationId": "getFileList", + "parameters": [ + { + "name": "folder", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "root" + } + ], + "responses": { + "200": { + "description": "Listing result (model-defined JSON)" + }, + "400": { + "description": "Invalid folder" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/getFileTags.php": { + "get": { + "tags": [ + "Tags" + ], + "summary": "Get global file tags", + "description": "Returns tag metadata (no auth in current implementation).", + "operationId": "getFileTags", + "responses": { + "200": { + "description": "Tags map (model-defined JSON)" + } + } + } + }, + "/api/file/getShareLinks.php": { + "get": { + "tags": [ + "Shares" + ], + "summary": "Get (raw) share links file", + "description": "Returns the full share links JSON (no auth in current implementation).", + "operationId": "getShareLinks", + "responses": { + "200": { + "description": "Share links (model-defined JSON)" + } + } + } + }, + "/api/file/getTrashItems.php": { + "get": { + "tags": [ + "Trash" + ], + "summary": "List items in Trash (admin only)", + "operationId": "getTrashItems", + "responses": { + "200": { + "description": "Trash contents (model-defined JSON)" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Admin only" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/renameFile.php": { + "put": { + "tags": [ + "Files" + ], + "summary": "Rename a file", + "description": "Requires write access; non-admins must own the file.", + "operationId": "renameFile", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder", + "oldName", + "newName" + ], + "properties": { + "folder": { + "type": "string", + "example": "root" + }, + "oldName": { + "type": "string", + "example": "old.pdf" + }, + "newName": { + "type": "string", + "example": "new.pdf" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Rename result (model-defined)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/restoreFiles.php": { + "post": { + "tags": [ + "Trash" + ], + "summary": "Restore files from Trash (admin only)", + "operationId": "restoreFiles", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "files" + ], + "properties": { + "files": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "trash/12345.json" + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Restore result (model-defined)" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Admin only" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/saveFile.php": { + "put": { + "tags": [ + "Files" + ], + "summary": "Create or overwrite a file’s content", + "description": "Requires write access. Overwrite enforces ownership for non-admins. Certain executable extensions are denied.", + "operationId": "saveFile", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder", + "fileName", + "content" + ], + "properties": { + "folder": { + "type": "string", + "example": "root" + }, + "fileName": { + "type": "string", + "example": "readme.txt" + }, + "content": { + "type": "string", + "example": "Hello world" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Save result (model-defined)" + }, + "400": { + "description": "Invalid input or disallowed extension" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/saveFileTag.php": { + "post": { + "tags": [ + "Tags" + ], + "summary": "Save tags for a file (or delete one)", + "description": "Requires write access and (for non-admins) ownership when modifying.", + "operationId": "saveFileTag", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder", + "file" + ], + "properties": { + "folder": { + "type": "string", + "example": "root" + }, + "file": { + "type": "string", + "example": "doc.md" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "work", + "urgent" + ] + }, + "deleteGlobal": { + "type": "boolean", + "example": false + }, + "tagToDelete": { + "type": "string", + "example": null, + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Save result (model-defined)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/file/share.php": { + "get": { + "tags": [ + "Shares" + ], + "summary": "Open a shared file by token", + "description": "If the link is password-protected and no password is supplied, an HTML password form is returned. Otherwise the file is streamed.", + "operationId": "shareFile", + "parameters": [ + { + "name": "token", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pass", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Binary file (or HTML password form when missing password)", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "text/html": {} + } + }, + "400": { + "description": "Missing token / invalid input" + }, + "403": { + "description": "Expired or invalid password" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/folder/capabilities.php": { + "get": { + "tags": [ + "Folders" + ], + "summary": "Get effective capabilities for the current user in a folder", + "description": "Computes the caller's capabilities for a given folder by combining account flags (readOnly/disableUpload), ACL grants (read/write/share), and the user-folder-only scope. Returns booleans indicating what the user can do.", + "operationId": "getFolderCapabilities", + "parameters": [ + { + "name": "folder", + "in": "query", + "description": "Target folder path. Defaults to 'root'. Supports nested paths like 'team/reports'.", + "required": false, + "schema": { + "type": "string" + }, + "example": "projects/acme" + } + ], + "responses": { + "200": { + "description": "Capabilities computed successfully.", + "content": { + "application/json": { + "schema": { + "required": [ + "user", + "folder", + "isAdmin", + "flags", + "canView", + "canUpload", + "canCreate", + "canRename", + "canDelete", + "canMoveIn", + "canShare" + ], + "properties": { + "user": { + "type": "string", + "example": "alice" + }, + "folder": { + "type": "string", + "example": "projects/acme" + }, + "isAdmin": { + "type": "boolean", + "example": false + }, + "flags": { + "required": [ + "folderOnly", + "readOnly", + "disableUpload" + ], + "properties": { + "folderOnly": { + "type": "boolean", + "example": false + }, + "readOnly": { + "type": "boolean", + "example": false + }, + "disableUpload": { + "type": "boolean", + "example": false + } + }, + "type": "object" + }, + "owner": { + "type": "string", + "example": "alice", + "nullable": true + }, + "canView": { + "description": "User can view items in this folder.", + "type": "boolean", + "example": true + }, + "canUpload": { + "description": "User can upload/edit/rename/move/delete items (i.e., WRITE).", + "type": "boolean", + "example": true + }, + "canCreate": { + "description": "User can create subfolders here.", + "type": "boolean", + "example": true + }, + "canRename": { + "description": "User can rename items here.", + "type": "boolean", + "example": true + }, + "canDelete": { + "description": "User can delete items here.", + "type": "boolean", + "example": true + }, + "canMoveIn": { + "description": "User can move items into this folder.", + "type": "boolean", + "example": true + }, + "canShare": { + "description": "User can create share links for this folder.", + "type": "boolean", + "example": false + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "Invalid folder name." + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/folder/createFolder.php": { + "post": { + "tags": [ + "Folders" + ], + "summary": "Create a new folder", + "description": "Requires authentication, CSRF token, and write access to the parent folder. Seeds ACL owner.", + "operationId": "createFolder", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "description": "CSRF token from the current session", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folderName" + ], + "properties": { + "folderName": { + "type": "string", + "example": "reports" + }, + "parent": { + "description": "Parent folder (default root)", + "type": "string", + "example": "root", + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Creation result (model-defined JSON)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "405": { + "description": "Method not allowed" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/folder/createShareFolderLink.php": { + "post": { + "tags": [ + "Shared Folders" + ], + "summary": "Create a share link for a folder", + "description": "Requires authentication, CSRF token, and share permission. Non-admins must own the folder (unless bypass) and cannot share root.", + "operationId": "createShareFolderLink", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder" + ], + "properties": { + "folder": { + "type": "string", + "example": "team/reports" + }, + "expirationValue": { + "type": "integer", + "example": 60 + }, + "expirationUnit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days" + ], + "example": "minutes" + }, + "password": { + "type": "string", + "example": "" + }, + "allowUpload": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "example": 0 + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Share folder link created", + "content": { + "application/json": { + "schema": { + "properties": { + "token": { + "type": "string", + "example": "sf_abc123" + }, + "url": { + "type": "string", + "example": "/api/folder/shareFolder.php?token=sf_abc123" + }, + "expires": { + "type": "integer", + "example": 1700000000 + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/folder/deleteFolder.php": { + "post": { + "tags": [ + "Folders" + ], + "summary": "Delete a folder", + "description": "Requires authentication, CSRF token, write scope, and (for non-admins) folder ownership.", + "operationId": "deleteFolder", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder" + ], + "properties": { + "folder": { + "type": "string", + "example": "userA/reports" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Deletion result (model-defined JSON)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "405": { + "description": "Method not allowed" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/folder/deleteShareFolderLink.php": { + "post": { + "tags": [ + "Shared Folders", + "Admin" + ], + "summary": "Delete a shared-folder link by token (admin only)", + "description": "Requires authentication, CSRF token, and admin privileges.", + "operationId": "deleteShareFolderLink", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "example": "sf_abc123" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Deleted" + }, + "400": { + "description": "No token provided" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Admin only" + }, + "404": { + "description": "Not found" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/folder/downloadSharedFile.php": { + "get": { + "tags": [ + "Shared Folders" + ], + "summary": "Download a file from a shared folder (by token)", + "description": "Public endpoint; validates token and file name, then streams the file.", + "operationId": "downloadSharedFile", + "parameters": [ + { + "name": "token", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "file", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "report.pdf" + } + ], + "responses": { + "200": { + "description": "Binary file", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Invalid input" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/folder/getFolderList.php": { + "get": { + "tags": [ + "Folders" + ], + "summary": "List folders (optionally under a parent)", + "description": "Requires authentication. Non-admins see folders for which they have full view or own-only access.", + "operationId": "getFolderList", + "parameters": [ + { + "name": "folder", + "in": "query", + "description": "Parent folder to include and descend (default all); use 'root' for top-level", + "required": false, + "schema": { + "type": "string" + }, + "example": "root" + } + ], + "responses": { + "200": { + "description": "List of folders", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "properties": { + "folder": { + "type": "string", + "example": "team/reports" + }, + "fileCount": { + "type": "integer", + "example": 12 + }, + "metadataFile": { + "type": "string", + "example": "/path/to/meta.json" + } + }, + "type": "object" + } + } + } + } + }, + "400": { + "description": "Invalid folder" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/folder/getShareFolderLinks.php": { + "get": { + "tags": [ + "Shared Folders", + "Admin" + ], + "summary": "List active shared-folder links (admin only)", + "description": "Returns all non-expired shared-folder links. Admin-only.", + "operationId": "getShareFolderLinks", + "responses": { + "200": { + "description": "Active share-folder links (model-defined JSON)" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Admin only" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/folder/renameFolder.php": { + "post": { + "tags": [ + "Folders" + ], + "summary": "Rename or move a folder", + "description": "Requires authentication, CSRF token, scope checks on old and new paths, and (for non-admins) ownership of the source folder.", + "operationId": "renameFolder", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "oldFolder", + "newFolder" + ], + "properties": { + "oldFolder": { + "type": "string", + "example": "team/q1" + }, + "newFolder": { + "type": "string", + "example": "team/quarter-1" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Rename result (model-defined JSON)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "405": { + "description": "Method not allowed" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/folder/shareFolder.php": { + "get": { + "tags": [ + "Shared Folders" + ], + "summary": "Open a shared folder by token (HTML UI)", + "description": "If the share is password-protected and no password is supplied, an HTML password form is returned. Otherwise renders an HTML listing with optional upload form.", + "operationId": "shareFolder", + "parameters": [ + { + "name": "token", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pass", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1 + }, + "example": 1 + } + ], + "responses": { + "200": { + "description": "HTML page (password form or folder listing)", + "content": { + "text/html": {} + } + }, + "400": { + "description": "Missing/invalid token" + }, + "403": { + "description": "Forbidden or wrong password" + } + } + } + }, + "/api/folder/uploadToSharedFolder.php": { + "post": { + "tags": [ + "Shared Folders" + ], + "summary": "Upload a file into a shared folder (by token)", + "description": "Public form-upload endpoint. Only allowed when the share link has uploads enabled. On success responds with a redirect to the share page.", + "operationId": "uploadToSharedFolder", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "token", + "fileToUpload" + ], + "properties": { + "token": { + "description": "Share token", + "type": "string" + }, + "fileToUpload": { + "description": "File to upload", + "type": "string", + "format": "binary" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "302": { + "description": "Redirect to /api/folder/shareFolder.php?token=..." + }, + "400": { + "description": "Upload error or invalid input" + }, + "405": { + "description": "Method not allowed" + } + } + } + }, + "/api/getUserPermissions.php": { + "get": { + "tags": [ + "Users" + ], + "summary": "Retrieve user permissions", + "description": "Returns the permissions for the current user, or all permissions if the user is an admin.", + "operationId": "getUserPermissions", + "responses": { + "200": { + "description": "Successful response with user permissions", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/getUsers.php": { + "get": { + "tags": [ + "Users" + ], + "summary": "Retrieve a list of users", + "description": "Returns a JSON array of users. Only available to authenticated admin users.", + "operationId": "getUsers", + "responses": { + "200": { + "description": "Successful response with an array of users", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "properties": { + "username": { + "type": "string", + "example": "johndoe" + }, + "role": { + "type": "string", + "example": "admin" + } + }, + "type": "object" + } + } + } + } + }, + "401": { + "description": "Unauthorized: the user is not authenticated or is not an admin" + } + } + } + }, + "/api/profile/getCurrentUser.php": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get the currently authenticated user's profile", + "description": "Returns basic info for the logged-in user. Requires a valid session cookie.", + "operationId": "getCurrentUser", + "responses": { + "200": { + "description": "User profile returned. If the user record is missing, an empty result may be returned.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "required": [ + "username", + "isAdmin", + "totp_enabled", + "profile_picture" + ], + "properties": { + "username": { + "type": "string", + "example": "alice" + }, + "isAdmin": { + "type": "boolean", + "example": false + }, + "totp_enabled": { + "type": "boolean", + "example": true + }, + "profile_picture": { + "description": "Public URL path to the profile picture (leading slash).", + "type": "string", + "example": "/uploads/profile_pics/alice_9f3c2e1a8bcd.png" + } + }, + "type": "object" + }, + { + "type": "array", + "maxItems": 0, + "example": [] + } + ] + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/profile/uploadPicture.php": { + "post": { + "tags": [ + "Users" + ], + "summary": "Upload or replace the current user's profile picture", + "description": "Accepts a single image file (JPEG, PNG, or GIF) up to 2 MB. Requires a valid session cookie and CSRF token.", + "operationId": "uploadProfilePicture", + "parameters": [ + { + "name": "X-CSRF-Token", + "in": "header", + "description": "Anti-CSRF token associated with the current session.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "profile_picture" + ], + "properties": { + "profile_picture": { + "description": "JPEG, PNG, or GIF image. Max size: 2 MB.", + "type": "string", + "format": "binary" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Profile picture updated.", + "content": { + "application/json": { + "schema": { + "required": [ + "success", + "url" + ], + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "url": { + "type": "string", + "example": "/uploads/profile_pics/alice_9f3c2e1a8bcd.png" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "No file uploaded, invalid file type, or file too large." + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "description": "Server error while saving the picture." + } + }, + "security": [ + { + "cookieAuth": [] + } + ] + } + }, + "/api/removeUser.php": { + "delete": { + "tags": [ + "Users" + ], + "summary": "Remove a user", + "description": "Removes the specified user from the system. Cannot remove the currently logged-in user.", + "operationId": "removeUser", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "username" + ], + "properties": { + "username": { + "type": "string", + "example": "johndoe" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "User removed successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "string", + "example": "User removed successfully" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "Bad Request" + }, "401": { "description": "Unauthorized" }, "403": { "description": "Invalid CSRF token" - }, - "400": { - "description": "Bad Request" } } } @@ -2564,36 +3177,477 @@ } } } + }, + "/api/updateUserPanel.php": { + "put": { + "tags": [ + "Users" + ], + "summary": "Update user panel settings", + "description": "Updates user panel settings by disabling TOTP when not enabled. Accessible to authenticated users.", + "operationId": "updateUserPanel", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "totp_enabled" + ], + "properties": { + "totp_enabled": { + "type": "boolean", + "example": false + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "User panel updated successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "string", + "example": "User panel updated: TOTP disabled" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Invalid CSRF token" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/updateUserPermissions.php": { + "put": { + "tags": [ + "Users" + ], + "summary": "Update user permissions", + "description": "Updates permissions for users. Only available to authenticated admin users.", + "operationId": "updateUserPermissions", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "permissions" + ], + "properties": { + "permissions": { + "type": "array", + "items": { + "properties": { + "username": { + "type": "string", + "example": "johndoe" + }, + "folderOnly": { + "type": "boolean", + "example": true + }, + "readOnly": { + "type": "boolean", + "example": false + }, + "disableUpload": { + "type": "boolean", + "example": false + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "User permissions updated successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "string", + "example": "User permissions updated successfully." + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Invalid CSRF token" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/upload/removeChunks.php": { + "post": { + "tags": [ + "Uploads" + ], + "summary": "Remove temporary chunk directory", + "description": "Deletes the temporary directory used for a chunked upload. Requires a valid CSRF token in the form field.", + "operationId": "removeChunks", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "folder" + ], + "properties": { + "folder": { + "type": "string", + "example": "resumable_myupload123" + }, + "csrf_token": { + "description": "CSRF token for this session", + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Removal result", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Temporary folder removed." + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "Invalid input" + }, + "403": { + "description": "Invalid CSRF token" + } + } + } + } + }, + "components": { + "schemas": { + "SimpleSuccess": { + "properties": { + "success": { + "type": "boolean", + "example": true + } + }, + "type": "object" + }, + "SimpleError": { + "properties": { + "error": { + "type": "string", + "example": "Something went wrong" + } + }, + "type": "object" + }, + "LoginOptionsPublic": { + "properties": { + "disableFormLogin": { + "type": "boolean" + }, + "disableBasicAuth": { + "type": "boolean" + }, + "disableOIDCLogin": { + "type": "boolean" + } + }, + "type": "object" + }, + "LoginOptionsAdminExtra": { + "properties": { + "authBypass": { + "type": "boolean", + "nullable": true + }, + "authHeaderName": { + "type": "string", + "example": "X-Remote-User", + "nullable": true + } + }, + "type": "object" + }, + "OIDCConfigPublic": { + "properties": { + "providerUrl": { + "type": "string", + "example": "https://accounts.example.com" + }, + "redirectUri": { + "type": "string", + "example": "https://your.filerise.app/callback" + } + }, + "type": "object" + }, + "AdminGetConfigPublic": { + "required": [ + "header_title", + "loginOptions", + "globalOtpauthUrl", + "enableWebDAV", + "sharedMaxUploadSize", + "oidc" + ], + "properties": { + "header_title": { + "type": "string", + "example": "FileRise" + }, + "loginOptions": { + "$ref": "#/components/schemas/LoginOptionsPublic" + }, + "globalOtpauthUrl": { + "type": "string" + }, + "enableWebDAV": { + "type": "boolean" + }, + "sharedMaxUploadSize": { + "type": "integer", + "format": "int64" + }, + "oidc": { + "$ref": "#/components/schemas/OIDCConfigPublic" + } + }, + "type": "object" + }, + "AdminGetConfigAdmin": { + "allOf": [ + { + "properties": { + "loginOptions": { + "allOf": [ + { + "$ref": "#/components/schemas/LoginOptionsPublic" + }, + { + "$ref": "#/components/schemas/LoginOptionsAdminExtra" + } + ] + } + }, + "type": "object" + }, + { + "$ref": "#/components/schemas/AdminGetConfigPublic" + } + ] + }, + "AdminUpdateConfigRequest": { + "properties": { + "header_title": { + "type": "string", + "maxLength": 100, + "example": "FileRise" + }, + "loginOptions": { + "properties": { + "disableFormLogin": { + "type": "boolean", + "example": false + }, + "disableBasicAuth": { + "type": "boolean", + "example": false + }, + "disableOIDCLogin": { + "description": "false = OIDC enabled", + "type": "boolean", + "example": true + }, + "authBypass": { + "type": "boolean", + "example": false + }, + "authHeaderName": { + "description": "Letters/numbers/dashes only", + "type": "string", + "pattern": "^[A-Za-z0-9\\\\-]+$", + "example": "X-Remote-User" + } + }, + "type": "object", + "additionalProperties": false + }, + "globalOtpauthUrl": { + "type": "string", + "example": "otpauth://totp/{label}?secret={secret}&issuer=FileRise" + }, + "enableWebDAV": { + "type": "boolean", + "example": false + }, + "sharedMaxUploadSize": { + "type": "integer", + "format": "int64", + "minimum": 0, + "example": 52428800 + }, + "oidc": { + "description": "When disableOIDCLogin=false (OIDC enabled), providerUrl, redirectUri, and clientId are required.", + "properties": { + "providerUrl": { + "type": "string", + "format": "uri", + "example": "https://issuer.example.com" + }, + "clientId": { + "type": "string", + "example": "my-client-id" + }, + "clientSecret": { + "type": "string", + "writeOnly": true, + "example": "***" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "example": "https://app.example.com/auth/callback" + } + }, + "type": "object", + "additionalProperties": false + } + }, + "type": "object", + "additionalProperties": false + } + }, + "responses": { + "Unauthorized": { + "description": "Unauthorized (no session)", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + }, + "type": "object" + } + } + } + }, + "Forbidden": { + "description": "Forbidden (not enough privileges)", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Invalid CSRF token." + } + }, + "type": "object" + } + } + } + }, + "200": { + "description": "Move result (model-defined)" + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal error" + } + }, + "parameters": { + "X-CSRF-Token": { + "name": "X-CSRF-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + }, + "requestBodies": {}, + "securitySchemes": { + "cookieAuth": { + "type": "apiKey", + "description": "Session cookie used for authenticated endpoints", + "name": "PHPSESSID", + "in": "cookie" + }, + "CsrfHeader": { + "type": "apiKey", + "description": "CSRF token header required for state-changing requests", + "name": "X-CSRF-Token", + "in": "header" + } } }, "tags": [ { "name": "Admin", - "description": "Admin" - }, - { - "name": "Auth", - "description": "Auth" - }, - { - "name": "Files", - "description": "Files" - }, - { - "name": "Folders", - "description": "Folders" - }, - { - "name": "Uploads", - "description": "Uploads" - }, - { - "name": "Users", - "description": "Users" - }, - { - "name": "TOTP", - "description": "TOTP" + "description": "Admin endpoints" } ] } \ No newline at end of file diff --git a/public/api.php b/public/api.php index 3e1c33f..4e1b41e 100644 --- a/public/api.php +++ b/public/api.php @@ -20,7 +20,7 @@ if (isset($_GET['spec'])) { FileRise API Docs diff --git a/public/api/addUser.php b/public/api/addUser.php index 9c90059..202200e 100644 --- a/public/api/addUser.php +++ b/public/api/addUser.php @@ -1,6 +1,40 @@ ${version}`; // Translate with fallback: if t(key) just echos the key, use a readable string. @@ -340,14 +340,14 @@ export function openAdminPanel() {

${adminTitle}

${[ - { id: "userManagement", label: t("user_management") }, - { id: "headerSettings", label: t("header_settings") }, - { id: "loginOptions", label: t("login_options") }, - { id: "webdav", label: "WebDAV Access" }, - { id: "upload", label: t("shared_max_upload_size_bytes_title") }, - { id: "oidc", label: t("oidc_configuration") + " & TOTP" }, - { id: "shareLinks", label: t("manage_shared_links") } - ].map(sec => ` + { id: "userManagement", label: t("user_management") }, + { id: "headerSettings", label: t("header_settings") }, + { id: "loginOptions", label: t("login_options") }, + { id: "webdav", label: "WebDAV Access" }, + { id: "upload", label: t("shared_max_upload_size_bytes_title") }, + { id: "oidc", label: t("oidc_configuration") + " & TOTP" }, + { id: "shareLinks", label: t("manage_shared_links") } + ].map(sec => ` @@ -384,7 +384,7 @@ export function openAdminPanel() { `; - + document.getElementById("adminOpenAddUser") .addEventListener("click", () => { toggleVisibility("addUserModal", true); @@ -472,25 +472,25 @@ export function openAdminPanel() { }); // after you set #userManagementContent.innerHTML (right after those three buttons are inserted) -const userMgmt = document.getElementById("userManagementContent"); + const userMgmt = document.getElementById("userManagementContent"); -// defensive: remove any old listener first -userMgmt?.removeEventListener("click", window.__userMgmtDelegatedClick); + // defensive: remove any old listener first + userMgmt?.removeEventListener("click", window.__userMgmtDelegatedClick); -window.__userMgmtDelegatedClick = (e) => { - const flagsBtn = e.target.closest("#adminOpenUserFlags"); - if (flagsBtn) { - e.preventDefault(); - openUserFlagsModal(); - } - const folderBtn = e.target.closest("#adminOpenUserPermissions"); - if (folderBtn) { - e.preventDefault(); - openUserPermissionsModal(); - } -}; + window.__userMgmtDelegatedClick = (e) => { + const flagsBtn = e.target.closest("#adminOpenUserFlags"); + if (flagsBtn) { + e.preventDefault(); + openUserFlagsModal(); + } + const folderBtn = e.target.closest("#adminOpenUserPermissions"); + if (folderBtn) { + e.preventDefault(); + openUserPermissionsModal(); + } + }; -userMgmt?.addEventListener("click", window.__userMgmtDelegatedClick); + userMgmt?.addEventListener("click", window.__userMgmtDelegatedClick); // Initialize inputs from config + capture document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true; @@ -615,15 +615,26 @@ function renderFolderGrantsUI(username, container, folders, grants) { // toolbar const toolbar = document.createElement('div'); toolbar.className = 'folder-access-toolbar'; + // Toolbar (bulk toggles with descriptions) toolbar.innerHTML = ` - - - - - - - (${tf('applies_to_filtered','applies to filtered list')}) - `; + + + + + + + (${tf('applies_to_filtered', 'applies to filtered list')}) +`; container.appendChild(toolbar); // list (will contain sticky header + rows) @@ -631,16 +642,27 @@ function renderFolderGrantsUI(username, container, folders, grants) { list.className = 'folder-access-list'; container.appendChild(list); + // Header (compact labels, descriptive tooltips so the column width stays the same) const headerHtml = ` -
-
${tf('folder', 'Folder')}
-
${tf('view_all','View (all)')}
-
${tf('view_own','View (own)')}
-
${tf('upload','Upload')}
-
${tf('manage','Manage')}
-
${tf('share','Share')}
+
+
${tf('folder', 'Folder')}
+
+ ${tf('view_all', 'View (all)')}
- `; +
+ ${tf('view_own', 'View (own)')} +
+
+ ${tf('write', 'Write')} +
+
+ ${tf('manage', 'Manage')} +
+
+ ${tf('share', 'Share')} +
+
+`; function rowHtml(folder) { const g = grants[folder] || {}; @@ -648,28 +670,28 @@ function renderFolderGrantsUI(username, container, folders, grants) { return `
folder${name}
-
+
-
-
-
+
+
+
`; } // Dependencies function applyDeps(row) { - const cbView = row.querySelector('input[data-cap="view"]'); + const cbView = row.querySelector('input[data-cap="view"]'); const cbViewOwn = row.querySelector('input[data-cap="viewOwn"]'); - const cbUpload = row.querySelector('input[data-cap="upload"]'); - const cbManage = row.querySelector('input[data-cap="manage"]'); - const cbShare = row.querySelector('input[data-cap="share"]'); + const cbUpload = row.querySelector('input[data-cap="upload"]'); + const cbManage = row.querySelector('input[data-cap="manage"]'); + const cbShare = row.querySelector('input[data-cap="share"]'); // Manage ⇒ full view + upload + share if (cbManage.checked) { - cbView.checked = true; + cbView.checked = true; cbUpload.checked = true; - cbShare.checked = true; + cbShare.checked = true; } // Share ⇒ full view @@ -684,7 +706,7 @@ function renderFolderGrantsUI(username, container, folders, grants) { if (cbView.checked || cbManage.checked) { cbViewOwn.checked = false; cbViewOwn.disabled = true; - cbViewOwn.title = tf('full_view_supersedes_own','Full view supersedes own-only'); + cbViewOwn.title = tf('full_view_supersedes_own', 'Full view supersedes own-only'); } else { cbViewOwn.disabled = false; cbViewOwn.removeAttribute('title'); @@ -701,14 +723,14 @@ function renderFolderGrantsUI(username, container, folders, grants) { } function wireRow(row) { - const cbView = row.querySelector('input[data-cap="view"]'); + const cbView = row.querySelector('input[data-cap="view"]'); const cbViewOwn = row.querySelector('input[data-cap="viewOwn"]'); - const cbUpload = row.querySelector('input[data-cap="upload"]'); - const cbManage = row.querySelector('input[data-cap="manage"]'); - const cbShare = row.querySelector('input[data-cap="share"]'); + const cbUpload = row.querySelector('input[data-cap="upload"]'); + const cbManage = row.querySelector('input[data-cap="manage"]'); + const cbShare = row.querySelector('input[data-cap="share"]'); cbUpload.addEventListener('change', () => applyDeps(row)); - cbShare .addEventListener('change', () => applyDeps(row)); + cbShare.addEventListener('change', () => applyDeps(row)); cbManage.addEventListener('change', () => applyDeps(row)); cbView.addEventListener('change', () => { @@ -762,13 +784,13 @@ function renderFolderGrantsUI(username, container, folders, grants) { row.querySelector('input[data-cap="view"]').checked = true; } if (which === 'upload' && bulk.checked) { - const v = row.querySelector('input[data-cap="view"]'); + const v = row.querySelector('input[data-cap="view"]'); const vo = row.querySelector('input[data-cap="viewOwn"]'); if (!v.checked && !vo.checked) vo.checked = true; } if (which === 'view' && !bulk.checked) { row.querySelector('input[data-cap="manage"]').checked = false; - row.querySelector('input[data-cap="share"]').checked = false; + row.querySelector('input[data-cap="share"]').checked = false; } applyDeps(row); @@ -784,11 +806,11 @@ function collectGrantsFrom(container) { const folder = row.dataset.folder; if (!folder) return; const g = { - view: row.querySelector('input[data-cap="view"]').checked, + view: row.querySelector('input[data-cap="view"]').checked, viewOwn: row.querySelector('input[data-cap="viewOwn"]').checked, - upload: row.querySelector('input[data-cap="upload"]').checked, - manage: row.querySelector('input[data-cap="manage"]').checked, - share: row.querySelector('input[data-cap="share"]').checked + upload: row.querySelector('input[data-cap="upload"]').checked, + manage: row.querySelector('input[data-cap="manage"]').checked, + share: row.querySelector('input[data-cap="share"]').checked }; if (g.view || g.viewOwn || g.upload || g.manage || g.share) out[folder] = g; }); @@ -801,14 +823,17 @@ export function openUserPermissionsModal() { const isDarkMode = document.body.classList.contains("dark-mode"); const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; const modalContentStyles = ` - background: ${isDarkMode ? "#2c2c2c" : "#fff"}; - color: ${isDarkMode ? "#e0e0e0" : "#000"}; - padding: 20px; - max-width: 780px; - width: 95%; - border-radius: 8px; - position: relative; - `; + background: ${isDarkMode ? "#2c2c2c" : "#fff"}; + color: ${isDarkMode ? "#e0e0e0" : "#000"}; + padding: 20px; + /* Wider, responsive */ + width: clamp(980px, 92vw, 1280px); + max-width: none; + border-radius: 8px; + position: relative; + max-height: 90vh; + overflow: auto; +`; if (!userPermissionsModal) { userPermissionsModal = document.createElement("div"); @@ -825,9 +850,9 @@ export function openUserPermissionsModal() { ×

${tf("folder_access", "Folder Access")}

- ${tf("grant_folders_help", "Grant per-folder capabilities to each user. 'Upload/Manage/Share' imply 'View'.")} + ${tf("grant_folders_help", "Grant per-folder capabilities to each user. 'Write/Manage/Share' imply 'View'.")}
-
+
@@ -856,19 +881,19 @@ export function openUserPermissionsModal() { }); try { - if (saves.length === 0) { - showToast(tf("nothing_to_save", "Nothing to save")); - return; - } - for (const payload of saves) { - await sendRequest("/api/admin/acl/saveGrants.php", "POST", payload, { "X-CSRF-Token": window.csrfToken }); - } - showToast(tf("user_permissions_updated_successfully", "User permissions updated successfully")); - userPermissionsModal.style.display = "none"; - } catch (err) { - console.error(err); - showToast(tf("error_updating_permissions", "Error updating permissions"), "error"); - } + if (saves.length === 0) { + showToast(tf("nothing_to_save", "Nothing to save")); + return; + } + for (const payload of saves) { + await sendRequest("/api/admin/acl/saveGrants.php", "POST", payload, { "X-CSRF-Token": window.csrfToken }); + } + showToast(tf("user_permissions_updated_successfully", "User permissions updated successfully")); + userPermissionsModal.style.display = "none"; + } catch (err) { + console.error(err); + showToast(tf("error_updating_permissions", "Error updating permissions"), "error"); + } }); } else { userPermissionsModal.style.display = "flex"; @@ -887,12 +912,12 @@ async function fetchAllUserFlags() { const r = await fetch("/api/getUserPermissions.php", { credentials: "include" }); const data = await r.json(); // remove deprecated flag if present, so UI never shows it - if (data && typeof data === "object") { - const map = data.allPermissions || data.permissions || data; - if (map && typeof map === "object") { - Object.values(map).forEach(u => { if (u && typeof u === "object") delete u.folderOnly; }); - } - } + if (data && typeof data === "object") { + const map = data.allPermissions || data.permissions || data; + if (map && typeof map === "object") { + Object.values(map).forEach(u => { if (u && typeof u === "object") delete u.folderOnly; }); + } + } // Accept both shapes: {users:[...]} or a plain object map if (Array.isArray(data)) { // unlikely, but normalize @@ -901,7 +926,7 @@ async function fetchAllUserFlags() { return out; } if (data && data.allPermissions) return data.allPermissions; - if (data && data.permissions) return data.permissions; + if (data && data.permissions) return data.permissions; return data || {}; } @@ -912,33 +937,49 @@ function flagRow(u, flags) { return ` ${u.username} - - - - + + + + `; } export async function openUserFlagsModal() { + const isDark = document.body.classList.contains("dark-mode"); + const overlayBg = isDark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; + const contentBg = isDark ? "#2c2c2c" : "#fff"; + const contentFg = isDark ? "#e0e0e0" : "#000"; + const borderCol = isDark ? "#555" : "#ccc"; + let modal = document.getElementById("userFlagsModal"); if (!modal) { modal = document.createElement("div"); modal.id = "userFlagsModal"; modal.style.cssText = ` - position:fixed; inset:0; background:rgba(0,0,0,.5); + position:fixed; inset:0; background:${overlayBg}; display:flex; align-items:center; justify-content:center; z-index:3600; `; modal.innerHTML = ` -