Skip to main content

Patient Picker Portal

What it is

The portal at http://localhost:9000/portal is the equivalent of Epic's LaunchPad — the UI where a clinician logs in, selects a patient from the HAPI FHIR server, and launches the SMART client app. It replaces Epic when developing or running the platform without Epic.

Flow

1. Clinician opens http://localhost:9000/portal
→ redirected to /login (not authenticated)

2. Clinician enters username + password
→ Spring Security form login
→ redirected to /portal on success

3. Portal fetches patients from HAPI FHIR
→ GET http://localhost:8080/fhir/Patient?_count=20
→ renders patient list

4. Clinician selects a patient, clicks "Launch App"
→ POST /portal/launch
patientId=ePatient-8675309
encounterId=eEncounter-001 (optional)

5. Server creates a launch token (32-byte random, 5-minute expiry)
→ stored in PostgreSQL

6. Browser redirects to SMART client:
GET http://localhost:8081/launch
?iss=http://localhost:8080/fhir
&launch={token}

7. SMART client completes OAuth2 handshake
→ SmartTokenCustomizer resolves the launch token
→ patient+encounter in token response

LaunchPortalController

@GetMapping
public String portal(Model model,
@RequestParam(required = false) String search) {
List<Map<String, String>> patients = fetchPatients(search);
model.addAttribute("patients", patients);
model.addAttribute("fhirBaseUrl", serverProperties.fhirBaseUrl());
return "portal";
}

@PostMapping("/launch")
public String launch(
@RequestParam String patientId,
@RequestParam(required = false) String encounterId,
@AuthenticationPrincipal UserDetails principal) {

String launchToken = launchContextService.createLaunchToken(
patientId, encounterId,
serverProperties.defaultNeedPatientBanner(),
DEFAULT_CLIENT_ID,
principal.getUsername()
);

String launchUrl = SMART_CLIENT_LAUNCH_URL
+ "?iss=" + serverProperties.fhirBaseUrl()
+ "&launch=" + launchToken;

return "redirect:" + launchUrl;
}

The portal supports patient search by name:

GET /portal?search=Kumar

This passes search to fetchPatients() which calls:

client.search().forResource(Patient.class)
.where(Patient.NAME.matches().value(search))
.count(20)
.returnBundle(Bundle.class)
.execute();

Configuring the SMART client URL

The SMART client launch URL is configured via smart.server.smart-client-launch-url. It defaults to http://localhost:8081/launch for local development and must be set explicitly for any other deployment:

smart:
server:
smart-client-launch-url: ${SMART_CLIENT_LAUNCH_URL:http://localhost:8081/launch}

Or via environment variable:

export SMART_CLIENT_LAUNCH_URL=https://smart.yourhospital.org/launch

The portal reads this value from SmartServerProperties.smartClientLaunchUrl() — there is no hardcoded URL in the source.

Clinician credentials

The DataInitializer seeds one test clinician on first startup:

UsernamePasswordDisplay name
drsmithpasswordDr. Jane Smith

Add more clinicians by inserting rows into the clinician table:

INSERT INTO clinician (id, username, password_hash, display_name, fhir_user_id)
VALUES (
gen_random_uuid(),
'drjones',
'$2a$12$...bcrypt-hash...',
'Dr. Bob Jones',
'eProvider-DEF456'
);

Generate a BCrypt hash: spring security encode --password yourpassword or use any online BCrypt generator.


Scope Interceptor · Developer Guide Home