mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-06 22:21:29 +02:00
feat(oidc): show the env-managed provider as locked in settings
The API answers 409 to any write against this provider, so offering edit, delete and the enable toggle would promise a change that cannot land -- the operator would click, see nothing happen, and have no way to tell why. The controls are hidden and a lock badge names the reason instead. Reuses settings.environmentManagedLabel, the string the Home Assistant env-managed fields already use: same situation, same wording, and no new key to keep in parity across eleven locales. is_env_managed is optional on the client type so a response from an older backend still type-checks. Refs #2593
This commit is contained in:
@@ -325,3 +325,54 @@ describe('OIDCProviderSettings', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('env-managed provider (#2593)', () => {
|
||||
const envManagedProvider = {
|
||||
...mockProviders[0],
|
||||
id: 2,
|
||||
name: 'EnvIdP',
|
||||
is_env_managed: true,
|
||||
};
|
||||
|
||||
it('marks the provider as environment managed', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([envManagedProvider]))
|
||||
);
|
||||
render(<OIDCProviderSettings />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('EnvIdP')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(/Environment Managed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('offers no edit or delete control for it', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([envManagedProvider]))
|
||||
);
|
||||
render(<OIDCProviderSettings />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('EnvIdP')).toBeInTheDocument();
|
||||
});
|
||||
// Startup rewrites this row from the environment on every boot, and the API
|
||||
// answers 409 — offering the controls would promise an edit that cannot land.
|
||||
expect(screen.queryByTestId('edit-provider-2')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('delete-provider-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('still offers them for a UI-created provider', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/auth/oidc/providers/all', () =>
|
||||
HttpResponse.json([{ ...mockProviders[0], is_env_managed: false }])
|
||||
)
|
||||
);
|
||||
render(<OIDCProviderSettings />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('TestIdP')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('edit-provider-1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('delete-provider-1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3518,6 +3518,11 @@ export interface OIDCProvider {
|
||||
// #1589: when true, the LoginPage redirects unauthenticated visitors
|
||||
// straight to this provider on mount. At most one provider may carry this.
|
||||
is_autologin: boolean;
|
||||
// #2593: defined by BAMBUDDY_OIDC_* and rewritten from the environment on
|
||||
// every boot. The API answers 409 to any write, so the settings UI must not
|
||||
// offer edit/delete controls that cannot succeed. Optional so a response
|
||||
// from an older backend still type-checks.
|
||||
is_env_managed?: boolean;
|
||||
}
|
||||
|
||||
export interface OIDCProviderCreate {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Plus, Edit2, Trash2, Globe, Check, X, RefreshCw, ExternalLink, ImageOff } from 'lucide-react';
|
||||
import { Plus, Edit2, Trash2, Globe, Check, X, RefreshCw, ExternalLink, ImageOff, Lock } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { api } from '../api/client';
|
||||
import type { Group, OIDCProvider, OIDCProviderCreate } from '../api/client';
|
||||
@@ -386,6 +386,11 @@ export function OIDCProviderSettings() {
|
||||
<X className="w-3 h-3" /> {t('common.disabled')}
|
||||
</span>
|
||||
)}
|
||||
{provider.is_env_managed && (
|
||||
<span className="flex items-center gap-1 text-xs text-bambu-green">
|
||||
<Lock className="w-3 h-3" /> {t('settings.environmentManagedLabel')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-bambu-gray text-xs mt-0.5">
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
@@ -417,21 +422,34 @@ export function OIDCProviderSettings() {
|
||||
<ImageOff className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Toggle
|
||||
checked={provider.is_enabled}
|
||||
onChange={() => toggleEnabled(provider)}
|
||||
disabled={updateMutation.isPending}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setEditingId(editingId === provider.id ? null : provider.id)}
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" onClick={() => setDeleteTarget(provider)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
{/* #2593: startup rewrites the env-managed row from BAMBUDDY_OIDC_*
|
||||
and the API answers 409, so offering these would promise a
|
||||
change that cannot land. */}
|
||||
{!provider.is_env_managed && (
|
||||
<>
|
||||
<Toggle
|
||||
checked={provider.is_enabled}
|
||||
onChange={() => toggleEnabled(provider)}
|
||||
disabled={updateMutation.isPending}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setEditingId(editingId === provider.id ? null : provider.id)}
|
||||
data-testid={`edit-provider-${provider.id}`}
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => setDeleteTarget(provider)}
|
||||
data-testid={`delete-provider-${provider.id}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user