Add folder permissions UI and WebDAV protocol support
Permissions UI: - FolderPermissionsDialog component with public/private toggle, role/user permission management, and access level badges - Integrated into file manager toolbar (visible for folder admins) - Backend returns accessLevel in folder detail endpoint WebDAV server: - Full WebDAV protocol at /webdav/ with Basic Auth (existing credentials) - PROPFIND, GET, PUT, DELETE, MKCOL, COPY, MOVE, LOCK/UNLOCK support - Permission-checked against existing folder access model - In-memory lock stubs for Windows client compatibility - 22 API integration tests covering all operations Also fixes canAccess to check folder creator (was missing).
This commit is contained in:
@@ -23,10 +23,11 @@ function getExtension(contentType: string): string {
|
||||
|
||||
export const StoragePermissionService = {
|
||||
async canAccess(db: PostgresJsDatabase<any>, folderId: string, userId: string): Promise<boolean> {
|
||||
// Check if folder is public
|
||||
const [folder] = await db.select({ isPublic: storageFolders.isPublic }).from(storageFolders).where(eq(storageFolders.id, folderId)).limit(1)
|
||||
// Check if folder is public or user is creator
|
||||
const [folder] = await db.select({ isPublic: storageFolders.isPublic, createdBy: storageFolders.createdBy }).from(storageFolders).where(eq(storageFolders.id, folderId)).limit(1)
|
||||
if (!folder) return false
|
||||
if (folder.isPublic) return true
|
||||
if (folder.createdBy === userId) return true
|
||||
|
||||
// Check direct user permission
|
||||
const [userPerm] = await db.select({ id: storageFolderPermissions.id }).from(storageFolderPermissions)
|
||||
|
||||
121
packages/backend/src/services/webdav.service.ts
Normal file
121
packages/backend/src/services/webdav.service.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { eq, and, isNull } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { storageFolders, storageFiles } from '../db/schema/storage.js'
|
||||
import type { DavResource } from '../utils/webdav-xml.js'
|
||||
|
||||
interface ResolvedPath {
|
||||
type: 'root' | 'folder' | 'file' | null
|
||||
folder?: typeof storageFolders.$inferSelect
|
||||
file?: typeof storageFiles.$inferSelect
|
||||
parentFolder?: typeof storageFolders.$inferSelect
|
||||
}
|
||||
|
||||
export const WebDavService = {
|
||||
/**
|
||||
* Resolve a WebDAV URL path to a database entity.
|
||||
* Path is relative to /webdav/ (e.g., "/HR Documents/Policies/handbook.pdf")
|
||||
*/
|
||||
async resolvePath(db: PostgresJsDatabase<any>, urlPath: string): Promise<ResolvedPath> {
|
||||
const segments = urlPath.split('/').filter(Boolean).map(decodeURIComponent)
|
||||
|
||||
if (segments.length === 0) {
|
||||
return { type: 'root' }
|
||||
}
|
||||
|
||||
// Walk the folder tree
|
||||
let currentFolder: typeof storageFolders.$inferSelect | undefined
|
||||
let parentFolder: typeof storageFolders.$inferSelect | undefined
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i]
|
||||
const isLast = i === segments.length - 1
|
||||
const parentId = currentFolder?.id ?? null
|
||||
|
||||
// Try to find a folder with this name under the current parent
|
||||
const whereClause = parentId
|
||||
? and(eq(storageFolders.name, segment), eq(storageFolders.parentId, parentId))
|
||||
: and(eq(storageFolders.name, segment), isNull(storageFolders.parentId))
|
||||
|
||||
const [folder] = await db.select().from(storageFolders).where(whereClause).limit(1)
|
||||
|
||||
if (folder) {
|
||||
parentFolder = currentFolder
|
||||
currentFolder = folder
|
||||
continue
|
||||
}
|
||||
|
||||
// Not a folder — if this is the last segment, check for a file
|
||||
if (isLast && currentFolder) {
|
||||
const [file] = await db.select().from(storageFiles)
|
||||
.where(and(eq(storageFiles.folderId, currentFolder.id), eq(storageFiles.filename, segment)))
|
||||
.limit(1)
|
||||
|
||||
if (file) {
|
||||
return { type: 'file', file, folder: currentFolder, parentFolder }
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for a file at root level (no parent folder) - not supported in our model
|
||||
// Files must be in folders
|
||||
return { type: null }
|
||||
}
|
||||
|
||||
return { type: 'folder', folder: currentFolder, parentFolder }
|
||||
},
|
||||
|
||||
/**
|
||||
* List children of a folder (or root) as DAV resources.
|
||||
*/
|
||||
async listChildren(db: PostgresJsDatabase<any>, folderId: string | null, basePath: string): Promise<DavResource[]> {
|
||||
const resources: DavResource[] = []
|
||||
|
||||
// Sub-folders
|
||||
const folderWhere = folderId
|
||||
? eq(storageFolders.parentId, folderId)
|
||||
: isNull(storageFolders.parentId)
|
||||
const subFolders = await db.select().from(storageFolders).where(folderWhere).orderBy(storageFolders.name)
|
||||
|
||||
for (const folder of subFolders) {
|
||||
resources.push({
|
||||
href: `${basePath}${encodeURIComponent(folder.name)}/`,
|
||||
isCollection: true,
|
||||
displayName: folder.name,
|
||||
lastModified: folder.updatedAt ? new Date(folder.updatedAt) : undefined,
|
||||
createdAt: folder.createdAt ? new Date(folder.createdAt) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// Files (only if we're inside a folder, not root)
|
||||
if (folderId) {
|
||||
const files = await db.select().from(storageFiles)
|
||||
.where(eq(storageFiles.folderId, folderId))
|
||||
.orderBy(storageFiles.filename)
|
||||
|
||||
for (const file of files) {
|
||||
resources.push({
|
||||
href: `${basePath}${encodeURIComponent(file.filename)}`,
|
||||
isCollection: false,
|
||||
displayName: file.filename,
|
||||
contentType: file.contentType,
|
||||
contentLength: file.sizeBytes,
|
||||
lastModified: file.createdAt ? new Date(file.createdAt) : undefined,
|
||||
createdAt: file.createdAt ? new Date(file.createdAt) : undefined,
|
||||
etag: file.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse the parent path and filename from a WebDAV path.
|
||||
*/
|
||||
parseParentAndName(urlPath: string): { parentPath: string; name: string } {
|
||||
const segments = urlPath.split('/').filter(Boolean).map(decodeURIComponent)
|
||||
if (segments.length === 0) return { parentPath: '/', name: '' }
|
||||
const name = segments[segments.length - 1]
|
||||
const parentPath = '/' + segments.slice(0, -1).join('/')
|
||||
return { parentPath, name }
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user