Skip to main content
Glama
SignInForm.tsx6.39 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { showNotification } from '@mantine/notifications'; import type { BaseLoginRequest, LoginAuthenticationResponse } from '@medplum/core'; import { normalizeErrorString } from '@medplum/core'; import type { ProjectMembership } from '@medplum/fhirtypes'; import { useMedplum } from '@medplum/react-hooks'; import type { JSX, ReactNode } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Document } from '../Document/Document'; import { AuthenticationForm } from './AuthenticationForm'; import { ChooseProfileForm } from './ChooseProfileForm'; import { ChooseScopeForm } from './ChooseScopeForm'; import { MfaForm } from './MfaForm'; import { NewProjectForm } from './NewProjectForm'; export interface SignInFormProps extends BaseLoginRequest { readonly login?: string; readonly chooseScopes?: boolean; readonly disableEmailAuth?: boolean; readonly disableGoogleAuth?: boolean; readonly onSuccess?: () => void; readonly onForgotPassword?: () => void; readonly onRegister?: () => void; readonly onCode?: (code: string) => void; readonly children?: ReactNode; } /** * The SignInForm component allows users to sign in to Medplum. * * "Signing in" is a multi-step process: * 1) Authentication - identify the user * 2) MFA - If MFA is enabled, prompt for MFA code * 3) Choose profile - If the user has multiple profiles, prompt to choose one * 4) Choose scope - If the user has multiple scopes, prompt to choose one * 5) Success - Return to the caller with either a code or a redirect * @param props - The SignInForm React props. * @returns The SignInForm React node. */ export function SignInForm(props: SignInFormProps): JSX.Element { const { login: loginCode, chooseScopes, onSuccess, onForgotPassword, onRegister, onCode, ...baseLoginRequest } = props; const medplum = useMedplum(); const [login, setLogin] = useState<string>(); const loginRequested = useRef(false); const [mfaEnrollRequired, setMfaEnrollRequired] = useState(false); const [enrollQrCode, setEnrollQrCode] = useState<string>(); const [mfaRequired, setMfaRequired] = useState(false); const [memberships, setMemberships] = useState<ProjectMembership[]>(); const handleCode = useCallback( (code: string): void => { if (onCode) { onCode(code); } else { medplum .processCode(code) .then(() => { if (onSuccess) { onSuccess(); } }) .catch((err: unknown) => showNotification({ color: 'red', message: normalizeErrorString(err) })); } }, [medplum, onCode, onSuccess] ); const handleAuthResponse = useCallback( (response: LoginAuthenticationResponse): void => { setMfaEnrollRequired(!!response.mfaEnrollRequired); setEnrollQrCode(response.enrollQrCode); setMfaRequired(!!response.mfaRequired); if (response.login) { setLogin(response.login); } if (response.memberships) { setMemberships(response.memberships); } if (response.code) { if (chooseScopes) { setMemberships(undefined); } else { handleCode(response.code as string); } } }, [chooseScopes, handleCode] ); const handleScopeResponse = useCallback( (response: LoginAuthenticationResponse): void => { handleCode(response.code as string); }, [handleCode] ); useEffect(() => { // Beware the race condition here // The `useMedplum` hook will return a new instance of the MedplumClient on login // We do not want to request the login status again in that case // Only request login status once if (loginCode && !loginRequested.current && !login) { loginRequested.current = true; medplum .get('auth/login/' + loginCode) .then(handleAuthResponse) .catch((err: unknown) => showNotification({ color: 'red', message: normalizeErrorString(err) })); } }, [medplum, loginCode, loginRequested, login, handleAuthResponse]); return ( <Document width={400} px="xl" py="xl" bdrs="md"> {(() => { if (!login) { return ( <AuthenticationForm onForgotPassword={onForgotPassword} onRegister={onRegister} handleAuthResponse={handleAuthResponse} disableGoogleAuth={props.disableGoogleAuth} disableEmailAuth={props.disableEmailAuth} {...baseLoginRequest} > {props.children} </AuthenticationForm> ); } else if (mfaEnrollRequired && enrollQrCode) { return ( <MfaForm title="Enroll in MFA" description="Scan this QR code with your authenticator app." buttonText="Enroll" qrCodeUrl={enrollQrCode} onSubmit={async (fields) => { const res = await medplum.post('auth/mfa/login-enroll', { login: login as string, token: fields.token, }); handleAuthResponse(res); }} /> ); } else if (mfaRequired) { return ( <MfaForm title="Enter MFA code" description="Enter the code from your authenticator app." buttonText="Submit Code" onSubmit={async (fields) => { const res = await medplum.post('auth/mfa/verify', { login: login as string, token: fields.token, }); handleAuthResponse(res); }} /> ); } else if (props.projectId === 'new') { return <NewProjectForm login={login} handleAuthResponse={handleAuthResponse} />; } else if (memberships) { return <ChooseProfileForm login={login} memberships={memberships} handleAuthResponse={handleAuthResponse} />; } else if (props.chooseScopes) { return <ChooseScopeForm login={login} scope={props.scope} handleAuthResponse={handleScopeResponse} />; } else { return <div>Success</div>; } })()} </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