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:
78
packages/backend/src/plugins/webdav-auth.ts
Normal file
78
packages/backend/src/plugins/webdav-auth.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { users } from '../db/schema/users.js'
|
||||
import { RbacService } from '../services/rbac.service.js'
|
||||
import type { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
|
||||
|
||||
/**
|
||||
* Permission inheritance — same logic as auth.ts
|
||||
*/
|
||||
const ACTION_HIERARCHY: Record<string, string[]> = {
|
||||
admin: ['admin', 'edit', 'view'],
|
||||
edit: ['edit', 'view'],
|
||||
view: ['view'],
|
||||
upload: ['upload'],
|
||||
delete: ['delete'],
|
||||
send: ['send'],
|
||||
export: ['export'],
|
||||
}
|
||||
|
||||
function expandPermissions(slugs: string[]): Set<string> {
|
||||
const expanded = new Set<string>()
|
||||
for (const slug of slugs) {
|
||||
expanded.add(slug)
|
||||
const [domain, action] = slug.split('.')
|
||||
const implied = ACTION_HIERARCHY[action]
|
||||
if (implied && domain) {
|
||||
for (const a of implied) expanded.add(`${domain}.${a}`)
|
||||
}
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
/**
|
||||
* WebDAV Basic Auth pre-handler.
|
||||
* Verifies HTTP Basic Auth credentials against the users table
|
||||
* and attaches request.user / request.permissions just like JWT auth.
|
||||
*/
|
||||
export function webdavBasicAuth(app: FastifyInstance) {
|
||||
return async function (request: FastifyRequest, reply: FastifyReply) {
|
||||
const authHeader = request.headers.authorization
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
return reply.status(401).send('Authentication required')
|
||||
}
|
||||
|
||||
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf-8')
|
||||
const colonIndex = decoded.indexOf(':')
|
||||
if (colonIndex === -1) {
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
const email = decoded.slice(0, colonIndex)
|
||||
const password = decoded.slice(colonIndex + 1)
|
||||
|
||||
const [user] = await app.db
|
||||
.select({ id: users.id, passwordHash: users.passwordHash, isActive: users.isActive, role: users.role })
|
||||
.from(users)
|
||||
.where(eq(users.email, email.toLowerCase()))
|
||||
.limit(1)
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, user.passwordHash)
|
||||
if (!valid) {
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
// Attach user and permissions
|
||||
request.user = { id: user.id, role: user.role }
|
||||
const permSlugs = await RbacService.getUserPermissions(app.db, user.id)
|
||||
request.permissions = expandPermissions(permSlugs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user