feat(12-06): connection-ID native lifecycle — always INSERT, PUT credentials, connectionsFor multi-account
- Replace _upsert_cloud_connection with _insert_cloud_connection (always inserts new UUID row)
- Add PUT /connections/{id}/credentials endpoint (owner-scoped, SSRF + health-check, password-preserve)
- SettingsCloudTab: connectionsFor() renders all same-provider connections with Add account row
- CloudCredentialModal.submit: calls updateWebDavCredentials on edit, connectWebDav on create
- utils.js: FastAPI validation arrays normalised to concise field messages (Rule 2)
- Backend tests: same-provider independence + IDOR negative test for credential update
This commit is contained in:
@@ -73,3 +73,17 @@ export function initiateOAuth(provider) {
|
||||
export function getConnectionConfig(connectionId) {
|
||||
return request(`/api/cloud/connections/${connectionId}/config`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update credentials for an existing WebDAV/Nextcloud connection (owner-scoped).
|
||||
*
|
||||
* Only fields included are changed. Omitting password preserves the existing secret.
|
||||
* Backend re-validates the URL and runs a health-check before committing.
|
||||
*/
|
||||
export function updateWebDavCredentials(connectionId, { serverUrl, username, password }) {
|
||||
const body = {}
|
||||
if (serverUrl !== undefined) body.server_url = serverUrl
|
||||
if (username !== undefined) body.username = username
|
||||
if (password !== undefined && password !== '') body.password = password
|
||||
return jsonRequest(`/api/cloud/connections/${connectionId}/credentials`, 'PUT', body)
|
||||
}
|
||||
|
||||
@@ -44,11 +44,18 @@ export async function request(path, options = {}) {
|
||||
let payload = null
|
||||
try {
|
||||
const body = await res.json()
|
||||
if (typeof body.detail === 'object' && body.detail !== null) {
|
||||
if (Array.isArray(body.detail)) {
|
||||
// FastAPI validation error array — summarise into one actionable message
|
||||
const parts = body.detail.map(e => {
|
||||
const loc = e.loc ? e.loc.filter(s => s !== 'body').join('.') : ''
|
||||
return loc ? `${loc}: ${e.msg}` : e.msg
|
||||
})
|
||||
msg = parts.join('; ') || `Validation error (HTTP ${res.status})`
|
||||
} else if (typeof body.detail === 'object' && body.detail !== null) {
|
||||
payload = body.detail
|
||||
msg = body.detail.message || `HTTP ${res.status}`
|
||||
} else {
|
||||
msg = body.detail || msg
|
||||
} else if (body.detail) {
|
||||
msg = body.detail
|
||||
}
|
||||
} catch {}
|
||||
const err = new Error(msg)
|
||||
|
||||
Reference in New Issue
Block a user