Add Nextcloud WebDAV directory browser endpoint
GET /settings/api/integrations/nextcloud/browse?path=<path> Uses stored Nextcloud credentials to PROPFIND the given path and returns a sorted list of subdirectories for the UI picker. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8a4c4182ec
commit
4f6fe6bb37
1 changed files with 59 additions and 0 deletions
|
|
@ -264,6 +264,65 @@ export async function integrationRoutes(app) {
|
|||
return { ...row.config, ...decryptSecrets(row) }
|
||||
})
|
||||
|
||||
// GET /settings/api/integrations/nextcloud/browse?path=/ — list directories via WebDAV PROPFIND
|
||||
app.get('/integrations/nextcloud/browse', { preHandler: requireAdmin }, async (req, reply) => {
|
||||
const { rows: [row] } = await pool.query('SELECT * FROM integrations WHERE slug = $1', ['nextcloud'])
|
||||
if (!row?.enabled) return reply.status(400).send({ error: 'Nextcloud integration is not enabled' })
|
||||
const creds = { ...row.config, ...decryptSecrets(row) }
|
||||
if (!creds.base_url || !creds.username || !creds.password) {
|
||||
return reply.status(400).send({ error: 'Nextcloud credentials not fully configured' })
|
||||
}
|
||||
|
||||
const rawPath = (req.query.path || '/').replace(/\/+/g, '/')
|
||||
if (rawPath.includes('..') || !rawPath.startsWith('/')) {
|
||||
return reply.status(400).send({ error: 'Invalid path' })
|
||||
}
|
||||
const browsePath = rawPath === '/' ? '/' : rawPath.replace(/\/$/, '')
|
||||
|
||||
const davBase = `${creds.base_url.replace(/\/$/, '')}/remote.php/dav/files/${encodeURIComponent(creds.username)}`
|
||||
const davUrl = browsePath === '/' ? `${davBase}/` : `${davBase}${browsePath}/`
|
||||
const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64')
|
||||
|
||||
let xmlRes
|
||||
try {
|
||||
xmlRes = await fetch(davUrl, {
|
||||
method: 'PROPFIND',
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
Depth: '1',
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
body: '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:displayname/></d:prop></d:propfind>',
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
})
|
||||
} catch (e) {
|
||||
return reply.status(502).send({ error: `Could not reach Nextcloud: ${e.message}` })
|
||||
}
|
||||
|
||||
if (xmlRes.status === 401) return reply.status(502).send({ error: 'Nextcloud credentials rejected' })
|
||||
if (xmlRes.status === 404) return reply.status(404).send({ error: 'Path not found' })
|
||||
if (xmlRes.status !== 207) return reply.status(502).send({ error: `Nextcloud returned ${xmlRes.status}` })
|
||||
|
||||
const xml = await xmlRes.text()
|
||||
const prefix = `/remote.php/dav/files/${creds.username}`
|
||||
const dirs = []
|
||||
|
||||
for (const block of xml.split(/<[^:>]*:?response[^>]*>/).slice(1)) {
|
||||
if (!block.includes('collection')) continue
|
||||
const m = block.match(/<[^:>]*:?href[^>]*>([^<]+)<\/[^:>]*:?href>/)
|
||||
if (!m) continue
|
||||
const decoded = decodeURIComponent(m[1].trim())
|
||||
if (!decoded.startsWith(prefix)) continue
|
||||
const relPath = decoded.slice(prefix.length).replace(/\/$/, '') || '/'
|
||||
if (relPath === browsePath) continue // skip self
|
||||
const name = relPath.split('/').filter(Boolean).pop() || ''
|
||||
if (name) dirs.push({ name, path: relPath })
|
||||
}
|
||||
|
||||
dirs.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return { path: browsePath, dirs }
|
||||
})
|
||||
|
||||
// PUT /settings/api/integrations/newbook/rooms — save reorder + colour changes
|
||||
app.put('/integrations/newbook/rooms', { preHandler: requireAdmin }, async (req, reply) => {
|
||||
const { rows: [row] } = await pool.query(`SELECT value FROM global_config WHERE key = 'newbook.rooms'`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue