doesnt work
|
|
@ -6,25 +6,39 @@ Production-ready Kotlin/Spring Boot 3 modular monolith skeleton for patient-care
|
||||||
- Java 21
|
- Java 21
|
||||||
- Docker + Docker Compose
|
- Docker + Docker Compose
|
||||||
|
|
||||||
## Local Dev
|
## Local run
|
||||||
1) Start dependencies:
|
1) Start dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
2) Run the API (JWT resource server):
|
2) Run the API (local profile, local auth enabled):
|
||||||
|
|
||||||
```bash
|
```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
|
```bash
|
||||||
./gradlew :workers:notification-worker:bootRun
|
./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
|
## Auth
|
||||||
- The backend is a JWT resource server and does not handle user passwords.
|
- The backend is a JWT resource server and does not handle user passwords.
|
||||||
- Local auth shortcut is available only when `SPRING_PROFILES_ACTIVE=local`
|
- Local auth shortcut is available only when `SPRING_PROFILES_ACTIVE=local`
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class SecurityConfig {
|
||||||
.csrf { it.disable() }
|
.csrf { it.disable() }
|
||||||
.authorizeHttpRequests {
|
.authorizeHttpRequests {
|
||||||
it.requestMatchers("/actuator/**", "/health", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
|
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.requestMatchers(HttpMethod.POST, "/api/v1/tenants").hasRole("ADMIN")
|
||||||
it.anyRequest().authenticated()
|
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,
|
"directAccessGrantsEnabled": true,
|
||||||
"standardFlowEnabled": true,
|
"standardFlowEnabled": true,
|
||||||
"redirectUris": ["*"]
|
"redirectUris": ["*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "mosenioring-mobile",
|
||||||
|
"enabled": true,
|
||||||
|
"publicClient": true,
|
||||||
|
"directAccessGrantsEnabled": false,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"redirectUris": ["com.mosenioring.app://oauth2redirect"],
|
||||||
|
"webOrigins": ["*"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"users": [
|
"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.
|
Runtime configuration is provided via `--dart-define` values.
|
||||||
|
|
||||||
Auth + environment flags:
|
Auth + environment flags:
|
||||||
- `API_BASE_URL` (default: `http://localhost:8080`)
|
- `API_BASE_URL` (required)
|
||||||
- `USE_LOCAL_AUTH` (`true`/`false`, default: `false`)
|
- `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`)
|
- `LOCAL_ROLES` (default: `CAREGIVER`)
|
||||||
- `KEYCLOAK_ISSUER_URI` or `KEYCLOAK_ISSUER` (default: `http://localhost:8081/realms/mosenioring`)
|
- `KEYCLOAK_ISSUER_URI` or `KEYCLOAK_ISSUER` (required when `USE_LOCAL_AUTH=false`)
|
||||||
- `KEYCLOAK_CLIENT_ID` (default: `mosenioring-mobile`)
|
- `KEYCLOAK_CLIENT_ID` (required when `USE_LOCAL_AUTH=false`)
|
||||||
- `KEYCLOAK_REDIRECT_URL` (default: `com.mosenioring.app://oauth2redirect`)
|
- `KEYCLOAK_REDIRECT_URL` (required when `USE_LOCAL_AUTH=false`)
|
||||||
|
|
||||||
Local dev behavior:
|
Local dev behavior:
|
||||||
- When `USE_LOCAL_AUTH=true`, the app keeps the email/password fields and sends
|
- 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.
|
`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`.
|
- 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:
|
Keycloak redirect URI to register:
|
||||||
- `com.mosenioring.app://oauth2redirect` (Android + iOS)
|
- `com.mosenioring.app://oauth2redirect` (Android + iOS)
|
||||||
|
|
@ -47,11 +48,24 @@ flutter pub get
|
||||||
flutter run
|
flutter run
|
||||||
```
|
```
|
||||||
|
|
||||||
Quick start (Keycloak mode):
|
## Local run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
flutter run \
|
flutter run \
|
||||||
--dart-define=API_BASE_URL=http://localhost:8080 \
|
--dart-define=API_BASE_URL=http://10.0.2.2:8080 \
|
||||||
--dart-define=KEYCLOAK_ISSUER_URI=http://localhost:8081/realms/mosenioring \
|
--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_CLIENT_ID=mosenioring-mobile \
|
||||||
--dart-define=KEYCLOAK_REDIRECT_URL=com.mosenioring.app://oauth2redirect
|
--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 SPRING_PROFILES_ACTIVE=local
|
||||||
export ALLOW_LOCAL_AUTH=true
|
export ALLOW_LOCAL_AUTH=true
|
||||||
|
|
||||||
flutter run \
|
flutter run -d <device_id> \
|
||||||
--dart-define=API_BASE_URL=http://localhost:8080 \
|
--dart-define=API_BASE_URL=http://10.0.2.2:8080 \
|
||||||
--dart-define=USE_LOCAL_AUTH=true \
|
--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
|
--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 INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
tools:targetApi="n"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
<application
|
<application
|
||||||
android:label="mosenioring"
|
android:label="mosenioring"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
@ -29,8 +31,8 @@
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<data
|
<data
|
||||||
android:scheme="${appAuthRedirectScheme}"
|
android:scheme="com.mosenioring.app"
|
||||||
android:host="${appAuthRedirectHost}"/>
|
android:host="oauth2redirect"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- 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;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|
@ -484,7 +484,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
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>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class AppConfig {
|
class AppConfig {
|
||||||
const AppConfig({
|
const AppConfig({
|
||||||
required this.apiBaseUrl,
|
required this.apiBaseUrl,
|
||||||
|
|
@ -19,4 +21,37 @@ class AppConfig {
|
||||||
|
|
||||||
String get keycloakDiscoveryUrl =>
|
String get keycloakDiscoveryUrl =>
|
||||||
'$keycloakIssuer/.well-known/openid-configuration';
|
'$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';
|
import '../features/auth/presentation/auth_state.dart';
|
||||||
|
|
||||||
final appConfigProvider = Provider<AppConfig>((ref) {
|
final appConfigProvider = Provider<AppConfig>((ref) {
|
||||||
const apiBaseUrl =
|
const apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
|
||||||
String.fromEnvironment('API_BASE_URL', defaultValue: 'http://localhost:8080');
|
|
||||||
const useLocalAuth =
|
const useLocalAuth =
|
||||||
bool.fromEnvironment('USE_LOCAL_AUTH', defaultValue: false);
|
bool.fromEnvironment('USE_LOCAL_AUTH', defaultValue: false);
|
||||||
const localTenantId =
|
const localTenantId = String.fromEnvironment(
|
||||||
String.fromEnvironment('LOCAL_TENANT_ID', defaultValue: 'local-tenant');
|
'LOCAL_TENANT_ID',
|
||||||
|
defaultValue: '11111111-1111-1111-1111-111111111111',
|
||||||
|
);
|
||||||
const localRoles =
|
const localRoles =
|
||||||
String.fromEnvironment('LOCAL_ROLES', defaultValue: 'CAREGIVER');
|
String.fromEnvironment('LOCAL_ROLES', defaultValue: 'CAREGIVER');
|
||||||
const keycloakIssuer = String.fromEnvironment(
|
const keycloakIssuer = String.fromEnvironment('KEYCLOAK_ISSUER', defaultValue: '');
|
||||||
'KEYCLOAK_ISSUER',
|
const keycloakIssuerUri =
|
||||||
defaultValue: '',
|
String.fromEnvironment('KEYCLOAK_ISSUER_URI', defaultValue: '');
|
||||||
);
|
|
||||||
const keycloakIssuerUri = String.fromEnvironment(
|
|
||||||
'KEYCLOAK_ISSUER_URI',
|
|
||||||
defaultValue: 'http://localhost:8081/realms/mosenioring',
|
|
||||||
);
|
|
||||||
final resolvedIssuer =
|
final resolvedIssuer =
|
||||||
keycloakIssuer.isNotEmpty ? keycloakIssuer : keycloakIssuerUri;
|
keycloakIssuer.isNotEmpty ? keycloakIssuer : keycloakIssuerUri;
|
||||||
const keycloakClientId =
|
const keycloakClientId =
|
||||||
String.fromEnvironment('KEYCLOAK_CLIENT_ID', defaultValue: 'mosenioring-mobile');
|
String.fromEnvironment('KEYCLOAK_CLIENT_ID', defaultValue: '');
|
||||||
const keycloakRedirectUrl = String.fromEnvironment(
|
const keycloakRedirectUrl = String.fromEnvironment(
|
||||||
'KEYCLOAK_REDIRECT_URL',
|
'KEYCLOAK_REDIRECT_URL',
|
||||||
defaultValue: 'com.mosenioring.app://oauth2redirect',
|
defaultValue: '',
|
||||||
);
|
);
|
||||||
return AppConfig(
|
return AppConfig(
|
||||||
apiBaseUrl: apiBaseUrl,
|
apiBaseUrl: apiBaseUrl,
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,17 @@ class AuthRemoteDataSource {
|
||||||
required String password,
|
required String password,
|
||||||
}) async {
|
}) async {
|
||||||
final _ = password;
|
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(
|
final response = await _appAuth.authorizeAndExchangeCode(
|
||||||
AuthorizationTokenRequest(
|
AuthorizationTokenRequest(
|
||||||
_config.keycloakClientId,
|
_config.keycloakClientId,
|
||||||
_config.keycloakRedirectUrl,
|
_config.keycloakRedirectUrl,
|
||||||
discoveryUrl: _config.keycloakDiscoveryUrl,
|
discoveryUrl: _config.keycloakDiscoveryUrl,
|
||||||
|
allowInsecureConnections: _config.allowInsecureConnections,
|
||||||
loginHint: email,
|
loginHint: email,
|
||||||
promptValues: const ['login'],
|
promptValues: const ['login'],
|
||||||
scopes: const ['openid', 'profile', 'offline_access'],
|
scopes: const ['openid', 'profile', 'offline_access'],
|
||||||
|
|
@ -39,11 +45,17 @@ class AuthRemoteDataSource {
|
||||||
Future<AuthToken> refreshToken({
|
Future<AuthToken> refreshToken({
|
||||||
required String refreshToken,
|
required String refreshToken,
|
||||||
}) async {
|
}) 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(
|
final response = await _appAuth.token(
|
||||||
TokenRequest(
|
TokenRequest(
|
||||||
_config.keycloakClientId,
|
_config.keycloakClientId,
|
||||||
_config.keycloakRedirectUrl,
|
_config.keycloakRedirectUrl,
|
||||||
discoveryUrl: _config.keycloakDiscoveryUrl,
|
discoveryUrl: _config.keycloakDiscoveryUrl,
|
||||||
|
allowInsecureConnections: _config.allowInsecureConnections,
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
scopes: const ['openid', 'profile', 'offline_access'],
|
scopes: const ['openid', 'profile', 'offline_access'],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ class AuthController extends Notifier<AuthState> {
|
||||||
Future<void> _bootstrap() async {
|
Future<void> _bootstrap() async {
|
||||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||||
final config = ref.read(appConfigProvider);
|
final config = ref.read(appConfigProvider);
|
||||||
|
final configError = config.validationError;
|
||||||
|
if (configError != null) {
|
||||||
|
state = state.copyWith(isLoading: false, errorMessage: configError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (config.useLocalAuth) {
|
if (config.useLocalAuth) {
|
||||||
final localSession = await _localDataSource.readLocalSession();
|
final localSession = await _localDataSource.readLocalSession();
|
||||||
state = AuthState(
|
state = AuthState(
|
||||||
|
|
@ -38,6 +43,11 @@ class AuthController extends Notifier<AuthState> {
|
||||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||||
try {
|
try {
|
||||||
final config = ref.read(appConfigProvider);
|
final config = ref.read(appConfigProvider);
|
||||||
|
final configError = config.validationError;
|
||||||
|
if (configError != null) {
|
||||||
|
state = state.copyWith(isLoading: false, errorMessage: configError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (config.useLocalAuth) {
|
if (config.useLocalAuth) {
|
||||||
await _localDataSource.saveLocalSession(
|
await _localDataSource.saveLocalSession(
|
||||||
email: email,
|
email: email,
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
final authState = ref.watch(authControllerProvider);
|
final authState = ref.watch(authControllerProvider);
|
||||||
final config = ref.watch(appConfigProvider);
|
final config = ref.watch(appConfigProvider);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final configError = config.validationError;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(l10n.signInTitle)),
|
appBar: AppBar(title: Text(l10n.signInTitle)),
|
||||||
|
|
@ -55,6 +56,12 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/logo.png',
|
||||||
|
height: 96,
|
||||||
|
width: 96,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
l10n.welcomeTitle,
|
l10n.welcomeTitle,
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
|
||||||
|
|
@ -81,7 +88,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: authState.isLoading ? null : _submit,
|
onPressed: authState.isLoading || configError != null ? null : _submit,
|
||||||
child: authState.isLoading
|
child: authState.isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
|
|
@ -91,10 +98,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
: Text(l10n.loginButton),
|
: Text(l10n.loginButton),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (authState.errorMessage != null) ...[
|
if (authState.errorMessage != null || configError != null) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
authState.errorMessage!,
|
authState.errorMessage ?? configError!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ dev_dependencies:
|
||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
@ -67,9 +68,9 @@ flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
assets:
|
||||||
# - images/a_dot_burr.jpeg
|
- assets/logo.png
|
||||||
# - images/a_dot_ham.jpeg
|
- assets/launcher/icon.png
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
@ -96,3 +97,10 @@ flutter:
|
||||||
#
|
#
|
||||||
# For details regarding fonts from package dependencies,
|
# For details regarding fonts from package dependencies,
|
||||||
# see https://flutter.dev/to/font-from-package
|
# 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
|
||||||
|
|
|
||||||