feat(astrolabe): implement app password provisioning for multi-user background sync
Adds complete app password provisioning workflow for multi-user BasicAuth
deployments, allowing users to independently enable background sync by
generating and storing Nextcloud app passwords.
**New Components:**
Backend (PHP):
- CredentialsController: Validates and stores app passwords
* Validates app password format and authenticity via OCS API
* Stores encrypted passwords in oc_preferences
* Provides status and credential management endpoints
- AstrolabeAdminSettings: Admin configuration page for MCP server URL
- AstrolabeAdminSettingsListener: Event listener for admin section
- Updated McpTokenStorage: Added background sync credential methods
Frontend:
- personalSettings.js: Form handling for app password entry
* AJAX submission with error handling
* Shows success/error notifications
* Triggers page reload after successful save
- settings.css: Styling for settings pages
- Updated personal.php template: Two-option UI
* Option 1: OAuth refresh token (future, not yet available)
* Option 2: App password (works today, recommended)
* Shows "Active" badge when provisioned
* Displays credential type and provisioned timestamp
Routes:
- POST /api/v1/background-sync/credentials - Store app password
- GET /api/v1/background-sync/status - Get provisioning status
- DELETE /api/v1/background-sync/credentials - Revoke credentials
- GET /api/v1/background-sync/credentials/{userId} - Admin only
**Testing:**
- test_astrolabe_settings_buttons.py: Integration test for UI buttons
**Workflow:**
1. User generates app password in Nextcloud Security settings
2. User navigates to Astrolabe personal settings
3. User enters app password in "Option 2: App Password" form
4. Backend validates password via OCS API call
5. Password stored encrypted in oc_preferences
6. Page reloads showing "Active" badge with credential details
7. MCP server can now use stored password for background operations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.5
parent
b293258210
commit
65c3f099fa
+124
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Personal settings page JavaScript for Astrolabe.
|
||||
*
|
||||
* Loads styles for the personal settings page and handles form interactions.
|
||||
*/
|
||||
|
||||
import './styles/settings.css'
|
||||
|
||||
// Wait for DOM to be ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Helper function to show error notifications
|
||||
function showError(message) {
|
||||
if (typeof OC !== 'undefined' && OC.Notification) {
|
||||
OC.Notification.showTemporary(message, { type: 'error' })
|
||||
} else {
|
||||
alert(message)
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
if (typeof OC !== 'undefined' && OC.Notification) {
|
||||
OC.Notification.showTemporary(message, { type: 'success' })
|
||||
} else {
|
||||
alert(message)
|
||||
}
|
||||
}
|
||||
|
||||
// App password form with error handling
|
||||
const appPasswordForm = document.getElementById('mcp-app-password-form')
|
||||
if (appPasswordForm) {
|
||||
appPasswordForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault()
|
||||
const submitButton = document.getElementById('mcp-save-app-password-button')
|
||||
const originalText = submitButton.textContent
|
||||
|
||||
try {
|
||||
submitButton.disabled = true
|
||||
submitButton.textContent = t('astrolabe', 'Saving...')
|
||||
|
||||
const formData = new FormData(appPasswordForm)
|
||||
const response = await fetch(appPasswordForm.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showSuccess(t('astrolabe', 'Background sync access successfully provisioned!'))
|
||||
setTimeout(() => window.location.reload(), 1000)
|
||||
} else {
|
||||
showError(result.error || t('astrolabe', 'Failed to save app password. Please check that it is valid.'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('App password provisioning error:', error)
|
||||
showError(t('astrolabe', 'Unable to connect to server. Please check that the MCP server is running and try again.'))
|
||||
} finally {
|
||||
submitButton.disabled = false
|
||||
submitButton.textContent = originalText
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Revoke form confirmation
|
||||
const revokeForm = document.getElementById('mcp-revoke-form')
|
||||
if (revokeForm) {
|
||||
revokeForm.addEventListener('submit', function(e) {
|
||||
if (!confirm(t('astrolabe', 'Are you sure you want to disable indexing? Your content will be removed from semantic search.'))) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Disconnect form confirmation
|
||||
const disconnectForm = document.getElementById('mcp-disconnect-form')
|
||||
if (disconnectForm) {
|
||||
disconnectForm.addEventListener('submit', function(e) {
|
||||
if (!confirm(t('astrolabe', 'Are you sure you want to disconnect from Astrolabe? You will need to re-authorize to use semantic search.'))) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Revoke background access form with error handling
|
||||
const revokeBackgroundForm = document.getElementById('mcp-revoke-background-form')
|
||||
if (revokeBackgroundForm) {
|
||||
revokeBackgroundForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault()
|
||||
|
||||
if (!confirm(t('astrolabe', 'Are you sure you want to revoke background sync access? The MCP server will no longer be able to access your Nextcloud data for background operations.'))) {
|
||||
return
|
||||
}
|
||||
|
||||
const submitButton = revokeBackgroundForm.querySelector('button[type="submit"]')
|
||||
const originalText = submitButton.textContent
|
||||
|
||||
try {
|
||||
submitButton.disabled = true
|
||||
submitButton.textContent = t('astrolabe', 'Revoking...')
|
||||
|
||||
const formData = new FormData(revokeBackgroundForm)
|
||||
const response = await fetch(revokeBackgroundForm.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showSuccess(t('astrolabe', 'Background sync access revoked successfully.'))
|
||||
setTimeout(() => window.location.reload(), 1000)
|
||||
} else {
|
||||
showError(result.error || t('astrolabe', 'Failed to revoke background sync access.'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Revoke error:', error)
|
||||
showError(t('astrolabe', 'Unable to connect to server. Your access may already be revoked, or the server may be down.'))
|
||||
} finally {
|
||||
submitButton.disabled = false
|
||||
submitButton.textContent = originalText
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user