style: run prettier on entire codebase
This commit is contained in:
177
docs/superpowers/specs/2026-03-23-project-files-design.md
Normal file
177
docs/superpowers/specs/2026-03-23-project-files-design.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Project File Sharing — boha-app-ts
|
||||
|
||||
**Date:** 2026-03-23
|
||||
**Status:** Approved
|
||||
**Scope:** NAS-based file management for projects — port of PHP NasFileManager with security improvements
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The PHP boha-app has a complete project file sharing system via NAS-mounted storage. The TS version has only a placeholder UI and the `NAS_PATH` env var configured. This spec covers porting the full functionality with security improvements identified during audit.
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend — NasFileManager Service
|
||||
|
||||
**File:** `src/services/nas-file-manager.ts`
|
||||
|
||||
Port of `NasFileManager.php` (622 lines) with security improvements.
|
||||
|
||||
### Security Improvements Over PHP
|
||||
|
||||
- **No symlink/junction following** — use `fs.lstatSync()` everywhere. Walk each path component with `lstat()` and reject if any component is a symlink. Do NOT use `fs.realpathSync()` (it follows symlinks by design). Instead, use `path.resolve()` for prefix validation after confirming no symlinks exist.
|
||||
- **Extended blocked extensions** — add `vbs, vbe, js, ws, wsf, scr, pif, jar, reg` to the existing list (Windows Script Host executables).
|
||||
- **Download headers** — `X-Content-Type-Options: nosniff` is already set globally by security middleware, but also set on download responses explicitly.
|
||||
- **MIME validation** via `file-type` package **version 16.x** (last CJS-compatible version — the project uses CommonJS modules). Install as `file-type@16`.
|
||||
- **Use `fs.promises.rm()` with `recursive: true`** instead of hand-rolled recursive delete. Pre-check for symlinks in the tree before calling `rm` to prevent junction traversal on Windows.
|
||||
|
||||
### Path Resolution Algorithm (`resolveProjectPath`)
|
||||
|
||||
```
|
||||
1. Find project folder by prefix scan
|
||||
2. If subPath is empty, return project folder
|
||||
3. Reject if subPath contains null bytes or ".."
|
||||
4. Normalize separators (backslash → forward slash), trim slashes
|
||||
5. Build candidate = projectFolder + "/" + subPath
|
||||
6. Walk each path component with lstatSync():
|
||||
- If component is a symlink → return null (reject)
|
||||
7. Verify path.resolve(candidate) starts with projectFolder
|
||||
8. Return candidate (or null if validation fails)
|
||||
```
|
||||
|
||||
### Retained Security (same as PHP)
|
||||
- Null byte detection in paths
|
||||
- `..` traversal blocking
|
||||
- Filename sanitization: strip control chars (0x00-0x1f, 0x7f), invalid chars (`<>:"/\|?*`), trim dots/spaces, max 255 chars, `path.basename()` extraction
|
||||
- Blocked extensions: `exe, bat, sh, php, htaccess, env, cmd, com, msi, ps1, vbs, vbe, js, ws, wsf, scr, pif, jar, reg`
|
||||
- Executable MIME type detection (application/x-executable, x-msdos-program, x-dosexec, x-msdownload)
|
||||
- PHP MIME detection (any MIME containing "php" or "x-httpd")
|
||||
- Root folder deletion prevention
|
||||
- Duplicate file naming (append `_1`, `_2`, etc.) — note: TOCTOU race exists on SMB mounts but is acceptable for this use case (low concurrency internal app)
|
||||
- File size limit from `MAX_UPLOAD_SIZE` env var (default 52MB)
|
||||
|
||||
### Public Methods
|
||||
```
|
||||
isConfigured(): boolean
|
||||
createProjectFolder(projectNumber, projectName): boolean
|
||||
deleteProjectFolder(projectNumber): Promise<boolean>
|
||||
projectFolderExists(projectNumber): boolean
|
||||
renameProjectFolder(projectNumber, newName): boolean
|
||||
listFiles(projectNumber, subPath): ListResult | null
|
||||
uploadFile(projectNumber, subPath, fileBuffer, fileName): Promise<string | null>
|
||||
downloadFile(projectNumber, filePath): DownloadResult | null
|
||||
deleteItem(projectNumber, filePath): Promise<string | null>
|
||||
moveItem(projectNumber, fromPath, toPath): string | null
|
||||
createFolder(projectNumber, subPath, folderName): string | null
|
||||
sanitizeFilename(name): string
|
||||
```
|
||||
|
||||
### Private Methods
|
||||
```
|
||||
findProjectFolder(projectNumber): string | null
|
||||
buildFolderName(projectNumber, projectName): string
|
||||
resolveProjectPath(projectNumber, subPath): string | null
|
||||
walkAndRejectSymlinks(fullPath, basePath): boolean
|
||||
countItems(dirPath): number
|
||||
formatFileSize(bytes): string
|
||||
isSuspiciousMime(mime, ext): boolean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend — Project Files Route
|
||||
|
||||
**File:** `src/routes/admin/project-files.ts`
|
||||
|
||||
**Modify:** `src/server.ts` — add import and register: `app.register(projectFilesRoutes, { prefix: '/api/admin/project-files' })`
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Query Params | Permission | Action |
|
||||
|--------|-------------|------------|--------|
|
||||
| GET | `project_id, path` | `projects.view` | List files/folders |
|
||||
| GET | `action=download, project_id, path` | `projects.view` | Stream file download |
|
||||
| POST | `action=upload, project_id, path` | `projects.files` | Upload file (multipart) |
|
||||
| POST | `action=create_folder, project_id, path, folder_name` | `projects.files` | Create subfolder |
|
||||
| PUT | `action=move, project_id, from_path, to_path` | `projects.files` | Move/rename |
|
||||
| DELETE | `project_id, path` | `projects.files` | Delete file/folder |
|
||||
|
||||
### Behavior
|
||||
- All endpoints validate `project_id` exists in database before file operations
|
||||
- All write operations logged via `logAudit()`
|
||||
- Download sets `Content-Disposition: attachment` and `Content-Type` from detected MIME
|
||||
- Upload route: register `@fastify/multipart` within the plugin scope (same pattern as orders route) with `bodyLimit: config.nas.maxUploadSize` to override the global 1MB limit
|
||||
- Folder name max 100 characters on create
|
||||
- `moveItem`: handle `EXDEV` error (cross-device) gracefully with error message — source and target are expected to be on the same NAS mount
|
||||
|
||||
---
|
||||
|
||||
## 3. Project CRUD Integration
|
||||
|
||||
**Modify:** `src/services/projects.service.ts`
|
||||
|
||||
- `createProject()` — after DB insert, call `nasFileManager.createProjectFolder(projectNumber, name)`
|
||||
- `updateProject()` — if project name changed, call `nasFileManager.renameProjectFolder(projectNumber, newName)`
|
||||
- `deleteProject()` — if `deleteFiles` flag is true, call `nasFileManager.deleteProjectFolder(projectNumber)`
|
||||
- `getProject()` — add `has_nas_folder: nasFileManager.projectFolderExists(projectNumber)` to response
|
||||
|
||||
---
|
||||
|
||||
## 4. Frontend — ProjectFileManager Component
|
||||
|
||||
**File:** `src/admin/components/ProjectFileManager.tsx`
|
||||
|
||||
Port of PHP's `ProjectFileManager.jsx` (657 lines).
|
||||
|
||||
### Features
|
||||
- File/folder table with columns: Name, Size, Modified, Actions
|
||||
- File type icons by extension (folder, pdf, image, doc, xls, zip, etc.)
|
||||
- Breadcrumb navigation with clickable path segments
|
||||
- Full path display (real path on disk)
|
||||
- Drag-and-drop upload zone with visual feedback
|
||||
- Click-to-upload fallback via hidden file input
|
||||
- Create folder dialog (input + confirm)
|
||||
- Inline rename (click name to edit)
|
||||
- Delete with ConfirmModal
|
||||
- Download via direct link with auth cookie (NOT blob URL — avoids loading large files into memory)
|
||||
- Permission check: write operations only shown if `hasPermission('projects.files')`
|
||||
- Item count for folders, formatted size for files
|
||||
- Folders sorted first, then files, both alphabetically
|
||||
|
||||
### Props
|
||||
```typescript
|
||||
interface ProjectFileManagerProps {
|
||||
projectId: number
|
||||
projectNumber: string | null
|
||||
hasPermission: (perm: string) => boolean
|
||||
hasNasFolder: boolean
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. ProjectDetail Integration
|
||||
|
||||
**Modify:** `src/admin/pages/ProjectDetail.tsx`
|
||||
|
||||
- Replace the placeholder "Soubory" section with `<ProjectFileManager />` component
|
||||
- Pass `projectId`, `projectNumber`, `hasPermission`, `hasNasFolder` props
|
||||
- Delete dialog: add checkbox "Smazat i soubory na disku" that sends `delete_files: true`
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `file-type@16` — MIME detection (version 16 is the last CJS-compatible version)
|
||||
- `@fastify/multipart` — already installed (used by orders route)
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- File versioning / history
|
||||
- File sharing links (public URLs)
|
||||
- Thumbnail generation for images
|
||||
- Full-text search within files
|
||||
- Virus scanning
|
||||
Reference in New Issue
Block a user