diff --git a/.env b/.env index 02d0a3080242683127c4046ca9c08987b7fba4eb..20bfb0a74c8e88ae613364a837696f2fba800197 100644 --- a/.env +++ b/.env @@ -2,6 +2,7 @@ AUTH_SERVICE_HOSTNAME=auth-api USERS_SERVICE_HOSTNAME=users-api HOTELS_SERVICE_HOSTNAME=hotels-api BOOKINGS_SERVICE_HOSTNAME=bookings-api +ROOMS_BOOKING_SERVICE_HOSTNAME=rooms-booking-api DB_SERVICE_HOSTNAME=RoomsBooking-database DB_DATABASE_NAME=RoomsBooking DB_USER=user diff --git a/angular/RestClient/Dockerfile b/angular/RestClient/Dockerfile index 2e7bc5701936d528a696ce40b7c6214df7a35159..aaff9569e4c40a877c90fe14463a0e0d2e2d1e04 100644 --- a/angular/RestClient/Dockerfile +++ b/angular/RestClient/Dockerfile @@ -12,7 +12,7 @@ RUN npm install COPY . . # Compilar la aplicación Angular para producción -RUN npm run build -- --output-path=dist/app --configuration production +RUN npm run build -- --output-path=dist/app --c production # Etapa 2: Servidor Nginx para producción FROM nginx:alpine AS production @@ -21,7 +21,7 @@ FROM nginx:alpine AS production COPY --from=build /app/dist/app/browser /usr/share/nginx/html # Exponer el puerto 80 para Nginx -EXPOSE 7920 +EXPOSE 80 # Iniciar Nginx -CMD ["nginx", "-g", "daemon off;"] +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/angular/RestClient/angular.json b/angular/RestClient/angular.json index 32760665f49d122760236268ce4124bfaa0b6bde..6f0edcd86785afd8cc8db5af00a9e2da641eb1d0 100644 --- a/angular/RestClient/angular.json +++ b/angular/RestClient/angular.json @@ -36,6 +36,12 @@ }, "configurations": { "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], "budgets": [ { "type": "initial", @@ -50,22 +56,6 @@ ], "outputHashing": "all" }, - "monolith": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.monolith.ts" - } - ] - }, - "microservices": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.microservices.ts" - } - ] - }, "development": { "optimization": false, "extractLicenses": false, diff --git a/angular/RestClient/environments/environment.prod.ts b/angular/RestClient/environments/environment.prod.ts deleted file mode 100644 index e7abc07ec43af506db487532b2175c50676f3534..0000000000000000000000000000000000000000 --- a/angular/RestClient/environments/environment.prod.ts +++ /dev/null @@ -1,9 +0,0 @@ -const monolithUrl = 'http://room-booking:8080'; - -export const environment = { - production: true, - authAPI: 'http://auth-api:8101', - userAPI: `http://${monolithUrl}`, - hotelAPI: `http://${monolithUrl}`, - bookingAPI: `http://${monolithUrl}`, -}; diff --git a/angular/RestClient/src/app/app.config.ts b/angular/RestClient/src/app/app.config.ts index a7435caa914532ca9a3c5f085f6b02b3f36aa603..c0664f634af159df0208b1a877011ce66c1471ac 100644 --- a/angular/RestClient/src/app/app.config.ts +++ b/angular/RestClient/src/app/app.config.ts @@ -9,7 +9,7 @@ import { import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { ReactiveFormsModule } from '@angular/forms'; // Added import for ReactiveFormsModule import { provideNativeDateAdapter } from '@angular/material/core'; -import { authRequest } from './auth/auth.interceptor'; +import { authRequest } from './security/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ diff --git a/angular/RestClient/src/app/app.routes.ts b/angular/RestClient/src/app/app.routes.ts index 13e66397bddf5dafcd427e12591f6b682ab1f4d2..4be490fc5813f4e0bca3846bfd83e67c06ce0654 100644 --- a/angular/RestClient/src/app/app.routes.ts +++ b/angular/RestClient/src/app/app.routes.ts @@ -1,4 +1,4 @@ -import { Routes } from '@angular/router'; +import { Route, Routes } from '@angular/router'; import { HotelListComponent } from './core/features/hotel/hotel-list/hotel-list.component'; import { BookingComponent } from './core/features/bookings/booking/booking.component'; import { HotelRegisterComponent } from './core/features/hotel/hotel-register/hotel-register.component'; @@ -6,120 +6,179 @@ import { MainPageComponent } from './core/features/user/main-page/main-page.comp import { BookingListComponent } from './core/features/bookings/booking-list/booking-list.component'; import { UserBookingListComponent } from './core/features/user/user-booking-list/user-booking-list.component'; import { UserFormComponent } from './core/features/user/user-form/user-form.component'; +import { UnauthorizedComponent } from './page/unauthorized/unauthorized.component'; +import { rolGuard } from './security/rol.guard'; +import { UserRol, UserRolesArray } from './types'; -import { LoginComponent } from './core/features/auth/login/login.component'; -import { UserFormComponent } from './core/features/user/user-form/user-form.component'; +interface RouteData { + expectedRole: UserRol | UserRol[]; +} -export const routes: Routes = [ - { - path: '', // Ruta principal - component: MainPageComponent, - }, - // auth +type AppRoute = Omit<Route, 'data'> & { + data?: RouteData; +}; + +export const routes: AppRoute[] = [ + // Auth { path: 'login', - component: LoginComponent, + component: UserFormComponent, }, { path: 'register', + component: UserFormComponent, }, + // Hoteles { path: 'hotels', // Ruta para la lista de hoteles + component: HotelListComponent, }, { path: 'hotels/register', // Registrar nuevo hotel + component: HotelRegisterComponent, + canActivate: [rolGuard], + data: { expectedRole: 'HOTEL_ADMIN' }, }, { path: 'hotels/:id', // Hotel concreto + component: HotelRegisterComponent, }, - { - path: 'hotels/:id/edit', // Modificar hotel - }, + // Usuario { path: 'me', // Main + canActivate: [rolGuard], + data: { expectedRole: UserRolesArray }, + component: UserFormComponent, }, { path: 'me/edit', // Main + component: UserFormComponent, + canActivate: [rolGuard], + data: { expectedRole: UserRolesArray }, + }, + { + path: 'me/change-passwd', // Main + component: UserFormComponent, + canActivate: [rolGuard], + data: { expectedRole: UserRolesArray }, }, // Usuario HOTEL admin { path: 'me/hotels', + component: HotelListComponent, + canActivate: [rolGuard], + data: { expectedRole: 'HOTEL_ADMIN' }, }, { path: 'me/hotels/:id', + component: HotelRegisterComponent, + canActivate: [rolGuard], + data: { expectedRole: 'HOTEL_ADMIN' }, }, - { - path: 'me/hotels/:id/bookings', - }, - { - path: 'me/hotels/:id/rooms', - }, - { - path: 'me/hotels/:id/rooms/:id', - }, - { - path: 'me/hotels/:id/rooms/:id/bookings', - }, + // { + // path: 'me/hotels/:id/bookings', + // component: BookingListComponent, + // }, + // { + // path: 'me/hotels/:id/rooms/:id/bookings', + // component: BookingListComponent, + // }, + // Usuario Cliente { path: 'me/bookings', + component: UserBookingListComponent, + canActivate: [rolGuard], + data: { expectedRole: 'CLIENT' }, }, { path: 'me/bookings/:id', + component: BookingComponent, + canActivate: [rolGuard], + data: { expectedRole: 'CLIENT' }, }, { path: 'me/bookings/new', + component: BookingComponent, + canActivate: [rolGuard], + data: { expectedRole: 'CLIENT' }, }, + // Administrador { path: 'admin', // Main + component: UserFormComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, }, { path: 'admin/users', // Main + component: MainPageComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, }, { path: 'admin/users/:id', // Main + component: UserFormComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, + }, + { + path: 'admin/users/:id/edit', // Main + component: UserFormComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, + }, + { + path: 'admin/users/:id/change-passwd', // Main + component: UserFormComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, + }, + { + path: 'admin/users/:id/bookings', + component: UserBookingListComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, }, - - // ! OTRO // NO MIRAR - - // { - // path: 'bookings/search', - // component: BookingListComponent, - // }, // { - // path: 'bookings/new', + // path: 'admin/users/:id/bookings/:bookingId', // component: BookingComponent, // }, + { + path: 'admin/users/:id/hotels', + component: HotelListComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, + }, + { + path: 'admin/users/:userId/hotels/:id', + component: HotelRegisterComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, + }, // { - // path: 'users/:id/bookings', - // component: UserBookingListComponent, - // }, - // { - // path: 'hotels', - // component: HotelListComponent, - // }, - // { - // path: 'hotels/new', - // component: HotelRegisterComponent, - // }, - // { - // path: 'hotels/:id', - // component: HotelRegisterComponent, - // }, - // { - // path: 'users/:id', - // component: UserFormComponent, + // path: 'admin/users/:userId/hotels/:id/bookings', + // component: BookingListComponent, + // canActivate: [rolGuard], + // data: { expectedRole: 'ADMIN' }, // }, // { - // path: 'register', - // component: UserFormComponent, + // path: 'admin/users/:userId/hotels/:hotelId/rooms/:id/bookings', + // component: BookingListComponent, + // canActivate: [rolGuard], + // data: { expectedRole: 'ADMIN' }, // }, + + { + path: 'unauthorized', + component: UnauthorizedComponent, + }, { path: '**', - redirectTo: '', + redirectTo: '/login', pathMatch: 'full', }, ]; diff --git a/angular/RestClient/src/app/core/features/auth/login/login.component.css b/angular/RestClient/src/app/core/features/auth/login/login.component.css index 8fdfb8e0797b1434dd56017e25bd16af766914aa..79b4834ee543b62f19df182154eeb62fa0911db1 100644 --- a/angular/RestClient/src/app/core/features/auth/login/login.component.css +++ b/angular/RestClient/src/app/core/features/auth/login/login.component.css @@ -1,131 +1,21 @@ -/* General container styles */ .container { - max-width: 400px; - margin: 50px auto; - padding: 30px; - background: linear-gradient(135deg, #1e1e2f, #2a2a45); - border-radius: 12px; - box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3); - color: #fff; - font-family: 'Roboto', sans-serif; - } - - h2 { - text-align: center; - font-size: 1.8em; - font-weight: bold; - margin-bottom: 20px; - color: #fff; - letter-spacing: 1px; - text-transform: uppercase; - background: linear-gradient(90deg, #7f7fd5, #86a8e7, #91eae4); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - } - - /* Form fields */ - form div { - margin-bottom: 20px; - } - - label { - font-size: 0.9em; - font-weight: bold; - margin-bottom: 8px; - display: block; - letter-spacing: 0.5px; - color: #b3b3d1; - } - - input { - width: 100%; - padding: 12px 15px; - font-size: 0.95em; - border: 1px solid #3e3e5e; - border-radius: 8px; - background: #252540; - color: #fff; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); - transition: all 0.3s ease-in-out; - } - - input:focus { - outline: none; - border-color: #7f7fd5; - background: #2e2e50; - box-shadow: 0 0 8px rgba(127, 127, 213, 0.8); - } - - /* Buttons */ - button { - width: 100%; - padding: 12px; - font-size: 1em; - font-weight: bold; - color: #fff; - background: linear-gradient(90deg, #7f7fd5, #86a8e7, #91eae4); - border: none; - border-radius: 8px; - cursor: pointer; - text-transform: uppercase; - transition: transform 0.2s, box-shadow 0.2s; - } - - button:hover { - transform: translateY(-3px); - box-shadow: 0 8px 15px rgba(127, 127, 213, 0.5); - } - - button:disabled { - background: #3e3e5e; - cursor: not-allowed; - opacity: 0.7; - box-shadow: none; - } - - /* Small error messages */ - small { - display: block; - margin-top: 5px; - font-size: 0.85em; - color: #ff6b6b; - } - - /* Extra animations and effects */ - .container::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(120deg, rgba(127, 127, 213, 0.2), rgba(86, 168, 231, 0.1)); - z-index: -1; - filter: blur(20px); - } - - input::placeholder { - color: #b3b3d1; - } - - form div:last-child { - margin-top: 30px; - } - - /* Responsiveness */ - @media (max-width: 768px) { - .container { - padding: 20px; - margin: 20px; - } - - h2 { - font-size: 1.5em; - } - - input, - button { - font-size: 0.9em; - } - } - \ No newline at end of file + max-width: 600px; + margin-top: 2rem; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f9f9f9; +} + +h2 { + text-align: center; + margin-bottom: 20px; +} + +.form-group { + margin-bottom: 15px; +} + +label { + font-weight: bold; +} diff --git a/angular/RestClient/src/app/core/features/auth/login/login.component.html b/angular/RestClient/src/app/core/features/auth/login/login.component.html index 45790edab0d7039a0cef5f79d4b8d89450e28884..f6fcf96c6ad1294dad508f3a339c7f9f900c4647 100644 --- a/angular/RestClient/src/app/core/features/auth/login/login.component.html +++ b/angular/RestClient/src/app/core/features/auth/login/login.component.html @@ -1,23 +1,73 @@ -<div class="login-container"> - <h2>Login</h2> - <form [formGroup]="loginForm" (ngSubmit)="onSubmit()"> - <div> - <label for="email">Email:</label> - <input id="email" type="email" formControlName="email" /> - <div *ngIf="loginForm.get('email')?.invalid && loginForm.get('email')?.touched"> - <small>Email is required and must be valid.</small> +<div class="container"> + <form [formGroup]="loginForm" (ngSubmit)="onSubmit()"> + <mat-card> + <mat-card-title class="flex text-center p-4"> + <strong class="text-5xl">Login</strong> + </mat-card-title> + <mat-card-content> + <div class="form-group"> + <label class="text-2xl" for="email">Email:</label> + <input + id="email" + type="email" + formControlName="email" + class="form-control" + /> + @if (loginForm.get('email')?.invalid && + loginForm.get('email')?.touched) { + <div> + <small class="text-red-500 font-bold" + >Email is required and must be valid.</small + > + </div> + } </div> - </div> - - <div> - <label for="password">Password:</label> - <input id="password" type="password" formControlName="password" /> - <div *ngIf="loginForm.get('password')?.invalid && loginForm.get('password')?.touched"> - <small>Password is required.</small> + + <div class="form-group"> + <label class="text-2xl" for="password">Password:</label> + <input + id="password" + type="password" + formControlName="password" + class="form-control" + /> + @if (loginForm.get('password')?.invalid && + loginForm.get('password')?.touched){ + + <div> + <small class="text-red-500 font-bold">Password is required.</small> + </div> + } </div> - </div> - - <button type="submit" [disabled]="loginForm.invalid">Login</button> - </form> - </div> - + + <div class="form-group text-2xl"> + <label class="text-2xl" for="rol">Rol:</label> + <mat-form-field class="w-full" formControlName="rol"> + <mat-label class="text-2xl">Seleccione un rol</mat-label> + <mat-select [(value)]="selectedRol" name="rol"> + @for (rol of rolOptions; track rol) { + <mat-option [value]="rol"> + <span class="text-2xl">{{ rol }}</span> + </mat-option> + } + </mat-select> + </mat-form-field> + </div> + <mat-card-actions class="flex justify-center mb-5"> + <button + type="submit" + class="btn btn-success text-4xl" + [disabled]="loginForm.invalid" + > + Login + </button> + </mat-card-actions> + @if (errorMessage) { + <div class="text-red-500 font-bold"> + {{ errorMessage }} + </div> + } + </mat-card-content> + </mat-card> + </form> +</div> diff --git a/angular/RestClient/src/app/core/features/auth/login/login.component.ts b/angular/RestClient/src/app/core/features/auth/login/login.component.ts index b58031ce3fef6f2d9c5dc886019d2faec2c5ead8..d3bed7a4132fa619c110a0ebd74c3e8225e16216 100644 --- a/angular/RestClient/src/app/core/features/auth/login/login.component.ts +++ b/angular/RestClient/src/app/core/features/auth/login/login.component.ts @@ -1,28 +1,64 @@ import { Component } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { MatCardModule } from '@angular/material/card'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { Router } from '@angular/router'; +import { SessionService } from '../../../../shared/session.service'; +import { UserRol, UserRolesArray } from '../../../../types'; @Component({ standalone: true, selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'], - imports: [ReactiveFormsModule] + imports: [ + ReactiveFormsModule, + CommonModule, + MatCardModule, + MatInputModule, + MatFormFieldModule, + MatSelectModule, + MatSlideToggleModule, + ], }) export class LoginComponent { loginForm: FormGroup; + selectedRol?: UserRol; + rolOptions: UserRol[] = UserRolesArray; + errorMessage: string | null = null; - constructor(private fb: FormBuilder) { + constructor( + private fb: FormBuilder, + private sessionManager: SessionService, + private router: Router + ) { this.loginForm = this.fb.group({ email: ['', [Validators.required, Validators.email]], - password: ['', Validators.required] + password: ['', [Validators.required]], }); } onSubmit() { if (this.loginForm.valid) { const { email, password } = this.loginForm.value; - console.log('Login data:', { email, password }); - // Aquí iría el llamado al servicio de login con JWT + this.sessionManager.login(email, password).subscribe({ + next: (response) => { + this.router.navigateByUrl(response.mainPage); + }, + }); } } + + isAuthenticated(): boolean { + return !!localStorage.getItem('authToken'); + } } diff --git a/angular/RestClient/src/app/core/features/auth/register/register.component.css b/angular/RestClient/src/app/core/features/auth/register/register.component.css index 8fdfb8e0797b1434dd56017e25bd16af766914aa..79b4834ee543b62f19df182154eeb62fa0911db1 100644 --- a/angular/RestClient/src/app/core/features/auth/register/register.component.css +++ b/angular/RestClient/src/app/core/features/auth/register/register.component.css @@ -1,131 +1,21 @@ -/* General container styles */ .container { - max-width: 400px; - margin: 50px auto; - padding: 30px; - background: linear-gradient(135deg, #1e1e2f, #2a2a45); - border-radius: 12px; - box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3); - color: #fff; - font-family: 'Roboto', sans-serif; - } - - h2 { - text-align: center; - font-size: 1.8em; - font-weight: bold; - margin-bottom: 20px; - color: #fff; - letter-spacing: 1px; - text-transform: uppercase; - background: linear-gradient(90deg, #7f7fd5, #86a8e7, #91eae4); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - } - - /* Form fields */ - form div { - margin-bottom: 20px; - } - - label { - font-size: 0.9em; - font-weight: bold; - margin-bottom: 8px; - display: block; - letter-spacing: 0.5px; - color: #b3b3d1; - } - - input { - width: 100%; - padding: 12px 15px; - font-size: 0.95em; - border: 1px solid #3e3e5e; - border-radius: 8px; - background: #252540; - color: #fff; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); - transition: all 0.3s ease-in-out; - } - - input:focus { - outline: none; - border-color: #7f7fd5; - background: #2e2e50; - box-shadow: 0 0 8px rgba(127, 127, 213, 0.8); - } - - /* Buttons */ - button { - width: 100%; - padding: 12px; - font-size: 1em; - font-weight: bold; - color: #fff; - background: linear-gradient(90deg, #7f7fd5, #86a8e7, #91eae4); - border: none; - border-radius: 8px; - cursor: pointer; - text-transform: uppercase; - transition: transform 0.2s, box-shadow 0.2s; - } - - button:hover { - transform: translateY(-3px); - box-shadow: 0 8px 15px rgba(127, 127, 213, 0.5); - } - - button:disabled { - background: #3e3e5e; - cursor: not-allowed; - opacity: 0.7; - box-shadow: none; - } - - /* Small error messages */ - small { - display: block; - margin-top: 5px; - font-size: 0.85em; - color: #ff6b6b; - } - - /* Extra animations and effects */ - .container::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(120deg, rgba(127, 127, 213, 0.2), rgba(86, 168, 231, 0.1)); - z-index: -1; - filter: blur(20px); - } - - input::placeholder { - color: #b3b3d1; - } - - form div:last-child { - margin-top: 30px; - } - - /* Responsiveness */ - @media (max-width: 768px) { - .container { - padding: 20px; - margin: 20px; - } - - h2 { - font-size: 1.5em; - } - - input, - button { - font-size: 0.9em; - } - } - \ No newline at end of file + max-width: 600px; + margin-top: 2rem; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f9f9f9; +} + +h2 { + text-align: center; + margin-bottom: 20px; +} + +.form-group { + margin-bottom: 15px; +} + +label { + font-weight: bold; +} diff --git a/angular/RestClient/src/app/core/features/auth/register/register.component.html b/angular/RestClient/src/app/core/features/auth/register/register.component.html index 8f54cfe88e9dc8c5d3babb878e5ddb0a4165fb4e..90cc523dc3e5fc2d2a12e9c2f259f5827c0a36ef 100644 --- a/angular/RestClient/src/app/core/features/auth/register/register.component.html +++ b/angular/RestClient/src/app/core/features/auth/register/register.component.html @@ -1,31 +1,85 @@ -<div class="register-container"> - <h2>Register</h2> - <form [formGroup]="registerForm" (ngSubmit)="onSubmit()"> - <div> - <label for="email">Email:</label> - <input id="email" type="email" formControlName="email" /> - <div *ngIf="registerForm.get('email')?.invalid && registerForm.get('email')?.touched"> - <small>Email is required and must be valid.</small> +<div class="container"> + <form [formGroup]="registerForm" (ngSubmit)="onSubmit()"> + <mat-card> + <mat-card-title class="flex text-center p-4"> + <strong class="text-5xl">Regístrate</strong> + </mat-card-title> + + <mat-card-content> + <div class="form-group"> + <label class="text-2xl" for="name">Nombre</label> + <input + id="name" + class="form-control" + formControlName="name" + placeholder="Introduce tu nombre" + required + /> + @if (registerForm.get('name')?.invalid && + registerForm.get('name')?.touched) { + + <div> + <small class="text-red-500 font-bold"> + El nombre es obligatorio y debe tener al menos 3 caracteres. + </small> + </div> + } </div> - </div> - - <div> - <label for="password">Password:</label> - <input id="password" type="password" formControlName="password" /> - <div *ngIf="registerForm.get('password')?.invalid && registerForm.get('password')?.touched"> - <small>Password is required.</small> + + <div class="form-group"> + <label class="text-2xl" for="email">Correo Electrónico</label> + <input + id="email" + type="email" + class="form-control" + formControlName="email" + placeholder="Introduce tu correo" + required + /> + @if (registerForm.get('email')?.invalid && + registerForm.get('email')?.touched) { + <div> + <small class="text-red-500 font-bold"> + El correo electrónico no es válido. + </small> + </div> + } </div> - </div> - - <div> - <label for="confirmPassword">Confirm Password:</label> - <input id="confirmPassword" type="password" formControlName="confirmPassword" /> - <div *ngIf="registerForm.get('confirmPassword')?.invalid && registerForm.get('confirmPassword')?.touched"> - <small>Passwords must match.</small> + + <div class="form-group"> + <label class="text-2xl" for="password">Contraseña</label> + <input + id="password" + type="password" + class="form-control" + formControlName="password" + placeholder="Introduce tu contraseña" + required + /> + @if (registerForm.get('password')?.invalid && + registerForm.get('password')?.touched) { + <div> + <small class="text-red-500 font-bold"> + La contraseña debe tener al menos 6 caracteres. + </small> + </div> + } </div> - </div> - - <button type="submit" [disabled]="registerForm.invalid">Register</button> - </form> - </div> - \ No newline at end of file + <mat-card-actions class="flex justify-center mb-5"> + <button + type="submit" + class="btn btn-success text-4xl" + [disabled]="registerForm.invalid" + > + Registrarse + </button> + </mat-card-actions> + @if (errorMessage) { + <div class="text-red-500 font-bold"> + {{ errorMessage }} + </div> + } + </mat-card-content> + </mat-card> + </form> +</div> diff --git a/angular/RestClient/src/app/core/features/auth/register/register.component.ts b/angular/RestClient/src/app/core/features/auth/register/register.component.ts index 586790f70598414db832d76e7c05d0851a38076c..e594c19b132e84967d2509e2c3e4a5053a66f048 100644 --- a/angular/RestClient/src/app/core/features/auth/register/register.component.ts +++ b/angular/RestClient/src/app/core/features/auth/register/register.component.ts @@ -1,31 +1,79 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatCardModule } from '@angular/material/card'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { Router } from '@angular/router'; +import { AuthClientService } from '../../../../shared/auth-client.service'; +import { SessionService } from '../../../../shared/session.service'; + +// TODO agregar selector de roles @Component({ + standalone: true, selector: 'app-register', templateUrl: './register.component.html', - styleUrls: ['./register.component.css'] + styleUrls: ['./register.component.css'], + imports: [ + ReactiveFormsModule, + CommonModule, + MatCardModule, + MatInputModule, + MatFormFieldModule, + MatSelectModule, + MatSlideToggleModule, + ], }) export class RegisterComponent { registerForm: FormGroup; + errorMessage: string | null = null; - constructor(private fb: FormBuilder) { + constructor( + private fb: FormBuilder, + private authClient: AuthClientService, + private sessionManager: SessionService, + private router: Router + ) { + if (this.sessionManager.isValid()) { + const s = this.sessionManager.getSession(); + console.log({ s }); + } this.registerForm = this.fb.group({ + name: ['', [Validators.required, Validators.minLength(3)]], email: ['', [Validators.required, Validators.email]], - password: ['', Validators.required], - confirmPassword: ['', Validators.required] + password: ['', [Validators.required, Validators.minLength(6)]], }); } onSubmit() { if (this.registerForm.valid) { - const { email, password, confirmPassword } = this.registerForm.value; - if (password === confirmPassword) { - console.log('Register data:', { email, password }); - // Aquí iría el llamado al servicio de register con JWT - } else { - console.error('Passwords do not match.'); - } + const { name, email, password } = this.registerForm.value; + + this.authClient.register(name, email, password).subscribe({ + next: (res: any) => { + console.log({ res }); + this.sessionManager.login(res); + alert('Usuario registrado con éxito.'); + this.router.navigate(['/']); // Redirigir al login + }, + error: (err) => { + if (err.error instanceof ErrorEvent) { + this.errorMessage = `Error: ${err.error.message}`; + } else { + // Si el backend devuelve un objeto de error + this.errorMessage = + err.error.message || 'Ocurrió un error al registrar el usuario.'; + } + }, + }); } } } diff --git a/angular/RestClient/src/app/core/features/bookings/booking-list/booking-list.component.ts b/angular/RestClient/src/app/core/features/bookings/booking-list/booking-list.component.ts index 6fb90798ea3a10e2dece5c0b0661e4a43ec393a4..0e573456823b7cc0cbb4e098075f835141678a21 100644 --- a/angular/RestClient/src/app/core/features/bookings/booking-list/booking-list.component.ts +++ b/angular/RestClient/src/app/core/features/bookings/booking-list/booking-list.component.ts @@ -36,9 +36,9 @@ const selectableRoomTypeArray: SelectableRoomType[] = ['All', ...roomTypeArray]; }) export class BookingListComponent { searched: boolean = false; + hotels!: Hotel[]; start?: Date; end?: Date; - hotels!: Hotel[]; hotelSelected?: Hotel; roomTypeSelected?: SelectableRoomType; roomTypes = selectableRoomTypeArray; diff --git a/angular/RestClient/src/app/core/features/bookings/booking/booking.component.html b/angular/RestClient/src/app/core/features/bookings/booking/booking.component.html index 4b09b386e32b53641cb4c00d522958fabcb21e07..d21112a5b1473871dc8a7df48d3d7125a06c5cb6 100644 --- a/angular/RestClient/src/app/core/features/bookings/booking/booking.component.html +++ b/angular/RestClient/src/app/core/features/bookings/booking/booking.component.html @@ -1,7 +1,7 @@ <div class="container"> - <h2>Crear Reserva</h2> + <h2><strong class="text-5xl font-bold">Confirmar reserva</strong></h2> <form [formGroup]="bookingForm" (ngSubmit)="submitBooking()"> - <div class="form-group"> + <!-- <div class="form-group"> <label for="userId">ID del Usuario:</label> <select type="number" @@ -13,7 +13,7 @@ <option value="{{ user.id }}">{{ user.name }}</option> } </select> - </div> + </div> --> <div class="form-group"> <label for="roomId">ID del Habitación:</label> @@ -26,7 +26,7 @@ </div> <div class="form-group"> - <label for="startDate">Fecha de Inicio:</label> + <label for="startDate">Fecha de Inicio (MM/dd/YYYY):</label> <input type="date" id="startDate" @@ -36,7 +36,7 @@ </div> <div class="form-group"> - <label for="endDate">Fecha de Fin:</label> + <label for="endDate">Fecha de Fin (MM/dd/YYYY):</label> <input type="date" id="endDate" @@ -45,8 +45,6 @@ /> </div> - <button [disabled]="!isUserSelected" type="submit" class="btn btn-primary"> - Reservar - </button> + <button type="submit" class="btn btn-primary">Reservar</button> </form> </div> diff --git a/angular/RestClient/src/app/core/features/bookings/booking/booking.component.ts b/angular/RestClient/src/app/core/features/bookings/booking/booking.component.ts index 6ad8c379ae5b2e117c10a160eb07df12890f45fb..b7b1dc209637a0ab617eae1ab4f91b0f7a479b2b 100644 --- a/angular/RestClient/src/app/core/features/bookings/booking/booking.component.ts +++ b/angular/RestClient/src/app/core/features/bookings/booking/booking.component.ts @@ -11,6 +11,7 @@ import { Booking, User } from '../../../../types'; import { LocalStorageService } from '../../../../shared/local-storage.service'; import { BookingClientService } from '../../../../shared/booking-client.service'; import { UserClientService } from '../../../../shared/user-client.service'; +import { SessionService } from '../../../../shared/session.service'; type communication = { roomId: number; @@ -25,8 +26,8 @@ type communication = { templateUrl: './booking.component.html', styleUrls: ['./booking.component.css'], }) -export class BookingComponent implements OnInit { - users: User[] = []; +export class BookingComponent { + user: User = { id: 0, email: '', name: '', rol: 'CLIENT' }; bookingForm: FormGroup; bookingLocal: { roomId: number; startDate: Date; endDate: Date } = { roomId: 0, @@ -39,16 +40,16 @@ export class BookingComponent implements OnInit { private router: Router, private route: ActivatedRoute, private fb: FormBuilder, + private sessionService: SessionService, private bookingClient: BookingClientService, private userClient: UserClientService, private storage: LocalStorageService ) { // Inicialización del formulario con validaciones this.bookingForm = this.fb.group({ - userId: ['', Validators.required], - roomId: ['', Validators.required], - startDate: ['', Validators.required], - endDate: ['', Validators.required], + roomId: [{ value: '', disabled: true }, Validators.required], + startDate: [{ value: '', disabled: true }, Validators.required], + endDate: [{ value: '', disabled: true }, Validators.required], }); const localBooking = storage.read<communication | null>('booking-data'); if (localBooking === null) { @@ -63,81 +64,60 @@ export class BookingComponent implements OnInit { this.router.navigate(['/bookings', 'search']); return; } + this.bookingLocal = { + ...this.bookingLocal, + startDate: new Date(this.bookingLocal.startDate), + endDate: new Date(this.bookingLocal.endDate), + }; this.loadBooking(); }); - this.userClient.getAllUsers().subscribe({ - next: (resp) => { - this.users = resp; + this.sessionService.getSession().subscribe({ + next: (session) => { + if (session) this.user = session; }, }); } - get userId() { - return this.bookingForm.get('userId')!.value; - } - get isUserSelected() { - return !isNaN(this.userId); - } - - ngOnInit() { - this.loadBooking(); - } - loadBooking() { const booking = this.bookingLocal; if (!booking) return; const start = new Date(booking.startDate).toISOString().split('T')[0]; const end = new Date(booking.endDate).toISOString().split('T')[0]; this.bookingForm = this.fb.group({ - userId: [Validators.required], - roomId: [booking.roomId, Validators.required], - startDate: [start, Validators.required], - endDate: [end, Validators.required], + roomId: [{ value: booking.roomId, disabled: true }, Validators.required], + startDate: [{ value: start, disabled: true }, Validators.required], + endDate: [{ value: end, disabled: true }, Validators.required], }); - this.bookingForm.get('roomId')?.disable(); - this.bookingForm.get('startDate')?.disable(); - this.bookingForm.get('endDate')?.disable(); } submitBooking() { - if (this.bookingForm.valid) { - const formValue = this.bookingForm.value; - const userId = Number(formValue.userId); - const bookingRequest: any = { - ...this.bookingLocal, - userId: { id: userId }, - roomId: { id: this.roomId }, - }; + const { id } = this.user; + const bookingRequest: any = { + ...this.bookingLocal, + userId: { id }, + roomId: { id: this.roomId }, + }; - // Llama al servicio para crear una nueva reserva - this.bookingClient.createBooking(bookingRequest).subscribe({ - next: (response) => { - console.log('Reserva creada con éxito', response); - // Llama al servicio para actualizar el estado del usuario - this.userClient - .alterUserStatus(userId, 'WITH_ACTIVE_BOOKINGS') - .subscribe({ - next: (response) => { - console.log( - 'Estado de usuario actualizado con exito', - response - ); - this.storage.remove('booking-data'); - this.router.navigate(['/user', userId, 'bookings']); - }, - error: (error) => { - console.error('Error al cambiar el estado del usuario', error); - }, - }); - }, - error: (error) => { - console.error('Error al crear la reserva', error); - // Manejo de errores - }, - }); - } else { - console.warn('El formulario no es válido'); - // Puedes mostrar un mensaje al usuario sobre la validez del formulario - } + // Llama al servicio para crear una nueva reserva + this.bookingClient.createBooking(bookingRequest).subscribe({ + next: (response) => { + console.log('Reserva creada con éxito', response); + // Llama al servicio para actualizar el estado del usuario + this.userClient.alterUserStatus(id, 'WITH_ACTIVE_BOOKINGS').subscribe({ + next: (response) => { + console.log('Estado de usuario actualizado con exito', response); + this.storage.remove('booking-data'); + this.router.navigate(['/me', 'bookings']); + }, + error: (error) => { + console.error('Error al cambiar el estado del usuario', error); + }, + }); + }, + error: (error) => { + console.error('Error al crear la reserva', error); + // Manejo de errores + }, + }); } } diff --git a/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.css b/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.css index fcbb63ff2da3990e7eb2bffbfedcd84cadac8004..53ab55f4a45f24f76a2278c0c6b7bed895e56751 100644 --- a/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.css +++ b/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.css @@ -6,3 +6,7 @@ border-radius: 5px; background-color: #f9f9f9; } +.example-card { + max-width: 400px; + margin-bottom: 8px; +} diff --git a/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.html b/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.html index a97254ba286fcb168c472d0f717d7bff62409a50..86b8a3546bcd2d2e9c809872132aa7923dbcb6d0 100644 --- a/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.html +++ b/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.html @@ -1,5 +1,6 @@ <div class="container"> <h2 class="text-center text-5xl font-bold mb-4">Hotel List</h2> + @if (isManaging) { <mat-accordion> <mat-expansion-panel disabled class="cursor-default"> <mat-expansion-panel-header> @@ -23,17 +24,18 @@ </mat-expansion-panel-header> <div class="text-end mb-4"> - <button - mat-raised-button - (click)="goToHotelDetails(hotel.id)" - style=" - font-size: medium; - background-color: rgb(28, 197, 248); - color: rgb(250, 250, 250); - " - > - View hotel - </button> + <a [routerLink]="getHotelUri(hotel.id)"> + <button + mat-raised-button + style=" + font-size: medium; + background-color: rgb(28, 197, 248); + color: rgb(250, 250, 250); + " + > + View hotel + </button> + </a> <button mat-raised-button (click)="deleteHotel(hotel.id)" @@ -90,4 +92,105 @@ </mat-expansion-panel> } </mat-accordion> + } @else { + <div class="form-group text-xl flex justify-center gap-20"> + <mat-form-field> + <mat-label class="text-2xl">Enter a date range</mat-label> + <form [formGroup]="dateRangeForm" (change)="update()"> + <mat-date-range-input [rangePicker]="picker" formGroupName="dateRange"> + <input + matStartDate + formControlName="start" + placeholder="Fecha de inicio" + /> + <input matEndDate formControlName="end" placeholder="Fecha de fin" /> + </mat-date-range-input> + <mat-datepicker-toggle + matIconSuffix + [for]="picker" + ></mat-datepicker-toggle> + <mat-date-range-picker #picker></mat-date-range-picker> + </form> + <!-- (dateInput)="updateStart($event)" + (dateChange)="updateStart($event)" --> + + <!-- (dateInput)="updateEnd($event)" + (dateChange)="updateEnd($event)" --> + </mat-form-field> + <mat-form-field> + <mat-label class="text-2xl">Hotel</mat-label> + <mat-select + [(value)]="hotelSelected" + class="text-2xl" + (selectionChange)="update()" + > + @for (hotel of _hotels; track hotel.id) { + <mat-option [value]="hotel" class="text-3xl">{{ + hotel.name + }}</mat-option> + } + </mat-select> + </mat-form-field> + <mat-form-field> + <mat-label>Filter by Room Type</mat-label> + <mat-select [(value)]="roomTypeSelected"> + @for (type of roomTypes; track type) { + <mat-option [value]="type">{{ type }}</mat-option> + } + </mat-select> + </mat-form-field> + </div> + @for(hotel of hotels; track hotel.id) { + <div class="mt-10 shadow-md"> + <mat-card appearance="raised"> + <mat-card-header class="flex justify-center p-4 mb-4"> + <mat-card-title class="text-center"> + <a [routerLink]="getHotelUri(hotel.id)"> + <strong class="text-4xl flex items-center justify-center gap-4"> + <mat-icon>hotel</mat-icon> {{ hotel.name }}</strong + > + <p class="mt-5 text-2xl italic"> + {{ hotel.address.streetKind }} {{ hotel.address.streetName }}. Nº + {{ hotel.address.number }} [{{ hotel.address.postCode }}] + </p> + @if (hotel.address.otherInfo) { + <small>{{ hotel.address.otherInfo }}</small> + } + </a> + </mat-card-title> + </mat-card-header> + <mat-card-content> + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> + @for(room of hotel.rooms; track room.id) { @if (showRequested(room)) { + <div class="drop-shadow-lg hover:shadow-2xl"> + <mat-card appearance="raised"> + <mat-card-header class="flex justify-center"> + <mat-card-title> + <strong + >(#{{ $index + 1 }}) Habitación {{ room.roomNumber }} + </strong> + <span class="italic">({{ room.type }})</span> + </mat-card-title> + </mat-card-header> + <mat-card-content class="mt-4"> + <button + [disabled]="!isAvailable(room)" + mat-raised-button + class="w-full text-center py-3 rounded-lg shadow-md hover:shadow-lg bg-sky-600 text-slate-200 font-bold" + (click)="bookingRoom(room.id)" + > + <span class="flex items-center justify-center text-2xl"> + <mat-icon>booking</mat-icon> + Reservar + </span> + </button> + </mat-card-content> + </mat-card> + </div> + }} + </div> + </mat-card-content> + </mat-card> + </div> + } } </div> diff --git a/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.ts b/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.ts index d951865749ebd766b23898f9428d593341cd73d6..e684545c1d5b962a028fdc22e0ef2cd842742d86 100644 --- a/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.ts +++ b/angular/RestClient/src/app/core/features/hotel/hotel-list/hotel-list.component.ts @@ -1,6 +1,6 @@ -import { Component } from '@angular/core'; -import { RouterModule, Router } from '@angular/router'; -import { Hotel } from '../../../../types'; +import { Component, NgModule } from '@angular/core'; +import { RouterModule, Router, ActivatedRoute, Data } from '@angular/router'; +import { Hotel, Room, RoomType, roomTypeArray } from '../../../../types'; import { MatAccordion, MatExpansionPanel, @@ -11,13 +11,29 @@ import { import { MatSlideToggle } from '@angular/material/slide-toggle'; import { MatTable, MatTableModule } from '@angular/material/table'; import { MatButton } from '@angular/material/button'; -import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { + NgbAccordionModule, + NgbDatepickerModule, +} from '@ng-bootstrap/ng-bootstrap'; import { HotelClientService } from '../../../../shared/hotel-client.service'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatSelectModule } from '@angular/material/select'; +import { LocalStorageService } from '../../../../shared/local-storage.service'; +import { SessionService } from '../../../../shared/session.service'; +import { getBasePath } from '../../../../utils/utils'; + +type SelectableRoomType = 'All' | RoomType; +const selectableRoomTypeArray: SelectableRoomType[] = ['All', ...roomTypeArray]; @Component({ selector: 'app-hotel-list', standalone: true, imports: [ + NgbDatepickerModule, RouterModule, MatAccordion, MatSlideToggle, @@ -29,28 +45,93 @@ import { HotelClientService } from '../../../../shared/hotel-client.service'; MatExpansionPanelTitle, MatExpansionPanelDescription, NgbAccordionModule, + MatCardModule, + MatIconModule, + MatFormFieldModule, + MatDatepickerModule, + MatSelectModule, + ReactiveFormsModule, ], templateUrl: './hotel-list.component.html', styleUrl: './hotel-list.component.css', }) export class HotelListComponent { + _hotels!: Hotel[]; hotels!: Hotel[]; - mostrarMensaje!: boolean; - mensaje!: string; + isEditing = false; + isManaging = false; + dateRangeForm: FormGroup; + hotelSelected?: Hotel; + roomTypeSelected: SelectableRoomType = 'All'; + roomTypes = selectableRoomTypeArray; + rooms: Room[] = []; + trateRooms: Room[] = []; constructor( + private fb: FormBuilder, + private hotelClient: HotelClientService, private router: Router, - private hotelClient: HotelClientService - ) {} + private route: ActivatedRoute, + private storage: LocalStorageService, + private sessionService: SessionService + ) { + this.isManaging = + this.route.snapshot.url[0].path === 'me' || + this.route.snapshot.url[0].path === 'admin'; + const today = new Date(); - ngOnInit() { + // Inicializa el formulario con las fechas predefinidas + this.dateRangeForm = this.fb.group({ + dateRange: this.fb.group({ + start: [today], // Fecha de inicio + end: [today], // Fecha de fin + }), + }); + + this.sessionService.getSession().subscribe({ + next: (session) => { + if (session && session.rol !== 'CLIENT') { + this.isEditing = true; + } + }, + }); + } + + ngOnInit(): void { this.getHotels(); } + update() { + // TODO completar + this.hotels = !!this.hotelSelected + ? this._hotels.filter((h) => h.id === this.hotelSelected!.id) + : [...this._hotels]; + } + + showRequested(room: Room) { + return ( + this.roomTypeSelected === 'All' || + (room.type as SelectableRoomType) === this.roomTypeSelected + ); + } + + isAvailable(room: Room) { + const value = + !this.isEditing && + room.available && + (this.roomTypeSelected === 'All' || + (room.type as SelectableRoomType) === this.roomTypeSelected); + + return value; + } + getHotels() { this.hotelClient.getAllHotels().subscribe({ next: (resp) => { - if (!!resp || (resp as never[]).length != 0) this.hotels = [...resp]; + if (!!resp && (resp as never[]).length >= 0) { + this._hotels = resp; + this.update(); + } }, error(err) { console.log('Error al traer la lista: ' + err.message); @@ -61,17 +142,9 @@ export class HotelListComponent { deleteHotel(id: number) { if (!confirm(`Borrar hotel con id ${id}. Continuar?`)) return; - this.hotelClient.deleteHotel(id).subscribe({ - next: (resp) => { - if (resp.status < 400) { - this.mostrarMensaje = true; - this.mensaje = resp.body as string; - this.getHotels(); - } else { - this.mostrarMensaje = true; - this.mensaje = 'Error al eliminar registro'; - } + next: () => { + this.getHotels(); }, error: (err) => { console.log('Error al borrar: ' + err.message); @@ -89,14 +162,8 @@ export class HotelListComponent { .alterRoomAvailability(hotelId, roomId, availability) .subscribe({ next: (resp) => { - if (resp.status < 400) { - this.mostrarMensaje = true; - this.mensaje = resp.body as string; + if (resp) { this.getHotels(); - } else { - this.mostrarMensaje = true; - this.mensaje = 'Error al cambiar disponibilidad'; - console.error(this.mensaje); } }, error: (error) => { @@ -106,7 +173,28 @@ export class HotelListComponent { }); } - goToHotelDetails(hotelId: number): void { - this.router.navigate(['/hotels', hotelId]); + getHotelUri(hotelId: number) { + var base; + try { + base = getBasePath(this.route) + '/'; + } catch (error) { + base = ''; + } + return (this.isManaging ? base : '/') + 'hotels/' + hotelId; + } + + bookingRoom(roomId: number) { + const { start, end } = this.dateRangeForm.value.dateRange as { + start: Date; + end: Date; + }; + this.storage.save('booking-data', { + roomId, + startDate: start.toString(), + endDate: end.toString(), + }); + this.router.navigate(['/me', 'bookings', 'new'], { + queryParams: { roomId, startDate: start.toLocaleDateString() }, + }); } } diff --git a/angular/RestClient/src/app/core/features/hotel/hotel-register/hotel-register.component.ts b/angular/RestClient/src/app/core/features/hotel/hotel-register/hotel-register.component.ts index 9e2c2d433b6fa68621be16b30b7841e34ec8829f..9ad5d71c17a7835cd12cc5d4d38440848e872f5c 100644 --- a/angular/RestClient/src/app/core/features/hotel/hotel-register/hotel-register.component.ts +++ b/angular/RestClient/src/app/core/features/hotel/hotel-register/hotel-register.component.ts @@ -16,6 +16,7 @@ import { CommonModule } from '@angular/common'; import { Address, Hotel, Room } from '../../../../types'; import { ActivatedRoute, Router } from '@angular/router'; import { HotelClientService } from '../../../../shared/hotel-client.service'; +import { MatIconModule } from '@angular/material/icon'; const emptyRoom: Room = { id: 0, @@ -66,12 +67,15 @@ export class HotelRegisterComponent { this.route.paramMap.subscribe({ next: (params) => { const id = Number(params.get('id')); + if (!id) { + this.router.navigateByUrl('/hotels/register'); + } this.editMode = id !== 0; if (this.editMode) { this.hotelClient.getHotel(id).subscribe({ next: (h) => this.setHotelForm(h), error: (error) => { - this.router.navigate(['/hotels/new']); + this.router.navigateByUrl('/hotels/register'); }, }); } @@ -104,10 +108,10 @@ export class HotelRegisterComponent { const hotel = this.hotelForm.value as Hotel; this.hotelClient.addHotel(hotel).subscribe({ next: (resp) => { - if (resp.status < 400) { + if (resp) { + console.log({ resp }); alert('Hotel guardado correctamente'); - this.router.navigate(['/hotels']); - } else { + // this.router.navigate(['/hotels']); } }, error: (err) => { diff --git a/angular/RestClient/src/app/core/features/user/main-page/main-page.component.css b/angular/RestClient/src/app/core/features/user/main-page/main-page.component.css index f3f3318a77b3200dc595486e2d703bec99a0811c..ccc36ce397f5384e2bcdbe39089b2c077bb444c4 100644 --- a/angular/RestClient/src/app/core/features/user/main-page/main-page.component.css +++ b/angular/RestClient/src/app/core/features/user/main-page/main-page.component.css @@ -1,112 +1,42 @@ -/* Contenedor principal con esquinas redondeadas y sombra sutil */ -.user-container { - max-width: 900px; - margin: 30px auto; - padding: 25px; - background-color: #1f1f1f; - border-radius: 12px; - box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.6); +.mat-mdc-row .mat-mdc-cell { + border-bottom: 2px solid transparent; + border-top: 2px solid transparent; + cursor: pointer; + font-size: 1.75rem; + line-height: 2rem; } -/* Título centralizado y con un estilo moderno */ -h1 { - text-align: center; - font-size: 2em; - margin-bottom: 25px; - color: #ffffff; - letter-spacing: 1px; -} - -/* Contenedor de filtro centrado y limpio */ -.filter-container { - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 20px; -} - -label { - font-weight: 600; - margin-right: 10px; - color: #a1a1a1; -} - -/* Select estilizado con borde suave y sombras */ -select { - padding: 10px; - font-size: 1em; - border: none; - outline: none; - background-color: #292929; - color: #e0e0e0; - border-radius: 6px; - box-shadow: inset 0px 3px 8px rgba(0, 0, 0, 0.5); - transition: all 0.3s ease; -} - -select:hover { - background-color: #3a3a3a; -} - -/* Lista de usuarios sin puntos y con bordes modernos */ -.user-list { - list-style-type: none; - padding: 0; - margin: 0; -} - -/* Elementos de la lista con espaciado y efecto hover futurista */ -.user-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 15px 20px; - margin-bottom: 8px; - background-color: #2c2c2c; - border-radius: 8px; - transition: transform 0.3s ease, background-color 0.3s ease; +.table { + border: 0.7rem #ccc solid; + border-radius: 1rem; } -.user-item:hover { - background-color: #333333; - transform: translateY(-2px); - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.3); +.mat-mdc-row:hover .mat-mdc-cell { + border-color: currentColor; + background-color: #feffab; } -/* Estilos de usuario con tipografía clara y futurista */ -.user-name { - flex: 0.5; - font-weight: 700; - color: #ffffff; +.demo-row-is-clicked { + font-weight: bold; } - -.user-email { - flex: 1.5; - color: #b0b0b0; - font-style: italic; - text-align: center; - padding-left: 15px; +.container { + max-width: 1000px; + margin-top: 2rem; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f9f9f9; } -.user-status { - flex: 1; - font-weight: 500; - color: #76c7c0; - text-transform: uppercase; +h2 { text-align: center; + margin-bottom: 20px; } -.user-bookings { - flex: 1; - font-weight: 500; - color: #76c7c0; - text-transform: uppercase; - text-align: center; +.form-group { + margin-bottom: 15px; } -.user-bookings button { - padding: 0.5rem 1rem; - background-color: #3d6b74; - color: white; - border-radius: 20px; +label { + font-weight: bold; } diff --git a/angular/RestClient/src/app/core/features/user/main-page/main-page.component.html b/angular/RestClient/src/app/core/features/user/main-page/main-page.component.html index 9c34f26569237817882140eb2960e3fc98cc0fcf..8f3659e24b6181f5a54781dc36376aae20b3affe 100644 --- a/angular/RestClient/src/app/core/features/user/main-page/main-page.component.html +++ b/angular/RestClient/src/app/core/features/user/main-page/main-page.component.html @@ -1,34 +1,85 @@ -<div class="user-container"> - <h1>Listado de Usuarios</h1> +<div class="container"> + <mat-card> + <mat-card-title class="flex text-center p-4"> + <strong class="text-5xl">Usuarios</strong> + </mat-card-title> + <mat-card-content> + <div class="filter-container"> + <label for="filter">Filtrar por estado:</label> + <select + id="filter" + [(ngModel)]="selectedStatus" + (change)="filterUsers()" + > + <option value="All">Todos</option> + <option value="NO_BOOKINGS">Sin reservas</option> + <option value="WITH_ACTIVE_BOOKINGS">Con reservas activas</option> + <option value="WITH_INACTIVE_BOOKINGS">Con reservas inactivas</option> + </select> + </div> + <div class="mat-elevation-z8 demo-table table"> + <table mat-table [dataSource]="dataSource"> + <ng-container matColumnDef="id"> + <th mat-header-cell *matHeaderCellDef> + <span class="text-3xl font-bold">ID</span> + </th> + <td mat-cell *matCellDef="let element"> + <span class="text-2xl"> + {{ element.id }} + </span> + </td> + </ng-container> - <div class="filter-container"> - <label for="filter">Filtrar por estado:</label> - <select id="filter" [(ngModel)]="selectedStatus" (change)="filterUsers()"> - <option value="All">Todos</option> - <option value="NO_BOOKINGS">Sin reservas</option> - <option value="WITH_ACTIVE_BOOKINGS">Con reservas activas</option> - <option value="WITH_INACTIVE_BOOKINGS">Con reservas inactivas</option> - </select> - </div> + <!-- Name Column --> + <ng-container matColumnDef="name"> + <th mat-header-cell *matHeaderCellDef> + <span class="text-3xl font-bold">Name</span> + </th> + <td mat-cell *matCellDef="let element"> + <span class="text-2xl"> + {{ element.name }} + </span> + </td> + </ng-container> - <ul class="user-list"> - <li class="user-item"> - <span class="user-name">Nombre</span> - <span class="user-email">Dirección de correo electronica</span> - <span class="user-status">Estado de usuario</span> - <span class="user-bookings">Reservas</span> - </li> - @for (user of filteredUsers; track user.id) { - <li class="user-item"> - <span class="user-name">{{ user.name }}</span> - <span class="user-email">{{ user.email }}</span> - <span class="user-status">{{ getState(user) }}</span> - <span class="user-bookings"> - <a [routerLink]="['/users', user.id, 'bookings']"> - <button>Reservas</button> - </a> - </span> - </li> - } - </ul> + <!-- Weight Column --> + <ng-container matColumnDef="email"> + <th mat-header-cell *matHeaderCellDef> + <span class="text-3xl font-bold">Email</span> + </th> + <td mat-cell *matCellDef="let element"> + <span class="text-2xl"> + {{ element.email }} + </span> + </td> + </ng-container> + + <!-- Symbol Column --> + <ng-container matColumnDef="rol"> + <th mat-header-cell *matHeaderCellDef> + <span class="text-3xl font-bold">Rol</span> + </th> + <td mat-cell *matCellDef="let element"> + <span class="text-2xl"> + {{ element.rol }} + </span> + </td> + </ng-container> + + <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> + <tr + mat-row + *matRowDef="let row; columns: displayedColumns" + (click)="userDetails(row.id)" + ></tr> + </table> + </div> + <mat-paginator + [pageSizeOptions]="[5, 10, 20]" + showFirstLastButtons + aria-label="Select page of periodic elements" + > + </mat-paginator> + </mat-card-content> + </mat-card> </div> diff --git a/angular/RestClient/src/app/core/features/user/main-page/main-page.component.ts b/angular/RestClient/src/app/core/features/user/main-page/main-page.component.ts index 14b5a9d6c0e871d0e71b3d2154554f83e8ecc50d..09770a81445c949b6754000aeb014d66933e95c1 100644 --- a/angular/RestClient/src/app/core/features/user/main-page/main-page.component.ts +++ b/angular/RestClient/src/app/core/features/user/main-page/main-page.component.ts @@ -1,14 +1,24 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { Client, User, UserStateFilter } from '../../../../types'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; +import { Router, RouterModule } from '@angular/router'; import { UserClientService } from '../../../../shared/user-client.service'; import { users } from '../../../../../mocks/users'; // Renombrado para claridad +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; +import { MatCardModule } from '@angular/material/card'; @Component({ standalone: true, - imports: [FormsModule, CommonModule, RouterModule], + imports: [ + FormsModule, + CommonModule, + RouterModule, + MatTableModule, + MatCardModule, + MatPaginatorModule, + ], selector: 'app-main-page', templateUrl: './main-page.component.html', styleUrls: ['./main-page.component.css'], @@ -17,13 +27,12 @@ export class MainPageComponent implements OnInit { users: Client[] = []; filteredUsers: Client[] = []; selectedStatus: UserStateFilter = 'All'; + displayedColumns: string[] = ['id', 'name', 'email', 'rol']; + dataSource = new MatTableDataSource<User>(); - constructor(private userClient: UserClientService) {} + constructor(private userClient: UserClientService, private router: Router) {} ngOnInit(): void { - // Validar que el mock sea del tipo correcto - // const isValidMock = Array.isArray(mockUsers) && mockUsers.every(user => 'id' in user && 'name' in user && 'status' in user); - // this.users = isValidMock ? (mockUsers as User[]) : []; this.users = users; this.filteredUsers = [...this.users]; @@ -31,12 +40,14 @@ export class MainPageComponent implements OnInit { this.userClient.getAllUsers().subscribe({ next: (data: Client[]) => { this.users = data; - this.filteredUsers = [...data]; + this.filterUsers(); }, error: (err) => console.error('Error al cargar usuarios:', err), }); } + @ViewChild(MatPaginator) paginator?: MatPaginator; + filterUsers(): void { if (this.selectedStatus === 'All') { this.filteredUsers = [...this.users]; @@ -45,6 +56,8 @@ export class MainPageComponent implements OnInit { (user) => user.status === this.selectedStatus ); } + this.dataSource = new MatTableDataSource<User>(this.filteredUsers); + this.dataSource.paginator = this.paginator!; } getState(user: Client): string { @@ -59,4 +72,8 @@ export class MainPageComponent implements OnInit { return 'ESTADO DESCONOCIDO'; } } + + userDetails(id: number) { + this.router.navigate(['/admin', 'users', id]); + } } diff --git a/angular/RestClient/src/app/core/features/user/user-booking-list/user-booking-list.component.html b/angular/RestClient/src/app/core/features/user/user-booking-list/user-booking-list.component.html index 88e7c25943f832f6984cd1ee0c5bcf34729f5ced..6a845bc703fc7e279909d96f39e55c2f387e33c5 100644 --- a/angular/RestClient/src/app/core/features/user/user-booking-list/user-booking-list.component.html +++ b/angular/RestClient/src/app/core/features/user/user-booking-list/user-booking-list.component.html @@ -1,6 +1,6 @@ <div class="booking-container"> <h1>Listado de Reservas</h1> - <h3>para el usuario {{ user?.name }} (ID: {{ userId }})</h3> + <h3>para el usuario {{ user.name }} (ID: {{ user.id }})</h3> <div class="filter-container"> <label for="filter">Filtrar por estado:</label> diff --git a/angular/RestClient/src/app/core/features/user/user-booking-list/user-booking-list.component.ts b/angular/RestClient/src/app/core/features/user/user-booking-list/user-booking-list.component.ts index 878e6446e5bc8a162d67e880edd8633a5cbb3a94..f5ca26c52374160a4a16fe933a03cec8a8fed823 100644 --- a/angular/RestClient/src/app/core/features/user/user-booking-list/user-booking-list.component.ts +++ b/angular/RestClient/src/app/core/features/user/user-booking-list/user-booking-list.component.ts @@ -6,6 +6,8 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { UserClientService } from '../../../../shared/user-client.service'; import { BookingClientService } from '../../../../shared/booking-client.service'; +import { SessionService } from '../../../../shared/session.service'; +import { Observable } from 'rxjs'; type state = 'all' | 'active' | 'inactive'; @@ -20,31 +22,35 @@ export class UserBookingListComponent { selectedState: state = 'all'; search = false; bookings: Booking[] = []; - userId: number = 0; - user?: User; + user: User = { id: 0, name: '', email: '', rol: 'CLIENT' }; constructor( private userClient: UserClientService, private bookingClient: BookingClientService, + private sessionService: SessionService, private route: ActivatedRoute ) { - this.route.paramMap.subscribe({ - next: (params) => { - this.userId = Number(params.get('id')); + this.loadUser(); + } + + resolve(): Observable<any> { + const id = this.route.snapshot.paramMap.get('id'); + return id + ? this.userClient.getUser(Number(id)) + : this.sessionService.getSession(); + } + + loadUser() { + this.resolve().subscribe({ + next: (user) => { + this.user = user; this.updateBookings(); }, }); - this.userClient - .getUser(this.userId) - .subscribe({ next: (user) => (this.user = user) }); - } - - ngOnInit() { - this.updateBookings(); } updateBookings() { - this.bookingClient.getBookingsByUser(this.userId).subscribe({ + this.bookingClient.getBookingsByUser(this.user.id).subscribe({ next: (bookings) => { this.search = true; switch (this.selectedState) { @@ -70,9 +76,10 @@ export class UserBookingListComponent { } genBookingState(booking: Booking) { - return new Date(booking.endDate).getTime() < Date.now() - ? 'Reserva inactiva' - : 'Reserva activa'; + return new Date().setHours(0, 0, 0, 0) <= + new Date(booking.endDate).getTime() + ? 'Reserva activa' + : 'Reserva inactiva'; } deleteBooking(bookingId: number) { @@ -88,7 +95,7 @@ export class UserBookingListComponent { } updateUserStatus() { - this.bookingClient.getBookingsByUser(this.userId).subscribe({ + this.bookingClient.getBookingsByUser(this.user.id).subscribe({ next: (bookings) => { const withActive = bookings.find( (booking) => this.genBookingState(booking) === 'Reserva activa' @@ -98,7 +105,7 @@ export class UserBookingListComponent { ); if (withActive) { this.userClient - .alterUserStatus(this.userId, 'WITH_ACTIVE_BOOKINGS') + .alterUserStatus(this.user.id, 'WITH_ACTIVE_BOOKINGS') .subscribe({ next: (response) => { console.log('Cambio de estado en el usuario a activo correcto'); @@ -109,7 +116,7 @@ export class UserBookingListComponent { }); } else if (withInactive) { this.userClient - .alterUserStatus(this.userId, 'WITH_INACTIVE_BOOKINGS') + .alterUserStatus(this.user.id, 'WITH_INACTIVE_BOOKINGS') .subscribe({ next: (response) => { console.log( @@ -124,7 +131,7 @@ export class UserBookingListComponent { }); } else { this.userClient - .alterUserStatus(this.userId, 'NO_BOOKINGS') + .alterUserStatus(this.user.id, 'NO_BOOKINGS') .subscribe({ next: (response) => { console.log( diff --git a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.css b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.css index 1eef9bba18e0bd279742ea1d297e8ab49da1c394..79b4834ee543b62f19df182154eeb62fa0911db1 100644 --- a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.css +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.css @@ -1,17 +1,15 @@ -.form-container { - max-width: 400px; - margin: 50px auto; - background: #f8f9fa; +.container { + max-width: 600px; + margin-top: 2rem; padding: 20px; - border-radius: 10px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - font-family: Arial, sans-serif; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f9f9f9; } h2 { text-align: center; margin-bottom: 20px; - color: #333; } .form-group { @@ -19,63 +17,5 @@ h2 { } label { - display: block; font-weight: bold; - margin-bottom: 5px; - color: #555; -} - -input { - width: 100%; - padding: 10px; - font-size: 14px; - border: 1px solid #ccc; - border-radius: 5px; - background: #fff; -} - -input[readonly] { - background: #f3f3f3; -} - -.button-group { - display: flex; - justify-content: space-between; - margin-top: 20px; -} - -.btn { - padding: 10px 20px; - font-size: 14px; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s ease; -} - -.btn-primary { - background-color: #007bff; - color: white; -} - -.btn-primary:hover { - background-color: #0056b3; -} - -.btn-secondary { - background-color: #6c757d; - color: white; -} - -.btn-secondary:hover { - background-color: #5a6268; -} - -.btn-success { - background-color: #28a745; - color: white; -} - -.btn-success:hover { - background-color: #218838; } diff --git a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html index d68f32bfd2d6a14846503422e0273bc04effd127..677115e1ef16e7735abdfe809b92fdb65262747e 100644 --- a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html @@ -1,75 +1,148 @@ -<div class="form-container"> - <h2>Perfil de Usuario</h2> - <form [formGroup]="userForm" (ngSubmit)="saveChanges()"> - <!-- Campo Nombre --> - <div class="form-group"> - <label for="name">Nombre:</label> - <input - id="name" - type="text" - class="form-control" - formControlName="name" - [readonly]="!isEditing" - /> - </div> - - <!-- Campo Email --> - <div class="form-group"> - <label for="email">Email:</label> - <input - id="email" - type="email" - class="form-control" - formControlName="email" - [readonly]="!isEditing" - /> - </div> - - @if (isEditing) { - <!-- Campo Contraseña Actual (solo en edición) --> - <div class="form-group"> - <label for="currentPassword">Contraseña actual:</label> - <input - id="currentPassword" - type="password" - class="form-control" - formControlName="currentPassword" - placeholder="Introduce tu contraseña actual" - /> - </div> - - <!-- Campo Nueva Contraseña (solo en edición) --> - <div class="form-group"> - <label for="newPassword">Nueva contraseña:</label> - <input - id="newPassword" - type="password" - class="form-control" - formControlName="newPassword" - placeholder="Introduce tu nueva contraseña" - /> - </div> - } - - <!-- Grupo de Botones --> - <div class="button-group"> - @if (!isEditing) { - <button type="button" class="btn btn-primary" (click)="toggleEdit()"> - Editar - </button> - } @else { - <button type="button" class="btn btn-secondary" (click)="cancelEdit()"> - Cancelar - </button> - - <button - type="submit" - class="btn btn-success" - [disabled]="!userForm.valid" - > - Guardar - </button> +<div class="container"> + <mat-card> + <mat-card-title class="flex text-center p-4"> + <strong class="text-5xl">{{ titleText }}</strong> + </mat-card-title> + <mat-card-content> + @if (!isAuth) { + <div class="grid grid-flow-col mb-5"> + @if (!isEditing) { + <div> + @if (isHotelManager) { + <a [routerLink]="[getHotelsUri()]"> + <button class="btn btn-primary">Mis hoteles</button> + </a> + }@else if(!isAdmin){ + <a [routerLink]="[getBookingsUri()]"> + <button class="btn btn-primary">Mis Reservas</button> + </a> + } + </div> + } + <div> + <div class="flex items-center gap-5"> + <span class="text-3xl font-bold ml-auto"> Is Editing </span> + <mat-slide-toggle + [(ngModel)]="isEditing" + (toggleChange)="switchMode()" + /> + </div> + @if (isEditing) { + <div class="flex items-center gap-5 mt-3"> + <span class="text-3xl font-bold ml-auto"> Change password </span> + <mat-slide-toggle + [(ngModel)]="isChangePassword" + (toggleChange)="togglePassword()" + /> + </div> + } + </div> + </div> } - </div> - </form> + <form [formGroup]="userForm" (submit)="onSubmit()"> + @if (!isChangePassword) { @if (!isLogin){ + <!-- Campo Nombre --> + <div class="form-group"> + <label for="name">Nombre:</label> + <input + id="name" + type="text" + class="form-control" + autocomplete="name" + formControlName="name" + placeholder="Introduce tu nombre" + /> + </div> + } + <!-- Campo Email --> + <div class="form-group"> + <label for="email">Email:</label> + <input + id="email" + type="email" + autocomplete="email" + class="form-control" + formControlName="email" + placeholder="Introduce tu email" + /> + </div> + } @if ((isChangePassword || isAuth) && (isMeRoute || isAuth)) { + <!-- Campo Contraseña Actual (solo en edición) --> + <div class="form-group"> + <label for="currentPassword">{{ currentPasswordText }}:</label> + <input + id="currentPassword" + type="password" + class="form-control" + formControlName="currentPassword" + autocomplete="current-password" + placeholder="Introduce tu {{ currentPasswordText }}" + /> + </div> + } @if (isChangePassword) { + <!-- Campo Nueva Contraseña (solo en edición) --> + <div class="form-group"> + <label for="newPassword">Nueva contraseña:</label> + <input + id="newPassword" + type="password" + class="form-control" + formControlName="newPassword" + autocomplete="new-password" + placeholder="Introduce la nueva contraseña" + /> + </div> + }@if (isChangePassword || isRegister) { + <!-- Campo Contraseña Actual (solo en edición) --> + <div class="form-group"> + <label for="confirmPassword">Confirma contraseña:</label> + <input + id="confirmPassword" + type="password" + class="form-control" + formControlName="confirmPassword" + autocomplete="current-password" + placeholder="Confirma {{ + isChangePassword ? 'tu nueva contraseña' : 'la contraseña' + }}" + /> + </div> + } @if (isRegister) { + <div class="form-group text-2xl"> + <label class="text-2xl" for="rol">Rol:</label> + <mat-form-field class="w-full"> + <mat-label class="text-2xl">Seleccione un rol</mat-label> + <mat-select name="rol" formControlName="rol"> + @for (rol of rolOptions; track rol) { + <mat-option [value]="rol"> + <span class="text-2xl">{{ rol }}</span> + </mat-option> + } + </mat-select> + </mat-form-field> + </div> + } @if (!isViewUser) { @if (isAuth) { + <p class="text-right"> + @if (isLogin) { + <a [routerLink]="['/register']">¿No tienes cuenta?</a> + }@else { + <a [routerLink]="['/login']">¿Ya tienes cuenta?</a> + } + </p> + } + <!-- Grupo de Botones --> + <mat-card-actions class="flex justify-center mb-5"> + <!-- [disabled]="registerForm.invalid" --> + <button + type="submit" + class="btn btn-success text-4xl" + [disabled]="!validForm()" + > + {{ submitButtonText }} + </button> + </mat-card-actions> + } + </form> + </mat-card-content> + </mat-card> </div> diff --git a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.ts b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.ts index 558e1c58a73e3d7ef1eaf852bce1a989a40bc42b..1c6d272d01c5a9b899b1c77a84773fe47c43c7bb 100644 --- a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.ts +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.ts @@ -7,87 +7,379 @@ import { FormsModule, } from '@angular/forms'; import { UserClientService } from '../../../../shared/user-client.service'; -import { users } from '../../../../../mocks/users'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { MatCardModule } from '@angular/material/card'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { SessionService } from '../../../../shared/session.service'; +import { Session, UserRol, UserRolesArray } from '../../../../types'; +import { MatSelectModule } from '@angular/material/select'; +import { Observable } from 'rxjs'; +import { getBasePath } from '../../../../utils/utils'; +import { environment } from '../../../../../environments/environment'; + +type EditMode = + | 'Login' + | 'Register' + | 'ViewUser' + | 'EditUser' + | 'ChangePassword' + | 'Other'; +const defaultUser: Session = { + id: 0, + name: 'test', + email: 'test@dev.com', + rol: 'ADMIN', +}; @Component({ standalone: true, selector: 'app-user-form', templateUrl: './user-form.component.html', styleUrls: ['./user-form.component.css'], - imports: [ReactiveFormsModule, FormsModule], + imports: [ + ReactiveFormsModule, + FormsModule, + RouterModule, + MatSlideToggleModule, + MatCardModule, + MatInputModule, + MatSelectModule, + MatFormFieldModule, + ], }) export class UserFormComponent implements OnInit { userForm!: FormGroup; - isEditing = false; - id = 0; + rolOptions: UserRol[] = UserRolesArray; + mode: EditMode = 'Other'; + isMeRoute = false; + + /** is editing the user data */ + isEditing: boolean = false; + /** is showing the user data */ + isViewUser: boolean = false; + /** is trying to do an auth action*/ + isAuth: boolean = false; + /** is trying to login */ + isLogin: boolean = false; + /** is trying to register */ + isRegister: boolean = false; + /** don't want to update the password */ + isChangePassword = false; + + titleText = 'Mis datos'; + submitButtonText = 'Submit'; + currentPasswordText = 'Contraseña actual'; + + user = defaultUser; + isHotelManager = false; + isAdmin = false; constructor( private fb: FormBuilder, + private sessionService: SessionService, private userService: UserClientService, - private route: ActivatedRoute + private route: ActivatedRoute, + private router: Router ) {} + isEditRoute(urlSegments: any[], isMeRoute: boolean): boolean { + return isMeRoute + ? urlSegments.length >= 2 && urlSegments[1].path === 'edit' + : urlSegments.length >= 4 && urlSegments[3].path === 'edit'; + } + + isChangePasswordRoute(urlSegments: any[], isMeRoute: boolean): boolean { + return isMeRoute + ? urlSegments.length >= 2 && urlSegments[1].path === 'change-passwd' + : urlSegments.length >= 4 && urlSegments[3].path === 'change-passwd'; + } + + isViewUserRoute(urlSegments: any[], isMeRoute: boolean): boolean { + return isMeRoute + ? urlSegments.length === 1 + : (urlSegments.length === 1 && urlSegments[0].path === 'admin') || + urlSegments.length === 3; + } + + isAuthRoute(urlSegments: any[], route: string): boolean { + return urlSegments.length === 1 && urlSegments[0].path === route; + } + ngOnInit(): void { - this.initializeForm(); - this.route.paramMap.subscribe({ - next: (params) => { - const id = Number(params.get('id')); - if (id) { - this.id = id; - this.isEditing = true; - this.loadUserData(); - } - }, - }); + this.setUp(); + + // const auth = this.session.getSession(); + // this.user = auth; + // this.userForm.patchValue({ + // name: this.user.name, + // email: this.user.email, + // }); } private initializeForm(): void { + // Solicitar contraseña actual + const confirmIdentity = + !this.isViewUser && + (this.isChangePassword || this.isAuth) && + (this.isMeRoute || this.isAuth); + // Solicitar nueva contraseña + const isChangePassword = this.isChangePassword; + // Solicitar confirmación de contraseña + const confirmPassword = + !this.isViewUser && (this.isChangePassword || this.isRegister); + // Solicitar email + const emailNotRequired = this.isViewUser || isChangePassword; + // Solicitar nombre + const nameNotRequired = emailNotRequired || this.isLogin; + // Solicitar rol + const rolNotRequired = !this.isRegister; + // console.log({ + // confirmIdentity, + // isChangePassword, + // confirmPassword, + // emailNotRequired, + // nameNotRequired, + // rolNotRequired, + // }); + this.userForm = this.fb.group({ - name: [{ value: '', disabled: true }, Validators.required], + name: [{ value: '', disabled: nameNotRequired }, Validators.required], email: [ - { value: '', disabled: true }, + { value: '', disabled: emailNotRequired }, [Validators.required, Validators.email], ], - currentPassword: [''], // Solo habilitado en modo edición - newPassword: [''], // Solo habilitado en modo edición + currentPassword: [ + { value: '', disabled: !confirmIdentity }, + Validators.required, + ], // Solo habilitado en edición + newPassword: [ + { value: '', disabled: !isChangePassword }, + Validators.required, + ], // Solo habilitado en edición + confirmPassword: [ + { value: '', disabled: !confirmPassword }, + Validators.required, + ], + rol: [{ value: '', disabled: rolNotRequired }, Validators.required], // Solo habilitado en registro }); } - private loadUserData(): void { - // this.userService.getCurrentUser().subscribe((user) => { - console.log({ id: this.id }); - users - .filter((u) => u.id == this.id) - .slice(0) - .map((user) => { - this.userForm.patchValue({ - name: user.name, - email: user.email, - }); + setUp() { + const urlSeg = this.route.snapshot.url; + if (this.isAuthRoute(urlSeg, 'login')) { + // Login + this.isAuth = true; + this.isLogin = true; + this.mode = 'Login'; + this.currentPasswordText = 'Contraseña'; + this.submitButtonText = 'Login'; + this.titleText = 'Login'; + } else if (this.isAuthRoute(urlSeg, 'register')) { + // Register + this.isAuth = true; + this.isRegister = true; + this.mode = 'Register'; + this.currentPasswordText = 'Contraseña'; + this.submitButtonText = 'Create'; + this.titleText = 'Register'; + } else { + // Identificar si estamos usando /me o /users/:id + getBasePath(this.route); + const isMeRoute = urlSeg[0].path === 'me'; + this.isMeRoute = isMeRoute; + + if (this.isEditRoute(urlSeg, isMeRoute)) { + this.isEditing = true; + this.mode = 'EditUser'; + this.titleText = 'Editar mis datos'; + } else if (this.isChangePasswordRoute(urlSeg, isMeRoute)) { + this.mode = 'ChangePassword'; + this.isEditing = true; + this.isChangePassword = true; + this.currentPasswordText = 'Contraseña actual'; + this.titleText = 'Cambiar mi contraseña'; + } else if (this.isViewUserRoute(urlSeg, isMeRoute)) { + this.mode = 'ViewUser'; + this.isViewUser = true; + this.titleText = 'Mis datos'; + } + + this.submitButtonText = 'Update'; + } + + this.initializeForm(); + if (!this.isAuth) { + this.loadUser(); + } else { + this.sessionService.getSession().subscribe({ + next: (session) => { + if (session) { + this.router.navigateByUrl( + this.sessionService.getMainPage(session.rol) + ); + } + }, }); + } } - toggleEdit(): void { - this.isEditing = true; - this.userForm.get('name')?.enable(); - this.userForm.get('email')?.enable(); + getHotelsUri() { + const basePath = getBasePath(this.route); // Obtener la base: '/me' o '/users/:id' + return `${basePath}/hotels`; } - cancelEdit(): void { - this.isEditing = false; - this.loadUserData(); // Volver a cargar los datos originales - this.userForm.get('name')?.disable(); - this.userForm.get('email')?.disable(); + getBookingsUri() { + const basePath = getBasePath(this.route); // Obtener la base: '/me' o '/users/:id' + return `${basePath}/bookings`; } - saveChanges(): void { - if (this.userForm.valid) { - const updatedData = this.userForm.value; - this.userService.updateUser(updatedData).subscribe(() => { - this.isEditing = false; - this.loadUserData(); - }); + togglePassword() { + const basePath = getBasePath(this.route); // Obtener la base: '/me' o '/users/:id' + + if (this.mode === 'EditUser') { + this.router.navigateByUrl(`${basePath}/change-passwd`); + } else if (this.mode === 'ChangePassword') { + this.router.navigateByUrl(`${basePath}/edit`); } } + + switchMode() { + const basePath = getBasePath(this.route); // Obtener la base: '/me' o '/users/:id' + console.log({ ...this }); + if (this.mode === 'EditUser') { + this.router.navigateByUrl(basePath); + } else if (this.mode === 'ViewUser') { + this.router.navigateByUrl(`${basePath}/edit`); + } + } + + private resolve(): Observable<any> { + const userId = this.route.snapshot.paramMap.get('id'); + console.log({ userId }); + return userId + ? this.userService.getUser(Number(userId)) + : this.sessionService.getSession(); + } + + private loadUser() { + // this.setData(); + this.resolve().subscribe({ + next: (user) => { + this.user = user; + this.isHotelManager = (user.rol as UserRol) === 'HOTEL_ADMIN'; + this.isAdmin = (user.rol as UserRol) === 'ADMIN'; + this.setData(); + }, + error: (error) => { + console.error('Error:', error); + }, + }); + } + + private setData() { + this.userForm.patchValue({ + name: this.user.name, + email: this.user.email, + rol: this.user.rol, + }); + } + + validForm() { + const validForm = this.userForm.valid; + const differentData = + this.isEditing && !this.isChangePassword && this.modifiedData(); + const validatePassword = this.validatePassword(); + + return validForm && (differentData || validatePassword || this.isLogin); + } + + private modifiedData() { + return ( + this.userForm.get('name')?.value !== this.user.name || + this.userForm.get('email')?.value !== this.user.email + ); + } + + private validatePassword() { + const { currentPassword, newPassword, confirmPassword } = + this.userForm.value; + const updatePasswordValidate = + this.isEditing && + this.isChangePassword && + newPassword === confirmPassword && + currentPassword !== newPassword; + const registerPasswordValidate = + this.isRegister && currentPassword === confirmPassword; + return updatePasswordValidate || registerPasswordValidate; + } + + onSubmit() { + const data = this.userForm.value; + console.log({ data }); + + switch (this.mode) { + case 'Login': + this.login(data.email, data.currentPassword); + break; + case 'Register': + this.register(data.name, data.email, data.currentPassword, data.rol); + break; + case 'EditUser': + this.updateUser(data.name, data.email); + break; + case 'ChangePassword': + this.changePassword(data.currentPassword, data.newPassword); + break; + default: + break; + } + } + + private login(email: string, password: string) { + this.sessionService.login(email, password).subscribe({ + next: (r: any) => { + this.router.navigateByUrl(r.mainPage); + }, + error: (error) => { + console.error(error); + // this.toastr.error('Invalid email or password'); + }, + }); + } + + private register( + name: string, + email: string, + password: string, + rol: UserRol + ) { + console.log({ name, email, password, rol }); + this.sessionService.register(name, email, password, rol).subscribe({ + next: (r: any) => { + this.router.navigateByUrl(r.mainPage); + }, + error: (error) => { + console.error(error); + // this.toastr.error('Invalid email or password'); + }, + }); + } + + private updateUser(name: string, email: string) { + this.userService.updateUser(this.user.id, { name, email }).subscribe({ + next: () => { + this.router.navigateByUrl(getBasePath(this.route)); + }, + error: (error) => { + console.error(error); + // this.toastr.error('Invalid email or password'); + }, + }); + } + + private changePassword(password: string | undefined, newPassword: string) { + alert('Unimplemented yet'); + } } diff --git a/angular/RestClient/src/app/core/navigation/navigation.component.css b/angular/RestClient/src/app/core/navigation/navigation.component.css index e3b01c1fac3de8d58e16185d24ac3791394e7b67..dd40d570ba45e75417e8615f515a70fea3667aec 100644 --- a/angular/RestClient/src/app/core/navigation/navigation.component.css +++ b/angular/RestClient/src/app/core/navigation/navigation.component.css @@ -25,7 +25,7 @@ a.nav-link:visited { a.nav-link:hover { font-weight: bold; text-decoration: underline; - color: yellow; + color: rgb(112, 112, 112); transition: transform 0.3s ease; transform: scale(1.5); } diff --git a/angular/RestClient/src/app/core/navigation/navigation.component.html b/angular/RestClient/src/app/core/navigation/navigation.component.html index 94a99ba6dbf61fec024f7818dce9c8cc41115c6a..3cfc3e49603a529a745791cb68b9916d62fdbe45 100644 --- a/angular/RestClient/src/app/core/navigation/navigation.component.html +++ b/angular/RestClient/src/app/core/navigation/navigation.component.html @@ -1,20 +1,7 @@ <nav> <ul class="nav"> - <li><a class="btn nav-link" [routerLink]="['/']">Home - Usuarios</a></li> - <li> - <a class="btn nav-link" [routerLink]="['/hotels', 'new']" - >Registrar Hotel</a - > - </li> <li><a class="btn nav-link" [routerLink]="['/hotels']">Hoteles</a></li> - <li> - <a class="btn nav-link" [routerLink]="['/bookings', 'search']" - >Nueva Reserva</a - > - </li> - <!-- Solucionar que no va --> - <!-- <li class="ml-auto"> --> - <li style="margin-left: auto"> + <li class="ml-auto"> @if (isLogged){ <!-- Dropdown para usuario registrado --> @@ -58,8 +45,8 @@ class="btn bg-blue-500 text-white hover:bg-blue-600 ml-auto" (click)="login()" > - <!-- <a class="simple" [routerLink]="['/login']"> --> - <a class="simple"> + <a class="simple" [routerLink]="['/login']"> + <!-- <a class="simple"> --> <button class="flex items-center gap-3"> <span class="text-4xl">Login</span> <mat-icon>login</mat-icon> diff --git a/angular/RestClient/src/app/core/navigation/navigation.component.ts b/angular/RestClient/src/app/core/navigation/navigation.component.ts index 1e7a72d5eb50276981b4f00a3950847105e06f0d..f6c8317d8abca7534558fe3ebabe62c1ec5c2c45 100644 --- a/angular/RestClient/src/app/core/navigation/navigation.component.ts +++ b/angular/RestClient/src/app/core/navigation/navigation.component.ts @@ -3,10 +3,11 @@ import { RouterModule } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; import { MatIconModule } from '@angular/material/icon'; -import { User, UserRol } from '../../types'; -import { AuthInstance, SessionService } from '../../shared/session.service'; +import { Session, User, UserRol } from '../../types'; +import { SessionService } from '../../shared/session.service'; import { UserClientService } from '../../shared/user-client.service'; import { AuthClientService } from '../../shared/auth-client.service'; +import { Observable } from 'rxjs'; var comp_id = 0; @@ -34,7 +35,7 @@ export class NavigationComponent implements OnInit { @ViewChild(MatMenuTrigger) trigger?: MatMenuTrigger; isLogged = false; - user: AuthInstance = { + user: Session = { id: 0, name: '', email: '', @@ -42,23 +43,24 @@ export class NavigationComponent implements OnInit { }; sections: Section[] = []; - constructor( - private sessionService: SessionService, - private auth: AuthClientService - ) {} + constructor(private sessionService: SessionService) {} ngOnInit() { this.loadUser(); } loadUser() { - const isLogged = this.sessionService.isValid(); - if (isLogged) { - this.user = this.sessionService.getSession(); - console.log({ user: this.user }); - this.sections = this.genSections(); - } - this.isLogged = isLogged; + this.sessionService.getSession().subscribe({ + next: (session) => { + if (session) { + this.user = session; + this.isLogged = true; + this.sections = this.genSections(); + } else { + this.isLogged = false; + } + }, + }); } toggleDropdown() { @@ -80,43 +82,45 @@ export class NavigationComponent implements OnInit { icon: 'calendar_today', text: 'Reservas', allowRoles: ['CLIENT'], - link: '/users/1/bookings', - // link: '/bookings', + link: '/me/bookings', }, { id: genId(), icon: 'hotel', text: 'Hoteles', allowRoles: ['HOTEL_ADMIN'], - link: '/hotels', + link: '/me/hotels', + }, + { + id: genId(), + icon: 'settings', + text: 'Admin Zone', + allowRoles: ['ADMIN'], + link: '/admin', + }, + { + id: genId(), + icon: 'group', + text: 'Users', + allowRoles: ['ADMIN'], + link: '/admin/users', }, ]; genSections() { return this.schemaSections.filter( (section) => - this.user.rol === 'ADMIN' || // Es administrador del sistema !section.allowRoles || section.allowRoles.length === 0 || // No tiene limitación section.allowRoles.includes(this.user.rol) // El rol del usuario es aceptado ); } - login() { - const email = 'client@dev.com'; - this.auth.login(email, 'NQSASorry').subscribe({ - next: (data: any) => { - console.log(email, data); - this.sessionService.login(data); - this.loadUser(); - }, - }); - } + login() {} logout() { - if (confirm('You are trying to logout')) { - this.sessionService.logout(); - this.loadUser(); - } + // if (confirm('You are trying to logout')) + this.sessionService.logout(); + this.loadUser(); } } diff --git a/angular/RestClient/src/app/page/unauthorized/unauthorized.component.css b/angular/RestClient/src/app/page/unauthorized/unauthorized.component.css new file mode 100644 index 0000000000000000000000000000000000000000..8c77ba55f871f914a54f60a38e0419dafb94e1b0 --- /dev/null +++ b/angular/RestClient/src/app/page/unauthorized/unauthorized.component.css @@ -0,0 +1,44 @@ +/* El contenedor principal ocupa toda la pantalla disponible */ +.unauthorized-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 88vh; + background-color: #f8f9fa; + flex-direction: column; /* Alinea los elementos en columna */ +} + +/* Contenedor interno centrado con margen */ +.content { + text-align: center; + padding: 2rem; + border: 1px solid #ddd; + border-radius: 10px; + background: #ffffff; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); + max-width: 400px; /* Limitamos el ancho del contenido */ + width: 100%; /* Asegura que no exceda el tamaño máximo */ + + .icon { + /* font-size: 80px; */ + color: #ff5722; + margin-bottom: 1rem; + } + + .title { + font-size: 2rem; + color: #333333; + margin-bottom: 0.5rem; + } + + .message { + font-size: 1.2rem; + color: #555555; + margin-bottom: 2rem; + } + + button { + padding: 0.5rem 2rem; + font-size: 1rem; + } +} diff --git a/angular/RestClient/src/app/page/unauthorized/unauthorized.component.html b/angular/RestClient/src/app/page/unauthorized/unauthorized.component.html new file mode 100644 index 0000000000000000000000000000000000000000..505aed70edb5c7f010c70db46e57ed4fc48f54d3 --- /dev/null +++ b/angular/RestClient/src/app/page/unauthorized/unauthorized.component.html @@ -0,0 +1,10 @@ +<div class="unauthorized-container"> + <div class="content"> + <mat-icon class="icon">lock</mat-icon> + <h1 class="title">Acceso Denegado</h1> + <p class="message">No tienes permisos para acceder a esta página.</p> + <a [routerLink]="[mainPage]"> + <button mat-raised-button color="primary">Volver al inicio</button> + </a> + </div> +</div> diff --git a/angular/RestClient/src/app/page/unauthorized/unauthorized.component.spec.ts b/angular/RestClient/src/app/page/unauthorized/unauthorized.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bab5ad8d9e3b005c4068dffa26dacb9de7417c15 --- /dev/null +++ b/angular/RestClient/src/app/page/unauthorized/unauthorized.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UnauthorizedComponent } from './unauthorized.component'; + +describe('UnauthorizedComponent', () => { + let component: UnauthorizedComponent; + let fixture: ComponentFixture<UnauthorizedComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UnauthorizedComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UnauthorizedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/angular/RestClient/src/app/page/unauthorized/unauthorized.component.ts b/angular/RestClient/src/app/page/unauthorized/unauthorized.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..061a73dbe09d717285d19188179a450cd780a699 --- /dev/null +++ b/angular/RestClient/src/app/page/unauthorized/unauthorized.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { SessionService } from '../../shared/session.service'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; + +@Component({ + selector: 'app-unauthorized', + standalone: true, + imports: [RouterModule, MatIconModule, MatButtonModule], + templateUrl: './unauthorized.component.html', + styleUrl: './unauthorized.component.css', +}) +export class UnauthorizedComponent { + mainPage: string = ''; + + constructor(private sessionService: SessionService) { + this.sessionService.getSession().subscribe({ + next: (session) => { + this.mainPage = session + ? sessionService.getMainPage(session.rol) + : '/login'; + }, + }); + } +} diff --git a/angular/RestClient/src/app/auth/auth.interceptor.ts b/angular/RestClient/src/app/security/auth.interceptor.ts similarity index 70% rename from angular/RestClient/src/app/auth/auth.interceptor.ts rename to angular/RestClient/src/app/security/auth.interceptor.ts index 63a139b74505e95db8b5c3c88f508696e7ff73be..4a3dadfdaa637c526d0beb436546a9117359f49f 100644 --- a/angular/RestClient/src/app/auth/auth.interceptor.ts +++ b/angular/RestClient/src/app/security/auth.interceptor.ts @@ -1,6 +1,7 @@ import { HttpInterceptorFn } from '@angular/common/http'; import { inject } from '@angular/core'; import { LocalStorageService } from '../shared/local-storage.service'; +import { SessionService } from '../shared/session.service'; const excluded = ['/login', '/register']; @@ -10,18 +11,22 @@ function isExcludedUrl(url: string) { export const authRequest: HttpInterceptorFn = (req, next) => { // Obtener el token desde localStorage (o cualquier otro mecanismo) - const storage = inject(LocalStorageService); // Obtener instancia del servicio - const token = storage.read<{ token: string }>('token'); + const session = inject(SessionService); // Obtener instancia del servicio + const isLogged = session.isLogged(); - if (isExcludedUrl(req.url) || !token) { + if (isExcludedUrl(req.url) || !isLogged) { return next(req); // No modificar la solicitud } + const token = session.getToken(); + + console.log('TOKEN:', { token }); + // Clonar la solicitud y agregar el token al encabezado si existe const authReq = token ? req.clone({ setHeaders: { - Authorization: `Bearer ${token.token}`, + Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }) diff --git a/angular/RestClient/src/app/security/rol.guard.spec.ts b/angular/RestClient/src/app/security/rol.guard.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0086470eb8061f59d7eea03aed354b751b725743 --- /dev/null +++ b/angular/RestClient/src/app/security/rol.guard.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { CanActivateFn } from '@angular/router'; + +import { rolGuard } from './rol.guard'; + +describe('rolGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => rolGuard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeGuard).toBeTruthy(); + }); +}); diff --git a/angular/RestClient/src/app/security/rol.guard.ts b/angular/RestClient/src/app/security/rol.guard.ts new file mode 100644 index 0000000000000000000000000000000000000000..53017db2422c5f9e8c62c37622786bc4988673bf --- /dev/null +++ b/angular/RestClient/src/app/security/rol.guard.ts @@ -0,0 +1,43 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { SessionService } from '../shared/session.service'; +import { UserRol, Session } from '../types'; +import { map } from 'rxjs'; + +export const rolGuard: CanActivateFn = (route, state) => { + const sessionService = inject(SessionService); + const router = inject(Router); + // Obtén el rol esperado desde los datos de la ruta + const expectedRole = route.data?.['expectedRole']; + + // Verifica si el usuario tiene sesión activa + const session = sessionService.isValid(); + + if (!session) { + console.log('no session'); + router.navigate(['/login']); + return false; + } + + return sessionService.getSession().pipe( + map((session: Session | null) => { + if (!session) return false; + + if ( + Array.isArray(expectedRole) && + (expectedRole as UserRol[]).includes(session.rol) + ) { + console.log('Rol in Rol arry'); + return true; + } else if (session.rol === expectedRole) { + console.log('Rol valido'); + return true; + } + console.log('Unautorizado'); + + // Redirige si el usuario no tiene el rol necesario + router.navigate(['/unauthorized']); + return false; + }) + ); +}; diff --git a/angular/RestClient/src/app/shared/auth-client.service.ts b/angular/RestClient/src/app/shared/auth-client.service.ts index 931fc7dbacbec3db5e5d048722fa5980017355e7..99d3ebbfc0c91cc88ba2f4266cd1f895ccbfebd9 100644 --- a/angular/RestClient/src/app/shared/auth-client.service.ts +++ b/angular/RestClient/src/app/shared/auth-client.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { environment } from '../../../environments/environment'; +import { environment } from '../../environments/environment'; import { HttpClient } from '@angular/common/http'; @Injectable({ @@ -35,12 +35,12 @@ export class AuthClientService { email, password, rol, - }, - { - headers: { - 'Content-Type': 'application/json', - }, } + // { + // headers: { + // 'Content-Type': 'application/json', + // }, + // } ); } } diff --git a/angular/RestClient/src/app/shared/booking-client.service.ts b/angular/RestClient/src/app/shared/booking-client.service.ts index 3e44163b0944263f937334320090ec3ee2ef786e..a15867105f00cf0bd6ea093a4a64bebbb3fd048f 100644 --- a/angular/RestClient/src/app/shared/booking-client.service.ts +++ b/angular/RestClient/src/app/shared/booking-client.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { environment } from '../../../environments/environment'; +import { environment } from '../../environments/environment'; import { Booking } from '../types/Booking'; // Ajusta la ruta a tu modelo Booking @Injectable({ @@ -14,11 +14,11 @@ export class BookingClientService { // Método para crear una nueva reserva createBooking(bookingRequest: Booking): Observable<Booking> { - return this.http.post<Booking>(this.URI, bookingRequest, { - headers: new HttpHeaders({ - 'Content-Type': 'application/json', - }), - }); + const { startDate, endDate } = bookingRequest; + const end = endDate.toISOString(); + console.log({ bookingRequest, end }); + + return this.http.post<Booking>(this.URI, bookingRequest); } // Método para obtener todas las reservas diff --git a/angular/RestClient/src/app/shared/hotel-client.service.ts b/angular/RestClient/src/app/shared/hotel-client.service.ts index 87756a392895ecc95b745101b7accaef0e20a3e1..6f5a9203df6dcbe5c0cdac2202eddd04ad7141ee 100644 --- a/angular/RestClient/src/app/shared/hotel-client.service.ts +++ b/angular/RestClient/src/app/shared/hotel-client.service.ts @@ -1,14 +1,20 @@ import { Injectable } from '@angular/core'; -import { environment } from '../../../environments/environment'; +import { environment } from '../../environments/environment'; import { HttpClient } from '@angular/common/http'; import { Hotel, Room } from '../types'; +import { SessionService } from './session.service'; +import { catchError, map, switchMap, throwError } from 'rxjs'; +import { log } from 'console'; @Injectable({ providedIn: 'root', }) export class HotelClientService { private readonly URI = environment.hotelAPI; - constructor(private http: HttpClient) {} + constructor( + private http: HttpClient, + private sessionService: SessionService + ) {} getHotel(id: number) { const url = `${this.URI}/${id}`; @@ -22,15 +28,30 @@ export class HotelClientService { deleteHotel(id: number) { const url = `${this.URI}/${id}`; - return this.http.delete(url, { observe: 'response', responseType: 'text' }); + return this.http.delete(url); } addHotel(hotel: Hotel) { const url = `${this.URI}`; - return this.http.post(url, hotel, { - observe: 'response', - responseType: 'text', - }); + return this.sessionService.getSession().pipe( + map((session) => { + if (!session) { + throw new Error('No session found'); + } + const { id } = session; + const hotelWithHM = { ...hotel, hotelManager: { id } }; + return hotelWithHM; + }), + switchMap((hotelWithHM) => + this.http.post(url, hotelWithHM).pipe( + // Opcional: Puedes manejar transformaciones o errores aquí. + catchError((err) => { + console.error('Error al agregar hotel:', err); + return throwError(() => err); + }) + ) + ) + ); } alterRoomAvailability( @@ -39,14 +60,7 @@ export class HotelClientService { availability: boolean ) { const url = `${this.URI}/${hotelId}/rooms/${roomId}`; - return this.http.patch( - url, - { available: availability }, - { - observe: 'response', - responseType: 'text', - } - ); + return this.http.patch(url, { available: availability }); } getRoomsAvailableInDateRange(hotelId: number, start: Date, end: Date) { diff --git a/angular/RestClient/src/app/shared/session.service.ts b/angular/RestClient/src/app/shared/session.service.ts index 82e9daaceed948aa6ed973b710f704421f5b30f5..796036c5fa4fc2e6b20ab3d9049448be30b8e027 100644 --- a/angular/RestClient/src/app/shared/session.service.ts +++ b/angular/RestClient/src/app/shared/session.service.ts @@ -1,58 +1,157 @@ import { Injectable } from '@angular/core'; import { LocalStorageService } from './local-storage.service'; -import { UserRol } from '../types'; - +import { PersistenToken, Session, UserRol } from '../types'; +import { BehaviorSubject, Observable, throwError } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; import { jwtDecode } from 'jwt-decode'; - -export interface AuthInstance { - id: number; - name: string; - email: string; - rol: UserRol; -} - -interface PersistenToken { - token: string; -} +import { AuthClientService } from './auth-client.service'; +import { Router } from '@angular/router'; @Injectable({ providedIn: 'root', }) export class SessionService { - constructor(private storage: LocalStorageService) {} - private tokenKey = 'token'; + private session$: BehaviorSubject<Session | null>; + mainPage = '/me'; + + constructor( + private router: Router, + private storage: LocalStorageService, + private authService: AuthClientService + ) { + // Inicializar el estado de sesión desde el token almacenado + const initialSession = this.loadSessionFromToken(); + console.log({ initialSession }); + + this.session$ = new BehaviorSubject<Session | null>(initialSession); + } + + getMainPage(rol: UserRol) { + return rol === 'ADMIN' ? '/admin' : '/me'; + } - login(session: PersistenToken) { - this.storage.save(this.tokenKey, session); + private setSession(resp: any) { + const decoded = jwtDecode<{ user: Session }>(resp.token); + this.session$.next(decoded.user); + this.storage.save(this.tokenKey, { ...resp, session: decoded.user }); + const mainPage = this.getMainPage(decoded.user.rol as UserRol); + return { ...resp, mainPage }; } - logout() { + /** + * Realiza el login y actualiza el estado de la sesión. + */ + login(email: string, password: string): Observable<any> { + return this.authService.login(email, password).pipe( + map((r) => this.setSession(r)), + catchError((error) => { + console.error('Login failed', error); + return throwError(() => new Error('Login failed')); + }) + ); + } + + /** + * Realiza el registro, guarda el token y actualiza el estado de la sesión. + */ + register( + name: string, + email: string, + password: string, + rol: UserRol + ): Observable<any> { + return this.authService.register(name, email, password, rol).pipe( + map((r) => this.setSession(r)), + catchError((error) => { + console.error('Registration failed', error); + return throwError(() => new Error('Registration failed')); + }) + ); + } + + /** + * Realiza el logout, elimina el token y limpia el estado de sesión. + */ + logout(): void { this.storage.remove(this.tokenKey); + this.session$.next(null); + this.router.navigate(['/login']); + } + + getSaved() { + return this.storage.read<PersistenToken>(this.tokenKey); } - getToken() { - const savedToken = this.storage.read<PersistenToken>(this.tokenKey); - if (!savedToken) throw new Error('No session'); - return savedToken.token; + /** + * Obtiene el token almacenado. Lanza un error si no hay sesión activa. + */ + getToken(): string { + const saved = this.getSaved(); + if (!saved) { + throw new Error('No session'); + } + return saved.token; } - getSession() { - const token = this.getToken(); - const r = jwtDecode<{ user: AuthInstance }>(token); - return r.user; + /** + * Proporciona un Observable del estado de la sesión. + */ + getSession(): Observable<Session | null> { + return this.session$.asObservable(); } - isLogged() { - return !!this.storage.read<PersistenToken>(this.tokenKey); + updateData(data: Partial<Session>) { + // const session: Session = { ...this.session$.getValue() } as Session; + const saved = this.getSaved(); + if (!saved) return; + const session = { ...saved.session, ...data } as Session; + this.storage.save(this.tokenKey, { + ...saved, + session, + }); + this.session$.next(session); } - isValid() { - console.warn({ log: this.isLogged() }); + /** + * Verifica si el usuario está logueado. + */ + isLogged(): boolean { + return !!this.session$.getValue(); + } + + /** + * Valida si el token almacenado es válido (no expirado). + */ + isValid(): boolean { if (!this.isLogged()) return false; - const token = this.getToken(); - const r = jwtDecode(token); - // Validate if the token have been expired or not - return r.exp! > Math.floor(Date.now() / 1000); + + try { + const token = this.getToken(); + const decoded = jwtDecode<{ exp: number }>(token); + const valid = decoded.exp > Math.floor(Date.now() / 1000); + console.log({ valid, rem: decoded.exp - Math.floor(Date.now() / 1000) }); + if (!valid) { + this.logout(); + } + return valid; + } catch (error) { + console.error('Token validation failed', error); + return false; + } + } + + /** + * Carga la sesión desde el token almacenado. + */ + private loadSessionFromToken(): Session | null { + try { + // const token = this.getToken(); + // const decoded = jwtDecode<{ user: Session }>(token); + // return decoded.user; + return this.getSaved()!.session!; + } catch { + return null; // Retornar null si no hay token válido. + } } } diff --git a/angular/RestClient/src/app/shared/user-client.service.ts b/angular/RestClient/src/app/shared/user-client.service.ts index 2b2592e00c49b292df6799a76b4a6feb95b48380..36dd28ae09ce46e8e49af70548588c229b068dd0 100644 --- a/angular/RestClient/src/app/shared/user-client.service.ts +++ b/angular/RestClient/src/app/shared/user-client.service.ts @@ -1,7 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { environment } from '../../../environments/environment'; -import { Client, User, UserState } from '../types'; +import { environment } from '../../environments/environment'; +import { Client, Session, User, UserState } from '../types'; +import { SessionService } from './session.service'; +import { tap } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -9,7 +11,10 @@ import { Client, User, UserState } from '../types'; export class UserClientService { private readonly URI = environment.userAPI; - constructor(private http: HttpClient) {} + constructor( + private http: HttpClient, + private sessionService: SessionService + ) {} // Obtener un usuario por ID getUser(userId: number) { @@ -37,17 +42,13 @@ export class UserClientService { ); } - // Obtener el usuario actual (autenticado) - getCurrentUser() { - return this.http.get<User>(`${this.URI}/me`); - } - // Actualizar los datos del usuario - updateUser(user: Partial<User>) { - return this.http.patch(`${this.URI}/me`, user, { - observe: 'response', - responseType: 'text', - }); + updateUser(userId: number, user: Partial<User>) { + return this.http.put(`${this.URI}/${userId}`, user).pipe( + tap(() => { + this.sessionService.updateData(user as Partial<Session>); + }) + ); } // Cambiar la contraseña del usuario diff --git a/angular/RestClient/src/app/types/Session.d.ts b/angular/RestClient/src/app/types/Session.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..4dd1f742866d586be8be2ac02487d23ede92a486 --- /dev/null +++ b/angular/RestClient/src/app/types/Session.d.ts @@ -0,0 +1,11 @@ +export interface Session { + id: number; + name: string; + email: string; + rol: UserRol; +} + +interface PersistenToken { + token: string; + session?: Session; +} diff --git a/angular/RestClient/src/app/types/index.ts b/angular/RestClient/src/app/types/index.ts index b974a05f55ecc2a2b9a6e953882d7cc526689ab6..09338254dd1b87fdc9aa00cb7342f0865fc382de 100644 --- a/angular/RestClient/src/app/types/index.ts +++ b/angular/RestClient/src/app/types/index.ts @@ -1,4 +1,5 @@ import { RoomType } from './Room'; +import { UserRol } from './User'; export type * from './User'; export type * from './Address'; @@ -7,3 +8,5 @@ export type * from './Room'; export const roomTypeArray: RoomType[] = ['SINGLE', 'DOUBLE', 'SUITE']; export type * from './Booking'; export type * from './User'; +export const UserRolesArray: UserRol[] = ['CLIENT', 'HOTEL_ADMIN', 'ADMIN']; +export type * from './Session'; diff --git a/angular/RestClient/src/app/utils/utils.ts b/angular/RestClient/src/app/utils/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c97d18b18ba30a2588acf79ecddb5a0f9747146 --- /dev/null +++ b/angular/RestClient/src/app/utils/utils.ts @@ -0,0 +1,18 @@ +import { ActivatedRoute } from '@angular/router'; + +export function getBasePath(route: ActivatedRoute): string { + const urlSegments = route.snapshot.url; + if (urlSegments[0].path === 'me') { + return '/me'; + } else if ( + urlSegments.length >= 3 && + urlSegments[0].path === 'admin' && + urlSegments[1].path === 'users' && + urlSegments[2] + ) { + return `/admin/users/${urlSegments[2]}`; // Devuelve la ruta con el ID del usuario + } else if (urlSegments[0].path === 'admin') { + return '/me'; + } + throw new Error('Invalid route structure'); // Manejo de errores si la URL no es válida +} diff --git a/angular/RestClient/src/environments/environment.prod.ts b/angular/RestClient/src/environments/environment.prod.ts new file mode 100644 index 0000000000000000000000000000000000000000..94288538c0da8e4d87a31280989c466176416195 --- /dev/null +++ b/angular/RestClient/src/environments/environment.prod.ts @@ -0,0 +1,9 @@ +const monolithUrl = 'http://rooms-booking-api:8080'; + +export const environment = { + production: true, + authAPI: 'http://auth-api:8101', + userAPI: `http://${monolithUrl}/users`, + hotelAPI: `http://${monolithUrl}/hotels`, + bookingAPI: `http://${monolithUrl}/bookings`, +}; diff --git a/angular/RestClient/environments/environment.ts b/angular/RestClient/src/environments/environment.ts similarity index 91% rename from angular/RestClient/environments/environment.ts rename to angular/RestClient/src/environments/environment.ts index d82bc7fa8278b4fb12dd2db673f4b2c116cf8e3b..4264ad5b8fcbc528de02ea9e038a19aa2f617254 100644 --- a/angular/RestClient/environments/environment.ts +++ b/angular/RestClient/src/environments/environment.ts @@ -1,7 +1,7 @@ const monolithUrl = 'localhost:8080'; export const environment = { - production: true, + production: false, authAPI: 'http://localhost:8101', userAPI: `http://${monolithUrl}/users`, hotelAPI: `http://${monolithUrl}/hotels`, diff --git a/docker-compose.yml b/docker-compose.yml index 85641c264792a1c3cafb824d8b0b4337f2764ad1..6f86e46bad45d0d7f2a56bc0ce180ea343d0353e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ networks: services: Auth-API: image: auth-api-image - hostname: $${AUTH_SERVICE_HOSTNAME} + hostname: ${AUTH_SERVICE_HOSTNAME} build: context: ./java/services/auth dockerfile: Dockerfile @@ -18,64 +18,29 @@ services: networks: - kong-net environment: - SPRING_DATASOURCE_URL: jdbc:mysql://$${DB_SERVICE_HOSTNAME}:3306/$${DB_DATABASE_NAME}?createDatabaseIfNotExist=true + SPRING_DATASOURCE_URL: jdbc:mysql://${DB_SERVICE_HOSTNAME}:3306/${DB_DATABASE_NAME}?createDatabaseIfNotExist=true depends_on: - RoomsBooking-database - Users-API: - image: users-api-image - hostname: "$${USERS_SERVICE_HOSTNAME}" + RoomsBooking-API: + image: rooms-booking-api-image + hostname: ${ROOMS_BOOKING_SERVICE_HOSTNAME} build: - context: ./java/services/users + context: ./java/roomBooking dockerfile: Dockerfile restart: unless-stopped ports: - - 8111:8111 + - 8080:8080 networks: - kong-net environment: - SPRING_DATASOURCE_URL: jdbc:mysql://$${DB_SERVICE_HOSTNAME}:3306/$${DB_DATABASE_NAME}?createDatabaseIfNotExist=true - depends_on: - - RoomsBooking-database - - Hotels-API: - image: hotels-api-image - hostname: $${HOTELS_SERVICE_HOSTNAME} - build: - context: ./java/services/hotels - dockerfile: Dockerfile - restart: unless-stopped - ports: - - 8121:8121 - networks: - - kong-net - environment: - SPRING_DATASOURCE_URL: jdbc:mysql://$${DB_SERVICE_HOSTNAME}:3306/$${DB_DATABASE_NAME}?createDatabaseIfNotExist=true - SPRING_DATASOURCE_USER: $${USER_DATABASE} - SPRING_DATASOURCE_PASSWORD: $${} - depends_on: - - RoomsBooking-database - - Bookings-API - - Bookings-API: - image: bookings-api-image - hostname: $${BOOKINGS_SERVICE_HOSTNAME} - build: - context: ./java/services/bookings - dockerfile: Dockerfile - restart: unless-stopped - ports: - - 8131:8131 - networks: - - kong-net - environment: - SPRING_DATASOURCE_URL: jdbc:mysql://$${DB_SERVICE_HOSTNAME}:3306/$${DB_DATABASE_NAME}?createDatabaseIfNotExist=true + SPRING_DATASOURCE_URL: jdbc:mysql://${DB_SERVICE_HOSTNAME}:3306/${DB_DATABASE_NAME}?createDatabaseIfNotExist=true depends_on: - RoomsBooking-database RoomsBooking-database: image: mysql - hostname: $${DB_SERVICE_HOSTNAME} + hostname: ${DB_SERVICE_HOSTNAME} cap_add: - SYS_NICE restart: unless-stopped @@ -99,10 +64,10 @@ services: dockerfile: ./Dockerfile restart: unless-stopped ports: - - 3328:80 + - 4200:80 networks: - kong-net environment: - SPRING_DATASOURCE_URL: jdbc:mysql://$${DB_SERVICE_HOSTNAME}:3306/$${DB_DATABASE_NAME}?createDatabaseIfNotExist=true + SPRING_DATASOURCE_URL: jdbc:mysql://${DB_SERVICE_HOSTNAME}:3306/${DB_DATABASE_NAME}?createDatabaseIfNotExist=true depends_on: - RoomsBooking-database diff --git a/java/roomBooking/src/main/java/com/uva/monolith/config/SecurityConfig.java b/java/roomBooking/src/main/java/com/uva/monolith/config/SecurityConfig.java index 5cc2695222079be62bcb16f493158c0452920569..d9b84fbad965da3f54577198e81d76122beb5d2e 100644 --- a/java/roomBooking/src/main/java/com/uva/monolith/config/SecurityConfig.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/config/SecurityConfig.java @@ -31,6 +31,9 @@ public class SecurityConfig { .requestMatchers("users", "users/**") .hasAnyRole(UserRol.ADMIN.toString(), UserRol.CLIENT.toString()) // Acceso restringido a gestores de hoteles y administradores + .requestMatchers(HttpMethod.GET, "hotels", "hotels/*").hasAnyRole( + UserRol.CLIENT.toString(), UserRol.HOTEL_ADMIN.toString(), UserRol.ADMIN.toString()) + .requestMatchers("hotels", "hotels/**") .hasAnyRole(UserRol.ADMIN.toString(), UserRol.HOTEL_ADMIN.toString()) // Acceso restringido a cualquier usuario del sistema diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/controllers/HotelController.java b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/controllers/HotelController.java index 5852aa794da7dcd05a8359f2756e296c829a40da..000345d139ab7c3da4aca18dcef8addc6576c688 100644 --- a/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/controllers/HotelController.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/controllers/HotelController.java @@ -2,8 +2,10 @@ package com.uva.monolith.services.hotels.controllers; import java.util.List; import java.util.Map; +import java.util.Optional; import java.time.LocalDate; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -17,11 +19,15 @@ import com.uva.monolith.services.hotels.models.Hotel; import com.uva.monolith.services.hotels.models.Room; import com.uva.monolith.services.hotels.repositories.HotelRepository; import com.uva.monolith.services.hotels.repositories.RoomRepository; +import com.uva.monolith.services.users.models.HotelManager; +import com.uva.monolith.services.users.repositories.HotelManagerRepository; @RestController @RequestMapping("hotels") @CrossOrigin(origins = "*") public class HotelController { + @Autowired + private HotelManagerRepository hotelManagerRepository; private final HotelRepository hotelRepository; private final RoomRepository roomRepository; private final BookingRepository bookingRepository; @@ -42,6 +48,11 @@ public class HotelController { // Añadir un hotel con sus habitaciones @PostMapping public ResponseEntity<Hotel> addHotel(@RequestBody Hotel hotel) { + Optional<HotelManager> hm = hotelManagerRepository.findById(hotel.getHotelManager().getId()); + if (!hm.isPresent()) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + hotel.setHotelManager(hm.get()); Hotel savedHotel = hotelRepository.save(hotel); return new ResponseEntity<>(savedHotel, HttpStatus.CREATED); } @@ -60,6 +71,10 @@ public class HotelController { Hotel target = hotelRepository.findById(id) .orElseThrow(() -> new HotelNotFoundException(id)); bookingRepository.deleteAllByHotelId(id); + HotelManager hm = target.getHotelManager(); + hm.getHotels().removeIf(h -> h.getId() == target.getId()); + hotelManagerRepository.save(hm); + bookingRepository.flush(); hotelRepository.delete(target); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/repositories/HotelRepository.java b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/repositories/HotelRepository.java index ad88e98985f82e695aa9ed40df30d1501990eff5..80132b3776dad47e6f4da36beecd2ed3bba43d53 100644 --- a/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/repositories/HotelRepository.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/repositories/HotelRepository.java @@ -5,5 +5,4 @@ import org.springframework.data.jpa.repository.JpaRepository; import com.uva.monolith.services.hotels.models.Hotel; public interface HotelRepository extends JpaRepository<Hotel, Integer> { - } diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/controllers/UserController.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/controllers/UserController.java index 32b8c4d9fdfe17b26a913dd062fe19302e1bb90c..070fb5772a4f49f622617abf7d55958f1b6007c8 100644 --- a/java/roomBooking/src/main/java/com/uva/monolith/services/users/controllers/UserController.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/controllers/UserController.java @@ -61,6 +61,8 @@ public class UserController { @PutMapping("/{id}") public ResponseEntity<?> updateUserData(@PathVariable int id, @RequestBody Map<String, String> json) { + System.err.println(json.entrySet().size()); + json.keySet().forEach(k -> System.err.println(k)); String name = json.get("name"); String email = json.get("email"); if (name == null || email == null) { diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/remote/Booking.java b/java/services/auth/src/main/java/com/uva/authentication/models/remote/Booking.java new file mode 100644 index 0000000000000000000000000000000000000000..354c65d506674f162fa3009b593943be01a92e6f --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/Booking.java @@ -0,0 +1,49 @@ +package com.uva.authentication.models.remote; + +import jakarta.persistence.Basic; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "bookings") +public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + private int id; + @JoinColumn(name = "user_id", referencedColumnName = "id") + @ManyToOne(optional = false, fetch = FetchType.EAGER, cascade = CascadeType.MERGE) + private Client userId; + + public Booking() { + } + + public Booking(int id, Client userId) { + this.id = id; + this.userId = userId; + } + + public void setId(int id) { + this.id = id; + } + + public int getId() { + return this.id; + } + + public void setUserId(Client userId) { + this.userId = userId; + } + + public Client getUserId() { + return this.userId; + } + +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/remote/Client.java b/java/services/auth/src/main/java/com/uva/authentication/models/remote/Client.java index 69bb23c34c2f3c47f53365fab16abfda76871c60..5ebf50a3b5b1135b6c6b1f7206486c95a9e95b92 100644 --- a/java/services/auth/src/main/java/com/uva/authentication/models/remote/Client.java +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/Client.java @@ -1,10 +1,16 @@ package com.uva.authentication.models.remote; +import java.util.ArrayList; +import java.util.List; + import jakarta.persistence.Basic; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity @@ -14,24 +20,19 @@ public class Client extends User { @Basic(optional = false) @Column(nullable = false) @Enumerated(EnumType.STRING) - private UserStatus status; + private UserStatus status = UserStatus.NO_BOOKINGS; - // @JsonIgnore - // @OneToMany(mappedBy = "userId", fetch = FetchType.EAGER, cascade = - // CascadeType.ALL) - // private List<?> bookings; + @OneToMany(mappedBy = "userId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + private List<Booking> bookings = new ArrayList<>(); public Client() { super(); - // bookings = new ArrayList<>(); - status = UserStatus.NO_BOOKINGS; } - public Client(int id, String name, String email, String password, UserStatus status) { - // , List<?> bookings) { + public Client(int id, String name, String email, String password, UserStatus status, List<Booking> bookings) { super(id, name, email, password, UserRol.CLIENT); setStatus(status); - // setBookings(bookings); + setBookings(bookings); } public UserStatus getStatus() { @@ -42,11 +43,11 @@ public class Client extends User { this.status = status; } - // public List<?> getBookings() { - // return this.bookings; - // } + public List<Booking> getBookings() { + return this.bookings; + } - // public void setBookings(List<?> bookings) { - // this.bookings = bookings; - // } + public void setBookings(List<Booking> bookings) { + this.bookings = bookings; + } } diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/remote/Hotel.java b/java/services/auth/src/main/java/com/uva/authentication/models/remote/Hotel.java new file mode 100644 index 0000000000000000000000000000000000000000..5710f9d545b57d39363435fa8776a856461291b6 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/Hotel.java @@ -0,0 +1,48 @@ +package com.uva.authentication.models.remote; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "hotels") +public class Hotel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + private int id; + + @ManyToOne(optional = false) + @JoinColumn(name = "hotel_manager", referencedColumnName = "id") + private HotelManager hotelManager; + + public Hotel() { + } + + public Hotel(int id, HotelManager hotelManager) { + setId(id); + setHotelManager(hotelManager); + } + + public int getId() { + return this.id; + } + + public void setId(int id) { + this.id = id; + } + + public void setHotelManager(HotelManager hotelManager) { + this.hotelManager = hotelManager; + } + + public HotelManager getHotelManager() { + return hotelManager; + } +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/remote/HotelManager.java b/java/services/auth/src/main/java/com/uva/authentication/models/remote/HotelManager.java index 584a33677a7a846d2896dc300c811b9759c9168f..dd58a508436be9e9e51a99d26038cf6efec373a6 100644 --- a/java/services/auth/src/main/java/com/uva/authentication/models/remote/HotelManager.java +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/HotelManager.java @@ -1,33 +1,32 @@ package com.uva.authentication.models.remote; +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity @Table(name = "hotel_manager_user") public class HotelManager extends User { - // TODO tener en cuenta que esto hay que tener un control adecuado - // @JsonIgnore - // @OneToMany(mappedBy = "userId", fetch = FetchType.EAGER, cascade = - // CascadeType.ALL) - // private List<?> hotels; + @OneToMany(mappedBy = "hotelManager", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + private List<Hotel> hotels = new ArrayList<>(); public HotelManager() { super(); - // hotels = new ArrayList<>(); } - public HotelManager(int id, String name, String email, String password) { // , List<?> hotels) { + public HotelManager(int id, String name, String email, String password, List<Hotel> hotels) { super(id, name, email, password, UserRol.HOTEL_ADMIN); - // setHotels(hotels); + setHotels(hotels); } - // public List<?> getHotels() { - // return this.hotels; - // } + public void setHotels(List<Hotel> hotels) { + this.hotels = hotels; + } - // public void setHotels(List<?> hotels) { - // this.hotels = hotels; - // } } diff --git a/java/services/auth/src/main/java/com/uva/authentication/repositories/ClientRepository.java b/java/services/auth/src/main/java/com/uva/authentication/repositories/ClientRepository.java index 3019f8145452aa3f8e1347ab9f9296b2ffeea71a..6d2b8eeb777f2d3c9800c51dc34fe72ea504efd3 100644 --- a/java/services/auth/src/main/java/com/uva/authentication/repositories/ClientRepository.java +++ b/java/services/auth/src/main/java/com/uva/authentication/repositories/ClientRepository.java @@ -4,8 +4,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.uva.authentication.models.remote.Client; -import com.uva.authentication.models.remote.User; -public interface ClientRepository extends JpaRepository<User, Integer> { +public interface ClientRepository extends JpaRepository<Client, Integer> { Optional<Client> findByEmail(String email); } \ No newline at end of file diff --git a/java/services/auth/src/main/java/com/uva/authentication/repositories/HotelManagerRepository.java b/java/services/auth/src/main/java/com/uva/authentication/repositories/HotelManagerRepository.java index 2d50305d0fc98a1ca2fdfb55cf4dd8de4fd4b659..c051ddd0425ff264337261645dbe6f17cfa55b51 100644 --- a/java/services/auth/src/main/java/com/uva/authentication/repositories/HotelManagerRepository.java +++ b/java/services/auth/src/main/java/com/uva/authentication/repositories/HotelManagerRepository.java @@ -4,8 +4,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.uva.authentication.models.remote.HotelManager; -import com.uva.authentication.models.remote.User; -public interface HotelManagerRepository extends JpaRepository<User, Integer> { +public interface HotelManagerRepository extends JpaRepository<HotelManager, Integer> { Optional<HotelManager> findByEmail(String email); } \ No newline at end of file diff --git a/java/services/auth/src/main/java/com/uva/authentication/services/AuthService.java b/java/services/auth/src/main/java/com/uva/authentication/services/AuthService.java index e392a7bb693ab2db0b176eae4dc69aafc44db6b3..3ed67ececda2b0632e00f1b31ff04f3c05df866e 100644 --- a/java/services/auth/src/main/java/com/uva/authentication/services/AuthService.java +++ b/java/services/auth/src/main/java/com/uva/authentication/services/AuthService.java @@ -79,7 +79,7 @@ public class AuthService { newUser = hotelManagerRepository.save(hm); break; - case ADMIN: // TODO revisar + case ADMIN: User admin = new User(); BeanUtils.copyProperties(registerRequest, admin); newUser = userRepository.save(admin); diff --git a/java/services/auth/src/main/java/com/uva/authentication/utils/JwtUtil.java b/java/services/auth/src/main/java/com/uva/authentication/utils/JwtUtil.java index 518261304ec00dc5c48dd4f860a519cb46b03f75..e21efcdbabe51754bc2a2bf705eba0361f0696d6 100644 --- a/java/services/auth/src/main/java/com/uva/authentication/utils/JwtUtil.java +++ b/java/services/auth/src/main/java/com/uva/authentication/utils/JwtUtil.java @@ -49,7 +49,7 @@ public class JwtUtil { return Jwts .builder() .setClaims(extraClaims) - .setSubject(user.getName()) + .setSubject(String.valueOf(user.getId())) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(getSignInKey(), SignatureAlgorithm.HS256)