Files
nextcloud-mcp-server/third_party/astrolabe/src/styles/settings.css
T
Chris Coutinho 65c3f099fa 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>
2025-12-22 19:39:13 +01:00

291 lines
6.4 KiB
CSS

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Astrolabe settings styles
* Relies on Nextcloud's core .section class for layout
*/
/* Info tables */
.mcp-info-table {
width: 100%;
border-collapse: collapse;
margin: calc(var(--default-grid-baseline) * 3) 0;
}
.mcp-info-table tr {
border-bottom: 1px solid var(--color-border);
}
.mcp-info-table tr:last-child {
border-bottom: none;
}
.mcp-info-table td {
padding: calc(var(--default-grid-baseline) * 2) 0;
vertical-align: top;
}
.mcp-info-table td:first-child {
width: 200px;
color: var(--color-text-maxcontrast);
font-weight: 600;
padding-inline-end: calc(var(--default-grid-baseline) * 4);
}
.mcp-info-table td:last-child {
color: var(--color-main-text);
}
/* Status badges */
.badge {
display: inline-flex;
align-items: center;
gap: calc(var(--default-grid-baseline) * 1.5);
padding: calc(var(--default-grid-baseline) * 1.5) calc(var(--default-grid-baseline) * 3);
border-radius: calc(var(--border-radius-element) * 1.5);
font-size: 13px;
font-weight: 600;
}
.badge-success {
background: var(--color-success);
color: var(--color-success-text);
}
.badge-warning {
background: var(--color-warning);
color: var(--color-warning-text);
}
.badge-neutral {
background: var(--color-background-dark);
color: var(--color-text-maxcontrast);
}
.badge-info {
background: var(--color-primary-element);
color: var(--color-primary-element-text);
}
/* Input groups */
.mcp-input-group {
display: flex;
gap: calc(var(--default-grid-baseline) * 2);
align-items: stretch;
margin-top: calc(var(--default-grid-baseline) * 2);
}
.mcp-input-group input[type='password'],
.mcp-input-group input[type='text'] {
flex: 1;
font-family: monospace;
}
/* Revoke/warning sections */
.mcp-revoke-section {
margin-top: calc(var(--default-grid-baseline) * 4);
padding: calc(var(--default-grid-baseline) * 4);
background: var(--color-warning);
border-radius: var(--border-radius-element);
border-inline-start: calc(var(--default-grid-baseline)) solid var(--color-warning-text);
}
/* Feature lists */
.mcp-feature-list {
list-style: none;
padding: 0;
margin: calc(var(--default-grid-baseline) * 3) 0;
}
.mcp-feature-list li {
display: flex;
gap: calc(var(--default-grid-baseline) * 3);
padding: calc(var(--default-grid-baseline) * 2) 0;
align-items: start;
}
.mcp-feature-list .icon {
flex-shrink: 0;
width: 24px;
height: 24px;
opacity: 0.7;
}
.mcp-feature-list div {
flex: 1;
}
.mcp-feature-list strong {
display: block;
font-weight: 600;
margin-bottom: calc(var(--default-grid-baseline));
}
.mcp-feature-list p {
margin: 0;
color: var(--color-text-maxcontrast);
}
/* Responsive tables */
@media (max-width: 768px) {
.mcp-info-table td:first-child,
.mcp-info-table td:last-child {
display: block;
width: 100%;
}
.mcp-info-table td:first-child {
padding-bottom: calc(var(--default-grid-baseline));
}
.mcp-info-table td:last-child {
padding-top: calc(var(--default-grid-baseline));
}
}
/* Admin settings forms */
.mcp-settings-form {
max-width: 600px;
}
.mcp-form-group {
margin-bottom: calc(var(--default-grid-baseline) * 5);
}
.mcp-form-group label {
display: block;
font-weight: 600;
margin-bottom: calc(var(--default-grid-baseline) * 2);
}
.mcp-range {
width: 100%;
margin-top: calc(var(--default-grid-baseline) * 2);
accent-color: var(--color-primary-element);
}
.mcp-form-actions {
display: flex;
align-items: center;
gap: calc(var(--default-grid-baseline) * 4);
margin-top: calc(var(--default-grid-baseline) * 6);
padding-top: calc(var(--default-grid-baseline) * 5);
border-top: 1px solid var(--color-border);
}
/* Webhook preset cards */
.mcp-preset-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: calc(var(--default-grid-baseline) * 4);
margin: calc(var(--default-grid-baseline) * 4) 0;
}
.mcp-preset-card {
background: var(--color-background-dark);
border-radius: var(--border-radius-container);
padding: calc(var(--default-grid-baseline) * 4);
border: 2px solid transparent;
transition: border-color var(--animation-slow), box-shadow var(--animation-slow);
}
.mcp-preset-card:hover {
border-color: var(--color-border-dark);
box-shadow: 0 2px 8px var(--color-box-shadow);
}
.mcp-preset-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: calc(var(--default-grid-baseline) * 3);
}
.mcp-preset-header h4 {
margin: 0;
font-weight: 600;
}
.mcp-preset-status {
padding: calc(var(--default-grid-baseline)) calc(var(--default-grid-baseline) * 2.5);
border-radius: calc(var(--border-radius-element) * 1.5);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.mcp-status-enabled {
background: var(--color-success);
color: var(--color-success-text);
}
.mcp-status-disabled {
background: var(--color-background-darker);
color: var(--color-text-maxcontrast);
}
.mcp-preset-description {
color: var(--color-text-maxcontrast);
margin-bottom: calc(var(--default-grid-baseline) * 3);
}
.mcp-preset-meta {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: calc(var(--default-grid-baseline) * 3);
border-top: 1px solid var(--color-border);
margin-bottom: calc(var(--default-grid-baseline) * 3);
font-size: 12px;
color: var(--color-text-maxcontrast);
}
.mcp-preset-actions {
display: flex;
gap: calc(var(--default-grid-baseline) * 2);
}
.mcp-preset-toggle {
flex: 1;
padding: calc(var(--default-grid-baseline) * 2) calc(var(--default-grid-baseline) * 4);
border-radius: var(--border-radius-element);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all var(--animation-quick);
border: none;
}
.mcp-preset-toggle.primary {
background: var(--color-primary-element);
color: var(--color-primary-element-text);
}
.mcp-preset-toggle.primary:hover:not(:disabled) {
background: var(--color-primary-element-hover);
}
.mcp-preset-toggle.secondary {
background: var(--color-background-darker);
color: var(--color-main-text);
border: 1px solid var(--color-border);
}
.mcp-preset-toggle.secondary:hover:not(:disabled) {
background: var(--color-background-hover);
border-color: var(--color-border-dark);
}
.mcp-preset-toggle:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mcp-loading {
text-align: center;
padding: calc(var(--default-grid-baseline) * 5);
color: var(--color-text-maxcontrast);
font-style: italic;
}