Estado base antes de integración Cognito.
Incluye admin web Spring Boot, API JWT para escáner y app Flutter qrscanner con login y escaneo de pases.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>mx.gob.slp.baseadminweb</groupId>
|
||||
<artifactId>base-admin-web</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>base-admin-web-web</artifactId>
|
||||
<name>base-admin-web-web</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>mx.gob.slp.baseadminweb</groupId>
|
||||
<artifactId>base-admin-web-application</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mx.gob.slp.baseadminweb</groupId>
|
||||
<artifactId>base-admin-web-configuration</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package mx.gob.slp.baseadminweb.web.controller;
|
||||
|
||||
import mx.gob.slp.baseadminweb.configuration.security.ApiAuthService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthApiController {
|
||||
|
||||
private final ApiAuthService apiAuthService;
|
||||
|
||||
public AuthApiController(ApiAuthService apiAuthService) {
|
||||
this.apiAuthService = apiAuthService;
|
||||
}
|
||||
|
||||
@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()
|
||||
));
|
||||
} catch (BadCredentialsException ex) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of(
|
||||
"success", false,
|
||||
"message", ex.getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public record LoginRequest(String username, String password) {
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package mx.gob.slp.baseadminweb.web.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class AuthController {
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login(Authentication authentication, Model model, HttpServletRequest request) {
|
||||
if (authentication != null && authentication.isAuthenticated()
|
||||
&& !(authentication instanceof AnonymousAuthenticationToken)) {
|
||||
return "redirect:/dashboard";
|
||||
}
|
||||
|
||||
model.addAttribute("hasError", request.getParameter("error") != null);
|
||||
model.addAttribute("loggedOut", request.getParameter("logout") != null);
|
||||
return "login";
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package mx.gob.slp.baseadminweb.web.controller;
|
||||
|
||||
import mx.gob.slp.baseadminweb.domain.model.AppUser;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String home() {
|
||||
return "redirect:/dashboard";
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public String dashboard(@AuthenticationPrincipal AppUser user, Model model) {
|
||||
model.addAttribute("user", user);
|
||||
return "dashboard";
|
||||
}
|
||||
|
||||
@GetMapping("/admin")
|
||||
public String adminHome() {
|
||||
return "redirect:/dashboard";
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package mx.gob.slp.baseadminweb.web.controller;
|
||||
|
||||
import mx.gob.slp.baseadminweb.application.service.PassCodeService;
|
||||
import mx.gob.slp.baseadminweb.domain.model.PassCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pass-codes")
|
||||
public class PassCodeApiController {
|
||||
|
||||
private final PassCodeService passCodeService;
|
||||
|
||||
public PassCodeApiController(PassCodeService passCodeService) {
|
||||
this.passCodeService = passCodeService;
|
||||
}
|
||||
|
||||
@PostMapping("/scan")
|
||||
public ResponseEntity<Map<String, Object>> scan(@RequestBody ScanRequest request) {
|
||||
try {
|
||||
PassCode passCode = passCodeService.scan(request.code());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "Codigo pase escaneado correctamente.",
|
||||
"passCode", toResponse(passCode)
|
||||
));
|
||||
} catch (IllegalArgumentException | IllegalStateException ex) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"success", false,
|
||||
"message", ex.getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> toResponse(PassCode passCode) {
|
||||
return Map.of(
|
||||
"id", passCode.getId(),
|
||||
"name", passCode.getName(),
|
||||
"code", passCode.getCode(),
|
||||
"status", passCode.getStatus().name(),
|
||||
"scanned", passCode.isScanned(),
|
||||
"createdAt", passCode.getCreatedAt().toString(),
|
||||
"scannedAt", passCode.getScannedAt() == null ? "" : passCode.getScannedAt().toString()
|
||||
);
|
||||
}
|
||||
|
||||
public record ScanRequest(String code) {
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
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 org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/pass-codes")
|
||||
public class PassCodeController {
|
||||
|
||||
private final PassCodeService passCodeService;
|
||||
|
||||
public PassCodeController(PassCodeService passCodeService) {
|
||||
this.passCodeService = passCodeService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String list(@AuthenticationPrincipal AppUser user, Model model) {
|
||||
model.addAttribute("user", user);
|
||||
model.addAttribute("passCodes", passCodeService.findAll());
|
||||
return "pass-codes";
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ResponseBody
|
||||
public List<PassCodeListItem> listJson() {
|
||||
return passCodeService.findAll().stream()
|
||||
.map(PassCodeListItem::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String create(@RequestParam String name, RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
var created = passCodeService.create(name);
|
||||
redirectAttributes.addFlashAttribute("successMessage",
|
||||
"Codigo pase creado: " + created.getCode());
|
||||
} catch (RuntimeException ex) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", ex.getMessage());
|
||||
}
|
||||
return "redirect:/admin/pass-codes";
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package mx.gob.slp.baseadminweb.web.dto;
|
||||
|
||||
import mx.gob.slp.baseadminweb.domain.model.PassCode;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record PassCodeListItem(
|
||||
Long id,
|
||||
String name,
|
||||
String code,
|
||||
Instant createdAt,
|
||||
String status,
|
||||
boolean scanned,
|
||||
Instant scannedAt
|
||||
) {
|
||||
|
||||
public static PassCodeListItem from(PassCode passCode) {
|
||||
return new PassCodeListItem(
|
||||
passCode.getId(),
|
||||
passCode.getName(),
|
||||
passCode.getCode(),
|
||||
passCode.getCreatedAt(),
|
||||
passCode.getStatus().name(),
|
||||
passCode.isScanned(),
|
||||
passCode.getScannedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dashboard | Base Admin Web</title>
|
||||
<link rel="stylesheet" th:href="@{/assets/plugins/global/plugins.bundle.css}">
|
||||
<link rel="stylesheet" th:href="@{/assets/css/style.bundle.css}">
|
||||
</head>
|
||||
<body id="kt_body" class="app-default">
|
||||
<div class="d-flex flex-column flex-root app-root" id="kt_app_root">
|
||||
<div class="app-page flex-column flex-column-fluid" id="kt_app_page">
|
||||
<div class="app-wrapper flex-column flex-row-fluid" id="kt_app_wrapper">
|
||||
<div class="app-main flex-column flex-row-fluid" id="kt_app_main">
|
||||
<div class="d-flex flex-column flex-column-fluid">
|
||||
<div class="app-header d-flex align-items-stretch border-bottom border-gray-200 bg-white shadow-sm">
|
||||
<div class="app-container container-xxl d-flex align-items-center justify-content-between w-100 py-4">
|
||||
<div>
|
||||
<div class="fs-2 fw-bold text-gray-900">Base Admin Web</div>
|
||||
<div class="text-gray-500 fs-7">Panel administrativo inicial</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="text-end">
|
||||
<div class="fw-semibold text-gray-800" th:text="${user.displayName}">Administrador Base</div>
|
||||
<div class="text-gray-500 fs-8" th:text="${user.username}">admin</div>
|
||||
</div>
|
||||
<form th:action="@{/logout}" method="post" class="m-0">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button type="submit" class="btn btn-light-danger btn-sm">Cerrar sesion</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-toolbar py-6 py-lg-8 bg-light-primary">
|
||||
<div class="app-container container-xxl d-flex flex-stack flex-wrap">
|
||||
<div>
|
||||
<h1 class="text-gray-900 fw-bold mb-2">Dashboard</h1>
|
||||
<div class="text-gray-700 fs-6">Base funcional para construir el panel administrativo.</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<a th:href="@{/admin/pass-codes}" class="btn btn-primary btn-sm">Codigos pase</a>
|
||||
<span class="badge badge-primary fs-7 fw-bold px-4 py-3">Rol activo: ADMIN</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-content flex-column-fluid">
|
||||
<div class="app-container container-xxl py-10">
|
||||
<div class="row g-5 g-xl-8 mb-10">
|
||||
<div class="col-xl-4">
|
||||
<div class="card card-flush h-xl-100">
|
||||
<div class="card-header pt-7">
|
||||
<div class="card-title d-flex flex-column">
|
||||
<span class="fs-2hx fw-bold text-gray-900">OK</span>
|
||||
<span class="text-gray-500 pt-1 fw-semibold fs-6">Autenticacion local operando</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column justify-content-end pb-7">
|
||||
<span class="text-gray-700 fs-6">El acceso con usuario semilla, seguridad y persistencia ya estan listos.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card card-flush h-xl-100">
|
||||
<div class="card-header pt-7">
|
||||
<div class="card-title d-flex flex-column">
|
||||
<span class="fs-2hx fw-bold text-gray-900">H2</span>
|
||||
<span class="text-gray-500 pt-1 fw-semibold fs-6">Base de datos embebida</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column justify-content-end pb-7">
|
||||
<span class="text-gray-700 fs-6">La app arranca sin infraestructura externa y queda lista para migrar a PostgreSQL despues.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card card-flush h-xl-100" th:style="|background-image:url('@{/assets/media/auth/bg6.jpg}'); background-size:cover; background-position:center;|
|
||||
">
|
||||
<div class="card-body d-flex flex-column justify-content-end">
|
||||
<div class="bg-dark bg-opacity-50 rounded p-5">
|
||||
<div class="fs-3 fw-bold text-white mb-2">Metronic integrado</div>
|
||||
<div class="text-white opacity-75">La base visual ya quedo lista para expandir menus, modulos y catalogos.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header border-0 pt-6">
|
||||
<div class="card-title">
|
||||
<h2 class="fw-bold">Siguientes pasos sugeridos</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-2">
|
||||
<div class="timeline-label">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-label fw-bold text-gray-800 fs-6">01</div>
|
||||
<div class="timeline-badge"><i class="fa fa-genderless text-primary fs-1"></i></div>
|
||||
<div class="timeline-content fw-semibold text-gray-700 ps-3">Crear catalogos y modulos reales del panel.</div>
|
||||
</div>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-label fw-bold text-gray-800 fs-6">02</div>
|
||||
<div class="timeline-badge"><i class="fa fa-genderless text-success fs-1"></i></div>
|
||||
<div class="timeline-content fw-semibold text-gray-700 ps-3">Agregar gestion de usuarios, roles y permisos finos.</div>
|
||||
</div>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-label fw-bold text-gray-800 fs-6">03</div>
|
||||
<div class="timeline-badge"><i class="fa fa-genderless text-info fs-1"></i></div>
|
||||
<div class="timeline-content fw-semibold text-gray-700 ps-3">Migrar de H2 a PostgreSQL cuando el dominio ya este definido.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:src="@{/assets/plugins/global/plugins.bundle.js}"></script>
|
||||
<script th:src="@{/assets/js/scripts.bundle.js}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Iniciar sesion | Base Admin Web</title>
|
||||
<link rel="stylesheet" th:href="@{/assets/plugins/global/plugins.bundle.css}">
|
||||
<link rel="stylesheet" th:href="@{/assets/css/style.bundle.css}">
|
||||
</head>
|
||||
<body id="kt_body" class="app-blank bgi-size-cover bgi-attachment-fixed bgi-position-center"
|
||||
th:style="|background-image: url('@{/assets/media/auth/bg10.jpeg}');|">
|
||||
<div class="d-flex flex-column flex-root" id="kt_app_root">
|
||||
<div class="d-flex flex-column flex-lg-row flex-column-fluid">
|
||||
<div class="d-flex flex-column flex-lg-row-fluid w-lg-50 p-10 order-2 order-lg-1">
|
||||
<div class="d-flex flex-center flex-column flex-lg-row-fluid">
|
||||
<div class="w-lg-500px rounded-4 bg-body shadow-sm p-10 p-lg-15">
|
||||
<div class="text-center mb-11">
|
||||
<h1 class="text-gray-900 fw-bolder mb-3">Base Admin Web</h1>
|
||||
<div class="text-gray-500 fw-semibold fs-6">Proyecto base Spring Boot con Metronic</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${hasError}" class="alert alert-danger d-flex align-items-center p-5 mb-8">
|
||||
<i class="ki-duotone ki-shield-cross fs-2hx text-danger me-4"><span class="path1"></span><span class="path2"></span></i>
|
||||
<div class="d-flex flex-column">
|
||||
<span>Usuario o contrasena incorrectos.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${loggedOut}" class="alert alert-success d-flex align-items-center p-5 mb-8">
|
||||
<i class="ki-duotone ki-abstract-26 fs-2hx text-success me-4"><span class="path1"></span><span class="path2"></span></i>
|
||||
<div class="d-flex flex-column">
|
||||
<span>La sesion se cerro correctamente.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="form w-100" th:action="@{/login}" method="post">
|
||||
<div class="fv-row mb-8">
|
||||
<input type="text" placeholder="Usuario" name="username" autocomplete="username"
|
||||
class="form-control bg-transparent" value="admin">
|
||||
</div>
|
||||
|
||||
<div class="fv-row mb-4">
|
||||
<input type="password" placeholder="Contrasena" name="password" autocomplete="current-password"
|
||||
class="form-control bg-transparent" value="Admin123*">
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between text-gray-500 fs-7 mb-8">
|
||||
<span>Usuario demo: <strong>admin</strong></span>
|
||||
<span>Password: <strong>Admin123*</strong></span>
|
||||
</div>
|
||||
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
|
||||
<div class="d-grid mb-5">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="indicator-label">Entrar al panel</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-lg-row-fluid w-lg-50 bgi-size-cover bgi-position-center order-1 order-lg-2"
|
||||
th:style="|background-image: url('@{/assets/media/auth/bg1.jpg}');|">
|
||||
<div class="d-flex flex-column flex-center py-7 py-lg-15 px-5 px-md-15 w-100 bg-dark bg-opacity-50">
|
||||
<div class="mb-8 text-center">
|
||||
<span class="badge badge-light-primary fs-7 fw-bold">Spring Boot 4.0 + Java 25</span>
|
||||
</div>
|
||||
<h2 class="text-white fs-2hx fw-bolder text-center mb-5">Base lista para crecer</h2>
|
||||
<div class="text-white fs-5 text-center opacity-75 mw-500px">
|
||||
Login funcional, panel administrativo inicial y estructura modular limpia para extender el sistema.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:src="@{/assets/plugins/global/plugins.bundle.js}"></script>
|
||||
<script th:src="@{/assets/js/scripts.bundle.js}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Codigos pase | Base Admin Web</title>
|
||||
<link rel="stylesheet" th:href="@{/assets/plugins/global/plugins.bundle.css}">
|
||||
<link rel="stylesheet" th:href="@{/assets/css/style.bundle.css}">
|
||||
</head>
|
||||
<body id="kt_body" class="app-default" th:attr="data-pass-codes-list-url=@{/admin/pass-codes/list}">
|
||||
<div class="d-flex flex-column flex-root app-root" id="kt_app_root">
|
||||
<div class="app-page flex-column flex-column-fluid" id="kt_app_page">
|
||||
<div class="app-wrapper flex-column flex-row-fluid" id="kt_app_wrapper">
|
||||
<div class="app-main flex-column flex-row-fluid" id="kt_app_main">
|
||||
<div class="d-flex flex-column flex-column-fluid">
|
||||
<div class="app-header d-flex align-items-stretch border-bottom border-gray-200 bg-white shadow-sm">
|
||||
<div class="app-container container-xxl d-flex align-items-center justify-content-between w-100 py-4">
|
||||
<div>
|
||||
<div class="fs-2 fw-bold text-gray-900">Base Admin Web</div>
|
||||
<div class="text-gray-500 fs-7">Panel administrativo inicial</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="text-end">
|
||||
<div class="fw-semibold text-gray-800" th:text="${user.displayName}">Administrador Base</div>
|
||||
<div class="text-gray-500 fs-8" th:text="${user.username}">admin</div>
|
||||
</div>
|
||||
<form th:action="@{/logout}" method="post" class="m-0">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button type="submit" class="btn btn-light-danger btn-sm">Cerrar sesion</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-toolbar py-6 py-lg-8 bg-light-primary">
|
||||
<div class="app-container container-xxl d-flex flex-stack flex-wrap">
|
||||
<div>
|
||||
<h1 class="text-gray-900 fw-bold mb-2">Codigos pase</h1>
|
||||
<div class="text-gray-700 fs-6">Genera codigos aleatorios y consulta si ya fueron escaneados.</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<a th:href="@{/dashboard}" class="btn btn-primary btn-sm">Dashboard</a>
|
||||
<span class="badge badge-primary fs-7 fw-bold px-4 py-3">Rol activo: ADMIN</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-content flex-column-fluid">
|
||||
<div class="app-container container-xxl py-10">
|
||||
<div th:if="${successMessage}" class="alert alert-success mb-8" th:text="${successMessage}"></div>
|
||||
<div th:if="${errorMessage}" class="alert alert-danger mb-8" th:text="${errorMessage}"></div>
|
||||
|
||||
<div class="card card-flush mb-10">
|
||||
<div class="card-header border-0 pt-6">
|
||||
<div class="card-title">
|
||||
<h2 class="fw-bold mb-0">Nuevo codigo pase</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<form th:action="@{/admin/pass-codes}" method="post" class="row g-5 align-items-end">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label fw-semibold">Nombre</label>
|
||||
<input type="text" name="name" class="form-control form-control-solid"
|
||||
placeholder="Ej. Acceso evento principal" required maxlength="120">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100">Generar codigo</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-flush">
|
||||
<div class="card-header border-0 pt-6">
|
||||
<div class="card-title">
|
||||
<h2 class="fw-bold mb-0">Codigos registrados</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-row-dashed align-middle gs-0 gy-4">
|
||||
<thead>
|
||||
<tr class="fw-bold text-gray-500">
|
||||
<th>Nombre</th>
|
||||
<th>Codigo</th>
|
||||
<th>Fecha creacion</th>
|
||||
<th>Estatus</th>
|
||||
<th>Escaneado</th>
|
||||
<th>Fecha escaneo</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pass-codes-table-body">
|
||||
<tr th:if="${#lists.isEmpty(passCodes)}">
|
||||
<td colspan="7" class="text-gray-500">Aun no hay codigos pase registrados.</td>
|
||||
</tr>
|
||||
<tr th:each="passCode : ${passCodes}">
|
||||
<td class="fw-semibold text-gray-800" th:text="${passCode.name}">Nombre</td>
|
||||
<td>
|
||||
<span class="badge badge-light-dark fs-7 fw-bold"
|
||||
th:text="${passCode.code}">ABC123</span>
|
||||
</td>
|
||||
<td th:text="${#temporals.format(passCode.createdAt.atZone(T(java.time.ZoneId).systemDefault()), 'dd/MM/yyyy HH:mm')}">
|
||||
09/06/2026 11:00
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge fs-7 fw-bold"
|
||||
th:classappend="${passCode.status.name() == 'ACTIVO'} ? 'badge-light-success' : 'badge-light-warning'"
|
||||
th:text="${passCode.status}">ACTIVO</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge fs-7 fw-bold"
|
||||
th:classappend="${passCode.scanned} ? 'badge-light-warning' : 'badge-light-primary'"
|
||||
th:text="${passCode.scanned} ? 'Si' : 'No'">No</span>
|
||||
</td>
|
||||
<td th:text="${passCode.scannedAt != null ? #temporals.format(passCode.scannedAt.atZone(T(java.time.ZoneId).systemDefault()), 'dd/MM/yyyy HH:mm') : '-'}">
|
||||
-
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button type="button"
|
||||
class="btn btn-light-primary btn-sm btn-qr-pass-code"
|
||||
th:attr="data-code=${passCode.code},data-name=${passCode.name},data-status=${passCode.status}">
|
||||
<i class="ki-duotone ki-scan-barcode fs-3">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
<span class="path3"></span>
|
||||
<span class="path4"></span>
|
||||
</i>
|
||||
Ver QR
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-flush mt-10">
|
||||
<div class="card-header border-0 pt-6">
|
||||
<div class="card-title">
|
||||
<h2 class="fw-bold mb-0">API de escaneo</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<div class="text-gray-700 fs-7 mb-3">
|
||||
1. Login escaner: POST <code>/api/auth/login</code>
|
||||
</div>
|
||||
<div class="text-gray-700 fs-7 mb-3">
|
||||
<code>{"username":"scanner","password":"Scanner123*"}</code>
|
||||
</div>
|
||||
<div class="text-gray-700 fs-7 mb-3">
|
||||
2. Escanear: POST <code>/api/pass-codes/scan</code> con header
|
||||
<code>Authorization: Bearer TOKEN</code>
|
||||
</div>
|
||||
<div class="text-gray-700 fs-7 mb-3">
|
||||
<code>{"code":"CODIGO_GENERADO"}</code>
|
||||
</div>
|
||||
<div class="text-gray-500 fs-8">
|
||||
Solo usuarios con rol SCANNER pueden iniciar sesion por API y escanear codigos.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="kt_modal_pass_code_qr" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered mw-650px">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header pb-0 border-0 justify-content-end">
|
||||
<div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
|
||||
<i class="ki-duotone ki-cross fs-1">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body scroll-y mx-5 mx-xl-15 my-7 pt-0">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="fw-bold mb-3">Codigo QR del pase</h2>
|
||||
<div class="text-gray-500 fs-6">Escanea este codigo para validar el acceso.</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center gap-5">
|
||||
<div class="border border-dashed border-gray-300 rounded p-5 bg-light">
|
||||
<div id="pass_code_qr_container"></div>
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3 mb-4">
|
||||
<span class="badge badge-light-dark fs-7 fw-bold" id="pass_code_qr_code">CODIGO</span>
|
||||
<span class="badge fs-7 fw-bold badge-light-success" id="pass_code_qr_status">ACTIVO</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-gray-500 fs-7 mb-1">Nombre del pase</div>
|
||||
<div class="fs-4 fw-bold text-gray-900" id="pass_code_qr_name">Nombre</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer flex-center border-0 pt-0 pb-10">
|
||||
<button type="button" class="btn btn-light btn-sm" data-bs-dismiss="modal">Cerrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:src="@{/assets/plugins/global/plugins.bundle.js}"></script>
|
||||
<script th:src="@{/assets/js/scripts.bundle.js}"></script>
|
||||
<script th:src="@{/assets/js/vendor/qrcode.min.js}"></script>
|
||||
<script th:src="@{/assets/js/pass-codes.js}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user