diff --git a/.gitignore b/.gitignore index a4fda30..dbad684 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,55 @@ -/data/ -/target/ +# --- Maven / Java --- +target/ **/target/ -/.idea/ -/.vscode/ +*.class +*.jar +*.war +*.ear +hs_err_pid* +replay_pid* + +# --- Spring Boot / runtime local --- +**/data/h2/ +*.mv.db +*.lock.db +*.trace.db + +# --- Docker / Floci (generado en local) --- +/docker/floci/generated/* +!/docker/floci/generated/.gitkeep + +# --- App Flutter (proyecto aparte, no versionar aquí) --- +/flutter-app/ + +# --- IDE --- +.idea/ *.iml -*.log -/.mvn/ -/.DS_Store +*.ipr +*.iws +.vscode/ .classpath .project .settings/ **/.classpath **/.project **/.settings/ + +# --- Maven wrapper local (si se genera) --- +.mvn/wrapper/maven-wrapper.jar + +# --- Logs y temporales --- +*.log +*.tmp +*.swp +*~ + +# --- OS --- +.DS_Store +Thumbs.db + +# --- Maven auxiliar --- effective-pom.xml **/effective-pom.xml + +# --- Datos locales en raíz (legacy) --- +/data/ \ No newline at end of file diff --git a/README.md b/README.md index 5d0903e..ec35609 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,362 @@ # Base Admin Web -Proyecto base de Spring Boot con Metronic, autenticacion local y estructura modular lista para reutilizar en nuevos paneles administrativos. +Proyecto base de Spring Boot con Metronic, autenticación Cognito (o local), panel administrativo y API de escaneo para la app móvil Flutter. + +Sirve como plantilla reutilizable para nuevos paneles internos: estructura modular, seguridad dual (web + API), módulo de códigos pase con QR y soporte H2/PostgreSQL. + +--- + +## Tabla de contenidos + +- [Vista general](#vista-general) +- [Stack](#stack) +- [Arquitectura del sistema](#arquitectura-del-sistema) +- [Módulos Maven](#módulos-maven) +- [Seguridad y autenticación](#seguridad-y-autenticación) +- [Cognito: local (Floci) vs AWS](#cognito-local-floci-vs-aws) +- [Módulo de códigos pase](#módulo-de-códigos-pase) +- [API REST](#api-rest) +- [App Flutter (escáner)](#app-flutter-escáner) +- [Arranque local](#arranque-local) +- [Arranque manual e IntelliJ IDEA](#arranque-manual-e-intellij-idea) +- [Perfiles Spring](#perfiles-spring) +- [Variables de entorno](#variables-de-entorno) +- [Objetivo de esta base](#objetivo-de-esta-base) + +--- + +## Vista general + +El sistema tiene **dos canales de acceso** con roles distintos: + +| Canal | Usuario | Rol | Autenticación | +|-------|---------|-----|---------------| +| Panel web (Thymeleaf) | `admin` | `ROLE_ADMIN` | Sesión HTTP (form login) | +| API móvil / externa | `scanner` | `ROLE_SCANNER` | JWT Bearer (stateless) | + +Por defecto ambos canales usan **Amazon Cognito** (emulado en local con Floci). Con `APP_COGNITO_ENABLED=false` se usa autenticación local contra la base de datos y JWT firmado con secreto propio. + +```mermaid +flowchart TB + subgraph clients["Clientes"] + Browser["NavegadorPanel admin"] + Flutter["App FlutterQR Scanner"] + end + + subgraph app["Spring Boot — base-admin-web-jar"] + WebChain["Filter chain websesión + CSRF"] + ApiChain["Filter chain APIstateless + JWT"] + PassCodes["PassCodeService"] + DB[("H2 / PostgreSQL")] + end + + subgraph auth["Autenticación"] + Cognito["CognitoFloci local o AWS"] + LocalAuth["BD local + JWT HS256APP_COGNITO_ENABLED=false"] + end + + Browser -->|"/login, /admin/**"| WebChain + Flutter -->|"/api/auth/login/api/pass-codes/scan"| ApiChain + WebChain --> PassCodes + ApiChain --> PassCodes + PassCodes --> DB + WebChain --> Cognito + ApiChain --> Cognito + WebChain -.-> LocalAuth + ApiChain -.-> LocalAuth +``` + +**URLs locales:** + +| Recurso | URL | +|---------|-----| +| Login | http://localhost:8080/login | +| Dashboard | http://localhost:8080/dashboard | +| Códigos pase | http://localhost:8080/admin/pass-codes | +| H2 Console | http://localhost:8080/h2-console | +| Floci (Cognito local) | http://localhost:4566 | + +--- ## Stack -- Spring Boot 4.0.6 -- Java 25 -- Spring Security -- Thymeleaf -- JPA / Hibernate -- H2 en archivo para desarrollo rapido -- PostgreSQL listo por perfil -- JWT para autenticacion API de escaneo -- Assets Metronic reutilizados desde `base-web` +| Capa | Tecnología | +|------|------------| +| Backend | Spring Boot 4.0.6, Java 25 | +| Seguridad | Spring Security, OAuth2 Resource Server, AWS SDK Cognito | +| Vistas | Thymeleaf + Metronic (`base-web`) | +| Persistencia | JPA / Hibernate, H2 (dev), PostgreSQL (perfil `postgres`) | +| Auth | Cognito (default) o BD local + JWT HS256 | +| Móvil | Flutter (`flutter-app/qrscanner`) | +| Local AWS | [Floci](https://floci.io/floci/getting-started/quick-start/) (Cognito en puerto 4566) | -## Estructura +--- -- `base-admin-web-domain`: entidades y repositorios -- `base-admin-web-application`: servicios y carga inicial -- `base-admin-web-configuration`: seguridad, JWT y perfiles -- `base-admin-web-web`: controladores y vistas Thymeleaf -- `base-admin-web-jar`: punto de entrada ejecutable +## Arquitectura del sistema -## Usuarios iniciales +### Capas y dependencias -### Administrador (panel web) +```mermaid +flowchart BT + jar["base-admin-web-jarpunto de entrada + application.yml"] + web["base-admin-web-webcontroladores + Thymeleaf"] + config["base-admin-web-configurationseguridad, JWT, Cognito"] + app["base-admin-web-applicationservicios + carga inicial"] + domain["base-admin-web-domainentidades + repositorios"] -- Usuario: `admin` -- Password: `Admin123*` -- Rol: `ROLE_ADMIN` -- Acceso: login web en `/login`, dashboard y modulo de codigos pase + jar --> web + jar --> config + jar --> app + jar --> domain + web --> config + web --> app + config --> app + app --> domain +``` -Variables de entorno: +| Módulo | Responsabilidad | +|--------|-----------------| +| `base-admin-web-domain` | Entidades JPA (`AppUser`, `PassCode`, `Role`), repositorios | +| `base-admin-web-application` | `PassCodeService`, `AppUserDetailsService`, `InitialDataLoader` | +| `base-admin-web-configuration` | `SecurityConfig`, `ApiSecurityConfig`, Cognito, `JwtConfig` | +| `base-admin-web-web` | Controladores MVC y API, DTOs, plantillas HTML | +| `base-admin-web-jar` | `BaseAdminWebApplication`, perfiles, dependencias runtime (H2, PostgreSQL) | -- `APP_ADMIN_USERNAME` -- `APP_ADMIN_PASSWORD` -- `APP_ADMIN_DISPLAY_NAME` +### Dos cadenas de seguridad -### Escaneador (API) +Spring Security define **dos filter chains** con distinto comportamiento: -- Usuario: `scanner` -- Password: `Scanner123*` -- Rol: `ROLE_SCANNER` -- Acceso: solo login por API y escaneo de codigos pase +```mermaid +flowchart LR + subgraph order1["@Order(1) — ApiSecurityConfig"] + A1["/api/**"] + A2["Stateless"] + A3["JWT Bearer"] + A4["ROLE_SCANNER en /scan"] + end -Variables de entorno: + subgraph order2["@Order(2) — SecurityConfig"] + B1["Todo excepto /api/**"] + B2["Sesión HTTP"] + B3["Form login /login"] + B4["ROLE_ADMIN en /admin/**"] + end -- `APP_SCANNER_USERNAME` -- `APP_SCANNER_PASSWORD` -- `APP_SCANNER_DISPLAY_NAME` + A1 --> A2 --> A3 --> A4 + B1 --> B2 --> B3 --> B4 +``` -## Modulo de codigos pase +| Ruta | Acceso | Mecanismo | +|------|--------|-----------| +| `/login`, `/assets/**`, `/error`, `/h2-console/**` | Público | — | +| `/dashboard`, `/admin/**` | `ROLE_ADMIN` | Sesión web | +| `/api/auth/login`, `/api/auth/refresh` | Público | Emite tokens (solo `scanner`) | +| `/api/pass-codes/scan` | `ROLE_SCANNER` | JWT Bearer | -Panel administrativo para dar de alta y consultar codigos pase. +--- -- URL: `http://localhost:8080/admin/pass-codes` -- Tambien accesible desde el boton **Codigos pase** en el dashboard -- Alta por nombre; el sistema genera un codigo aleatorio de 10 caracteres -- Vista con tabla de estatus, escaneo y fecha de creacion -- Modal Metronic para ver el QR de cada codigo -- Al cerrar el modal, la lista se actualiza automaticamente por si el codigo fue utilizado +## Seguridad y autenticación -Campos del codigo pase: +### Usuarios iniciales -- Nombre -- Codigo aleatorio -- Fecha de creacion -- Estatus (`ACTIVO` / `UTILIZADO`) -- Indicador de escaneado -- Fecha de escaneo +#### Administrador (panel web) -## API +- Usuario: `admin` / `Admin123*` +- Grupo Cognito: `admins` → `ROLE_ADMIN` +- Acceso: login en `/login`, dashboard, módulo de códigos pase +- El usuario `scanner` **no** puede entrar al panel web -### Login escaner +#### Escáner (API / Flutter) + +- Usuario: `scanner` / `Scanner123*` +- Grupo Cognito: `scanners` → `ROLE_SCANNER` +- Acceso: `POST /api/auth/login` y `POST /api/pass-codes/scan` +- El usuario `admin` **no** puede autenticarse por API + +### Flujo de login web (panel admin) + +```mermaid +sequenceDiagram + actor Admin as Navegador (admin) + participant SC as SecurityConfig + participant CP as CognitoWebAuthenticationProvider + participant CA as CognitoUserAuthenticator + participant Cognito as Cognito (Floci/AWS) + + Admin->>SC: POST /login (user, password) + SC->>CP: authenticate() + CP->>CA: admin client + ROLE_ADMIN + CA->>Cognito: InitiateAuth USER_PASSWORD_AUTH + Cognito-->>CA: tokens + cognito:groups + CA-->>CP: AuthenticatedWebUser (grupo admins) + CP-->>SC: sesión HTTP creada + SC-->>Admin: redirect /dashboard +``` + +**Clases clave:** `SecurityConfig`, `CognitoWebSecurityConfig`, `CognitoWebAuthenticationProvider`, `CognitoUserAuthenticator`, `AuthController` + +Con `APP_COGNITO_ENABLED=false`, el flujo usa `DaoAuthenticationProvider` → `AppUserDetailsService` → tabla `users` (BCrypt). + +### Flujo de login y escaneo API + +```mermaid +sequenceDiagram + actor App as Flutter (scanner) + participant API as AuthApiController + participant AS as ApiAuthService + participant Cognito as Cognito + participant Scan as PassCodeApiController + participant Svc as PassCodeService + + App->>API: POST /api/auth/login + API->>AS: login() + AS->>Cognito: scanner client + ROLE_SCANNER + Cognito-->>App: accessToken, refreshToken, idToken + + App->>Scan: POST /api/pass-codes/scanAuthorization: Bearer ... + Scan->>Scan: validar JWT (JWK o HS256) + Scan->>Svc: scan(code) + Svc-->>App: código marcado UTILIZADO +``` + +**Clases clave:** `ApiSecurityConfig`, `AuthApiController`, `ApiAuthService`, `CognitoAuthService`, `PassCodeApiController`, `JwtConfig` + +### Modo local sin Cognito + +Con `APP_COGNITO_ENABLED=false` (perfil `test` lo usa automáticamente): + +```mermaid +flowchart LR + subgraph web["Web"] + W1["Form login"] --> W2["AppUser en BD"] + end + subgraph api["API"] + A1["POST /api/auth/login"] --> A2["BD + JwtEncoder HS256"] + A3["POST /api/auth/refresh"] --> A4["No disponible"] + A5["Bearer token"] --> A6["localJwtDecoder HS256"] + end +``` + +| `APP_COGNITO_ENABLED` | Web | API login | Validación JWT | Refresh | +|----------------------|-----|-----------|----------------|---------| +| `true` (default) | Cognito client admin | Cognito client scanner | JWK Set URI | Sí | +| `false` | BD + BCrypt | BD + JWT local | Secreto HS256 | No | + +Los usuarios se siembran con `InitialDataLoader` usando `APP_ADMIN_*` y `APP_SCANNER_*`. + +--- + +## Cognito: local (Floci) vs AWS + +El SDK de Cognito elige el destino según `AWS_ENDPOINT_URL`: + +```mermaid +flowchart TD + Start["APP_COGNITO_ENABLED=true"] + Check{"¿AWS_ENDPOINT_URLdefinido?"} + Floci["Floci localhost:4566credenciales test/test"] + AWS["Cognito real en AWSDefaultCredentialsProviderIAM / perfil / env AWS"] + + Start --> Check + Check -->|Sí| Floci + Check -->|No| AWS +``` + +### Local con Floci + +`./run-local.sh` hace todo el setup: + +1. Levanta Floci (y PostgreSQL si el perfil lo incluye) +2. Espera el bootstrap de Cognito (hasta 120 s) +3. Carga `docker/floci/generated/cognito.local.env` automáticamente + +El script `docker/floci/init/ready.d/10-cognito-bootstrap.sh` crea: + +| Recurso | Valor | +|---------|-------| +| User Pool ID | `us-east-1_BaseAdminWeb` | +| Usuario admin | `admin` / `Admin123*` → grupo `admins` | +| Usuario scanner | `scanner` / `Scanner123*` → grupo `scanners` | +| App client admin | `base-admin-web-admin` | +| App client scanner | `base-admin-web-scanner` | + +Variables generadas en `docker/floci/generated/cognito.local.env` (gitignored). + +**Levantar solo Floci:** + +```bash +./scripts/floci-up.sh +``` + +**Levantar stack Docker completo:** + +```bash +docker compose -f docker-compose.local.yml up -d +``` + +| Servicio | Puerto | Persistencia | +|----------|--------|--------------| +| Floci | 4566 | volumen `base_admin_web_floci_data` | +| PostgreSQL | 5432 | volumen `base_admin_web_postgres_data` | + +### Producción con AWS Cognito + +**No definir** `AWS_ENDPOINT_URL`. Configura el User Pool real: + +```bash +export APP_COGNITO_ENABLED=true +export COGNITO_REGION=us-east-1 +export COGNITO_USER_POOL_ID=us-east-1_XXXXX +export COGNITO_ISSUER_URI=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXX +export COGNITO_JWK_SET_URI=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXX/.well-known/jwks.json +export COGNITO_ADMIN_CLIENT_ID=... +export COGNITO_SCANNER_CLIENT_ID=... +# Credenciales AWS vía IAM role, perfil o AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY +``` + +En AWS replica la misma estructura que Floci: grupos `admins`/`scanners`, clients con `USER_PASSWORD_AUTH` habilitado. + +### Verificar Cognito local + +```bash +export AWS_ENDPOINT_URL=http://localhost:4566 +export AWS_DEFAULT_REGION=us-east-1 +export AWS_ACCESS_KEY_ID=test +export AWS_SECRET_ACCESS_KEY=test + +aws cognito-idp list-user-pools --max-results 10 --endpoint-url $AWS_ENDPOINT_URL +aws cognito-idp list-users --user-pool-id us-east-1_BaseAdminWeb --endpoint-url $AWS_ENDPOINT_URL +``` + +--- + +## Módulo de códigos pase + +Panel para dar de alta y consultar códigos pase con QR. + +```mermaid +stateDiagram-v2 + [*] --> ACTIVO: admin crea código (10 chars) + ACTIVO --> UTILIZADO: scanner escanea QR +``` + +| Campo | Descripción | +|-------|-------------| +| Nombre | Etiqueta descriptiva | +| Código | Aleatorio de 10 caracteres (único) | +| Estatus | `ACTIVO` / `UTILIZADO` | +| Escaneado | Indicador booleano | +| Fechas | Creación y escaneo | + +**Panel web:** `GET /admin/pass-codes` — tabla Metronic, modal QR, refresco automático al cerrar el modal. + +**Entidad:** `PassCode` → **Servicio:** `PassCodeService` → **Controladores:** `PassCodeController` (web), `PassCodeApiController` (API). + +--- + +## API REST + +### Login escáner ```http POST /api/auth/login @@ -84,22 +368,35 @@ Content-Type: application/json } ``` -Respuesta exitosa: +Respuesta (Cognito): ```json { "success": true, "accessToken": "eyJhbG...", + "refreshToken": "dXMtZWFzdC0x...", + "idToken": "eyJhbG...", "tokenType": "Bearer", - "expiresIn": 28800, + "expiresIn": 3600, "username": "scanner", "roles": ["ROLE_SCANNER"] } ``` -Solo usuarios con rol `SCANNER` pueden iniciar sesion por API. El usuario `admin` no puede autenticarse por este endpoint. +### Renovar token -### Escanear codigo pase +```http +POST /api/auth/refresh +Content-Type: application/json + +{ + "refreshToken": "dXMtZWFzdC0x..." +} +``` + +Solo disponible con Cognito habilitado. + +### Escanear código pase ```http POST /api/pass-codes/scan @@ -111,19 +408,48 @@ Content-Type: application/json } ``` -Marca el codigo como escaneado y cambia su estatus a `UTILIZADO`. - -### Listado para el panel (requiere sesion admin) +### Listado JSON (panel admin, requiere sesión) ```http GET /admin/pass-codes/list ``` -Devuelve JSON con los codigos registrados. Se usa internamente para refrescar la tabla del panel. +--- + +## App Flutter (escáner) + +Ubicación: `flutter-app/qrscanner/` + +```mermaid +flowchart LR + Login["LoginScreen"] --> Home["HomeScreen"] + Home --> Scanner["ScannerScreen"] + Scanner -->|QR detectado| API["POST /api/pass-codes/scan"] + Login -->|credenciales| Auth["POST /api/auth/login"] +``` + +| Archivo | Rol | +|---------|-----| +| `lib/screens/login_screen.dart` | Login y sesión | +| `lib/screens/scanner_screen.dart` | Lectura QR + escaneo | +| `lib/services/auth_service.dart` | Login, SharedPreferences | +| `lib/services/pass_code_service.dart` | Scan con Bearer token | +| `lib/config/api_config.dart` | URL base de la API | + +**Ejecutar apuntando al backend local:** + +```bash +cd flutter-app/qrscanner +flutter run --dart-define=API_BASE_URL=http://TU_IP:8080 +``` + +La app valida que el usuario tenga `ROLE_SCANNER` (o grupo `scanners`) antes de permitir el escaneo. + +--- ## Arranque local -Desde la raiz del proyecto: +### Opción recomendada ```bash ./run-local.sh @@ -131,44 +457,39 @@ Desde la raiz del proyecto: El script: -- Detecta automaticamente Temurin/Java 25 -- Compila todos los modulos del proyecto -- Arranca el modulo ejecutable `base-admin-web-jar` -- Muestra el perfil activo y la base de datos en uso +- Detecta Java 25 (Temurin) +- Levanta Floci (y PostgreSQL si aplica) +- Espera y carga `cognito.local.env` +- Compila e inicia `base-admin-web-jar` -Si `SPRING_PROFILES_ACTIVE` incluye `postgres`, el script intenta levantar automaticamente el contenedor definido en `docker-compose-postgres.local.yml` antes de iniciar la aplicacion. +Si ya tienes `AWS_ENDPOINT_URL` exportado, no sobrescribe tus variables (útil para probar contra AWS). -Tambien puedes ejecutar: +### Maven manual (terminal) ```bash -mvn test +# 1. Levantar Floci (Cognito local) +docker compose -f docker-compose.local.yml up -d floci + +# 2. Esperar el bootstrap (genera cognito.local.env) +# Revisa que exista: docker/floci/generated/cognito.local.env + +# 3. Cargar variables Cognito en la shell +set -a && source docker/floci/generated/cognito.local.env && set +a + +# 4. Compilar y arrancar mvn -pl base-admin-web-jar -am -DskipTests install mvn -pl base-admin-web-jar spring-boot:run ``` -Aplicacion: +Si Floci ya estaba levantado de antes, basta con el `source` y el `spring-boot:run`. -- Login: `http://localhost:8080/login` -- Dashboard: `http://localhost:8080/dashboard` -- Codigos pase: `http://localhost:8080/admin/pass-codes` -- H2 Console: `http://localhost:8080/h2-console` - -## Perfiles - -- `dev`: H2 en archivo (`./data/h2/`), consola H2 habilitada, datos persistentes entre reinicios -- `prod`: cache Thymeleaf y H2 console deshabilitada -- `postgres`: datasource PostgreSQL, datos persistentes en el servidor o volumen Docker -- `test`: H2 en memoria con `create-drop`, usado solo por `mvn test` - -Con `postgres`, `run-local.sh` intenta arrancar Docker Compose automaticamente. - -Ejemplo con PostgreSQL local: +### PostgreSQL local ```bash SPRING_PROFILES_ACTIVE=postgres ./run-local.sh ``` -Ejemplo con PostgreSQL remoto: +### PostgreSQL remoto ```bash SPRING_PROFILES_ACTIVE=postgres \ @@ -178,44 +499,230 @@ SPRING_DATASOURCE_PASSWORD=tu_password \ ./run-local.sh ``` -## Variables utiles +--- -### Aplicacion +## Arranque manual e IntelliJ IDEA -- `SERVER_PORT` -- `SPRING_PROFILES_ACTIVE` -- `APP_TITLE` +Si abres el proyecto en IntelliJ (o cualquier IDE) sin usar `run-local.sh`, debes levantar Floci y configurar variables de entorno tú mismo. `run-local.sh` automatiza exactamente esos pasos. + +```mermaid +flowchart TD + A["Abrir proyecto Maven en IntelliJ"] --> B["Java 25 como Project SDK"] + B --> C["docker compose up floci"] + C --> D["Esperar cognito.local.env"] + D --> E["Configurar Run Configuration"] + E --> F["Run BaseAdminWebApplication"] + F --> G["http://localhost:8080/login"] +``` + +### Requisitos previos + +| Requisito | Versión / nota | +|-----------|----------------| +| JDK | **Java 25** (Temurin recomendado) | +| Maven | Incluido en el proyecto (wrapper no obligatorio; usa el Maven del IDE) | +| Docker | Para Cognito local (Floci) en puerto `4566` | +| IntelliJ | Ultimate o Community (con plugin Spring Boot en Community) | + +### 1. Importar el proyecto + +1. **File → Open** y selecciona la carpeta raíz del repo (donde está el `pom.xml` padre). +2. IntelliJ detecta el proyecto **Maven multi-módulo**. Espera a que termine la indexación e importación. +3. **File → Project Structure → Project**: SDK = **Java 25**, language level = **25**. +4. **File → Settings → Build → Build Tools → Maven → Runner**: JRE = **Project SDK (Java 25)**. + +### 2. Levantar Floci (Cognito local) + +En una terminal (puede ser la Terminal integrada de IntelliJ): + +```bash +docker compose -f docker-compose.local.yml up -d floci +``` + +La primera vez Floci ejecuta el bootstrap y escribe: + +``` +docker/floci/generated/cognito.local.env +``` + +Comprueba que el archivo existe (puede tardar ~30–120 s): + +```bash +cat docker/floci/generated/cognito.local.env +``` + +Si no aparece, revisa los logs: + +```bash +docker logs base-admin-web-floci +``` + +**PostgreSQL local:** si usas el perfil `postgres`, levanta también el servicio: + +```bash +docker compose -f docker-compose.local.yml up -d postgres floci +``` + +### 3. Run Configuration en IntelliJ + +Crea una configuración **Spring Boot** (o **Application**): + +| Campo | Valor | +|-------|-------| +| **Name** | `BaseAdminWebApplication` | +| **Main class** | `mx.gob.slp.baseadminweb.jar.BaseAdminWebApplication` | +| **Module** | `base-admin-web-jar` | +| **Working directory** | `$PROJECT_DIR$/base-admin-web-jar` | +| **Active profiles** | `dev` (o `postgres` si aplica) | + +> **Working directory:** H2 guarda datos en `./data/h2/` relativo al directorio de trabajo. Debe ser `base-admin-web-jar` para que coincida con `mvn spring-boot:run` y no mezclar rutas de BD. + +#### Variables de entorno (Cognito local) + +En **Environment variables**, pega el contenido de `docker/floci/generated/cognito.local.env`: + +``` +COGNITO_USER_POOL_ID=us-east-1_BaseAdminWeb +COGNITO_REGION=us-east-1 +COGNITO_ISSUER_URI=http://localhost:4566/us-east-1_BaseAdminWeb +COGNITO_JWK_SET_URI=http://localhost:4566/us-east-1_BaseAdminWeb/.well-known/jwks.json +COGNITO_ADMIN_CLIENT_ID= +COGNITO_SCANNER_CLIENT_ID= +COGNITO_ADMIN_GROUP=admins +COGNITO_SCANNER_GROUP=scanners +AWS_ENDPOINT_URL=http://localhost:4566 +AWS_DEFAULT_REGION=us-east-1 +AWS_ACCESS_KEY_ID=test +AWS_SECRET_ACCESS_KEY=test +``` + +Los `CLIENT_ID` cambian en cada bootstrap; **copia siempre los del archivo generado**, no uses valores fijos de ejemplos antiguos. + +**Alternativa:** plugin [EnvFile](https://plugins.jetbrains.com/plugin/7861-envfile) → añade `docker/floci/generated/cognito.local.env` como archivo de entorno en la Run Configuration. + +#### Perfil `postgres` en IntelliJ + +Además de **Active profiles** = `postgres`, añade en Environment variables: + +``` +SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/base_admin_web +SPRING_DATASOURCE_USERNAME=postgres +SPRING_DATASOURCE_PASSWORD=postgres +``` + +(Y las variables Cognito del paso anterior.) + +### 4. Ejecutar + +1. **Build → Build Project** (o `Ctrl+F9`). +2. Run ▶ en `BaseAdminWebApplication`. +3. Abre http://localhost:8080/login — usuario `admin` / `Admin123*`. + +### 5. Desarrollo sin Docker (modo local puro) + +Si no quieres Floci, desactiva Cognito y usa usuarios de la BD: + +| Variable | Valor | +|----------|-------| +| `APP_COGNITO_ENABLED` | `false` | + +No hace falta `AWS_ENDPOINT_URL` ni levantar Docker. Los usuarios `admin` y `scanner` se crean al arrancar vía `InitialDataLoader`. + +El login API devuelve JWT local (HS256); **refresh no está disponible** en este modo. + +### 6. Checklist de problemas frecuentes + +| Síntoma | Causa probable | Solución | +|---------|----------------|----------| +| Login web falla con Cognito habilitado | Floci no está arriba o faltan env vars | `docker compose up -d floci` + cargar `cognito.local.env` | +| Login va a AWS real en local | Falta `AWS_ENDPOINT_URL` | Definir `AWS_ENDPOINT_URL=http://localhost:4566` | +| `Invalid client id` | Client IDs desactualizados | Regenerar con bootstrap y copiar de `cognito.local.env` | +| H2 vacío o ruta rara | Working directory incorrecto | `$PROJECT_DIR$/base-admin-web-jar` | +| Error de versión Java | SDK distinto a 25 | Project Structure → SDK Java 25 | +| Tests en IDE fallan por Cognito | Perfil `test` desactiva Cognito | Ejecutar tests con perfil `test` (ya configurado en `application-test.yml`) | + +### 7. Comandos útiles desde IntelliJ + +| Acción | Comando Maven (ventana Maven o terminal) | +|--------|------------------------------------------| +| Compilar todo | `mvn -pl base-admin-web-jar -am -DskipTests install` | +| Tests | `mvn test` | +| Solo módulo jar | clic derecho en `base-admin-web-jar` → Run Maven → `spring-boot:run` (tras exportar env vars en la shell) | + +--- + +## Perfiles Spring + +| Perfil | Base de datos | Cognito | Uso | +|--------|---------------|---------|-----| +| `dev` (default) | H2 en archivo `./data/h2/` | Floci (manual o `run-local.sh`) | Desarrollo diario | +| `postgres` | PostgreSQL | Floci | Desarrollo con BD real | +| `prod` | Configurable | AWS | Producción | +| `test` | H2 en memoria `create-drop` | Deshabilitado | `mvn test` | + +--- + +## Variables de entorno + +### Aplicación + +| Variable | Default | Descripción | +|----------|---------|-------------| +| `SERVER_PORT` | `8080` | Puerto HTTP | +| `SPRING_PROFILES_ACTIVE` | `dev` | Perfil Spring | +| `APP_TITLE` | `Base Admin Web` | Título en vistas | + +### Cognito + +| Variable | Local (Floci) | Producción (AWS) | +|----------|---------------|------------------| +| `APP_COGNITO_ENABLED` | `true` | `true` | +| `COGNITO_REGION` | `us-east-1` | tu región | +| `COGNITO_USER_POOL_ID` | `us-east-1_BaseAdminWeb` | `us-east-1_XXXXX` | +| `COGNITO_ISSUER_URI` | `http://localhost:4566/...` | `https://cognito-idp.{region}.amazonaws.com/{poolId}` | +| `COGNITO_JWK_SET_URI` | `http://localhost:4566/.../jwks.json` | `https://cognito-idp.{region}.amazonaws.com/{poolId}/.well-known/jwks.json` | +| `COGNITO_ADMIN_CLIENT_ID` | generado por Floci | client del pool | +| `COGNITO_SCANNER_CLIENT_ID` | generado por Floci | client del pool | +| `COGNITO_ADMIN_GROUP` | `admins` | `admins` | +| `COGNITO_SCANNER_GROUP` | `scanners` | `scanners` | +| `AWS_ENDPOINT_URL` | `http://localhost:4566` | **no definir** | ### Base de datos -- `SPRING_DATASOURCE_URL` -- `SPRING_DATASOURCE_USERNAME` -- `SPRING_DATASOURCE_PASSWORD` -- `SPRING_JPA_HIBERNATE_DDL_AUTO` +| Variable | Descripción | +|----------|-------------| +| `SPRING_DATASOURCE_URL` | JDBC URL | +| `SPRING_DATASOURCE_USERNAME` | Usuario BD | +| `SPRING_DATASOURCE_PASSWORD` | Contraseña BD | +| `SPRING_JPA_HIBERNATE_DDL_AUTO` | `update` por defecto | -### JWT API +### JWT local (solo `APP_COGNITO_ENABLED=false`) -- `APP_JWT_SECRET` (minimo 32 caracteres) -- `APP_JWT_EXPIRATION_HOURS` +| Variable | Descripción | +|----------|-------------| +| `APP_JWT_SECRET` | Secreto HS256 (mín. 32 caracteres) | +| `APP_JWT_EXPIRATION_HOURS` | Expiración del token | -## Seguridad +### Usuarios semilla (modo local sin Cognito) -- Panel web: autenticacion por formulario con usuarios en base de datos -- API de escaneo: autenticacion stateless con JWT -- `/admin/**` y `/dashboard`: requieren rol `ADMIN` -- `/api/pass-codes/scan`: requiere rol `SCANNER` y token Bearer -- `/api/auth/login`: publico, pero solo emite token a usuarios `SCANNER` +| Variable | Default | +|----------|---------| +| `APP_ADMIN_USERNAME` / `APP_ADMIN_PASSWORD` | `admin` / `Admin123*` | +| `APP_SCANNER_USERNAME` / `APP_SCANNER_PASSWORD` | `scanner` / `Scanner123*` | + +--- ## Objetivo de esta base -Esta base deja resuelto lo minimo para arrancar nuevos proyectos internos: +Esta base deja resuelto lo mínimo para arrancar nuevos proyectos internos: -- autenticacion local funcional para administradores -- autenticacion API para escaneo movil o externo -- layout inicial con Metronic -- dashboard simple -- modulo de codigos pase con QR y API de escaneo -- estructura modular limpia -- soporte rapido para H2 y PostgreSQL +- Autenticación Cognito (local y AWS) con fallback a BD local +- Panel web con Metronic y sesión HTTP para administradores +- API stateless con JWT para escaneo móvil o externo +- App Flutter de referencia para escaneo QR +- Módulo de códigos pase con alta, listado, QR y API de escaneo +- Estructura modular Maven limpia +- Soporte rápido para H2 y PostgreSQL +- Floci para desarrollo sin depender de AWS -El dashboard actual se mantiene deliberadamente sencillo para servir como punto de partida para futuras integraciones. \ No newline at end of file +El dashboard se mantiene deliberadamente sencillo como punto de partida para futuras integraciones. \ No newline at end of file diff --git a/base-admin-web-configuration/pom.xml b/base-admin-web-configuration/pom.xml index 6b49d43..640b6c9 100644 --- a/base-admin-web-configuration/pom.xml +++ b/base-admin-web-configuration/pom.xml @@ -27,6 +27,10 @@ org.springframework.boot spring-boot-starter-oauth2-resource-server + + software.amazon.awssdk + cognitoidentityprovider + org.springframework.boot spring-boot-starter-thymeleaf diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiAuthService.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiAuthService.java index 3019404..c12173d 100644 --- a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiAuthService.java +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiAuthService.java @@ -1,77 +1,50 @@ package mx.gob.slp.baseadminweb.configuration.security; -import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.jose.jws.MacAlgorithm; -import org.springframework.security.oauth2.jwt.JwsHeader; -import org.springframework.security.oauth2.jwt.JwtClaimsSet; -import org.springframework.security.oauth2.jwt.JwtEncoder; -import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.stereotype.Service; -import java.time.Instant; -import java.util.List; - @Service public class ApiAuthService { - private static final String SCANNER_ROLE = "ROLE_SCANNER"; + private final ObjectProvider cognitoAuthService; + private final ObjectProvider localJwtAuthService; - private final AuthenticationManager authenticationManager; - private final JwtEncoder jwtEncoder; - private final JwtProperties jwtProperties; - - public ApiAuthService(AuthenticationManager authenticationManager, - JwtEncoder jwtEncoder, - JwtProperties jwtProperties) { - this.authenticationManager = authenticationManager; - this.jwtEncoder = jwtEncoder; - this.jwtProperties = jwtProperties; + public ApiAuthService(ObjectProvider cognitoAuthService, + ObjectProvider localJwtAuthService) { + this.cognitoAuthService = cognitoAuthService; + this.localJwtAuthService = localJwtAuthService; } public ApiLoginResponse login(String username, String password) { - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(username, password)); - - List roles = authentication.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .toList(); - - if (roles.stream().noneMatch(SCANNER_ROLE::equals)) { - throw new BadCredentialsException("El usuario no tiene permisos para escanear codigos."); + CognitoAuthService cognito = cognitoAuthService.getIfAvailable(); + if (cognito != null) { + return cognito.login(username, password); } - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plusSeconds(jwtProperties.getExpirationHours() * 3600L); + LocalJwtAuthService local = localJwtAuthService.getIfAvailable(); + if (local != null) { + return local.login(username, password); + } - JwtClaimsSet claims = JwtClaimsSet.builder() - .subject(authentication.getName()) - .issuedAt(issuedAt) - .expiresAt(expiresAt) - .claim("roles", roles) - .build(); - - JwsHeader header = JwsHeader.with(MacAlgorithm.HS256).build(); - String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue(); - - return new ApiLoginResponse( - accessToken, - "Bearer", - jwtProperties.getExpirationHours() * 3600L, - authentication.getName(), - roles - ); + throw new IllegalStateException("No hay proveedor de autenticacion API configurado."); } - public record ApiLoginResponse( - String accessToken, - String tokenType, - long expiresIn, - String username, - List roles - ) { + public ApiLoginResponse refresh(String refreshToken) { + if (refreshToken == null || refreshToken.isBlank()) { + throw new BadCredentialsException("El refresh token es obligatorio."); + } + + CognitoAuthService cognito = cognitoAuthService.getIfAvailable(); + if (cognito != null) { + return cognito.refresh(refreshToken); + } + + LocalJwtAuthService local = localJwtAuthService.getIfAvailable(); + if (local != null) { + return local.refresh(refreshToken); + } + + throw new IllegalStateException("No hay proveedor de autenticacion API configurado."); } } \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiLoginResponse.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiLoginResponse.java new file mode 100644 index 0000000..0ddcc36 --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiLoginResponse.java @@ -0,0 +1,14 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import java.util.List; + +public record ApiLoginResponse( + String accessToken, + String refreshToken, + String idToken, + String tokenType, + long expiresIn, + String username, + List roles +) { +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiSecurityConfig.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiSecurityConfig.java index ee84e6c..984ebcb 100644 --- a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiSecurityConfig.java +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/ApiSecurityConfig.java @@ -27,7 +27,7 @@ public class ApiSecurityConfig { .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/api/auth/login").permitAll() + .requestMatchers("/api/auth/login", "/api/auth/refresh").permitAll() .requestMatchers("/api/pass-codes/scan").hasRole("SCANNER") .anyRequest().authenticated() ) diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/AuthenticatedWebUser.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/AuthenticatedWebUser.java new file mode 100644 index 0000000..4958d01 --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/AuthenticatedWebUser.java @@ -0,0 +1,6 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import java.io.Serializable; + +public record AuthenticatedWebUser(String username, String displayName) implements Serializable { +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoAuthService.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoAuthService.java new file mode 100644 index 0000000..c6aaa30 --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoAuthService.java @@ -0,0 +1,52 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true") +public class CognitoAuthService { + + private static final String SCANNER_ROLE = "ROLE_SCANNER"; + + private final CognitoUserAuthenticator cognitoUserAuthenticator; + private final CognitoProperties cognitoProperties; + + public CognitoAuthService(CognitoUserAuthenticator cognitoUserAuthenticator, + CognitoProperties cognitoProperties) { + this.cognitoUserAuthenticator = cognitoUserAuthenticator; + this.cognitoProperties = cognitoProperties; + } + + public ApiLoginResponse login(String username, String password) { + CognitoUserAuthenticator.AuthenticationResult result = cognitoUserAuthenticator.authenticate( + username, + password, + cognitoProperties.getScannerClientId(), + SCANNER_ROLE + ); + return toLoginResponse(result); + } + + public ApiLoginResponse refresh(String refreshToken) { + CognitoUserAuthenticator.AuthenticationResult result = cognitoUserAuthenticator.refresh( + refreshToken, + cognitoProperties.getScannerClientId(), + SCANNER_ROLE + ); + return toLoginResponse(result); + } + + private ApiLoginResponse toLoginResponse(CognitoUserAuthenticator.AuthenticationResult result) { + var tokens = result.tokens(); + return new ApiLoginResponse( + tokens.accessToken(), + result.refreshToken(), + tokens.idToken(), + tokens.tokenType() != null ? tokens.tokenType() : "Bearer", + tokens.expiresIn() != null ? tokens.expiresIn() : 0L, + result.username(), + result.roles() + ); + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoClientConfig.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoClientConfig.java new file mode 100644 index 0000000..2c503b2 --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoClientConfig.java @@ -0,0 +1,33 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient; + +import java.net.URI; + +@Configuration +@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true") +public class CognitoClientConfig { + + @Bean(destroyMethod = "close") + CognitoIdentityProviderClient cognitoIdentityProviderClient(CognitoProperties properties) { + var builder = CognitoIdentityProviderClient.builder() + .region(Region.of(properties.getRegion())); + + if (properties.usesLocalEndpoint()) { + builder.endpointOverride(URI.create(properties.getEndpoint())) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("test", "test"))); + } else { + builder.credentialsProvider(DefaultCredentialsProvider.create()); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoDisplayNameResolver.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoDisplayNameResolver.java new file mode 100644 index 0000000..ff54e64 --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoDisplayNameResolver.java @@ -0,0 +1,31 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import com.nimbusds.jwt.JWTParser; + +final class CognitoDisplayNameResolver { + + private CognitoDisplayNameResolver() { + } + + static String resolve(String idToken, String accessToken, String fallbackUsername) { + String displayName = extractNameClaim(idToken); + if (displayName == null || displayName.isBlank()) { + displayName = extractNameClaim(accessToken); + } + if (displayName == null || displayName.isBlank()) { + return fallbackUsername; + } + return displayName; + } + + private static String extractNameClaim(String token) { + if (token == null || token.isBlank()) { + return null; + } + try { + return JWTParser.parse(token).getJWTClaimsSet().getStringClaim("name"); + } catch (Exception ex) { + return null; + } + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoProperties.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoProperties.java new file mode 100644 index 0000000..7c986eb --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoProperties.java @@ -0,0 +1,102 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "app.cognito") +public class CognitoProperties { + + private boolean enabled; + private String region = "us-east-1"; + private String userPoolId = "us-east-1_BaseAdminWeb"; + private String issuerUri = "http://localhost:4566/us-east-1_BaseAdminWeb"; + private String jwkSetUri = "http://localhost:4566/us-east-1_BaseAdminWeb/.well-known/jwks.json"; + private String endpoint; + private String scannerClientId; + private String adminClientId; + private String scannerGroup = "scanners"; + private String adminGroup = "admins"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public String getUserPoolId() { + return userPoolId; + } + + public void setUserPoolId(String userPoolId) { + this.userPoolId = userPoolId; + } + + public String getIssuerUri() { + return issuerUri; + } + + public void setIssuerUri(String issuerUri) { + this.issuerUri = issuerUri; + } + + public String getJwkSetUri() { + return jwkSetUri; + } + + public void setJwkSetUri(String jwkSetUri) { + this.jwkSetUri = jwkSetUri; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public boolean usesLocalEndpoint() { + return endpoint != null && !endpoint.isBlank(); + } + + public String getScannerClientId() { + return scannerClientId; + } + + public void setScannerClientId(String scannerClientId) { + this.scannerClientId = scannerClientId; + } + + public String getAdminClientId() { + return adminClientId; + } + + public void setAdminClientId(String adminClientId) { + this.adminClientId = adminClientId; + } + + public String getScannerGroup() { + return scannerGroup; + } + + public void setScannerGroup(String scannerGroup) { + this.scannerGroup = scannerGroup; + } + + public String getAdminGroup() { + return adminGroup; + } + + public void setAdminGroup(String adminGroup) { + this.adminGroup = adminGroup; + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoRoleMapper.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoRoleMapper.java new file mode 100644 index 0000000..526f9a6 --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoRoleMapper.java @@ -0,0 +1,29 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +final class CognitoRoleMapper { + + private CognitoRoleMapper() { + } + + static List toRoles(Collection groups, CognitoProperties properties) { + if (groups == null || groups.isEmpty()) { + return List.of(); + } + + List roles = new ArrayList<>(); + for (String group : groups) { + if (properties.getScannerGroup().equalsIgnoreCase(group)) { + roles.add("ROLE_SCANNER"); + } else if (properties.getAdminGroup().equalsIgnoreCase(group)) { + roles.add("ROLE_ADMIN"); + } else { + roles.add("ROLE_" + group.toUpperCase()); + } + } + return roles; + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoUserAuthenticator.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoUserAuthenticator.java new file mode 100644 index 0000000..d86748d --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoUserAuthenticator.java @@ -0,0 +1,119 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import com.nimbusds.jwt.JWTParser; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.stereotype.Component; +import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthFlowType; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthenticationResultType; +import software.amazon.awssdk.services.cognitoidentityprovider.model.InitiateAuthRequest; +import software.amazon.awssdk.services.cognitoidentityprovider.model.InitiateAuthResponse; +import software.amazon.awssdk.services.cognitoidentityprovider.model.NotAuthorizedException; +import software.amazon.awssdk.services.cognitoidentityprovider.model.UserNotFoundException; + +import java.text.ParseException; +import java.util.List; +import java.util.Map; + +@Component +@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true") +public class CognitoUserAuthenticator { + + private final CognitoIdentityProviderClient cognitoClient; + private final CognitoProperties cognitoProperties; + + public CognitoUserAuthenticator(CognitoIdentityProviderClient cognitoClient, + CognitoProperties cognitoProperties) { + this.cognitoClient = cognitoClient; + this.cognitoProperties = cognitoProperties; + } + + public AuthenticationResult authenticate(String username, + String password, + String clientId, + String requiredRole) { + try { + InitiateAuthResponse response = cognitoClient.initiateAuth(InitiateAuthRequest.builder() + .authFlow(AuthFlowType.USER_PASSWORD_AUTH) + .clientId(clientId) + .authParameters(Map.of( + "USERNAME", username, + "PASSWORD", password + )) + .build()); + + return toAuthenticationResult(response.authenticationResult(), username, requiredRole, null); + } catch (NotAuthorizedException | UserNotFoundException ex) { + throw new BadCredentialsException("Credenciales invalidas."); + } + } + + public AuthenticationResult refresh(String refreshToken, String clientId, String requiredRole) { + try { + InitiateAuthResponse response = cognitoClient.initiateAuth(InitiateAuthRequest.builder() + .authFlow(AuthFlowType.REFRESH_TOKEN_AUTH) + .clientId(clientId) + .authParameters(Map.of("REFRESH_TOKEN", refreshToken)) + .build()); + + return toAuthenticationResult(response.authenticationResult(), null, requiredRole, refreshToken); + } catch (NotAuthorizedException ex) { + throw new BadCredentialsException("Refresh token invalido o expirado."); + } + } + + private AuthenticationResult toAuthenticationResult(AuthenticationResultType authResult, + String fallbackUsername, + String requiredRole, + String fallbackRefreshToken) { + if (authResult == null || authResult.accessToken() == null) { + throw new BadCredentialsException("No se pudo autenticar con Cognito."); + } + + List groups = extractGroups(authResult.accessToken()); + List roles = CognitoRoleMapper.toRoles(groups, cognitoProperties); + if (roles.stream().noneMatch(requiredRole::equals)) { + throw new BadCredentialsException("El usuario no tiene permisos para acceder."); + } + + String username = extractUsername(authResult.accessToken(), fallbackUsername); + String refreshToken = authResult.refreshToken() != null + ? authResult.refreshToken() + : fallbackRefreshToken; + + return new AuthenticationResult(authResult, username, roles, groups, refreshToken); + } + + private String extractUsername(String accessToken, String fallback) { + try { + var claims = JWTParser.parse(accessToken).getJWTClaimsSet(); + String username = claims.getStringClaim("username"); + if (username == null || username.isBlank()) { + username = claims.getStringClaim("cognito:username"); + } + return username != null && !username.isBlank() ? username : fallback; + } catch (ParseException ex) { + throw new BadCredentialsException("No se pudo leer el usuario del token."); + } + } + + private List extractGroups(String accessToken) { + try { + var claims = JWTParser.parse(accessToken).getJWTClaimsSet(); + List groups = claims.getStringListClaim("cognito:groups"); + return groups != null ? groups : List.of(); + } catch (ParseException ex) { + throw new BadCredentialsException("No se pudieron leer los grupos del token."); + } + } + + public record AuthenticationResult( + AuthenticationResultType tokens, + String username, + List roles, + List groups, + String refreshToken + ) { + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoWebAuthenticationProvider.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoWebAuthenticationProvider.java new file mode 100644 index 0000000..5d32e0b --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoWebAuthenticationProvider.java @@ -0,0 +1,63 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true") +public class CognitoWebAuthenticationProvider implements AuthenticationProvider { + + private static final String ADMIN_ROLE = "ROLE_ADMIN"; + + private final CognitoUserAuthenticator cognitoUserAuthenticator; + private final CognitoProperties cognitoProperties; + + public CognitoWebAuthenticationProvider(CognitoUserAuthenticator cognitoUserAuthenticator, + CognitoProperties cognitoProperties) { + this.cognitoUserAuthenticator = cognitoUserAuthenticator; + this.cognitoProperties = cognitoProperties; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + String username = authentication.getName(); + Object credentials = authentication.getCredentials(); + if (credentials == null) { + throw new BadCredentialsException("La contrasena es obligatoria."); + } + + CognitoUserAuthenticator.AuthenticationResult result = cognitoUserAuthenticator.authenticate( + username, + credentials.toString(), + cognitoProperties.getAdminClientId(), + ADMIN_ROLE + ); + + var authorities = result.roles().stream() + .map(SimpleGrantedAuthority::new) + .toList(); + + String displayName = CognitoDisplayNameResolver.resolve( + result.tokens().idToken(), + result.tokens().accessToken(), + result.username() + ); + + return UsernamePasswordAuthenticationToken.authenticated( + new AuthenticatedWebUser(result.username(), displayName), + null, + authorities + ); + } + + @Override + public boolean supports(Class> authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoWebSecurityConfig.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoWebSecurityConfig.java new file mode 100644 index 0000000..65d2b03 --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/CognitoWebSecurityConfig.java @@ -0,0 +1,17 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; + +@Configuration +@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true") +public class CognitoWebSecurityConfig { + + @Bean(name = "webAuthenticationManager") + AuthenticationManager webAuthenticationManager(CognitoWebAuthenticationProvider cognitoWebAuthenticationProvider) { + return new ProviderManager(cognitoWebAuthenticationProvider); + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/JwtConfig.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/JwtConfig.java index fa2df17..895fe77 100644 --- a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/JwtConfig.java +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/JwtConfig.java @@ -1,9 +1,13 @@ package mx.gob.slp.baseadminweb.configuration.security; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; @@ -19,12 +23,16 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; @Configuration -@EnableConfigurationProperties(JwtProperties.class) +@EnableConfigurationProperties({JwtProperties.class, CognitoProperties.class}) public class JwtConfig { @Bean + @ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "false") JwtEncoder jwtEncoder(JwtProperties jwtProperties) { OctetSequenceKey jwk = new OctetSequenceKey.Builder(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)) .algorithm(JWSAlgorithm.HS256) @@ -33,20 +41,35 @@ public class JwtConfig { } @Bean - JwtDecoder jwtDecoder(JwtProperties jwtProperties) { + @ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "false") + JwtDecoder localJwtDecoder(JwtProperties jwtProperties) { return NimbusJwtDecoder.withSecretKey(secretKey(jwtProperties)) .macAlgorithm(MacAlgorithm.HS256) .build(); } @Bean - JwtAuthenticationConverter jwtAuthenticationConverter() { - JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); - authoritiesConverter.setAuthoritiesClaimName("roles"); - authoritiesConverter.setAuthorityPrefix(""); + @ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true") + JwtDecoder cognitoJwtDecoder(CognitoProperties cognitoProperties) { + return NimbusJwtDecoder.withJwkSetUri(cognitoProperties.getJwkSetUri()).build(); + } + + @Bean + JwtAuthenticationConverter jwtAuthenticationConverter(CognitoProperties cognitoProperties) { + JwtGrantedAuthoritiesConverter localRolesConverter = new JwtGrantedAuthoritiesConverter(); + localRolesConverter.setAuthoritiesClaimName("roles"); + localRolesConverter.setAuthorityPrefix(""); JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter(); - authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + authenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> { + Collection authorities = new ArrayList<>(); + List groups = jwt.getClaimAsStringList("cognito:groups"); + CognitoRoleMapper.toRoles(groups, cognitoProperties).stream() + .map(SimpleGrantedAuthority::new) + .forEach(authorities::add); + authorities.addAll(localRolesConverter.convert(jwt)); + return authorities; + }); return authenticationConverter; } diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/LocalJwtAuthService.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/LocalJwtAuthService.java new file mode 100644 index 0000000..14c5ccb --- /dev/null +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/LocalJwtAuthService.java @@ -0,0 +1,76 @@ +package mx.gob.slp.baseadminweb.configuration.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jwt.JwsHeader; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; + +@Service +@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "false") +public class LocalJwtAuthService { + + private static final String SCANNER_ROLE = "ROLE_SCANNER"; + + private final AuthenticationManager authenticationManager; + private final JwtEncoder jwtEncoder; + private final JwtProperties jwtProperties; + + public LocalJwtAuthService(AuthenticationManager authenticationManager, + JwtEncoder jwtEncoder, + JwtProperties jwtProperties) { + this.authenticationManager = authenticationManager; + this.jwtEncoder = jwtEncoder; + this.jwtProperties = jwtProperties; + } + + public ApiLoginResponse login(String username, String password) { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(username, password)); + + List roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .toList(); + + if (roles.stream().noneMatch(SCANNER_ROLE::equals)) { + throw new BadCredentialsException("El usuario no tiene permisos para escanear codigos."); + } + + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plusSeconds(jwtProperties.getExpirationHours() * 3600L); + + JwtClaimsSet claims = JwtClaimsSet.builder() + .subject(authentication.getName()) + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .claim("roles", roles) + .build(); + + JwsHeader header = JwsHeader.with(MacAlgorithm.HS256).build(); + String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue(); + + return new ApiLoginResponse( + accessToken, + null, + null, + "Bearer", + jwtProperties.getExpirationHours() * 3600L, + authentication.getName(), + roles + ); + } + + public ApiLoginResponse refresh(String refreshToken) { + throw new BadCredentialsException("Refresh token no disponible sin Cognito."); + } +} \ No newline at end of file diff --git a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/SecurityConfig.java b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/SecurityConfig.java index 1a128fe..e861cbd 100644 --- a/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/SecurityConfig.java +++ b/base-admin-web-configuration/src/main/java/mx/gob/slp/baseadminweb/configuration/security/SecurityConfig.java @@ -1,10 +1,13 @@ package mx.gob.slp.baseadminweb.configuration.security; import mx.gob.slp.baseadminweb.application.service.AppUserDetailsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -22,12 +25,17 @@ public class SecurityConfig { private final AppUserDetailsService appUserDetailsService; + @Autowired(required = false) + @Qualifier("webAuthenticationManager") + private AuthenticationManager webAuthenticationManager; + public SecurityConfig(AppUserDetailsService appUserDetailsService) { this.appUserDetailsService = appUserDetailsService; } @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, + PasswordEncoder passwordEncoder) throws Exception { http .securityMatcher(request -> !request.getRequestURI().startsWith("/api/")) .authorizeHttpRequests(authorize -> authorize @@ -49,9 +57,16 @@ public class SecurityConfig { .deleteCookies("JSESSIONID") ) .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) - .userDetailsService(appUserDetailsService) .rememberMe(Customizer.withDefaults()); + if (webAuthenticationManager != null) { + http.authenticationManager(webAuthenticationManager); + } else { + DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(appUserDetailsService); + daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); + http.authenticationProvider(daoAuthenticationProvider); + } + http.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**")); return http.build(); } @@ -65,4 +80,4 @@ public class SecurityConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -} +} \ No newline at end of file diff --git a/base-admin-web-jar/data/h2/baseadminweb.mv.db b/base-admin-web-jar/data/h2/baseadminweb.mv.db deleted file mode 100644 index 6b78434..0000000 Binary files a/base-admin-web-jar/data/h2/baseadminweb.mv.db and /dev/null differ diff --git a/base-admin-web-jar/src/main/resources/application-test.yml b/base-admin-web-jar/src/main/resources/application-test.yml index 880aa51..5604b2c 100644 --- a/base-admin-web-jar/src/main/resources/application-test.yml +++ b/base-admin-web-jar/src/main/resources/application-test.yml @@ -1,3 +1,7 @@ +app: + cognito: + enabled: false + spring: datasource: url: jdbc:h2:mem:baseadminweb-test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE diff --git a/base-admin-web-jar/src/main/resources/application.yml b/base-admin-web-jar/src/main/resources/application.yml index 268eccd..c5fdf94 100644 --- a/base-admin-web-jar/src/main/resources/application.yml +++ b/base-admin-web-jar/src/main/resources/application.yml @@ -35,6 +35,17 @@ logging: app: title: ${APP_TITLE:Base Admin Web} + cognito: + enabled: ${APP_COGNITO_ENABLED:true} + region: ${COGNITO_REGION:us-east-1} + user-pool-id: ${COGNITO_USER_POOL_ID:us-east-1_BaseAdminWeb} + issuer-uri: ${COGNITO_ISSUER_URI:http://localhost:4566/us-east-1_BaseAdminWeb} + jwk-set-uri: ${COGNITO_JWK_SET_URI:http://localhost:4566/us-east-1_BaseAdminWeb/.well-known/jwks.json} + endpoint: ${AWS_ENDPOINT_URL:} + scanner-client-id: ${COGNITO_SCANNER_CLIENT_ID:4f878f50de4847b7adc7c0632a} + admin-client-id: ${COGNITO_ADMIN_CLIENT_ID:dde208d5b768465db6a2d8c6d2} + scanner-group: ${COGNITO_SCANNER_GROUP:scanners} + admin-group: ${COGNITO_ADMIN_GROUP:admins} jwt: secret: ${APP_JWT_SECRET:base-admin-web-dev-secret-change-me-32chars!!} expiration-hours: ${APP_JWT_EXPIRATION_HOURS:8} diff --git a/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/AuthApiController.java b/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/AuthApiController.java index 9900450..3074108 100644 --- a/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/AuthApiController.java +++ b/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/AuthApiController.java @@ -1,6 +1,7 @@ package mx.gob.slp.baseadminweb.web.controller; import mx.gob.slp.baseadminweb.configuration.security.ApiAuthService; +import mx.gob.slp.baseadminweb.configuration.security.ApiLoginResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.BadCredentialsException; @@ -9,6 +10,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.LinkedHashMap; import java.util.Map; @RestController @@ -24,23 +26,46 @@ public class AuthApiController { @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginRequest request) { try { - ApiAuthService.ApiLoginResponse response = apiAuthService.login(request.username(), request.password()); - return ResponseEntity.ok(Map.of( - "success", true, - "accessToken", response.accessToken(), - "tokenType", response.tokenType(), - "expiresIn", response.expiresIn(), - "username", response.username(), - "roles", response.roles() - )); + ApiLoginResponse response = apiAuthService.login(request.username(), request.password()); + return ResponseEntity.ok(toAuthBody(response)); } catch (BadCredentialsException ex) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of( - "success", false, - "message", ex.getMessage() - )); + return unauthorized(ex.getMessage()); } } + @PostMapping("/refresh") + public ResponseEntity> refresh(@RequestBody RefreshRequest request) { + try { + ApiLoginResponse response = apiAuthService.refresh(request.refreshToken()); + return ResponseEntity.ok(toAuthBody(response)); + } catch (BadCredentialsException ex) { + return unauthorized(ex.getMessage()); + } + } + + private Map toAuthBody(ApiLoginResponse response) { + Map body = new LinkedHashMap<>(); + body.put("success", true); + body.put("accessToken", response.accessToken()); + body.put("refreshToken", response.refreshToken()); + body.put("idToken", response.idToken()); + body.put("tokenType", response.tokenType()); + body.put("expiresIn", response.expiresIn()); + body.put("username", response.username()); + body.put("roles", response.roles()); + return body; + } + + private ResponseEntity> unauthorized(String message) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of( + "success", false, + "message", message + )); + } + public record LoginRequest(String username, String password) { } + + public record RefreshRequest(String refreshToken) { + } } \ No newline at end of file diff --git a/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/DashboardController.java b/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/DashboardController.java index 14d1ae8..33b6eb9 100644 --- a/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/DashboardController.java +++ b/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/DashboardController.java @@ -1,7 +1,7 @@ package mx.gob.slp.baseadminweb.web.controller; -import mx.gob.slp.baseadminweb.domain.model.AppUser; -import org.springframework.security.core.annotation.AuthenticationPrincipal; +import mx.gob.slp.baseadminweb.web.support.WebUserView; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -15,8 +15,8 @@ public class DashboardController { } @GetMapping("/dashboard") - public String dashboard(@AuthenticationPrincipal AppUser user, Model model) { - model.addAttribute("user", user); + public String dashboard(Authentication authentication, Model model) { + model.addAttribute("user", WebUserView.from(authentication)); return "dashboard"; } @@ -24,4 +24,4 @@ public class DashboardController { public String adminHome() { return "redirect:/dashboard"; } -} +} \ No newline at end of file diff --git a/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/PassCodeController.java b/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/PassCodeController.java index b277e98..48f3106 100644 --- a/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/PassCodeController.java +++ b/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/controller/PassCodeController.java @@ -1,9 +1,9 @@ package mx.gob.slp.baseadminweb.web.controller; import mx.gob.slp.baseadminweb.application.service.PassCodeService; -import mx.gob.slp.baseadminweb.domain.model.AppUser; import mx.gob.slp.baseadminweb.web.dto.PassCodeListItem; -import org.springframework.security.core.annotation.AuthenticationPrincipal; +import mx.gob.slp.baseadminweb.web.support.WebUserView; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -26,8 +26,8 @@ public class PassCodeController { } @GetMapping - public String list(@AuthenticationPrincipal AppUser user, Model model) { - model.addAttribute("user", user); + public String list(Authentication authentication, Model model) { + model.addAttribute("user", WebUserView.from(authentication)); model.addAttribute("passCodes", passCodeService.findAll()); return "pass-codes"; } diff --git a/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/support/WebUserView.java b/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/support/WebUserView.java new file mode 100644 index 0000000..9fc8ac2 --- /dev/null +++ b/base-admin-web-web/src/main/java/mx/gob/slp/baseadminweb/web/support/WebUserView.java @@ -0,0 +1,25 @@ +package mx.gob.slp.baseadminweb.web.support; + +import mx.gob.slp.baseadminweb.configuration.security.AuthenticatedWebUser; +import mx.gob.slp.baseadminweb.domain.model.AppUser; +import org.springframework.security.core.Authentication; + +public record WebUserView(String username, String displayName) { + + public static WebUserView from(Authentication authentication) { + if (authentication == null) { + return new WebUserView("usuario", "Usuario"); + } + + Object principal = authentication.getPrincipal(); + if (principal instanceof AppUser appUser) { + return new WebUserView(appUser.getUsername(), appUser.getDisplayName()); + } + if (principal instanceof AuthenticatedWebUser webUser) { + return new WebUserView(webUser.username(), webUser.displayName()); + } + + String name = authentication.getName(); + return new WebUserView(name, name); + } +} \ No newline at end of file diff --git a/base-admin-web-web/src/main/resources/templates/login.html b/base-admin-web-web/src/main/resources/templates/login.html index e28d85a..23a2e8b 100644 --- a/base-admin-web-web/src/main/resources/templates/login.html +++ b/base-admin-web-web/src/main/resources/templates/login.html @@ -45,9 +45,12 @@ - Usuario demo: admin + Usuario: admin Password: Admin123* + + Autenticacion via Cognito local (grupo admins) + diff --git a/docker-compose-postgres.local.yml b/docker-compose-postgres.local.yml index 0649662..7436ce2 100644 --- a/docker-compose-postgres.local.yml +++ b/docker-compose-postgres.local.yml @@ -1,15 +1,3 @@ -services: - postgres: - image: postgres:16 - container_name: base-admin-web-postgres - environment: - POSTGRES_DB: base_admin_web - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - ports: - - "5432:5432" - volumes: - - base_admin_web_postgres_data:/var/lib/postgresql/data - -volumes: - base_admin_web_postgres_data: +# Alias de compatibilidad. Usa docker-compose.local.yml como archivo principal. +include: + - path: docker-compose.local.yml \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..fcecfba --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,45 @@ +services: + postgres: + image: postgres:16 + container_name: base-admin-web-postgres + environment: + POSTGRES_DB: base_admin_web + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - base_admin_web_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d base_admin_web"] + interval: 5s + timeout: 5s + retries: 10 + + floci: + image: floci/floci:latest-compat + container_name: base-admin-web-floci + ports: + - "4566:4566" + volumes: + - base_admin_web_floci_data:/app/data + - ./docker/floci/init/ready.d:/etc/floci/init/ready.d:ro + - ./docker/floci/generated:/generated + environment: + FLOCI_HOSTNAME: floci + FLOCI_STORAGE_MODE: persistent + FLOCI_STORAGE_PERSISTENT_PATH: /app/data + FLOCI_DEFAULT_REGION: us-east-1 + FLOCI_DEFAULT_ACCOUNT_ID: "000000000000" + FLOCI_INIT_HOOKS_TIMEOUT_SECONDS: "120" + COGNITO_USER_POOL_ID: us-east-1_BaseAdminWeb + COGNITO_ADMIN_USERNAME: admin + COGNITO_ADMIN_PASSWORD: Admin123* + COGNITO_SCANNER_USERNAME: scanner + COGNITO_SCANNER_PASSWORD: Scanner123* + COGNITO_ENV_OUTPUT_DIR: /generated + restart: unless-stopped + +volumes: + base_admin_web_postgres_data: + base_admin_web_floci_data: \ No newline at end of file diff --git a/docker/floci/cognito.local.env.example b/docker/floci/cognito.local.env.example new file mode 100644 index 0000000..c692cc8 --- /dev/null +++ b/docker/floci/cognito.local.env.example @@ -0,0 +1,15 @@ +# Copia generada automaticamente en ./docker/floci/generated/cognito.local.env +# despues del primer arranque de Floci. + +COGNITO_USER_POOL_ID=us-east-1_BaseAdminWeb +COGNITO_REGION=us-east-1 +COGNITO_ISSUER_URI=http://localhost:4566/us-east-1_BaseAdminWeb +COGNITO_JWK_SET_URI=http://localhost:4566/us-east-1_BaseAdminWeb/.well-known/jwks.json +COGNITO_ADMIN_CLIENT_ID=REEMPLAZAR_CON_VALOR_GENERADO +COGNITO_SCANNER_CLIENT_ID=REEMPLAZAR_CON_VALOR_GENERADO +COGNITO_ADMIN_GROUP=admins +COGNITO_SCANNER_GROUP=scanners +AWS_ENDPOINT_URL=http://localhost:4566 +AWS_DEFAULT_REGION=us-east-1 +AWS_ACCESS_KEY_ID=test +AWS_SECRET_ACCESS_KEY=test \ No newline at end of file diff --git a/docker/floci/generated/.gitkeep b/docker/floci/generated/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker/floci/init/ready.d/10-cognito-bootstrap.sh b/docker/floci/init/ready.d/10-cognito-bootstrap.sh new file mode 100755 index 0000000..b78374c --- /dev/null +++ b/docker/floci/init/ready.d/10-cognito-bootstrap.sh @@ -0,0 +1,172 @@ +#!/bin/sh +set -eu + +POOL_ID="${COGNITO_USER_POOL_ID:-us-east-1_BaseAdminWeb}" +POOL_NAME="${COGNITO_USER_POOL_NAME:-base-admin-web}" +REGION="${AWS_DEFAULT_REGION:-us-east-1}" +DATA_DIR="${FLOCI_STORAGE_PERSISTENT_PATH:-/app/data}" +ENV_OUTPUT_DIR="${COGNITO_ENV_OUTPUT_DIR:-/generated}" +ENV_FILE="${ENV_OUTPUT_DIR}/cognito.local.env" + +ADMIN_USERNAME="${COGNITO_ADMIN_USERNAME:-admin}" +ADMIN_PASSWORD="${COGNITO_ADMIN_PASSWORD:-Admin123*}" +ADMIN_NAME="${COGNITO_ADMIN_DISPLAY_NAME:-Administrador Base}" + +SCANNER_USERNAME="${COGNITO_SCANNER_USERNAME:-scanner}" +SCANNER_PASSWORD="${COGNITO_SCANNER_PASSWORD:-Scanner123*}" +SCANNER_NAME="${COGNITO_SCANNER_DISPLAY_NAME:-Escaneador}" + +ADMIN_CLIENT_NAME="${COGNITO_ADMIN_CLIENT_NAME:-base-admin-web-admin}" +SCANNER_CLIENT_NAME="${COGNITO_SCANNER_CLIENT_NAME:-base-admin-web-scanner}" +ADMIN_GROUP="${COGNITO_ADMIN_GROUP:-admins}" +SCANNER_GROUP="${COGNITO_SCANNER_GROUP:-scanners}" + +mkdir -p "$DATA_DIR" "$ENV_OUTPUT_DIR" + +log() { + printf '[cognito-bootstrap] %s\n' "$1" >&2 +} + +pool_exists() { + aws cognito-idp describe-user-pool --user-pool-id "$POOL_ID" >/dev/null 2>&1 +} + +create_pool() { + log "Creando user pool $POOL_ID" + aws cognito-idp create-user-pool \ + --pool-name "$POOL_NAME" \ + --user-pool-tags "floci:override-id=${POOL_ID}" \ + --policies "PasswordPolicy={MinimumLength=8,RequireUppercase=true,RequireLowercase=true,RequireNumbers=true,RequireSymbols=true}" \ + --username-configuration "CaseSensitive=false" \ + --mfa-configuration OFF \ + --account-recovery-setting "RecoveryMechanisms=[{Priority=1,Name=verified_email}]" +} + +find_client_id() { + client_name="$1" + aws cognito-idp list-user-pool-clients \ + --user-pool-id "$POOL_ID" \ + --max-results 60 \ + --query "UserPoolClients[?ClientName=='${client_name}'].ClientId | [0]" \ + --output text +} + +create_client() { + client_name="$1" + existing_id="$(find_client_id "$client_name" || true)" + if [ -n "$existing_id" ] && [ "$existing_id" != "None" ]; then + log "App client $client_name ya existe ($existing_id)" + printf '%s' "$existing_id" + return 0 + fi + + log "Creando app client $client_name" + aws cognito-idp create-user-pool-client \ + --user-pool-id "$POOL_ID" \ + --client-name "$client_name" \ + --explicit-auth-flows \ + ALLOW_USER_PASSWORD_AUTH \ + ALLOW_REFRESH_TOKEN_AUTH \ + ALLOW_USER_SRP_AUTH \ + --prevent-user-existence-errors ENABLED \ + --supported-identity-providers COGNITO \ + --query "UserPoolClient.ClientId" \ + --output text +} + +create_group_if_needed() { + group_name="$1" + description="$2" + if aws cognito-idp get-group --user-pool-id "$POOL_ID" --group-name "$group_name" >/dev/null 2>&1; then + log "Grupo $group_name ya existe" + return 0 + fi + + log "Creando grupo $group_name" + aws cognito-idp create-group \ + --user-pool-id "$POOL_ID" \ + --group-name "$group_name" \ + --description "$description" +} + +user_exists() { + username="$1" + aws cognito-idp admin-get-user \ + --user-pool-id "$POOL_ID" \ + --username "$username" >/dev/null 2>&1 +} + +create_user_if_needed() { + username="$1" + password="$2" + display_name="$3" + group_name="$4" + + if ! user_exists "$username"; then + log "Creando usuario $username" + aws cognito-idp admin-create-user \ + --user-pool-id "$POOL_ID" \ + --username "$username" \ + --message-action SUPPRESS \ + --user-attributes "Name=name,Value=${display_name}" + else + log "Usuario $username ya existe" + fi + + aws cognito-idp admin-set-user-password \ + --user-pool-id "$POOL_ID" \ + --username "$username" \ + --password "$password" \ + --permanent + + aws cognito-idp admin-add-user-to-group \ + --user-pool-id "$POOL_ID" \ + --username "$username" \ + --group-name "$group_name" >/dev/null 2>&1 || true +} + +write_env_file() { + admin_client_id="$1" + scanner_client_id="$2" + + cat > "$ENV_FILE" < - - - diff --git a/flutter-app/qrscanner/android/app/src/main/AndroidManifest.xml b/flutter-app/qrscanner/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 2fbd078..0000000 --- a/flutter-app/qrscanner/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutter-app/qrscanner/android/app/src/main/kotlin/com/example/qrscanner/MainActivity.kt b/flutter-app/qrscanner/android/app/src/main/kotlin/com/example/qrscanner/MainActivity.kt deleted file mode 100644 index ca0660c..0000000 --- a/flutter-app/qrscanner/android/app/src/main/kotlin/com/example/qrscanner/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.qrscanner - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/flutter-app/qrscanner/android/app/src/main/res/drawable-v21/launch_background.xml b/flutter-app/qrscanner/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f..0000000 --- a/flutter-app/qrscanner/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/flutter-app/qrscanner/android/app/src/main/res/drawable/launch_background.xml b/flutter-app/qrscanner/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/flutter-app/qrscanner/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/flutter-app/qrscanner/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter-app/qrscanner/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4..0000000 Binary files a/flutter-app/qrscanner/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/flutter-app/qrscanner/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter-app/qrscanner/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b7..0000000 Binary files a/flutter-app/qrscanner/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/flutter-app/qrscanner/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter-app/qrscanner/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391..0000000 Binary files a/flutter-app/qrscanner/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/flutter-app/qrscanner/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter-app/qrscanner/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d..0000000 Binary files a/flutter-app/qrscanner/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/flutter-app/qrscanner/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter-app/qrscanner/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372e..0000000 Binary files a/flutter-app/qrscanner/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/flutter-app/qrscanner/android/app/src/main/res/values-night/styles.xml b/flutter-app/qrscanner/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be..0000000 --- a/flutter-app/qrscanner/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/flutter-app/qrscanner/android/app/src/main/res/values/styles.xml b/flutter-app/qrscanner/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef88..0000000 --- a/flutter-app/qrscanner/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/flutter-app/qrscanner/android/app/src/profile/AndroidManifest.xml b/flutter-app/qrscanner/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f698..0000000 --- a/flutter-app/qrscanner/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/flutter-app/qrscanner/android/build.gradle.kts b/flutter-app/qrscanner/android/build.gradle.kts deleted file mode 100644 index dbee657..0000000 --- a/flutter-app/qrscanner/android/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -val newBuildDir: Directory = - rootProject.layout.buildDirectory - .dir("../../build") - .get() -rootProject.layout.buildDirectory.value(newBuildDir) - -subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) -} -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} diff --git a/flutter-app/qrscanner/android/gradle.properties b/flutter-app/qrscanner/android/gradle.properties deleted file mode 100644 index e96108c..0000000 --- a/flutter-app/qrscanner/android/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true -# This newDsl flag was added by the Flutter template -android.newDsl=false -# This builtInKotlin flag was added by the Flutter template -android.builtInKotlin=false diff --git a/flutter-app/qrscanner/android/gradle/wrapper/gradle-wrapper.properties b/flutter-app/qrscanner/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 2d428bf..0000000 --- a/flutter-app/qrscanner/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/flutter-app/qrscanner/android/settings.gradle.kts b/flutter-app/qrscanner/android/settings.gradle.kts deleted file mode 100644 index c21f0c5..0000000 --- a/flutter-app/qrscanner/android/settings.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "9.0.1" apply false - id("org.jetbrains.kotlin.android") version "2.3.20" apply false -} - -include(":app") diff --git a/flutter-app/qrscanner/assets/sounds/scan.wav b/flutter-app/qrscanner/assets/sounds/scan.wav deleted file mode 100644 index 298f989..0000000 Binary files a/flutter-app/qrscanner/assets/sounds/scan.wav and /dev/null differ diff --git a/flutter-app/qrscanner/assets/sounds/success.wav b/flutter-app/qrscanner/assets/sounds/success.wav deleted file mode 100644 index f30e8f1..0000000 Binary files a/flutter-app/qrscanner/assets/sounds/success.wav and /dev/null differ diff --git a/flutter-app/qrscanner/assets/sounds/warning.wav b/flutter-app/qrscanner/assets/sounds/warning.wav deleted file mode 100644 index c4c3550..0000000 Binary files a/flutter-app/qrscanner/assets/sounds/warning.wav and /dev/null differ diff --git a/flutter-app/qrscanner/ios/.gitignore b/flutter-app/qrscanner/ios/.gitignore deleted file mode 100644 index 7a7f987..0000000 --- a/flutter-app/qrscanner/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/flutter-app/qrscanner/ios/Flutter/AppFrameworkInfo.plist b/flutter-app/qrscanner/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 391a902..0000000 --- a/flutter-app/qrscanner/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - - diff --git a/flutter-app/qrscanner/ios/Flutter/Debug.xcconfig b/flutter-app/qrscanner/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/flutter-app/qrscanner/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/flutter-app/qrscanner/ios/Flutter/Release.xcconfig b/flutter-app/qrscanner/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/flutter-app/qrscanner/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/flutter-app/qrscanner/ios/Runner.xcodeproj/project.pbxproj b/flutter-app/qrscanner/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index d04de4b..0000000 --- a/flutter-app/qrscanner/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,644 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; - }; -/* End XCLocalSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { - isa = XCSwiftPackageProductDependency; - productName = FlutterGeneratedPluginSwiftPackage; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/flutter-app/qrscanner/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/flutter-app/qrscanner/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter-app/qrscanner/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index c3fedb2..0000000 --- a/flutter-app/qrscanner/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutter-app/qrscanner/ios/Runner.xcworkspace/contents.xcworkspacedata b/flutter-app/qrscanner/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/flutter-app/qrscanner/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/flutter-app/qrscanner/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter-app/qrscanner/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/flutter-app/qrscanner/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/flutter-app/qrscanner/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/flutter-app/qrscanner/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/flutter-app/qrscanner/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/flutter-app/qrscanner/ios/Runner/AppDelegate.swift b/flutter-app/qrscanner/ios/Runner/AppDelegate.swift deleted file mode 100644 index c30b367..0000000 --- a/flutter-app/qrscanner/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { - GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) - } -} diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d93..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b00..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe73094..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773c..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec3034..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec3034..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32a..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf1..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/flutter-app/qrscanner/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/flutter-app/qrscanner/ios/Runner/Base.lproj/LaunchScreen.storyboard b/flutter-app/qrscanner/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/flutter-app/qrscanner/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutter-app/qrscanner/ios/Runner/Base.lproj/Main.storyboard b/flutter-app/qrscanner/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/flutter-app/qrscanner/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutter-app/qrscanner/ios/Runner/Info.plist b/flutter-app/qrscanner/ios/Runner/Info.plist deleted file mode 100644 index 1b77c6e..0000000 --- a/flutter-app/qrscanner/ios/Runner/Info.plist +++ /dev/null @@ -1,72 +0,0 @@ - - - - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Qrscanner - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - qrscanner - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - NSCameraUsageDescription - La app necesita la cámara para escanear códigos QR de pase. - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneConfigurationName - flutter - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/flutter-app/qrscanner/ios/Runner/Runner-Bridging-Header.h b/flutter-app/qrscanner/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a5..0000000 --- a/flutter-app/qrscanner/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/flutter-app/qrscanner/ios/Runner/SceneDelegate.swift b/flutter-app/qrscanner/ios/Runner/SceneDelegate.swift deleted file mode 100644 index b9ce8ea..0000000 --- a/flutter-app/qrscanner/ios/Runner/SceneDelegate.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Flutter -import UIKit - -class SceneDelegate: FlutterSceneDelegate { - -} diff --git a/flutter-app/qrscanner/ios/RunnerTests/RunnerTests.swift b/flutter-app/qrscanner/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b..0000000 --- a/flutter-app/qrscanner/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/flutter-app/qrscanner/lib/config/api_config.dart b/flutter-app/qrscanner/lib/config/api_config.dart deleted file mode 100644 index ddb0b71..0000000 --- a/flutter-app/qrscanner/lib/config/api_config.dart +++ /dev/null @@ -1,16 +0,0 @@ -/// URL base del API de base-admin-web. -/// -/// Configúrala al ejecutar o compilar si necesitas otra IP: -/// `flutter run --dart-define=API_BASE_URL=http://otra-ip:8080` -class ApiConfig { - ApiConfig._(); - - static const String baseUrl = String.fromEnvironment( - 'API_BASE_URL', - defaultValue: 'http://10.99.51.6:8080', - ); - - static String get loginUrl => '$baseUrl/api/auth/login'; - - static String get scanUrl => '$baseUrl/api/pass-codes/scan'; -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/main.dart b/flutter-app/qrscanner/lib/main.dart deleted file mode 100644 index 3261d47..0000000 --- a/flutter-app/qrscanner/lib/main.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:qrscanner/screens/login_screen.dart'; -import 'package:qrscanner/theme/app_theme.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'QR Scanner', - debugShowCheckedModeBanner: false, - theme: AppTheme.darkTheme, - home: const LoginScreen(), - ); - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/models/auth_session.dart b/flutter-app/qrscanner/lib/models/auth_session.dart deleted file mode 100644 index 1bddef2..0000000 --- a/flutter-app/qrscanner/lib/models/auth_session.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:qrscanner/models/login_response.dart'; - -class AuthSession { - const AuthSession({ - required this.username, - required this.accessToken, - required this.tokenType, - required this.expiresIn, - required this.roles, - }); - - factory AuthSession.fromLoginResponse(LoginResponse response) { - return AuthSession( - username: response.username ?? '', - accessToken: response.accessToken ?? '', - tokenType: response.tokenType ?? 'Bearer', - expiresIn: response.expiresIn ?? 0, - roles: response.roles, - ); - } - - factory AuthSession.fromJson(Map json) { - return AuthSession( - username: json['username'] as String? ?? '', - accessToken: json['accessToken'] as String? ?? '', - tokenType: json['tokenType'] as String? ?? 'Bearer', - expiresIn: (json['expiresIn'] as num?)?.toInt() ?? 0, - roles: (json['roles'] as List?) - ?.map((role) => role.toString()) - .toList() ?? - const [], - ); - } - - final String username; - final String accessToken; - final String tokenType; - final int expiresIn; - final List roles; - - String get authorizationHeader => '$tokenType $accessToken'; - - bool get isScanner => roles.contains('ROLE_SCANNER'); - - Map toJson() => { - 'username': username, - 'accessToken': accessToken, - 'tokenType': tokenType, - 'expiresIn': expiresIn, - 'roles': roles, - }; -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/models/login_response.dart b/flutter-app/qrscanner/lib/models/login_response.dart deleted file mode 100644 index 1b62970..0000000 --- a/flutter-app/qrscanner/lib/models/login_response.dart +++ /dev/null @@ -1,54 +0,0 @@ -class LoginResponse { - const LoginResponse({ - required this.success, - this.accessToken, - this.tokenType, - this.expiresIn, - this.username, - this.roles = const [], - this.message, - }); - - factory LoginResponse.fromJson(Map json) { - return LoginResponse( - success: json['success'] == true, - accessToken: json['accessToken'] as String?, - tokenType: json['tokenType'] as String?, - expiresIn: (json['expiresIn'] as num?)?.toInt(), - username: json['username'] as String?, - roles: (json['roles'] as List?) - ?.map((role) => role.toString()) - .toList() ?? - const [], - message: json['message'] as String?, - ); - } - - final bool success; - final String? accessToken; - final String? tokenType; - final int? expiresIn; - final String? username; - final List roles; - final String? message; - - LoginResponse copyWith({ - bool? success, - String? accessToken, - String? tokenType, - int? expiresIn, - String? username, - List? roles, - String? message, - }) { - return LoginResponse( - success: success ?? this.success, - accessToken: accessToken ?? this.accessToken, - tokenType: tokenType ?? this.tokenType, - expiresIn: expiresIn ?? this.expiresIn, - username: username ?? this.username, - roles: roles ?? this.roles, - message: message ?? this.message, - ); - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/models/pass_code.dart b/flutter-app/qrscanner/lib/models/pass_code.dart deleted file mode 100644 index 3a1f87b..0000000 --- a/flutter-app/qrscanner/lib/models/pass_code.dart +++ /dev/null @@ -1,31 +0,0 @@ -class PassCode { - const PassCode({ - required this.id, - required this.name, - required this.code, - required this.status, - required this.scanned, - required this.createdAt, - this.scannedAt, - }); - - factory PassCode.fromJson(Map json) { - return PassCode( - id: (json['id'] as num).toInt(), - name: json['name'] as String? ?? '', - code: json['code'] as String? ?? '', - status: json['status'] as String? ?? '', - scanned: json['scanned'] == true, - createdAt: json['createdAt'] as String? ?? '', - scannedAt: json['scannedAt'] as String?, - ); - } - - final int id; - final String name; - final String code; - final String status; - final bool scanned; - final String createdAt; - final String? scannedAt; -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/models/scan_response.dart b/flutter-app/qrscanner/lib/models/scan_response.dart deleted file mode 100644 index 392e34b..0000000 --- a/flutter-app/qrscanner/lib/models/scan_response.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:qrscanner/models/pass_code.dart'; - -class ScanResponse { - const ScanResponse({ - required this.success, - this.message, - this.passCode, - }); - - factory ScanResponse.fromJson(Map json) { - return ScanResponse( - success: json['success'] == true, - message: json['message'] as String?, - passCode: json['passCode'] is Map - ? PassCode.fromJson(json['passCode'] as Map) - : null, - ); - } - - final bool success; - final String? message; - final PassCode? passCode; -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/screens/home_screen.dart b/flutter-app/qrscanner/lib/screens/home_screen.dart deleted file mode 100644 index 6555771..0000000 --- a/flutter-app/qrscanner/lib/screens/home_screen.dart +++ /dev/null @@ -1,311 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:qrscanner/models/auth_session.dart'; -import 'package:qrscanner/screens/login_screen.dart'; -import 'package:qrscanner/screens/scanner_screen.dart'; -import 'package:qrscanner/services/auth_service.dart'; -import 'package:qrscanner/theme/app_theme.dart'; - -class HomeScreen extends StatelessWidget { - const HomeScreen({ - super.key, - required this.session, - }); - - final AuthSession session; - - Future _logout(BuildContext context) async { - await AuthService().clearSession(); - - if (!context.mounted) return; - - Navigator.of(context).pushReplacement( - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - const LoginScreen(), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition(opacity: animation, child: child); - }, - transitionDuration: const Duration(milliseconds: 400), - ), - ); - } - - void _openScanner(BuildContext context) { - Navigator.of(context).push( - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - ScannerScreen(session: session), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.06), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - )), - child: child, - ), - ); - }, - transitionDuration: const Duration(milliseconds: 380), - ), - ); - } - - @override - Widget build(BuildContext context) { - final expiresHours = (session.expiresIn / 3600).round(); - - return Scaffold( - body: Stack( - children: [ - Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.backgroundDark, - Color(0xFF0D1B2A), - Color(0xFF12182B), - ], - ), - ), - ), - Positioned( - top: -60, - right: -40, - child: Container( - width: 220, - height: 220, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - AppTheme.primary.withValues(alpha: 0.12), - Colors.transparent, - ], - ), - ), - ), - ), - SafeArea( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Row( - children: [ - const Spacer(), - IconButton( - icon: const Icon(Icons.logout_rounded), - tooltip: 'Cerrar sesión', - onPressed: () => _logout(context), - ), - ], - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 110, - height: 110, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(28), - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [AppTheme.primary, AppTheme.primaryDark], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.primary.withValues(alpha: 0.35), - blurRadius: 28, - offset: const Offset(0, 10), - ), - ], - ), - child: const Icon( - Icons.qr_code_scanner_rounded, - size: 52, - color: Colors.white, - ), - ), - const SizedBox(height: 28), - Text( - 'Hola, ${session.username}', - style: - Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - Text( - 'Listo para validar pases', - style: - Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Colors.white.withValues(alpha: 0.55), - ), - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.06), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.white.withValues(alpha: 0.08), - ), - ), - child: Text( - 'Sesión activa · $expiresHours h', - style: TextStyle( - color: Colors.white.withValues(alpha: 0.45), - fontSize: 13, - ), - ), - ), - const SizedBox(height: 48), - _ScanButton(onPressed: () => _openScanner(context)), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ); - } -} - -class _ScanButton extends StatefulWidget { - const _ScanButton({required this.onPressed}); - - final VoidCallback onPressed; - - @override - State<_ScanButton> createState() => _ScanButtonState(); -} - -class _ScanButtonState extends State<_ScanButton> - with SingleTickerProviderStateMixin { - late final AnimationController _controller; - late final Animation _glow; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1800), - )..repeat(reverse: true); - _glow = Tween(begin: 0.18, end: 0.38).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOut), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _glow, - builder: (context, child) { - return Container( - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: AppTheme.primary.withValues(alpha: _glow.value), - blurRadius: 28, - offset: const Offset(0, 10), - ), - ], - ), - child: child, - ); - }, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: widget.onPressed, - borderRadius: BorderRadius.circular(20), - child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [AppTheme.primary, AppTheme.primaryDark], - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.18), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.qr_code_scanner_rounded, - color: Colors.white, - size: 26, - ), - ), - const SizedBox(width: 16), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Abrir escáner', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w700, - ), - ), - Text( - 'Validar código de pase', - style: TextStyle( - color: Color(0xCCFFFFFF), - fontSize: 13, - ), - ), - ], - ), - const Spacer(), - const Icon( - Icons.arrow_forward_rounded, - color: Colors.white, - ), - ], - ), - ), - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/screens/login_screen.dart b/flutter-app/qrscanner/lib/screens/login_screen.dart deleted file mode 100644 index 57353b1..0000000 --- a/flutter-app/qrscanner/lib/screens/login_screen.dart +++ /dev/null @@ -1,440 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:qrscanner/config/api_config.dart'; -import 'package:qrscanner/models/auth_session.dart'; -import 'package:qrscanner/screens/home_screen.dart'; -import 'package:qrscanner/services/auth_service.dart'; -import 'package:qrscanner/theme/app_theme.dart'; - -class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); - - @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State - with SingleTickerProviderStateMixin { - final _formKey = GlobalKey(); - final _usernameController = TextEditingController(); - final _passwordController = TextEditingController(); - final _authService = AuthService(); - - late final AnimationController _animationController; - late final Animation _fadeAnimation; - late final Animation _slideAnimation; - - bool _obscurePassword = true; - bool _isLoading = false; - bool _rememberMe = false; - String? _errorMessage; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 900), - ); - _fadeAnimation = CurvedAnimation( - parent: _animationController, - curve: Curves.easeOut, - ); - _slideAnimation = Tween( - begin: const Offset(0, 0.08), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutCubic, - )); - _animationController.forward(); - _checkStoredSession(); - } - - Future _checkStoredSession() async { - final session = await _authService.loadStoredSession(); - if (!mounted || session == null) return; - - _goToHome(session); - } - - @override - void dispose() { - _animationController.dispose(); - _usernameController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - Future _handleLogin() async { - if (!_formKey.currentState!.validate()) return; - - setState(() { - _isLoading = true; - _errorMessage = null; - }); - - try { - final response = await _authService.login( - username: _usernameController.text.trim(), - password: _passwordController.text, - rememberMe: _rememberMe, - ); - - if (!mounted) return; - - if (response.success && response.accessToken != null) { - _goToHome( - AuthSession.fromLoginResponse( - response.copyWith( - username: response.username ?? _usernameController.text.trim(), - ), - ), - ); - return; - } - - setState(() { - _errorMessage = response.message ?? 'Credenciales incorrectas'; - }); - } catch (_) { - if (!mounted) return; - setState(() { - _errorMessage = - 'No se pudo conectar al servidor. Revisa API_BASE_URL (${ApiConfig.baseUrl})'; - }); - } finally { - if (mounted) { - setState(() => _isLoading = false); - } - } - } - - void _goToHome(AuthSession session) { - Navigator.of(context).pushReplacement( - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - HomeScreen(session: session), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition(opacity: animation, child: child); - }, - transitionDuration: const Duration(milliseconds: 400), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - _buildBackground(), - SafeArea( - child: FadeTransition( - opacity: _fadeAnimation, - child: SlideTransition( - position: _slideAnimation, - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: Column( - children: [ - _buildHeader(), - const SizedBox(height: 40), - _buildLoginCard(), - const SizedBox(height: 24), - _buildFooter(), - ], - ), - ), - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildBackground() { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.backgroundDark, - Color(0xFF0D1B2A), - Color(0xFF1A1A3E), - ], - ), - ), - child: Stack( - children: [ - Positioned( - top: -80, - right: -60, - child: Container( - width: 260, - height: 260, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - AppTheme.primary.withValues(alpha: 0.15), - Colors.transparent, - ], - ), - ), - ), - ), - Positioned( - bottom: -100, - left: -80, - child: Container( - width: 300, - height: 300, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - AppTheme.accent.withValues(alpha: 0.12), - Colors.transparent, - ], - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildHeader() { - return Column( - children: [ - Container( - width: 88, - height: 88, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(22), - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [AppTheme.primary, AppTheme.primaryDark], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.primary.withValues(alpha: 0.35), - blurRadius: 24, - offset: const Offset(0, 8), - ), - ], - ), - child: const Icon( - Icons.qr_code_scanner_rounded, - size: 44, - color: Colors.white, - ), - ), - const SizedBox(height: 20), - Text( - 'QR Scanner', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - letterSpacing: -0.5, - ), - ), - const SizedBox(height: 8), - Text( - 'Inicia sesión para continuar', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white.withValues(alpha: 0.55), - ), - ), - ], - ); - } - - Widget _buildLoginCard() { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(28), - decoration: BoxDecoration( - color: AppTheme.surfaceDark.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.white.withValues(alpha: 0.08)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 32, - offset: const Offset(0, 16), - ), - ], - ), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (_errorMessage != null) ...[ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.red.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.red.withValues(alpha: 0.3)), - ), - child: Text( - _errorMessage!, - style: const TextStyle(color: Color(0xFFFF8A80), fontSize: 13), - ), - ), - const SizedBox(height: 16), - ], - TextFormField( - controller: _usernameController, - keyboardType: TextInputType.text, - textInputAction: TextInputAction.next, - autocorrect: false, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: 'Usuario', - prefixIcon: Icon( - Icons.person_outline_rounded, - color: Colors.white.withValues(alpha: 0.5), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Ingresa tu usuario'; - } - return null; - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - textInputAction: TextInputAction.done, - onFieldSubmitted: (_) => _handleLogin(), - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: 'Contraseña', - prefixIcon: Icon( - Icons.lock_outline_rounded, - color: Colors.white.withValues(alpha: 0.5), - ), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility_off_outlined - : Icons.visibility_outlined, - color: Colors.white.withValues(alpha: 0.5), - ), - onPressed: () { - setState(() => _obscurePassword = !_obscurePassword); - }, - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Ingresa tu contraseña'; - } - return null; - }, - ), - const SizedBox(height: 12), - Row( - children: [ - SizedBox( - height: 36, - child: Checkbox( - value: _rememberMe, - activeColor: AppTheme.primary, - side: BorderSide( - color: Colors.white.withValues(alpha: 0.3), - ), - onChanged: (value) { - setState(() => _rememberMe = value ?? false); - }, - ), - ), - Text( - 'Recordarme', - style: TextStyle( - color: Colors.white.withValues(alpha: 0.7), - fontSize: 14, - ), - ), - ], - ), - const SizedBox(height: 8), - _buildLoginButton(), - const SizedBox(height: 16), - Text( - 'API: ${ApiConfig.baseUrl}', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.35), - fontSize: 11, - ), - ), - ], - ), - ), - ); - } - - Widget _buildLoginButton() { - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - gradient: const LinearGradient( - colors: [AppTheme.primary, AppTheme.primaryDark], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.primary.withValues(alpha: 0.3), - blurRadius: 16, - offset: const Offset(0, 6), - ), - ], - ), - child: ElevatedButton( - onPressed: _isLoading ? null : _handleLogin, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - disabledBackgroundColor: Colors.transparent, - ), - child: _isLoading - ? const SizedBox( - height: 22, - width: 22, - child: CircularProgressIndicator( - strokeWidth: 2.5, - color: Colors.white, - ), - ) - : const Text( - 'Iniciar sesión', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ); - } - - Widget _buildFooter() { - return Text( - 'POST /api/auth/login', - style: TextStyle( - color: Colors.white.withValues(alpha: 0.35), - fontSize: 12, - ), - ); - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/screens/scanner_screen.dart b/flutter-app/qrscanner/lib/screens/scanner_screen.dart deleted file mode 100644 index e7aa8db..0000000 --- a/flutter-app/qrscanner/lib/screens/scanner_screen.dart +++ /dev/null @@ -1,590 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:qrscanner/models/auth_session.dart'; -import 'package:qrscanner/models/pass_code.dart'; -import 'package:qrscanner/models/scan_response.dart'; -import 'package:qrscanner/services/feedback_service.dart'; -import 'package:qrscanner/services/pass_code_service.dart'; -import 'package:qrscanner/theme/app_theme.dart'; - -enum _ScanPhase { scanning, processing, success, warning } - -const double _scanWindowSize = 260; -const double _scanWindowRadius = 28; -const Alignment _scanWindowAlignment = Alignment(0, -0.12); - -class ScannerScreen extends StatefulWidget { - const ScannerScreen({ - super.key, - required this.session, - }); - - final AuthSession session; - - @override - State createState() => _ScannerScreenState(); -} - -class _ScannerScreenState extends State - with TickerProviderStateMixin { - final _scannerController = MobileScannerController( - detectionSpeed: DetectionSpeed.normal, - detectionTimeoutMs: 500, - facing: CameraFacing.back, - formats: const [BarcodeFormat.qrCode], - autoZoom: true, - ); - final _passCodeService = PassCodeService(); - final _feedbackService = FeedbackService(); - - _ScanPhase _phase = _ScanPhase.scanning; - bool _isHandlingDetection = false; - String? _lastScannedCode; - String? _message; - PassCode? _passCode; - - late final AnimationController _scanLineController; - late final AnimationController _resultController; - late final Animation _scanLineAnimation; - late final Animation _scaleAnimation; - late final Animation _arrowSlide; - - @override - void initState() { - super.initState(); - _scanLineController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 2400), - )..repeat(); - _scanLineAnimation = CurvedAnimation( - parent: _scanLineController, - curve: Curves.easeInOut, - ); - - _resultController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 700), - ); - _scaleAnimation = CurvedAnimation( - parent: _resultController, - curve: Curves.elasticOut, - ); - _arrowSlide = Tween( - begin: const Offset(0, -0.35), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _resultController, - curve: Curves.easeOutBack, - )); - } - - Rect _scanWindowRect(Size size) { - final center = _scanWindowAlignment.alongSize(size); - return Rect.fromCenter( - center: center, - width: _scanWindowSize, - height: _scanWindowSize, - ); - } - - @override - void dispose() { - _scanLineController.dispose(); - _resultController.dispose(); - _scannerController.dispose(); - _feedbackService.dispose(); - super.dispose(); - } - - Future _onDetect(BarcodeCapture capture) async { - if (_phase != _ScanPhase.scanning || _isHandlingDetection) return; - - final rawValue = capture.barcodes.firstOrNull?.rawValue?.trim(); - if (rawValue == null || rawValue.isEmpty) return; - - final code = _extractCode(rawValue); - if (code.isEmpty || code == _lastScannedCode) return; - - _isHandlingDetection = true; - setState(() { - _phase = _ScanPhase.processing; - _lastScannedCode = code; - _message = null; - _passCode = null; - }); - - _scanLineController.stop(); - await _scannerController.stop(); - await _feedbackService.onCodeDetected(); - - try { - final response = await _passCodeService.scan( - session: widget.session, - code: code, - ); - - if (!mounted) return; - await _showResult(response); - } catch (_) { - if (!mounted) return; - await _showResult( - const ScanResponse( - success: false, - message: 'No se pudo validar el código. Revisa la conexión.', - ), - ); - } finally { - _isHandlingDetection = false; - } - } - - String _extractCode(String rawValue) { - final uri = Uri.tryParse(rawValue); - if (uri != null) { - final queryCode = uri.queryParameters['code']; - if (queryCode != null && queryCode.isNotEmpty) return queryCode; - - final segments = uri.pathSegments.where((s) => s.isNotEmpty).toList(); - if (segments.isNotEmpty) return segments.last; - } - return rawValue; - } - - Future _showResult(ScanResponse response) async { - _resultController.reset(); - - setState(() { - _message = response.message; - _passCode = response.passCode; - _phase = response.success ? _ScanPhase.success : _ScanPhase.warning; - }); - - if (response.success) { - await _feedbackService.onSuccess(); - } else { - await _feedbackService.onWarning(); - } - - await _resultController.forward(); - } - - Future _scanAgain() async { - _resultController.reset(); - _isHandlingDetection = false; - setState(() { - _phase = _ScanPhase.scanning; - _lastScannedCode = null; - _message = null; - _passCode = null; - }); - _scanLineController.repeat(); - await _scannerController.start(); - } - - @override - Widget build(BuildContext context) { - final isSuccess = _phase == _ScanPhase.success; - final isWarning = _phase == _ScanPhase.warning; - final overlayColor = isSuccess - ? AppTheme.success.withValues(alpha: 0.82) - : isWarning - ? AppTheme.warning.withValues(alpha: 0.82) - : Colors.transparent; - - return Scaffold( - backgroundColor: Colors.black, - extendBodyBehindAppBar: true, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back_rounded), - onPressed: () => Navigator.of(context).pop(), - ), - title: const Text('Escanear pase'), - centerTitle: true, - ), - body: Stack( - fit: StackFit.expand, - children: [ - if (_phase == _ScanPhase.scanning || _phase == _ScanPhase.processing) - LayoutBuilder( - builder: (context, constraints) { - final scanRect = _scanWindowRect(constraints.biggest); - return MobileScanner( - controller: _scannerController, - onDetect: _onDetect, - scanWindow: scanRect, - overlayBuilder: (context, constraints) { - if (_phase != _ScanPhase.scanning) { - return const SizedBox.shrink(); - } - - return AnimatedBuilder( - animation: _scanLineAnimation, - builder: (context, child) { - return CustomPaint( - size: constraints.biggest, - painter: _ScannerOverlayPainter( - scanRect: scanRect, - scanLineProgress: _scanLineAnimation.value, - ), - ); - }, - ); - }, - ); - }, - ), - if (_phase != _ScanPhase.scanning && _phase != _ScanPhase.processing) - Container(color: AppTheme.backgroundDark), - AnimatedContainer( - duration: const Duration(milliseconds: 450), - curve: Curves.easeOutCubic, - color: overlayColor, - ), - if (_phase == _ScanPhase.processing) - SafeArea( - child: Center(child: _buildProcessingCard()), - ), - if (_phase == _ScanPhase.success || _phase == _ScanPhase.warning) - SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 16), - child: _buildResultCard(), - ), - ), - ), - if (_phase == _ScanPhase.scanning) - SafeArea( - child: Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.fromLTRB(32, 0, 32, 28), - child: _buildHint(), - ), - ), - ), - ], - ), - ); - } - - Widget _buildProcessingCard() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 28), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), - decoration: BoxDecoration( - color: AppTheme.surfaceDark.withValues(alpha: 0.92), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.white.withValues(alpha: 0.08)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - width: 42, - height: 42, - child: CircularProgressIndicator( - strokeWidth: 3, - color: AppTheme.primary, - ), - ), - const SizedBox(height: 18), - Text( - 'Validando código...', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - if (_lastScannedCode != null) ...[ - const SizedBox(height: 8), - Text( - _lastScannedCode!, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.45), - letterSpacing: 1.2, - ), - ), - ], - ], - ), - ); - } - - Widget _buildResultCard() { - final isSuccess = _phase == _ScanPhase.success; - final accent = isSuccess ? AppTheme.success : AppTheme.warning; - final accentDark = isSuccess ? AppTheme.successDark : AppTheme.warningDark; - - return FadeTransition( - opacity: _scaleAnimation, - child: ScaleTransition( - scale: _scaleAnimation, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 24), - padding: const EdgeInsets.fromLTRB(24, 32, 24, 24), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - accent.withValues(alpha: 0.95), - accentDark.withValues(alpha: 0.95), - ], - ), - borderRadius: BorderRadius.circular(28), - boxShadow: [ - BoxShadow( - color: accent.withValues(alpha: 0.45), - blurRadius: 32, - offset: const Offset(0, 12), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SlideTransition( - position: _arrowSlide, - child: Container( - width: 88, - height: 88, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.18), - shape: BoxShape.circle, - ), - child: Icon( - isSuccess - ? Icons.arrow_downward_rounded - : Icons.warning_amber_rounded, - size: isSuccess ? 52 : 46, - color: Colors.white, - ), - ), - ), - const SizedBox(height: 20), - Text( - isSuccess ? '¡Pase válido!' : 'No se pudo validar', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - if (!isSuccess && (_message?.isNotEmpty ?? false)) ...[ - const SizedBox(height: 10), - Text( - _message!, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - height: 1.4, - color: Colors.white.withValues(alpha: 0.92), - ), - ), - ], - if (isSuccess && _passCode != null) ...[ - const SizedBox(height: 20), - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.18), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - _detailRow('Nombre', _passCode!.name), - const SizedBox(height: 8), - _detailRow('Código', _passCode!.code), - ], - ), - ), - ], - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: _scanAgain, - icon: const Icon(Icons.qr_code_scanner_rounded), - label: const Text('Escanear otro'), - style: FilledButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: accentDark, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _detailRow(String label, String value) { - return Row( - children: [ - Text( - '$label: ', - style: TextStyle( - color: Colors.white.withValues(alpha: 0.7), - fontSize: 14, - ), - ), - Expanded( - child: Text( - value, - textAlign: TextAlign.end, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - ), - ), - ], - ); - } - - Widget _buildHint() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Text( - 'Apunta la cámara al código QR del pase', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.65), - fontSize: 14, - ), - ), - ); - } -} - -class _ScannerOverlayPainter extends CustomPainter { - _ScannerOverlayPainter({ - required this.scanRect, - required this.scanLineProgress, - }); - - final Rect scanRect; - final double scanLineProgress; - - static const double _cornerLength = 36; - static const double _cornerStroke = 4; - - @override - void paint(Canvas canvas, Size size) { - final cutout = RRect.fromRectAndRadius( - scanRect, - const Radius.circular(_scanWindowRadius), - ); - - final background = Path()..addRect(Offset.zero & size); - final hole = Path()..addRRect(cutout); - final overlay = Path.combine(PathOperation.difference, background, hole); - - canvas.drawPath( - overlay, - Paint()..color = Colors.black.withValues(alpha: 0.55), - ); - - final borderPaint = Paint() - ..color = AppTheme.primary.withValues(alpha: 0.35) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.5; - canvas.drawRRect(cutout, borderPaint); - - final cornerPaint = Paint() - ..color = AppTheme.primary - ..style = PaintingStyle.stroke - ..strokeWidth = _cornerStroke - ..strokeCap = StrokeCap.round; - - _drawCorners(canvas, scanRect, cornerPaint); - - final lineY = scanRect.top + - _scanWindowRadius + - (scanRect.height - _scanWindowRadius * 2) * scanLineProgress; - final linePaint = Paint() - ..shader = LinearGradient( - colors: [ - AppTheme.primary.withValues(alpha: 0), - AppTheme.primary.withValues(alpha: 0.85), - AppTheme.primary.withValues(alpha: 0), - ], - stops: const [0, 0.5, 1], - ).createShader( - Rect.fromLTWH(scanRect.left + 16, lineY - 1, scanRect.width - 32, 2), - ) - ..strokeWidth = 2 - ..strokeCap = StrokeCap.round; - - canvas.drawLine( - Offset(scanRect.left + 16, lineY), - Offset(scanRect.right - 16, lineY), - linePaint, - ); - } - - void _drawCorners(Canvas canvas, Rect rect, Paint paint) { - const r = _scanWindowRadius; - const len = _cornerLength; - - canvas.drawLine( - Offset(rect.left + r, rect.top), - Offset(rect.left + r + len, rect.top), - paint, - ); - canvas.drawLine( - Offset(rect.left, rect.top + r), - Offset(rect.left, rect.top + r + len), - paint, - ); - - canvas.drawLine( - Offset(rect.right - r - len, rect.top), - Offset(rect.right - r, rect.top), - paint, - ); - canvas.drawLine( - Offset(rect.right, rect.top + r), - Offset(rect.right, rect.top + r + len), - paint, - ); - - canvas.drawLine( - Offset(rect.left + r, rect.bottom), - Offset(rect.left + r + len, rect.bottom), - paint, - ); - canvas.drawLine( - Offset(rect.left, rect.bottom - r - len), - Offset(rect.left, rect.bottom - r), - paint, - ); - - canvas.drawLine( - Offset(rect.right - r - len, rect.bottom), - Offset(rect.right - r, rect.bottom), - paint, - ); - canvas.drawLine( - Offset(rect.right, rect.bottom - r - len), - Offset(rect.right, rect.bottom - r), - paint, - ); - } - - @override - bool shouldRepaint(covariant _ScannerOverlayPainter oldDelegate) { - return oldDelegate.scanRect != scanRect || - oldDelegate.scanLineProgress != scanLineProgress; - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/services/auth_service.dart b/flutter-app/qrscanner/lib/services/auth_service.dart deleted file mode 100644 index c484f6b..0000000 --- a/flutter-app/qrscanner/lib/services/auth_service.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; -import 'package:qrscanner/config/api_config.dart'; -import 'package:qrscanner/models/auth_session.dart'; -import 'package:qrscanner/models/login_response.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class AuthService { - static const _sessionKey = 'auth_session'; - - AuthSession? _currentSession; - - AuthSession? get currentSession => _currentSession; - - Future login({ - required String username, - required String password, - bool rememberMe = false, - }) async { - final response = await http.post( - Uri.parse(ApiConfig.loginUrl), - headers: const {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'username': username, - 'password': password, - }), - ); - - final Map body = - jsonDecode(response.body) as Map; - final loginResponse = LoginResponse.fromJson(body); - - if (response.statusCode == 200 && - loginResponse.success && - loginResponse.accessToken != null) { - final session = AuthSession.fromLoginResponse( - loginResponse.copyWith(username: loginResponse.username ?? username), - ); - _currentSession = session; - - if (rememberMe) { - await _saveSession(session); - } else { - await clearSession(); - } - - return loginResponse; - } - - return LoginResponse( - success: false, - message: loginResponse.message ?? - 'No se pudo iniciar sesión (${response.statusCode})', - ); - } - - Future _saveSession(AuthSession session) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_sessionKey, jsonEncode(session.toJson())); - } - - Future clearSession() async { - _currentSession = null; - final prefs = await SharedPreferences.getInstance(); - await prefs.remove(_sessionKey); - } - - Future loadStoredSession() async { - final prefs = await SharedPreferences.getInstance(); - final raw = prefs.getString(_sessionKey); - if (raw == null || raw.isEmpty) return null; - - final session = AuthSession.fromJson( - jsonDecode(raw) as Map, - ); - _currentSession = session; - return session; - } - - Future hasStoredSession() async { - final session = await loadStoredSession(); - return session != null && session.accessToken.isNotEmpty; - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/services/feedback_service.dart b/flutter-app/qrscanner/lib/services/feedback_service.dart deleted file mode 100644 index e53aa55..0000000 --- a/flutter-app/qrscanner/lib/services/feedback_service.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:flutter/services.dart'; -import 'package:vibration/vibration.dart'; - -class FeedbackService { - FeedbackService() : _player = AudioPlayer() { - _player.setReleaseMode(ReleaseMode.stop); - } - - final AudioPlayer _player; - - Future onCodeDetected() async { - await Future.wait([ - _play('sounds/scan.wav'), - _vibratePattern(const [0, 40]), - ]); - } - - Future onSuccess() async { - await Future.wait([ - _play('sounds/success.wav'), - _vibratePattern(const [0, 90, 70, 140]), - ]); - await HapticFeedback.heavyImpact(); - } - - Future onWarning() async { - await Future.wait([ - _play('sounds/warning.wav'), - _vibratePattern(const [0, 120, 80, 120, 80, 120]), - ]); - await HapticFeedback.mediumImpact(); - } - - Future _play(String asset) async { - try { - await _player.stop(); - await _player.play(AssetSource(asset)); - } catch (_) { - // Ignore audio errors on devices without speaker routing. - } - } - - Future _vibratePattern(List pattern) async { - try { - final hasVibrator = await Vibration.hasVibrator(); - if (hasVibrator != true) return; - - final hasAmplitude = await Vibration.hasAmplitudeControl(); - if (hasAmplitude == true) { - final intensities = pattern - .asMap() - .entries - .map((entry) => entry.key.isOdd ? 180 : 0) - .toList(); - await Vibration.vibrate( - pattern: pattern, - intensities: intensities, - ); - return; - } - - await Vibration.vibrate(pattern: pattern); - } catch (_) { - await HapticFeedback.selectionClick(); - } - } - - Future dispose() async { - await _player.dispose(); - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/services/pass_code_service.dart b/flutter-app/qrscanner/lib/services/pass_code_service.dart deleted file mode 100644 index 686b83b..0000000 --- a/flutter-app/qrscanner/lib/services/pass_code_service.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; -import 'package:qrscanner/config/api_config.dart'; -import 'package:qrscanner/models/auth_session.dart'; -import 'package:qrscanner/models/scan_response.dart'; - -class PassCodeService { - Future scan({ - required AuthSession session, - required String code, - }) async { - final response = await http.post( - Uri.parse(ApiConfig.scanUrl), - headers: { - 'Content-Type': 'application/json', - 'Authorization': session.authorizationHeader, - }, - body: jsonEncode({'code': code}), - ); - - final Map body = - jsonDecode(response.body) as Map; - - return ScanResponse.fromJson(body); - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/lib/theme/app_theme.dart b/flutter-app/qrscanner/lib/theme/app_theme.dart deleted file mode 100644 index 410974c..0000000 --- a/flutter-app/qrscanner/lib/theme/app_theme.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppTheme { - static const Color primary = Color(0xFF00D4AA); - static const Color primaryDark = Color(0xFF00A88A); - static const Color backgroundDark = Color(0xFF0A0E1A); - static const Color surfaceDark = Color(0xFF141B2D); - static const Color accent = Color(0xFF6C63FF); - static const Color success = Color(0xFF22C55E); - static const Color successDark = Color(0xFF16A34A); - static const Color warning = Color(0xFFFBBF24); - static const Color warningDark = Color(0xFFF59E0B); - - static ThemeData get darkTheme { - return ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - colorScheme: ColorScheme.dark( - primary: primary, - secondary: accent, - surface: surfaceDark, - onPrimary: Colors.white, - onSurface: Colors.white, - ), - scaffoldBackgroundColor: backgroundDark, - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: Colors.white.withValues(alpha: 0.06), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.08)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide(color: primary, width: 1.5), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide(color: Color(0xFFFF6B6B)), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), - hintStyle: TextStyle(color: Colors.white.withValues(alpha: 0.4)), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 18), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/flutter-app/qrscanner/pubspec.lock b/flutter-app/qrscanner/pubspec.lock deleted file mode 100644 index b231bc5..0000000 --- a/flutter-app/qrscanner/pubspec.lock +++ /dev/null @@ -1,634 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" - source: hosted - version: "2.13.1" - audioplayers: - dependency: "direct main" - description: - name: audioplayers - sha256: f16640453cc47487b7de72a2b28d37c7df1ac97999849f4a46d92b1d2b0f093d - url: "https://pub.dev" - source: hosted - version: "6.7.1" - audioplayers_android: - dependency: transitive - description: - name: audioplayers_android - sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" - url: "https://pub.dev" - source: hosted - version: "5.2.1" - audioplayers_darwin: - dependency: transitive - description: - name: audioplayers_darwin - sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 - url: "https://pub.dev" - source: hosted - version: "6.4.0" - audioplayers_linux: - dependency: transitive - description: - name: audioplayers_linux - sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 - url: "https://pub.dev" - source: hosted - version: "4.2.1" - audioplayers_platform_interface: - dependency: transitive - description: - name: audioplayers_platform_interface - sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" - url: "https://pub.dev" - source: hosted - version: "7.1.1" - audioplayers_web: - dependency: transitive - description: - name: audioplayers_web - sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545" - url: "https://pub.dev" - source: hosted - version: "5.2.1" - audioplayers_windows: - dependency: transitive - description: - name: audioplayers_windows - sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a" - url: "https://pub.dev" - source: hosted - version: "4.3.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.dev" - source: hosted - version: "1.0.9" - device_info_plus: - dependency: transitive - description: - name: device_info_plus - sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65" - url: "https://pub.dev" - source: hosted - version: "13.1.0" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - ffi_leak_tracker: - dependency: transitive - description: - name: ffi_leak_tracker - sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" - url: "https://pub.dev" - source: hosted - version: "0.1.2" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - jni: - dependency: transitive - description: - name: jni - sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f - url: "https://pub.dev" - source: hosted - version: "1.0.0" - jni_flutter: - dependency: transitive - description: - name: jni_flutter - sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" - source: hosted - version: "0.12.19" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" - url: "https://pub.dev" - source: hosted - version: "1.18.0" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce - url: "https://pub.dev" - source: hosted - version: "7.2.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" - url: "https://pub.dev" - source: hosted - version: "9.4.1" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - record_use: - dependency: transitive - description: - name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" - url: "https://pub.dev" - source: hosted - version: "0.6.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.dev" - source: hosted - version: "2.5.5" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0 - url: "https://pub.dev" - source: hosted - version: "2.4.25" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" - url: "https://pub.dev" - source: hosted - version: "0.7.11" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" - source: hosted - version: "4.5.3" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vibration: - dependency: "direct main" - description: - name: vibration - sha256: "9bb06614c69260f8bd11c80fe01ed7988905cf00e3417d656c2647e41f261d87" - url: "https://pub.dev" - source: hosted - version: "3.1.8" - vibration_platform_interface: - dependency: transitive - description: - name: vibration_platform_interface - sha256: "258c273268f8aa40c88d29741137c536874a738779b92ddb8aa32ed093721ec5" - url: "https://pub.dev" - source: hosted - version: "0.1.2" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" - source: hosted - version: "15.2.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - win32: - dependency: transitive - description: - name: win32 - sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 - url: "https://pub.dev" - source: hosted - version: "6.3.0" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904" - url: "https://pub.dev" - source: hosted - version: "3.0.3" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.12.1 <4.0.0" - flutter: ">=3.44.0" diff --git a/flutter-app/qrscanner/pubspec.yaml b/flutter-app/qrscanner/pubspec.yaml deleted file mode 100644 index e0bbd60..0000000 --- a/flutter-app/qrscanner/pubspec.yaml +++ /dev/null @@ -1,92 +0,0 @@ -name: qrscanner -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 - -environment: - sdk: ^3.12.1 - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - audioplayers: ^6.5.0 - http: ^1.2.2 - mobile_scanner: ^7.0.1 - shared_preferences: ^2.3.3 - vibration: ^3.1.3 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^6.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - assets: - - assets/sounds/ - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package diff --git a/flutter-app/qrscanner/test/widget_test.dart b/flutter-app/qrscanner/test/widget_test.dart deleted file mode 100644 index f125623..0000000 --- a/flutter-app/qrscanner/test/widget_test.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:qrscanner/main.dart'; - -void main() { - testWidgets('Login screen renders correctly', (WidgetTester tester) async { - await tester.pumpWidget(const MyApp()); - - expect(find.text('QR Scanner'), findsOneWidget); - expect(find.text('Iniciar sesión'), findsOneWidget); - expect(find.text('Usuario'), findsOneWidget); - }); -} \ No newline at end of file diff --git a/pom.xml b/pom.xml index de4f4a1..608fff0 100644 --- a/pom.xml +++ b/pom.xml @@ -24,8 +24,21 @@ 25 25 UTF-8 + 2.31.78 + + + + software.amazon.awssdk + bom + ${aws.sdk.version} + pom + import + + + + base-admin-web-domain base-admin-web-application diff --git a/run-local.sh b/run-local.sh index b179385..9dd9823 100755 --- a/run-local.sh +++ b/run-local.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COGNITO_ENV_FILE="$SCRIPT_DIR/docker/floci/generated/cognito.local.env" + MIN_JAVA_VERSION=25 is_valid_jdk() { @@ -71,6 +74,39 @@ resolve_java_home() { return 1 } +wait_for_cognito_env() { + local elapsed=0 + local timeout=120 + + while [[ ! -f "$COGNITO_ENV_FILE" && $elapsed -lt $timeout ]]; do + sleep 2 + elapsed=$((elapsed + 2)) + if (( elapsed % 10 == 0 )); then + echo "Esperando bootstrap de Cognito en Floci..." >&2 + fi + done + + [[ -f "$COGNITO_ENV_FILE" ]] +} + +load_cognito_local_env() { + if [[ -n "${AWS_ENDPOINT_URL:-}" ]]; then + echo "AWS_ENDPOINT_URL ya definido; omitiendo carga de cognito.local.env" >&2 + return 0 + fi + + if [[ ! -f "$COGNITO_ENV_FILE" ]]; then + echo "No se encontro $COGNITO_ENV_FILE. Exporta las variables Cognito manualmente." >&2 + return 1 + fi + + set -a + # shellcheck disable=SC1090 + source "$COGNITO_ENV_FILE" + set +a + echo "Variables Cognito cargadas desde docker/floci/generated/cognito.local.env" >&2 +} + JAVA_HOME="$(resolve_java_home)" export JAVA_HOME export PATH="$JAVA_HOME/bin:$PATH" @@ -86,21 +122,34 @@ else echo "Base de datos: H2 en archivo local (./data/h2/). Para PostgreSQL usa: SPRING_PROFILES_ACTIVE=postgres ./run-local.sh" >&2 fi -if [[ "$PROFILE" == *"postgres"* ]]; then - if command -v docker >/dev/null 2>&1; then - if docker compose version >/dev/null 2>&1; then - docker compose -f docker-compose-postgres.local.yml up -d - else - echo "Docker Compose no esta disponible. Instala Docker Compose o levanta PostgreSQL manualmente." >&2 - exit 1 - fi +FLOCI_STARTED=false + +if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + COMPOSE_FILE="$SCRIPT_DIR/docker-compose.local.yml" + mkdir -p "$SCRIPT_DIR/docker/floci/generated" + chmod 777 "$SCRIPT_DIR/docker/floci/generated" 2>/dev/null || true + + if [[ "$PROFILE" == *"postgres"* ]]; then + echo "Levantando PostgreSQL y Floci (Cognito local)..." >&2 + docker compose -f "$COMPOSE_FILE" up -d postgres floci else - echo "Docker no esta disponible. Instala Docker o usa el perfil dev con H2." >&2 - exit 1 + echo "Levantando Floci (Cognito local) en http://localhost:4566 ..." >&2 + docker compose -f "$COMPOSE_FILE" up -d floci + fi + FLOCI_STARTED=true +elif [[ "$PROFILE" == *"postgres"* ]]; then + echo "Docker Compose no esta disponible. Instala Docker o levanta PostgreSQL manualmente." >&2 + exit 1 +fi + +if [[ "$FLOCI_STARTED" == true ]]; then + if wait_for_cognito_env; then + load_cognito_local_env || true + else + echo "Advertencia: timeout esperando cognito.local.env. Revisa Floci o exporta las variables manualmente." >&2 fi fi -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" mvn -pl base-admin-web-jar -am -DskipTests install diff --git a/scripts/floci-up.sh b/scripts/floci-up.sh new file mode 100755 index 0000000..bcd4737 --- /dev/null +++ b/scripts/floci-up.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$ROOT_DIR" + +mkdir -p docker/floci/generated +chmod 777 docker/floci/generated + +if ! command -v docker >/dev/null 2>&1; then + echo "Docker no esta instalado." >&2 + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "Docker Compose no esta disponible." >&2 + exit 1 +fi + +docker compose -f docker-compose.local.yml up -d floci + +cat <