feat: add access profile selection support with device-specific profile persistence

Add SelectOwnProfile handler to allow users to choose from available access profiles. Store selected profile ID per device in settings table with device_access_profile category. Implement GetSelectedProfileID and SetSelectedProfileID repository methods using JSONB storage.

Add ListSelectableProfiles to policy repository and service to query user/group/device-specific profiles ordered by priority. Filter gateway
This commit is contained in:
2026-03-18 12:21:48 +01:00
parent 1ddcbf0b14
commit aaa601a8ba
14 changed files with 549 additions and 43 deletions

View File

@@ -33,23 +33,27 @@ export function AppHeader({
</div>
<div className="header-actions">
{enrolled ? (
<>
<ActionButton disabled={syncing} onClick={onSync} variant="secondary">
{syncing ? "Syncing..." : "Sync"}
</ActionButton>
<ActionButton onClick={onLogout} variant="ghost">
Logout
</ActionButton>
</>
) : null}
<ActionButton
disabled={!enrolled}
onClick={onToggleConnection}
variant={connected ? "danger" : "primary"}
>
{!enrolled ? "Provision first" : connected ? "Disconnect" : "Connect"}
</ActionButton>
<div className="header-actions-secondary">
{enrolled ? (
<>
<ActionButton disabled={syncing} onClick={onSync} variant="secondary">
{syncing ? "Syncing..." : "Sync"}
</ActionButton>
<ActionButton onClick={onLogout} variant="ghost">
Logout
</ActionButton>
</>
) : null}
</div>
<div className="header-actions-primary">
<ActionButton
disabled={!enrolled}
onClick={onToggleConnection}
variant={connected ? "danger" : "primary"}
>
{!enrolled ? "Provision first" : connected ? "Disconnect" : "Connect"}
</ActionButton>
</div>
</div>
</header>
);

View File

@@ -10,13 +10,34 @@ function ResourceListItem({ value }: { value: string }) {
}
type ResourcePanelProps = {
connected: boolean;
profiles: Array<{
id: string;
name: string;
description: string;
destinations: string[];
}>;
resources: string[];
profileLabel: string;
selectedProfileId: string | null;
selectingProfile: boolean;
onSelectProfile: (profileId: string) => void;
onReset: () => void;
};
export function ResourcePanel({ resources, profileLabel, onReset }: ResourcePanelProps) {
export function ResourcePanel({
connected,
profiles,
resources,
profileLabel,
selectedProfileId,
selectingProfile,
onSelectProfile,
onReset
}: ResourcePanelProps) {
const effectiveResources = resources.length > 0 ? resources : ["Keine Ressourcen zugewiesen"];
const showSelector = profiles.length > 1;
const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) ?? null;
return (
<aside className="resource-panel">
@@ -31,8 +52,29 @@ export function ResourcePanel({ resources, profileLabel, onReset }: ResourcePane
<div className="resource-meta">
<span className="resource-meta-label">Zugriffsprofil</span>
<strong>{profileLabel}</strong>
{selectedProfile?.description ? <small>{selectedProfile.description}</small> : null}
</div>
{showSelector ? (
<label className="resource-selector">
<span className="resource-meta-label">Ressource auswählen</span>
<select
disabled={connected || selectingProfile}
onChange={(event) => onSelectProfile(event.target.value)}
value={selectedProfileId ?? profiles[0]?.id ?? ""}
>
{profiles.map((profile) => (
<option key={profile.id} value={profile.id}>
{profile.name}
</option>
))}
</select>
<small>
{connected ? "Auswahl ist nur getrennt möglich." : selectingProfile ? "Profil wird aktualisiert..." : "Auswahl wird vor dem Verbinden in die Config übernommen."}
</small>
</label>
) : null}
<ul className="resource-list">
{effectiveResources.map((resource) => (
<ResourceListItem key={resource} value={resource} />