diff --git a/server/routes/webdav.ts b/server/routes/webdav.ts index 41cc475f..a5cb8b55 100644 --- a/server/routes/webdav.ts +++ b/server/routes/webdav.ts @@ -1,9 +1,10 @@ +import { defaultKeyHasher } from '@better-auth/api-key' import { and, eq, like, or } from 'drizzle-orm' import type { Context } from 'hono' import { Hono } from 'hono' import { DirType, ObjectStatus } from '../../shared/constants' import type { Storage as S3Storage } from '../../shared/types' -import { user } from '../db/auth-schema' +import { apikey, user } from '../db/auth-schema' import { matters } from '../db/schema' import type { Env } from '../middleware/platform' import { @@ -75,18 +76,28 @@ async function requireWebDavApiKey(c: DavContext): Promise { if (!credentials) return unauthorized() try { - // biome-ignore lint/suspicious/noExplicitAny: better-auth plugin API is not fully typed - const result = (await (c.get('auth').api as any).verifyApiKey({ - body: { configId: WEBDAV_CONFIG_ID, key: credentials.password, permissions: { [WEBDAV_RESOURCE]: [action] } }, - })) as { - valid: boolean - key: { referenceId: string } | null - error: { message?: string } | null - } - if (!result?.valid || !result.key?.referenceId) return unauthorized() - if (!(await usernameMatches(c.get('platform').db, result.key.referenceId, credentials.username))) + const db = c.get('platform').db + const hashedKey = await defaultKeyHasher(credentials.password) + const rows = await db + .select({ + id: apikey.id, + referenceId: apikey.referenceId, + permissions: apikey.permissions, + enabled: apikey.enabled, + expiresAt: apikey.expiresAt, + }) + .from(apikey) + .where(and(eq(apikey.configId, WEBDAV_CONFIG_ID), eq(apikey.key, hashedKey))) + .limit(1) + const key = rows[0] + if ( + !key?.enabled || + (key.expiresAt && key.expiresAt.getTime() <= Date.now()) || + !hasWebDavPermission(key.permissions, action) + ) return unauthorized() - return { userId: result.key.referenceId } + if (!(await usernameMatches(db, key.referenceId, credentials.username))) return unauthorized() + return { userId: key.referenceId } } catch { return unauthorized() } @@ -117,6 +128,12 @@ function parseBasicAuth(header: string | null): { username: string; password: st return { username, password } } +function hasWebDavPermission(permissions: string | null, action: 'read' | 'write'): boolean { + if (!permissions) return false + const parsed = JSON.parse(permissions) as Partial> + return parsed[WEBDAV_RESOURCE]?.includes(action) ?? false +} + async function usernameMatches( db: Env['Variables']['platform']['db'], userId: string, diff --git a/server/services/webdav-state.ts b/server/services/webdav-state.ts index 09a2107e..8395d9fe 100644 --- a/server/services/webdav-state.ts +++ b/server/services/webdav-state.ts @@ -275,8 +275,8 @@ export async function refreshLock( const now = new Date() const expiresAt = new Date(now.getTime() + timeoutSeconds * 1000) const rows = await db - .update(webdavLocks) - .set({ expiresAt, updatedAt: now }) + .select() + .from(webdavLocks) .where( and( eq(webdavLocks.orgId, orgId), @@ -289,14 +289,18 @@ export async function refreshLock( ), ), ) - .returning() - return rows[0] ?? null + .limit(1) + const lock = rows[0] + if (!lock) return null + await db.update(webdavLocks).set({ expiresAt, updatedAt: now }).where(eq(webdavLocks.id, lock.id)) + return { ...lock, expiresAt, updatedAt: now } } export async function removeLock(db: Database, orgId: string, resourcePath: string, token: string): Promise { await purgeExpiredLocks(db) const rows = await db - .delete(webdavLocks) + .select({ id: webdavLocks.id }) + .from(webdavLocks) .where( and( eq(webdavLocks.orgId, orgId), @@ -309,8 +313,11 @@ export async function removeLock(db: Database, orgId: string, resourcePath: stri ), ), ) - .returning({ id: webdavLocks.id }) - return rows.length > 0 + .limit(1) + const lock = rows[0] + if (!lock) return false + await db.delete(webdavLocks).where(eq(webdavLocks.id, lock.id)) + return true } async function purgeExpiredLocks(db: Database): Promise {