21. Autenticación

Versión para imprimir.

A. Introducción

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/srvaut?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Usa o crea una cuenta de Google.

  3. Crea una cuenta de Replit usando la cuenta de Google.

  4. Crea un proyecto PHP Web Server en Replit y edita o sube los archivos de este proyecto.

  5. Depura el proyecto.

  6. Crea la cover page o página de spotlight del proyecto.

G. Archivos

Haz clic en los triángulos para expandir las carpetas

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Autenticación</title>
10
11 <script type="module" src="lib/js/muestraError.js"></script>
12 <script type="module" src="./js/protege.js"></script>
13 <script type="module" src="./js/custom/mi-nav.js"></script>
14
15</head>
16
17<body onload="protege('srv/srvSesion.php')
18 .then(sesion => nav.sesion = sesion)
19 .catch(muestraError)">
20
21 <mi-nav id="nav"></mi-nav>
22
23 <h1>Autenticación</h1>
24
25 <p>Bienvenid@.</p>
26
27</body>
28
29</html>

I. perfil.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Perfil</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="./js/protege.js"></script>
14 <script type="module" src="./js/custom/mi-nav.js"></script>
15
16</head>
17
18<body onload="protege('srv/srvSesion.php')
19 .then(sesion => {
20 nav.sesion = sesion
21 const cue = sesion.cue
22 if (cue === '') {
23 login.hidden = false
24 outputCue.value = 'No ha iniciado sesión.'
25 outputRoles.value = ''
26 } else {
27 logout.hidden = false
28 outputCue.value = cue
29 const rolIds = sesion.rolIds
30 outputRoles.value = rolIds.size === 0
31 ? 'Sin roles'
32 : Array.from(rolIds).join(', ')
33 }
34 })
35 .catch(muestraError)">
36
37 <mi-nav id="nav"></mi-nav>
38
39 <h1>Perfil</h1>
40
41 <p>
42 <output id="outputCue">
43 <progress max="100">Cargando…</progress>
44 </output>
45 </p>
46
47 <p>
48 <output id="outputRoles">
49 <progress max="100">Cargando…</progress>
50 </output>
51 </p>
52
53 <p>
54
55 <a id="login" hidden href="login.html">Iniciar sesión</a>
56
57 <button type="button" id="logout" hidden
58 onclick="invocaServicio('srv/srvLogout.php')
59 .then(json => location.reload())
60 .catch(muestraError)">
61 Terminar sesión
62 </button>
63
64 </p>
65
66</body>
67
68</html>

J. login.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Iniciar sesión</title>
10
11 <script type="module" src="lib/js/submitForm.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="./js/protege.js"></script>
14
15</head>
16
17<body onload="protege('srv/srvSesion.php')
18 .then(sesion => {
19 if (sesion.cue !== '') {
20 location.href = 'perfil.html'
21 }
22 })
23 .catch(muestraError)">
24
25 <form id="login" onsubmit="submitForm('srv/srvLogin.php', event)
26 .then(sesion => location.href = 'perfil.html')
27 .catch(muestraError)">
28
29 <h1>Iniciar Sesión</h1>
30
31 <p>
32 <label>
33 Cue
34 <input name="cue">
35 </label>
36 </p>
37
38 <p>
39 <label>
40 Match
41 <input type="password" name="match">
42 </label>
43 </p>
44
45 <p>
46 <a href="perfil.html">Cancelar</a>
47 <button type="submit">Iniciar sesión</button>
48 </p>
49
50 </form>
51
52</body>
53
54</html>

K. admin.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Solo Administradores</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="./js/const/ROL_ADMINISTRADOR.js"></script>
14 <script type="module" src="./js/protege.js"></script>
15 <script type="module" src="./js/custom/mi-nav.js"></script>
16
17</head>
18
19<body onload="protege('srv/srvSesion.php', [ROL_ADMINISTRADOR], 'index.html')
20 .then(sesion => {
21 nav.sesion = sesion
22 main.hidden = false
23 })
24 .catch(muestraError)">
25
26 <mi-nav id="nav"></mi-nav>
27
28 <main id="main" hidden>
29
30 <h1>Solo Administradores</h1>
31
32 <p>Hola.</p>
33
34 <p>
35 <button type="button" onclick="invocaServicio('srv/srvSaludoCliente.php')
36 .then(saludo => alert(saludo.body))
37 .catch(muestraError)">
38 Ejecuta servicio
39 </button>
40 </p>
41
42 </main>
43
44</body>
45
46</html>

L. cliente.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Solo Clientes</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="./js/const/ROL_CLIENTE.js"></script>
14 <script type="module" src="./js/protege.js"></script>
15 <script type="module" src="./js/custom/mi-nav.js"></script>
16
17</head>
18
19<body onload="protege('srv/srvSesion.php', [ROL_CLIENTE], 'index.html')
20 .then(sesion => {
21 nav.sesion = sesion
22 main.hidden = false
23 })
24 .catch(muestraError)">
25
26 <mi-nav id="nav"></mi-nav>
27
28 <main id="main" hidden>
29
30 <h1>Solo Clientes</h1>
31
32 <p>Hola.</p>
33
34 <p>
35 <button type="button" onclick="invocaServicio('srv/srvSaludoCliente.php')
36 .then(saludo => alert(saludo.body))
37 .catch(muestraError)">
38 Ejecuta servicio
39 </button>
40 </p>
41
42 </main>
43
44</body>
45
46</html>

M. Carpeta « js »

Versión para imprimir.

A. Carpeta « js / const »

1. js / const / CUE.js

1export const CUE = "cue"

2. js / const / ROL_ADMINISTRADOR.js

1export const ROL_ADMINISTRADOR = "Administrador"
2
3// Permite que los eventos de html usen la constante.
4window["ROL_ADMINISTRADOR"] = ROL_ADMINISTRADOR

3. js / const / ROL_CLIENTE.js

1export const ROL_CLIENTE = "Cliente"
2
3// Permite que los eventos de html usen la constante.
4window["ROL_CLIENTE"] = ROL_CLIENTE

4. js / const / ROL_IDS.js

1export const ROL_IDS = "rolIds"

B. js / Sesion.js

1import { CUE } from "./const/CUE.js"
2import { ROL_IDS } from "./const/ROL_IDS.js"
3
4export class Sesion {
5
6 /**
7 * @param { any } objeto
8 */
9 constructor(objeto) {
10
11 /** @readonly */
12 this.cue = objeto[CUE]
13 if (typeof this.cue !== "string")
14 throw new Error("cue debe ser string.")
15
16 /** @readonly */
17 const rolIds = objeto[ROL_IDS]
18 if (!Array.isArray(rolIds))
19 throw new Error("rolIds debe ser arreglo.")
20 /** @readonly */
21 this.rolIds = new Set(rolIds)
22
23 }
24
25}
26
27// Permite que los eventos de html usen la clase.
28window["Sesion"] = Sesion

C. js / protege.js

1import { invocaServicio } from "../lib/js/invocaServicio.js"
2import { Sesion } from "./Sesion.js"
3
4/**
5 * @param {string} servicio
6 * @param {string[]} [rolIdsPermitidos]
7 * @param {string} [urlDeProtección]
8 */
9export async function protege(servicio, rolIdsPermitidos, urlDeProtección) {
10 const respuesta = await invocaServicio('srv/srvSesion.php')
11 const sesion = new Sesion(respuesta.body)
12 if (rolIdsPermitidos === undefined) {
13 return sesion
14 } else {
15 const rolIds = sesion.rolIds
16 for (const rolId of rolIdsPermitidos) {
17 if (rolIds.has(rolId)) {
18 return sesion
19 }
20 }
21 if (urlDeProtección !== undefined) {
22 location.href = urlDeProtección
23 }
24 throw new Error("No autorizado.")
25 }
26}
27
28// Permite que los eventos de html usen la función.
29window["protege"] = protege

D. Carpeta « js / custom »

1. js / custom / mi-nav.js

1import { htmlentities } from "../../lib/js/htmlentities.js"
2import { Sesion } from "../Sesion.js"
3import { ROL_ADMINISTRADOR } from "../const/ROL_ADMINISTRADOR.js"
4import { ROL_CLIENTE } from "../const/ROL_CLIENTE.js"
5
6export class MiNav extends HTMLElement {
7
8 connectedCallback() {
9
10 this.style.display = "block"
11
12 this.innerHTML = /* html */
13 `<nav>
14 <ul style="display: flex;
15 flex-wrap: wrap;
16 padding:0;
17 gap: 0.5em;
18 list-style-type: none">
19 <li><progress max="100">Cargando…</progress></li>
20 </ul>
21 </nav>`
22
23 }
24
25 /**
26 * @param {Sesion} sesion
27 */
28 set sesion(sesion) {
29 const cue = sesion.cue
30 const rolIds = sesion.rolIds
31 let innerHTML = /* html */ `<li><a href="index.html">Inicio</a></li>`
32 innerHTML += this.hipervinculosAdmin(rolIds)
33 innerHTML += this.hipervinculosCliente(rolIds)
34 const cueHtml = htmlentities(cue)
35 if (cue !== "") {
36 innerHTML += /* html */ `<li>${cueHtml}</li>`
37 }
38 innerHTML += /* html */ `<li><a href="perfil.html">Perfil</a></li>`
39 const ul = this.querySelector("ul")
40 if (ul !== null) {
41 ul.innerHTML = innerHTML
42 }
43 }
44
45 /**
46 * @param {Set<string>} rolIds
47 */
48 hipervinculosAdmin(rolIds) {
49 return rolIds.has(ROL_ADMINISTRADOR) ?
50 /* html */ `<li><a href="admin.html">Para administradores</a></li>`
51 : ""
52 }
53
54 /**
55 * @param {Set<string>} rolIds
56 */
57 hipervinculosCliente(rolIds) {
58 return rolIds.has(ROL_CLIENTE) ?
59 /* html */ `<li><a href="cliente.html">Para clientes</a></li>`
60 : ""
61 }
62}
63
64customElements.define("mi-nav", MiNav)

N. Carpeta « srv »

Versión para imprimir.

A. Carpeta « srv / const »

1. srv / const / CUE.php

1<?php
2
3const CUE = "cue";

2. srv / const / ROL_ADMINISTRADOR.php

1<?php
2
3const ROL_ADMINISTRADOR = "Administrador";
4

3. srv / const / ROL_CLIENTE.php

1<?php
2
3const ROL_CLIENTE = "Cliente";

4. srv / const / ROL_IDS.php

1<?php
2
3const ROL_IDS = "rolIds";

B. Carpeta « srv / modelo »

1. srv / modelo / Rol.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4
5class Rol
6{
7
8 public string $id;
9 public string $descripcion;
10
11 public function __construct(string $descripcion = "", string $id = "")
12 {
13 $this->id = $id;
14 $this->descripcion = $descripcion;
15 }
16
17 public function valida()
18 {
19
20 if ($this->id === "")
21 throw new ProblemDetails(
22 status: ProblemDetails::BadRequest,
23 type: "/error/faltaid.html",
24 title: "Falta el id.",
25 );
26
27 if ($this->descripcion === "")
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/faltadescripcion.html",
31 title: "Falta la descripción.",
32 );
33 }
34}
35

2. srv / modelo / Usuario.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/Rol.php";
5
6class Usuario
7{
8
9 public int $id;
10 public string $cue;
11 public string $match;
12 /** @var Rol[] */
13 public array $roles;
14
15 public function __construct(
16 string $cue = "",
17 string $match = "",
18 array $roles = [],
19 int $id = 0
20 ) {
21 $this->id = $id;
22 $this->cue = $cue;
23 $this->match = $match;
24 $this->roles = $roles;
25 }
26
27 public function valida()
28 {
29
30 if ($this->cue === "")
31 throw new ProblemDetails(
32 status: ProblemDetails::BadRequest,
33 type: "/error/faltacue.html",
34 title: "Falta el cue.",
35 );
36
37 if ($this->match === "")
38 throw new ProblemDetails(
39 status: ProblemDetails::BadRequest,
40 type: "/error/faltamatch.html",
41 title: "Falta el match.",
42 );
43
44 foreach ($this->roles as $rol) {
45 if (!($rol instanceof Rol))
46 throw new ProblemDetails(
47 status: ProblemDetails::BadRequest,
48 type: "/error/rolincorrecto.html",
49 title: "Tipo incorrecto para un rol.",
50 );
51 }
52 }
53}
54

C. srv / protege.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/const/CUE.php";
6require_once __DIR__ . "/const/ROL_IDS.php";
7require_once __DIR__ . "/const/ROL_CLIENTE.php";
8require_once __DIR__ . "/Sesion.php";
9
10const NO_AUTORIZADO = 401;
11
12function protege(?array $rolIdsPermitidos = null)
13{
14 session_start();
15 $cue = isset($_SESSION[CUE])
16 ? $_SESSION[CUE]
17 : "";
18 $rolIds = isset($_SESSION[ROL_IDS])
19 ? $_SESSION[ROL_IDS]
20 : [];
21 $sesion = new Sesion($cue, $rolIds);
22 if ($rolIdsPermitidos === null) {
23 return $sesion;
24 } else {
25 foreach ($rolIdsPermitidos as $rolId) {
26 if (array_search($rolId, $rolIds, true) !== false) {
27 return $sesion;
28 }
29 }
30 throw new ProblemDetails(
31 status: NO_AUTORIZADO,
32 type: "/error/noautorizado.html",
33 title: "No autorizado.",
34 detail: "No está autorizado para usar este recurso.",
35 );
36 }
37}
38

D. srv / Sesion.php

1<?php
2
3class Sesion
4{
5
6 public string $cue;
7 public array $rolIds;
8
9 public function __construct(string $cue, array $rolIds)
10 {
11 $this->cue = $cue;
12 $this->rolIds = $rolIds;
13 }
14}
15

E. srv / srvLogin.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/leeTexto.php";
6require_once __DIR__ . "/const/CUE.php";
7require_once __DIR__ . "/const/ROL_IDS.php";
8require_once __DIR__ . "/modelo/Rol.php";
9require_once __DIR__ . "/bd/usuarioVerifica.php";
10require_once __DIR__ . "/protege.php";
11
12ejecutaServicio(function () {
13 $sesion = protege();
14 if ($sesion->cue !== "") {
15 throw new ProblemDetails(
16 status: NO_AUTORIZADO,
17 type: "/error/sesioniniciada.html",
18 title: "Sesión iniciada.",
19 detail: "La sesión ya está iniciada.",
20 );
21 }
22 $cue = leeTexto("cue");
23 $match = leeTexto("match");
24 if ($cue === null || $cue === "")
25 throw new ProblemDetails(
26 status: ProblemDetails::BadRequest,
27 type: "/error/faltacue.html",
28 title: "Falta el cue.",
29 );
30
31 if ($match === null || $match === "")
32 throw new ProblemDetails(
33 status: ProblemDetails::BadRequest,
34 type: "/error/faltamatch.html",
35 title: "Falta el match.",
36 );
37
38 $usuario = usuarioVerifica(trim($cue), trim($match));
39 if ($usuario === false) {
40 throw new ProblemDetails(
41 status: ProblemDetails::BadRequest,
42 type: "/error/datosincorrectos.html",
43 title: "Datos incorrectos.",
44 detail: "El cue y/o el match proporcionados son incorrectos.",
45 );
46 } else {
47 $rolIds = [];
48 foreach ($usuario->roles as $rol) {
49 $rolIds[] = $rol->id;
50 }
51 $_SESSION[CUE] = $cue;
52 $_SESSION[ROL_IDS] = $rolIds;
53 return [
54 CUE => $cue,
55 ROL_IDS => $rolIds
56 ];
57 }
58});
59

F. srv / srvLogout.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/const/CUE.php";
5require_once __DIR__ . "/const/ROL_IDS.php";
6
7ejecutaServicio(function () {
8 session_start();
9 if (isset($_SESSION[CUE])) {
10 unset($_SESSION[CUE]);
11 }
12 if (isset($_SESSION[ROL_IDS])) {
13 unset($_SESSION[ROL_IDS]);
14 }
15 session_destroy();
16 return [];
17});
18

G. srv / srvSaludoCliente.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/const/ROL_CLIENTE.php";
5require_once __DIR__ . "/protege.php";
6
7ejecutaServicio(function () {
8 $sesion = protege([ROL_CLIENTE]);
9 return "Hola " . $sesion->cue;
10});
11

H. srv / srvSesion.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/protege.php";
5
6ejecutaServicio(function () {
7 return protege();
8});
9

I. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS USUARIO (
7 USU_ID INTEGER,
8 USU_CUE TEXT NOT NULL,
9 USU_MATCH TEXT NOT NULL,
10 CONSTRAINT USU_PK
11 PRIMARY KEY(USU_ID),
12 CONSTRAINT USU_CUE_UNQ
13 UNIQUE(USU_CUE)
14 )'
15 );
16 $con->exec(
17 'CREATE TABLE IF NOT EXISTS ROL (
18 ROL_ID TEXT,
19 ROL_DESCRIPCION TEXT NOT NULL,
20 CONSTRAINT ROL_PK
21 PRIMARY KEY(ROL_ID),
22 CONSTRAINT ROL_DESCR_UNQ
23 UNIQUE(ROL_DESCRIPCION)
24 )'
25 );
26 $con->exec(
27 'CREATE TABLE IF NOT EXISTS USU_ROL (
28 USU_ID INTEGER NOT NULL,
29 ROL_ID TEXT NOT NULL,
30 CONSTRAINT USU_ROL_PK
31 PRIMARY KEY(USU_ID, ROL_ID),
32 CONSTRAINT USU_ROL_USU_FK
33 FOREIGN KEY (USU_ID) REFERENCES USUARIO(USU_ID),
34 CONSTRAINT USU_ROL_ROL_FK
35 FOREIGN KEY (ROL_ID) REFERENCES ROL(ROL_ID)
36 )'
37 );
38}
39

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../const/ROL_CLIENTE.php";
4require_once __DIR__ . "/../const/ROL_ADMINISTRADOR.php";
5require_once __DIR__ . "/../modelo/Rol.php";
6require_once __DIR__ . "/../modelo/Usuario.php";
7require_once __DIR__ . "/bdCrea.php";
8require_once __DIR__ . "/usuarioBuscaCue.php";
9require_once __DIR__ . "/usuarioAgrega.php";
10require_once __DIR__ . "/rolConsulta.php";
11require_once __DIR__ . "/rolAgrega.php";
12require_once __DIR__ . "/rolBusca.php";
13
14class Bd
15{
16
17 private static ?PDO $conexion = null;
18
19 static function getConexion(): PDO
20 {
21 if (self::$conexion === null) {
22
23 self::$conexion = new PDO(
24 // cadena de conexión
25 "sqlite:srvaut.db",
26 // usuario
27 null,
28 // contraseña
29 null,
30 // Opciones: conexiones persistentes y lanza excepciones.
31 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
32 );
33
34 bdCrea(self::$conexion);
35
36 if (rolBusca(ROL_ADMINISTRADOR) === false) {
37 $administrador = new Rol(
38 id: ROL_ADMINISTRADOR,
39 descripcion: "Administra el sistema."
40 );
41 rolAgrega($administrador);
42 }
43
44 if (rolBusca("Cliente") === false) {
45 $cliente = new Rol(
46 id: "Cliente",
47 descripcion: "Realiza compras."
48 );
49 rolAgrega($cliente);
50 }
51
52 if (usuarioBuscaCue("pepito") === false) {
53 $usuario = new Usuario(
54 cue: "pepito",
55 match: "cuentos",
56 roles: [$cliente]
57 );
58 usuarioAgrega($usuario);
59 }
60
61 if (usuarioBuscaCue("susana") === false) {
62 $usuario = new Usuario(
63 cue: "susana",
64 match: "alegria",
65 roles: [$administrador]
66 );
67 usuarioAgrega($usuario);
68 }
69
70 if (usuarioBuscaCue("bebe") === false) {
71 $usuario = new Usuario(
72 cue: "bebe",
73 match: "saurio",
74 roles: [$administrador, $cliente]
75 );
76 usuarioAgrega($usuario);
77 }
78 }
79
80 return self::$conexion;
81 }
82}
83

3. srv / bd / rolAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/Bd.php";
5
6function rolAgrega(Rol $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO ROL
12 (ROL_ID, ROL_DESCRIPCION)
13 VALUES
14 (:id, :descripcion)"
15 );
16 $stmt->execute([
17 ":id" => $modelo->id,
18 ":descripcion" => $modelo->descripcion
19 ]);
20}
21

4. srv / bd / rolBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolConsulta.php";
6
7function rolBusca(string $id) : false|Rol
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 ROL_ID as id,
13 ROL_DESCRIPCION as descripcion
14 FROM ROL
15 WHERE ROL_ID = :id"
16 );
17 $stmt->execute([":id" => $id]);
18 $stmt->setFetchMode(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Rol::class
21 );
22 return $stmt->fetch();
23}
24

5. srv / bd / rolConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Rol.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Rol[] */
8function rolConsulta()
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 ROL_ID as id,
14 ROL_DESCRIPCION as descripcion
15 FROM ROL
16 ORDER BY ROL_ID"
17 );
18 $resultado = $stmt->fetchAll(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Rol::class
21 );
22 return recibeFetchAll($resultado);
23}
24

6. srv / bd / usuarioAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolAgrega.php";
6
7function usuarioAgrega(Usuario $modelo)
8{
9 $modelo->valida();
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 $stmt = $con->prepare(
13 "INSERT INTO USUARIO
14 (USU_CUE, USU_MATCH)
15 VALUES
16 (:cue, :match)"
17 );
18 $stmt->execute([
19 ":cue" => $modelo->cue,
20 ":match" => password_hash($modelo->match, PASSWORD_DEFAULT)
21 ]);
22 /* Si usas una secuencia para generar el id,
23 * pasa como parámetro de lastInsertId el
24 * nombre de dicha secuencia. */
25 $modelo->id = $con->lastInsertId();
26 usuRolAgrega($modelo);
27 $con->commit();
28}
29

7. srv / bd / usuarioBuscaCue.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolConsulta.php";
6
7function usuarioBuscaCue(string $cue)
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 USU_ID as id,
13 USU_CUE as cue,
14 USU_MATCH as match
15 FROM USUARIO
16 WHERE USU_CUE = :cue"
17 );
18 $stmt->execute([":cue" => $cue]);
19 $stmt->setFetchMode(
20 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
21 Usuario::class
22 );
23 /** @var false|Usuario */
24 $usuario = $stmt->fetch();
25 if ($usuario === false) {
26 return false;
27 } else {
28 $usuario->roles = usuRolConsulta($usuario->id);
29 return $usuario;
30 }
31}
32

8. srv / bd / usuarioVerifica.php

1<?php
2
3require_once __DIR__ . "/usuarioBuscaCue.php";
4
5function usuarioVerifica(string $cue, string $match)
6{
7 $usuario = usuarioBuscaCue($cue);
8 if ($usuario !== false && password_verify($match, $usuario->match)) {
9 return $usuario;
10 } else {
11 return false;
12 }
13}
14

9. srv / bd / usuRolAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5
6function usuRolAgrega(Usuario $usuario) {
7 $roles = $usuario->roles;
8 if (sizeof($roles) > 0) {
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO USU_ROL
12 (USU_ID, ROL_ID)
13 VALUES
14 (:usuId, :rolId)"
15 );
16 foreach ($roles as $rol) {
17 $stmt->execute(
18 [
19 ":usuId" => $usuario->id,
20 ":rolId" => $rol->id
21 ]
22 );
23 }
24 }
25}
26

10. srv / bd / usuRolConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Rol.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Rol[] */
8function usuRolConsulta(int $usuId)
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 UR.ROL_ID AS id,
14 R.ROL_DESCRIPCION AS descripcion
15 FROM USU_ROL UR, ROL R
16 WHERE
17 UR.ROL_ID = R.ROL_ID
18 AND UR.USU_ID = :usuId
19 ORDER BY UR.ROL_ID"
20 );
21 $stmt->execute([":usuId" => $usuId]);
22 $resultado = $stmt->fetchAll(
23 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
24 Rol::class
25 );
26 return recibeFetchAll($resultado);
27}
28

O. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / js »

1. lib / js / htmlentities.js

1/**
2 * Codifica un texto para que cambie los caracteres
3 * especiales y no se pueda interpretar como
4 * etiiqueta HTML. Esta técnica evita la inyección
5 * de código.
6 * @param { string } texto
7 * @returns { string } un texto que no puede
8 * interpretarse como HTML. */
9export function htmlentities(texto) {
10 return texto.replace(/[<>"']/g, textoDetectado => {
11 switch (textoDetectado) {
12 case "<": return "<"
13 case ">": return ">"
14 case '"': return "&quot;"
15 case "'": return "&#039;"
16 default: return textoDetectado
17 }
18 })
19}
20// Permite que los eventos de html usen la función.
21window["htmlentities"] = htmlentities

2. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

3. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

6. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

4. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

5. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

P. Carpeta « error »

Versión para imprimir.

A. error / datosincorrectos.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Datos incorrectos</title>
10
11</head>
12
13<body>
14
15 <h1>Datos incorrectos</h1>
16
17 <p>El cue y/o el match proporcionados son incorrectos.</p>
18</body>
19
20</html>

B. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

C. error / faltacue.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el cue</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el cue</h1>
16
17</body>
18
19</html>

D. error / faltadescripcion.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta la descripción</title>
10
11</head>
12
13<body>
14
15 <h1>Falta la descripción</h1>
16
17</body>
18
19</html>

E. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

F. error / faltamatch.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el match</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el match</h1>
16
17</body>
18
19</html>

G. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

H. error / noautorizado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>No autorizado</title>
10
11</head>
12
13<body>
14
15 <h1>No autorizado</h1>
16
17 <p>No está autorizado para usar este recurso.</p>
18
19</body>
20
21</html>

I. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

J. error / pasatiemponoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Pasatiempo no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Pasatiempo no encontrado</h1>
16
17 <p>No se encontró ningún pasatiempo con el id solicitado.</p>
18
19</body>
20
21</html>

K. error / rolincorrecto.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Tipo incorrecto para un rol</title>
10
11</head>
12
13<body>
14
15 <h1>Tipo incorrecto para un rol</h1>
16
17</body>
18
19</html>

L. error / sesioniniciada.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Sesión iniciada</title>
10
11</head>
12
13<body>
14
15 <h1>Sesión iniciada</h1>
16
17 <p>La sesión ya está iniciada.</p>
18
19</body>
20
21</html>

Q. jsconfig.json

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

R. Resumen