diff --git a/development/docker-compose.yaml b/development/docker-compose.yaml index 520fe75..2f988f6 100644 --- a/development/docker-compose.yaml +++ b/development/docker-compose.yaml @@ -1,12 +1,19 @@ services: # Lightweight Keycloak server for local development. keycloak: - image: ghcr.io/healthcare-monitoring-system/d4-platform-devops:latest + image: quay.io/keycloak/keycloak:26.6.1 + command: start-dev --import-realm environment: KC_BOOTSTRAP_ADMIN_USERNAME: admin KC_BOOTSTRAP_ADMIN_PASSWORD: admin ports: - "8080:8080" + volumes: + - ./keycloak/themes:/opt/keycloak/themes:ro + - ./keycloak/realm-config.json:/opt/keycloak/data/import/realm-config.json:ro + depends_on: + postgres: + condition: service_healthy # Your Backend Service backend: @@ -25,3 +32,6 @@ services: - "3000:3000" depends_on: - backend + +volumes: + postgres_data: \ No newline at end of file diff --git a/development/keycloak/realm-config.json b/development/keycloak/realm-config.json index d91cb9e..97bc2f3 100644 --- a/development/keycloak/realm-config.json +++ b/development/keycloak/realm-config.json @@ -1,89 +1,102 @@ { - "realm": "HealthCare", - "displayNameHtml": "

MediCare Sign-In

", - "enabled": true, - "registrationAllowed": true, - "registrationEmailAsUsername": false, - "rememberMe": true, - "verifyEmail": false, - "resetPasswordAllowed": true, - "editUsernameAllowed": false, - "users": [ + "realm": "HealthCare", + "displayNameHtml": "

MediCare Sign-In

", + "enabled": true, + "registrationAllowed": true, + "registrationEmailAsUsername": false, + "rememberMe": true, + "loginTheme": "medicare", + "verifyEmail": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "users": [ + { + "username": "Admin", + "enabled": true, + "emailVerified": true, + "credentials": [ { - "username": "Admin", - "enabled": true, - "emailVerified": true, - "credentials": [ - { - "type": "password", - "value": "admin" - } - ], - "realmRoles": [ - "admin" - ] - }, - { - "username": "Doctor1", - "enabled": true, - "emailVerified": true, - "credentials": [ - { - "type": "password", - "value": "user123" - } - ], - "realmRoles": [ - "doctor" - ] - }, + "type": "password", + "value": "admin" + } + ], + "realmRoles": [ + "admin" + ] + }, + { + "username": "Doctor1", + "enabled": true, + "emailVerified": true, + "credentials": [ { - "username": "test", - "email": "test@example.com", - "firstName": "Test", - "lastName": "User", - "enabled": true, - "emailVerified": true, - "credentials": [ - { - "type": "password", - "value": "test" - } - ], - "realmRoles": [ - "patient" - ] + "type": "password", + "value": "user123" } - ], - "roles": { - "realm": [ - { - "name": "admin", - "description": "Administrative role for the healthcare system" - }, - { - "name": "doctor", - "description": "Role for doctors in the healthcare system" - }, - { - "name": "patient", - "description": "Role for patients in the healthcare system" - } - ] + ], + "realmRoles": [ + "doctor" + ] }, - "clients": [ + { + "username": "test", + "email": "test@example.com", + "firstName": "Test", + "lastName": "User", + "enabled": true, + "emailVerified": true, + "credentials": [ { - "clientId": "medicare", - "publicClient": false, - "standardFlowEnabled": true, - "directAccessGrantsEnabled": false, - "redirectUris": [ - "http://localhost:3000/*" - ], - "webOrigins": [ - "http://localhost:3000/*", - "http://localhost:8000/*" - ] + "type": "password", + "value": "test" } + ], + "realmRoles": [ + "patient" + ] + } + ], + "roles": { + "realm": [ + { + "name": "admin", + "description": "Role for administrators in the healthcare system" + }, + { + "name": "doctor", + "description": "Role for doctors in the healthcare system" + }, + { + "name": "patient", + "description": "Role for patients in the healthcare system" + } ] -} + }, + "clients": [ + { + "clientId": "medicare", + "name": "MediCare Frontend", + "secret": "OVERRIDE-THIS-CLIENT-SECRET", + "baseUrl": "http://127.0.0.1:3000", + "publicClient": false, + "standardFlowEnabled": true, + "directAccessGrantsEnabled": false, + "redirectUris": [ + "http://localhost:3000/*", + "http://127.0.0.1:3000/*", + "http://127.0.0.1:8000/api/auth/callback", + "http://localhost:8000/api/auth/callback", + "http://localhost:3002/*" + ], + "attributes": { + "post.logout.redirect.uris": "http://localhost:3000/login##http://127.0.0.1:3000/login##http://localhost:3002/login" + }, + "webOrigins": [ + "http://localhost:3000", + "http://127.0.0.1:3000", + "http://localhost:3002", + "http://localhost:8000" + ] + } + ] +} \ No newline at end of file diff --git a/development/keycloak/themes/medicare/login/error.ftl b/development/keycloak/themes/medicare/login/error.ftl new file mode 100644 index 0000000..2faf5a2 --- /dev/null +++ b/development/keycloak/themes/medicare/login/error.ftl @@ -0,0 +1,27 @@ + + + + + + MediCare Sign-In Error + + + +
+
+
+ +

MediCare

+

Sign-In Error

+

${kcSanitize((message.summary)!'Unable to complete sign-in.')?no_esc}

+
+ + Back to MediCare Sign-In +
+
+ + diff --git a/development/keycloak/themes/medicare/login/info.ftl b/development/keycloak/themes/medicare/login/info.ftl new file mode 100644 index 0000000..0fedfa1 --- /dev/null +++ b/development/keycloak/themes/medicare/login/info.ftl @@ -0,0 +1,38 @@ + + + + + + MediCare + + + +
+
+
+ +

MediCare

+

Account Notice

+
+ + <#if messageHeader??> +
${kcSanitize(messageHeader)?no_esc}
+ + <#if message?has_content> +
${kcSanitize(message.summary)?no_esc}
+ + + <#if actionUri??> + ${msg("proceedWithAction")} + <#else> + Back to sign in + +
+
+ + diff --git a/development/keycloak/themes/medicare/login/login-reset-password.ftl b/development/keycloak/themes/medicare/login/login-reset-password.ftl new file mode 100644 index 0000000..44ac461 --- /dev/null +++ b/development/keycloak/themes/medicare/login/login-reset-password.ftl @@ -0,0 +1,39 @@ + + + + + + Reset MediCare Password + + + +
+
+
+ +

MediCare

+

Reset Password

+

Enter your username or email and we will send recovery instructions if the account exists.

+
+ + <#if message?has_content> + + + +
+
+ + +
+ +
+ + +
+
+ + diff --git a/development/keycloak/themes/medicare/login/login.ftl b/development/keycloak/themes/medicare/login/login.ftl new file mode 100644 index 0000000..3d27d82 --- /dev/null +++ b/development/keycloak/themes/medicare/login/login.ftl @@ -0,0 +1,63 @@ + + + + + + ${(realm.displayName)!'MediCare'} Sign In + + + +
+
+
+ +

MediCare

+

Secure Sign In

+

Use your MediCare account to access patient monitoring, reports, and administrative tools.

+
+ + <#if message?has_content> + + + +
+ <#if !usernameHidden??> +
+ + +
+ + +
+ + +
+ +
+ <#if realm.rememberMe && !usernameHidden??> + + + <#if realm.resetPasswordAllowed> + Forgot password? + +
+ + value="${auth.selectedCredential}"> + +
+ + <#if realm.registrationAllowed && !registrationDisabled??> + + +
+
+ + diff --git a/development/keycloak/themes/medicare/login/register.ftl b/development/keycloak/themes/medicare/login/register.ftl new file mode 100644 index 0000000..45ada1b --- /dev/null +++ b/development/keycloak/themes/medicare/login/register.ftl @@ -0,0 +1,69 @@ + + + + + + Create MediCare Account + + + +
+
+
+ +

MediCare

+

Create Account

+

Register with the MediCare identity service to continue securely.

+
+ + <#if message?has_content> + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + <#if !realm.registrationEmailAsUsername> +
+ + +
+ + + <#if passwordRequired??> +
+ + +
+ +
+ + +
+ + + +
+ + +
+
+ + diff --git a/development/keycloak/themes/medicare/login/resources/css/login.css b/development/keycloak/themes/medicare/login/resources/css/login.css new file mode 100644 index 0000000..feb7601 --- /dev/null +++ b/development/keycloak/themes/medicare/login/resources/css/login.css @@ -0,0 +1,215 @@ +:root { + --mc-sky: #00a6f4; + --mc-sky-dark: #0089c9; + --mc-sky-pale: #eef6fc; + --mc-text: #16324f; + --mc-text-secondary: #405b78; + --mc-muted: #566d86; + --mc-border: rgba(24, 59, 99, 0.1); + --mc-surface: rgba(255, 255, 255, 0.9); + --mc-danger: #dc4b4b; + --mc-success: #2f9e61; + --mc-focus: rgba(95, 152, 208, 0.35); + --mc-shadow: 0 20px 40px rgba(22, 50, 79, 0.14); +} + +* { + box-sizing: border-box; +} + +body { + min-height: 100vh; + margin: 0; + color: var(--mc-text); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: + radial-gradient(circle at 8% 8%, rgba(0, 166, 244, 0.18), transparent 32rem), + radial-gradient(circle at 88% 15%, rgba(47, 158, 97, 0.14), transparent 28rem), + linear-gradient(151deg, #b9d4ee 0%, #d8eaf7 35%, #fdf6ed 65%, #ffffff 100%); +} + +.mc-page { + display: flex; + min-height: 100vh; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +.mc-card { + width: min(100%, 34rem); + border: 1px solid var(--mc-border); + border-radius: 1.5rem; + background: var(--mc-surface); + box-shadow: var(--mc-shadow); + backdrop-filter: blur(18px); + padding: clamp(1.5rem, 4vw, 2.5rem); +} + +.mc-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + text-align: center; +} + +.mc-badge { + display: inline-flex; + width: 3.25rem; + height: 3.25rem; + align-items: center; + justify-content: center; + border-radius: 1.25rem; + background: linear-gradient(180deg, var(--mc-sky) 0%, var(--mc-sky-dark) 100%); + color: #fff; + box-shadow: 0 10px 24px rgba(22, 50, 79, 0.12); +} + +.mc-brand { + margin: 0; + color: var(--mc-sky); + font-size: 1.25rem; + font-weight: 700; + letter-spacing: 0; +} + +.mc-title { + margin: 0.25rem 0 0; + color: var(--mc-text); + font-size: clamp(2rem, 7vw, 2.5rem); + line-height: 1.1; + font-weight: 800; + letter-spacing: 0; +} + +.mc-copy { + margin: 0.5rem auto 0; + max-width: 26rem; + color: var(--mc-text-secondary); + font-size: 1rem; + line-height: 1.6; +} + +.mc-form { + margin-top: 2rem; +} + +.mc-field { + margin-top: 1rem; +} + +.mc-label { + display: block; + margin-bottom: 0.4rem; + color: var(--mc-text-secondary); + font-size: 0.875rem; + font-weight: 700; +} + +.mc-input { + width: 100%; + min-height: 2.85rem; + border: 1px solid var(--mc-border); + border-radius: 0.9rem; + background: #fff; + color: var(--mc-text); + font: inherit; + padding: 0 0.9rem; + outline: none; + transition: border-color 160ms ease, box-shadow 160ms ease; +} + +.mc-input:focus { + border-color: var(--mc-sky); + box-shadow: 0 0 0 4px var(--mc-focus); +} + +.mc-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: 1rem; + color: var(--mc-text-secondary); + font-size: 0.875rem; +} + +.mc-check { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.mc-check input { + width: 1.1rem; + height: 1.1rem; + accent-color: var(--mc-sky); +} + +.mc-link { + color: var(--mc-sky-dark); + font-weight: 700; + text-decoration: none; +} + +.mc-link:hover { + color: var(--mc-text); +} + +.mc-button { + display: inline-flex; + width: 100%; + min-height: 3rem; + align-items: center; + justify-content: center; + margin-top: 1.25rem; + border: 0; + border-radius: 1rem; + background: var(--mc-sky); + color: #fff; + cursor: pointer; + font: inherit; + font-weight: 800; + transition: background-color 160ms ease, transform 160ms ease; +} + +.mc-button:hover { + background: var(--mc-sky-dark); + transform: translateY(-1px); +} + +.mc-message { + margin-top: 1.25rem; + border-radius: 1rem; + padding: 0.85rem 1rem; + font-size: 0.9rem; + line-height: 1.5; +} + +.mc-message-error { + border: 1px solid rgba(220, 75, 75, 0.24); + background: #fdf1f1; + color: var(--mc-danger); +} + +.mc-message-success { + border: 1px solid rgba(47, 158, 97, 0.24); + background: #eef9f2; + color: var(--mc-success); +} + +.mc-footer { + margin-top: 1.5rem; + text-align: center; + color: var(--mc-muted); + font-size: 0.9rem; + line-height: 1.5; +} + +@media (max-width: 420px) { + .mc-row { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/development/keycloak/themes/medicare/login/theme.properties b/development/keycloak/themes/medicare/login/theme.properties new file mode 100644 index 0000000..bac7d8b --- /dev/null +++ b/development/keycloak/themes/medicare/login/theme.properties @@ -0,0 +1,2 @@ +parent=base +styles=css/login.css