doesnt work
|
|
@ -6,25 +6,39 @@ Production-ready Kotlin/Spring Boot 3 modular monolith skeleton for patient-care
|
|||
- Java 21
|
||||
- Docker + Docker Compose
|
||||
|
||||
## Local Dev
|
||||
## Local run
|
||||
1) Start dependencies:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
2) Run the API (JWT resource server):
|
||||
2) Run the API (local profile, local auth enabled):
|
||||
|
||||
```bash
|
||||
./gradlew :app:bootRun -Dspring.profiles.active=local
|
||||
SPRING_PROFILES_ACTIVE=local \
|
||||
ALLOW_LOCAL_AUTH=true \
|
||||
./gradlew :app:bootRun
|
||||
```
|
||||
|
||||
3) Run the worker:
|
||||
```bash
|
||||
SPRING_PROFILES_ACTIVE=dev \
|
||||
ALLOW_LOCAL_AUTH=false \
|
||||
./gradlew :app:bootRun
|
||||
```
|
||||
|
||||
3) (Optional) Run the worker:
|
||||
|
||||
```bash
|
||||
./gradlew :workers:notification-worker:bootRun
|
||||
```
|
||||
|
||||
Required env flags (local/dev):
|
||||
- `SPRING_PROFILES_ACTIVE=local`
|
||||
- `ALLOW_LOCAL_AUTH=true` (enables local auth headers)
|
||||
- `KEYCLOAK_ISSUER_URI=http://localhost:8081/realms/mosenioring` (if using Keycloak)
|
||||
- Frontend should set `USE_LOCAL_AUTH=true` when using local auth headers.
|
||||
|
||||
## Auth
|
||||
- The backend is a JWT resource server and does not handle user passwords.
|
||||
- Local auth shortcut is available only when `SPRING_PROFILES_ACTIVE=local`
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class SecurityConfig {
|
|||
.csrf { it.disable() }
|
||||
.authorizeHttpRequests {
|
||||
it.requestMatchers("/actuator/**", "/health", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
|
||||
it.requestMatchers("/api/v1/demo").permitAll()
|
||||
it.requestMatchers(HttpMethod.POST, "/api/v1/tenants").hasRole("ADMIN")
|
||||
it.anyRequest().authenticated()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.mosenioring.app.demo
|
||||
|
||||
object DemoDataIds {
|
||||
const val tenantId = "11111111-1111-1111-1111-111111111111"
|
||||
const val patientId = "22222222-2222-2222-2222-222222222222"
|
||||
|
||||
const val patientUserId = "33333333-3333-3333-3333-333333333333"
|
||||
const val caregiverUserId = "44444444-4444-4444-4444-444444444444"
|
||||
const val doctorUserId = "55555555-5555-5555-5555-555555555555"
|
||||
|
||||
const val patientCaregiverId = "66666666-6666-6666-6666-666666666666"
|
||||
const val patientDoctorId = "77777777-7777-7777-7777-777777777777"
|
||||
|
||||
const val medicationPlanId = "88888888-8888-8888-8888-888888888888"
|
||||
const val testOrderId = "99999999-9999-9999-9999-999999999999"
|
||||
const val messageOneId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
const val messageTwoId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
|
||||
const val fileId = "cccccccc-cccc-cccc-cccc-cccccccccccc"
|
||||
const val auditEventOneId = "dddddddd-dddd-dddd-dddd-dddddddddddd"
|
||||
const val auditEventTwoId = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"
|
||||
const val auditEventThreeId = "ffffffff-ffff-ffff-ffff-ffffffffffff"
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.mosenioring.app.demo
|
||||
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
data class DemoIdsResponse(
|
||||
val tenantId: String,
|
||||
val patientId: String,
|
||||
val userIds: Map<String, String>
|
||||
)
|
||||
|
||||
@RestController
|
||||
@Profile("local")
|
||||
@RequestMapping("/api/v1/demo")
|
||||
class LocalDemoController {
|
||||
|
||||
@GetMapping
|
||||
fun demo(): DemoIdsResponse {
|
||||
return DemoIdsResponse(
|
||||
tenantId = DemoDataIds.tenantId,
|
||||
patientId = DemoDataIds.patientId,
|
||||
userIds = mapOf(
|
||||
"patient" to DemoDataIds.patientUserId,
|
||||
"caregiver" to DemoDataIds.caregiverUserId,
|
||||
"doctor" to DemoDataIds.doctorUserId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
package com.mosenioring.app.demo
|
||||
|
||||
import com.mosenioring.audit.AuditEvent
|
||||
import com.mosenioring.audit.AuditRepository
|
||||
import com.mosenioring.clinical.MedicationPlan
|
||||
import com.mosenioring.clinical.TestOrder
|
||||
import com.mosenioring.clinical.repo.MedicationPlanRepository
|
||||
import com.mosenioring.clinical.repo.TestOrderRepository
|
||||
import com.mosenioring.common.BaseEntity
|
||||
import com.mosenioring.identity.Patient
|
||||
import com.mosenioring.identity.PatientCaregiver
|
||||
import com.mosenioring.identity.PatientDoctor
|
||||
import com.mosenioring.identity.Tenant
|
||||
import com.mosenioring.identity.User
|
||||
import com.mosenioring.identity.repo.PatientCaregiverRepository
|
||||
import com.mosenioring.identity.repo.PatientDoctorRepository
|
||||
import com.mosenioring.identity.repo.PatientRepository
|
||||
import com.mosenioring.identity.repo.TenantRepository
|
||||
import com.mosenioring.identity.repo.UserRepository
|
||||
import com.mosenioring.integrations.FileMetadata
|
||||
import com.mosenioring.integrations.repo.FileRepository
|
||||
import com.mosenioring.messaging.Message
|
||||
import com.mosenioring.messaging.repo.MessageRepository
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.CommandLineRunner
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@Profile("local")
|
||||
class LocalDemoSeeder(
|
||||
private val tenantRepository: TenantRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val patientRepository: PatientRepository,
|
||||
private val patientCaregiverRepository: PatientCaregiverRepository,
|
||||
private val patientDoctorRepository: PatientDoctorRepository,
|
||||
private val medicationPlanRepository: MedicationPlanRepository,
|
||||
private val testOrderRepository: TestOrderRepository,
|
||||
private val messageRepository: MessageRepository,
|
||||
private val fileRepository: FileRepository,
|
||||
private val auditRepository: AuditRepository
|
||||
) : CommandLineRunner {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(LocalDemoSeeder::class.java)
|
||||
|
||||
override fun run(args: Array<String>) {
|
||||
var created = 0
|
||||
|
||||
if (!tenantRepository.existsById(DemoDataIds.tenantId)) {
|
||||
tenantRepository.save(
|
||||
seedDefaults(Tenant(DemoDataIds.tenantId, "Local Demo Tenant"))
|
||||
)
|
||||
created += 1
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.patientUserId,
|
||||
userRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
User(
|
||||
DemoDataIds.patientUserId,
|
||||
"patient@local.test",
|
||||
"PATIENT",
|
||||
"ACTIVE"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.caregiverUserId,
|
||||
userRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
User(
|
||||
DemoDataIds.caregiverUserId,
|
||||
"caregiver@local.test",
|
||||
"CAREGIVER",
|
||||
"ACTIVE"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.doctorUserId,
|
||||
userRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
User(
|
||||
DemoDataIds.doctorUserId,
|
||||
"doctor@local.test",
|
||||
"DOCTOR",
|
||||
"ACTIVE"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.patientId,
|
||||
patientRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
Patient(
|
||||
DemoDataIds.patientId,
|
||||
"Alex Patient"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.patientCaregiverId,
|
||||
patientCaregiverRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
PatientCaregiver(
|
||||
DemoDataIds.patientCaregiverId,
|
||||
DemoDataIds.patientId,
|
||||
DemoDataIds.caregiverUserId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.patientDoctorId,
|
||||
patientDoctorRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
PatientDoctor(
|
||||
DemoDataIds.patientDoctorId,
|
||||
DemoDataIds.patientId,
|
||||
DemoDataIds.doctorUserId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.medicationPlanId,
|
||||
medicationPlanRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
MedicationPlan(
|
||||
DemoDataIds.medicationPlanId,
|
||||
DemoDataIds.patientId,
|
||||
"Morning: 5mg Samplex, Evening: 10mg Calmex"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.testOrderId,
|
||||
testOrderRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
TestOrder(
|
||||
DemoDataIds.testOrderId,
|
||||
DemoDataIds.patientId,
|
||||
"Blood pressure check",
|
||||
"ORDERED"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.messageOneId,
|
||||
messageRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
Message(
|
||||
DemoDataIds.messageOneId,
|
||||
DemoDataIds.patientId,
|
||||
DemoDataIds.caregiverUserId,
|
||||
"Morning meds taken."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.messageTwoId,
|
||||
messageRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
Message(
|
||||
DemoDataIds.messageTwoId,
|
||||
DemoDataIds.patientId,
|
||||
DemoDataIds.doctorUserId,
|
||||
"Thanks! Please record evening dose too."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.fileId,
|
||||
fileRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
FileMetadata(
|
||||
DemoDataIds.fileId,
|
||||
DemoDataIds.patientId,
|
||||
"care-plan.pdf",
|
||||
"application/pdf",
|
||||
"demo/care-plan.pdf"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.auditEventOneId,
|
||||
auditRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
AuditEvent(
|
||||
DemoDataIds.auditEventOneId,
|
||||
DemoDataIds.caregiverUserId,
|
||||
"MESSAGE_SENT",
|
||||
"message",
|
||||
DemoDataIds.messageOneId,
|
||||
DemoDataIds.patientId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.auditEventTwoId,
|
||||
auditRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
AuditEvent(
|
||||
DemoDataIds.auditEventTwoId,
|
||||
DemoDataIds.doctorUserId,
|
||||
"MESSAGE_SENT",
|
||||
"message",
|
||||
DemoDataIds.messageTwoId,
|
||||
DemoDataIds.patientId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
created += saveIfMissing(
|
||||
DemoDataIds.auditEventThreeId,
|
||||
auditRepository
|
||||
) {
|
||||
seedDefaults(
|
||||
AuditEvent(
|
||||
DemoDataIds.auditEventThreeId,
|
||||
DemoDataIds.doctorUserId,
|
||||
"MEDICATION_PLAN_UPDATED",
|
||||
"medication_plan",
|
||||
DemoDataIds.medicationPlanId,
|
||||
DemoDataIds.patientId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Local demo data ready (created {} records): tenantId={}, patientId={}, users={{patient={}, caregiver={}, doctor={}}}, emails={{patient@local.test, caregiver@local.test, doctor@local.test}}",
|
||||
created,
|
||||
DemoDataIds.tenantId,
|
||||
DemoDataIds.patientId,
|
||||
DemoDataIds.patientUserId,
|
||||
DemoDataIds.caregiverUserId,
|
||||
DemoDataIds.doctorUserId
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T : BaseEntity> seedDefaults(entity: T): T {
|
||||
entity.tenantId = DemoDataIds.tenantId
|
||||
entity.createdBy = "seed"
|
||||
entity.updatedBy = "seed"
|
||||
return entity
|
||||
}
|
||||
|
||||
private fun <T : BaseEntity> saveIfMissing(
|
||||
id: String,
|
||||
repository: org.springframework.data.jpa.repository.JpaRepository<T, String>,
|
||||
build: () -> T
|
||||
): Int {
|
||||
return if (repository.existsById(id)) {
|
||||
0
|
||||
} else {
|
||||
repository.save(build())
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,15 @@
|
|||
"directAccessGrantsEnabled": true,
|
||||
"standardFlowEnabled": true,
|
||||
"redirectUris": ["*"]
|
||||
},
|
||||
{
|
||||
"clientId": "mosenioring-mobile",
|
||||
"enabled": true,
|
||||
"publicClient": true,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"standardFlowEnabled": true,
|
||||
"redirectUris": ["com.mosenioring.app://oauth2redirect"],
|
||||
"webOrigins": ["*"]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
|
|
|
|||
|
|
@ -16,18 +16,19 @@ management, and a login flow ready to integrate with a Swagger/OpenAPI backend.
|
|||
Runtime configuration is provided via `--dart-define` values.
|
||||
|
||||
Auth + environment flags:
|
||||
- `API_BASE_URL` (default: `http://localhost:8080`)
|
||||
- `API_BASE_URL` (required)
|
||||
- `USE_LOCAL_AUTH` (`true`/`false`, default: `false`)
|
||||
- `LOCAL_TENANT_ID` (default: `local-tenant`)
|
||||
- `LOCAL_TENANT_ID` (default: `11111111-1111-1111-1111-111111111111`)
|
||||
- `LOCAL_ROLES` (default: `CAREGIVER`)
|
||||
- `KEYCLOAK_ISSUER_URI` or `KEYCLOAK_ISSUER` (default: `http://localhost:8081/realms/mosenioring`)
|
||||
- `KEYCLOAK_CLIENT_ID` (default: `mosenioring-mobile`)
|
||||
- `KEYCLOAK_REDIRECT_URL` (default: `com.mosenioring.app://oauth2redirect`)
|
||||
- `KEYCLOAK_ISSUER_URI` or `KEYCLOAK_ISSUER` (required when `USE_LOCAL_AUTH=false`)
|
||||
- `KEYCLOAK_CLIENT_ID` (required when `USE_LOCAL_AUTH=false`)
|
||||
- `KEYCLOAK_REDIRECT_URL` (required when `USE_LOCAL_AUTH=false`)
|
||||
|
||||
Local dev behavior:
|
||||
- When `USE_LOCAL_AUTH=true`, the app keeps the email/password fields and sends
|
||||
`X-Local-Email`, `X-Local-Roles`, and `X-Tenant-Id` headers to the backend.
|
||||
- The backend must run with `SPRING_PROFILES_ACTIVE=local` and `ALLOW_LOCAL_AUTH=true`.
|
||||
- Keycloak login via `flutter_appauth` is supported only on Android/iOS.
|
||||
|
||||
Keycloak redirect URI to register:
|
||||
- `com.mosenioring.app://oauth2redirect` (Android + iOS)
|
||||
|
|
@ -47,11 +48,24 @@ flutter pub get
|
|||
flutter run
|
||||
```
|
||||
|
||||
Quick start (Keycloak mode):
|
||||
## Local run
|
||||
|
||||
```sh
|
||||
flutter run \
|
||||
--dart-define=API_BASE_URL=http://localhost:8080 \
|
||||
--dart-define=KEYCLOAK_ISSUER_URI=http://localhost:8081/realms/mosenioring \
|
||||
--dart-define=API_BASE_URL=http://10.0.2.2:8080 \
|
||||
--dart-define=USE_LOCAL_AUTH=true
|
||||
```
|
||||
|
||||
Android emulator note:
|
||||
- Use `10.0.2.2` instead of `localhost` for `API_BASE_URL` and `KEYCLOAK_ISSUER_URI`.
|
||||
- `http://` Keycloak issuers are allowed for local dev; the app enables insecure connections automatically for non-HTTPS issuers.
|
||||
- iOS dev builds allow HTTP via App Transport Security; tighten this for production.
|
||||
|
||||
Quick start (Keycloak mode):
|
||||
```sh
|
||||
flutter run -d emulator-5554 \
|
||||
--dart-define=API_BASE_URL=http://10.0.2.2:8080 \
|
||||
--dart-define=KEYCLOAK_ISSUER_URI=http://10.0.2.2:8081/realms/mosenioring \
|
||||
--dart-define=KEYCLOAK_CLIENT_ID=mosenioring-mobile \
|
||||
--dart-define=KEYCLOAK_REDIRECT_URL=com.mosenioring.app://oauth2redirect
|
||||
```
|
||||
|
|
@ -61,9 +75,18 @@ Quick start (local auth, no Keycloak):
|
|||
export SPRING_PROFILES_ACTIVE=local
|
||||
export ALLOW_LOCAL_AUTH=true
|
||||
|
||||
flutter run \
|
||||
--dart-define=API_BASE_URL=http://localhost:8080 \
|
||||
flutter run -d <device_id> \
|
||||
--dart-define=API_BASE_URL=http://10.0.2.2:8080 \
|
||||
--dart-define=USE_LOCAL_AUTH=true \
|
||||
--dart-define=LOCAL_TENANT_ID=local-tenant \
|
||||
--dart-define=LOCAL_TENANT_ID=11111111-1111-1111-1111-111111111111 \
|
||||
--dart-define=LOCAL_ROLES=CAREGIVER
|
||||
```
|
||||
|
||||
Redirect configuration:
|
||||
- Android: `android/app/src/main/AndroidManifest.xml` includes an intent-filter for `com.mosenioring.app://oauth2redirect`.
|
||||
- iOS: `ios/Runner/Info.plist` registers `com.mosenioring.app` under `CFBundleURLTypes`.
|
||||
|
||||
Launcher icons:
|
||||
```sh
|
||||
flutter pub run flutter_launcher_icons
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="n"/>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
<application
|
||||
android:label="mosenioring"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
|
@ -29,8 +31,8 @@
|
|||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
android:scheme="${appAuthRedirectScheme}"
|
||||
android:host="${appAuthRedirectHost}"/>
|
||||
android:scheme="com.mosenioring.app"
|
||||
android:host="oauth2redirect"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 794 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0D3B3E</color>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
BIN
front001/mosenioring/assets/launcher/icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
front001/mosenioring/assets/logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -427,7 +427,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
@ -484,7 +484,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 308 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 623 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 936 B |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 456 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 889 B |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 623 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 818 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 956 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
|
@ -24,6 +24,11 @@
|
|||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class AppConfig {
|
||||
const AppConfig({
|
||||
required this.apiBaseUrl,
|
||||
|
|
@ -19,4 +21,37 @@ class AppConfig {
|
|||
|
||||
String get keycloakDiscoveryUrl =>
|
||||
'$keycloakIssuer/.well-known/openid-configuration';
|
||||
|
||||
bool get allowInsecureConnections =>
|
||||
keycloakIssuer.trim().startsWith('http://');
|
||||
|
||||
bool get isAppAuthSupportedPlatform {
|
||||
if (kIsWeb) {
|
||||
return false;
|
||||
}
|
||||
return defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
}
|
||||
|
||||
String? get validationError {
|
||||
if (apiBaseUrl.trim().isEmpty) {
|
||||
return 'Missing API_BASE_URL. Provide it via --dart-define=API_BASE_URL=...';
|
||||
}
|
||||
if (useLocalAuth) {
|
||||
return null;
|
||||
}
|
||||
if (!isAppAuthSupportedPlatform) {
|
||||
return 'Keycloak login is supported only on Android/iOS. Use USE_LOCAL_AUTH=true for desktop/web.';
|
||||
}
|
||||
if (keycloakIssuer.trim().isEmpty) {
|
||||
return 'Missing KEYCLOAK_ISSUER_URI or KEYCLOAK_ISSUER. Provide it via --dart-define.';
|
||||
}
|
||||
if (keycloakClientId.trim().isEmpty) {
|
||||
return 'Missing KEYCLOAK_CLIENT_ID. Provide it via --dart-define.';
|
||||
}
|
||||
if (keycloakRedirectUrl.trim().isEmpty) {
|
||||
return 'Missing KEYCLOAK_REDIRECT_URL. Provide it via --dart-define.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,29 +13,25 @@ import '../features/auth/presentation/auth_controller.dart';
|
|||
import '../features/auth/presentation/auth_state.dart';
|
||||
|
||||
final appConfigProvider = Provider<AppConfig>((ref) {
|
||||
const apiBaseUrl =
|
||||
String.fromEnvironment('API_BASE_URL', defaultValue: 'http://localhost:8080');
|
||||
const apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
|
||||
const useLocalAuth =
|
||||
bool.fromEnvironment('USE_LOCAL_AUTH', defaultValue: false);
|
||||
const localTenantId =
|
||||
String.fromEnvironment('LOCAL_TENANT_ID', defaultValue: 'local-tenant');
|
||||
const localTenantId = String.fromEnvironment(
|
||||
'LOCAL_TENANT_ID',
|
||||
defaultValue: '11111111-1111-1111-1111-111111111111',
|
||||
);
|
||||
const localRoles =
|
||||
String.fromEnvironment('LOCAL_ROLES', defaultValue: 'CAREGIVER');
|
||||
const keycloakIssuer = String.fromEnvironment(
|
||||
'KEYCLOAK_ISSUER',
|
||||
defaultValue: '',
|
||||
);
|
||||
const keycloakIssuerUri = String.fromEnvironment(
|
||||
'KEYCLOAK_ISSUER_URI',
|
||||
defaultValue: 'http://localhost:8081/realms/mosenioring',
|
||||
);
|
||||
const keycloakIssuer = String.fromEnvironment('KEYCLOAK_ISSUER', defaultValue: '');
|
||||
const keycloakIssuerUri =
|
||||
String.fromEnvironment('KEYCLOAK_ISSUER_URI', defaultValue: '');
|
||||
final resolvedIssuer =
|
||||
keycloakIssuer.isNotEmpty ? keycloakIssuer : keycloakIssuerUri;
|
||||
const keycloakClientId =
|
||||
String.fromEnvironment('KEYCLOAK_CLIENT_ID', defaultValue: 'mosenioring-mobile');
|
||||
String.fromEnvironment('KEYCLOAK_CLIENT_ID', defaultValue: '');
|
||||
const keycloakRedirectUrl = String.fromEnvironment(
|
||||
'KEYCLOAK_REDIRECT_URL',
|
||||
defaultValue: 'com.mosenioring.app://oauth2redirect',
|
||||
defaultValue: '',
|
||||
);
|
||||
return AppConfig(
|
||||
apiBaseUrl: apiBaseUrl,
|
||||
|
|
|
|||
|
|
@ -14,11 +14,17 @@ class AuthRemoteDataSource {
|
|||
required String password,
|
||||
}) async {
|
||||
final _ = password;
|
||||
if (!_config.isAppAuthSupportedPlatform) {
|
||||
throw Exception(
|
||||
'Keycloak login is supported only on Android/iOS. Use USE_LOCAL_AUTH=true for desktop/web.',
|
||||
);
|
||||
}
|
||||
final response = await _appAuth.authorizeAndExchangeCode(
|
||||
AuthorizationTokenRequest(
|
||||
_config.keycloakClientId,
|
||||
_config.keycloakRedirectUrl,
|
||||
discoveryUrl: _config.keycloakDiscoveryUrl,
|
||||
allowInsecureConnections: _config.allowInsecureConnections,
|
||||
loginHint: email,
|
||||
promptValues: const ['login'],
|
||||
scopes: const ['openid', 'profile', 'offline_access'],
|
||||
|
|
@ -39,11 +45,17 @@ class AuthRemoteDataSource {
|
|||
Future<AuthToken> refreshToken({
|
||||
required String refreshToken,
|
||||
}) async {
|
||||
if (!_config.isAppAuthSupportedPlatform) {
|
||||
throw Exception(
|
||||
'Keycloak refresh is supported only on Android/iOS. Use USE_LOCAL_AUTH=true for desktop/web.',
|
||||
);
|
||||
}
|
||||
final response = await _appAuth.token(
|
||||
TokenRequest(
|
||||
_config.keycloakClientId,
|
||||
_config.keycloakRedirectUrl,
|
||||
discoveryUrl: _config.keycloakDiscoveryUrl,
|
||||
allowInsecureConnections: _config.allowInsecureConnections,
|
||||
refreshToken: refreshToken,
|
||||
scopes: const ['openid', 'profile', 'offline_access'],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ class AuthController extends Notifier<AuthState> {
|
|||
Future<void> _bootstrap() async {
|
||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||
final config = ref.read(appConfigProvider);
|
||||
final configError = config.validationError;
|
||||
if (configError != null) {
|
||||
state = state.copyWith(isLoading: false, errorMessage: configError);
|
||||
return;
|
||||
}
|
||||
if (config.useLocalAuth) {
|
||||
final localSession = await _localDataSource.readLocalSession();
|
||||
state = AuthState(
|
||||
|
|
@ -38,6 +43,11 @@ class AuthController extends Notifier<AuthState> {
|
|||
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||
try {
|
||||
final config = ref.read(appConfigProvider);
|
||||
final configError = config.validationError;
|
||||
if (configError != null) {
|
||||
state = state.copyWith(isLoading: false, errorMessage: configError);
|
||||
return;
|
||||
}
|
||||
if (config.useLocalAuth) {
|
||||
await _localDataSource.saveLocalSession(
|
||||
email: email,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||
final authState = ref.watch(authControllerProvider);
|
||||
final config = ref.watch(appConfigProvider);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final configError = config.validationError;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n.signInTitle)),
|
||||
|
|
@ -55,6 +56,12 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/logo.png',
|
||||
height: 96,
|
||||
width: 96,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.welcomeTitle,
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
|
||||
|
|
@ -81,7 +88,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: authState.isLoading ? null : _submit,
|
||||
onPressed: authState.isLoading || configError != null ? null : _submit,
|
||||
child: authState.isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
|
|
@ -91,10 +98,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||
: Text(l10n.loginButton),
|
||||
),
|
||||
),
|
||||
if (authState.errorMessage != null) ...[
|
||||
if (authState.errorMessage != null || configError != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
authState.errorMessage!,
|
||||
authState.errorMessage ?? configError!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ dev_dependencies:
|
|||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^6.0.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
|
@ -67,9 +68,9 @@ flutter:
|
|||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
assets:
|
||||
- assets/logo.png
|
||||
- assets/launcher/icon.png
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
|
@ -96,3 +97,10 @@ flutter:
|
|||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: assets/launcher/icon.png
|
||||
adaptive_icon_background: "#0D3B3E"
|
||||
adaptive_icon_foreground: assets/launcher/icon.png
|
||||
|
|
|
|||