Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d29900d6ba | ||
|
|
5ffc068041 | ||
|
|
1935cb2442 | ||
|
|
af9887e651 | ||
|
|
327eea2835 | ||
|
|
3843daa228 | ||
|
|
169e03be5d |
69
CHANGELOG.md
69
CHANGELOG.md
@@ -1,5 +1,74 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 10/20/2025 (v1.5.3)
|
||||
|
||||
security(acl): enforce folder-scope & own-only; fix file list “Select All”; harden ops
|
||||
|
||||
### fileListView.js (v1.5.3)
|
||||
|
||||
- Restore master “Select All” checkbox behavior and row highlighting.
|
||||
- Keep selection working with own-only filtered lists.
|
||||
- Build preview/thumb URLs via secure API endpoints; avoid direct /uploads.
|
||||
- Minor UI polish: slider wiring and pagination focus handling.
|
||||
|
||||
### FileController.php (v1.5.3)
|
||||
|
||||
- Add enforceFolderScope($folder, $user, $perms, $need) and apply across actions.
|
||||
- Copy/Move: require read on source, write on destination; apply scope on both.
|
||||
- When user only has read_own, enforce per-file ownership (uploader==user).
|
||||
- Extract ZIP: require write + scope; consistent 403 messages.
|
||||
- Save/Rename/Delete/Create: tighten ACL checks; block dangerous extensions; consistent CSRF/Auth handling and error codes.
|
||||
- Download/ZIP: honor read vs read_own; own-only gates by uploader; safer headers.
|
||||
|
||||
### FolderController.php (v1.5.3)
|
||||
|
||||
- Align with ACL: enforce folder-scope for non-admins; require owner or bypass for destructive ops.
|
||||
- Create/Rename/Delete: gate by write on parent/target + ownership when needed.
|
||||
- Share folder link: require share capability; forbid root sharing for non-admins; validate expiry; optional password.
|
||||
- Folder listing: return only folders user can fully view or has read_own.
|
||||
- Shared downloads/uploads: stricter validation, headers, and error handling.
|
||||
|
||||
This commits a consistent, least-privilege ACL model (owners/read/write/share/read_own), fixes bulk-select in the UI, and closes scope/ownership gaps across file & folder actions.
|
||||
|
||||
feat(dnd): default cards to sidebar on medium screens when no saved layout
|
||||
|
||||
- Adds one-time responsive default in loadSidebarOrder() (uses layoutDefaultApplied_v1)
|
||||
- Preserves existing sidebarOrder/headerOrder and small-screen behavior
|
||||
- Keeps user changes persistent; no override once a layout exists
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
33
README.md
33
README.md
@@ -2,8 +2,9 @@
|
||||
|
||||
[](https://github.com/error311/FileRise)
|
||||
[](https://hub.docker.com/r/error311/filerise-docker)
|
||||
[](https://github.com/error311/filerise-docker/actions/workflows/main.yml)
|
||||
[](https://github.com/error311/FileRise/actions/workflows/ci.yml)
|
||||
[](https://demo.filerise.net) **demo / demo**
|
||||
[](https://demo.filerise.net)
|
||||
[](https://github.com/error311/FileRise/releases)
|
||||
[](LICENSE)
|
||||
|
||||
@@ -12,6 +13,8 @@
|
||||
**Elevate your File Management** – A modern, self-hosted web file manager.
|
||||
Upload, organize, and share files or folders through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
|
||||
|
||||
> ⚠️ **Security fix in v1.5.0** — ACL hardening. If you’re on ≤1.4.x, please upgrade.
|
||||
|
||||
**4/3/2025 Video demo:**
|
||||
|
||||
<https://github.com/user-attachments/assets/221f6a53-85f5-48d4-9abe-89445e0af90e>
|
||||
@@ -27,24 +30,28 @@ Upload, organize, and share files or folders through a sleek web interface. **Fi
|
||||
|
||||
- 🗂️ **File Management:** Full set of file/folder operations – move or copy files (via intuitive drag-drop or dialogs), rename items, and delete in batches. You can download selected files as a ZIP archive or extract uploaded ZIP files server-side. Organize content with an interactive folder tree and breadcrumb navigation for quick jumps.
|
||||
|
||||
- 🗃️ **Folder Sharing & File Sharing:** Share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls; file sizes are displayed in MB for clarity. Share individual files with one-time or expiring links (optional password protection).
|
||||
- 🗃️ **Folder Sharing & File Sharing:** Share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items/page); file sizes are displayed in MB. Share individual files with one-time or expiring links (optional password protection).
|
||||
|
||||
- 🔌 **WebDAV Support:** Mount FileRise as a network drive **or use it head-less from the CLI**. Standard WebDAV operations (upload / download / rename / delete) work in Cyberduck, WinSCP, GNOME Files, Finder, etc., and you can also script against it with `curl` – see the [WebDAV](https://github.com/error311/FileRise/wiki/WebDAV) + [curl](https://github.com/error311/FileRise/wiki/Accessing-FileRise-via-curl-(WebDAV)) quick-starts. Folder-Only users are restricted to their personal directory; admins and unrestricted users have full access.
|
||||
- 🔐 **Fine-grained Access Control (ACL):** Per-folder grants for **owners**, **read** (view all), **read_own** (own-only visibility), **write** (upload/edit), and **share**.
|
||||
- _Note:_ **write no longer implies read**. Grant **read** if uploaders should see all files; or **read_own** for self-only listings.
|
||||
- Enforced server-side across UI, API, and WebDAV. Includes an admin UI for bulk editing (atomic updates) and safe defaults.
|
||||
|
||||
- 🔌 **WebDAV Support (ACL-aware):** Mount FileRise as a network drive **or use it headless from the CLI**. Standard WebDAV ops (upload / download / delete) work in Cyberduck, WinSCP, GNOME Files, Finder, etc., and you can script with `curl`. Listings require **read**; users with **read_own** only see their own files; writes require **write**.
|
||||
|
||||
- 📚 **API Documentation:** Auto-generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
||||
|
||||
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal. Edit text/code files in your browser with a CodeMirror-based editor featuring syntax highlighting and line numbers.
|
||||
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal. Edit text/code files in your browser with a CodeMirror-based editor with syntax highlighting and line numbers.
|
||||
|
||||
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags and locate them instantly using indexed real-time search. **Advanced Search** adds fuzzy matching across file names, tags, uploader fields, and within text file contents.
|
||||
|
||||
- 🔒 **User Authentication & Permissions:** Username/password login with multi-user support (admin UI). Current permissions: **Folder-only**, **Read-only**, **Disable upload**. SSO via OIDC providers (Google/Authentik/Keycloak) and optional TOTP 2FA.
|
||||
- 🔒 **Auth & SSO:** Username/password login, optional TOTP 2FA, and OIDC (Google/Authentik/Keycloak). Per-user flags like **readOnly**/**disableUpload** still supported, but folder access is governed by the ACL above.
|
||||
|
||||
- 🗑️ **Trash & Recovery:** Deleted items go to Trash first; **admins** can restore or empty. Old trash entries auto-purge (default 3 days).
|
||||
|
||||
- 🎨 **Responsive UI (Dark/Light Mode):** Mobile-friendly layout with theme toggle. The interface remembers your preferences (layout, items per page, last visited folder, etc.).
|
||||
|
||||
- 🌐 **Internationalization & Localization:** Switch languages via the UI (English, Spanish, French, German). Contributions welcome.
|
||||
|
||||
- 🗑️ **Trash & File Recovery:** Deleted items go to Trash first; admins can restore or empty. Old trash entries auto-purge (default 3 days).
|
||||
|
||||
- ⚙️ **Lightweight & Self-Contained:** Runs on PHP **8.3+** with no external database. Single-folder install or Docker image. Low footprint; scales to thousands of files with pagination and sorting.
|
||||
|
||||
(For full features and changelogs, see the [Wiki](https://github.com/error311/FileRise/wiki), [CHANGELOG](https://github.com/error311/FileRise/blob/master/CHANGELOG.md) or [Releases](https://github.com/error311/FileRise/releases).)
|
||||
@@ -56,7 +63,7 @@ Upload, organize, and share files or folders through a sleek web interface. **Fi
|
||||
[](https://demo.filerise.net)
|
||||
**Demo credentials:** `demo` / `demo`
|
||||
|
||||
Curious about the UI? **Check out the live demo:** <https://demo.filerise.net> (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:** <https://demo.filerise.net> (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!
|
||||
|
||||
---
|
||||
|
||||
@@ -281,6 +288,16 @@ For more Q&A or to ask for help, open a Discussion or Issue.
|
||||
|
||||
---
|
||||
|
||||
## Security posture
|
||||
|
||||
We practice responsible disclosure. All known security issues are fixed in **v1.5.0** (ACL hardening).
|
||||
Advisories: [GHSA-6p87-q9rh-95wh](https://github.com/error311/FileRise/security/advisories/GHSA-6p87-q9rh-95wh) (≤ 1.3.15), [GHSA-jm96-2w52-5qjj](https://github.com/error311/FileRise/security/advisories/GHSA-jm96-2w52-5qjj) (v1.4.0). Fixed in **v1.5.0**. Thanks to [@kiwi865](https://github.com/kiwi865) for reporting.
|
||||
If you’re running ≤1.4.x, please upgrade.
|
||||
|
||||
See also: [SECURITY.md](./SECURITY.md) for how to report vulnerabilities.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
65
SECURITY.md
65
SECURITY.md
@@ -4,35 +4,58 @@
|
||||
|
||||
We provide security fixes for the latest minor release line.
|
||||
|
||||
| Version | Supported |
|
||||
|------------|-----------|
|
||||
| v1.5.x | ✅ |
|
||||
| < v1.5.0 | ❌ |
|
||||
| Version | Supported |
|
||||
|----------|-----------|
|
||||
| v1.5.x | ✅ |
|
||||
| ≤ v1.4.x | ❌ |
|
||||
|
||||
> Known issues in ≤ v1.4.x are fixed in **v1.5.0** and later.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability, please do not open a public issue. Instead, follow these steps:
|
||||
**Please do not open a public issue.** Use one of the private channels below:
|
||||
|
||||
1. **Email Us Privately:**
|
||||
Send an email to [security@filerise.net](mailto:security@filerise.net) with the subject line “[FileRise] Security Vulnerability Report”.
|
||||
1) **GitHub Security Advisory (preferred)**
|
||||
Open a private report here: <https://github.com/error311/FileRise/security/advisories/new>
|
||||
|
||||
2. **Include Details:**
|
||||
Provide a detailed description of the vulnerability, steps to reproduce it, and any other relevant information (e.g., affected versions, screenshots, logs).
|
||||
2) **Email**
|
||||
Send details to **<security@filerise.net>** with subject: `[FileRise] Security Vulnerability Report`.
|
||||
|
||||
3. **Secure Communication (Optional):**
|
||||
If you wish to discuss the vulnerability securely, you can use our PGP key. You can obtain our PGP key by emailing us, and we will send it upon request.
|
||||
### What to include
|
||||
|
||||
## Disclosure Policy
|
||||
- Affected versions (e.g., v1.4.0), component/endpoint, and impact
|
||||
- Reproduction steps / PoC
|
||||
- Any logs, screenshots, or crash traces
|
||||
- Safe test scope used (see below)
|
||||
|
||||
- **Acknowledgement:**
|
||||
We will acknowledge receipt of your report within 48 hours.
|
||||
|
||||
- **Resolution Timeline:**
|
||||
We aim to fix confirmed vulnerabilities within 30 days. In cases where a delay is necessary, we will communicate updates to you directly.
|
||||
If you’d like encrypted comms, ask for our PGP key in your first email.
|
||||
|
||||
- **Public Disclosure:**
|
||||
After a fix is available, details of the vulnerability will be disclosed publicly in a way that does not compromise user security.
|
||||
## Coordinated Disclosure
|
||||
|
||||
## Additional Information
|
||||
- **Acknowledgement:** within **48 hours**
|
||||
- **Triage & initial assessment:** within **7 days**
|
||||
- **Fix target:** within **30 days** for high-severity issues (may vary by complexity)
|
||||
- **CVE & advisory:** we publish a GitHub Security Advisory and request a CVE when appropriate.
|
||||
We notify the reporter before public disclosure and credit them (unless they prefer to remain anonymous).
|
||||
|
||||
We appreciate responsible disclosure of vulnerabilities and thank all researchers who help keep FileRise secure. For any questions related to this policy, please contact us at [admin@filerise.net](mailto:admin@filerise.net).
|
||||
## Safe-Harbor / Rules of Engagement
|
||||
|
||||
We support good-faith research. Please:
|
||||
|
||||
- Avoid privacy violations, data exfiltration, and service disruption (no DoS, spam, or brute-forcing)
|
||||
- Don’t access other users’ data beyond what’s necessary to demonstrate the issue
|
||||
- Don’t run automated scans against production installs you don’t own
|
||||
- Follow applicable laws and make a good-faith effort to respect data and availability
|
||||
|
||||
If you follow these guidelines, we won’t pursue or support legal action.
|
||||
|
||||
## Published Advisories
|
||||
|
||||
- **GHSA-6p87-q9rh-95wh** — ≤ **1.3.15**: Improper ownership/permission validation allowed cross-tenant file operations.
|
||||
- **GHSA-jm96-2w52-5qjj** — **v1.4.0**: Insecure folder visibility via name-based mapping and incomplete ACL checks.
|
||||
|
||||
Both are fixed in **v1.5.0** (ACL hardening). Thanks to **[@kiwi865](https://github.com/kiwi865)** for responsible disclosure.
|
||||
|
||||
## Questions
|
||||
|
||||
General security questions: **<admin@filerise.net>**
|
||||
|
||||
5098
openapi.json.dist
5098
openapi.json.dist
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ if (isset($_GET['spec'])) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>FileRise API Docs</title>
|
||||
<script defer src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
|
||||
integrity="sha384-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX"
|
||||
integrity="sha384-70P5pmIdaQdVbxvjhrcTDv1uKcKqalZ3OHi7S2J+uzDl0PW8dO6L+pHOpm9EEjGJ"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="/js/redoc-init.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
<?php
|
||||
// public/api/addUser.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/addUser.php",
|
||||
* 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",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"username", "password"},
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="password", type="string", example="securepassword"),
|
||||
* @OA\Property(property="isAdmin", type="boolean", example=true)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User added successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="User added successfully")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
<?php
|
||||
// public/api/admin/acl/getGrants.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/admin/acl/getGrants.php",
|
||||
* summary="Get ACL grants for a user",
|
||||
* tags={"Admin","ACL"},
|
||||
* security={{"cookieAuth":{}}},
|
||||
* @OA\Parameter(name="user", in="query", required=true, @OA\Schema(type="string")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Map of folder → grant flags",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* required={"grants"},
|
||||
* @OA\Property(property="grants", ref="#/components/schemas/GrantsMap")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid user"),
|
||||
* @OA\Response(response=401, description="Unauthorized")
|
||||
* )
|
||||
*/
|
||||
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../../config/config.php';
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
<?php
|
||||
// public/api/admin/acl/saveGrants.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/admin/acl/saveGrants.php",
|
||||
* summary="Save ACL grants (single-user or batch)",
|
||||
* tags={"Admin","ACL"},
|
||||
* security={{"cookieAuth":{}}},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* description="Either {user,grants} or {changes:[{user,grants}]}",
|
||||
* @OA\JsonContent(oneOf={
|
||||
* @OA\Schema(ref="#/components/schemas/SaveGrantsSingle"),
|
||||
* @OA\Schema(ref="#/components/schemas/SaveGrantsBatch")
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Saved"),
|
||||
* @OA\Response(response=400, description="Invalid payload"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Invalid CSRF")
|
||||
* )
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../../config/config.php';
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
<?php
|
||||
// public/api/admin/getConfig.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/admin/getConfig.php",
|
||||
* tags={"Admin"},
|
||||
* summary="Get UI configuration",
|
||||
* description="Returns a public subset for everyone; authenticated admins receive additional loginOptions fields.",
|
||||
* operationId="getAdminConfig",
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Configuration loaded",
|
||||
* @OA\JsonContent(
|
||||
* oneOf={
|
||||
* @OA\Schema(ref="#/components/schemas/AdminGetConfigPublic"),
|
||||
* @OA\Schema(ref="#/components/schemas/AdminGetConfigAdmin")
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=500, description="Server error")
|
||||
* )
|
||||
*
|
||||
* Retrieves the admin configuration settings and outputs JSON.
|
||||
* @return void
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
<?php
|
||||
// public/api/admin/readMetadata.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/admin/readMetadata.php",
|
||||
* summary="Read share metadata JSON",
|
||||
* description="Admin-only: returns the cleaned metadata for file or folder share links.",
|
||||
* tags={"Admin"},
|
||||
* operationId="readMetadata",
|
||||
* security={{"cookieAuth":{}}},
|
||||
* @OA\Parameter(
|
||||
* name="file",
|
||||
* in="query",
|
||||
* required=true,
|
||||
* description="Which metadata file to read",
|
||||
* @OA\Schema(type="string", enum={"share_links.json","share_folder_links.json"})
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="OK",
|
||||
* @OA\JsonContent(oneOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ShareLinksMap"),
|
||||
* @OA\Schema(ref="#/components/schemas/ShareFolderLinksMap")
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Missing or invalid file param"),
|
||||
* @OA\Response(response=403, description="Forbidden (admin only)"),
|
||||
* @OA\Response(response=500, description="Corrupted JSON")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
// Only admins may read these
|
||||
|
||||
@@ -1,6 +1,45 @@
|
||||
<?php
|
||||
// public/api/admin/updateConfig.php
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/admin/updateConfig.php",
|
||||
* summary="Update admin configuration",
|
||||
* description="Merges the provided settings into the on-disk configuration and persists them. Requires an authenticated admin session and a valid CSRF token. When OIDC is enabled (disableOIDCLogin=false), `providerUrl`, `redirectUri`, and `clientId` are required and must be HTTPS (HTTP allowed only for localhost).",
|
||||
* operationId="updateAdminConfig",
|
||||
* tags={"Admin"},
|
||||
* security={ {{"cookieAuth": {}, "CsrfHeader": {}}} },
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(ref="#/components/schemas/AdminUpdateConfigRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Configuration updated",
|
||||
* @OA\JsonContent(ref="#/components/schemas/SimpleSuccess")
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Validation error (e.g., bad authHeaderName, missing OIDC fields when enabled, or negative upload limit)",
|
||||
* @OA\JsonContent(ref="#/components/schemas/SimpleError")
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Unauthorized access or invalid CSRF token",
|
||||
* @OA\JsonContent(ref="#/components/schemas/SimpleError")
|
||||
* // or: ref to the reusable response
|
||||
* // ref="#/components/responses/Forbidden"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="Server error while loading or saving configuration",
|
||||
* @OA\JsonContent(ref="#/components/schemas/SimpleError")
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,52 @@
|
||||
<?php
|
||||
// public/api/auth/auth.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/auth/auth.php",
|
||||
* summary="Authenticate user",
|
||||
* description="Handles user authentication via OIDC or form-based credentials. For OIDC flows, processes callbacks; otherwise, performs standard authentication with optional TOTP verification.",
|
||||
* operationId="authUser",
|
||||
* tags={"Auth"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"username", "password"},
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="password", type="string", example="secretpassword"),
|
||||
* @OA\Property(property="remember_me", type="boolean", example=true),
|
||||
* @OA\Property(property="totp_code", type="string", example="123456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Login successful; returns user info and status",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="string", example="ok"),
|
||||
* @OA\Property(property="success", type="string", example="Login successful"),
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="isAdmin", type="boolean", example=true)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request (e.g., missing credentials)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized (e.g., invalid credentials, too many attempts)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=429,
|
||||
* description="Too many failed login attempts"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Handles user authentication via OIDC or form-based login.
|
||||
*
|
||||
* @return void Redirects on success or outputs JSON error.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
<?php
|
||||
// public/api/auth/checkAuth.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/auth/checkAuth.php",
|
||||
* summary="Check authentication status",
|
||||
* operationId="checkAuth",
|
||||
* tags={"Auth"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Authenticated status or setup flag",
|
||||
* @OA\JsonContent(
|
||||
* oneOf={
|
||||
* @OA\Schema(
|
||||
* type="object",
|
||||
* @OA\Property(property="authenticated", type="boolean", example=true),
|
||||
* @OA\Property(property="isAdmin", type="boolean", example=true),
|
||||
* @OA\Property(property="totp_enabled", type="boolean", example=false),
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="folderOnly", type="boolean", example=false)
|
||||
* ),
|
||||
* @OA\Schema(
|
||||
* type="object",
|
||||
* @OA\Property(property="setup", type="boolean", example=true)
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
<?php
|
||||
// public/api/auth/login_basic.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/auth/login_basic.php",
|
||||
* summary="Authenticate using HTTP Basic Authentication",
|
||||
* description="Performs HTTP Basic authentication. If credentials are missing, sends a 401 response prompting for Basic auth. On valid credentials, optionally handles TOTP verification and finalizes session login.",
|
||||
* operationId="loginBasic",
|
||||
* tags={"Auth"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Login successful; redirects to index.html",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="success", type="string", example="Login successful")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized due to missing credentials or invalid credentials."
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Handles HTTP Basic authentication (with optional TOTP) and logs the user in.
|
||||
*
|
||||
* @return void Redirects on success or sends a 401 header.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
<?php
|
||||
// public/api/auth/logout.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/auth/logout.php",
|
||||
* summary="Logout user",
|
||||
* description="Clears the session, removes persistent login tokens, and redirects the user to the login page.",
|
||||
* operationId="logoutUser",
|
||||
* tags={"Auth"},
|
||||
* @OA\Response(
|
||||
* response=302,
|
||||
* description="Redirects to the login page with a logout flag."
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Logs the user out by clearing session data, removing persistent tokens, and destroying the session.
|
||||
*
|
||||
* @return void Redirects to index.html with a logout flag.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
<?php
|
||||
// public/api/auth/token.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/auth/token.php",
|
||||
* summary="Retrieve CSRF token and share URL",
|
||||
* description="Returns the current CSRF token along with the configured share URL.",
|
||||
* operationId="getToken",
|
||||
* tags={"Auth"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="CSRF token and share URL",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="csrf_token", type="string", example="0123456789abcdef..."),
|
||||
* @OA\Property(property="share_url", type="string", example="https://yourdomain.com/share.php")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Returns the CSRF token and share URL.
|
||||
*
|
||||
* @return void Outputs the JSON response.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,44 @@
|
||||
<?php
|
||||
// public/api/changePassword.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/changePassword.php",
|
||||
* summary="Change user password",
|
||||
* description="Allows an authenticated user to change their password by verifying the old password and updating to a new one.",
|
||||
* operationId="changePassword",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"oldPassword", "newPassword", "confirmPassword"},
|
||||
* @OA\Property(property="oldPassword", type="string", example="oldpass123"),
|
||||
* @OA\Property(property="newPassword", type="string", example="newpass456"),
|
||||
* @OA\Property(property="confirmPassword", type="string", example="newpass456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Password updated successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="Password updated successfully.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
<?php
|
||||
// public/api/file/copyFiles.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/copyFiles.php",
|
||||
* summary="Copy files between folders",
|
||||
* description="Requires read access on source and write access on destination. Enforces folder scope and ownership.",
|
||||
* operationId="copyFiles",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(
|
||||
* name="X-CSRF-Token", in="header", required=true,
|
||||
* description="CSRF token from the current session",
|
||||
* @OA\Schema(type="string")
|
||||
* ),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"source","destination","files"},
|
||||
* @OA\Property(property="source", type="string", example="root"),
|
||||
* @OA\Property(property="destination", type="string", example="userA/projects"),
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"report.pdf","notes.txt"})
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Copy result (model-defined)"),
|
||||
* @OA\Response(response=400, description="Invalid request or folder name"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
<?php
|
||||
// public/api/file/createFile.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/createFile.php",
|
||||
* summary="Create an empty file",
|
||||
* description="Requires write access on the target folder. Enforces folder-only scope.",
|
||||
* operationId="createFile",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","name"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="name", type="string", example="new.txt")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Creation result (model-defined)"),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
<?php
|
||||
// public/api/file/createShareLink.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/createShareLink.php",
|
||||
* 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",
|
||||
* tags={"Shares"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","file"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="file", type="string", example="invoice.pdf"),
|
||||
* @OA\Property(property="expirationValue", type="integer", example=60),
|
||||
* @OA\Property(property="expirationUnit", type="string", enum={"seconds","minutes","hours","days"}, example="minutes"),
|
||||
* @OA\Property(property="password", type="string", example="")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Share link created",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="token", type="string", example="abc123"),
|
||||
* @OA\Property(property="url", type="string", example="/api/file/share.php?token=abc123"),
|
||||
* @OA\Property(property="expires", type="integer", example=1700000000)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
<?php
|
||||
// public/api/file/deleteFiles.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/deleteFiles.php",
|
||||
* summary="Delete files to Trash",
|
||||
* description="Requires write access on the folder and (for non-admins) ownership of the files.",
|
||||
* operationId="deleteFiles",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(
|
||||
* name="X-CSRF-Token", in="header", required=true,
|
||||
* @OA\Schema(type="string")
|
||||
* ),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","files"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"old.docx","draft.md"})
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Delete result (model-defined)"),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/deleteShareLink.php",
|
||||
* summary="Delete a share link by token",
|
||||
* description="Deletes a share token. NOTE: Current implementation does not require authentication.",
|
||||
* operationId="deleteShareLink",
|
||||
* tags={"Shares"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"token"},
|
||||
* @OA\Property(property="token", type="string", example="abc123")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Deletion result (success or not found)")
|
||||
* )
|
||||
*/
|
||||
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
<?php
|
||||
// public/api/file/deleteTrashFiles.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/deleteTrashFiles.php",
|
||||
* summary="Permanently delete Trash items (admin only)",
|
||||
* operationId="deleteTrashFiles",
|
||||
* tags={"Trash"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* oneOf={
|
||||
* @OA\Schema(
|
||||
* required={"deleteAll"},
|
||||
* @OA\Property(property="deleteAll", type="boolean", example=true)
|
||||
* ),
|
||||
* @OA\Schema(
|
||||
* required={"files"},
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"trash/abc","trash/def"})
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Deletion result (model-defined)"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Admin only"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
<?php
|
||||
// public/api/file/download.php
|
||||
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/download.php",
|
||||
* summary="Download a file",
|
||||
* description="Requires view access (or own-only with ownership). Streams the file with appropriate Content-Type.",
|
||||
* operationId="downloadFile",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="folder", in="query", required=true, @OA\Schema(type="string"), example="root"),
|
||||
* @OA\Parameter(name="file", in="query", required=true, @OA\Schema(type="string"), example="photo.jpg"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Binary file",
|
||||
* content={
|
||||
* "application/octet-stream": @OA\MediaType(
|
||||
* mediaType="application/octet-stream",
|
||||
* @OA\Schema(type="string", format="binary")
|
||||
* )
|
||||
* }
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid folder/file"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=404, description="Not found")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,41 @@
|
||||
<?php
|
||||
// public/api/file/downloadZip.php
|
||||
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/downloadZip.php",
|
||||
* summary="Download multiple files as a ZIP",
|
||||
* description="Requires view access (or own-only with ownership). May be gated by account flag.",
|
||||
* operationId="downloadZip",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","files"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"a.jpg","b.png"})
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="ZIP archive",
|
||||
* content={
|
||||
* "application/zip": @OA\MediaType(
|
||||
* mediaType="application/zip",
|
||||
* @OA\Schema(type="string", format="binary")
|
||||
* )
|
||||
* }
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
<?php
|
||||
// public/api/file/extractZip.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/extractZip.php",
|
||||
* summary="Extract ZIP file(s) into a folder",
|
||||
* description="Requires write access on the target folder.",
|
||||
* operationId="extractZip",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","files"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"archive.zip"})
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Extraction result (model-defined)"),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
<?php
|
||||
// public/api/file/getFileList.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/getFileList.php",
|
||||
* summary="List files in a folder",
|
||||
* description="Requires view access (full) or read_own (own-only results).",
|
||||
* operationId="getFileList",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="folder", in="query", required=true, @OA\Schema(type="string"), example="root"),
|
||||
* @OA\Response(response=200, description="Listing result (model-defined JSON)"),
|
||||
* @OA\Response(response=400, description="Invalid folder"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<?php
|
||||
// public/api/file/getFileTag.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/getFileTags.php",
|
||||
* summary="Get global file tags",
|
||||
* description="Returns tag metadata (no auth in current implementation).",
|
||||
* operationId="getFileTags",
|
||||
* tags={"Tags"},
|
||||
* @OA\Response(response=200, description="Tags map (model-defined JSON)")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/getShareLinks.php",
|
||||
* summary="Get (raw) share links file",
|
||||
* description="Returns the full share links JSON (no auth in current implementation).",
|
||||
* operationId="getShareLinks",
|
||||
* tags={"Shares"},
|
||||
* @OA\Response(response=200, description="Share links (model-defined JSON)")
|
||||
* )
|
||||
*/
|
||||
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<?php
|
||||
// public/api/file/getTrashItems.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/getTrashItems.php",
|
||||
* summary="List items in Trash (admin only)",
|
||||
* operationId="getTrashItems",
|
||||
* tags={"Trash"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Response(response=200, description="Trash contents (model-defined JSON)"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Admin only"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<?php
|
||||
// public/api/file/moveFiles.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/moveFiles.php",
|
||||
* operationId="moveFiles",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth":{}}},
|
||||
* @OA\RequestBody(ref="#/components/requestBodies/MoveFilesRequest"),
|
||||
* @OA\Response(response=200, description="Moved"),
|
||||
* @OA\Response(response=400, description="Bad Request"),
|
||||
* @OA\Response(response=401, ref="#/components/responses/Unauthorized"),
|
||||
* @OA\Response(response=403, ref="#/components/responses/Forbidden")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
<?php
|
||||
// public/api/file/renameFile.php
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/file/renameFile.php",
|
||||
* summary="Rename a file",
|
||||
* description="Requires write access; non-admins must own the file.",
|
||||
* operationId="renameFile",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","oldName","newName"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="oldName", type="string", example="old.pdf"),
|
||||
* @OA\Property(property="newName", type="string", example="new.pdf")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Rename result (model-defined)"),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
<?php
|
||||
// public/api/file/restoreFiles.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/restoreFiles.php",
|
||||
* summary="Restore files from Trash (admin only)",
|
||||
* operationId="restoreFiles",
|
||||
* tags={"Trash"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"files"},
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"trash/12345.json"})
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Restore result (model-defined)"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Admin only"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
<?php
|
||||
// public/api/file/saveFile.php
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/file/saveFile.php",
|
||||
* 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",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","fileName","content"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="fileName", type="string", example="readme.txt"),
|
||||
* @OA\Property(property="content", type="string", example="Hello world")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Save result (model-defined)"),
|
||||
* @OA\Response(response=400, description="Invalid input or disallowed extension"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
<?php
|
||||
// public/api/file/saveFileTag.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/saveFileTag.php",
|
||||
* summary="Save tags for a file (or delete one)",
|
||||
* description="Requires write access and (for non-admins) ownership when modifying.",
|
||||
* operationId="saveFileTag",
|
||||
* tags={"Tags"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","file"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="file", type="string", example="doc.md"),
|
||||
* @OA\Property(property="tags", type="array", @OA\Items(type="string"), example={"work","urgent"}),
|
||||
* @OA\Property(property="deleteGlobal", type="boolean", example=false),
|
||||
* @OA\Property(property="tagToDelete", type="string", nullable=true, example=null)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Save result (model-defined)"),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=500, description="Internal error")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
<?php
|
||||
// public/api/file/share.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/share.php",
|
||||
* 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",
|
||||
* tags={"Shares"},
|
||||
* @OA\Parameter(name="token", in="query", required=true, @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="pass", in="query", required=false, @OA\Schema(type="string")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Binary file (or HTML password form when missing password)",
|
||||
* content={
|
||||
* "application/octet-stream": @OA\MediaType(
|
||||
* mediaType="application/octet-stream",
|
||||
* @OA\Schema(type="string", format="binary")
|
||||
* ),
|
||||
* "text/html": @OA\MediaType(mediaType="text/html")
|
||||
* }
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Missing token / invalid input"),
|
||||
* @OA\Response(response=403, description="Expired or invalid password"),
|
||||
* @OA\Response(response=404, description="Not found")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
|
||||
@@ -1,5 +1,57 @@
|
||||
<?php
|
||||
// public/api/folder/capabilities.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/folder/capabilities.php",
|
||||
* 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",
|
||||
* tags={"Folders"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(
|
||||
* name="folder",
|
||||
* in="query",
|
||||
* required=false,
|
||||
* description="Target folder path. Defaults to 'root'. Supports nested paths like 'team/reports'.",
|
||||
* @OA\Schema(type="string"),
|
||||
* example="projects/acme"
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Capabilities computed successfully.",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* required={"user","folder","isAdmin","flags","canView","canUpload","canCreate","canRename","canDelete","canMoveIn","canShare"},
|
||||
* @OA\Property(property="user", type="string", example="alice"),
|
||||
* @OA\Property(property="folder", type="string", example="projects/acme"),
|
||||
* @OA\Property(property="isAdmin", type="boolean", example=false),
|
||||
* @OA\Property(
|
||||
* property="flags",
|
||||
* type="object",
|
||||
* required={"folderOnly","readOnly","disableUpload"},
|
||||
* @OA\Property(property="folderOnly", type="boolean", example=false),
|
||||
* @OA\Property(property="readOnly", type="boolean", example=false),
|
||||
* @OA\Property(property="disableUpload", type="boolean", example=false)
|
||||
* ),
|
||||
* @OA\Property(property="owner", type="string", nullable=true, example="alice"),
|
||||
* @OA\Property(property="canView", type="boolean", example=true, description="User can view items in this folder."),
|
||||
* @OA\Property(property="canUpload", type="boolean", example=true, description="User can upload/edit/rename/move/delete items (i.e., WRITE)."),
|
||||
* @OA\Property(property="canCreate", type="boolean", example=true, description="User can create subfolders here."),
|
||||
* @OA\Property(property="canRename", type="boolean", example=true, description="User can rename items here."),
|
||||
* @OA\Property(property="canDelete", type="boolean", example=true, description="User can delete items here."),
|
||||
* @OA\Property(property="canMoveIn", type="boolean", example=true, description="User can move items into this folder."),
|
||||
* @OA\Property(property="canShare", type="boolean", example=false, description="User can create share links for this folder.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid folder name."),
|
||||
* @OA\Response(response=401, ref="#/components/responses/Unauthorized")
|
||||
* )
|
||||
*/
|
||||
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
<?php
|
||||
// public/api/folder/createFolder.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/folder/createFolder.php",
|
||||
* summary="Create a new folder",
|
||||
* description="Requires authentication, CSRF token, and write access to the parent folder. Seeds ACL owner.",
|
||||
* operationId="createFolder",
|
||||
* tags={"Folders"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(
|
||||
* name="X-CSRF-Token", in="header", required=true,
|
||||
* description="CSRF token from the current session",
|
||||
* @OA\Schema(type="string")
|
||||
* ),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folderName"},
|
||||
* @OA\Property(property="folderName", type="string", example="reports"),
|
||||
* @OA\Property(property="parent", type="string", nullable=true, example="root",
|
||||
* description="Parent folder (default root)")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Creation result (model-defined JSON)"),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=405, description="Method not allowed")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
<?php
|
||||
// public/api/folder/createShareFolderLink.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/folder/createShareFolderLink.php",
|
||||
* 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",
|
||||
* tags={"Shared Folders"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder"},
|
||||
* @OA\Property(property="folder", type="string", example="team/reports"),
|
||||
* @OA\Property(property="expirationValue", type="integer", example=60),
|
||||
* @OA\Property(property="expirationUnit", type="string", enum={"seconds","minutes","hours","days"}, example="minutes"),
|
||||
* @OA\Property(property="password", type="string", example=""),
|
||||
* @OA\Property(property="allowUpload", type="integer", enum={0,1}, example=0)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Share folder link created",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="token", type="string", example="sf_abc123"),
|
||||
* @OA\Property(property="url", type="string", example="/api/folder/shareFolder.php?token=sf_abc123"),
|
||||
* @OA\Property(property="expires", type="integer", example=1700000000)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
<?php
|
||||
// public/api/folder/deleteFolder.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/folder/deleteFolder.php",
|
||||
* summary="Delete a folder",
|
||||
* description="Requires authentication, CSRF token, write scope, and (for non-admins) folder ownership.",
|
||||
* operationId="deleteFolder",
|
||||
* tags={"Folders"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder"},
|
||||
* @OA\Property(property="folder", type="string", example="userA/reports")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Deletion result (model-defined JSON)"),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=405, description="Method not allowed")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/folder/deleteShareFolderLink.php",
|
||||
* summary="Delete a shared-folder link by token (admin only)",
|
||||
* description="Requires authentication, CSRF token, and admin privileges.",
|
||||
* operationId="deleteShareFolderLink",
|
||||
* tags={"Shared Folders","Admin"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"token"},
|
||||
* @OA\Property(property="token", type="string", example="sf_abc123")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Deleted"),
|
||||
* @OA\Response(response=400, description="No token provided"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Admin only"),
|
||||
* @OA\Response(response=404, description="Not found")
|
||||
* )
|
||||
*/
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
<?php
|
||||
// public/api/folder/downloadSharedFile.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/folder/downloadSharedFile.php",
|
||||
* summary="Download a file from a shared folder (by token)",
|
||||
* description="Public endpoint; validates token and file name, then streams the file.",
|
||||
* operationId="downloadSharedFile",
|
||||
* tags={"Shared Folders"},
|
||||
* @OA\Parameter(name="token", in="query", required=true, @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="file", in="query", required=true, @OA\Schema(type="string"), example="report.pdf"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Binary file",
|
||||
* content={
|
||||
* "application/octet-stream": @OA\MediaType(
|
||||
* mediaType="application/octet-stream",
|
||||
* @OA\Schema(type="string", format="binary")
|
||||
* )
|
||||
* }
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=404, description="Not found")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,38 @@
|
||||
<?php
|
||||
// public/api/folder/getFolderList.php
|
||||
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/folder/getFolderList.php",
|
||||
* 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",
|
||||
* tags={"Folders"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(
|
||||
* name="folder", in="query", required=false,
|
||||
* description="Parent folder to include and descend (default all); use 'root' for top-level",
|
||||
* @OA\Schema(type="string"), example="root"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="List of folders",
|
||||
* @OA\JsonContent(
|
||||
* type="array",
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
* @OA\Property(property="folder", type="string", example="team/reports"),
|
||||
* @OA\Property(property="fileCount", type="integer", example=12),
|
||||
* @OA\Property(property="metadataFile", type="string", example="/path/to/meta.json")
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid folder"),
|
||||
* @OA\Response(response=401, description="Unauthorized")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/folder/getShareFolderLinks.php",
|
||||
* summary="List active shared-folder links (admin only)",
|
||||
* description="Returns all non-expired shared-folder links. Admin-only.",
|
||||
* operationId="getShareFolderLinks",
|
||||
* tags={"Shared Folders","Admin"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Response(response=200, description="Active share-folder links (model-defined JSON)"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Admin only")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
<?php
|
||||
// public/api/folder/renameFolder.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/folder/renameFolder.php",
|
||||
* 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",
|
||||
* tags={"Folders"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"oldFolder","newFolder"},
|
||||
* @OA\Property(property="oldFolder", type="string", example="team/q1"),
|
||||
* @OA\Property(property="newFolder", type="string", example="team/quarter-1")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Rename result (model-defined JSON)"),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=403, description="Forbidden"),
|
||||
* @OA\Response(response=405, description="Method not allowed")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
<?php
|
||||
// public/api/folder/shareFolder.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/folder/shareFolder.php",
|
||||
* 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",
|
||||
* tags={"Shared Folders"},
|
||||
* @OA\Parameter(name="token", in="query", required=true, @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="pass", in="query", required=false, @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", minimum=1), example=1),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="HTML page (password form or folder listing)",
|
||||
* content={"text/html": @OA\MediaType(mediaType="text/html")}
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Missing/invalid token"),
|
||||
* @OA\Response(response=403, description="Forbidden or wrong password")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
<?php
|
||||
// public/api/folder/uploadToSharedFolder.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/folder/uploadToSharedFolder.php",
|
||||
* 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",
|
||||
* tags={"Shared Folders"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* content={
|
||||
* "multipart/form-data": @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
* @OA\Schema(
|
||||
* type="object",
|
||||
* required={"token","fileToUpload"},
|
||||
* @OA\Property(property="token", type="string", description="Share token"),
|
||||
* @OA\Property(property="fileToUpload", type="string", format="binary", description="File to upload")
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* ),
|
||||
* @OA\Response(response=302, description="Redirect to /api/folder/shareFolder.php?token=..."),
|
||||
* @OA\Response(response=400, description="Upload error or invalid input"),
|
||||
* @OA\Response(response=405, description="Method not allowed")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
<?php
|
||||
// public/api/getUserPermissions.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/getUserPermissions.php",
|
||||
* summary="Retrieve user permissions",
|
||||
* description="Returns the permissions for the current user, or all permissions if the user is an admin.",
|
||||
* operationId="getUserPermissions",
|
||||
* tags={"Users"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Successful response with user permissions",
|
||||
* @OA\JsonContent(type="object")
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
<?php
|
||||
// public/api/getUsers.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/getUsers.php",
|
||||
* summary="Retrieve a list of users",
|
||||
* description="Returns a JSON array of users. Only available to authenticated admin users.",
|
||||
* operationId="getUsers",
|
||||
* tags={"Users"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Successful response with an array of users",
|
||||
* @OA\JsonContent(
|
||||
* type="array",
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="role", type="string", example="admin")
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized: the user is not authenticated or is not an admin"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,4 +1,29 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/profile/getCurrentUser.php",
|
||||
* operationId="getCurrentUser",
|
||||
* tags={"Users"},
|
||||
* security={{"cookieAuth":{}}},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Current user",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* required={"username","isAdmin","totp_enabled","profile_picture"},
|
||||
* @OA\Property(property="username", type="string", example="ryan"),
|
||||
* @OA\Property(property="isAdmin", type="boolean"),
|
||||
* @OA\Property(property="totp_enabled", type="boolean"),
|
||||
* @OA\Property(property="profile_picture", type="string", example="/uploads/profile_pics/ryan.png")
|
||||
* // If you had an array: @OA\Property(property="roles", type="array", @OA\Items(type="string"))
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=401, ref="#/components/responses/Unauthorized")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||
|
||||
|
||||
@@ -2,6 +2,57 @@
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/profile/uploadPicture.php",
|
||||
* 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",
|
||||
* tags={"Users"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(
|
||||
* name="X-CSRF-Token",
|
||||
* in="header",
|
||||
* required=true,
|
||||
* description="Anti-CSRF token associated with the current session.",
|
||||
* @OA\Schema(type="string")
|
||||
* ),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
* @OA\Schema(
|
||||
* required={"profile_picture"},
|
||||
* @OA\Property(
|
||||
* property="profile_picture",
|
||||
* type="string",
|
||||
* format="binary",
|
||||
* description="JPEG, PNG, or GIF image. Max size: 2 MB."
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Profile picture updated.",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* required={"success","url"},
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="url", type="string", example="/uploads/profile_pics/alice_9f3c2e1a8bcd.png")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=400, description="No file uploaded, invalid file type, or file too large."),
|
||||
* @OA\Response(response=401, ref="#/components/responses/Unauthorized"),
|
||||
* @OA\Response(response=403, ref="#/components/responses/Forbidden"),
|
||||
* @OA\Response(response=500, description="Server error while saving the picture.")
|
||||
* )
|
||||
*/
|
||||
|
||||
// Always JSON, even on PHP notices
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
<?php
|
||||
// public/api/removeUser.php
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/removeUser.php",
|
||||
* summary="Remove a user",
|
||||
* description="Removes the specified user from the system. Cannot remove the currently logged-in user.",
|
||||
* operationId="removeUser",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"username"},
|
||||
* @OA\Property(property="username", type="string", example="johndoe")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User removed successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="User removed successfully")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
<?php
|
||||
// public/api/totp_disable.php
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/totp_disable.php",
|
||||
* summary="Disable TOTP for the authenticated user",
|
||||
* description="Clears the TOTP secret from the users file for the current user.",
|
||||
* operationId="disableTOTP",
|
||||
* tags={"TOTP"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="TOTP disabled successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="TOTP disabled successfully.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Not authenticated or invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="Failed to disable TOTP"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
@@ -1,6 +1,46 @@
|
||||
<?php
|
||||
// public/api/totp_recover.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/totp_recover.php",
|
||||
* summary="Recover TOTP",
|
||||
* description="Verifies a recovery code to disable TOTP and finalize login.",
|
||||
* operationId="recoverTOTP",
|
||||
* tags={"TOTP"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"recovery_code"},
|
||||
* @OA\Property(property="recovery_code", type="string", example="ABC123DEF456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Recovery successful",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="string", example="ok")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Invalid input or recovery code"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=405,
|
||||
* description="Method not allowed"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=429,
|
||||
* description="Too many attempts"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
<?php
|
||||
// public/api/totp_saveCode.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/totp_saveCode.php",
|
||||
* summary="Generate and save a new TOTP recovery code",
|
||||
* description="Generates a new TOTP recovery code for the authenticated user, stores its hash, and returns the plain text recovery code.",
|
||||
* operationId="totpSaveCode",
|
||||
* tags={"TOTP"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Recovery code generated successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="string", example="ok"),
|
||||
* @OA\Property(property="recoveryCode", type="string", example="ABC123DEF456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token or unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=405,
|
||||
* description="Method not allowed"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
<?php
|
||||
// public/api/totp_setup.php
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/totp_setup.php",
|
||||
* summary="Set up TOTP and generate a QR code",
|
||||
* description="Generates (or retrieves) the TOTP secret for the user and builds a QR code image for scanning.",
|
||||
* operationId="setupTOTP",
|
||||
* tags={"TOTP"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="QR code image for TOTP setup",
|
||||
* @OA\MediaType(
|
||||
* mediaType="image/png"
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Unauthorized or invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="Server error"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
@@ -1,6 +1,43 @@
|
||||
<?php
|
||||
// public/api/totp_verify.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/totp_verify.php",
|
||||
* summary="Verify TOTP code",
|
||||
* description="Verifies a TOTP code and completes login for pending users or validates TOTP for setup verification.",
|
||||
* operationId="verifyTOTP",
|
||||
* tags={"TOTP"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"totp_code"},
|
||||
* @OA\Property(property="totp_code", type="string", example="123456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="TOTP successfully verified",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="string", example="ok"),
|
||||
* @OA\Property(property="message", type="string", example="Login successful")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request (e.g., invalid input)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Not authenticated or invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=429,
|
||||
* description="Too many attempts. Try again later."
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
<?php
|
||||
// public/api/updateUserPanel.php
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/updateUserPanel.php",
|
||||
* summary="Update user panel settings",
|
||||
* description="Updates user panel settings by disabling TOTP when not enabled. Accessible to authenticated users.",
|
||||
* operationId="updateUserPanel",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"totp_enabled"},
|
||||
* @OA\Property(property="totp_enabled", type="boolean", example=false)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User panel updated successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="User panel updated: TOTP disabled")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,52 @@
|
||||
<?php
|
||||
// public/api/updateUserPermissions.php
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/updateUserPermissions.php",
|
||||
* summary="Update user permissions",
|
||||
* description="Updates permissions for users. Only available to authenticated admin users.",
|
||||
* operationId="updateUserPermissions",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"permissions"},
|
||||
* @OA\Property(
|
||||
* property="permissions",
|
||||
* type="array",
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="folderOnly", type="boolean", example=true),
|
||||
* @OA\Property(property="readOnly", type="boolean", example=false),
|
||||
* @OA\Property(property="disableUpload", type="boolean", example=false)
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User permissions updated successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="User permissions updated successfully.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
<?php
|
||||
// public/api/upload/removeChunks.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/upload/removeChunks.php",
|
||||
* 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",
|
||||
* tags={"Uploads"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder"},
|
||||
* @OA\Property(property="folder", type="string", example="resumable_myupload123"),
|
||||
* @OA\Property(property="csrf_token", type="string", description="CSRF token for this session")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Removal result",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="Temporary folder removed.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=403, description="Invalid CSRF token")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
|
||||
@@ -1,5 +1,84 @@
|
||||
<?php
|
||||
// public/api/upload/upload.php
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/upload/upload.php",
|
||||
* summary="Upload a file (supports chunked + full uploads)",
|
||||
* description="Requires a session (cookie) and a CSRF token (header preferred; falls back to form field). Checks user/account flags and folder-level WRITE ACL, then delegates to the model. Returns JSON for chunked uploads; full uploads may redirect after success.",
|
||||
* operationId="handleUpload",
|
||||
* tags={"Uploads"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(
|
||||
* name="X-CSRF-Token", in="header", required=false,
|
||||
* description="CSRF token for this session (preferred). If omitted, send as form field `csrf_token`.",
|
||||
* @OA\Schema(type="string")
|
||||
* ),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* content={
|
||||
* "multipart/form-data": @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
* @OA\Schema(
|
||||
* type="object",
|
||||
* required={"fileToUpload"},
|
||||
* @OA\Property(
|
||||
* property="fileToUpload", type="string", format="binary",
|
||||
* description="File or chunk payload."
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="folder", type="string", example="root",
|
||||
* description="Target folder (defaults to 'root' if omitted)."
|
||||
* ),
|
||||
* @OA\Property(property="csrf_token", type="string", description="CSRF token (form fallback)."),
|
||||
* @OA\Property(property="upload_token", type="string", description="Legacy alias for CSRF token (accepted by server)."),
|
||||
* @OA\Property(property="resumableChunkNumber", type="integer"),
|
||||
* @OA\Property(property="resumableTotalChunks", type="integer"),
|
||||
* @OA\Property(property="resumableChunkSize", type="integer"),
|
||||
* @OA\Property(property="resumableCurrentChunkSize", type="integer"),
|
||||
* @OA\Property(property="resumableTotalSize", type="integer"),
|
||||
* @OA\Property(property="resumableType", type="string"),
|
||||
* @OA\Property(property="resumableIdentifier", type="string"),
|
||||
* @OA\Property(property="resumableFilename", type="string"),
|
||||
* @OA\Property(property="resumableRelativePath", type="string")
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="JSON result (success, chunk status, or CSRF refresh).",
|
||||
* @OA\JsonContent(
|
||||
* oneOf={
|
||||
* @OA\Schema( ; Success (full or model-returned)
|
||||
* type="object",
|
||||
* @OA\Property(property="success", type="string", example="File uploaded successfully"),
|
||||
* @OA\Property(property="newFilename", type="string", example="5f2d7c123a_example.png")
|
||||
* ),
|
||||
* @OA\Schema( ; Chunk flow
|
||||
* type="object",
|
||||
* @OA\Property(property="status", type="string", example="chunk uploaded")
|
||||
* ),
|
||||
* @OA\Schema( ; CSRF soft-refresh path
|
||||
* type="object",
|
||||
* @OA\Property(property="csrf_expired", type="boolean", example=true),
|
||||
* @OA\Property(property="csrf_token", type="string", example="b1c2...f9")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=302,
|
||||
* description="Redirect after a successful full upload.",
|
||||
* @OA\Header(header="Location", description="Where the client is redirected", @OA\Schema(type="string"))
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Bad request (missing/invalid fields, model error)"),
|
||||
* @OA\Response(response=401, description="Unauthorized (no session)"),
|
||||
* @OA\Response(response=403, description="Forbidden (upload disabled or no WRITE to folder)"),
|
||||
* @OA\Response(response=500, description="Server error while processing upload")
|
||||
* )
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
|
||||
@@ -2305,4 +2305,7 @@ body.dark-mode .user-dropdown .user-menu .item:hover {
|
||||
.folder-strip-container .folder-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
:root { --perm-caret: #444; } /* light */
|
||||
body.dark-mode { --perm-caret: #ccc; } /* dark */
|
||||
@@ -4,7 +4,7 @@ import { loadAdminConfigFunc } from './auth.js';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
|
||||
const version = "v1.5.1";
|
||||
const version = "v1.5.3";
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||
|
||||
// Translate with fallback: if t(key) just echos the key, use a readable string.
|
||||
@@ -340,14 +340,14 @@ export function openAdminPanel() {
|
||||
<h3>${adminTitle}</h3>
|
||||
<form id="adminPanelForm">
|
||||
${[
|
||||
{ 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 => `
|
||||
<div id="${sec.id}Header" class="section-header collapsed">
|
||||
${sec.label} <i class="material-icons">expand_more</i>
|
||||
</div>
|
||||
@@ -384,7 +384,7 @@ export function openAdminPanel() {
|
||||
<button type="button" id="adminOpenUserFlags" class="btn btn-secondary">${tf("user_permissions", "User Permissions")}</button>
|
||||
`;
|
||||
|
||||
|
||||
|
||||
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 = `
|
||||
<input type="text" class="form-control" style="max-width:220px;" placeholder="${tf('search_folders', 'Search folders')}" />
|
||||
<label class="muted"><input type="checkbox" data-bulk="view" /> ${tf('view_all','View (all)')}</label>
|
||||
<label class="muted"><input type="checkbox" data-bulk="viewOwn" /> ${tf('view_own','View (own)')}</label>
|
||||
<label class="muted"><input type="checkbox" data-bulk="upload" /> ${tf('upload','Upload')}</label>
|
||||
<label class="muted"><input type="checkbox" data-bulk="manage" /> ${tf('manage','Manage')}</label>
|
||||
<label class="muted"><input type="checkbox" data-bulk="share" /> ${tf('share','Share')}</label>
|
||||
<span class="muted">(${tf('applies_to_filtered','applies to filtered list')})</span>
|
||||
`;
|
||||
<input type="text" class="form-control" style="max-width:220px;" placeholder="${tf('search_folders', 'Search folders')}" />
|
||||
<label class="muted" title="${tf('view_all_help', 'See all files in this folder (everyone’s files)')}">
|
||||
<input type="checkbox" data-bulk="view" /> ${tf('view_all', 'View (all)')}
|
||||
</label>
|
||||
<label class="muted" title="${tf('view_own_help', 'See only files you uploaded in this folder')}">
|
||||
<input type="checkbox" data-bulk="viewOwn" /> ${tf('view_own', 'View (own)')}
|
||||
</label>
|
||||
<label class="muted" title="${tf('write_help', 'Create/upload files and edit/rename/move/delete items in this folder')}">
|
||||
<input type="checkbox" data-bulk="upload" /> ${tf('write_full', 'Write (upload/edit/delete)')}
|
||||
</label>
|
||||
<label class="muted" title="${tf('manage_help', 'Owner-level: can grant access; implies View (all) + Write + Share')}">
|
||||
<input type="checkbox" data-bulk="manage" /> ${tf('manage', 'Manage')}
|
||||
</label>
|
||||
<label class="muted" title="${tf('share_help', 'Create/manage share links; implies View (all)')}">
|
||||
<input type="checkbox" data-bulk="share" /> ${tf('share', 'Share')}
|
||||
</label>
|
||||
<span class="muted">(${tf('applies_to_filtered', 'applies to filtered list')})</span>
|
||||
`;
|
||||
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 = `
|
||||
<div class="folder-access-header">
|
||||
<div>${tf('folder', 'Folder')}</div>
|
||||
<div class="perm-col">${tf('view_all','View (all)')}</div>
|
||||
<div class="perm-col">${tf('view_own','View (own)')}</div>
|
||||
<div class="perm-col">${tf('upload','Upload')}</div>
|
||||
<div class="perm-col">${tf('manage','Manage')}</div>
|
||||
<div class="perm-col">${tf('share','Share')}</div>
|
||||
<div class="folder-access-header">
|
||||
<div title="${tf('folder_help', 'Folder path within FileRise')}">${tf('folder', 'Folder')}</div>
|
||||
<div class="perm-col" title="${tf('view_all_help', 'See all files in this folder (everyones files)')}">
|
||||
${tf('view_all', 'View (all)')}
|
||||
</div>
|
||||
`;
|
||||
<div class="perm-col" title="${tf('view_own_help', 'See only files you uploaded in this folder')}">
|
||||
${tf('view_own', 'View (own)')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('write_help', 'Create/upload files and edit/rename/move/delete items in this folder')}">
|
||||
${tf('write', 'Write')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('manage_help', 'Owner-level: can grant access; implies View (all) + Write + Share')}">
|
||||
${tf('manage', 'Manage')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('share_help', 'Create/manage share links; implies View (all)')}">
|
||||
${tf('share', 'Share')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function rowHtml(folder) {
|
||||
const g = grants[folder] || {};
|
||||
@@ -648,28 +670,28 @@ function renderFolderGrantsUI(username, container, folders, grants) {
|
||||
return `
|
||||
<div class="folder-access-row" data-folder="${folder}">
|
||||
<div class="folder-badge"><i class="material-icons" style="font-size:18px;">folder</i>${name}</div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="view" ${g.view ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="view" ${g.view ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="viewOwn" ${g.viewOwn ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="upload" ${g.upload ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="manage" ${g.manage ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="share" ${g.share ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="upload" ${g.upload ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="manage" ${g.manage ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="share" ${g.share ? 'checked' : ''}></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
<span id="closeUserPermissionsModal" class="editor-close-btn">×</span>
|
||||
<h3>${tf("folder_access", "Folder Access")}</h3>
|
||||
<div class="muted" style="margin:-4px 0 10px;">
|
||||
${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'.")}
|
||||
</div>
|
||||
<div id="userPermissionsList" style="max-height: 60vh; overflow-y: auto; margin-bottom: 15px;">
|
||||
<div id="userPermissionsList" style="max-height: 70vh; overflow-y: auto; margin-bottom: 15px;">
|
||||
<!-- User rows will load here -->
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
||||
@@ -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 `
|
||||
<tr data-username="${u.username}">
|
||||
<td><strong>${u.username}</strong></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-flag="readOnly" ${f.readOnly ? "checked":""}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-flag="disableUpload" ${f.disableUpload ? "checked":""}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-flag="canShare" ${f.canShare ? "checked":""}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-flag="bypassOwnership" ${f.bypassOwnership ? "checked":""}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-flag="readOnly" ${f.readOnly ? "checked" : ""}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-flag="disableUpload" ${f.disableUpload ? "checked" : ""}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-flag="canShare" ${f.canShare ? "checked" : ""}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-flag="bypassOwnership" ${f.bypassOwnership ? "checked" : ""}></td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="modal-content" style="background:#fff; color:#000; padding:16px; max-width:900px; width:95%; border-radius:8px; position:relative;">
|
||||
<span id="closeUserFlagsModal" class="editor-close-btn" style="right:8px; top:8px;">×</span>
|
||||
<div class="modal-content"
|
||||
style="background:${contentBg}; color:${contentFg};
|
||||
padding:16px; max-width:900px; width:95%;
|
||||
border-radius:8px; position:relative;
|
||||
border:1px solid ${borderCol};">
|
||||
<span id="closeUserFlagsModal"
|
||||
class="editor-close-btn"
|
||||
style="right:8px; top:8px;">×</span>
|
||||
|
||||
<h3>${tf("user_permissions", "User Permissions")}</h3>
|
||||
<p class="muted" style="margin-top:-6px;">
|
||||
${tf("user_flags_help", "Account-level switches. These are NOT per-folder grants.")}
|
||||
</p>
|
||||
<div id="userFlagsBody" style="max-height:60vh; overflow:auto; margin:8px 0;">
|
||||
|
||||
<div id="userFlagsBody"
|
||||
style="max-height:60vh; overflow:auto; margin:8px 0;">
|
||||
${t("loading")}…
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; gap:8px;">
|
||||
<button type="button" id="cancelUserFlags" class="btn btn-secondary">${t("cancel")}</button>
|
||||
<button type="button" id="saveUserFlags" class="btn btn-primary">${t("save_permissions")}</button>
|
||||
@@ -946,10 +987,21 @@ export async function openUserFlagsModal() {
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
document.getElementById("closeUserFlagsModal").onclick = () => modal.style.display = "none";
|
||||
document.getElementById("cancelUserFlags").onclick = () => modal.style.display = "none";
|
||||
document.getElementById("saveUserFlags").onclick = saveUserFlags;
|
||||
|
||||
document.getElementById("closeUserFlagsModal").onclick = () => (modal.style.display = "none");
|
||||
document.getElementById("cancelUserFlags").onclick = () => (modal.style.display = "none");
|
||||
document.getElementById("saveUserFlags").onclick = saveUserFlags;
|
||||
} else {
|
||||
// Re-apply theme if user toggled dark mode since last open
|
||||
modal.style.background = overlayBg;
|
||||
const content = modal.querySelector(".modal-content");
|
||||
if (content) {
|
||||
content.style.background = contentBg;
|
||||
content.style.color = contentFg;
|
||||
content.style.border = `1px solid ${borderCol}`;
|
||||
}
|
||||
}
|
||||
|
||||
modal.style.display = "flex";
|
||||
loadUserFlagsList();
|
||||
}
|
||||
@@ -990,9 +1042,9 @@ async function saveUserFlags() {
|
||||
const get = k => tr.querySelector(`input[data-flag="${k}"]`).checked;
|
||||
permissions.push({
|
||||
username,
|
||||
readOnly: get("readOnly"),
|
||||
disableUpload: get("disableUpload"),
|
||||
canShare: get("canShare"),
|
||||
readOnly: get("readOnly"),
|
||||
disableUpload: get("disableUpload"),
|
||||
canShare: get("canShare"),
|
||||
bypassOwnership: get("bypassOwnership")
|
||||
});
|
||||
});
|
||||
@@ -1051,7 +1103,10 @@ async function loadUserPermissionsList() {
|
||||
padding:8px 6px;border-radius:6px;cursor:pointer;
|
||||
background:var(--perm-header-bg, rgba(0,0,0,0.04));">
|
||||
<span style="font-weight:600;">${user.username}</span>
|
||||
<i class="material-icons perm-caret" style="transition:transform .2s; transform:rotate(-90deg);">expand_more</i>
|
||||
<i class="material-icons perm-caret"
|
||||
style="transition:transform .2s; transform:rotate(-90deg); color: var(--perm-caret, #444);">
|
||||
expand_more
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<div class="user-perm-details" style="display:none;margin:8px 4px 2px 10px;">
|
||||
@@ -1063,9 +1118,9 @@ async function loadUserPermissionsList() {
|
||||
<hr style="margin:8px 0 4px;border:0;border-bottom:1px solid #ccc;">
|
||||
`;
|
||||
|
||||
const header = row.querySelector(".user-perm-header");
|
||||
const header = row.querySelector(".user-perm-header");
|
||||
const details = row.querySelector(".user-perm-details");
|
||||
const caret = row.querySelector(".perm-caret");
|
||||
const caret = row.querySelector(".perm-caret");
|
||||
const grantsBox = row.querySelector(".folder-grants-box");
|
||||
|
||||
async function ensureLoaded() {
|
||||
|
||||
@@ -5,32 +5,74 @@
|
||||
// It also includes functions to handle the drag-and-drop events, including mouse movements and drop zones.
|
||||
// It uses CSS classes to manage the appearance of the sidebar and header drop zones during drag-and-drop operations.
|
||||
|
||||
// ---- responsive defaults ----
|
||||
const MEDIUM_MIN = 1205; // matches your small-screen cutoff
|
||||
const MEDIUM_MAX = 1600; // tweak as you like
|
||||
|
||||
function isMediumScreen() {
|
||||
const w = window.innerWidth;
|
||||
return w >= MEDIUM_MIN && w < MEDIUM_MAX;
|
||||
}
|
||||
|
||||
// Moves cards into the sidebar based on the saved order in localStorage.
|
||||
export function loadSidebarOrder() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const orderStr = localStorage.getItem('sidebarOrder');
|
||||
if (orderStr) {
|
||||
const order = JSON.parse(orderStr);
|
||||
if (order.length > 0) {
|
||||
// Ensure main wrapper is visible.
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) {
|
||||
mainWrapper.style.display = 'flex';
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
|
||||
const orderStr = localStorage.getItem('sidebarOrder');
|
||||
const headerOrderStr = localStorage.getItem('headerOrder');
|
||||
const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump the suffix if you ever change logic
|
||||
|
||||
// If we have a saved order (sidebar or header), just honor it as before
|
||||
if (orderStr) {
|
||||
const order = JSON.parse(orderStr || '[]');
|
||||
if (Array.isArray(order) && order.length > 0) {
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) mainWrapper.style.display = 'flex';
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode?.id !== 'sidebarDropArea') {
|
||||
sidebar.appendChild(card);
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
// For each saved ID, move the card into the sidebar.
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode.id !== 'sidebarDropArea') {
|
||||
sidebar.appendChild(card);
|
||||
// Animate vertical slide for sidebar card
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
updateSidebarVisibility();
|
||||
return;
|
||||
}
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
|
||||
// No sidebar order saved yet: if user has header icons saved, do nothing (they've customized)
|
||||
const headerOrder = JSON.parse(headerOrderStr || '[]');
|
||||
if (Array.isArray(headerOrder) && headerOrder.length > 0) {
|
||||
updateSidebarVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
// One-time default: on medium screens, start cards in the sidebar
|
||||
const alreadyApplied = localStorage.getItem(defaultAppliedKey) === '1';
|
||||
if (!alreadyApplied && isMediumScreen()) {
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) mainWrapper.style.display = 'flex';
|
||||
|
||||
const candidates = ['uploadCard', 'folderManagementCard'];
|
||||
const moved = [];
|
||||
candidates.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode?.id !== 'sidebarDropArea') {
|
||||
sidebar.appendChild(card);
|
||||
animateVerticalSlide(card);
|
||||
moved.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
if (moved.length) {
|
||||
localStorage.setItem('sidebarOrder', JSON.stringify(moved));
|
||||
localStorage.setItem(defaultAppliedKey, '1');
|
||||
}
|
||||
}
|
||||
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
|
||||
|
||||
export function loadHeaderOrder() {
|
||||
|
||||
@@ -65,6 +65,59 @@ import {
|
||||
return `/api/file/download.php?${q.toString()}`;
|
||||
}
|
||||
|
||||
// Wire "select all" header checkbox for the current table render
|
||||
function wireSelectAll(fileListContent) {
|
||||
// Be flexible about how the header checkbox is identified
|
||||
const selectAll = fileListContent.querySelector(
|
||||
'thead input[type="checkbox"].select-all, ' +
|
||||
'thead .select-all input[type="checkbox"], ' +
|
||||
'thead input#selectAll, ' +
|
||||
'thead input#selectAllCheckbox, ' +
|
||||
'thead input[data-select-all]'
|
||||
);
|
||||
if (!selectAll) return;
|
||||
|
||||
const getRowCbs = () =>
|
||||
Array.from(fileListContent.querySelectorAll('tbody .file-checkbox'))
|
||||
.filter(cb => !cb.disabled);
|
||||
|
||||
// Toggle all rows when the header checkbox changes
|
||||
selectAll.addEventListener('change', () => {
|
||||
const checked = selectAll.checked;
|
||||
getRowCbs().forEach(cb => {
|
||||
cb.checked = checked;
|
||||
updateRowHighlight(cb);
|
||||
});
|
||||
updateFileActionButtons();
|
||||
// No indeterminate state when explicitly toggled
|
||||
selectAll.indeterminate = false;
|
||||
});
|
||||
|
||||
// Keep header checkbox state in sync with row selections
|
||||
const syncHeader = () => {
|
||||
const cbs = getRowCbs();
|
||||
const total = cbs.length;
|
||||
const checked = cbs.filter(cb => cb.checked).length;
|
||||
if (!total) {
|
||||
selectAll.checked = false;
|
||||
selectAll.indeterminate = false;
|
||||
return;
|
||||
}
|
||||
selectAll.checked = checked === total;
|
||||
selectAll.indeterminate = checked > 0 && checked < total;
|
||||
};
|
||||
|
||||
// Listen for any row checkbox changes to refresh header state
|
||||
fileListContent.addEventListener('change', (e) => {
|
||||
if (e.target && e.target.classList.contains('file-checkbox')) {
|
||||
syncHeader();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial sync on mount
|
||||
syncHeader();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
Helper: robust JSON handling
|
||||
----------------------------- */
|
||||
@@ -607,6 +660,8 @@ import {
|
||||
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
|
||||
|
||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
wireSelectAll(fileListContent);
|
||||
|
||||
// PATCH each row's preview/thumb to use the secure API URLs
|
||||
if (totalFiles > 0) {
|
||||
@@ -985,6 +1040,7 @@ import {
|
||||
|
||||
// render
|
||||
fileListContent.innerHTML = galleryHTML;
|
||||
|
||||
|
||||
// pagination buttons for gallery
|
||||
const prevBtn = document.getElementById("prevPageBtn");
|
||||
|
||||
@@ -5,51 +5,7 @@ require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||
|
||||
class AdminController
|
||||
{
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/admin/getConfig.php",
|
||||
* summary="Retrieve admin configuration",
|
||||
* description="Returns the admin configuration settings, decrypting the configuration file and providing default values if not set.",
|
||||
* operationId="getAdminConfig",
|
||||
* tags={"Admin"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Configuration retrieved successfully",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="header_title", type="string", example="FileRise"),
|
||||
* @OA\Property(
|
||||
* property="oidc",
|
||||
* type="object",
|
||||
* @OA\Property(property="providerUrl", type="string", example="https://your-oidc-provider.com"),
|
||||
* @OA\Property(property="clientId", type="string", example="YOUR_CLIENT_ID"),
|
||||
* @OA\Property(property="clientSecret", type="string", example="YOUR_CLIENT_SECRET"),
|
||||
* @OA\Property(property="redirectUri", type="string", example="https://yourdomain.com/auth.php?oidc=callback")
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="loginOptions",
|
||||
* type="object",
|
||||
* @OA\Property(property="disableFormLogin", type="boolean", example=false),
|
||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
||||
* ),
|
||||
* @OA\Property(property="globalOtpauthUrl", type="string", example=""),
|
||||
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||
* @OA\Property(property="sharedMaxUploadSize", type="integer", example=52428800)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="Failed to decrypt configuration or server error"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Retrieves the admin configuration settings.
|
||||
*
|
||||
* @return void Outputs a JSON response with configuration data.
|
||||
*/
|
||||
{
|
||||
public function getConfig(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
@@ -100,64 +56,6 @@ class AdminController
|
||||
echo json_encode($public);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/admin/updateConfig.php",
|
||||
* summary="Update admin configuration",
|
||||
* description="Updates the admin configuration settings. Requires admin privileges and a valid CSRF token.",
|
||||
* operationId="updateAdminConfig",
|
||||
* tags={"Admin"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"header_title", "oidc", "loginOptions"},
|
||||
* @OA\Property(property="header_title", type="string", example="FileRise"),
|
||||
* @OA\Property(
|
||||
* property="oidc",
|
||||
* type="object",
|
||||
* @OA\Property(property="providerUrl", type="string", example="https://your-oidc-provider.com"),
|
||||
* @OA\Property(property="clientId", type="string", example="YOUR_CLIENT_ID"),
|
||||
* @OA\Property(property="clientSecret", type="string", example="YOUR_CLIENT_SECRET"),
|
||||
* @OA\Property(property="redirectUri", type="string", example="https://yourdomain.com/api/auth/auth.php?oidc=callback")
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="loginOptions",
|
||||
* type="object",
|
||||
* @OA\Property(property="disableFormLogin", type="boolean", example=false),
|
||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
||||
* ),
|
||||
* @OA\Property(property="globalOtpauthUrl", type="string", example=""),
|
||||
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||
* @OA\Property(property="sharedMaxUploadSize", type="integer", example=52428800)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Configuration updated successfully",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="success", type="string", example="Configuration updated successfully.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request (e.g., invalid input, incomplete OIDC configuration)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Unauthorized (user not admin or invalid CSRF token)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="Server error (failed to write configuration file)"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Updates the admin configuration settings.
|
||||
*
|
||||
* @return void Outputs a JSON response indicating success or failure.
|
||||
*/
|
||||
public function updateConfig(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -13,53 +13,6 @@ use Jumbojett\OpenIDConnectClient;
|
||||
class AuthController
|
||||
{
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/auth/auth.php",
|
||||
* summary="Authenticate user",
|
||||
* description="Handles user authentication via OIDC or form-based credentials. For OIDC flows, processes callbacks; otherwise, performs standard authentication with optional TOTP verification.",
|
||||
* operationId="authUser",
|
||||
* tags={"Auth"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"username", "password"},
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="password", type="string", example="secretpassword"),
|
||||
* @OA\Property(property="remember_me", type="boolean", example=true),
|
||||
* @OA\Property(property="totp_code", type="string", example="123456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Login successful; returns user info and status",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="string", example="ok"),
|
||||
* @OA\Property(property="success", type="string", example="Login successful"),
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="isAdmin", type="boolean", example=true)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request (e.g., missing credentials)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized (e.g., invalid credentials, too many attempts)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=429,
|
||||
* description="Too many failed login attempts"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Handles user authentication via OIDC or form-based login.
|
||||
*
|
||||
* @return void Redirects on success or outputs JSON error.
|
||||
*/
|
||||
// in src/controllers/AuthController.php
|
||||
|
||||
public function auth(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
@@ -307,40 +260,6 @@ class AuthController
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/auth/checkAuth.php",
|
||||
* summary="Check authentication status",
|
||||
* description="Checks if the current session is authenticated. If the users file is missing or empty, returns a setup flag. Also returns information about admin privileges, TOTP status, and folder-only access.",
|
||||
* operationId="checkAuth",
|
||||
* tags={"Auth"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns authentication status and user details",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="authenticated", type="boolean", example=true),
|
||||
* @OA\Property(property="isAdmin", type="boolean", example=true),
|
||||
* @OA\Property(property="totp_enabled", type="boolean", example=false),
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="folderOnly", type="boolean", example=false)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Setup mode (if the users file is missing or empty)",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="setup", type="boolean", example=true)
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Checks whether the user is authenticated or if the system is in setup mode.
|
||||
*
|
||||
* @return void Outputs a JSON response with authentication details.
|
||||
*/
|
||||
|
||||
public function checkAuth(): void
|
||||
{
|
||||
|
||||
@@ -427,28 +346,6 @@ class AuthController
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/auth/token.php",
|
||||
* summary="Retrieve CSRF token and share URL",
|
||||
* description="Returns the current CSRF token along with the configured share URL.",
|
||||
* operationId="getToken",
|
||||
* tags={"Auth"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="CSRF token and share URL",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="csrf_token", type="string", example="0123456789abcdef..."),
|
||||
* @OA\Property(property="share_url", type="string", example="https://yourdomain.com/share.php")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Returns the CSRF token and share URL.
|
||||
*
|
||||
* @return void Outputs the JSON response.
|
||||
*/
|
||||
public function getToken(): void
|
||||
{
|
||||
// 1) Ensure session and CSRF token exist
|
||||
@@ -468,31 +365,6 @@ class AuthController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/auth/login_basic.php",
|
||||
* summary="Authenticate using HTTP Basic Authentication",
|
||||
* description="Performs HTTP Basic authentication. If credentials are missing, sends a 401 response prompting for Basic auth. On valid credentials, optionally handles TOTP verification and finalizes session login.",
|
||||
* operationId="loginBasic",
|
||||
* tags={"Auth"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Login successful; redirects to index.html",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="success", type="string", example="Login successful")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized due to missing credentials or invalid credentials."
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Handles HTTP Basic authentication (with optional TOTP) and logs the user in.
|
||||
*
|
||||
* @return void Redirects on success or sends a 401 header.
|
||||
*/
|
||||
public function loginBasic(): void
|
||||
{
|
||||
// Set header for plain-text or JSON as needed.
|
||||
@@ -550,27 +422,6 @@ class AuthController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/auth/logout.php",
|
||||
* summary="Logout user",
|
||||
* description="Clears the session, removes persistent login tokens, and redirects the user to the login page.",
|
||||
* operationId="logoutUser",
|
||||
* tags={"Auth"},
|
||||
* @OA\Response(
|
||||
* response=302,
|
||||
* description="Redirects to the login page with a logout flag."
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Logs the user out by clearing session data, removing persistent tokens, and destroying the session.
|
||||
*
|
||||
* @return void Redirects to index.html with a logout flag.
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
// Retrieve headers and check CSRF token.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,6 +96,11 @@ class FolderController
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function isFolderOnly(array $perms): bool
|
||||
{
|
||||
return !empty($perms['folderOnly']) || !empty($perms['userFolderOnly']) || !empty($perms['UserFolderOnly']);
|
||||
}
|
||||
|
||||
private static function requireNotReadOnly(): void
|
||||
{
|
||||
$perms = self::getPerms();
|
||||
@@ -126,23 +131,52 @@ class FolderController
|
||||
return round($bytes / 1073741824, 2) . " GB";
|
||||
}
|
||||
|
||||
/** Enforce "user folder only" scope for non-admins. Returns error string or null if allowed. */
|
||||
private static function enforceFolderScope(string $folder, string $username, array $perms): ?string
|
||||
/** Return true if user is explicit owner of the folder or any of its ancestors (admins also true). */
|
||||
private static function ownsFolderOrAncestor(string $folder, string $username, array $perms): bool
|
||||
{
|
||||
if (self::isAdmin($perms)) return true;
|
||||
$folder = ACL::normalizeFolder($folder);
|
||||
$f = $folder;
|
||||
while ($f !== '' && strtolower($f) !== 'root') {
|
||||
if (ACL::isOwner($username, $perms, $f)) return true;
|
||||
$pos = strrpos($f, '/');
|
||||
$f = ($pos === false) ? '' : substr($f, 0, $pos);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce per-folder scope for folder-only accounts.
|
||||
* $need: 'read' | 'write' | 'manage' | 'share' | 'read_own' (default 'read')
|
||||
* Returns null if allowed, or an error string if forbidden.
|
||||
*/
|
||||
private static function enforceFolderScope(string $folder, string $username, array $perms, string $need = 'read'): ?string
|
||||
{
|
||||
// Admins bypass scope
|
||||
if (self::isAdmin($perms)) return null;
|
||||
|
||||
$folderOnly = !empty($perms['folderOnly']) || !empty($perms['userFolderOnly']) || !empty($perms['UserFolderOnly']);
|
||||
if (!$folderOnly) return null;
|
||||
// Not a folder-only account? no gate here
|
||||
if (!self::isFolderOnly($perms)) return null;
|
||||
|
||||
$folder = trim($folder);
|
||||
if ($folder === '' || strcasecmp($folder, 'root') === 0) {
|
||||
return "Forbidden: non-admins may not operate on the root folder.";
|
||||
$folder = ACL::normalizeFolder($folder);
|
||||
|
||||
// If user owns folder or an ancestor, allow
|
||||
$f = $folder;
|
||||
while ($f !== '' && strtolower($f) !== 'root') {
|
||||
if (ACL::isOwner($username, $perms, $f)) return null;
|
||||
$pos = strrpos($f, '/');
|
||||
$f = ($pos === false) ? '' : substr($f, 0, $pos);
|
||||
}
|
||||
|
||||
if ($folder === $username || strpos($folder, $username . '/') === 0) {
|
||||
return null;
|
||||
// Otherwise, require specific capability on the target folder
|
||||
switch ($need) {
|
||||
case 'manage': $ok = ACL::canManage($username, $perms, $folder); break;
|
||||
case 'write': $ok = ACL::canWrite($username, $perms, $folder); break;
|
||||
case 'share': $ok = ACL::canShare($username, $perms, $folder); break;
|
||||
case 'read_own': $ok = ACL::canReadOwn($username, $perms, $folder);break;
|
||||
default: $ok = ACL::canRead($username, $perms, $folder);
|
||||
}
|
||||
return "Forbidden: folder scope violation.";
|
||||
return $ok ? null : "Forbidden: folder scope violation.";
|
||||
}
|
||||
|
||||
/** Returns true if caller can ignore ownership (admin or bypassOwnership/default). */
|
||||
@@ -152,62 +186,58 @@ class FolderController
|
||||
return (bool)($perms['bypassOwnership'] ?? (defined('DEFAULT_BYPASS_OWNERSHIP') ? DEFAULT_BYPASS_OWNERSHIP : false));
|
||||
}
|
||||
|
||||
/** Returns true if caller can share. */
|
||||
private static function canShare(array $perms): bool
|
||||
/** ACL-aware folder owner check (explicit). */
|
||||
private static function isFolderOwner(string $folder, string $username, array $perms): bool
|
||||
{
|
||||
if (self::isAdmin($perms)) return true;
|
||||
return (bool)($perms['canShare'] ?? (defined('DEFAULT_CAN_SHARE') ? DEFAULT_CAN_SHARE : false));
|
||||
}
|
||||
|
||||
/** Check folder ownership via mapping; returns true if $username is the explicit owner. */
|
||||
private static function isFolderOwner(string $folder, string $username): bool
|
||||
{
|
||||
$owner = FolderModel::getOwnerFor($folder);
|
||||
return is_string($owner) && strcasecmp($owner, $username) === 0;
|
||||
return ACL::isOwner($username, $perms, $folder);
|
||||
}
|
||||
|
||||
/* -------------------- API: Create Folder -------------------- */
|
||||
public function createFolder(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
self::requireAuth();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed.']); exit; }
|
||||
self::requireCsrf();
|
||||
self::requireNotReadOnly();
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
self::requireAuth();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed.']); exit; }
|
||||
self::requireCsrf();
|
||||
self::requireNotReadOnly();
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!isset($input['folderName'])) { http_response_code(400); echo json_encode(['error' => 'Folder name not provided.']); exit; }
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!isset($input['folderName'])) { http_response_code(400); echo json_encode(['error' => 'Folder name not provided.']); exit; }
|
||||
|
||||
$folderName = trim((string)$input['folderName']);
|
||||
$parentIn = isset($input['parent']) ? trim((string)$input['parent']) : '';
|
||||
$folderName = trim((string)$input['folderName']);
|
||||
$parentIn = isset($input['parent']) ? trim((string)$input['parent']) : '';
|
||||
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
http_response_code(400); echo json_encode(['error' => 'Invalid folder name.']); exit;
|
||||
}
|
||||
if ($parentIn !== '' && strcasecmp($parentIn, 'root') !== 0 && !preg_match(REGEX_FOLDER_NAME, $parentIn)) {
|
||||
http_response_code(400); echo json_encode(['error' => 'Invalid parent folder name.']); exit;
|
||||
}
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
http_response_code(400); echo json_encode(['error' => 'Invalid folder name.']); exit;
|
||||
}
|
||||
if ($parentIn !== '' && strcasecmp($parentIn, 'root') !== 0 && !preg_match(REGEX_FOLDER_NAME, $parentIn)) {
|
||||
http_response_code(400); echo json_encode(['error' => 'Invalid parent folder name.']); exit;
|
||||
}
|
||||
|
||||
// Normalize parent to an ACL key
|
||||
$parent = ($parentIn === '' || strcasecmp($parentIn, 'root') === 0) ? 'root' : $parentIn;
|
||||
// Normalize parent to an ACL key
|
||||
$parent = ($parentIn === '' || strcasecmp($parentIn, 'root') === 0) ? 'root' : $parentIn;
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$perms = self::getPerms();
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$perms = self::getPerms();
|
||||
|
||||
// ACL: must be able to WRITE into the parent folder (admins pass)
|
||||
if (!self::isAdmin($perms) && !ACL::canWrite($username, $perms, $parent)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden: no write access to parent folder.']);
|
||||
// Must be able to write into parent OR be owner (or ancestor owner) of it
|
||||
if (!(ACL::canWrite($username, $perms, $parent) || self::ownsFolderOrAncestor($parent, $username, $perms))) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden: no write access to parent folder.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Folder-scope gate for folder-only accounts (need write on parent)
|
||||
if ($msg = self::enforceFolderScope($parent, $username, $perms, 'write')) {
|
||||
http_response_code(403); echo json_encode(['error' => $msg]); exit;
|
||||
}
|
||||
|
||||
// Model should create folder and seed ACL (owner = creator)
|
||||
$result = FolderModel::createFolder($folderName, $parent, $username);
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Let the model do the filesystem work AND seed ACL owner
|
||||
$result = FolderModel::createFolder($folderName, $parent, $username);
|
||||
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* -------------------- API: Delete Folder -------------------- */
|
||||
public function deleteFolder(): void
|
||||
{
|
||||
@@ -220,15 +250,26 @@ class FolderController
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!isset($input['folder'])) { http_response_code(400); echo json_encode(["error" => "Folder name not provided."]); exit; }
|
||||
|
||||
$folder = trim($input['folder']);
|
||||
$folder = trim((string)$input['folder']);
|
||||
if (strcasecmp($folder, 'root') === 0) { http_response_code(400); echo json_encode(["error" => "Cannot delete root folder."]); exit; }
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folder)) { http_response_code(400); echo json_encode(["error" => "Invalid folder name."]); exit; }
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$perms = self::getPerms();
|
||||
|
||||
if ($msg = self::enforceFolderScope($folder, $username, $perms)) { http_response_code(403); echo json_encode(["error" => $msg]); exit; }
|
||||
if (!self::canBypassOwnership($perms) && !self::isFolderOwner($folder, $username)) {
|
||||
// Folder-scope: need manage (owner) OR explicit manage grant
|
||||
if ($msg = self::enforceFolderScope($folder, $username, $perms, 'manage')) {
|
||||
http_response_code(403); echo json_encode(["error" => $msg]); exit;
|
||||
}
|
||||
|
||||
// Require either manage permission or ancestor ownership (strong gate)
|
||||
$canManage = ACL::canManage($username, $perms, $folder) || self::ownsFolderOrAncestor($folder, $username, $perms);
|
||||
if (!$canManage) {
|
||||
http_response_code(403); echo json_encode(["error" => "Forbidden: you lack manage rights for this folder."]); exit;
|
||||
}
|
||||
|
||||
// If not bypassing ownership, require ownership (direct or ancestor) as an extra safeguard
|
||||
if (!self::canBypassOwnership($perms) && !self::ownsFolderOrAncestor($folder, $username, $perms)) {
|
||||
http_response_code(403); echo json_encode(["error" => "Forbidden: you are not the folder owner."]); exit;
|
||||
}
|
||||
|
||||
@@ -251,8 +292,8 @@ class FolderController
|
||||
http_response_code(400); echo json_encode(['error' => 'Required folder names not provided.']); exit;
|
||||
}
|
||||
|
||||
$oldFolder = trim($input['oldFolder']);
|
||||
$newFolder = trim($input['newFolder']);
|
||||
$oldFolder = trim((string)$input['oldFolder']);
|
||||
$newFolder = trim((string)$input['newFolder']);
|
||||
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
|
||||
http_response_code(400); echo json_encode(['error' => 'Invalid folder name(s).']); exit;
|
||||
@@ -261,10 +302,23 @@ class FolderController
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$perms = self::getPerms();
|
||||
|
||||
if ($msg = self::enforceFolderScope($oldFolder, $username, $perms)) { http_response_code(403); echo json_encode(["error" => $msg]); exit; }
|
||||
if ($msg = self::enforceFolderScope($newFolder, $username, $perms)) { http_response_code(403); echo json_encode(["error" => "New path not allowed: " . $msg]); exit; }
|
||||
// Must be allowed to manage the old folder
|
||||
if ($msg = self::enforceFolderScope($oldFolder, $username, $perms, 'manage')) {
|
||||
http_response_code(403); echo json_encode(["error" => $msg]); exit;
|
||||
}
|
||||
// For the new folder path, require write scope (we're "creating" a path)
|
||||
if ($msg = self::enforceFolderScope($newFolder, $username, $perms, 'write')) {
|
||||
http_response_code(403); echo json_encode(["error" => "New path not allowed: " . $msg]); exit;
|
||||
}
|
||||
|
||||
if (!self::canBypassOwnership($perms) && !self::isFolderOwner($oldFolder, $username)) {
|
||||
// Strong gates: need manage on old OR ancestor owner; need write on new parent or ancestor owner
|
||||
$canManageOld = ACL::canManage($username, $perms, $oldFolder) || self::ownsFolderOrAncestor($oldFolder, $username, $perms);
|
||||
if (!$canManageOld) {
|
||||
http_response_code(403); echo json_encode(['error' => 'Forbidden: you lack manage rights on the source folder.']); exit;
|
||||
}
|
||||
|
||||
// If not bypassing ownership, require ownership (direct or ancestor) on the old folder
|
||||
if (!self::canBypassOwnership($perms) && !self::ownsFolderOrAncestor($oldFolder, $username, $perms)) {
|
||||
http_response_code(403); echo json_encode(['error' => 'Forbidden: you are not the folder owner.']); exit;
|
||||
}
|
||||
|
||||
@@ -275,66 +329,64 @@ class FolderController
|
||||
|
||||
/* -------------------- API: Get Folder List -------------------- */
|
||||
public function getFolderList(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
self::requireAuth();
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
self::requireAuth();
|
||||
|
||||
// Optional "folder" filter (supports nested like "team/reports")
|
||||
$parent = $_GET['folder'] ?? null;
|
||||
if ($parent !== null && $parent !== '' && strcasecmp($parent, 'root') !== 0) {
|
||||
$parts = array_filter(explode('/', trim($parent, "/\\ ")), fn($p) => $p !== '');
|
||||
if (empty($parts)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
foreach ($parts as $seg) {
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $seg)) {
|
||||
// Optional "folder" filter (supports nested like "team/reports")
|
||||
$parent = $_GET['folder'] ?? null;
|
||||
if ($parent !== null && $parent !== '' && strcasecmp($parent, 'root') !== 0) {
|
||||
$parts = array_filter(explode('/', trim($parent, "/\\ ")), fn($p) => $p !== '');
|
||||
if (empty($parts)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
foreach ($parts as $seg) {
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $seg)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$parent = implode('/', $parts);
|
||||
}
|
||||
$parent = implode('/', $parts);
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$perms = loadUserPermissions($username) ?: [];
|
||||
$isAdmin = self::isAdmin($perms);
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$perms = self::getPerms();
|
||||
$isAdmin = self::isAdmin($perms);
|
||||
|
||||
// 1) full list from model
|
||||
$all = FolderModel::getFolderList(); // each row: ["folder","fileCount","metadataFile"]
|
||||
if (!is_array($all)) {
|
||||
echo json_encode([]);
|
||||
// 1) Full list from model
|
||||
$all = FolderModel::getFolderList(); // each row: ["folder","fileCount","metadataFile"]
|
||||
if (!is_array($all)) { echo json_encode([]); exit; }
|
||||
|
||||
// 2) Filter by view rights
|
||||
if (!$isAdmin) {
|
||||
$all = array_values(array_filter($all, function ($row) use ($username, $perms) {
|
||||
$f = $row['folder'] ?? '';
|
||||
if ($f === '') return false;
|
||||
|
||||
// Full view if canRead OR owns ancestor; otherwise allow if read_own granted
|
||||
$fullView = ACL::canRead($username, $perms, $f) || FolderController::ownsFolderOrAncestor($f, $username, $perms);
|
||||
$ownOnly = ACL::hasGrant($username, $f, 'read_own');
|
||||
|
||||
return $fullView || $ownOnly;
|
||||
}));
|
||||
}
|
||||
|
||||
// 3) Optional parent filter (applies to both admin and non-admin)
|
||||
if ($parent && strcasecmp($parent, 'root') !== 0) {
|
||||
$pref = $parent . '/';
|
||||
$all = array_values(array_filter($all, function ($row) use ($parent, $pref) {
|
||||
$f = $row['folder'] ?? '';
|
||||
return ($f === $parent) || (strpos($f, $pref) === 0);
|
||||
}));
|
||||
}
|
||||
|
||||
echo json_encode($all);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2) Admin sees all; others: include folder if user has full view OR own-only view
|
||||
if (!$isAdmin) {
|
||||
$all = array_values(array_filter($all, function ($row) use ($username, $perms) {
|
||||
$f = $row['folder'] ?? '';
|
||||
if ($f === '') return false;
|
||||
|
||||
$fullView = ACL::canRead($username, $perms, $f); // owners|write|read
|
||||
$ownOnly = ACL::hasGrant($username, $f, 'read_own'); // view-own
|
||||
|
||||
return $fullView || $ownOnly;
|
||||
}));
|
||||
}
|
||||
|
||||
// 3) Optional parent filter (applies to both admin and non-admin)
|
||||
if ($parent && strcasecmp($parent, 'root') !== 0) {
|
||||
$pref = $parent . '/';
|
||||
$all = array_values(array_filter($all, function ($row) use ($parent, $pref) {
|
||||
$f = $row['folder'] ?? '';
|
||||
return ($f === $parent) || (strpos($f, $pref) === 0);
|
||||
}));
|
||||
}
|
||||
|
||||
echo json_encode($all);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* -------------------- Public Shared Folder HTML -------------------- */
|
||||
public function shareFolder(): void
|
||||
{
|
||||
@@ -451,10 +503,10 @@ for ($i = $startPage; $i <= $endPage; $i++): ?>
|
||||
$in = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$in || !isset($in['folder'])) { http_response_code(400); echo json_encode(["error" => "Invalid input."]); exit; }
|
||||
|
||||
$folder = trim($in['folder']);
|
||||
$folder = trim((string)$in['folder']);
|
||||
$value = isset($in['expirationValue']) ? intval($in['expirationValue']) : 60;
|
||||
$unit = $in['expirationUnit'] ?? 'minutes';
|
||||
$password = $in['password'] ?? '';
|
||||
$password = (string)($in['password'] ?? '');
|
||||
$allowUpload = intval($in['allowUpload'] ?? 0);
|
||||
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { http_response_code(400); echo json_encode(["error" => "Invalid folder name."]); exit; }
|
||||
@@ -463,14 +515,18 @@ for ($i = $startPage; $i <= $endPage; $i++): ?>
|
||||
$perms = self::getPerms();
|
||||
$isAdmin = self::isAdmin($perms);
|
||||
|
||||
if (!self::canShare($perms)) { http_response_code(403); echo json_encode(["error" => "Sharing is not permitted for your account."]); exit; }
|
||||
|
||||
if (!$isAdmin) {
|
||||
if (strcasecmp($folder, 'root') === 0) { http_response_code(403); echo json_encode(["error" => "Only admins may share the root folder."]); exit; }
|
||||
if ($msg = self::enforceFolderScope($folder, $username, $perms)) { http_response_code(403); echo json_encode(["error" => $msg]); exit; }
|
||||
// Must have share on this folder OR be ancestor owner
|
||||
if (!(ACL::canShare($username, $perms, $folder) || self::ownsFolderOrAncestor($folder, $username, $perms))) {
|
||||
http_response_code(403); echo json_encode(["error" => "Sharing is not permitted for your account."]); exit;
|
||||
}
|
||||
|
||||
if (!self::canBypassOwnership($perms) && !self::isFolderOwner($folder, $username)) {
|
||||
// Folder-scope: need share capability within scope
|
||||
if ($msg = self::enforceFolderScope($folder, $username, $perms, 'share')) {
|
||||
http_response_code(403); echo json_encode(["error" => $msg]); exit;
|
||||
}
|
||||
|
||||
// Ownership requirement unless bypassed (allow ancestor owners)
|
||||
if (!self::canBypassOwnership($perms) && !self::ownsFolderOrAncestor($folder, $username, $perms)) {
|
||||
http_response_code(403); echo json_encode(["error" => "Forbidden: you are not the owner of this folder."]); exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,70 +7,6 @@ require_once PROJECT_ROOT . '/src/models/UploadModel.php';
|
||||
|
||||
class UploadController {
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/upload/upload.php",
|
||||
* 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",
|
||||
* tags={"Uploads"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* description="Multipart form data for file upload. For chunked uploads, include fields like 'resumableChunkNumber', 'resumableTotalChunks', 'resumableIdentifier', 'resumableFilename', etc.",
|
||||
* @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
* @OA\Schema(
|
||||
* required={"token", "fileToUpload"},
|
||||
* @OA\Property(property="token", type="string", description="Share token or upload token."),
|
||||
* @OA\Property(
|
||||
* property="fileToUpload",
|
||||
* type="string",
|
||||
* format="binary",
|
||||
* description="The file to upload."
|
||||
* ),
|
||||
* @OA\Property(property="resumableChunkNumber", type="integer", description="Chunk number for chunked uploads."),
|
||||
* @OA\Property(property="resumableTotalChunks", type="integer", description="Total number of chunks."),
|
||||
* @OA\Property(property="resumableFilename", type="string", description="Original filename."),
|
||||
* @OA\Property(property="folder", type="string", description="Target folder (default 'root').")
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="File uploaded successfully (or chunk uploaded status).",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="success", type="string", example="File uploaded successfully"),
|
||||
* @OA\Property(property="newFilename", type="string", example="5f2d7c123a_example.png"),
|
||||
* @OA\Property(property="status", type="string", example="chunk uploaded")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=302,
|
||||
* description="Redirection on full upload success."
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request (e.g., missing file, invalid parameters)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Forbidden (e.g., invalid CSRF token, upload disabled)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="Server error during file processing"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Handles file uploads, both chunked and full, and redirects upon success.
|
||||
*
|
||||
* @return void Outputs JSON response (for chunked uploads) or redirects on successful full upload.
|
||||
*/
|
||||
public function handleUpload(): void {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -148,43 +84,7 @@ class UploadController {
|
||||
'newFilename' => $result['newFilename'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/upload/removeChunks.php",
|
||||
* 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",
|
||||
* tags={"Uploads"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder"},
|
||||
* @OA\Property(property="folder", type="string", example="resumable_myupload123")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Temporary folder removed successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="Temporary folder removed.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Invalid input (e.g., missing folder or invalid folder name)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Removes the temporary upload folder for chunked uploads.
|
||||
*
|
||||
* @return void Outputs a JSON response.
|
||||
*/
|
||||
|
||||
public function removeChunks(): void {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -122,31 +122,6 @@ class UserController
|
||||
|
||||
/* ------------------------- End helpers -------------------------- */
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/getUsers.php",
|
||||
* summary="Retrieve a list of users",
|
||||
* description="Returns a JSON array of users. Only available to authenticated admin users.",
|
||||
* operationId="getUsers",
|
||||
* tags={"Users"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Successful response with an array of users",
|
||||
* @OA\JsonContent(
|
||||
* type="array",
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="role", type="string", example="admin")
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized: the user is not authenticated or is not an admin"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function getUsers()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -158,39 +133,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/addUser.php",
|
||||
* 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",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"username", "password"},
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="password", type="string", example="securepassword"),
|
||||
* @OA\Property(property="isAdmin", type="boolean", example=true)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User added successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="User added successfully")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function addUser()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -258,41 +200,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/removeUser.php",
|
||||
* summary="Remove a user",
|
||||
* description="Removes the specified user from the system. Cannot remove the currently logged-in user.",
|
||||
* operationId="removeUser",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"username"},
|
||||
* @OA\Property(property="username", type="string", example="johndoe")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User removed successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="User removed successfully")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function removeUser()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -322,24 +229,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/getUserPermissions.php",
|
||||
* summary="Retrieve user permissions",
|
||||
* description="Returns the permissions for the current user, or all permissions if the user is an admin.",
|
||||
* operationId="getUserPermissions",
|
||||
* tags={"Users"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Successful response with user permissions",
|
||||
* @OA\JsonContent(type="object")
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function getUserPermissions()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -350,51 +239,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/updateUserPermissions.php",
|
||||
* summary="Update user permissions",
|
||||
* description="Updates permissions for users. Only available to authenticated admin users.",
|
||||
* operationId="updateUserPermissions",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"permissions"},
|
||||
* @OA\Property(
|
||||
* property="permissions",
|
||||
* type="array",
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
* @OA\Property(property="username", type="string", example="johndoe"),
|
||||
* @OA\Property(property="folderOnly", type="boolean", example=true),
|
||||
* @OA\Property(property="readOnly", type="boolean", example=false),
|
||||
* @OA\Property(property="disableUpload", type="boolean", example=false)
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User permissions updated successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="User permissions updated successfully.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function updateUserPermissions()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -415,43 +259,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/changePassword.php",
|
||||
* summary="Change user password",
|
||||
* description="Allows an authenticated user to change their password by verifying the old password and updating to a new one.",
|
||||
* operationId="changePassword",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"oldPassword", "newPassword", "confirmPassword"},
|
||||
* @OA\Property(property="oldPassword", type="string", example="oldpass123"),
|
||||
* @OA\Property(property="newPassword", type="string", example="newpass456"),
|
||||
* @OA\Property(property="confirmPassword", type="string", example="newpass456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Password updated successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="Password updated successfully.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function changePassword()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -488,41 +295,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/updateUserPanel.php",
|
||||
* summary="Update user panel settings",
|
||||
* description="Updates user panel settings by disabling TOTP when not enabled. Accessible to authenticated users.",
|
||||
* operationId="updateUserPanel",
|
||||
* tags={"Users"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"totp_enabled"},
|
||||
* @OA\Property(property="totp_enabled", type="boolean", example=false)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User panel updated successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="string", example="User panel updated: TOTP disabled")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function updateUserPanel()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -551,31 +323,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/totp_disable.php",
|
||||
* summary="Disable TOTP for the authenticated user",
|
||||
* description="Clears the TOTP secret from the users file for the current user.",
|
||||
* operationId="disableTOTP",
|
||||
* tags={"TOTP"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="TOTP disabled successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="TOTP disabled successfully.")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Not authenticated or invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="Failed to disable TOTP"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function disableTOTP()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -601,45 +348,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/totp_recover.php",
|
||||
* summary="Recover TOTP",
|
||||
* description="Verifies a recovery code to disable TOTP and finalize login.",
|
||||
* operationId="recoverTOTP",
|
||||
* tags={"TOTP"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"recovery_code"},
|
||||
* @OA\Property(property="recovery_code", type="string", example="ABC123DEF456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Recovery successful",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="string", example="ok")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Invalid input or recovery code"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=405,
|
||||
* description="Method not allowed"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=429,
|
||||
* description="Too many attempts"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function recoverTOTP()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -681,35 +389,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/totp_saveCode.php",
|
||||
* summary="Generate and save a new TOTP recovery code",
|
||||
* description="Generates a new TOTP recovery code for the authenticated user, stores its hash, and returns the plain text recovery code.",
|
||||
* operationId="totpSaveCode",
|
||||
* tags={"TOTP"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Recovery code generated successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="string", example="ok"),
|
||||
* @OA\Property(property="recoveryCode", type="string", example="ABC123DEF456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Invalid CSRF token or unauthorized"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=405,
|
||||
* description="Method not allowed"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function saveTOTPRecoveryCode()
|
||||
{
|
||||
self::jsonHeaders();
|
||||
@@ -739,30 +418,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/totp_setup.php",
|
||||
* summary="Set up TOTP and generate a QR code",
|
||||
* description="Generates (or retrieves) the TOTP secret for the user and builds a QR code image for scanning.",
|
||||
* operationId="setupTOTP",
|
||||
* tags={"TOTP"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="QR code image for TOTP setup",
|
||||
* @OA\MediaType(
|
||||
* mediaType="image/png"
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Unauthorized or invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="Server error"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function setupTOTP()
|
||||
{
|
||||
// Allow access if authenticated OR pending TOTP
|
||||
@@ -799,42 +454,6 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/totp_verify.php",
|
||||
* summary="Verify TOTP code",
|
||||
* description="Verifies a TOTP code and completes login for pending users or validates TOTP for setup verification.",
|
||||
* operationId="verifyTOTP",
|
||||
* tags={"TOTP"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"totp_code"},
|
||||
* @OA\Property(property="totp_code", type="string", example="123456")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="TOTP successfully verified",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="string", example="ok"),
|
||||
* @OA\Property(property="message", type="string", example="Login successful")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=400,
|
||||
* description="Bad Request (e.g., invalid input)"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Not authenticated or invalid CSRF token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=429,
|
||||
* description="Too many attempts. Try again later."
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function verifyTOTP()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
168
src/openapi/Components.php
Normal file
168
src/openapi/Components.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
// src/openapi/Components.php
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* @OA\Info(
|
||||
* title="FileRise API",
|
||||
* version="1.5.2"
|
||||
* )
|
||||
*
|
||||
* @OA\Server(
|
||||
* url="/",
|
||||
* description="Same-origin server"
|
||||
* )
|
||||
*
|
||||
* @OA\Tag(
|
||||
* name="Admin",
|
||||
* description="Admin endpoints"
|
||||
* )
|
||||
*
|
||||
* @OA\Components(
|
||||
* @OA\SecurityScheme(
|
||||
* securityScheme="cookieAuth",
|
||||
* type="apiKey",
|
||||
* in="cookie",
|
||||
* name="PHPSESSID",
|
||||
* description="Session cookie used for authenticated endpoints"
|
||||
* ),
|
||||
* @OA\SecurityScheme(
|
||||
* securityScheme="CsrfHeader",
|
||||
* type="apiKey",
|
||||
* in="header",
|
||||
* name="X-CSRF-Token",
|
||||
* description="CSRF token header required for state-changing requests"
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response="Unauthorized",
|
||||
* description="Unauthorized (no session)",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="error", type="string", example="Unauthorized")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="Forbidden",
|
||||
* description="Forbidden (not enough privileges)",
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* @OA\Property(property="error", type="string", example="Invalid CSRF token.")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="SimpleSuccess",
|
||||
* type="object",
|
||||
* @OA\Property(property="success", type="boolean", example=true)
|
||||
* ),
|
||||
* @OA\Schema(
|
||||
* schema="SimpleError",
|
||||
* type="object",
|
||||
* @OA\Property(property="error", type="string", example="Something went wrong")
|
||||
* ),
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="LoginOptionsPublic",
|
||||
* type="object",
|
||||
* @OA\Property(property="disableFormLogin", type="boolean"),
|
||||
* @OA\Property(property="disableBasicAuth", type="boolean"),
|
||||
* @OA\Property(property="disableOIDCLogin", type="boolean")
|
||||
* ),
|
||||
* @OA\Schema(
|
||||
* schema="LoginOptionsAdminExtra",
|
||||
* type="object",
|
||||
* @OA\Property(property="authBypass", type="boolean", nullable=true),
|
||||
* @OA\Property(property="authHeaderName", type="string", nullable=true, example="X-Remote-User")
|
||||
* ),
|
||||
* @OA\Schema(
|
||||
* schema="OIDCConfigPublic",
|
||||
* type="object",
|
||||
* @OA\Property(property="providerUrl", type="string", example="https://accounts.example.com"),
|
||||
* @OA\Property(property="redirectUri", type="string", example="https://your.filerise.app/callback")
|
||||
* ),
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="AdminGetConfigPublic",
|
||||
* type="object",
|
||||
* required={"header_title","loginOptions","globalOtpauthUrl","enableWebDAV","sharedMaxUploadSize","oidc"},
|
||||
* @OA\Property(property="header_title", type="string", example="FileRise"),
|
||||
* @OA\Property(property="loginOptions", ref="#/components/schemas/LoginOptionsPublic"),
|
||||
* @OA\Property(property="globalOtpauthUrl", type="string"),
|
||||
* @OA\Property(property="enableWebDAV", type="boolean"),
|
||||
* @OA\Property(property="sharedMaxUploadSize", type="integer", format="int64"),
|
||||
* @OA\Property(property="oidc", ref="#/components/schemas/OIDCConfigPublic")
|
||||
* ),
|
||||
* @OA\Schema(
|
||||
* schema="AdminGetConfigAdmin",
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/AdminGetConfigPublic"),
|
||||
* @OA\Schema(
|
||||
* type="object",
|
||||
* @OA\Property(
|
||||
* property="loginOptions",
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/LoginOptionsPublic"),
|
||||
* @OA\Schema(ref="#/components/schemas/LoginOptionsAdminExtra")
|
||||
* }
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* ),
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="AdminUpdateConfigRequest",
|
||||
* type="object",
|
||||
* additionalProperties=false,
|
||||
* @OA\Property(property="header_title", type="string", maxLength=100, example="FileRise"),
|
||||
* @OA\Property(
|
||||
* property="loginOptions",
|
||||
* type="object",
|
||||
* additionalProperties=false,
|
||||
* @OA\Property(property="disableFormLogin", type="boolean", example=false),
|
||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=true, description="false = OIDC enabled"),
|
||||
* @OA\Property(property="authBypass", type="boolean", example=false),
|
||||
* @OA\Property(
|
||||
* property="authHeaderName",
|
||||
* type="string",
|
||||
* pattern="^[A-Za-z0-9\\-]+$",
|
||||
* example="X-Remote-User",
|
||||
* description="Letters/numbers/dashes only"
|
||||
* )
|
||||
* ),
|
||||
* @OA\Property(property="globalOtpauthUrl", type="string", example="otpauth://totp/{label}?secret={secret}&issuer=FileRise"),
|
||||
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||
* @OA\Property(property="sharedMaxUploadSize", type="integer", format="int64", minimum=0, example=52428800),
|
||||
* @OA\Property(
|
||||
* property="oidc",
|
||||
* type="object",
|
||||
* additionalProperties=false,
|
||||
* description="When disableOIDCLogin=false (OIDC enabled), providerUrl, redirectUri, and clientId are required.",
|
||||
* @OA\Property(property="providerUrl", type="string", format="uri", example="https://issuer.example.com"),
|
||||
* @OA\Property(property="clientId", type="string", example="my-client-id"),
|
||||
* @OA\Property(property="clientSecret", type="string", writeOnly=true, example="***"),
|
||||
* @OA\Property(property="redirectUri", type="string", format="uri", example="https://app.example.com/auth/callback")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @OA\RequestBody(
|
||||
* request="MoveFilesRequest",
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* type="object",
|
||||
* required={"source","destination","files"},
|
||||
* @OA\Property(property="source", type="string", example="inbox"),
|
||||
* @OA\Property(property="destination", type="string", example="archive"),
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"))
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
|
||||
final class OpenAPIComponents {}
|
||||
Reference in New Issue
Block a user