From 238051db5d2a15780cbf1a8ff7c538b20ff6f647 Mon Sep 17 00:00:00 2001 From: Vero Date: Tue, 1 Apr 2025 13:04:20 +0200 Subject: [PATCH 01/14] feat: enhance login functionality with password visibility toggle and form validation --- .../navigation-button.component.tsx | 7 ++- src/modules/login/login.pod.tsx | 50 +++++++++++++++++-- src/modules/login/login.styles.ts | 16 +++++- src/modules/login/login.vm.ts | 9 ++++ 4 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 src/modules/login/login.vm.ts diff --git a/src/common/components/navigation-button/navigation-button.component.tsx b/src/common/components/navigation-button/navigation-button.component.tsx index c0d71cf..cece7ad 100644 --- a/src/common/components/navigation-button/navigation-button.component.tsx +++ b/src/common/components/navigation-button/navigation-button.component.tsx @@ -8,14 +8,17 @@ interface Props { text: string; params?: Record; variant?: ButtonProps['variant']; + fullWidth?: boolean; } export const NavigationButton: React.FC = props => { - const { path, params, text, variant = 'contained' } = props; + const { path, params, text, variant = 'contained', fullWidth } = props; return ( - + ); }; diff --git a/src/modules/login/login.pod.tsx b/src/modules/login/login.pod.tsx index d5321f5..d2ddca9 100644 --- a/src/modules/login/login.pod.tsx +++ b/src/modules/login/login.pod.tsx @@ -1,13 +1,53 @@ import React from 'react'; -import { Link } from '@tanstack/react-router'; -import { Typography } from '@mui/material'; +import { Button, IconButton, Paper, TableContainer, Typography } from '@mui/material'; + import * as classes from './login.styles'; +import { usePassword } from '#modules/users/create/use-password.hook'; +import { Visibility, VisibilityOff } from '@mui/icons-material'; +import { Form, Formik } from 'formik'; +import { createEmptyCredenciales, Credenciales } from './login.vm'; +import { TextFieldForm } from '#common/components'; export const LoginPod: React.FC = () => { + const { showPassword, toggleShowPassword } = usePassword(); + + const handleSubmit = (credenciales: Credenciales) => { + console.log(credenciales); + }; + return ( -
- Soy la página de login - Navegar a listado de expedientes +
+ + Inicia sesión en tu cuenta +
+ + {() => ( +
+ + + {showPassword ? : } + + ), + }, + }} + /> + + + )} +
+
+ + {/* */} +
); }; diff --git a/src/modules/login/login.styles.ts b/src/modules/login/login.styles.ts index 6bc32c9..0a9ea78 100644 --- a/src/modules/login/login.styles.ts +++ b/src/modules/login/login.styles.ts @@ -1,8 +1,22 @@ +import { theme } from '#core/theme/theme'; import { css } from '@emotion/css'; export const root = css` display: flex; flex-direction: column; - gap: 30px; align-items: center; + justify-content: center; + gap: ${theme.spacing(4)}; + padding: ${theme.spacing(6)}; + & > * { + width: 372px; + } +`; + +export const loginContainer = css` + display: flex; + flex-direction: column; + justify-content: center; + gap: 16px; + width: 372px; `; diff --git a/src/modules/login/login.vm.ts b/src/modules/login/login.vm.ts new file mode 100644 index 0000000..fb83973 --- /dev/null +++ b/src/modules/login/login.vm.ts @@ -0,0 +1,9 @@ +export interface Credenciales { + email: string; + contraseña: string; +} + +export const createEmptyCredenciales = (): Credenciales => ({ + email: '', + contraseña: '', +}); From d79bd04887134ebf3ba83a8bcbf125391d53247b Mon Sep 17 00:00:00 2001 From: Vero Date: Mon, 12 May 2025 11:40:46 +0200 Subject: [PATCH 02/14] feat: implement login component with form validation and password visibility toggle --- src/modules/login/index.ts | 5 ++ src/modules/login/login.component.tsx | 73 +++++++++++++++++++ src/modules/login/login.pod.tsx | 50 +------------ .../login/validations/login.literals.ts | 10 +++ .../login/validations/login.validations.ts | 26 +++++++ 5 files changed, 116 insertions(+), 48 deletions(-) create mode 100644 src/modules/login/login.component.tsx create mode 100644 src/modules/login/validations/login.literals.ts create mode 100644 src/modules/login/validations/login.validations.ts diff --git a/src/modules/login/index.ts b/src/modules/login/index.ts index ff54d5d..75669d9 100644 --- a/src/modules/login/index.ts +++ b/src/modules/login/index.ts @@ -1 +1,6 @@ +export * from './login.component'; export * from './login.pod'; +export * from './login.styles'; +export * from './login.vm'; +export * from './validations/login.literals'; +export * from './validations/login.validations'; diff --git a/src/modules/login/login.component.tsx b/src/modules/login/login.component.tsx new file mode 100644 index 0000000..3961cfb --- /dev/null +++ b/src/modules/login/login.component.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Button, IconButton, Paper, TableContainer, Typography } from '@mui/material'; +import { useToggle } from '#common/hooks'; +import { Visibility, VisibilityOff } from '@mui/icons-material'; +import { Form, Formik } from 'formik'; +import { createEmptyCredenciales, Credenciales } from './login.vm'; +import { NavigationButton, TextFieldForm } from '#common/components'; +import { formValidation } from './validations/login.validations.ts'; +// import { db } from '../../../mock-server/src/dals/mock.data.ts'; +import { useNavigate } from '@tanstack/react-router'; +import * as classes from './login.styles'; + +export const Login: React.FC = () => { + const { isOpen: showPassword, onToggle } = useToggle(false); + const [loginError, setLoginError] = React.useState(false); + const navigate = useNavigate(); + + const handleSubmit = (credenciales: Credenciales) => { + const { email, contraseña } = credenciales; + // const usuario = db.users.find(user => user.email === email && user.contraseña === contraseña); + const usuario = email === 'carlos.gonzalez@example.com' && contraseña === 'carlos123'; + if (usuario) { + setLoginError(false); + navigate({ to: '/expedientes' }); + } else { + setLoginError(true); + } + }; + + return ( +
+ + Inicia sesión en tu cuenta +
+ + {() => ( +
+ + {showPassword ? : } + ), + }, + }} + /> + + {loginError && ( + + Email o contraseña incorrectos + + )} + + + )} +
+
+ + +
+
+ ); +}; diff --git a/src/modules/login/login.pod.tsx b/src/modules/login/login.pod.tsx index d2ddca9..16e8beb 100644 --- a/src/modules/login/login.pod.tsx +++ b/src/modules/login/login.pod.tsx @@ -1,53 +1,7 @@ import React from 'react'; -import { Button, IconButton, Paper, TableContainer, Typography } from '@mui/material'; -import * as classes from './login.styles'; -import { usePassword } from '#modules/users/create/use-password.hook'; -import { Visibility, VisibilityOff } from '@mui/icons-material'; -import { Form, Formik } from 'formik'; -import { createEmptyCredenciales, Credenciales } from './login.vm'; -import { TextFieldForm } from '#common/components'; +import { Login } from './login.component'; export const LoginPod: React.FC = () => { - const { showPassword, toggleShowPassword } = usePassword(); - - const handleSubmit = (credenciales: Credenciales) => { - console.log(credenciales); - }; - - return ( -
- - Inicia sesión en tu cuenta -
- - {() => ( -
- - - {showPassword ? : } - - ), - }, - }} - /> - - - )} -
-
- - {/* */} -
-
- ); + return ; }; diff --git a/src/modules/login/validations/login.literals.ts b/src/modules/login/validations/login.literals.ts new file mode 100644 index 0000000..2ab3b38 --- /dev/null +++ b/src/modules/login/validations/login.literals.ts @@ -0,0 +1,10 @@ +const requiredMessage = 'Este campo es obligatorio.'; + +export const validationMessages = { + email: { + required: requiredMessage, + notValid: 'Por favor, introduce un email válido.', + notAvailable: 'Email no disponible en el sistema, introduce otro email.', + }, + contraseña: { required: requiredMessage }, +}; diff --git a/src/modules/login/validations/login.validations.ts b/src/modules/login/validations/login.validations.ts new file mode 100644 index 0000000..4a2929c --- /dev/null +++ b/src/modules/login/validations/login.validations.ts @@ -0,0 +1,26 @@ +import { ValidationSchema, Validators } from '@lemoncode/fonk'; +import { createFormikValidation } from '@lemoncode/fonk-formik'; +import { validationMessages } from './login.literals'; + +const validationSchema: ValidationSchema = { + field: { + email: [ + { + validator: Validators.required, + message: validationMessages.email.required, + }, + { + validator: Validators.email, + message: validationMessages.email.notValid, + }, + ], + contraseña: [ + { + validator: Validators.required, + message: validationMessages.contraseña.required, + }, + ], + }, +}; + +export const formValidation = createFormikValidation(validationSchema); From f7ce37d924ab919137100a68168082a815f9d13d Mon Sep 17 00:00:00 2001 From: Vero Date: Mon, 12 May 2025 14:04:31 +0200 Subject: [PATCH 03/14] feat: implement authentication flow with JWT, user session management, and login functionality --- mock-server/.env.example | 1 + mock-server/package-lock.json | 138 ++++++++++++++++++ mock-server/package.json | 2 + .../src/common/models/credentials.model.ts | 4 + mock-server/src/common/models/index.ts | 2 + .../src/common/models/user-session.model.ts | 6 + .../src/core/constants/env.constants.ts | 1 + mock-server/src/dals/user/index.ts | 1 + mock-server/src/dals/user/user.repository.ts | 4 +- mock-server/src/global-types.d.ts | 13 ++ mock-server/src/index.ts | 2 + mock-server/src/pods/security/index.ts | 1 + .../src/pods/security/security.middlewares.ts | 32 ++++ .../src/pods/security/security.rest-api.ts | 49 +++++++ src/core/auth/api/auth.api-model.ts | 12 ++ src/core/auth/api/auth.api.ts | 13 ++ src/core/auth/api/index.ts | 2 + src/core/auth/auth.model.ts | 16 ++ src/core/auth/auth.provider.tsx | 42 ++++++ src/core/auth/auth.query.hook.ts | 52 +++++++ src/core/auth/index.ts | 3 + src/core/react-query/query-keys.ts | 7 + src/main.tsx | 10 +- src/modules/login/login.component.tsx | 39 ++--- src/modules/login/login.pod.tsx | 9 +- src/modules/login/login.vm.ts | 7 +- src/scenes/__root.tsx | 9 +- src/scenes/_auth.tsx | 13 +- 28 files changed, 447 insertions(+), 43 deletions(-) create mode 100644 mock-server/src/common/models/credentials.model.ts create mode 100644 mock-server/src/common/models/user-session.model.ts create mode 100644 mock-server/src/global-types.d.ts create mode 100644 mock-server/src/pods/security/index.ts create mode 100644 mock-server/src/pods/security/security.middlewares.ts create mode 100644 mock-server/src/pods/security/security.rest-api.ts create mode 100644 src/core/auth/api/auth.api-model.ts create mode 100644 src/core/auth/api/auth.api.ts create mode 100644 src/core/auth/api/index.ts create mode 100644 src/core/auth/auth.model.ts create mode 100644 src/core/auth/auth.provider.tsx create mode 100644 src/core/auth/auth.query.hook.ts create mode 100644 src/core/auth/index.ts diff --git a/mock-server/.env.example b/mock-server/.env.example index dc30bca..3d1261e 100644 --- a/mock-server/.env.example +++ b/mock-server/.env.example @@ -1,3 +1,4 @@ NODE_ENV=development PORT=3000 SIMULATED_DELAY=2000 +AUTH_SECRET=MY_AUTH_SECRET \ No newline at end of file diff --git a/mock-server/package-lock.json b/mock-server/package-lock.json index 4f7405f..9156c50 100644 --- a/mock-server/package-lock.json +++ b/mock-server/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", "mongodb": "^6.10.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", @@ -17,6 +18,7 @@ }, "devDependencies": { "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.9", "@types/prompts": "^2.4.9", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", @@ -823,6 +825,17 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -830,6 +843,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", @@ -1282,6 +1302,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1668,6 +1694,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2877,6 +2912,67 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3023,16 +3119,58 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", diff --git a/mock-server/package.json b/mock-server/package.json index afba141..f4e2db6 100644 --- a/mock-server/package.json +++ b/mock-server/package.json @@ -25,6 +25,7 @@ "license": "ISC", "dependencies": { "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", "mongodb": "^6.10.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", @@ -32,6 +33,7 @@ }, "devDependencies": { "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.9", "@types/prompts": "^2.4.9", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", diff --git a/mock-server/src/common/models/credentials.model.ts b/mock-server/src/common/models/credentials.model.ts new file mode 100644 index 0000000..5d34c46 --- /dev/null +++ b/mock-server/src/common/models/credentials.model.ts @@ -0,0 +1,4 @@ +export interface UserCredentials { + email: string; + contraseña: string; +} diff --git a/mock-server/src/common/models/index.ts b/mock-server/src/common/models/index.ts index a3be7da..36358b2 100644 --- a/mock-server/src/common/models/index.ts +++ b/mock-server/src/common/models/index.ts @@ -1,2 +1,4 @@ export * from './collection.model.js'; export * from './lookup.model.js'; +export * from './credentials.model.js'; +export * from './user-session.model.js'; diff --git a/mock-server/src/common/models/user-session.model.ts b/mock-server/src/common/models/user-session.model.ts new file mode 100644 index 0000000..208b2ab --- /dev/null +++ b/mock-server/src/common/models/user-session.model.ts @@ -0,0 +1,6 @@ +import { Lookup } from './lookup.model.js'; + +export interface UserSession { + id: string; + rol: Lookup; +} diff --git a/mock-server/src/core/constants/env.constants.ts b/mock-server/src/core/constants/env.constants.ts index 16e4f90..e16e6c5 100644 --- a/mock-server/src/core/constants/env.constants.ts +++ b/mock-server/src/core/constants/env.constants.ts @@ -2,4 +2,5 @@ export const ENV = { IS_PRODUCTION: process.env.NODE_ENV === 'production', PORT: Number(process.env.PORT), SIMULATED_DELAY: +process.env.SIMULATED_DELAY, + AUTH_SECRET: process.env.AUTH_SECRET, }; diff --git a/mock-server/src/dals/user/index.ts b/mock-server/src/dals/user/index.ts index 96a8a4e..fae8fe2 100644 --- a/mock-server/src/dals/user/index.ts +++ b/mock-server/src/dals/user/index.ts @@ -1 +1,2 @@ export * from './user.model.js'; +export * from './user.repository.js'; diff --git a/mock-server/src/dals/user/user.repository.ts b/mock-server/src/dals/user/user.repository.ts index 71de253..cbf3259 100644 --- a/mock-server/src/dals/user/user.repository.ts +++ b/mock-server/src/dals/user/user.repository.ts @@ -1,5 +1,5 @@ import { paginateItems } from '#common/helpers/index.js'; -import { CollectionQuery } from '#common/models/index.js'; +import { CollectionQuery, UserCredentials } from '#common/models/index.js'; import { db } from '#dals/mock.data.js'; import * as model from './user.model.js'; @@ -27,4 +27,6 @@ export const userRepository = { } return index !== -1; }, + getUserByCredentials: async (userCredentials: UserCredentials) => + db.users.find(user => user.email === userCredentials.email && user.contraseña === userCredentials.contraseña), }; diff --git a/mock-server/src/global-types.d.ts b/mock-server/src/global-types.d.ts new file mode 100644 index 0000000..a1b037b --- /dev/null +++ b/mock-server/src/global-types.d.ts @@ -0,0 +1,13 @@ +declare namespace Express { + export interface UserSession { + id: string; + rol: { + id: string; + nombre: string; + }; + } + + export interface Request { + userSession?: UserSession; + } +} diff --git a/mock-server/src/index.ts b/mock-server/src/index.ts index a506258..162f9d7 100644 --- a/mock-server/src/index.ts +++ b/mock-server/src/index.ts @@ -4,6 +4,7 @@ import { logger } from '#core/logger/index.js'; import { createRestApiServer } from '#core/servers/index.js'; import { userApi } from '#pods/user/index.js'; import { lookupApi } from '#pods/lookup/index.js'; +import { securityApi } from '#pods/security/security.rest-api.js'; const app = createRestApiServer(); @@ -11,6 +12,7 @@ app.use(logRequestMiddleware(logger)); app.use('/api/user', userApi); app.use('/api/lookup', lookupApi); +app.use('/api/security', securityApi); app.use(logErrorRequestMiddleware(logger)); diff --git a/mock-server/src/pods/security/index.ts b/mock-server/src/pods/security/index.ts new file mode 100644 index 0000000..c58b956 --- /dev/null +++ b/mock-server/src/pods/security/index.ts @@ -0,0 +1 @@ +export * from './security.rest-api.js'; diff --git a/mock-server/src/pods/security/security.middlewares.ts b/mock-server/src/pods/security/security.middlewares.ts new file mode 100644 index 0000000..375225b --- /dev/null +++ b/mock-server/src/pods/security/security.middlewares.ts @@ -0,0 +1,32 @@ +import { RequestHandler } from 'express'; +import jwt from 'jsonwebtoken'; +import { UserSession } from '#common/models/index.js'; +import { ENV } from '#core/constants/index.js'; + +const verify = (token: string, secret: string): Promise => + new Promise((resolve, reject) => { + jwt.verify(token, secret, (error, userSession: UserSession) => { + if (error) { + reject(error); + } + + if (userSession) { + resolve(userSession); + } else { + reject(); + } + }); + }); + +export const authenticationMiddleware: RequestHandler = async (req, res, next) => { + try { + const [, token] = req.cookies.authorization?.split(' ') || []; + const userSession = await verify(token, ENV.AUTH_SECRET); + req.userSession = userSession; + next(); + } catch (error) { + console.error(error); + res.clearCookie('authorization'); + res.sendStatus(401); + } +}; diff --git a/mock-server/src/pods/security/security.rest-api.ts b/mock-server/src/pods/security/security.rest-api.ts new file mode 100644 index 0000000..f9dd92a --- /dev/null +++ b/mock-server/src/pods/security/security.rest-api.ts @@ -0,0 +1,49 @@ +import { Router } from 'express'; +import jwt from 'jsonwebtoken'; +import { UserCredentials, UserSession } from '#common/models/index.js'; +import { userRepository } from '#dals/user/index.js'; +import { authenticationMiddleware } from './security.middlewares.js'; + +export const securityApi = Router(); + +securityApi + .post('/login', async (req, res, next) => { + try { + const body = req.body as UserCredentials; + const user = await userRepository.getUserByCredentials(body); + + if (user) { + const userSession: UserSession = { + id: user._id.toHexString(), + rol: user.rol, + }; + // TODO: MOVE secret to .env + const token = jwt.sign(userSession, 'patata', { + expiresIn: '1d', + algorithm: 'HS256', + }); + res.cookie('authorization', `Bearer ${token}`, { + httpOnly: true, + }); + res.sendStatus(204); + } else { + res.sendStatus(401); + } + } catch (error) { + next(error); + } + }) + .get('/whoami', authenticationMiddleware, async (req, res, next) => { + try { + const user = await userRepository.getUserById(req.userSession?.id); + + if (user) { + res.status(200).send({ id: user._id.toHexString(), nombre: user.nombre, rol: user.rol }); + } else { + res.clearCookie('authorization'); + res.sendStatus(401); + } + } catch (error) { + next(error); + } + }); diff --git a/src/core/auth/api/auth.api-model.ts b/src/core/auth/api/auth.api-model.ts new file mode 100644 index 0000000..4d06047 --- /dev/null +++ b/src/core/auth/api/auth.api-model.ts @@ -0,0 +1,12 @@ +import { Lookup } from '#common/models'; + +export interface UserCredentials { + email: string; + contraseña: string; +} + +export interface User { + id: string; + nombre: string; + rol: Lookup; +} diff --git a/src/core/auth/api/auth.api.ts b/src/core/auth/api/auth.api.ts new file mode 100644 index 0000000..55801c0 --- /dev/null +++ b/src/core/auth/api/auth.api.ts @@ -0,0 +1,13 @@ +// login, logout, whoami(me) +import axios from 'axios'; +import { User, UserCredentials } from './auth.api-model'; + +export const login = async (userCredentials: UserCredentials): Promise => { + const response = await axios.post('/api/security/login', userCredentials); + return response.data; +}; + +export const whoami = async (): Promise => { + const response = await axios.get('/api/security/whoami', { withCredentials: true }); + return response.data; +}; diff --git a/src/core/auth/api/index.ts b/src/core/auth/api/index.ts new file mode 100644 index 0000000..db154de --- /dev/null +++ b/src/core/auth/api/index.ts @@ -0,0 +1,2 @@ +export * from './auth.api'; +export * from './auth.api-model'; diff --git a/src/core/auth/auth.model.ts b/src/core/auth/auth.model.ts new file mode 100644 index 0000000..5a7bc95 --- /dev/null +++ b/src/core/auth/auth.model.ts @@ -0,0 +1,16 @@ +import { User, UserCredentials } from './api/auth.api-model'; + +export interface AuthContextModel { + isAuthenticated: boolean; + user: User; + doLogin: (UserCredentials: UserCredentials) => Promise; +} + +export const createEmptyUser = (): User => ({ + id: '', + nombre: '', + rol: { + id: '', + nombre: '', + }, +}); diff --git a/src/core/auth/auth.provider.tsx b/src/core/auth/auth.provider.tsx new file mode 100644 index 0000000..3390912 --- /dev/null +++ b/src/core/auth/auth.provider.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { AuthContextModel } from './auth.model'; +import { useLoginMutation, useWhoamiQuery } from './auth.query.hook'; + +const AuthContext = React.createContext(null); + +interface Props { + children: React.ReactNode; +} + +export const AuthProvider: React.FC = props => { + const { children } = props; + + const { doLogin } = useLoginMutation(); + const { user, isAuthenticated, isLoading } = useWhoamiQuery(); + + const isReady = !isLoading; + + if (!isReady) { + return null; + } + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextModel => { + const context = React.useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/core/auth/auth.query.hook.ts b/src/core/auth/auth.query.hook.ts new file mode 100644 index 0000000..cf1d7be --- /dev/null +++ b/src/core/auth/auth.query.hook.ts @@ -0,0 +1,52 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { UserCredentials, login, User, whoami } from './api'; +import { createEmptyUser } from './auth.model'; +import { authQueryKeys } from '../react-query'; + +interface LoginMutationResult { + doLogin: (userCredentials: UserCredentials) => Promise; + isPending: boolean; +} + +export const useLoginMutation = (): LoginMutationResult => { + const { mutateAsync: doLogin, isPending } = useMutation({ + mutationFn: (userCredentials: UserCredentials) => login(userCredentials), + onSuccess: () => { + // TODO: EJERCUTAR EL WHOAMI + }, + }); + + return { + doLogin, + isPending, + }; +}; + +interface UseWhoamIQueryResult { + user: User; + isAuthenticated: boolean; + isLoading: boolean; +} + +export const useWhoamiQuery = (): UseWhoamIQueryResult => { + const { + data: user = createEmptyUser(), + isLoading, + isError, + } = useQuery({ + queryKey: authQueryKeys.whoami(), + queryFn: async () => { + const user = await whoami(); + return user; + }, + retry: false, + }); + + const isAuthenticated = !isError && Boolean(user?.id); + + return { + user, + isAuthenticated, + isLoading, + }; +}; diff --git a/src/core/auth/index.ts b/src/core/auth/index.ts new file mode 100644 index 0000000..917e2e7 --- /dev/null +++ b/src/core/auth/index.ts @@ -0,0 +1,3 @@ +export * from './auth.provider'; +export * from './auth.model'; +export * from './api'; diff --git a/src/core/react-query/query-keys.ts b/src/core/react-query/query-keys.ts index 24147ec..bda993a 100644 --- a/src/core/react-query/query-keys.ts +++ b/src/core/react-query/query-keys.ts @@ -18,3 +18,10 @@ export const certificacionesQueryKeys = { all: ['certificaciones'], certificacionCollection: (page?: number, pageSize?: number) => ['certificaciones', page, pageSize], }; + +export const authQueryKeys = { + all: ['auth'], + doLogin: () => ['auth', 'doLogin'], + // todo: add doLogout + whoami: () => ['auth', 'whoami'], +}; diff --git a/src/main.tsx b/src/main.tsx index f824cb0..bd1c154 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,21 +2,25 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { AuthProvider, useAuth } from '#core/auth'; import { RouterProvider } from '@tanstack/react-router'; import { queryClient } from './core/react-query'; import { router } from './core/router'; import { ThemeProvider } from './core/theme'; const App = () => { - return ; + const auth = useAuth(); + return ; }; createRoot(document.getElementById('root')!).render( - - + + + + diff --git a/src/modules/login/login.component.tsx b/src/modules/login/login.component.tsx index 3961cfb..39b64eb 100644 --- a/src/modules/login/login.component.tsx +++ b/src/modules/login/login.component.tsx @@ -1,42 +1,28 @@ import React from 'react'; import { Button, IconButton, Paper, TableContainer, Typography } from '@mui/material'; -import { useToggle } from '#common/hooks'; import { Visibility, VisibilityOff } from '@mui/icons-material'; import { Form, Formik } from 'formik'; -import { createEmptyCredenciales, Credenciales } from './login.vm'; import { NavigationButton, TextFieldForm } from '#common/components'; +import { useToggle } from '#common/hooks'; +import { UserCredentials } from '#core/auth'; +import { createEmptyCredenciales } from './login.vm'; import { formValidation } from './validations/login.validations.ts'; -// import { db } from '../../../mock-server/src/dals/mock.data.ts'; -import { useNavigate } from '@tanstack/react-router'; import * as classes from './login.styles'; -export const Login: React.FC = () => { - const { isOpen: showPassword, onToggle } = useToggle(false); - const [loginError, setLoginError] = React.useState(false); - const navigate = useNavigate(); +interface Props { + onSubmit: (userCredentials: UserCredentials) => void; +} - const handleSubmit = (credenciales: Credenciales) => { - const { email, contraseña } = credenciales; - // const usuario = db.users.find(user => user.email === email && user.contraseña === contraseña); - const usuario = email === 'carlos.gonzalez@example.com' && contraseña === 'carlos123'; - if (usuario) { - setLoginError(false); - navigate({ to: '/expedientes' }); - } else { - setLoginError(true); - } - }; +export const Login: React.FC = props => { + const { onSubmit } = props; + const { isOpen: showPassword, onToggle } = useToggle(false); return (
Inicia sesión en tu cuenta
- + {() => (
@@ -53,11 +39,6 @@ export const Login: React.FC = () => { }} /> - {loginError && ( - - Email o contraseña incorrectos - - )} diff --git a/src/modules/login/login.pod.tsx b/src/modules/login/login.pod.tsx index 16e8beb..8ecb0b9 100644 --- a/src/modules/login/login.pod.tsx +++ b/src/modules/login/login.pod.tsx @@ -1,7 +1,12 @@ import React from 'react'; - +import { useAuth } from '#core/auth'; import { Login } from './login.component'; +import { UserCredentials } from '#core/auth'; export const LoginPod: React.FC = () => { - return ; + const { doLogin } = useAuth(); + + const handleSubmit = (userCredentials: UserCredentials) => doLogin(userCredentials); + + return ; }; diff --git a/src/modules/login/login.vm.ts b/src/modules/login/login.vm.ts index fb83973..1f83d2e 100644 --- a/src/modules/login/login.vm.ts +++ b/src/modules/login/login.vm.ts @@ -1,9 +1,6 @@ -export interface Credenciales { - email: string; - contraseña: string; -} +import { UserCredentials } from '#core/auth'; -export const createEmptyCredenciales = (): Credenciales => ({ +export const createEmptyCredenciales = (): UserCredentials => ({ email: '', contraseña: '', }); diff --git a/src/scenes/__root.tsx b/src/scenes/__root.tsx index b3267fb..80032ec 100644 --- a/src/scenes/__root.tsx +++ b/src/scenes/__root.tsx @@ -1,8 +1,13 @@ import * as React from 'react'; -import { Outlet, createRootRoute } from '@tanstack/react-router'; +import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'; import { TanStackRouterDevtools } from '#core/router/router.dev-tools'; +import { AuthContextModel } from '#core/auth'; -export const Route = createRootRoute({ +interface Context { + auth: AuthContextModel; +} + +export const Route = createRootRouteWithContext()({ component: () => { return ( <> diff --git a/src/scenes/_auth.tsx b/src/scenes/_auth.tsx index 92c8de7..1ec9caf 100644 --- a/src/scenes/_auth.tsx +++ b/src/scenes/_auth.tsx @@ -1,7 +1,18 @@ -import { createFileRoute, Outlet } from '@tanstack/react-router'; +import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; import { AppLayout } from '#layouts/app.layout'; export const Route = createFileRoute('/_auth')({ + beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ + to: '/login', + }); + } else { + throw redirect({ + to: '/expedientes', + }); + } + }, component: () => { return ( From c6e2aebf3c4aeaabd095518f1018e40dec033528 Mon Sep 17 00:00:00 2001 From: manudous Date: Mon, 12 May 2025 15:13:10 +0200 Subject: [PATCH 04/14] feat: update authentication flow and improve route handling with cookie-parser integration --- mock-server/package.json | 3 +- .../src/core/servers/rest-api.server.ts | 2 + .../src/pods/security/security.rest-api.ts | 14 +- package-lock.json | 138 ++++++++++++++++++ src/core/auth/auth.query.hook.ts | 4 +- src/scenes/_auth.tsx | 4 - src/scenes/index.tsx | 6 +- src/scenes/login.tsx | 9 +- 8 files changed, 167 insertions(+), 13 deletions(-) diff --git a/mock-server/package.json b/mock-server/package.json index f4e2db6..6a8cbd9 100644 --- a/mock-server/package.json +++ b/mock-server/package.json @@ -32,7 +32,8 @@ "winston": "^3.16.0" }, "devDependencies": { - "@types/express": "^5.0.0", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^5.0.1", "@types/jsonwebtoken": "^9.0.9", "@types/prompts": "^2.4.9", "@types/swagger-jsdoc": "^6.0.4", diff --git a/mock-server/src/core/servers/rest-api.server.ts b/mock-server/src/core/servers/rest-api.server.ts index abd344e..b7d1fa9 100644 --- a/mock-server/src/core/servers/rest-api.server.ts +++ b/mock-server/src/core/servers/rest-api.server.ts @@ -1,9 +1,11 @@ import { ENV } from '#core/constants/env.constants.js'; +import cookieParser from 'cookie-parser'; import express from 'express'; export const createRestApiServer = () => { const app = express(); app.use(express.json()); + app.use(cookieParser()); app.use((req, res, next) => { setTimeout(next, ENV.SIMULATED_DELAY || 2000); }); diff --git a/mock-server/src/pods/security/security.rest-api.ts b/mock-server/src/pods/security/security.rest-api.ts index f9dd92a..65f721f 100644 --- a/mock-server/src/pods/security/security.rest-api.ts +++ b/mock-server/src/pods/security/security.rest-api.ts @@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken'; import { UserCredentials, UserSession } from '#common/models/index.js'; import { userRepository } from '#dals/user/index.js'; import { authenticationMiddleware } from './security.middlewares.js'; +import { ENV } from '#core/constants/env.constants.js'; export const securityApi = Router(); @@ -17,8 +18,7 @@ securityApi id: user._id.toHexString(), rol: user.rol, }; - // TODO: MOVE secret to .env - const token = jwt.sign(userSession, 'patata', { + const token = jwt.sign(userSession, ENV.AUTH_SECRET, { expiresIn: '1d', algorithm: 'HS256', }); @@ -28,6 +28,7 @@ securityApi res.sendStatus(204); } else { res.sendStatus(401); + res.clearCookie('authorization'); } } catch (error) { next(error); @@ -40,10 +41,17 @@ securityApi if (user) { res.status(200).send({ id: user._id.toHexString(), nombre: user.nombre, rol: user.rol }); } else { - res.clearCookie('authorization'); res.sendStatus(401); } } catch (error) { next(error); } + }) + .post('/logout', authenticationMiddleware, async (req, res, next) => { + try { + res.clearCookie('authorization'); + res.sendStatus(204); + } catch (error) { + next(error); + } }); diff --git a/package-lock.json b/package-lock.json index 5519dbd..9caa7be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@tanstack/react-router": "^1.82.1", "@tanstack/react-table": "^8.20.6", "axios": "^1.7.7", + "cookie-parser": "^1.4.7", "formik": "^2.4.6", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -29,6 +30,8 @@ "@tanstack/react-query-devtools": "^5.63.0", "@tanstack/router-devtools": "^1.82.1", "@tanstack/router-plugin": "^1.81.9", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^5.0.1", "@types/jest": "^29.5.14", "@types/node": "^22.9.0", "@types/react": "^18.3.12", @@ -2099,6 +2102,37 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2106,6 +2140,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", @@ -2115,6 +2174,13 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2156,6 +2222,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", @@ -2177,6 +2250,20 @@ "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", @@ -2206,6 +2293,29 @@ "@types/react": "*" } }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3620,6 +3730,34 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", diff --git a/src/core/auth/auth.query.hook.ts b/src/core/auth/auth.query.hook.ts index cf1d7be..33da914 100644 --- a/src/core/auth/auth.query.hook.ts +++ b/src/core/auth/auth.query.hook.ts @@ -1,7 +1,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { UserCredentials, login, User, whoami } from './api'; import { createEmptyUser } from './auth.model'; -import { authQueryKeys } from '../react-query'; +import { authQueryKeys, queryClient } from '../react-query'; interface LoginMutationResult { doLogin: (userCredentials: UserCredentials) => Promise; @@ -12,7 +12,7 @@ export const useLoginMutation = (): LoginMutationResult => { const { mutateAsync: doLogin, isPending } = useMutation({ mutationFn: (userCredentials: UserCredentials) => login(userCredentials), onSuccess: () => { - // TODO: EJERCUTAR EL WHOAMI + queryClient.removeQueries({ queryKey: authQueryKeys.whoami() }); }, }); diff --git a/src/scenes/_auth.tsx b/src/scenes/_auth.tsx index 1ec9caf..caab146 100644 --- a/src/scenes/_auth.tsx +++ b/src/scenes/_auth.tsx @@ -7,10 +7,6 @@ export const Route = createFileRoute('/_auth')({ throw redirect({ to: '/login', }); - } else { - throw redirect({ - to: '/expedientes', - }); } }, component: () => { diff --git a/src/scenes/index.tsx b/src/scenes/index.tsx index 1c159ab..f1ab086 100644 --- a/src/scenes/index.tsx +++ b/src/scenes/index.tsx @@ -1,7 +1,9 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ - beforeLoad: () => { - throw redirect({ to: '/login' }); + beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }); + } }, }); diff --git a/src/scenes/login.tsx b/src/scenes/login.tsx index 8b1b2aa..65a1186 100644 --- a/src/scenes/login.tsx +++ b/src/scenes/login.tsx @@ -1,8 +1,15 @@ -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute, redirect } from '@tanstack/react-router'; import { AuthLayout } from '#layouts/'; import { LoginPod } from '#modules/login'; export const Route = createFileRoute('/login')({ + beforeLoad: ({ context }) => { + if (context.auth.isAuthenticated) { + throw redirect({ + to: '/expedientes', + }); + } + }, component: () => { return ( From 70d1dad5f4f3c793724e478cf1e9f86af6cbb618 Mon Sep 17 00:00:00 2001 From: Vero Date: Tue, 1 Apr 2025 13:04:20 +0200 Subject: [PATCH 05/14] feat: enhance login functionality with password visibility toggle and form validation --- .../navigation-button.component.tsx | 7 ++- src/modules/login/login.pod.tsx | 50 +++++++++++++++++-- src/modules/login/login.styles.ts | 16 +++++- src/modules/login/login.vm.ts | 9 ++++ 4 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 src/modules/login/login.vm.ts diff --git a/src/common/components/navigation-button/navigation-button.component.tsx b/src/common/components/navigation-button/navigation-button.component.tsx index c0d71cf..cece7ad 100644 --- a/src/common/components/navigation-button/navigation-button.component.tsx +++ b/src/common/components/navigation-button/navigation-button.component.tsx @@ -8,14 +8,17 @@ interface Props { text: string; params?: Record; variant?: ButtonProps['variant']; + fullWidth?: boolean; } export const NavigationButton: React.FC = props => { - const { path, params, text, variant = 'contained' } = props; + const { path, params, text, variant = 'contained', fullWidth } = props; return ( - + ); }; diff --git a/src/modules/login/login.pod.tsx b/src/modules/login/login.pod.tsx index d5321f5..d2ddca9 100644 --- a/src/modules/login/login.pod.tsx +++ b/src/modules/login/login.pod.tsx @@ -1,13 +1,53 @@ import React from 'react'; -import { Link } from '@tanstack/react-router'; -import { Typography } from '@mui/material'; +import { Button, IconButton, Paper, TableContainer, Typography } from '@mui/material'; + import * as classes from './login.styles'; +import { usePassword } from '#modules/users/create/use-password.hook'; +import { Visibility, VisibilityOff } from '@mui/icons-material'; +import { Form, Formik } from 'formik'; +import { createEmptyCredenciales, Credenciales } from './login.vm'; +import { TextFieldForm } from '#common/components'; export const LoginPod: React.FC = () => { + const { showPassword, toggleShowPassword } = usePassword(); + + const handleSubmit = (credenciales: Credenciales) => { + console.log(credenciales); + }; + return ( -
- Soy la página de login - Navegar a listado de expedientes +
+ + Inicia sesión en tu cuenta +
+ + {() => ( + + + + {showPassword ? : } + + ), + }, + }} + /> + + + )} + +
+ + {/* */} +
); }; diff --git a/src/modules/login/login.styles.ts b/src/modules/login/login.styles.ts index 6bc32c9..0a9ea78 100644 --- a/src/modules/login/login.styles.ts +++ b/src/modules/login/login.styles.ts @@ -1,8 +1,22 @@ +import { theme } from '#core/theme/theme'; import { css } from '@emotion/css'; export const root = css` display: flex; flex-direction: column; - gap: 30px; align-items: center; + justify-content: center; + gap: ${theme.spacing(4)}; + padding: ${theme.spacing(6)}; + & > * { + width: 372px; + } +`; + +export const loginContainer = css` + display: flex; + flex-direction: column; + justify-content: center; + gap: 16px; + width: 372px; `; diff --git a/src/modules/login/login.vm.ts b/src/modules/login/login.vm.ts new file mode 100644 index 0000000..fb83973 --- /dev/null +++ b/src/modules/login/login.vm.ts @@ -0,0 +1,9 @@ +export interface Credenciales { + email: string; + contraseña: string; +} + +export const createEmptyCredenciales = (): Credenciales => ({ + email: '', + contraseña: '', +}); From 5f2db81723eba25c3b73530c03e570275111b3ec Mon Sep 17 00:00:00 2001 From: Vero Date: Mon, 12 May 2025 11:40:46 +0200 Subject: [PATCH 06/14] feat: implement login component with form validation and password visibility toggle --- src/modules/login/index.ts | 5 ++ src/modules/login/login.component.tsx | 73 +++++++++++++++++++ src/modules/login/login.pod.tsx | 50 +------------ .../login/validations/login.literals.ts | 10 +++ .../login/validations/login.validations.ts | 26 +++++++ 5 files changed, 116 insertions(+), 48 deletions(-) create mode 100644 src/modules/login/login.component.tsx create mode 100644 src/modules/login/validations/login.literals.ts create mode 100644 src/modules/login/validations/login.validations.ts diff --git a/src/modules/login/index.ts b/src/modules/login/index.ts index ff54d5d..75669d9 100644 --- a/src/modules/login/index.ts +++ b/src/modules/login/index.ts @@ -1 +1,6 @@ +export * from './login.component'; export * from './login.pod'; +export * from './login.styles'; +export * from './login.vm'; +export * from './validations/login.literals'; +export * from './validations/login.validations'; diff --git a/src/modules/login/login.component.tsx b/src/modules/login/login.component.tsx new file mode 100644 index 0000000..3961cfb --- /dev/null +++ b/src/modules/login/login.component.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Button, IconButton, Paper, TableContainer, Typography } from '@mui/material'; +import { useToggle } from '#common/hooks'; +import { Visibility, VisibilityOff } from '@mui/icons-material'; +import { Form, Formik } from 'formik'; +import { createEmptyCredenciales, Credenciales } from './login.vm'; +import { NavigationButton, TextFieldForm } from '#common/components'; +import { formValidation } from './validations/login.validations.ts'; +// import { db } from '../../../mock-server/src/dals/mock.data.ts'; +import { useNavigate } from '@tanstack/react-router'; +import * as classes from './login.styles'; + +export const Login: React.FC = () => { + const { isOpen: showPassword, onToggle } = useToggle(false); + const [loginError, setLoginError] = React.useState(false); + const navigate = useNavigate(); + + const handleSubmit = (credenciales: Credenciales) => { + const { email, contraseña } = credenciales; + // const usuario = db.users.find(user => user.email === email && user.contraseña === contraseña); + const usuario = email === 'carlos.gonzalez@example.com' && contraseña === 'carlos123'; + if (usuario) { + setLoginError(false); + navigate({ to: '/expedientes' }); + } else { + setLoginError(true); + } + }; + + return ( +
+ + Inicia sesión en tu cuenta +
+ + {() => ( +
+ + {showPassword ? : } + ), + }, + }} + /> + + {loginError && ( + + Email o contraseña incorrectos + + )} + + + )} +
+
+ + +
+
+ ); +}; diff --git a/src/modules/login/login.pod.tsx b/src/modules/login/login.pod.tsx index d2ddca9..16e8beb 100644 --- a/src/modules/login/login.pod.tsx +++ b/src/modules/login/login.pod.tsx @@ -1,53 +1,7 @@ import React from 'react'; -import { Button, IconButton, Paper, TableContainer, Typography } from '@mui/material'; -import * as classes from './login.styles'; -import { usePassword } from '#modules/users/create/use-password.hook'; -import { Visibility, VisibilityOff } from '@mui/icons-material'; -import { Form, Formik } from 'formik'; -import { createEmptyCredenciales, Credenciales } from './login.vm'; -import { TextFieldForm } from '#common/components'; +import { Login } from './login.component'; export const LoginPod: React.FC = () => { - const { showPassword, toggleShowPassword } = usePassword(); - - const handleSubmit = (credenciales: Credenciales) => { - console.log(credenciales); - }; - - return ( -
- - Inicia sesión en tu cuenta -
- - {() => ( -
- - - {showPassword ? : } - - ), - }, - }} - /> - - - )} -
-
- - {/* */} -
-
- ); + return ; }; diff --git a/src/modules/login/validations/login.literals.ts b/src/modules/login/validations/login.literals.ts new file mode 100644 index 0000000..2ab3b38 --- /dev/null +++ b/src/modules/login/validations/login.literals.ts @@ -0,0 +1,10 @@ +const requiredMessage = 'Este campo es obligatorio.'; + +export const validationMessages = { + email: { + required: requiredMessage, + notValid: 'Por favor, introduce un email válido.', + notAvailable: 'Email no disponible en el sistema, introduce otro email.', + }, + contraseña: { required: requiredMessage }, +}; diff --git a/src/modules/login/validations/login.validations.ts b/src/modules/login/validations/login.validations.ts new file mode 100644 index 0000000..4a2929c --- /dev/null +++ b/src/modules/login/validations/login.validations.ts @@ -0,0 +1,26 @@ +import { ValidationSchema, Validators } from '@lemoncode/fonk'; +import { createFormikValidation } from '@lemoncode/fonk-formik'; +import { validationMessages } from './login.literals'; + +const validationSchema: ValidationSchema = { + field: { + email: [ + { + validator: Validators.required, + message: validationMessages.email.required, + }, + { + validator: Validators.email, + message: validationMessages.email.notValid, + }, + ], + contraseña: [ + { + validator: Validators.required, + message: validationMessages.contraseña.required, + }, + ], + }, +}; + +export const formValidation = createFormikValidation(validationSchema); From 2816b5cc2c4ac491332d88f52a12ff7fc63c537a Mon Sep 17 00:00:00 2001 From: Vero Date: Mon, 12 May 2025 14:04:31 +0200 Subject: [PATCH 07/14] feat: implement authentication flow with JWT, user session management, and login functionality --- mock-server/.env.example | 1 + mock-server/package-lock.json | 138 ++++++++++++++++++ mock-server/package.json | 2 + .../src/common/models/credentials.model.ts | 4 + mock-server/src/common/models/index.ts | 2 + .../src/common/models/user-session.model.ts | 6 + .../src/core/constants/env.constants.ts | 1 + mock-server/src/dals/user/index.ts | 1 + mock-server/src/dals/user/user.repository.ts | 4 +- mock-server/src/global-types.d.ts | 13 ++ mock-server/src/index.ts | 2 + mock-server/src/pods/security/index.ts | 1 + .../src/pods/security/security.middlewares.ts | 32 ++++ .../src/pods/security/security.rest-api.ts | 49 +++++++ src/core/auth/api/auth.api-model.ts | 12 ++ src/core/auth/api/auth.api.ts | 13 ++ src/core/auth/api/index.ts | 2 + src/core/auth/auth.model.ts | 16 ++ src/core/auth/auth.provider.tsx | 42 ++++++ src/core/auth/auth.query.hook.ts | 52 +++++++ src/core/auth/index.ts | 3 + src/core/react-query/query-keys.ts | 7 + src/main.tsx | 10 +- src/modules/login/login.component.tsx | 39 ++--- src/modules/login/login.pod.tsx | 9 +- src/modules/login/login.vm.ts | 7 +- src/scenes/__root.tsx | 9 +- src/scenes/_auth.tsx | 13 +- 28 files changed, 447 insertions(+), 43 deletions(-) create mode 100644 mock-server/src/common/models/credentials.model.ts create mode 100644 mock-server/src/common/models/user-session.model.ts create mode 100644 mock-server/src/global-types.d.ts create mode 100644 mock-server/src/pods/security/index.ts create mode 100644 mock-server/src/pods/security/security.middlewares.ts create mode 100644 mock-server/src/pods/security/security.rest-api.ts create mode 100644 src/core/auth/api/auth.api-model.ts create mode 100644 src/core/auth/api/auth.api.ts create mode 100644 src/core/auth/api/index.ts create mode 100644 src/core/auth/auth.model.ts create mode 100644 src/core/auth/auth.provider.tsx create mode 100644 src/core/auth/auth.query.hook.ts create mode 100644 src/core/auth/index.ts diff --git a/mock-server/.env.example b/mock-server/.env.example index dc30bca..3d1261e 100644 --- a/mock-server/.env.example +++ b/mock-server/.env.example @@ -1,3 +1,4 @@ NODE_ENV=development PORT=3000 SIMULATED_DELAY=2000 +AUTH_SECRET=MY_AUTH_SECRET \ No newline at end of file diff --git a/mock-server/package-lock.json b/mock-server/package-lock.json index 4f7405f..9156c50 100644 --- a/mock-server/package-lock.json +++ b/mock-server/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", "mongodb": "^6.10.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", @@ -17,6 +18,7 @@ }, "devDependencies": { "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.9", "@types/prompts": "^2.4.9", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", @@ -823,6 +825,17 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -830,6 +843,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", @@ -1282,6 +1302,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1668,6 +1694,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2877,6 +2912,67 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3023,16 +3119,58 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", diff --git a/mock-server/package.json b/mock-server/package.json index afba141..f4e2db6 100644 --- a/mock-server/package.json +++ b/mock-server/package.json @@ -25,6 +25,7 @@ "license": "ISC", "dependencies": { "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", "mongodb": "^6.10.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", @@ -32,6 +33,7 @@ }, "devDependencies": { "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.9", "@types/prompts": "^2.4.9", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", diff --git a/mock-server/src/common/models/credentials.model.ts b/mock-server/src/common/models/credentials.model.ts new file mode 100644 index 0000000..5d34c46 --- /dev/null +++ b/mock-server/src/common/models/credentials.model.ts @@ -0,0 +1,4 @@ +export interface UserCredentials { + email: string; + contraseña: string; +} diff --git a/mock-server/src/common/models/index.ts b/mock-server/src/common/models/index.ts index a3be7da..36358b2 100644 --- a/mock-server/src/common/models/index.ts +++ b/mock-server/src/common/models/index.ts @@ -1,2 +1,4 @@ export * from './collection.model.js'; export * from './lookup.model.js'; +export * from './credentials.model.js'; +export * from './user-session.model.js'; diff --git a/mock-server/src/common/models/user-session.model.ts b/mock-server/src/common/models/user-session.model.ts new file mode 100644 index 0000000..208b2ab --- /dev/null +++ b/mock-server/src/common/models/user-session.model.ts @@ -0,0 +1,6 @@ +import { Lookup } from './lookup.model.js'; + +export interface UserSession { + id: string; + rol: Lookup; +} diff --git a/mock-server/src/core/constants/env.constants.ts b/mock-server/src/core/constants/env.constants.ts index 16e4f90..e16e6c5 100644 --- a/mock-server/src/core/constants/env.constants.ts +++ b/mock-server/src/core/constants/env.constants.ts @@ -2,4 +2,5 @@ export const ENV = { IS_PRODUCTION: process.env.NODE_ENV === 'production', PORT: Number(process.env.PORT), SIMULATED_DELAY: +process.env.SIMULATED_DELAY, + AUTH_SECRET: process.env.AUTH_SECRET, }; diff --git a/mock-server/src/dals/user/index.ts b/mock-server/src/dals/user/index.ts index 96a8a4e..fae8fe2 100644 --- a/mock-server/src/dals/user/index.ts +++ b/mock-server/src/dals/user/index.ts @@ -1 +1,2 @@ export * from './user.model.js'; +export * from './user.repository.js'; diff --git a/mock-server/src/dals/user/user.repository.ts b/mock-server/src/dals/user/user.repository.ts index 71de253..cbf3259 100644 --- a/mock-server/src/dals/user/user.repository.ts +++ b/mock-server/src/dals/user/user.repository.ts @@ -1,5 +1,5 @@ import { paginateItems } from '#common/helpers/index.js'; -import { CollectionQuery } from '#common/models/index.js'; +import { CollectionQuery, UserCredentials } from '#common/models/index.js'; import { db } from '#dals/mock.data.js'; import * as model from './user.model.js'; @@ -27,4 +27,6 @@ export const userRepository = { } return index !== -1; }, + getUserByCredentials: async (userCredentials: UserCredentials) => + db.users.find(user => user.email === userCredentials.email && user.contraseña === userCredentials.contraseña), }; diff --git a/mock-server/src/global-types.d.ts b/mock-server/src/global-types.d.ts new file mode 100644 index 0000000..a1b037b --- /dev/null +++ b/mock-server/src/global-types.d.ts @@ -0,0 +1,13 @@ +declare namespace Express { + export interface UserSession { + id: string; + rol: { + id: string; + nombre: string; + }; + } + + export interface Request { + userSession?: UserSession; + } +} diff --git a/mock-server/src/index.ts b/mock-server/src/index.ts index 6575fe8..bb5a38e 100644 --- a/mock-server/src/index.ts +++ b/mock-server/src/index.ts @@ -5,6 +5,7 @@ import { createRestApiServer } from '#core/servers/index.js'; import { userApi } from '#pods/user/index.js'; import { expedienteApi } from '#pods/expediente/index.js'; import { lookupApi } from '#pods/lookup/index.js'; +import { securityApi } from '#pods/security/security.rest-api.js'; const app = createRestApiServer(); @@ -12,6 +13,7 @@ app.use(logRequestMiddleware(logger)); app.use('/api/user', userApi); app.use('/api/lookup', lookupApi); +app.use('/api/security', securityApi); app.use('/api/expediente', expedienteApi); diff --git a/mock-server/src/pods/security/index.ts b/mock-server/src/pods/security/index.ts new file mode 100644 index 0000000..c58b956 --- /dev/null +++ b/mock-server/src/pods/security/index.ts @@ -0,0 +1 @@ +export * from './security.rest-api.js'; diff --git a/mock-server/src/pods/security/security.middlewares.ts b/mock-server/src/pods/security/security.middlewares.ts new file mode 100644 index 0000000..375225b --- /dev/null +++ b/mock-server/src/pods/security/security.middlewares.ts @@ -0,0 +1,32 @@ +import { RequestHandler } from 'express'; +import jwt from 'jsonwebtoken'; +import { UserSession } from '#common/models/index.js'; +import { ENV } from '#core/constants/index.js'; + +const verify = (token: string, secret: string): Promise => + new Promise((resolve, reject) => { + jwt.verify(token, secret, (error, userSession: UserSession) => { + if (error) { + reject(error); + } + + if (userSession) { + resolve(userSession); + } else { + reject(); + } + }); + }); + +export const authenticationMiddleware: RequestHandler = async (req, res, next) => { + try { + const [, token] = req.cookies.authorization?.split(' ') || []; + const userSession = await verify(token, ENV.AUTH_SECRET); + req.userSession = userSession; + next(); + } catch (error) { + console.error(error); + res.clearCookie('authorization'); + res.sendStatus(401); + } +}; diff --git a/mock-server/src/pods/security/security.rest-api.ts b/mock-server/src/pods/security/security.rest-api.ts new file mode 100644 index 0000000..f9dd92a --- /dev/null +++ b/mock-server/src/pods/security/security.rest-api.ts @@ -0,0 +1,49 @@ +import { Router } from 'express'; +import jwt from 'jsonwebtoken'; +import { UserCredentials, UserSession } from '#common/models/index.js'; +import { userRepository } from '#dals/user/index.js'; +import { authenticationMiddleware } from './security.middlewares.js'; + +export const securityApi = Router(); + +securityApi + .post('/login', async (req, res, next) => { + try { + const body = req.body as UserCredentials; + const user = await userRepository.getUserByCredentials(body); + + if (user) { + const userSession: UserSession = { + id: user._id.toHexString(), + rol: user.rol, + }; + // TODO: MOVE secret to .env + const token = jwt.sign(userSession, 'patata', { + expiresIn: '1d', + algorithm: 'HS256', + }); + res.cookie('authorization', `Bearer ${token}`, { + httpOnly: true, + }); + res.sendStatus(204); + } else { + res.sendStatus(401); + } + } catch (error) { + next(error); + } + }) + .get('/whoami', authenticationMiddleware, async (req, res, next) => { + try { + const user = await userRepository.getUserById(req.userSession?.id); + + if (user) { + res.status(200).send({ id: user._id.toHexString(), nombre: user.nombre, rol: user.rol }); + } else { + res.clearCookie('authorization'); + res.sendStatus(401); + } + } catch (error) { + next(error); + } + }); diff --git a/src/core/auth/api/auth.api-model.ts b/src/core/auth/api/auth.api-model.ts new file mode 100644 index 0000000..4d06047 --- /dev/null +++ b/src/core/auth/api/auth.api-model.ts @@ -0,0 +1,12 @@ +import { Lookup } from '#common/models'; + +export interface UserCredentials { + email: string; + contraseña: string; +} + +export interface User { + id: string; + nombre: string; + rol: Lookup; +} diff --git a/src/core/auth/api/auth.api.ts b/src/core/auth/api/auth.api.ts new file mode 100644 index 0000000..55801c0 --- /dev/null +++ b/src/core/auth/api/auth.api.ts @@ -0,0 +1,13 @@ +// login, logout, whoami(me) +import axios from 'axios'; +import { User, UserCredentials } from './auth.api-model'; + +export const login = async (userCredentials: UserCredentials): Promise => { + const response = await axios.post('/api/security/login', userCredentials); + return response.data; +}; + +export const whoami = async (): Promise => { + const response = await axios.get('/api/security/whoami', { withCredentials: true }); + return response.data; +}; diff --git a/src/core/auth/api/index.ts b/src/core/auth/api/index.ts new file mode 100644 index 0000000..db154de --- /dev/null +++ b/src/core/auth/api/index.ts @@ -0,0 +1,2 @@ +export * from './auth.api'; +export * from './auth.api-model'; diff --git a/src/core/auth/auth.model.ts b/src/core/auth/auth.model.ts new file mode 100644 index 0000000..5a7bc95 --- /dev/null +++ b/src/core/auth/auth.model.ts @@ -0,0 +1,16 @@ +import { User, UserCredentials } from './api/auth.api-model'; + +export interface AuthContextModel { + isAuthenticated: boolean; + user: User; + doLogin: (UserCredentials: UserCredentials) => Promise; +} + +export const createEmptyUser = (): User => ({ + id: '', + nombre: '', + rol: { + id: '', + nombre: '', + }, +}); diff --git a/src/core/auth/auth.provider.tsx b/src/core/auth/auth.provider.tsx new file mode 100644 index 0000000..3390912 --- /dev/null +++ b/src/core/auth/auth.provider.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { AuthContextModel } from './auth.model'; +import { useLoginMutation, useWhoamiQuery } from './auth.query.hook'; + +const AuthContext = React.createContext(null); + +interface Props { + children: React.ReactNode; +} + +export const AuthProvider: React.FC = props => { + const { children } = props; + + const { doLogin } = useLoginMutation(); + const { user, isAuthenticated, isLoading } = useWhoamiQuery(); + + const isReady = !isLoading; + + if (!isReady) { + return null; + } + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextModel => { + const context = React.useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/core/auth/auth.query.hook.ts b/src/core/auth/auth.query.hook.ts new file mode 100644 index 0000000..cf1d7be --- /dev/null +++ b/src/core/auth/auth.query.hook.ts @@ -0,0 +1,52 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { UserCredentials, login, User, whoami } from './api'; +import { createEmptyUser } from './auth.model'; +import { authQueryKeys } from '../react-query'; + +interface LoginMutationResult { + doLogin: (userCredentials: UserCredentials) => Promise; + isPending: boolean; +} + +export const useLoginMutation = (): LoginMutationResult => { + const { mutateAsync: doLogin, isPending } = useMutation({ + mutationFn: (userCredentials: UserCredentials) => login(userCredentials), + onSuccess: () => { + // TODO: EJERCUTAR EL WHOAMI + }, + }); + + return { + doLogin, + isPending, + }; +}; + +interface UseWhoamIQueryResult { + user: User; + isAuthenticated: boolean; + isLoading: boolean; +} + +export const useWhoamiQuery = (): UseWhoamIQueryResult => { + const { + data: user = createEmptyUser(), + isLoading, + isError, + } = useQuery({ + queryKey: authQueryKeys.whoami(), + queryFn: async () => { + const user = await whoami(); + return user; + }, + retry: false, + }); + + const isAuthenticated = !isError && Boolean(user?.id); + + return { + user, + isAuthenticated, + isLoading, + }; +}; diff --git a/src/core/auth/index.ts b/src/core/auth/index.ts new file mode 100644 index 0000000..917e2e7 --- /dev/null +++ b/src/core/auth/index.ts @@ -0,0 +1,3 @@ +export * from './auth.provider'; +export * from './auth.model'; +export * from './api'; diff --git a/src/core/react-query/query-keys.ts b/src/core/react-query/query-keys.ts index 24147ec..bda993a 100644 --- a/src/core/react-query/query-keys.ts +++ b/src/core/react-query/query-keys.ts @@ -18,3 +18,10 @@ export const certificacionesQueryKeys = { all: ['certificaciones'], certificacionCollection: (page?: number, pageSize?: number) => ['certificaciones', page, pageSize], }; + +export const authQueryKeys = { + all: ['auth'], + doLogin: () => ['auth', 'doLogin'], + // todo: add doLogout + whoami: () => ['auth', 'whoami'], +}; diff --git a/src/main.tsx b/src/main.tsx index f824cb0..bd1c154 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,21 +2,25 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { AuthProvider, useAuth } from '#core/auth'; import { RouterProvider } from '@tanstack/react-router'; import { queryClient } from './core/react-query'; import { router } from './core/router'; import { ThemeProvider } from './core/theme'; const App = () => { - return ; + const auth = useAuth(); + return ; }; createRoot(document.getElementById('root')!).render( - - + + + + diff --git a/src/modules/login/login.component.tsx b/src/modules/login/login.component.tsx index 3961cfb..39b64eb 100644 --- a/src/modules/login/login.component.tsx +++ b/src/modules/login/login.component.tsx @@ -1,42 +1,28 @@ import React from 'react'; import { Button, IconButton, Paper, TableContainer, Typography } from '@mui/material'; -import { useToggle } from '#common/hooks'; import { Visibility, VisibilityOff } from '@mui/icons-material'; import { Form, Formik } from 'formik'; -import { createEmptyCredenciales, Credenciales } from './login.vm'; import { NavigationButton, TextFieldForm } from '#common/components'; +import { useToggle } from '#common/hooks'; +import { UserCredentials } from '#core/auth'; +import { createEmptyCredenciales } from './login.vm'; import { formValidation } from './validations/login.validations.ts'; -// import { db } from '../../../mock-server/src/dals/mock.data.ts'; -import { useNavigate } from '@tanstack/react-router'; import * as classes from './login.styles'; -export const Login: React.FC = () => { - const { isOpen: showPassword, onToggle } = useToggle(false); - const [loginError, setLoginError] = React.useState(false); - const navigate = useNavigate(); +interface Props { + onSubmit: (userCredentials: UserCredentials) => void; +} - const handleSubmit = (credenciales: Credenciales) => { - const { email, contraseña } = credenciales; - // const usuario = db.users.find(user => user.email === email && user.contraseña === contraseña); - const usuario = email === 'carlos.gonzalez@example.com' && contraseña === 'carlos123'; - if (usuario) { - setLoginError(false); - navigate({ to: '/expedientes' }); - } else { - setLoginError(true); - } - }; +export const Login: React.FC = props => { + const { onSubmit } = props; + const { isOpen: showPassword, onToggle } = useToggle(false); return (
Inicia sesión en tu cuenta
- + {() => (
@@ -53,11 +39,6 @@ export const Login: React.FC = () => { }} /> - {loginError && ( - - Email o contraseña incorrectos - - )} diff --git a/src/modules/login/login.pod.tsx b/src/modules/login/login.pod.tsx index 16e8beb..8ecb0b9 100644 --- a/src/modules/login/login.pod.tsx +++ b/src/modules/login/login.pod.tsx @@ -1,7 +1,12 @@ import React from 'react'; - +import { useAuth } from '#core/auth'; import { Login } from './login.component'; +import { UserCredentials } from '#core/auth'; export const LoginPod: React.FC = () => { - return ; + const { doLogin } = useAuth(); + + const handleSubmit = (userCredentials: UserCredentials) => doLogin(userCredentials); + + return ; }; diff --git a/src/modules/login/login.vm.ts b/src/modules/login/login.vm.ts index fb83973..1f83d2e 100644 --- a/src/modules/login/login.vm.ts +++ b/src/modules/login/login.vm.ts @@ -1,9 +1,6 @@ -export interface Credenciales { - email: string; - contraseña: string; -} +import { UserCredentials } from '#core/auth'; -export const createEmptyCredenciales = (): Credenciales => ({ +export const createEmptyCredenciales = (): UserCredentials => ({ email: '', contraseña: '', }); diff --git a/src/scenes/__root.tsx b/src/scenes/__root.tsx index b3267fb..80032ec 100644 --- a/src/scenes/__root.tsx +++ b/src/scenes/__root.tsx @@ -1,8 +1,13 @@ import * as React from 'react'; -import { Outlet, createRootRoute } from '@tanstack/react-router'; +import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'; import { TanStackRouterDevtools } from '#core/router/router.dev-tools'; +import { AuthContextModel } from '#core/auth'; -export const Route = createRootRoute({ +interface Context { + auth: AuthContextModel; +} + +export const Route = createRootRouteWithContext()({ component: () => { return ( <> diff --git a/src/scenes/_auth.tsx b/src/scenes/_auth.tsx index 92c8de7..1ec9caf 100644 --- a/src/scenes/_auth.tsx +++ b/src/scenes/_auth.tsx @@ -1,7 +1,18 @@ -import { createFileRoute, Outlet } from '@tanstack/react-router'; +import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; import { AppLayout } from '#layouts/app.layout'; export const Route = createFileRoute('/_auth')({ + beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ + to: '/login', + }); + } else { + throw redirect({ + to: '/expedientes', + }); + } + }, component: () => { return ( From bac7a5447a57dba0a1ac205b04faa2194c841c79 Mon Sep 17 00:00:00 2001 From: manudous Date: Mon, 12 May 2025 15:13:10 +0200 Subject: [PATCH 08/14] feat: update authentication flow and improve route handling with cookie-parser integration --- mock-server/package.json | 3 +- .../src/core/servers/rest-api.server.ts | 2 + .../src/pods/security/security.rest-api.ts | 14 +- package-lock.json | 138 ++++++++++++++++++ src/core/auth/auth.query.hook.ts | 4 +- src/scenes/_auth.tsx | 4 - src/scenes/index.tsx | 6 +- src/scenes/login.tsx | 9 +- 8 files changed, 167 insertions(+), 13 deletions(-) diff --git a/mock-server/package.json b/mock-server/package.json index f4e2db6..6a8cbd9 100644 --- a/mock-server/package.json +++ b/mock-server/package.json @@ -32,7 +32,8 @@ "winston": "^3.16.0" }, "devDependencies": { - "@types/express": "^5.0.0", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^5.0.1", "@types/jsonwebtoken": "^9.0.9", "@types/prompts": "^2.4.9", "@types/swagger-jsdoc": "^6.0.4", diff --git a/mock-server/src/core/servers/rest-api.server.ts b/mock-server/src/core/servers/rest-api.server.ts index abd344e..b7d1fa9 100644 --- a/mock-server/src/core/servers/rest-api.server.ts +++ b/mock-server/src/core/servers/rest-api.server.ts @@ -1,9 +1,11 @@ import { ENV } from '#core/constants/env.constants.js'; +import cookieParser from 'cookie-parser'; import express from 'express'; export const createRestApiServer = () => { const app = express(); app.use(express.json()); + app.use(cookieParser()); app.use((req, res, next) => { setTimeout(next, ENV.SIMULATED_DELAY || 2000); }); diff --git a/mock-server/src/pods/security/security.rest-api.ts b/mock-server/src/pods/security/security.rest-api.ts index f9dd92a..65f721f 100644 --- a/mock-server/src/pods/security/security.rest-api.ts +++ b/mock-server/src/pods/security/security.rest-api.ts @@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken'; import { UserCredentials, UserSession } from '#common/models/index.js'; import { userRepository } from '#dals/user/index.js'; import { authenticationMiddleware } from './security.middlewares.js'; +import { ENV } from '#core/constants/env.constants.js'; export const securityApi = Router(); @@ -17,8 +18,7 @@ securityApi id: user._id.toHexString(), rol: user.rol, }; - // TODO: MOVE secret to .env - const token = jwt.sign(userSession, 'patata', { + const token = jwt.sign(userSession, ENV.AUTH_SECRET, { expiresIn: '1d', algorithm: 'HS256', }); @@ -28,6 +28,7 @@ securityApi res.sendStatus(204); } else { res.sendStatus(401); + res.clearCookie('authorization'); } } catch (error) { next(error); @@ -40,10 +41,17 @@ securityApi if (user) { res.status(200).send({ id: user._id.toHexString(), nombre: user.nombre, rol: user.rol }); } else { - res.clearCookie('authorization'); res.sendStatus(401); } } catch (error) { next(error); } + }) + .post('/logout', authenticationMiddleware, async (req, res, next) => { + try { + res.clearCookie('authorization'); + res.sendStatus(204); + } catch (error) { + next(error); + } }); diff --git a/package-lock.json b/package-lock.json index 5519dbd..9caa7be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@tanstack/react-router": "^1.82.1", "@tanstack/react-table": "^8.20.6", "axios": "^1.7.7", + "cookie-parser": "^1.4.7", "formik": "^2.4.6", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -29,6 +30,8 @@ "@tanstack/react-query-devtools": "^5.63.0", "@tanstack/router-devtools": "^1.82.1", "@tanstack/router-plugin": "^1.81.9", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^5.0.1", "@types/jest": "^29.5.14", "@types/node": "^22.9.0", "@types/react": "^18.3.12", @@ -2099,6 +2102,37 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2106,6 +2140,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", @@ -2115,6 +2174,13 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2156,6 +2222,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", @@ -2177,6 +2250,20 @@ "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", @@ -2206,6 +2293,29 @@ "@types/react": "*" } }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3620,6 +3730,34 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", diff --git a/src/core/auth/auth.query.hook.ts b/src/core/auth/auth.query.hook.ts index cf1d7be..33da914 100644 --- a/src/core/auth/auth.query.hook.ts +++ b/src/core/auth/auth.query.hook.ts @@ -1,7 +1,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { UserCredentials, login, User, whoami } from './api'; import { createEmptyUser } from './auth.model'; -import { authQueryKeys } from '../react-query'; +import { authQueryKeys, queryClient } from '../react-query'; interface LoginMutationResult { doLogin: (userCredentials: UserCredentials) => Promise; @@ -12,7 +12,7 @@ export const useLoginMutation = (): LoginMutationResult => { const { mutateAsync: doLogin, isPending } = useMutation({ mutationFn: (userCredentials: UserCredentials) => login(userCredentials), onSuccess: () => { - // TODO: EJERCUTAR EL WHOAMI + queryClient.removeQueries({ queryKey: authQueryKeys.whoami() }); }, }); diff --git a/src/scenes/_auth.tsx b/src/scenes/_auth.tsx index 1ec9caf..caab146 100644 --- a/src/scenes/_auth.tsx +++ b/src/scenes/_auth.tsx @@ -7,10 +7,6 @@ export const Route = createFileRoute('/_auth')({ throw redirect({ to: '/login', }); - } else { - throw redirect({ - to: '/expedientes', - }); } }, component: () => { diff --git a/src/scenes/index.tsx b/src/scenes/index.tsx index 1c159ab..f1ab086 100644 --- a/src/scenes/index.tsx +++ b/src/scenes/index.tsx @@ -1,7 +1,9 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ - beforeLoad: () => { - throw redirect({ to: '/login' }); + beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }); + } }, }); diff --git a/src/scenes/login.tsx b/src/scenes/login.tsx index 8b1b2aa..65a1186 100644 --- a/src/scenes/login.tsx +++ b/src/scenes/login.tsx @@ -1,8 +1,15 @@ -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute, redirect } from '@tanstack/react-router'; import { AuthLayout } from '#layouts/'; import { LoginPod } from '#modules/login'; export const Route = createFileRoute('/login')({ + beforeLoad: ({ context }) => { + if (context.auth.isAuthenticated) { + throw redirect({ + to: '/expedientes', + }); + } + }, component: () => { return ( From b87d6068ce168a37058f9e3a72c39a2a9c7ea43b Mon Sep 17 00:00:00 2001 From: Abel Date: Thu, 5 Jun 2025 11:38:40 +0200 Subject: [PATCH 09/14] chore: install cookie-parser in mock server --- mock-server/package-lock.json | 43 +++++++++-- mock-server/package.json | 1 + package-lock.json | 138 ---------------------------------- 3 files changed, 39 insertions(+), 143 deletions(-) diff --git a/mock-server/package-lock.json b/mock-server/package-lock.json index 9156c50..adcadd5 100644 --- a/mock-server/package-lock.json +++ b/mock-server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "cookie-parser": "^1.4.7", "express": "^4.21.1", "jsonwebtoken": "^9.0.2", "mongodb": "^6.10.0", @@ -17,7 +18,8 @@ "winston": "^3.16.0" }, "devDependencies": { - "@types/express": "^5.0.0", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^5.0.1", "@types/jsonwebtoken": "^9.0.9", "@types/prompts": "^2.4.9", "@types/swagger-jsdoc": "^6.0.4", @@ -781,6 +783,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -788,15 +800,14 @@ "dev": true }, "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, @@ -1521,6 +1532,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/mock-server/package.json b/mock-server/package.json index 6a8cbd9..66821ea 100644 --- a/mock-server/package.json +++ b/mock-server/package.json @@ -24,6 +24,7 @@ "author": "", "license": "ISC", "dependencies": { + "cookie-parser": "^1.4.7", "express": "^4.21.1", "jsonwebtoken": "^9.0.2", "mongodb": "^6.10.0", diff --git a/package-lock.json b/package-lock.json index 9caa7be..5519dbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "@tanstack/react-router": "^1.82.1", "@tanstack/react-table": "^8.20.6", "axios": "^1.7.7", - "cookie-parser": "^1.4.7", "formik": "^2.4.6", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -30,8 +29,6 @@ "@tanstack/react-query-devtools": "^5.63.0", "@tanstack/router-devtools": "^1.82.1", "@tanstack/router-plugin": "^1.81.9", - "@types/cookie-parser": "^1.4.8", - "@types/express": "^5.0.1", "@types/jest": "^29.5.14", "@types/node": "^22.9.0", "@types/react": "^18.3.12", @@ -2102,37 +2099,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookie-parser": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", - "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/express": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2140,31 +2106,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", - "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", @@ -2174,13 +2115,6 @@ "hoist-non-react-statics": "^3.3.0" } }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2222,13 +2156,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", @@ -2250,20 +2177,6 @@ "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "license": "MIT" }, - "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", @@ -2293,29 +2206,6 @@ "@types/react": "*" } }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3730,34 +3620,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", From cbf484ba3f35bd5f9f0fd60d95e17a9bdc11f346 Mon Sep 17 00:00:00 2001 From: Abel Date: Thu, 5 Jun 2025 12:53:48 +0200 Subject: [PATCH 10/14] chore: add apellido to whoami response --- mock-server/src/pods/security/security.rest-api.ts | 4 +++- src/core/auth/api/auth.api-model.ts | 1 + src/core/auth/auth.model.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mock-server/src/pods/security/security.rest-api.ts b/mock-server/src/pods/security/security.rest-api.ts index 65f721f..0a30a16 100644 --- a/mock-server/src/pods/security/security.rest-api.ts +++ b/mock-server/src/pods/security/security.rest-api.ts @@ -39,7 +39,9 @@ securityApi const user = await userRepository.getUserById(req.userSession?.id); if (user) { - res.status(200).send({ id: user._id.toHexString(), nombre: user.nombre, rol: user.rol }); + res + .status(200) + .send({ id: user._id.toHexString(), nombre: user.nombre, apellido: user.apellido, rol: user.rol }); } else { res.sendStatus(401); } diff --git a/src/core/auth/api/auth.api-model.ts b/src/core/auth/api/auth.api-model.ts index 4d06047..f36cdf5 100644 --- a/src/core/auth/api/auth.api-model.ts +++ b/src/core/auth/api/auth.api-model.ts @@ -8,5 +8,6 @@ export interface UserCredentials { export interface User { id: string; nombre: string; + apellido: string; rol: Lookup; } diff --git a/src/core/auth/auth.model.ts b/src/core/auth/auth.model.ts index 5a7bc95..2954cfb 100644 --- a/src/core/auth/auth.model.ts +++ b/src/core/auth/auth.model.ts @@ -9,6 +9,7 @@ export interface AuthContextModel { export const createEmptyUser = (): User => ({ id: '', nombre: '', + apellido: '', rol: { id: '', nombre: '', From 8c1a2a94b6d41ee77d2f3fb60b797a544df26221 Mon Sep 17 00:00:00 2001 From: Abel Date: Thu, 5 Jun 2025 12:57:44 +0200 Subject: [PATCH 11/14] feat: display user initials --- src/common/components/appbar/appbar.component.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/common/components/appbar/appbar.component.tsx b/src/common/components/appbar/appbar.component.tsx index 90fc9c2..c9a60f0 100644 --- a/src/common/components/appbar/appbar.component.tsx +++ b/src/common/components/appbar/appbar.component.tsx @@ -5,6 +5,7 @@ import MenuIcon from '@mui/icons-material/Menu'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; import * as classes from './appbar.styles'; +import { useAuth } from '../../../core/auth'; interface AppBarProps { isDrawerOpen: boolean; @@ -14,6 +15,14 @@ interface AppBarProps { export const AppBar: React.FC = props => { const { isDrawerOpen, onToggleDrawer } = props; + const { user } = useAuth(); + + const getInitials = (nombre: string, apellido: string): string => { + const first = nombre?.trim()?.[0] ?? ''; + const last = apellido?.trim()?.[0] ?? ''; + return `${first}${last}`.toUpperCase(); + }; + return ( @@ -25,7 +34,7 @@ export const AppBar: React.FC = props => { GEX
- PM + {getInitials(user.nombre, user.apellido)} ); From 2855aa558235bf778968f5a834b3428f26a2597f Mon Sep 17 00:00:00 2001 From: Abel Date: Thu, 5 Jun 2025 17:45:08 +0200 Subject: [PATCH 12/14] feat: add avatar menu --- .../components/appbar/appbar.component.tsx | 78 ++++++++++++++----- src/layouts/app.layout.tsx | 17 +++- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/common/components/appbar/appbar.component.tsx b/src/common/components/appbar/appbar.component.tsx index c9a60f0..a74d0d9 100644 --- a/src/common/components/appbar/appbar.component.tsx +++ b/src/common/components/appbar/appbar.component.tsx @@ -1,21 +1,38 @@ -import React from 'react'; - -import { AppBar as MUIAppbar, IconButton, Toolbar, Typography, Avatar } from '@mui/material'; +import React, { useRef } from 'react'; +import { + AppBar as MUIAppbar, + IconButton, + Toolbar, + Typography, + Avatar, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Paper, + Popper, + ClickAwayListener, + ListItem, +} from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; - +import LogoutIcon from '@mui/icons-material/Logout'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import * as classes from './appbar.styles'; import { useAuth } from '../../../core/auth'; interface AppBarProps { isDrawerOpen: boolean; + isListOpen: boolean; onToggleDrawer: () => void; + onAvatarMenuAction: (action: 'toggle' | 'close') => void; } export const AppBar: React.FC = props => { - const { isDrawerOpen, onToggleDrawer } = props; + const { isDrawerOpen, isListOpen, onToggleDrawer, onAvatarMenuAction } = props; const { user } = useAuth(); + const avatarRef = useRef(null); const getInitials = (nombre: string, apellido: string): string => { const first = nombre?.trim()?.[0] ?? ''; @@ -24,18 +41,43 @@ export const AppBar: React.FC = props => { }; return ( - - -
- - {isDrawerOpen ? : } - - - GEX - -
- {getInitials(user.nombre, user.apellido)} -
-
+ <> + + +
+ + {isDrawerOpen ? : } + + + GEX + +
+ onAvatarMenuAction('toggle')} sx={{ cursor: 'pointer' }}> + {getInitials(user.nombre, user.apellido)} + +
+
+ + + onAvatarMenuAction('close')}> + + + + + + + + + console.log('Logout')}> + + + + + + + + + + ); }; diff --git a/src/layouts/app.layout.tsx b/src/layouts/app.layout.tsx index 73cf5f3..98980eb 100644 --- a/src/layouts/app.layout.tsx +++ b/src/layouts/app.layout.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { AppBar, Drawer, SidebarMenu } from '#common/components'; import { useWithTheme } from '#core/theme'; import * as appLayoutClasses from './app.styles'; +import { useToggle } from '#common/hooks/toogle.hook.ts'; interface Props { children: React.ReactNode; @@ -10,13 +11,23 @@ interface Props { export const AppLayout: React.FC = props => { const { children } = props; const classes = useWithTheme(appLayoutClasses); - const [isDrawerOpen, toggleDrawer] = React.useState(false); - const handleToggleDrawer = () => toggleDrawer(!isDrawerOpen); + const { isOpen: isDrawerOpen, onToggle: toggleDrawer } = useToggle(false); + const { isOpen: isListOpen, onToggle: toggleList, setIsOpen: setListOpen } = useToggle(false); + + const handleAvatarMenu = (action: 'toggle' | 'close') => { + if (action === 'toggle') toggleList(); + if (action === 'close') setListOpen(false); + }; return (
- +
From 9ee4effaf06d156b933de323f023f4b380517dcb Mon Sep 17 00:00:00 2001 From: Abel Date: Thu, 5 Jun 2025 18:20:16 +0200 Subject: [PATCH 13/14] refactor: extract AvatarMenu from AppBar into separate component --- .../components/appbar/appbar.component.tsx | 46 ++++-------------- .../avatar-menu/avatar-menu.component.tsx | 48 +++++++++++++++++++ src/common/components/avatar-menu/index.ts | 1 + 3 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 src/common/components/avatar-menu/avatar-menu.component.tsx create mode 100644 src/common/components/avatar-menu/index.ts diff --git a/src/common/components/appbar/appbar.component.tsx b/src/common/components/appbar/appbar.component.tsx index a74d0d9..82eae17 100644 --- a/src/common/components/appbar/appbar.component.tsx +++ b/src/common/components/appbar/appbar.component.tsx @@ -1,25 +1,10 @@ import React, { useRef } from 'react'; -import { - AppBar as MUIAppbar, - IconButton, - Toolbar, - Typography, - Avatar, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Paper, - Popper, - ClickAwayListener, - ListItem, -} from '@mui/material'; +import { AppBar as MUIAppbar, IconButton, Toolbar, Typography, Avatar } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; -import LogoutIcon from '@mui/icons-material/Logout'; -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import * as classes from './appbar.styles'; import { useAuth } from '../../../core/auth'; +import { AvatarMenu } from '../avatar-menu'; interface AppBarProps { isDrawerOpen: boolean; @@ -58,26 +43,13 @@ export const AppBar: React.FC = props => { - - onAvatarMenuAction('close')}> - - - - - - - - - console.log('Logout')}> - - - - - - - - - + onAvatarMenuAction('close')} + userName={`${user.nombre} ${user.apellido}`} + onLogout={() => console.log('Logout')} + /> ); }; diff --git a/src/common/components/avatar-menu/avatar-menu.component.tsx b/src/common/components/avatar-menu/avatar-menu.component.tsx new file mode 100644 index 0000000..9840e06 --- /dev/null +++ b/src/common/components/avatar-menu/avatar-menu.component.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + Popper, + ClickAwayListener, + Paper, + List, + ListItem, + ListItemIcon, + ListItemText, + ListItemButton, +} from '@mui/material'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import LogoutIcon from '@mui/icons-material/Logout'; + +interface Props { + anchorEl: HTMLElement | null; + open: boolean; + onClose: () => void; + userName: string; + onLogout: () => void; +} + +export const AvatarMenu: React.FC = props => { + const { anchorEl, open, onClose, userName, onLogout } = props; + + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/common/components/avatar-menu/index.ts b/src/common/components/avatar-menu/index.ts new file mode 100644 index 0000000..45cda8b --- /dev/null +++ b/src/common/components/avatar-menu/index.ts @@ -0,0 +1 @@ +export * from './avatar-menu.component'; From 522c1fd240f0be2ac7c8f72876c4eb1584a86e41 Mon Sep 17 00:00:00 2001 From: Abel Date: Thu, 5 Jun 2025 19:40:55 +0200 Subject: [PATCH 14/14] feat: add logout --- src/common/components/appbar/appbar.component.tsx | 5 +++-- src/core/auth/auth.model.ts | 1 + src/core/auth/auth.provider.tsx | 6 ++++++ src/layouts/app.layout.tsx | 13 ++++++++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/common/components/appbar/appbar.component.tsx b/src/common/components/appbar/appbar.component.tsx index 82eae17..3ead9f8 100644 --- a/src/common/components/appbar/appbar.component.tsx +++ b/src/common/components/appbar/appbar.component.tsx @@ -16,7 +16,8 @@ interface AppBarProps { export const AppBar: React.FC = props => { const { isDrawerOpen, isListOpen, onToggleDrawer, onAvatarMenuAction } = props; - const { user } = useAuth(); + const { user, logout } = useAuth(); + const avatarRef = useRef(null); const getInitials = (nombre: string, apellido: string): string => { @@ -48,7 +49,7 @@ export const AppBar: React.FC = props => { anchorEl={avatarRef.current} onClose={() => onAvatarMenuAction('close')} userName={`${user.nombre} ${user.apellido}`} - onLogout={() => console.log('Logout')} + onLogout={() => logout()} /> ); diff --git a/src/core/auth/auth.model.ts b/src/core/auth/auth.model.ts index 2954cfb..82c49b0 100644 --- a/src/core/auth/auth.model.ts +++ b/src/core/auth/auth.model.ts @@ -4,6 +4,7 @@ export interface AuthContextModel { isAuthenticated: boolean; user: User; doLogin: (UserCredentials: UserCredentials) => Promise; + logout: () => Promise; } export const createEmptyUser = (): User => ({ diff --git a/src/core/auth/auth.provider.tsx b/src/core/auth/auth.provider.tsx index 3390912..236b182 100644 --- a/src/core/auth/auth.provider.tsx +++ b/src/core/auth/auth.provider.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { AuthContextModel } from './auth.model'; import { useLoginMutation, useWhoamiQuery } from './auth.query.hook'; +import { authQueryKeys, queryClient } from '../react-query'; const AuthContext = React.createContext(null); @@ -13,6 +14,10 @@ export const AuthProvider: React.FC = props => { const { doLogin } = useLoginMutation(); const { user, isAuthenticated, isLoading } = useWhoamiQuery(); + const logout = async () => { + await fetch('/api/security/logout', { method: 'POST', credentials: 'include' }); + await queryClient.invalidateQueries({ queryKey: authQueryKeys.whoami() }); + }; const isReady = !isLoading; @@ -26,6 +31,7 @@ export const AuthProvider: React.FC = props => { user, isAuthenticated, doLogin, + logout, }} > {children} diff --git a/src/layouts/app.layout.tsx b/src/layouts/app.layout.tsx index 98980eb..3836dc1 100644 --- a/src/layouts/app.layout.tsx +++ b/src/layouts/app.layout.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { AppBar, Drawer, SidebarMenu } from '#common/components'; import { useWithTheme } from '#core/theme'; import * as appLayoutClasses from './app.styles'; import { useToggle } from '#common/hooks/toogle.hook.ts'; +import { useAuth } from '#core/auth'; +import { useNavigate } from '@tanstack/react-router'; interface Props { children: React.ReactNode; @@ -15,11 +17,20 @@ export const AppLayout: React.FC = props => { const { isOpen: isDrawerOpen, onToggle: toggleDrawer } = useToggle(false); const { isOpen: isListOpen, onToggle: toggleList, setIsOpen: setListOpen } = useToggle(false); + const { isAuthenticated } = useAuth(); + const navigate = useNavigate(); + const handleAvatarMenu = (action: 'toggle' | 'close') => { if (action === 'toggle') toggleList(); if (action === 'close') setListOpen(false); }; + useEffect(() => { + if (!isAuthenticated) { + navigate({ to: '/login' }); + } + }, [isAuthenticated, navigate]); + return (