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

@@ -4,9 +4,19 @@ import { AppHeader } from "./components/AppHeader";
import { ResourcePanel } from "./components/ResourcePanel";
import { StatusCard } from "./components/StatusCard";
type AccessProfile = {
id: string;
name: string;
description: string;
fullTunnel: boolean;
destinations: string[];
};
type EnrollmentState = {
assignedIp: string;
resources: string[];
availableProfiles: AccessProfile[];
selectedProfileId: string | null;
profileRevision: number;
gatewayEndpoint: string;
profilePath: string;
@@ -36,6 +46,11 @@ function currentProfileLabel(state: EnrollmentState | null) {
return "Not provisioned";
}
const selectedProfile = state.availableProfiles.find((profile) => profile.id === state.selectedProfileId);
if (selectedProfile) {
return selectedProfile.name;
}
if (state.resources.includes("0.0.0.0/0")) {
return "Full tunnel";
}
@@ -53,6 +68,7 @@ export function App() {
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [syncing, setSyncing] = useState(false);
const [selectingProfile, setSelectingProfile] = useState(false);
const [error, setError] = useState<string | null>(null);
const [connected, setConnected] = useState(false);
const [state, setState] = useState<EnrollmentState | null>(null);
@@ -150,6 +166,25 @@ export function App() {
}
}
async function onSelectProfile(profileId: string) {
if (!state || profileId === state.selectedProfileId) {
return;
}
setSelectingProfile(true);
setError(null);
try {
const result = await invoke<EnrollmentState>("select_access_profile", { profileId });
setState(result);
await refreshTunnelStatus();
} catch (err) {
setError(formatInvokeError(err, "Profile selection failed"));
} finally {
setSelectingProfile(false);
}
}
async function toggleConnection() {
const command = connected ? "disconnect_tunnel" : "connect_tunnel";
try {
@@ -248,9 +283,14 @@ export function App() {
)}
<ResourcePanel
connected={connected}
onReset={resetEnrollment}
onSelectProfile={onSelectProfile}
profileLabel={profileLabel}
profiles={state?.availableProfiles ?? []}
resources={state?.resources ?? []}
selectedProfileId={state?.selectedProfileId ?? null}
selectingProfile={selectingProfile}
/>
</div>
</div>