65c3f099fa
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>
219 lines
8.7 KiB
PHP
219 lines
8.7 KiB
PHP
<?php
|
|
/**
|
|
* Personal settings template for Astrolabe.
|
|
*
|
|
* Displays semantic search status, background indexing access,
|
|
* and provides controls for managing content indexing.
|
|
*
|
|
* @var array $_ Template parameters
|
|
* @var string $_['userId'] Current user ID
|
|
* @var array $_['serverStatus'] Server status from API
|
|
* @var array $_['session'] User session details from API
|
|
* @var bool $_['vectorSyncEnabled'] Whether vector sync is enabled
|
|
* @var bool $_['backgroundAccessGranted'] Whether user has granted background access
|
|
* @var string $_['serverUrl'] Astrolabe service URL
|
|
*/
|
|
|
|
// Get URL generator from Nextcloud's service container
|
|
$urlGenerator = \OC::$server->getURLGenerator();
|
|
|
|
script('astrolabe', 'astrolabe-personalSettings');
|
|
style('astrolabe', 'astrolabe-personalSettings');
|
|
?>
|
|
|
|
<div class="section">
|
|
<h2><?php p($l->t('Astrolabe')); ?></h2>
|
|
<p><?php p($l->t('AI-powered semantic search across your Nextcloud content. Find documents by meaning, not just keywords.')); ?></p>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2><?php p($l->t('Service Status')); ?></h2>
|
|
<table class="mcp-info-table">
|
|
<tr>
|
|
<td><?php p($l->t('Service URL')); ?></td>
|
|
<td><code><?php p($_['serverUrl']); ?></code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><?php p($l->t('Version')); ?></td>
|
|
<td><?php p($_['serverStatus']['version'] ?? 'Unknown'); ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2><?php p($l->t('Background Sync Access')); ?></h2>
|
|
|
|
<?php if ($_['hasBackgroundAccess'] || $_['backgroundAccessGranted']): ?>
|
|
<!-- Already configured -->
|
|
<div class="mcp-background-status">
|
|
<p>
|
|
<span class="badge badge-success">
|
|
<span class="icon icon-checkmark-white"></span>
|
|
<?php p($l->t('Active')); ?>
|
|
</span>
|
|
</p>
|
|
<table class="mcp-info-table">
|
|
<tr>
|
|
<td><?php p($l->t('Credential Type')); ?></td>
|
|
<td>
|
|
<?php if ($_['backgroundSyncType'] === 'app_password'): ?>
|
|
<?php p($l->t('App Password')); ?>
|
|
<?php else: ?>
|
|
<?php p($l->t('OAuth Refresh Token')); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php if ($_['backgroundSyncProvisionedAt']): ?>
|
|
<tr>
|
|
<td><?php p($l->t('Provisioned At')); ?></td>
|
|
<td><?php p(date('c', $_['backgroundSyncProvisionedAt'])); ?></td>
|
|
</tr>
|
|
<?php elseif (isset($_['session']['background_access_details']['provisioned_at'])): ?>
|
|
<tr>
|
|
<td><?php p($l->t('Provisioned At')); ?></td>
|
|
<td><?php p($_['session']['background_access_details']['provisioned_at']); ?></td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php if (isset($_['session']['background_access_details']['scopes'])): ?>
|
|
<tr>
|
|
<td><?php p($l->t('Indexed Content')); ?></td>
|
|
<td><code><?php p($_['session']['background_access_details']['scopes'] ?? 'N/A'); ?></code></td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</table>
|
|
|
|
<div class="mcp-revoke-section">
|
|
<?php if ($_['backgroundSyncType'] === 'app_password'): ?>
|
|
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.deleteCredentials')); ?>" id="mcp-revoke-background-form">
|
|
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
|
|
<button type="submit" class="button warning" id="mcp-revoke-background-button">
|
|
<span class="icon icon-delete"></span>
|
|
<?php p($l->t('Revoke Access')); ?>
|
|
</button>
|
|
<p class="mcp-help-text">
|
|
<?php p($l->t('This will revoke background sync access. The MCP server will no longer be able to access your Nextcloud data for background operations.')); ?>
|
|
</p>
|
|
</form>
|
|
<?php else: ?>
|
|
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.api.revokeAccess')); ?>" id="mcp-revoke-form">
|
|
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
|
|
<button type="submit" class="button warning" id="mcp-revoke-button">
|
|
<span class="icon icon-delete"></span>
|
|
<?php p($l->t('Disable Indexing')); ?>
|
|
</button>
|
|
<p class="mcp-help-text">
|
|
<?php p($l->t('This will stop background indexing and remove your content from semantic search. You can re-enable it at any time.')); ?>
|
|
</p>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<!-- Not configured - show provisioning options -->
|
|
<p class="mcp-help-text">
|
|
<?php p($l->t('Enable background sync to allow the MCP server to access your Nextcloud data for background operations like content indexing.')); ?>
|
|
</p>
|
|
|
|
<div class="mcp-grant-section">
|
|
<h4><?php p($l->t('Option 1: OAuth Refresh Token (Recommended for Future)')); ?></h4>
|
|
<p class="mcp-help-text">
|
|
<?php p($l->t('When Nextcloud fully supports OAuth for app APIs. Currently waiting for upstream PR to merge.')); ?>
|
|
</p>
|
|
<a href="<?php p($_['serverUrl']); ?>/oauth/login?next=<?php p(urlencode($urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'astrolabe'])))); ?>" class="button">
|
|
<span class="icon icon-confirm"></span>
|
|
<?php p($l->t('Authorize via OAuth')); ?>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="mcp-grant-section">
|
|
<h4><?php p($l->t('Option 2: App Password (Works Today - Recommended)')); ?></h4>
|
|
<p class="mcp-help-text">
|
|
<?php p($l->t('Generate an app password in Security settings and provide it below. This is the recommended interim solution.')); ?>
|
|
</p>
|
|
|
|
<div class="mcp-app-password-steps">
|
|
<p><strong><?php p($l->t('Step 1:')); ?></strong>
|
|
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
|
|
<?php p($l->t('Generate app password in Security settings')); ?>
|
|
</a>
|
|
</p>
|
|
|
|
<p><strong><?php p($l->t('Step 2:')); ?></strong> <?php p($l->t('Enter the app password below:')); ?></p>
|
|
|
|
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.storeAppPassword')); ?>" id="mcp-app-password-form">
|
|
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
|
|
<div class="mcp-input-group">
|
|
<input type="password" name="appPassword" id="mcp-app-password-input"
|
|
placeholder="xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"
|
|
pattern="[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}"
|
|
required>
|
|
<button type="submit" class="button primary" id="mcp-save-app-password-button">
|
|
<span class="icon icon-checkmark"></span>
|
|
<?php p($l->t('Save')); ?>
|
|
</button>
|
|
</div>
|
|
<p class="mcp-help-text">
|
|
<?php p($l->t('The app password will be validated and securely encrypted before storage.')); ?>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php if (isset($_['session']['idp_profile'])): ?>
|
|
<div class="section">
|
|
<h2><?php p($l->t('Identity Provider Profile')); ?></h2>
|
|
<table class="mcp-info-table">
|
|
<?php foreach ($_['session']['idp_profile'] as $key => $value): ?>
|
|
<tr>
|
|
<td><?php p(ucfirst(str_replace('_', ' ', $key))); ?></td>
|
|
<td>
|
|
<?php if (is_array($value)): ?>
|
|
<?php p(implode(', ', $value)); ?>
|
|
<?php else: ?>
|
|
<?php p((string)$value); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($_['vectorSyncEnabled']): ?>
|
|
<div class="section">
|
|
<h2><?php p($l->t('Search Your Content')); ?></h2>
|
|
<p><?php p($l->t('Use natural language to search across your Notes, Files, Calendar, and Deck cards. Ask questions like "meeting notes from last week" or "recipes with chicken".')); ?></p>
|
|
<a href="<?php p($urlGenerator->linkToRoute('astrolabe.page.index')); ?>" class="button primary">
|
|
<span class="icon icon-search"></span>
|
|
<?php p($l->t('Open Astrolabe')); ?>
|
|
</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="section">
|
|
<h2><?php p($l->t('Semantic Search')); ?></h2>
|
|
<p>
|
|
<?php p($l->t('Semantic search is not enabled on this server. Contact your administrator to enable this feature.')); ?>
|
|
</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="section">
|
|
<h2><?php p($l->t('Manage Connection')); ?></h2>
|
|
<p><?php p($l->t('You are connected to the Astrolabe service.')); ?></p>
|
|
|
|
<div class="mcp-revoke-section">
|
|
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.oauth.disconnect')); ?>" id="mcp-disconnect-form">
|
|
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
|
|
<button type="submit" class="button warning" id="mcp-disconnect-button">
|
|
<span class="icon icon-close"></span>
|
|
<?php p($l->t('Disconnect')); ?>
|
|
</button>
|
|
<p class="mcp-help-text">
|
|
<?php p($l->t('This will disconnect from the Astrolabe service. You will need to re-authorize to use semantic search features.')); ?>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|