Skip to main content
Glama
SecurityPage.tsx3.91 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Anchor, Button, Table, Title } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import type { ProfileResource } from '@medplum/core'; import { formatDateTime, formatHumanName, getReferenceString, normalizeErrorString } from '@medplum/core'; import type { HumanName, UserConfiguration } from '@medplum/fhirtypes'; import { DescriptionList, DescriptionListEntry, Document, useMedplum } from '@medplum/react'; import type { JSX } from 'react'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; interface UserSession { readonly id: string; readonly lastUpdated: string; readonly authMethod: string; readonly remoteAddress: string; readonly browser?: string; readonly os?: string; } interface SecurityDetails { readonly profile: ProfileResource; readonly config: UserConfiguration; readonly security: { mfaEnrolled: boolean; sessions: UserSession[]; }; } export function SecurityPage(): JSX.Element | null { const navigate = useNavigate(); const medplum = useMedplum(); const [details, setDetails] = useState<SecurityDetails | undefined>(); useEffect(() => { medplum .get('auth/me', { cache: 'no-cache' }) .then(setDetails) .catch((err) => showNotification({ color: 'red', message: normalizeErrorString(err), autoClose: false })); }, [medplum]); function revokeLogin(loginId: string): void { medplum .post('auth/revoke', { loginId }) .then(() => medplum.get('auth/me', { cache: 'no-cache' })) .then(setDetails) .then(() => showNotification({ color: 'green', message: 'Login revoked' })) .catch((err) => showNotification({ color: 'red', message: normalizeErrorString(err), autoClose: false })); } if (!details) { return null; } return ( <> <Document> <Title>Security</Title> <DescriptionList> <DescriptionListEntry term="ID"> <Anchor href={`/${getReferenceString(details.profile)}`}>{details.profile.id}</Anchor> </DescriptionListEntry> <DescriptionListEntry term="Resource Type">{details.profile.resourceType}</DescriptionListEntry> <DescriptionListEntry term="Name"> {formatHumanName(details.profile.name?.[0] as HumanName)} </DescriptionListEntry> </DescriptionList> </Document> <Document> <Title>Sessions</Title> <Table> <thead> <tr> <th>OS</th> <th>Browser</th> <th>IP Address</th> <th>Auth Method</th> <th>Last Updated</th> <th /> </tr> </thead> <tbody> {details.security.sessions.map((session) => ( <tr key={session.id}> <td>{session.os}</td> <td>{session.browser}</td> <td>{session.remoteAddress}</td> <td>{session.authMethod}</td> <td>{formatDateTime(session.lastUpdated)}</td> <td> <Anchor href="#" onClick={() => revokeLogin(session.id)}> Revoke </Anchor> </td> </tr> ))} </tbody> </Table> </Document> <Document> <Title>Password</Title> <Button onClick={() => navigate('/changepassword')?.catch(console.error)}>Change password</Button> </Document> <Document> <Title>Multi Factor Auth</Title> <p>Enrolled: {details.security.mfaEnrolled.toString()}</p> {!details.security.mfaEnrolled && ( <Button onClick={() => navigate('/mfa')?.catch(console.error)}>Enroll</Button> )} </Document> </> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server