{"id":21836957,"url":"https://github.com/waldohidalgo/skate_park_app","last_synced_at":"2026-04-11T01:35:55.985Z","repository":{"id":236421455,"uuid":"792580225","full_name":"waldohidalgo/skate_park_app","owner":"waldohidalgo","description":"Repositorio con el código solución completo de la prueba con la que se finaliza el módulo 8 Implementación de API backend Node Express","archived":false,"fork":false,"pushed_at":"2024-05-06T16:00:02.000Z","size":1336,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-21T15:05:14.884Z","etag":null,"topics":["bootstrap-icons","bootstrap5","desafiolatam","express","express-fileupload","express-handlebars","jsonwebtoken","pg","postgresql","uuid"],"latest_commit_sha":null,"homepage":"https://skate-park-app.onrender.com/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/waldohidalgo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-27T01:29:44.000Z","updated_at":"2024-10-11T14:00:00.000Z","dependencies_parsed_at":"2024-05-06T17:26:23.131Z","dependency_job_id":"716253ce-a4b6-4e70-b8ec-684d825c8d5f","html_url":"https://github.com/waldohidalgo/skate_park_app","commit_stats":null,"previous_names":["waldohidalgo/skate_park_app"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/waldohidalgo/skate_park_app","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waldohidalgo%2Fskate_park_app","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waldohidalgo%2Fskate_park_app/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waldohidalgo%2Fskate_park_app/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waldohidalgo%2Fskate_park_app/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/waldohidalgo","download_url":"https://codeload.github.com/waldohidalgo/skate_park_app/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waldohidalgo%2Fskate_park_app/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267617643,"owners_count":24116208,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-29T02:00:12.549Z","response_time":2574,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bootstrap-icons","bootstrap5","desafiolatam","express","express-fileupload","express-handlebars","jsonwebtoken","pg","postgresql","uuid"],"created_at":"2024-11-27T20:43:47.891Z","updated_at":"2026-04-11T01:35:50.955Z","avatar_url":"https://github.com/waldohidalgo.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Solución Completa de la Prueba - Skate Park\n\nRepositorio con el código solución de la prueba con la que se finaliza el módulo 8 **Implementación de API backend Node Express** de la beca dada por Talento Digital para Chile **Desarrollo de aplicaciones Full Stack Javascript Trainee** y dictada por Desafío Latam.\n\nIncorporo bcrypt para el almacenamiento encriptado de passwords en la base de datos\n\n## Tabla de Contenido\n\n- [Solución Completa de la Prueba - Skate Park](#solución-completa-de-la-prueba---skate-park)\n  - [Tabla de Contenido](#tabla-de-contenido)\n  - [Librerías utilizadas](#librerías-utilizadas)\n  - [Deploy](#deploy)\n  - [Requisitos](#requisitos)\n  - [Diagrama de Flujo](#diagrama-de-flujo)\n    - [1.Página Home](#1página-home)\n    - [2.Página Registro](#2página-registro)\n      - [2.1.Registro de Participante Exitoso](#21registro-de-participante-exitoso)\n      - [2.2.Verificación de Registro exitoso de Participante Exitoso Front End](#22verificación-de-registro-exitoso-de-participante-exitoso-front-end)\n      - [2.3.Verificación de Registro exitoso de Participante Exitoso Back End](#23verificación-de-registro-exitoso-de-participante-exitoso-back-end)\n    - [3.Página LogIn de Participante](#3página-login-de-participante)\n      - [3.1.LogIn exitoso de Participante](#31login-exitoso-de-participante)\n      - [3.2.Página Cuenta de Usuario](#32página-cuenta-de-usuario)\n      - [3.3.Token Expirado después de 2 minutos](#33token-expirado-después-de-2-minutos)\n      - [3.4.Alerta Edición exitosa](#34alerta-edición-exitosa)\n      - [3.5.Verificación edición exitosa front end](#35verificación-edición-exitosa-front-end)\n      - [3.6.Verificación edición exitosa back end](#36verificación-edición-exitosa-back-end)\n    - [4.Página Admin](#4página-admin)\n      - [4.1.Alerta Cambio de estado exitoso de participante por Admin](#41alerta-cambio-de-estado-exitoso-de-participante-por-admin)\n      - [4.2.Verificación Cambio de estado exitoso de participante por Admin Front End](#42verificación-cambio-de-estado-exitoso-de-participante-por-admin-front-end)\n      - [4.3.Verificación Cambio de estado exitoso de participante por Admin Back End](#43verificación-cambio-de-estado-exitoso-de-participante-por-admin-back-end)\n    - [5.Alerta de Eliminación de Participante exitosa](#5alerta-de-eliminación-de-participante-exitosa)\n      - [5.1.Verificación de Eliminación de participante Front End](#51verificación-de-eliminación-de-participante-front-end)\n      - [5.2.Verificación de Eliminación de participante Back End](#52verificación-de-eliminación-de-participante-back-end)\n    - [6.Alerta Archivo Muy Pesado](#6alerta-archivo-muy-pesado)\n    - [7.Alerta Tipo de Archivo No Permitido](#7alerta-tipo-de-archivo-no-permitido)\n    - [8.Alerta password y password repetida no coinciden](#8alerta-password-y-password-repetida-no-coinciden)\n    - [9.Página 404](#9página-404)\n  - [Soluciones](#soluciones)\n    - [1. Crear una API REST con el Framework Express (3 Puntos)](#1-crear-una-api-rest-con-el-framework-express-3-puntos)\n    - [2. Servir contenido dinámico con express-handlebars (3 Puntos)](#2-servir-contenido-dinámico-con-express-handlebars-3-puntos)\n    - [3. Ofrecer la funcionalidad Upload File con express-fileupload (2 Puntos)](#3-ofrecer-la-funcionalidad-upload-file-con-express-fileupload-2-puntos)\n    - [4. Implementar seguridad y restricción de recursos o contenido con JWT (2 Puntos)](#4-implementar-seguridad-y-restricción-de-recursos-o-contenido-con-jwt-2-puntos)\n  - [Extra](#extra)\n    - [1. Ruta para resetear la data en la base de datos y borrar las imagenes](#1-ruta-para-resetear-la-data-en-la-base-de-datos-y-borrar-las-imagenes)\n    - [2. Script para resetear la data cada 30 minutos](#2-script-para-resetear-la-data-cada-30-minutos)\n\n## Librerías utilizadas\n\n| Librerias Utilizadas |\n| -------------------- |\n| bcrypt               |\n| bootstrap-icons      |\n| express              |\n| express-fileupload   |\n| express-handlebars   |\n| jsonwebtoken         |\n| pg                   |\n| uuid                 |\n\n## Deploy\n\nEl proyecto es 100% funcional y esta operativo en la web. Lo he desplegado en Render en el siguiente [link](https://skate-park-app.onrender.com/)\n\n## Requisitos\n\n![Requisitos 1 y 2](./screenshots/requisitos_1_2.webp)\n![Requisitos 3 y 4](./screenshots/requisitos_3_4.webp)\n![Requisito 5](./screenshots/requisitos_5.webp)\n\n## Diagrama de Flujo\n\n### 1.Página Home\n\n![Página Home](./screenshots/1inicio.webp)\n\n### 2.Página Registro\n\n![Página Registro](./screenshots/2registro.webp)\n\n#### 2.1.Registro de Participante Exitoso\n\n![Registro de Participante Exitoso](./screenshots/2.1registro_exitoso.webp)\n\n#### 2.2.Verificación de Registro exitoso de Participante Exitoso Front End\n\n![Verificación de Registro exitoso de Participante Exitoso Front](./screenshots/2.2verificacion_registro_exitoso_front.webp)\n\n#### 2.3.Verificación de Registro exitoso de Participante Exitoso Back End\n\n![Verificación de Registro exitoso de Participante Exitoso Back](./screenshots/2.3verificacion_registro_exitoso_back.webp)\n\n### 3.Página LogIn de Participante\n\n![Página LogIn de Participante](./screenshots/3login.webp)\n\n#### 3.1.LogIn exitoso de Participante\n\n![LogIn exitoso de Participante](./screenshots/3.1login_exitoso.webp)\n\n#### 3.2.Página Cuenta de Usuario\n\n![Página Cuenta de Usuario](./screenshots/3.2cuenta_usuario.webp)\n\n#### 3.3.Token Expirado después de 2 minutos\n\n![Token Expirado después de 2 minutos](./screenshots/3.3token_expirado.webp)\n\n#### 3.4.Alerta Edición exitosa\n\n![Alerta Edición exitosa](./screenshots/3.4edicion_exitosa.webp)\n\n#### 3.5.Verificación edición exitosa front end\n\n![Verificación edición exitosa front end](./screenshots/3.5verificacion_edicion_front.webp)\n\n#### 3.6.Verificación edición exitosa back end\n\n![Verificación edición exitosa back end](./screenshots/3.6.verificacion_edicion_back.webp)\n\n### 4.Página Admin\n\n![Página Admin](./screenshots/4.admin.webp)\n\n#### 4.1.Alerta Cambio de estado exitoso de participante por Admin\n\n![Alerta Cambio de estado exitoso de participante por Admin](./screenshots/4.1alerta_estado_cambiado.webp)\n\n#### 4.2.Verificación Cambio de estado exitoso de participante por Admin Front End\n\n![Verificación Cambio de estado exitoso de participante por Admin Front End](./screenshots/4.2verificacion_estado_cambiado_front.webp)\n\n#### 4.3.Verificación Cambio de estado exitoso de participante por Admin Back End\n\n![Verificación Cambio de estado exitoso de participante por Admin Back End](./screenshots/4.3verificacion_estado_cambiado_back.webp)\n\n### 5.Alerta de Eliminación de Participante exitosa\n\n![Alerta de Eliminación de Participante exitosa](./screenshots/5.alerta_eliminacion_participante.webp)\n\n#### 5.1.Verificación de Eliminación de participante Front End\n\n![Verificación de Eliminación de participante Front End](./screenshots/5.1verificacion_eliminacion_participante_front.webp)\n\n#### 5.2.Verificación de Eliminación de participante Back End\n\n![Verificación de Eliminación de participante Back End](./screenshots/5.2verificacion_eliminacion_participante_back.png)\n\n### 6.Alerta Archivo Muy Pesado\n\n![Alerta Archivo Muy Pesado](./screenshots/alerta_imagen_supera_limite.webp)\n\n### 7.Alerta Tipo de Archivo No Permitido\n\n![Alerta Tipo de Archivo No Permitido](./screenshots/alerta_tipo_archivo_no_permitido.webp)\n\n### 8.Alerta password y password repetida no coinciden\n\n![Alerta password y password repetida no coinciden](./screenshots/alerta_password_no_iguales.webp)\n\n### 9.Página 404\n\n![Página 404](./screenshots/pagina_404.webp)\n\n## Soluciones\n\n### 1. Crear una API REST con el Framework Express (3 Puntos)\n\nPara manipular la creación, edición, lectura y eliminación de participantes he creado la siguiente API REST:\n\n```js\nrouter.get(\"/participantes\", getParticipantes);\nrouter.post(\"/participante\", postParticipante);\nrouter.delete(\"/participante\", deleteParticipante);\nrouter.put(\"/participante\", putEditParticipante);\nrouter.patch(\"/participante\", patchEditParticipante);\n```\n\n### 2. Servir contenido dinámico con express-handlebars (3 Puntos)\n\nHe servido contenido dinámico con express-handlebars por ejemplo la página de Admin para la cual he utilizado el siguiente código:\n\n```hbs\n{{\u003e header}}\n\u003cmain class=\"py-3 main_page_admin\"\u003e\n  \u003ch2\u003eAdministración\u003c/h2\u003e\n  \u003chr class=\"w-50\" /\u003e\n  \u003cdiv class=\"table-responsive\"\u003e\n    \u003ctable class=\"table w-50 m-auto table-dark\"\u003e\n      \u003cthead\u003e\n        \u003ctr\u003e\n          \u003cth scope=\"col\"\u003e#\u003c/th\u003e\n          \u003cth scope=\"col\"\u003eFoto\u003c/th\u003e\n          \u003cth scope=\"col\"\u003eNombre\u003c/th\u003e\n          \u003cth scope=\"col\"\u003eAños de experiencia\u003c/th\u003e\n          \u003cth scope=\"col\"\u003eEspecialidad\u003c/th\u003e\n          \u003cth scope=\"col\"\u003eEstado\u003c/th\u003e\n        \u003c/tr\u003e\n      \u003c/thead\u003e\n      \u003ctbody\u003e\n        {{#if ( arrayVacio participantes) }}\n        \u003ctr\u003e\n          \u003ctd colspan=\"6\"\u003eNo hay participantes\u003c/td\u003e\n        \u003c/tr\u003e\n        {{else}}\n        {{#each participantes}}\n        \u003ctr\u003e\n          \u003cth scope=\"row\"\u003e{{addOne @index}}\u003c/th\u003e\n          \u003ctd\u003e\n            \u003cdiv class=\"participante_foto\"\u003e\n              \u003cimg\n                src=\"/public/imagenes/{{ this.foto }}\"\n                alt=\"Imagen de {{ this.nombre }}\"\n              /\u003e\n            \u003c/div\u003e\n          \u003c/td\u003e\n          \u003ctd\u003e{{ this.nombre }}\u003c/td\u003e\n          \u003ctd\u003e{{ this.anos_experiencia }}\u003c/td\u003e\n          \u003ctd\u003e{{ this.especialidad }}\u003c/td\u003e\n          \u003ctd\u003e\n            {{#if this.estado }}\n            \u003cinput\n              data-email=\"{{ this.email }}\"\n              class=\"aprobado_checkbox\"\n              type=\"checkbox\"\n              checked\n            /\u003e\n            {{else}}\n            \u003cinput\n              data-email=\"{{ this.email }}\"\n              class=\"aprobado_checkbox\"\n              type=\"checkbox\"\n            /\u003e\n            {{/if}}\n          \u003c/td\u003e\n        \u003c/tr\u003e\n        {{/each}}\n        {{/if}}\n      \u003c/tbody\u003e\n    \u003c/table\u003e\n  \u003c/div\u003e\n\u003c/main\u003e\n{{\u003e footer}}\n\n\u003cscript type=\"module\" src=\"/public/js/pages/admin.js\"\u003e\u003c/script\u003e\n\n```\n\n### 3. Ofrecer la funcionalidad Upload File con express-fileupload (2 Puntos)\n\nImplemento la librería express-fileupload al cargar un archivo en la creación de un participante:\n\n```js\nconst maxSize = 1 * 1024 * 1024;\nexport default async function postParticipante(req, res) {\n  try {\n    const {\n      email,\n      nombre,\n      anos_experiencia,\n      especialidad,\n      password,\n      password2,\n    } = req.body;\n    const participantes = await getAllDataParticipantesQuery();\n    if (participantes.find((participante) =\u003e participante.email === email)) {\n      res.status(400).send(\"El email ya existe\");\n      return;\n    }\n    const {\n      foto: { size, mimetype, mv: moveFile },\n    } = req.files;\n    if (password !== password2) {\n      res.status(400).send(\"Las contraseñas no coinciden\");\n      return;\n    }\n    if (size \u003e maxSize) {\n      res.status(413).send(\"El tamaño del archivo es demasiado grande\");\n      return;\n    }\n    if (\n      mimetype !== \"image/jpeg\" \u0026\u0026\n      mimetype !== \"image/png\" \u0026\u0026\n      mimetype !== \"image/jpg\" \u0026\u0026\n      mimetype !== \"image/gif\" \u0026\u0026\n      mimetype !== \"image/webp\"\n    ) {\n      res.status(415).send(\"El formato del archivo no es válido\");\n      return;\n    }\n    const nombreFoto = `${uuidv4().slice(0, 8)}.jpg`;\n    moveFile(path.resolve(\"public\", \"imagenes\", nombreFoto), async (err) =\u003e {\n      if (err) {\n        res.status(500).send(err);\n        return;\n      }\n      try {\n        const data = await postParticipantesQuery({\n          email,\n          nombre,\n          password,\n          anos_experiencia,\n          especialidad,\n          foto: nombreFoto,\n          estado: false,\n        });\n        console.log(data);\n        res.status(200).send(\"exito\");\n      } catch (error) {\n        res.status(500).send(error);\n      }\n    });\n  } catch (error) {\n    console.log(error);\n    res.status(500).send(error);\n  }\n}\n```\n\n### 4. Implementar seguridad y restricción de recursos o contenido con JWT (2 Puntos)\n\nImplemento creación de token en el login de usuario tal y como muestro a continuación:\n\n```js\nexport default async function loginUser(req, res) {\n  try {\n    const { email, password } = req.body;\n    if (!email || !password) {\n      res.status(400).send(\"Faltan datos\");\n      return;\n    }\n    const participantes = await getAllDataParticipantesQuery();\n    const participante = participantes.find(\n      (participante) =\u003e participante.email === email\n    );\n    if (!participante) {\n      res.status(400).send(\"El email no existe\");\n      return;\n    }\n    if (participante.password !== password) {\n      res.status(400).send(\"La contraseña es incorrecta\");\n      return;\n    }\n\n    jwt.sign(participante, secretKey, { expiresIn: 2 * 60 }, (err, token) =\u003e {\n      if (err) {\n        res.status(500).send(\"Ha ocurrido un error\");\n        return;\n      }\n      res.status(200).json({ token });\n    });\n  } catch (error) {\n    console.log(error);\n    res.status(500).send(error);\n  }\n}\n```\n\nImplemento verificación de token al editar o eliminar data del participante. En este caso muestro la verificación de token al eliminar a participante:\n\n```js\nexport default async function deleteParticipante(req, res) {\n  try {\n    const token = req.headers.authorization.split(\" \")[1];\n    jwt.verify(token, process.env.SECRET_KEY_JWT, async (err, decoded) =\u003e {\n      if (err) {\n        if (err.message === \"jwt expired\") {\n          res.status(401).send(\"El token ha expirado\");\n          return;\n        }\n        res.status(401).send(\"Token inválido\");\n        return;\n      }\n      const email = decoded.email;\n      const data = await deleteParticipanteQuery(email);\n      console.log(!data);\n      if (!data) {\n        res.status(404).send(\"Participante no encontrado\");\n        return;\n      }\n      console.log(path.resolve(\"public\", \"imagenes\", data.foto));\n      fs.unlink(path.resolve(\"public\", \"imagenes\", data.foto), (err) =\u003e {\n        if (err) {\n          console.log(err);\n          res.status(500).send(\"Error al borrar la imagen\");\n          return;\n        }\n        res.status(200).send(\"Participante eliminado\");\n      });\n    });\n  } catch (error) {\n    console.log(error.message);\n    res.status(500).send(error);\n  }\n}\n```\n\n## Extra\n\n### 1. Ruta para resetear la data en la base de datos y borrar las imagenes\n\nHe creado la siguiente ruta **reset** siguiente:\n\n```js\nrouter.get(\"/reset\", resetData);\n```\n\nLa cual utiliza el siguiente middleware:\n\n```js\nexport default async function resetData(req, res) {\n  try {\n    const data = await resetDataQuery();\n    if (data === \"exito\") {\n      const files = await fs.readdir(path.resolve(\"public\", \"imagenes\"));\n      for (const file of files) {\n        await fs.unlink(path.resolve(\"public\", \"imagenes\", file));\n      }\n      res.status(200).send(\"Data Reseteada con exito !\");\n    }\n  } catch (error) {\n    res.status(500).send(error);\n  }\n}\n```\n\nLa que a su vez utiliza la siguiente función **resetDataQuery**:\n\n```js\nexport default async function resetDataQuery() {\n  try {\n    const query = \"DELETE FROM prueba_skate_park_skaters;\";\n    await pool.query(query);\n    return \"exito\";\n  } catch (error) {\n    throw error;\n  }\n}\n```\n\n### 2. Script para resetear la data cada 30 minutos\n\n```js\nsetInterval(async () =\u003e {\n  try {\n    const data = await resetDataQuery();\n    if (data === \"exito\") {\n      const files = await fs.readdir(path.resolve(\"public\", \"imagenes\"));\n      for (const file of files) {\n        await fs.unlink(path.resolve(\"public\", \"imagenes\", file));\n      }\n      console.log(\"Data Reseteada con exito !\");\n    }\n  } catch (error) {\n    console.log(\"Error al resetear la data\", error.message);\n  }\n}, 1800000);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwaldohidalgo%2Fskate_park_app","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwaldohidalgo%2Fskate_park_app","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwaldohidalgo%2Fskate_park_app/lists"}