feat(access): add governed session management
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
||||
|
||||
export type AccountSession = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
current: boolean;
|
||||
status: "active" | "expired" | "revoked";
|
||||
created_at: string;
|
||||
last_seen_at?: string | null;
|
||||
expires_at: string;
|
||||
revoked_at?: string | null;
|
||||
client?: string | null;
|
||||
};
|
||||
|
||||
export type AccountSessionList = {
|
||||
sessions: AccountSession[];
|
||||
};
|
||||
|
||||
export function fetchAccountSessions(
|
||||
settings: ApiSettings
|
||||
): Promise<AccountSessionList> {
|
||||
return apiFetch<AccountSessionList>(settings, "/api/v1/auth/sessions", {
|
||||
cache: "no-store"
|
||||
});
|
||||
}
|
||||
|
||||
export function revokeAccountSession(
|
||||
settings: ApiSettings,
|
||||
sessionId: string
|
||||
): Promise<{ session: AccountSession; revoked: boolean }> {
|
||||
return apiFetch(settings, `/api/v1/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, {
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
export function revokeOtherAccountSessions(
|
||||
settings: ApiSettings
|
||||
): Promise<{ revoked_count: number }> {
|
||||
return apiFetch(settings, "/api/v1/auth/sessions/revoke-others", {
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchAdminUserSessions(
|
||||
settings: ApiSettings,
|
||||
userId: string
|
||||
): Promise<AccountSessionList> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/admin/users/${encodeURIComponent(userId)}/sessions`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
}
|
||||
|
||||
export function revokeAdminUserSession(
|
||||
settings: ApiSettings,
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
currentPassword: string
|
||||
): Promise<{ session: AccountSession; revoked: boolean }> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/admin/users/${encodeURIComponent(userId)}/sessions/${encodeURIComponent(sessionId)}/revoke`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ current_password: currentPassword })
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -367,7 +367,7 @@ export default function AdminPage({
|
||||
/>
|
||||
)}
|
||||
{!contributedSection && active === "system-roles" && <SystemRolesPanel settings={settings} canWrite={hasScope(auth, "system:roles:write")} onAuthRefresh={refreshAuth} />}
|
||||
{!contributedSection && active === "tenant-users" && <UsersPanel settings={settings} auth={auth} canCreate={hasScope(auth, "admin:users:create")} canUpdate={hasScope(auth, "admin:users:update")} canSuspend={hasScope(auth, "admin:users:suspend")} canManageGroups={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} onAuthRefresh={refreshAuth} />}
|
||||
{!contributedSection && active === "tenant-users" && <UsersPanel settings={settings} auth={auth} canCreate={hasScope(auth, "admin:users:create")} canUpdate={hasScope(auth, "admin:users:update")} canSuspend={hasScope(auth, "admin:users:suspend")} canManageGroups={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} canRevokeSessions={hasAnyScope(auth, ["admin:users:update", "access:membership:update"])} onAuthRefresh={refreshAuth} />}
|
||||
{!contributedSection && active === "tenant-groups" && <GroupsPanel settings={settings} auth={auth} canDefine={hasScope(auth, "admin:groups:write")} canManageMembers={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} onAuthRefresh={refreshAuth} />}
|
||||
{!contributedSection && active === "tenant-roles" && <RolesPanel settings={settings} auth={auth} canDefine={hasScope(auth, "admin:roles:write")} onAuthRefresh={refreshAuth} />}
|
||||
{!contributedSection && active === "tenant-function-role-mappings" && organizationFunctionPicker && <ExternalFunctionRoleMappingsPanel settings={settings} auth={auth} functionPicker={organizationFunctionPicker} canWrite={hasAnyScope(auth, ["admin:roles:write", "access:function:write", "access:role:assign"])} onAuthRefresh={refreshAuth} />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContentGrid, DescriptionItem, DescriptionList } from "@govoplan/core-webui";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { KeyRound, Pencil, Plus, Search, Trash2 } from "lucide-react";
|
||||
import { KeyRound, MonitorSmartphone, Pencil, Plus, Search, Trash2 } from "lucide-react";
|
||||
import type { FormGrid, ApiSettings, AuthInfo } from "@govoplan/core-webui";
|
||||
import { createUser, fetchGroupsDelta, fetchRolesDelta, fetchUserAccessExplanation, fetchUsersDelta, updateUser, type AccessRoleSourceItem, type FunctionFactExplanationItem, type GroupSummary, type RoleSummary, type UserAccessExplanationResponse, type UserAdminItem } from "../../api/admin";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
@@ -14,6 +14,11 @@ import { ConfirmDialog } from "@govoplan/core-webui";
|
||||
import { AdminIconButton, AdminPageLayout, AdminSelectionList, DocumentationHelpLink, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels } from "@govoplan/core-webui";
|
||||
import { hasTenantWildcard, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
import { loadDeltaRows } from "./utils/deltaRows";
|
||||
import {
|
||||
fetchAdminUserSessions,
|
||||
revokeAdminUserSession,
|
||||
type AccountSession
|
||||
} from "../../api/sessions";
|
||||
import {
|
||||
ACCESS_INTERFACE_I18N,
|
||||
ACCESS_WORKFLOW_DOCUMENTATION,
|
||||
@@ -30,7 +35,7 @@ const emptyDraft = {
|
||||
roleIds: [] as string[]
|
||||
};
|
||||
|
||||
export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSuspend, canManageGroups, canAssignRoles, onAuthRefresh
|
||||
export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSuspend, canManageGroups, canAssignRoles, canRevokeSessions, onAuthRefresh
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +44,7 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
||||
|
||||
|
||||
|
||||
}: {settings: ApiSettings;auth: AuthInfo;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;canManageGroups: boolean;canAssignRoles: boolean;onAuthRefresh: () => Promise<void>;}) {
|
||||
}: {settings: ApiSettings;auth: AuthInfo;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;canManageGroups: boolean;canAssignRoles: boolean;canRevokeSessions: boolean;onAuthRefresh: () => Promise<void>;}) {
|
||||
const [users, setUsers] = useState<UserAdminItem[]>([]);
|
||||
const [groups, setGroups] = useState<GroupSummary[]>([]);
|
||||
const [roles, setRoles] = useState<RoleSummary[]>([]);
|
||||
@@ -53,6 +58,12 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
||||
const [accessExplanation, setAccessExplanation] = useState<UserAccessExplanationResponse | null>(null);
|
||||
const [accessExplanationLoading, setAccessExplanationLoading] = useState(false);
|
||||
const [deactivating, setDeactivating] = useState<UserAdminItem | null>(null);
|
||||
const [sessionUser, setSessionUser] = useState<UserAdminItem | null>(null);
|
||||
const [accountSessions, setAccountSessions] = useState<AccountSession[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(false);
|
||||
const [sessionError, setSessionError] = useState("");
|
||||
const [revokingSession, setRevokingSession] = useState<AccountSession | null>(null);
|
||||
const [reauthorizationPassword, setReauthorizationPassword] = useState("");
|
||||
const [draft, setDraft] = useState(emptyDraft);
|
||||
const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft));
|
||||
const [temporaryPassword, setTemporaryPassword] = useState<{email: string;password: string;} | null>(null);
|
||||
@@ -186,6 +197,65 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserSessions(user: UserAdminItem) {
|
||||
setSessionsLoading(true);
|
||||
setSessionError("");
|
||||
try {
|
||||
const response = await fetchAdminUserSessions(settings, user.id);
|
||||
setAccountSessions(response.sessions);
|
||||
} catch (err) {
|
||||
setSessionError(adminErrorMessage(err));
|
||||
} finally {
|
||||
setSessionsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openUserSessions(user: UserAdminItem) {
|
||||
setSessionUser(user);
|
||||
setAccountSessions([]);
|
||||
setRevokingSession(null);
|
||||
setReauthorizationPassword("");
|
||||
void loadUserSessions(user);
|
||||
}
|
||||
|
||||
async function revokeSelectedSession() {
|
||||
if (!sessionUser || !revokingSession || !reauthorizationPassword) return;
|
||||
setBusy(true);
|
||||
setSessionError("");
|
||||
try {
|
||||
await revokeAdminUserSession(
|
||||
settings,
|
||||
sessionUser.id,
|
||||
revokingSession.id,
|
||||
reauthorizationPassword
|
||||
);
|
||||
setSuccess("i18n:govoplan-access.session_revoked.5e551008");
|
||||
setRevokingSession(null);
|
||||
setReauthorizationPassword("");
|
||||
await loadUserSessions(sessionUser);
|
||||
} catch (err) {
|
||||
setSessionError(adminErrorMessage(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const sessionColumns = useMemo<DataGridColumn<AccountSession>[]>(() => [
|
||||
{ id: "client", header: "i18n:govoplan-access.device_or_client.5e551002", width: "minmax(220px, 1fr)", fill: true, value: (row) => row.client || "", render: (row) => <div><strong>{row.current ? "i18n:govoplan-access.current_session.5e551003" : "i18n:govoplan-access.other_session.5e551004"}</strong><div className="muted small-note">{row.client || "i18n:govoplan-access.client_details_unavailable.5e551005"}</div></div> },
|
||||
{ id: "last_seen", header: "i18n:govoplan-access.last_seen.5e551006", width: 180, value: (row) => row.last_seen_at || "", render: (row) => formatDateTime(row.last_seen_at) },
|
||||
{ id: "created", header: "i18n:govoplan-access.created.accf40c8", width: 180, value: (row) => row.created_at, render: (row) => formatDateTime(row.created_at) },
|
||||
{ id: "expires", header: "i18n:govoplan-access.expires.a99be3da", width: 180, value: (row) => row.expires_at, render: (row) => formatDateTime(row.expires_at) },
|
||||
{ id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 96, sticky: "end", align: "right", render: (row) => <TableActionGroup actions={[{
|
||||
id: "revoke-session",
|
||||
label: "i18n:govoplan-access.revoke_session.5e551007",
|
||||
variant: "danger",
|
||||
applicable: !row.current,
|
||||
disabled: busy || !canRevokeSessions,
|
||||
disabledReason: !canRevokeSessions ? "i18n:govoplan-access.session_revocation_permission_required.5e551016" : busy ? ACCESS_INTERFACE_I18N.operationInProgress : undefined,
|
||||
onClick: () => { setRevokingSession(row); setReauthorizationPassword(""); setSessionError(""); }
|
||||
}]} /> }
|
||||
], [busy, canRevokeSessions]);
|
||||
|
||||
const columns = useMemo<DataGridColumn<UserAdminItem>[]>(() => [
|
||||
{ id: "user", header: "i18n:govoplan-access.user.9f8a2389", width: "minmax(230px, 1fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.display_name || ""} ${row.email}`, render: (row) => <div><strong>{row.display_name || row.email}</strong><div className="muted small-note">{row.email}</div></div> },
|
||||
{ id: "groups", header: "i18n:govoplan-access.groups.ae9629f4", width: 210, minWidth: 150, maxWidth: 420, resizable: true, sortable: true, filterable: true, value: (row) => joinLabels(row.groups) },
|
||||
@@ -195,11 +265,12 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
||||
{ id: "last_login", header: "i18n:govoplan-access.last_login.43dab84f", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.last_login_at || "", render: (row) => formatDateTime(row.last_login_at) },
|
||||
{ id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 190, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[
|
||||
{ id: "inspect", label: i18nMessage("i18n:govoplan-access.inspect_value.9d5d1071", { value0: row.email }), icon: <Search />, onClick: () => setViewing(row) },
|
||||
{ id: "sessions", label: i18nMessage("i18n:govoplan-access.inspect_sessions_for_value.5e551017", { value0: row.email }), icon: <MonitorSmartphone />, onClick: () => openUserSessions(row) },
|
||||
{ id: "explain", label: i18nMessage("i18n:govoplan-access.explain_access_for_value.3af96e47", { value0: row.email }), icon: <KeyRound />, onClick: () => void openAccessExplanation(row) },
|
||||
{ id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.email }), icon: <Pencil />, disabled: !(canUpdate || canSuspend || canManageGroups || canAssignRoles), disabledReason: !(canUpdate || canSuspend || canManageGroups || canAssignRoles) ? ACCESS_INTERFACE_I18N.updatePermissionRequired : undefined, onClick: () => openEdit(row) },
|
||||
{ id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.email }), icon: <Trash2 />, variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.is_last_active_owner, disabledReason: !row.is_active ? "i18n:govoplan-access.inactive.09af574c" : !canSuspend ? ACCESS_INTERFACE_I18N.updatePermissionRequired : row.is_last_active_owner ? ACCESS_INTERFACE_I18N.lastOwnerCannotBeDeactivated : undefined, onClick: () => setDeactivating(row) }
|
||||
]} /> }],
|
||||
[canAssignRoles, canManageGroups, canSuspend, canUpdate, settings]);
|
||||
[canAssignRoles, canManageGroups, canRevokeSessions, canSuspend, canUpdate, settings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -241,6 +312,22 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
||||
</DescriptionList>}
|
||||
</Dialog>
|
||||
|
||||
<Dialog variant="administration" size="wide" open={Boolean(sessionUser && !revokingSession)} title="i18n:govoplan-access.user_sessions.5e551018" onClose={() => !busy && setSessionUser(null)} className="" footer={<><Button onClick={() => sessionUser && void loadUserSessions(sessionUser)} disabled={sessionsLoading || busy} disabledReason={sessionsLoading || busy ? ACCESS_INTERFACE_I18N.loading : undefined}>i18n:govoplan-access.reload.cce71553</Button><Button onClick={() => setSessionUser(null)} disabled={busy}>i18n:govoplan-access.close.bbfa773e</Button></>}>
|
||||
{sessionError && <p className="admin-protection-note">{sessionError}</p>}
|
||||
{sessionUser && <>
|
||||
<p className="muted small-note">{sessionUser.display_name || sessionUser.email} · {sessionUser.email}</p>
|
||||
<div className="admin-table-surface"><DataGrid id="admin-user-sessions-v1" rows={accountSessions} columns={sessionColumns} initialFit="container" getRowKey={(row) => row.id} loading={sessionsLoading} emptyText="i18n:govoplan-access.no_active_sessions.5e551013" /></div>
|
||||
</>}
|
||||
</Dialog>
|
||||
|
||||
<Dialog variant="administration" size="large" open={Boolean(revokingSession)} title="i18n:govoplan-access.revoke_session.5e551007" onClose={() => !busy && setRevokingSession(null)} className="" footer={<><Button onClick={() => setRevokingSession(null)} disabled={busy}>i18n:govoplan-access.cancel.77dfd213</Button><Button variant="danger" onClick={() => void revokeSelectedSession()} disabled={busy || !reauthorizationPassword} disabledReason={!reauthorizationPassword ? "i18n:govoplan-access.current_password_required.5e551019" : busy ? ACCESS_INTERFACE_I18N.operationInProgress : undefined}>i18n:govoplan-access.revoke_session.5e551007</Button></>}>
|
||||
{sessionError && <p className="admin-protection-note">{sessionError}</p>}
|
||||
<p>i18n:govoplan-access.admin_session_revocation_confirmation.5e551020</p>
|
||||
<FormField label="i18n:govoplan-access.current_password.5e551021">
|
||||
<PasswordField value={reauthorizationPassword} autoComplete="current-password" onValueChange={setReauthorizationPassword} />
|
||||
</FormField>
|
||||
</Dialog>
|
||||
|
||||
<Dialog variant="administration" size="wide" open={Boolean(explaining)} title="i18n:govoplan-access.access_explanation.75ee7f62" onClose={() => { if (!accessExplanationLoading) { setExplaining(null); setAccessExplanation(null); } }} className="" footer={<Button onClick={() => { setExplaining(null); setAccessExplanation(null); }} disabled={accessExplanationLoading} disabledReason={accessExplanationLoading ? ACCESS_INTERFACE_I18N.loading : undefined}>i18n:govoplan-access.close.bbfa773e</Button>}>
|
||||
{accessExplanationLoading && <p className="muted small-note">i18n:govoplan-access.loading_access_explanation.04a7c934</p>}
|
||||
{accessExplanation && <>
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
ConfirmDialog,
|
||||
ContentGrid,
|
||||
DataGrid,
|
||||
DismissibleAlert,
|
||||
PageActionBar,
|
||||
StatusBadge,
|
||||
TableActionGroup,
|
||||
formatAdminDateTime,
|
||||
type ApiSettings,
|
||||
type AuthInfo,
|
||||
type DataGridColumn
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
fetchAccountSessions,
|
||||
revokeAccountSession,
|
||||
revokeOtherAccountSessions,
|
||||
type AccountSession
|
||||
} from "../../api/sessions";
|
||||
|
||||
export default function SessionSettingsPanel({
|
||||
settings,
|
||||
auth
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
}) {
|
||||
const [sessions, setSessions] = useState<AccountSession[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const [revoking, setRevoking] = useState<AccountSession | null>(null);
|
||||
const [revokingOthers, setRevokingOthers] = useState(false);
|
||||
const interactive = auth.principal?.auth_method === "session";
|
||||
|
||||
async function load() {
|
||||
if (!interactive) {
|
||||
setSessions([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await fetchAccountSessions(settings);
|
||||
setSessions(response.sessions);
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : String(reason));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [
|
||||
auth.principal?.session_id,
|
||||
settings.accessToken,
|
||||
settings.apiBaseUrl,
|
||||
settings.apiKey
|
||||
]);
|
||||
|
||||
const columns = useMemo<DataGridColumn<AccountSession>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "client",
|
||||
header: "i18n:govoplan-access.device_or_client.5e551002",
|
||||
width: "minmax(220px, 1fr)",
|
||||
minWidth: 180,
|
||||
fill: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
value: (row) => row.client || "",
|
||||
render: (row) => (
|
||||
<div>
|
||||
<strong>
|
||||
{row.current
|
||||
? "i18n:govoplan-access.current_session.5e551003"
|
||||
: "i18n:govoplan-access.other_session.5e551004"}
|
||||
</strong>
|
||||
<div className="muted small-note">
|
||||
{row.client || "i18n:govoplan-access.client_details_unavailable.5e551005"}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "i18n:govoplan-access.status.bae7d5be",
|
||||
width: 120,
|
||||
value: (row) => row.status,
|
||||
render: (row) => <StatusBadge status={row.status} />
|
||||
},
|
||||
{
|
||||
id: "last_seen",
|
||||
header: "i18n:govoplan-access.last_seen.5e551006",
|
||||
width: 180,
|
||||
sortable: true,
|
||||
value: (row) => row.last_seen_at || "",
|
||||
render: (row) => formatAdminDateTime(row.last_seen_at)
|
||||
},
|
||||
{
|
||||
id: "created",
|
||||
header: "i18n:govoplan-access.created.accf40c8",
|
||||
width: 180,
|
||||
sortable: true,
|
||||
value: (row) => row.created_at,
|
||||
render: (row) => formatAdminDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
id: "expires",
|
||||
header: "i18n:govoplan-access.expires.a99be3da",
|
||||
width: 180,
|
||||
sortable: true,
|
||||
value: (row) => row.expires_at,
|
||||
render: (row) => formatAdminDateTime(row.expires_at)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "i18n:govoplan-access.actions.c3cd636a",
|
||||
width: 96,
|
||||
sticky: "end",
|
||||
align: "right",
|
||||
render: (row) => (
|
||||
<TableActionGroup
|
||||
actions={[
|
||||
{
|
||||
id: "revoke",
|
||||
label: "i18n:govoplan-access.revoke_session.5e551007",
|
||||
variant: "danger",
|
||||
applicable: !row.current,
|
||||
disabled: busy,
|
||||
disabledReason: busy
|
||||
? "i18n:govoplan-access.an_access_administration_operation_is_in_progress.4af2c011"
|
||||
: undefined,
|
||||
onClick: () => setRevoking(row)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
],
|
||||
[busy]
|
||||
);
|
||||
|
||||
async function revokeOne() {
|
||||
if (!revoking) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await revokeAccountSession(settings, revoking.id);
|
||||
setRevoking(null);
|
||||
setSuccess("i18n:govoplan-access.session_revoked.5e551008");
|
||||
await load();
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : String(reason));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeOthers() {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await revokeOtherAccountSessions(settings);
|
||||
setRevokingOthers(false);
|
||||
setSuccess(
|
||||
response.revoked_count
|
||||
? "i18n:govoplan-access.other_sessions_revoked.5e551009"
|
||||
: "i18n:govoplan-access.no_other_active_sessions.5e551010"
|
||||
);
|
||||
await load();
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : String(reason));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!interactive) {
|
||||
return (
|
||||
<ContentGrid columns={1} collapseAt="workspace" className="">
|
||||
<Card title="i18n:govoplan-access.sessions_and_devices.5e551001">
|
||||
<p>i18n:govoplan-access.browser_session_required.5e551011</p>
|
||||
</Card>
|
||||
</ContentGrid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentGrid columns={1} collapseAt="workspace" className="">
|
||||
<PageActionBar
|
||||
variant="detail"
|
||||
actionScope="workspace"
|
||||
refreshable
|
||||
reloadAction={{
|
||||
onReload: () => void load(),
|
||||
loading,
|
||||
disabledReason: loading
|
||||
? "i18n:govoplan-access.administration_data_is_loading.4af2c001"
|
||||
: undefined
|
||||
}}
|
||||
destructiveActions={
|
||||
<Button
|
||||
variant="danger"
|
||||
disabled={busy || sessions.filter((item) => !item.current).length === 0}
|
||||
disabledReason={
|
||||
busy
|
||||
? "i18n:govoplan-access.an_access_administration_operation_is_in_progress.4af2c011"
|
||||
: sessions.filter((item) => !item.current).length === 0
|
||||
? "i18n:govoplan-access.no_other_active_sessions.5e551010"
|
||||
: undefined
|
||||
}
|
||||
onClick={() => setRevokingOthers(true)}
|
||||
>
|
||||
i18n:govoplan-access.revoke_all_other_sessions.5e551012
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{error && <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>}
|
||||
{success && <DismissibleAlert tone="success" resetKey={success}>{success}</DismissibleAlert>}
|
||||
<Card title="i18n:govoplan-access.sessions_and_devices.5e551001">
|
||||
<div className="admin-table-surface">
|
||||
<DataGrid
|
||||
id="personal-sessions-v1"
|
||||
rows={sessions}
|
||||
columns={columns}
|
||||
initialFit="container"
|
||||
getRowKey={(row) => row.id}
|
||||
emptyText="i18n:govoplan-access.no_active_sessions.5e551013"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<ConfirmDialog
|
||||
open={Boolean(revoking)}
|
||||
title="i18n:govoplan-access.revoke_session.5e551007"
|
||||
message="i18n:govoplan-access.revoke_session_confirmation.5e551014"
|
||||
confirmLabel="i18n:govoplan-access.revoke_session.5e551007"
|
||||
tone="danger"
|
||||
busy={busy}
|
||||
onCancel={() => setRevoking(null)}
|
||||
onConfirm={() => void revokeOne()}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={revokingOthers}
|
||||
title="i18n:govoplan-access.revoke_all_other_sessions.5e551012"
|
||||
message="i18n:govoplan-access.revoke_other_sessions_confirmation.5e551015"
|
||||
confirmLabel="i18n:govoplan-access.revoke_all_other_sessions.5e551012"
|
||||
tone="danger"
|
||||
busy={busy}
|
||||
onCancel={() => setRevokingOthers(false)}
|
||||
onConfirm={() => void revokeOthers()}
|
||||
/>
|
||||
</ContentGrid>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,27 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
"en": {
|
||||
"i18n:govoplan-access.sessions_and_devices.5e551001": "Sessions and devices",
|
||||
"i18n:govoplan-access.device_or_client.5e551002": "Device or client",
|
||||
"i18n:govoplan-access.current_session.5e551003": "Current session",
|
||||
"i18n:govoplan-access.other_session.5e551004": "Other session",
|
||||
"i18n:govoplan-access.client_details_unavailable.5e551005": "Client details unavailable",
|
||||
"i18n:govoplan-access.last_seen.5e551006": "Last seen",
|
||||
"i18n:govoplan-access.revoke_session.5e551007": "Revoke session",
|
||||
"i18n:govoplan-access.session_revoked.5e551008": "Session revoked.",
|
||||
"i18n:govoplan-access.other_sessions_revoked.5e551009": "All other active sessions were revoked.",
|
||||
"i18n:govoplan-access.no_other_active_sessions.5e551010": "There are no other active sessions.",
|
||||
"i18n:govoplan-access.browser_session_required.5e551011": "Session management is available only from an interactive browser session.",
|
||||
"i18n:govoplan-access.revoke_all_other_sessions.5e551012": "Revoke all other sessions",
|
||||
"i18n:govoplan-access.no_active_sessions.5e551013": "No active sessions were found.",
|
||||
"i18n:govoplan-access.revoke_session_confirmation.5e551014": "This device or client will lose access on its next authenticated request. The current session remains active.",
|
||||
"i18n:govoplan-access.revoke_other_sessions_confirmation.5e551015": "Revoke every other active session for this account? This current session remains active.",
|
||||
"i18n:govoplan-access.session_revocation_permission_required.5e551016": "Membership update permission is required to revoke sessions.",
|
||||
"i18n:govoplan-access.inspect_sessions_for_value.5e551017": "Inspect sessions for {value0}",
|
||||
"i18n:govoplan-access.user_sessions.5e551018": "User sessions",
|
||||
"i18n:govoplan-access.current_password_required.5e551019": "Enter your current password to continue.",
|
||||
"i18n:govoplan-access.admin_session_revocation_confirmation.5e551020": "Re-authorize this administrative action with your current password. The selected session will lose access on its next authenticated request.",
|
||||
"i18n:govoplan-access.current_password.5e551021": "Current password",
|
||||
"i18n:govoplan-access.administration_data_is_loading.4af2c001": "Administration data is loading.",
|
||||
"i18n:govoplan-access.create_permission_is_required.4af2c002": "Create permission is required for this action.",
|
||||
"i18n:govoplan-access.update_or_assignment_permission_is_required.4af2c003": "Update or assignment permission is required for this action.",
|
||||
@@ -382,6 +403,27 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-access.your_current_roles_do_not_grant_administrative_a.6eafee69": "Your current roles do not grant administrative access."
|
||||
},
|
||||
"de": {
|
||||
"i18n:govoplan-access.sessions_and_devices.5e551001": "Sitzungen und Geräte",
|
||||
"i18n:govoplan-access.device_or_client.5e551002": "Gerät oder Client",
|
||||
"i18n:govoplan-access.current_session.5e551003": "Aktuelle Sitzung",
|
||||
"i18n:govoplan-access.other_session.5e551004": "Andere Sitzung",
|
||||
"i18n:govoplan-access.client_details_unavailable.5e551005": "Keine Clientdetails verfügbar",
|
||||
"i18n:govoplan-access.last_seen.5e551006": "Zuletzt aktiv",
|
||||
"i18n:govoplan-access.revoke_session.5e551007": "Sitzung widerrufen",
|
||||
"i18n:govoplan-access.session_revoked.5e551008": "Sitzung wurde widerrufen.",
|
||||
"i18n:govoplan-access.other_sessions_revoked.5e551009": "Alle anderen aktiven Sitzungen wurden widerrufen.",
|
||||
"i18n:govoplan-access.no_other_active_sessions.5e551010": "Es gibt keine anderen aktiven Sitzungen.",
|
||||
"i18n:govoplan-access.browser_session_required.5e551011": "Die Sitzungsverwaltung ist nur in einer interaktiven Browsersitzung verfügbar.",
|
||||
"i18n:govoplan-access.revoke_all_other_sessions.5e551012": "Alle anderen Sitzungen widerrufen",
|
||||
"i18n:govoplan-access.no_active_sessions.5e551013": "Es wurden keine aktiven Sitzungen gefunden.",
|
||||
"i18n:govoplan-access.revoke_session_confirmation.5e551014": "Dieses Gerät oder dieser Client verliert beim nächsten authentifizierten Aufruf den Zugriff. Die aktuelle Sitzung bleibt aktiv.",
|
||||
"i18n:govoplan-access.revoke_other_sessions_confirmation.5e551015": "Alle anderen aktiven Sitzungen dieses Kontos widerrufen? Diese aktuelle Sitzung bleibt aktiv.",
|
||||
"i18n:govoplan-access.session_revocation_permission_required.5e551016": "Zum Widerrufen von Sitzungen ist die Berechtigung zum Ändern von Mitgliedschaften erforderlich.",
|
||||
"i18n:govoplan-access.inspect_sessions_for_value.5e551017": "Sitzungen von {value0} prüfen",
|
||||
"i18n:govoplan-access.user_sessions.5e551018": "Benutzersitzungen",
|
||||
"i18n:govoplan-access.current_password_required.5e551019": "Geben Sie Ihr aktuelles Passwort ein, um fortzufahren.",
|
||||
"i18n:govoplan-access.admin_session_revocation_confirmation.5e551020": "Autorisieren Sie diese administrative Aktion erneut mit Ihrem aktuellen Passwort. Die ausgewählte Sitzung verliert beim nächsten authentifizierten Aufruf den Zugriff.",
|
||||
"i18n:govoplan-access.current_password.5e551021": "Aktuelles Passwort",
|
||||
"i18n:govoplan-access.administration_data_is_loading.4af2c001": "Administrationsdaten werden geladen.",
|
||||
"i18n:govoplan-access.create_permission_is_required.4af2c002": "Für diese Aktion ist die Berechtigung zum Erstellen erforderlich.",
|
||||
"i18n:govoplan-access.update_or_assignment_permission_is_required.4af2c003": "Für diese Aktion ist eine Berechtigung zum Ändern oder Zuweisen erforderlich.",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { default } from "./module";
|
||||
export * from "./module";
|
||||
export * from "./api/admin";
|
||||
export * from "./api/sessions";
|
||||
export { default as AdminPage } from "./features/admin/AdminPage";
|
||||
export { ResourceAccessExplanation } from "@govoplan/core-webui";
|
||||
export type { ResourceAccessExplanationOptions, ResourceAccessExplanationProps, ResourceAccessExplanationUser } from "@govoplan/core-webui";
|
||||
|
||||
+20
-3
@@ -1,10 +1,11 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type { ActingContextRuntimeUiCapability, PlatformRouteContext, PlatformWebModule } from "@govoplan/core-webui";
|
||||
import type { ActingContextRuntimeUiCapability, PlatformRouteContext, PlatformWebModule, SettingsSectionsUiCapability } from "@govoplan/core-webui";
|
||||
import { adminReadScopes } from "@govoplan/core-webui";
|
||||
import ActingContextSelector from "./features/acting-context/ActingContextSelector";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
|
||||
const AdminPage = lazy(() => import("./features/admin/AdminPage"));
|
||||
const SessionSettingsPanel = lazy(() => import("./features/sessions/SessionSettingsPanel"));
|
||||
|
||||
const translations = {
|
||||
en: generatedTranslations.en,
|
||||
@@ -24,9 +25,24 @@ const accessAdminSurfaces = [
|
||||
{ id: "access.admin.tenant-service-accounts", moduleId: "access", kind: "section" as const, label: "Service accounts", order: 90 },
|
||||
{ id: "access.admin.group-credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.group_credentials.4af2c025", order: 30 },
|
||||
{ id: "access.admin.user-credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.user_credentials.4af2c026", order: 30 },
|
||||
{ id: "access.settings.credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.reusable_credentials.4af2c022", order: 30 }
|
||||
{ id: "access.settings.credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.reusable_credentials.4af2c022", order: 30 },
|
||||
{ id: "access.settings.sessions", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.sessions_and_devices.5e551001", order: 20 }
|
||||
];
|
||||
|
||||
const accessSettingsSections: SettingsSectionsUiCapability = {
|
||||
sections: [
|
||||
{
|
||||
id: "sessions",
|
||||
surfaceId: "access.settings.sessions",
|
||||
label: "i18n:govoplan-access.sessions_and_devices.5e551001",
|
||||
group: "account",
|
||||
order: 20,
|
||||
allOf: ["access:session:manage_own"],
|
||||
render: ({ settings, auth }) => createElement(SessionSettingsPanel, { settings, auth })
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function renderAdminRoute({ settings, auth, onAuthChange }: PlatformRouteContext) {
|
||||
if (!onAuthChange) {
|
||||
throw new Error("i18n:govoplan-access.the_access_admin_route_requires_the_platform_aut.0173a45f");
|
||||
@@ -46,7 +62,8 @@ export const accessModule: PlatformWebModule = {
|
||||
routes: [
|
||||
{ path: "/admin", anyOf: adminReadScopes, order: 900, render: renderAdminRoute }],
|
||||
uiCapabilities: {
|
||||
"access.actingContext": { Selector: ActingContextSelector } satisfies ActingContextRuntimeUiCapability
|
||||
"access.actingContext": { Selector: ActingContextSelector } satisfies ActingContextRuntimeUiCapability,
|
||||
"settings.sections": accessSettingsSections
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user