diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..20bfb0a74c8e88ae613364a837696f2fba800197 --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +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 +BD_PASSWORD=password \ No newline at end of file diff --git a/.gitignore b/.gitignore index 103bd2c647aba772a54dd4140f55da75da9f20f9..9d69baa07a9349a0ed0d73d664ad697aa5e65a9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ ### Own ### taller -*.pdf \ No newline at end of file +*.pdf +**/target/ +**/.vscode +**/tmp +**/*.tmp +*.ln +*.log + diff --git a/DBCS_2.mp4 b/DBCS_2.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..e5ebadf041596dd44e4abf085f9a7439152a903d Binary files /dev/null and b/DBCS_2.mp4 differ diff --git a/angular/RestClient/Dockerfile b/angular/RestClient/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3557c5fae54aafd2c685c1cfffd9080037b02932 --- /dev/null +++ b/angular/RestClient/Dockerfile @@ -0,0 +1,28 @@ +# Etapa 1: Construcción de la aplicación Angular +FROM node:20 AS build + +# Establecer el directorio de trabajo +WORKDIR /app + +# Copiar archivos de package.json e instalar dependencias +COPY package*.json ./ +RUN npm install + +# Copiar el código fuente de Angular +COPY . . + +# Compilar la aplicación Angular para producción +RUN npm run build -- --output-path=dist/app --c production + +# Etapa 2: Servidor Nginx para producción +FROM nginx:alpine AS production + +# Copiar los archivos de construcción generados en la etapa anterior a la carpeta de Nginx +COPY --from=build /app/dist/app/browser /usr/share/nginx/html +COPY --from=build /app/nginx-custom.conf /etc/nginx/conf.d/default.conf + +# Exponer el puerto 80 para Nginx +EXPOSE 80 + +# Iniciar Nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/angular/RestClient/angular.json b/angular/RestClient/angular.json index b9017eb47cbc0d902128a7211d7d8f08c6d1f48a..6f0edcd86785afd8cc8db5af00a9e2da641eb1d0 100644 --- a/angular/RestClient/angular.json +++ b/angular/RestClient/angular.json @@ -32,20 +32,21 @@ "scripts": [ "node_modules/jquery/dist/jquery.min.js", "node_modules/bootstrap/dist/js/bootstrap.min.js" - ], - "server": "src/main.server.ts", - "prerender": true, - "ssr": { - "entry": "server.ts" - } + ] }, "configurations": { "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], "budgets": [ { "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" + "maximumWarning": "1.2MB", + "maximumError": "1.3MB" }, { "type": "anyComponentStyle", @@ -71,6 +72,12 @@ }, "development": { "buildTarget": "RestClient:build:development" + }, + "monolith": { + "buildTarget": "RestClient:build:monolith" + }, + "microservices": { + "buildTarget": "RestClient:build:microservices" } }, "defaultConfiguration": "development" @@ -102,8 +109,5 @@ } } } - }, - "cli": { - "analytics": false } } diff --git a/angular/RestClient/nginx-custom.conf b/angular/RestClient/nginx-custom.conf new file mode 100644 index 0000000000000000000000000000000000000000..7bb593987ce2b7ab6fc25a2fa746a5f387a662da --- /dev/null +++ b/angular/RestClient/nginx-custom.conf @@ -0,0 +1,12 @@ +server { + listen 80; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html =404; + } + + error_page 404 /index.html; +} \ No newline at end of file diff --git a/angular/RestClient/package-lock.json b/angular/RestClient/package-lock.json index a5d201bf11869266cc967b0e328660d5d8870bc7..af241069cc8acddc06a48c70b6d1dd25125a07ac 100644 --- a/angular/RestClient/package-lock.json +++ b/angular/RestClient/package-lock.json @@ -25,6 +25,7 @@ "bootstrap": "^3.4.0", "express": "^4.18.2", "jquery": "^3.4.1", + "jwt-decode": "^4.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" @@ -5468,9 +5469,9 @@ "license": "ISC" }, "node_modules/bootstrap": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.0.tgz", - "integrity": "sha512-F1yTDO9OHKH0xjl03DsOe8Nu1OWBIeAugGMhy3UTIYDdbbIPesQIhCEbj+HEr6wqlwweGAlP8F3OBC6kEuhFuw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", + "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", "license": "MIT", "engines": { "node": ">=6" @@ -6329,9 +6330,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -8555,9 +8556,9 @@ } }, "node_modules/jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", "license": "MIT" }, "node_modules/js-tokens": { @@ -8654,6 +8655,15 @@ ], "license": "MIT" }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", diff --git a/angular/RestClient/package.json b/angular/RestClient/package.json index 262eacd1144f0eaab113dda4aad24eedab9ff958..a17778b07370cfdb71c9dcc5f6137b413ab929cf 100644 --- a/angular/RestClient/package.json +++ b/angular/RestClient/package.json @@ -28,6 +28,7 @@ "bootstrap": "^3.4.0", "express": "^4.18.2", "jquery": "^3.4.1", + "jwt-decode": "^4.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" diff --git a/angular/RestClient/src/app/app.config.ts b/angular/RestClient/src/app/app.config.ts index 1f668ea1741efbdb605f6d418a9a93f53f1327fa..c0664f634af159df0208b1a877011ce66c1471ac 100644 --- a/angular/RestClient/src/app/app.config.ts +++ b/angular/RestClient/src/app/app.config.ts @@ -1,19 +1,22 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; -import { provideHttpClient, withFetch } from '@angular/common/http'; +import { + provideHttpClient, + withFetch, + withInterceptors, +} from '@angular/common/http'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { ReactiveFormsModule } from '@angular/forms'; // Added import for ReactiveFormsModule import { provideNativeDateAdapter } from '@angular/material/core'; -import { provideClientHydration } from '@angular/platform-browser'; +import { authRequest } from './security/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideNativeDateAdapter(), provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), - provideClientHydration(), - provideHttpClient(withFetch()), + provideHttpClient(withFetch(), withInterceptors([authRequest])), provideAnimationsAsync(), ReactiveFormsModule, ], diff --git a/angular/RestClient/src/app/app.routes.ts b/angular/RestClient/src/app/app.routes.ts index b8ee77b36e9b121a4cf804746bde023134978c1c..4be490fc5813f4e0bca3846bfd83e67c06ce0654 100644 --- a/angular/RestClient/src/app/app.routes.ts +++ b/angular/RestClient/src/app/app.routes.ts @@ -1,43 +1,184 @@ -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'; import { MainPageComponent } from './core/features/user/main-page/main-page.component'; 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'; -export const routes: Routes = [ +interface RouteData { + expectedRole: UserRol | UserRol[]; +} + +type AppRoute = Omit<Route, 'data'> & { + data?: RouteData; +}; + +export const routes: AppRoute[] = [ + // Auth + { + path: 'login', + component: UserFormComponent, + }, + { + path: 'register', + component: UserFormComponent, + }, + + // Hoteles { - path: '', // Ruta principal - component: MainPageComponent, // Componente de la página principal + path: 'hotels', // Ruta para la lista de hoteles + component: HotelListComponent, }, { - path: 'bookings/search', - component: BookingListComponent, + path: 'hotels/register', // Registrar nuevo hotel + component: HotelRegisterComponent, + canActivate: [rolGuard], + data: { expectedRole: 'HOTEL_ADMIN' }, }, { - path: 'bookings/new', + path: 'hotels/:id', // Hotel concreto + component: HotelRegisterComponent, + }, + + // 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', + // 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: 'users/:id/bookings', + 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' }, }, + // { + // path: 'admin/users/:id/bookings/:bookingId', + // component: BookingComponent, + // }, { - path: 'hotels', + path: 'admin/users/:id/hotels', component: HotelListComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, }, { - path: 'hotels/new', + path: 'admin/users/:userId/hotels/:id', component: HotelRegisterComponent, + canActivate: [rolGuard], + data: { expectedRole: 'ADMIN' }, }, + // { + // path: 'admin/users/:userId/hotels/:id/bookings', + // component: BookingListComponent, + // canActivate: [rolGuard], + // data: { expectedRole: 'ADMIN' }, + // }, + // { + // path: 'admin/users/:userId/hotels/:hotelId/rooms/:id/bookings', + // component: BookingListComponent, + // canActivate: [rolGuard], + // data: { expectedRole: 'ADMIN' }, + // }, + { - path: 'hotels/:id', - component: HotelRegisterComponent, + 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 new file mode 100644 index 0000000000000000000000000000000000000000..79b4834ee543b62f19df182154eeb62fa0911db1 --- /dev/null +++ b/angular/RestClient/src/app/core/features/auth/login/login.component.css @@ -0,0 +1,21 @@ +.container { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..f6fcf96c6ad1294dad508f3a339c7f9f900c4647 --- /dev/null +++ b/angular/RestClient/src/app/core/features/auth/login/login.component.html @@ -0,0 +1,73 @@ +<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 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 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.spec.ts b/angular/RestClient/src/app/core/features/auth/login/login.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..18f3685d74d58daabcc34d3a66be081c6256a69d --- /dev/null +++ b/angular/RestClient/src/app/core/features/auth/login/login.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture<LoginComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000000000000000000000000000000000..d3bed7a4132fa619c110a0ebd74c3e8225e16216 --- /dev/null +++ b/angular/RestClient/src/app/core/features/auth/login/login.component.ts @@ -0,0 +1,64 @@ +import { Component } from '@angular/core'; +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, + CommonModule, + MatCardModule, + MatInputModule, + MatFormFieldModule, + MatSelectModule, + MatSlideToggleModule, + ], +}) +export class LoginComponent { + loginForm: FormGroup; + selectedRol?: UserRol; + rolOptions: UserRol[] = UserRolesArray; + errorMessage: string | null = null; + + constructor( + private fb: FormBuilder, + private sessionManager: SessionService, + private router: Router + ) { + this.loginForm = this.fb.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required]], + }); + } + + onSubmit() { + if (this.loginForm.valid) { + const { email, password } = this.loginForm.value; + 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 new file mode 100644 index 0000000000000000000000000000000000000000..79b4834ee543b62f19df182154eeb62fa0911db1 --- /dev/null +++ b/angular/RestClient/src/app/core/features/auth/register/register.component.css @@ -0,0 +1,21 @@ +.container { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..90cc523dc3e5fc2d2a12e9c2f259f5827c0a36ef --- /dev/null +++ b/angular/RestClient/src/app/core/features/auth/register/register.component.html @@ -0,0 +1,85 @@ +<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 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 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> + <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.spec.ts b/angular/RestClient/src/app/core/features/auth/register/register.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..757b8952cf4bbb851f3b079286c5c711dad73119 --- /dev/null +++ b/angular/RestClient/src/app/core/features/auth/register/register.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegisterComponent } from './register.component'; + +describe('RegisterComponent', () => { + let component: RegisterComponent; + let fixture: ComponentFixture<RegisterComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegisterComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000000000000000000000000000000000..e594c19b132e84967d2509e2c3e4a5053a66f048 --- /dev/null +++ b/angular/RestClient/src/app/core/features/auth/register/register.component.ts @@ -0,0 +1,79 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +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'], + imports: [ + ReactiveFormsModule, + CommonModule, + MatCardModule, + MatInputModule, + MatFormFieldModule, + MatSelectModule, + MatSlideToggleModule, + ], +}) +export class RegisterComponent { + registerForm: FormGroup; + errorMessage: string | null = null; + + 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, Validators.minLength(6)]], + }); + } + + onSubmit() { + if (this.registerForm.valid) { + 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 06aab259dd9efcd5efaccb4defdacf03a55f3518..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 @@ -7,12 +7,12 @@ import { import { FormsModule } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { Hotel, Room, RoomType, roomTypeArray } from '../../../../../types'; - +import { Hotel, Room, RoomType, roomTypeArray } from '../../../../types'; import { Router } from '@angular/router'; import { MatCardModule } from '@angular/material/card'; import { MatChipsModule } from '@angular/material/chips'; -import { ClienteApiRestService } from '../../../../shared/cliente-api-rest.service'; +import { LocalStorageService } from '../../../../shared/local-storage.service'; +import { HotelClientService } from '../../../../shared/hotel-client.service'; type SelectableRoomType = 'All' | RoomType; const selectableRoomTypeArray: SelectableRoomType[] = ['All', ...roomTypeArray]; @@ -36,23 +36,27 @@ 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; rooms: Room[] = []; trateRooms: Room[] = []; - constructor(private router: Router, private client: ClienteApiRestService) {} + constructor( + private router: Router, + private hotelClient: HotelClientService, + private storage: LocalStorageService + ) {} ngOnInit() { this.getHotels(); } getHotels() { - this.client.getAllHotels().subscribe({ + this.hotelClient.getAllHotels().subscribe({ next: (resp) => { if (resp != null) this.hotels = [...resp]; }, @@ -72,7 +76,7 @@ export class BookingListComponent { } search() { - this.client + this.hotelClient .getRoomsAvailableInDateRange( this.hotelSelected!.id, this.start!, @@ -95,14 +99,11 @@ export class BookingListComponent { } bookingRoom(roomId: number) { - localStorage.setItem( - 'booking-data', - JSON.stringify({ - roomId, - startDate: this.start, - endDate: this.end, - }) - ); + this.storage.save('booking-data', { + roomId, + startDate: this.start, + endDate: this.end, + }); this.router.navigate(['/bookings', 'new'], { queryParams: { roomId } }); } } 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..fb7485fc16360e63fe916ee75b1f53f320895776 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 cd63902449b286409ab2ae045a9b273a7033a1f8..6691027e241bff16a2b25fb71e23a1309ec1929e 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 @@ -6,10 +6,18 @@ import { Validators, } from '@angular/forms'; -import { BookingService } from '../../../../shared/booking.service'; // Asegúrate de que el servicio exista import { ActivatedRoute, Router } from '@angular/router'; -import { Booking, User } from '../../../../../types'; -import { ClienteApiRestService } from '../../../../shared/cliente-api-rest.service'; +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; + startDate: Date; + endDate: Date; +}; @Component({ standalone: true, @@ -18,111 +26,98 @@ import { ClienteApiRestService } from '../../../../shared/cliente-api-rest.servi 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 }; + bookingLocal: { roomId: number; startDate: Date; endDate: Date } = { + roomId: 0, + endDate: new Date(), + startDate: new Date(), + }; roomId: number = 0; constructor( private router: Router, private route: ActivatedRoute, private fb: FormBuilder, - private bookingService: BookingService, - private client: ClienteApiRestService + 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 localBookingStr = localStorage.getItem('booking-data'); - if (localBookingStr === null) { + const localBooking = storage.read<communication | null>('booking-data'); + if (localBooking === null) { this.router.navigate(['/booking', 'search']); + return; } - const localBooking = JSON.parse(localBookingStr!); - this.bookingLocal = localBooking; + this.bookingLocal = localBooking!; this.route.queryParams.subscribe((params) => { const roomId = Number(params['roomId']); this.roomId = roomId; - if (localBooking.roomId !== roomId) { + if (this.bookingLocal.roomId !== roomId) { this.router.navigate(['/bookings', 'search']); - this.loadBooking(localBooking); + return; } + this.bookingLocal = { + ...this.bookingLocal, + startDate: new Date(this.bookingLocal.startDate), + endDate: new Date(this.bookingLocal.endDate), + }; + this.loadBooking(); }); - this.client.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(this.bookingLocal); - } - - loadBooking(booking: { roomId: number; startDate: Date; endDate: Date }) { - const start = new Date(booking.startDate).toISOString().split('T')[0]; - const end = new Date(booking.endDate).toISOString().split('T')[0]; + loadBooking() { + const booking = this.bookingLocal; + if (!booking) return; + const start = new Date(booking.startDate).toISOString(); + const end = new Date(booking.endDate).toISOString(); 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.bookingService.createBooking(bookingRequest).subscribe({ - next: (response) => { - console.log('Reserva creada con éxito', response); - // Llama al servicio para actualizar el estado del usuario - this.client - .alterUserStatus(userId, 'WITH_ACTIVE_BOOKINGS') - .subscribe({ - next: (response) => { - console.log( - 'Estado de usuario actualizado con exito', - response - ); - localStorage.removeItem('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..d0f0b21a8390db0bffa538c150073391b3abcc54 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,101 @@ </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"> + <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> + </mat-form-field> + <mat-form-field> + <mat-label class="text-2xl">Hotel</mat-label> + <mat-select + [(value)]="hotelSelected" + class="text-2xl" + (selectionChange)="update()" + > + <mat-option [value]="undefined" class="text-3xl">All</mat-option> + @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" (selectionChange)="update()"> + @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 bdd87d3d36ce178cbb994508955b05bec3474803..1b6e34d096b35b4ef53be036aa767258592dee54 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 { ClienteApiRestService } from '../../../../shared/cliente-api-rest.service'; +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,25 +45,118 @@ import { ClienteApiRestService } from '../../../../shared/cliente-api-rest.servi 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 = undefined; + roomTypeSelected: SelectableRoomType = 'All'; + roomTypes = selectableRoomTypeArray; + rooms: Room[] = []; + trateRooms: Room[] = []; + userId = 0 + + constructor( + private fb: FormBuilder, + private hotelClient: HotelClientService, + private router: Router, + private route: ActivatedRoute, + private storage: LocalStorageService, + private sessionService: SessionService + ) { + const isHotelManger = this.route.snapshot.url[0].path === 'me' + const isAdmin = this.route.snapshot.url[0].path === 'admin' + this.isManaging = isHotelManger || isAdmin; + const today = new Date(); - constructor(private router: Router, private client: ClienteApiRestService) {} + // 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; + this.userId = isHotelManger + ? session.id + : Number(this.route.snapshot.paramMap.get('id')); + } + }, + }); + } - ngOnInit() { + ngOnInit(): void { this.getHotels(); + this.dateRangeForm.get('dateRange')?.valueChanges.subscribe(() => { + this.getHotels(); + }); + } + + update() { + this.hotels = ( + !!this.hotelSelected + ? [...this._hotels].filter((h) => h.id === this.hotelSelected!.id) + : [...this._hotels] + ) + .map((h) => { + h = { ...h, rooms: [...h.rooms] }; + h.rooms = h.rooms.filter( + (r) => + r.available && + (this.roomTypeSelected === 'All' || + (r.type as SelectableRoomType) === this.roomTypeSelected) + ); + return h; + }) + .filter((h) => h.rooms.length > 0); + } + + 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.client.getAllHotels().subscribe({ + const { start, end } = this.dateRangeForm.value.dateRange; + + const observable = this.isManaging + ? this.hotelClient.getAllHotelsByUser(this.userId, start, end) + : this.hotelClient.getAllHotels(start, end) + console.log({...this}) + observable.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); @@ -58,17 +167,9 @@ export class HotelListComponent { deleteHotel(id: number) { if (!confirm(`Borrar hotel con id ${id}. Continuar?`)) return; - - this.client.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'; - } + this.hotelClient.deleteHotel(id).subscribe({ + next: () => { + this.getHotels(); }, error: (err) => { console.log('Error al borrar: ' + err.message); @@ -82,26 +183,43 @@ export class HotelListComponent { roomId: number, availability: boolean ) { - this.client.alterRoomAvailability(hotelId, roomId, availability).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 cambiar disponibilidad'; - console.error(this.mensaje); - } - }, - error: (error) => { - console.log('Error al cambiar disponibilidad: ' + error.message); - throw error; - }, - }); + this.hotelClient + .alterRoomAvailability(hotelId, roomId, availability) + .subscribe({ + next: (resp) => { + if (resp) { + this.getHotels(); + } + }, + error: (error) => { + console.log('Error al cambiar disponibilidad: ' + error.message); + throw error; + }, + }); } - 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 fd09a256f2380ae5c144b373f0406228079dcd73..9408ea3e1082fb50c4277a6912870e6472979518 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 @@ -13,9 +13,10 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { CommonModule } from '@angular/common'; -import { ClienteApiRestService } from '../../../../shared/cliente-api-rest.service'; -import { Address, Hotel, Room } from '../../../../../types'; +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, @@ -59,19 +60,22 @@ export class HotelRegisterComponent { private router: Router, private route: ActivatedRoute, private fb: FormBuilder, - private client: ClienteApiRestService + private hotelClient: HotelClientService ) { this.hotelForm = this.setHotelForm(); this.editMode = false; 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.client.getHotel(id).subscribe({ + this.hotelClient.getHotel(id).subscribe({ next: (h) => this.setHotelForm(h), error: (error) => { - this.router.navigate(['/hotels/new']); + this.router.navigateByUrl('/hotels/register'); }, }); } @@ -102,12 +106,12 @@ export class HotelRegisterComponent { onSubmit(): void { if (this.hotelForm.valid) { const hotel = this.hotelForm.value as Hotel; - this.client.addHotel(hotel).subscribe({ + 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 { } }, 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 55760b59690b884457e25d55f8410a50c81cc0bd..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,44 +1,66 @@ -// main-page.component.ts -import { Component, OnInit } from '@angular/core'; -import { ClienteApiRestService } from '../../../../shared/cliente-api-rest.service'; -import { User, UserStateFilter } from '../../../../../types'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Client, User, UserStateFilter } from '../../../../types'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; -import users from '../../../../../mocks/users.json'; -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'], }) export class MainPageComponent implements OnInit { - users: User[] = []; - filteredUsers: User[] = []; + users: Client[] = []; + filteredUsers: Client[] = []; selectedStatus: UserStateFilter = 'All'; + displayedColumns: string[] = ['id', 'name', 'email', 'rol']; + dataSource = new MatTableDataSource<User>(); - constructor(private ClienteApiRestService: ClienteApiRestService) {} + constructor(private userClient: UserClientService, private router: Router) {} ngOnInit(): void { - this.users = users as unknown as User[]; - this.ClienteApiRestService.getAllUsers().subscribe((data: User[]) => { - this.users = data; - this.filteredUsers = data; // Inicialmente, muestra todos los usuarios + this.users = users; + this.filteredUsers = [...this.users]; + + // Sobrescribir con datos reales si están disponibles + this.userClient.getAllUsers().subscribe({ + next: (data: Client[]) => { + this.users = 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; + this.filteredUsers = [...this.users]; } else { this.filteredUsers = this.users.filter( (user) => user.status === this.selectedStatus ); } + this.dataSource = new MatTableDataSource<User>(this.filteredUsers); + this.dataSource.paginator = this.paginator!; } - getState(user: User) { + getState(user: Client): string { switch (user.status) { case 'NO_BOOKINGS': return 'SIN RESERVAS'; @@ -46,6 +68,12 @@ export class MainPageComponent implements OnInit { return 'CON RESERVAS ACTIVAS'; case 'WITH_INACTIVE_BOOKINGS': return 'CON RESERVAS INACTIVAS'; + default: + 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 5f1b3f1374342c3f9633486e6ce12027c2ebf7c5..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 @@ -1,11 +1,13 @@ import { Component } from '@angular/core'; -import { ClienteApiRestService } from '../../../../shared/cliente-api-rest.service'; -import { Booking, User } from '../../../../../types'; + +import { Booking, User } from '../../../../types'; import { ActivatedRoute, RouterModule } from '@angular/router'; -import { MatSelectModule } from '@angular/material/select'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { BookingService } from '../../../../shared/booking.service'; +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 client: ClienteApiRestService, - private bookingClient: BookingService, + 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.client - .getUser(this.userId) - .subscribe({ next: (user) => (this.user = user) }); - } - - ngOnInit() { - this.updateBookings(); } updateBookings() { - this.client.getUserBookings(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.client.getUserBookings(this.userId).subscribe({ + this.bookingClient.getBookingsByUser(this.user.id).subscribe({ next: (bookings) => { const withActive = bookings.find( (booking) => this.genBookingState(booking) === 'Reserva activa' @@ -97,8 +104,8 @@ export class UserBookingListComponent { (booking) => this.genBookingState(booking) === 'Reserva inactiva' ); if (withActive) { - this.client - .alterUserStatus(this.userId, 'WITH_ACTIVE_BOOKINGS') + this.userClient + .alterUserStatus(this.user.id, 'WITH_ACTIVE_BOOKINGS') .subscribe({ next: (response) => { console.log('Cambio de estado en el usuario a activo correcto'); @@ -108,8 +115,8 @@ export class UserBookingListComponent { }, }); } else if (withInactive) { - this.client - .alterUserStatus(this.userId, 'WITH_INACTIVE_BOOKINGS') + this.userClient + .alterUserStatus(this.user.id, 'WITH_INACTIVE_BOOKINGS') .subscribe({ next: (response) => { console.log( @@ -123,18 +130,20 @@ export class UserBookingListComponent { }, }); } else { - this.client.alterUserStatus(this.userId, 'NO_BOOKINGS').subscribe({ - next: (response) => { - console.log( - 'Cambio de estado en el usuario a sin reservas correcto' - ); - }, - error: (err) => { - console.error( - 'Error al cambiar de estado al usuario sin reservas' - ); - }, - }); + this.userClient + .alterUserStatus(this.user.id, 'NO_BOOKINGS') + .subscribe({ + next: (response) => { + console.log( + 'Cambio de estado en el usuario a sin reservas correcto' + ); + }, + error: (err) => { + console.error( + 'Error al cambiar de estado al usuario sin reservas' + ); + }, + }); } }, }); 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 new file mode 100644 index 0000000000000000000000000000000000000000..79b4834ee543b62f19df182154eeb62fa0911db1 --- /dev/null +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.css @@ -0,0 +1,21 @@ +.container { + 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/user/user-form/user-form.component.html b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..677115e1ef16e7735abdfe809b92fdb65262747e --- /dev/null +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html @@ -0,0 +1,148 @@ +<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> + } + <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.spec.ts b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..292f234075dbcba5482794cf23e4a231e666ba17 --- /dev/null +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.spec.ts @@ -0,0 +1,91 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HttpResponse } from '@angular/common/http'; +import { UserFormComponent } from './user-form.component'; +import { UserClientService } from '../../../../shared/user-client.service'; +import { of } from 'rxjs'; + +describe('UserFormComponent', () => { + let component: UserFormComponent; + let fixture: ComponentFixture<UserFormComponent>; + let userService: UserClientService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, FormsModule], + declarations: [UserFormComponent], + providers: [UserClientService], + }).compileComponents(); + + fixture = TestBed.createComponent(UserFormComponent); + component = fixture.componentInstance; + userService = TestBed.inject(UserClientService); + + spyOn(userService, 'getCurrentUser').and.returnValue( + of({ + id: 1, + name: 'John Doe', + email: 'johndoe@example.com', + rol: 'CONSUMER', + status: 'WITH_ACTIVE_BOOKINGS', + }) + ); + + spyOn(userService, 'updateUser').and.returnValue(of(new HttpResponse({ body: 'User updated successfully' }))); + spyOn(userService, 'updatePassword').and.returnValue(of(new HttpResponse({ body: 'Password updated successfully' }))); + + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should load user data on initialization', () => { + expect(component.userForm.value).toEqual({ + name: 'John Doe', + email: 'johndoe@example.com', + password: '', + confirmPassword: '', + }); + }); + + it('should call updateUser when saving valid user data', () => { + component.userForm.patchValue({ + name: 'Jane Doe', + email: 'janedoe@example.com', + }); + + component.saveChanges(); + + expect(userService.updateUser).toHaveBeenCalledWith({ + name: 'Jane Doe', + email: 'janedoe@example.com', + }); + }); + + it('should call updatePassword when password is updated and matches confirmPassword', () => { + component.userForm.patchValue({ + password: 'newpassword123', + confirmPassword: 'newpassword123', + }); + + component.saveChanges(); + + expect(userService.updatePassword).toHaveBeenCalledWith( + '', + 'newpassword123' + ); + }); + + it('should not call updatePassword if password and confirmPassword do not match', () => { + component.userForm.patchValue({ + password: 'newpassword123', + confirmPassword: 'differentpassword', + }); + + component.saveChanges(); + + expect(userService.updatePassword).not.toHaveBeenCalled(); + }); +}); 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 new file mode 100644 index 0000000000000000000000000000000000000000..1c6d272d01c5a9b899b1c77a84773fe47c43c7bb --- /dev/null +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.ts @@ -0,0 +1,385 @@ +import { Component, OnInit } from '@angular/core'; +import { + FormBuilder, + FormGroup, + Validators, + ReactiveFormsModule, + FormsModule, +} from '@angular/forms'; +import { UserClientService } from '../../../../shared/user-client.service'; +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, + RouterModule, + MatSlideToggleModule, + MatCardModule, + MatInputModule, + MatSelectModule, + MatFormFieldModule, + ], +}) +export class UserFormComponent implements OnInit { + userForm!: FormGroup; + 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 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.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: nameNotRequired }, Validators.required], + email: [ + { value: '', disabled: emailNotRequired }, + [Validators.required, Validators.email], + ], + 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 + }); + } + + 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) + ); + } + }, + }); + } + } + + getHotelsUri() { + const basePath = getBasePath(this.route); // Obtener la base: '/me' o '/users/:id' + return `${basePath}/hotels`; + } + + getBookingsUri() { + const basePath = getBasePath(this.route); // Obtener la base: '/me' o '/users/:id' + return `${basePath}/bookings`; + } + + 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 200c21a3a62ba294324b6a77c75d043c2bd5574b..dd40d570ba45e75417e8615f515a70fea3667aec 100644 --- a/angular/RestClient/src/app/core/navigation/navigation.component.css +++ b/angular/RestClient/src/app/core/navigation/navigation.component.css @@ -4,7 +4,7 @@ nav { padding: 1em; } -ul { +ul.nav { list-style: none; padding: 0; display: flex; @@ -14,18 +14,18 @@ li { margin-right: 20px; } -a, -a:visited { +a.nav-link, +a.nav-link:visited { color: white; text-decoration: none; transform: scale(1); transition: transform 0.3s ease; } -a:hover { +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); } @@ -34,6 +34,12 @@ a:hover { font-weight: bold; } +a.simple, +a.simple:visited { + color: white; + text-decoration: none; +} + @keyframes escalar { 0% { transform: scale(1); diff --git a/angular/RestClient/src/app/core/navigation/navigation.component.html b/angular/RestClient/src/app/core/navigation/navigation.component.html index 63615464cbbf7aec69cbd99e5e33ea9d1e5e08d8..3cfc3e49603a529a745791cb68b9916d62fdbe45 100644 --- a/angular/RestClient/src/app/core/navigation/navigation.component.html +++ b/angular/RestClient/src/app/core/navigation/navigation.component.html @@ -1,12 +1,59 @@ <nav> - <ul> - <li><a class="btn" [routerLink]="['/']">Home - Usuarios</a></li> - <li> - <a class="btn" [routerLink]="['/hotels', 'new']">Registrar Hotel</a> - </li> - <li><a class="btn" [routerLink]="['/hotels']">Hoteles</a></li> - <li> - <a class="btn" [routerLink]="['/bookings', 'search']">Nueva Reserva</a> + <ul class="nav"> + <li><a class="btn nav-link" [routerLink]="['/hotels']">Hoteles</a></li> + <li class="ml-auto"> + @if (isLogged){ + <!-- Dropdown para usuario registrado --> + + <div class="btn bg-blue-500 text-white rounded hover:bg-blue-600"> + <!-- mat-icon-button --> + <button + [matMenuTriggerFor]="sessionOptions" + class="flex items-center gap-3" + > + <span class="text-4xl">{{ user.name }}</span> + + @if (trigger?.menuOpen) { + <mat-icon class="text-4xl">arrow_drop_up</mat-icon> + }@else { + <mat-icon class="text-4xl">arrow_drop_down</mat-icon> + } + </button> + </div> + <mat-menu #sessionOptions="matMenu" xPosition="before"> + @for (section of sections; track section.id) { @if (section.link) { + <a [routerLink]="section.link" class="simple"> + <button mat-menu-item> + <mat-icon>{{ section.icon }}</mat-icon> + <span class="text-2xl">{{ section.text }}</span> + </button> + </a> + } @else { + <button mat-menu-item> + <mat-icon>{{ section.icon }}</mat-icon> + <span class="text-2xl">{{ section.text }}</span> + </button> + } } + <button mat-menu-item (click)="logout()"> + <mat-icon>logout</mat-icon> + <span class="text-2xl">Cerrar sesión</span> + </button> + </mat-menu> + + } @else { + <div + class="btn bg-blue-500 text-white hover:bg-blue-600 ml-auto" + (click)="login()" + > + <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> + </button> + </a> + </div> + } </li> </ul> </nav> diff --git a/angular/RestClient/src/app/core/navigation/navigation.component.ts b/angular/RestClient/src/app/core/navigation/navigation.component.ts index ccdab77b49379e3ed27abd65ada67e693882939a..effeb032ae23e20086be4661a92971ac5543cb36 100644 --- a/angular/RestClient/src/app/core/navigation/navigation.component.ts +++ b/angular/RestClient/src/app/core/navigation/navigation.component.ts @@ -1,10 +1,133 @@ -import { Component } from '@angular/core'; -import { Router, RouterModule } from '@angular/router'; +import { OnInit, Component, ViewChild } from '@angular/core'; +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 { 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; + +export function genId() { + const comp = 'navigation'; + return `${comp}-${comp_id++}`; +} + +interface Section { + id: string; + icon: string; + text: string; + link: string; + allowRoles?: UserRol[]; +} + @Component({ selector: 'app-navigation', standalone: true, - imports: [RouterModule], + imports: [RouterModule, MatButtonModule, MatMenuModule, MatIconModule], templateUrl: './navigation.component.html', styleUrl: './navigation.component.css', }) -export class NavigationComponent {} +export class NavigationComponent implements OnInit { + @ViewChild(MatMenuTrigger) + trigger?: MatMenuTrigger; + isLogged = false; + user: Session = { + id: 0, + name: '', + email: '', + rol: 'CLIENT', + }; + sections: Section[] = []; + + constructor(private sessionService: SessionService) {} + + ngOnInit() { + this.loadUser(); + } + + loadUser() { + this.sessionService.getSession().subscribe({ + next: (session) => { + if (session) { + this.user = session; + this.isLogged = true; + this.sections = this.genSections(); + } else { + this.isLogged = false; + } + }, + }); + } + + toggleDropdown() { + if (this.trigger) { + if (this.trigger.menuOpen) this.trigger.closeMenu(); + else this.trigger.openMenu(); + } + } + + schemaSections: Section[] = [ + { + id: genId(), + icon: 'person', + text: 'Información personal', + link: '/me', + }, + { + id: genId(), + icon: 'calendar_today', + text: 'Reservas', + allowRoles: ['CLIENT'], + link: '/me/bookings', + }, + { + id: genId(), + icon: 'hotel', + text: 'Hoteles', + allowRoles: ['HOTEL_ADMIN'], + link: '/me/hotels', + }, + { + id: genId(), + icon: 'fiber_new', + text: 'Registrar hotel', + allowRoles: ['HOTEL_ADMIN'], + link: '/hotels/register', + }, + { + 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) => + !section.allowRoles || + section.allowRoles.length === 0 || // No tiene limitación + section.allowRoles.includes(this.user.rol) // El rol del usuario es aceptado + ); + } + + login() {} + + logout() { + // 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/security/auth.interceptor.ts b/angular/RestClient/src/app/security/auth.interceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a3dadfdaa637c526d0beb436546a9117359f49f --- /dev/null +++ b/angular/RestClient/src/app/security/auth.interceptor.ts @@ -0,0 +1,37 @@ +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']; + +function isExcludedUrl(url: string) { + return excluded.some((excluded) => url.includes(excluded)); +} + +export const authRequest: HttpInterceptorFn = (req, next) => { + // Obtener el token desde localStorage (o cualquier otro mecanismo) + const session = inject(SessionService); // Obtener instancia del servicio + const isLogged = session.isLogged(); + + 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}`, + 'Content-Type': 'application/json', + }, + }) + : req; + + // Pasar la solicitud modificada al siguiente manejador + return next(authReq); +}; 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/data.service.spec.ts b/angular/RestClient/src/app/shared/auth-client.service.spec.ts similarity index 52% rename from angular/RestClient/src/app/shared/data.service.spec.ts rename to angular/RestClient/src/app/shared/auth-client.service.spec.ts index 38e8d9ec63f1f637f51a18393f66c56d564e49a9..718c264fe9fa26ade0a95ea726d68cf899584f1d 100644 --- a/angular/RestClient/src/app/shared/data.service.spec.ts +++ b/angular/RestClient/src/app/shared/auth-client.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { DataService } from './data.service'; +import { AuthClientService } from './auth-client.service'; -describe('DataService', () => { - let service: DataService; +describe('AuthClientService', () => { + let service: AuthClientService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(DataService); + service = TestBed.inject(AuthClientService); }); it('should be created', () => { diff --git a/angular/RestClient/src/app/shared/auth-client.service.ts b/angular/RestClient/src/app/shared/auth-client.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..99d3ebbfc0c91cc88ba2f4266cd1f895ccbfebd9 --- /dev/null +++ b/angular/RestClient/src/app/shared/auth-client.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; +import { HttpClient } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthClientService { + private readonly URI = environment.authAPI; + + constructor(private http: HttpClient) {} + + login(email: String, password: String) { + return this.http.post( + `${this.URI}/login`, + { email, password } + // { + // headers: { + // 'Content-Type': 'application/json', + // 'Access-Control-Allow-Origin': '*', + // 'Access-Control-Allow-Methods': + // 'GET, POST, OPTIONS, PUT, PATCH, DELETE', + // 'Access-Control-Allow-Headers': 'X-Requested-With,content-type', + // 'Access-Control-Allow-Credentials': 'true', + // }, + // } + ); + } + + register(name: String, email: String, password: String, rol?: String) { + return this.http.post( + `${this.URI}/register`, + { + name, + email, + password, + rol, + } + // { + // headers: { + // 'Content-Type': 'application/json', + // }, + // } + ); + } +} diff --git a/angular/RestClient/src/app/shared/booking-client.service.spec.ts b/angular/RestClient/src/app/shared/booking-client.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7155f3c6a365fd8f46220cabbf90d2889eeaa05a --- /dev/null +++ b/angular/RestClient/src/app/shared/booking-client.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BookingClientService } from './booking-client.service'; + +describe('BookingClientService', () => { + let service: BookingClientService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BookingClientService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/angular/RestClient/src/app/shared/booking-client.service.ts b/angular/RestClient/src/app/shared/booking-client.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a15867105f00cf0bd6ea093a4a64bebbb3fd048f --- /dev/null +++ b/angular/RestClient/src/app/shared/booking-client.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; +import { Booking } from '../types/Booking'; // Ajusta la ruta a tu modelo Booking + +@Injectable({ + providedIn: 'root', +}) +export class BookingClientService { + private URI = environment.bookingAPI; + + constructor(private http: HttpClient) {} + + // Método para crear una nueva reserva + createBooking(bookingRequest: Booking): Observable<Booking> { + 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 + getAllBookings(): Observable<Booking[]> { + return this.http.get<Booking[]>(this.URI); + } + + // Método para obtener una reserva por ID + getBookingById(id: number): Observable<Booking> { + return this.http.get<Booking>(`${this.URI}/${id}`); + } + + getBookingsByUser(userId: number) { + return this.http.get<Booking[]>(`${this.URI}?userId=${userId}`); + } + + // Método para eliminar una reserva + deleteBooking(id: number) { + return this.http.delete(`${this.URI}/${id}`); + } +} diff --git a/angular/RestClient/src/app/shared/booking.service.ts b/angular/RestClient/src/app/shared/booking.service.ts deleted file mode 100644 index 535f4d55ae2c7492a18e0bbf8472c0e78ca99392..0000000000000000000000000000000000000000 --- a/angular/RestClient/src/app/shared/booking.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -// booking.service.ts -import { Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable } from 'rxjs'; - -import { Booking } from '../../types/Booking'; // Ajusta la ruta a tu modelo Booking -import { User, UserState } from '../../types'; - -@Injectable({ - providedIn: 'root', // Esto hace que el servicio esté disponible en toda la aplicación -}) -export class BookingService { - private apiUrl = 'http://localhost:8080/bookings'; - - constructor(private http: HttpClient) {} - - // Método para crear una nueva reserva - createBooking(bookingRequest: Booking): Observable<Booking> { - return this.http.post<Booking>(this.apiUrl, bookingRequest, { - headers: new HttpHeaders({ - 'Content-Type': 'application/json', - }), - }); - } - - // Método para obtener todas las reservas - getAllBookings(): Observable<Booking[]> { - return this.http.get<Booking[]>(this.apiUrl); - } - - // Método para obtener una reserva por ID - getBookingById(id: number): Observable<Booking> { - return this.http.get<Booking>(`${this.apiUrl}/${id}`); - } - - // Método para eliminar una reserva - deleteBooking(id: number) { - return this.http.delete(`${this.apiUrl}/${id}`); - } -} diff --git a/angular/RestClient/src/app/shared/cliente-api-rest.service.spec.ts b/angular/RestClient/src/app/shared/cliente-api-rest.service.spec.ts deleted file mode 100644 index 737c9449305eb7a05fdbb88e75ef1492b05990b3..0000000000000000000000000000000000000000 --- a/angular/RestClient/src/app/shared/cliente-api-rest.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { ClienteApiRestService } from './cliente-api-rest.service'; - -describe('ClienteApiRestService', () => { - let service: ClienteApiRestService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(ClienteApiRestService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/angular/RestClient/src/app/shared/cliente-api-rest.service.ts b/angular/RestClient/src/app/shared/cliente-api-rest.service.ts deleted file mode 100644 index cb4326ef52f9ed54bc172aa9fad9406872423815..0000000000000000000000000000000000000000 --- a/angular/RestClient/src/app/shared/cliente-api-rest.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Hotel, Booking, Room, UserState } from '../../types'; -import { User } from '../../types'; - -@Injectable({ - providedIn: 'root', -}) -export class ClienteApiRestService { - private static readonly BASE_URI = 'http://localhost:8080'; - private static readonly HOTEL_URI = `${ClienteApiRestService.BASE_URI}/hotels`; - private static readonly USER_URI = `${ClienteApiRestService.BASE_URI}/users`; - constructor(private http: HttpClient) {} - - getHotel(id: number) { - const url = `${ClienteApiRestService.HOTEL_URI}/${id}`; - return this.http.get<Hotel>(url); - } - - getAllHotels() { - const url = `${ClienteApiRestService.HOTEL_URI}`; - return this.http.get<Hotel[]>(url); - } - - deleteHotel(id: number) { - const url = `${ClienteApiRestService.HOTEL_URI}/${id}`; - return this.http.delete(url, { observe: 'response', responseType: 'text' }); - } - - addHotel(hotel: Hotel) { - const url = `${ClienteApiRestService.HOTEL_URI}`; - return this.http.post(url, hotel, { - observe: 'response', - responseType: 'text', - }); - } - - alterRoomAvailability( - hotelId: number, - roomId: number, - availability: boolean - ) { - const url = `${ClienteApiRestService.HOTEL_URI}/${hotelId}/rooms/${roomId}`; - return this.http.patch( - url, - { available: availability }, - { - observe: 'response', - responseType: 'text', - } - ); - } - - createBooking(bookingRequest: Booking) { - return this.http.post('http://localhost:8080/bookings', bookingRequest); - } - - getRoomsAvailableInDateRange(hotelId: number, start: Date, end: Date) { - const startStr = start.toISOString().split('T')[0]; - const endStr = end.toISOString().split('T')[0]; - const url = `${ClienteApiRestService.HOTEL_URI}/${hotelId}/rooms?start=${startStr}&end=${endStr}`; - return this.http.get<Room[]>(url); - } - - getUser(userId: number) { - return this.http.get<User>(`http://localhost:8080/users/${userId}`); - } - - getAllUsers() { - return this.http.get<User[]>('http://localhost:8080/users', { - observe: 'body', - }); - } - - getUserBookings(userId: number) { - return this.http.get<Booking[]>( - `${ClienteApiRestService.BASE_URI}/users/${userId}/bookings` - ); - } - - alterUserStatus(userId: number, status: UserState) { - return this.http.patch( - `${ClienteApiRestService.BASE_URI}/users/${userId}`, - { - status, - }, - { - observe: 'response', - responseType: 'text', - } - ); - } -} diff --git a/angular/RestClient/src/app/shared/data.service.ts b/angular/RestClient/src/app/shared/data.service.ts deleted file mode 100644 index f22afaeb6662ab48290b344d645945fc9a1c77b0..0000000000000000000000000000000000000000 --- a/angular/RestClient/src/app/shared/data.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; - -@Injectable({ - providedIn: 'root', -}) -export class DataService { - private message = new BehaviorSubject('hotel list'); - currentMessage = this.message.asObservable(); - - private showMessage = new BehaviorSubject<boolean>(false); - showCurrentMessage = this.showMessage.asObservable(); - - constructor() {} - - setMessage(message: string) { - this.message.next(message); - } - - setShowCurrentMessage(valor: boolean) { - this.showMessage.next(valor); - } -} diff --git a/angular/RestClient/src/app/shared/hotel-client.service.spec.ts b/angular/RestClient/src/app/shared/hotel-client.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d32879cfc481ef393ffba104889798d7d982863 --- /dev/null +++ b/angular/RestClient/src/app/shared/hotel-client.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { HotelClientService } from './hotel-client.service'; + +describe('HotelClientService', () => { + let service: HotelClientService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(HotelClientService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/angular/RestClient/src/app/shared/hotel-client.service.ts b/angular/RestClient/src/app/shared/hotel-client.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c50231651d1e0bc61b036e3e1220d468009a4f8 --- /dev/null +++ b/angular/RestClient/src/app/shared/hotel-client.service.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@angular/core'; +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'; + +@Injectable({ + providedIn: 'root', +}) +export class HotelClientService { + private readonly URI = environment.hotelAPI; + constructor( + private http: HttpClient, + private sessionService: SessionService + ) {} + + getHotel(id: number) { + const url = `${this.URI}/${id}`; + return this.http.get<Hotel>(url); + } + + getAllHotels(startDate?: Date, endDate?: Date) { + const url = `${this.URI}`; + if (!startDate || !endDate) return this.http.get<Hotel[]>(url); + const start = new Date(startDate).toISOString().split('T')[0]; + const end = new Date(endDate).toISOString().split('T')[0]; + return this.http.get<Hotel[]>(url, { params: { start, end } }); + } + + getAllHotelsByUser(userId:number, startDate?: Date, endDate?: Date) { + const url = `${this.URI}`; + if (!startDate || !endDate) + return this.http.get<Hotel[]>(url, { params: { managerId:userId } }); + const start = new Date(startDate).toISOString().split('T')[0]; + const end = new Date(endDate).toISOString().split('T')[0]; + return this.http.get<Hotel[]>(url, { params: { managerId:userId, start, end } }); + } + + deleteHotel(id: number) { + const url = `${this.URI}/${id}`; + return this.http.delete(url); + } + + addHotel(hotel: Hotel) { + const url = `${this.URI}`; + 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( + hotelId: number, + roomId: number, + availability: boolean + ) { + const url = `${this.URI}/${hotelId}/rooms/${roomId}`; + return this.http.patch(url, { available: availability }); + } + + getRoomsAvailableInDateRange(hotelId: number, start: Date, end: Date) { + const startStr = start.toISOString().split('T')[0]; + const endStr = end.toISOString().split('T')[0]; + const url = `${this.URI}/${hotelId}/rooms?start=${startStr}&end=${endStr}`; + return this.http.get<Room[]>(url); + } +} diff --git a/angular/RestClient/src/app/shared/local-storage.service.spec.ts b/angular/RestClient/src/app/shared/local-storage.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba1dbd4362ebcfddcd262fc30de07f4beeb466e3 --- /dev/null +++ b/angular/RestClient/src/app/shared/local-storage.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LocalStorageService } from './local-storage.service'; + +describe('LocalStorageService', () => { + let service: LocalStorageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LocalStorageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/angular/RestClient/src/app/shared/local-storage.service.ts b/angular/RestClient/src/app/shared/local-storage.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc328655c80df4c87132a25305026eb3f00d1fec --- /dev/null +++ b/angular/RestClient/src/app/shared/local-storage.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class LocalStorageService { + save(key: string, value: object) { + const content = JSON.stringify(value); + localStorage.setItem(key, content); + } + + read<T>(key: string) { + const json = localStorage.getItem(key); + const ret = json ? (JSON.parse(json) as T) : null; + return ret; + } + + consume<T>(key: string) { + const value = this.read<T>(key); + if (value !== null) { + this.remove(key); + } + return value; + } + + remove(key: string) { + localStorage.removeItem(key); + } +} diff --git a/angular/RestClient/src/app/shared/booking.service.spec.ts b/angular/RestClient/src/app/shared/session.service.spec.ts similarity index 54% rename from angular/RestClient/src/app/shared/booking.service.spec.ts rename to angular/RestClient/src/app/shared/session.service.spec.ts index 3992ef555abf9021f683a3b7fd42344842c4d6ed..4238e142b07aad273072a64a6df73c7960707501 100644 --- a/angular/RestClient/src/app/shared/booking.service.spec.ts +++ b/angular/RestClient/src/app/shared/session.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { BookingService } from './booking.service'; +import { SessionService } from './session.service'; -describe('BookingService', () => { - let service: BookingService; +describe('SessionService', () => { + let service: SessionService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(BookingService); + service = TestBed.inject(SessionService); }); it('should be created', () => { diff --git a/angular/RestClient/src/app/shared/session.service.ts b/angular/RestClient/src/app/shared/session.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..c654e45b787c55dcfe7eae814056c55f6b87549d --- /dev/null +++ b/angular/RestClient/src/app/shared/session.service.ts @@ -0,0 +1,159 @@ +import { Injectable } from '@angular/core'; +import { LocalStorageService } from './local-storage.service'; +import { PersistenToken, Session, UserRol } from '../types'; +import { BehaviorSubject, Observable, throwError } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; +import { jwtDecode } from 'jwt-decode'; +import { AuthClientService } from './auth-client.service'; +import { Router } from '@angular/router'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionService { + 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'; + } + + 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 }; + } + + /** + * 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); + } + + /** + * 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; + } + + /** + * Proporciona un Observable del estado de la sesión. + */ + getSession(): Observable<Session | null> { + return this.session$.asObservable(); + } + + updateData(data: Partial<Session>) { + // const session: Session = { ...this.session$.getValue() } as Session; + const saved = this.getSaved(); + console.log({ saved, data }); + + if (!saved || data.id !== saved.session?.id) return; + const session = { ...saved.session, ...data } as Session; + this.storage.save(this.tokenKey, { + ...saved, + session, + }); + this.session$.next(session); + } + + /** + * 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; + + 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.spec.ts b/angular/RestClient/src/app/shared/user-client.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3af6ef2803edf936358c39133305278bb689dd4c --- /dev/null +++ b/angular/RestClient/src/app/shared/user-client.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserClientService } from './user-client.service'; + +describe('UserClientService', () => { + let service: UserClientService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserClientService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/angular/RestClient/src/app/shared/user-client.service.ts b/angular/RestClient/src/app/shared/user-client.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..63575df4f440973090cde151f50137fb002b9d5d --- /dev/null +++ b/angular/RestClient/src/app/shared/user-client.service.ts @@ -0,0 +1,68 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; +import { Client, Session, User, UserState } from '../types'; +import { SessionService } from './session.service'; +import { tap } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class UserClientService { + private readonly URI = environment.userAPI; + + constructor( + private http: HttpClient, + private sessionService: SessionService + ) {} + + // Obtener un usuario por ID + getUser(userId: number) { + return this.http.get<User>(`${this.URI}/${userId}`); + } + + // Obtener todos los usuarios + getAllUsers() { + return this.http.get<Client[]>(this.URI, { + observe: 'body', + }); + } + + // Cambiar estado de un usuario + alterUserStatus(userId: number, status: UserState) { + return this.http.patch( + `${this.URI}/${userId}`, + { + status, + }, + { + observe: 'response', + responseType: 'text', + } + ); + } + + // Actualizar los datos del usuario + updateUser(userId: number, user: Partial<User>) { + return this.http.put(`${this.URI}/${userId}`, user).pipe( + tap(() => { + this.sessionService.updateData({ + id: userId, + ...user, + } as Partial<Session>); + }) + ); + } + + // Cambiar la contraseña del usuario + updatePassword(currentPassword: string, newPassword: string) { + return this.http.patch( + `${this.URI}/me/password`, + { currentPassword, newPassword }, + { + observe: 'response', + responseType: 'text', + } + ); + } +} diff --git a/angular/RestClient/src/types/Address.d.ts b/angular/RestClient/src/app/types/Address.d.ts similarity index 100% rename from angular/RestClient/src/types/Address.d.ts rename to angular/RestClient/src/app/types/Address.d.ts diff --git a/angular/RestClient/src/types/Booking.d.ts b/angular/RestClient/src/app/types/Booking.d.ts similarity index 100% rename from angular/RestClient/src/types/Booking.d.ts rename to angular/RestClient/src/app/types/Booking.d.ts diff --git a/angular/RestClient/src/types/Hotel.d.ts b/angular/RestClient/src/app/types/Hotel.d.ts similarity index 100% rename from angular/RestClient/src/types/Hotel.d.ts rename to angular/RestClient/src/app/types/Hotel.d.ts diff --git a/angular/RestClient/src/types/Room.d.ts b/angular/RestClient/src/app/types/Room.d.ts similarity index 100% rename from angular/RestClient/src/types/Room.d.ts rename to angular/RestClient/src/app/types/Room.d.ts 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/types/User.d.ts b/angular/RestClient/src/app/types/User.d.ts similarity index 51% rename from angular/RestClient/src/types/User.d.ts rename to angular/RestClient/src/app/types/User.d.ts index 1ecc3d7512e7ff0b644e5555d290c48e88518219..277f806a74fb1b2f854d733cc10acca67f18b840 100644 --- a/angular/RestClient/src/types/User.d.ts +++ b/angular/RestClient/src/app/types/User.d.ts @@ -2,10 +2,20 @@ export interface User { id: number; name: string; email: String; - // status: "noBookings" | "withActiveBookings" | "withInactiveBookings"; + rol: UserRol; +} + +export interface Client extends User { status: UserState; + // bookings: number[] // Booking[] } +export interface HotelAdmin extends User { + // hotels: number[] // Hotel[] +} + +export type UserRol = 'ADMIN' | 'CLIENT' | 'HOTEL_ADMIN'; + export type UserStateFilter = 'All' | UserState; export type UserState = diff --git a/angular/RestClient/src/types/index.ts b/angular/RestClient/src/app/types/index.ts similarity index 66% rename from angular/RestClient/src/types/index.ts rename to angular/RestClient/src/app/types/index.ts index b974a05f55ecc2a2b9a6e953882d7cc526689ab6..09338254dd1b87fdc9aa00cb7342f0865fc382de 100644 --- a/angular/RestClient/src/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..4449314c617cbe840dd565f8465279c597b0dd0a --- /dev/null +++ b/angular/RestClient/src/environments/environment.prod.ts @@ -0,0 +1,18 @@ +// 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`, +// }; +const monolithUrl = 'localhost:8080'; + +export const environment = { + production: false, + authAPI: 'http://localhost:8101', + userAPI: `http://${monolithUrl}/users`, + hotelAPI: `http://${monolithUrl}/hotels`, + bookingAPI: `http://${monolithUrl}/bookings`, +}; diff --git a/angular/RestClient/src/environments/environment.ts b/angular/RestClient/src/environments/environment.ts new file mode 100644 index 0000000000000000000000000000000000000000..4264ad5b8fcbc528de02ea9e038a19aa2f617254 --- /dev/null +++ b/angular/RestClient/src/environments/environment.ts @@ -0,0 +1,9 @@ +const monolithUrl = 'localhost:8080'; + +export const environment = { + production: false, + authAPI: 'http://localhost:8101', + userAPI: `http://${monolithUrl}/users`, + hotelAPI: `http://${monolithUrl}/hotels`, + bookingAPI: `http://${monolithUrl}/bookings`, +}; diff --git a/angular/RestClient/src/mocks/bookings.json b/angular/RestClient/src/mocks/bookings.json deleted file mode 100644 index 7db55ff76f1627de21762d95a7732a4f7be1ce34..0000000000000000000000000000000000000000 --- a/angular/RestClient/src/mocks/bookings.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "user": { - "name": "John Doe", - "email": "john.doe@example.com", - "status": "NO_BOOKINGS" - }, - "room": { - "roomNumber": "101", - "type": "SINGLE", - "available": true - }, - "startDate": "2024-03-01", - "endDate": "2024-03-08" - }, - { - "user": { - "name": "Pepe", - "email": "pepe@example.com", - "status": "WITH_ACTIVE_BOOKINGS" - }, - "room": { - "roomNumber": "101", - "type": "SINGLE", - "available": true - }, - "startDate": "2024-03-15", - "endDate": "2024-03-22" - } -] diff --git a/angular/RestClient/src/mocks/hotels.json b/angular/RestClient/src/mocks/hotels.json deleted file mode 100644 index 11381112f37ac1123d22f06a2a0526c07825f211..0000000000000000000000000000000000000000 --- a/angular/RestClient/src/mocks/hotels.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "id": 1, - "name": "Hotel 1", - "address": { - "id": 1, - "streetName": "Aca al lao", - "streetKind": "Alargada", - "number": 12, - "postCode": "12345" - }, - "rooms": [ - { - "id": 1, - "roomNumber": "101", - "type": "SINGLE", - "available": true - }, - { - "id": 2, - "roomNumber": "102", - "type": "DOUBLE", - "available": false - } - ] - }, - { - "id": 2, - "name": "Hotel 2", - "address": { - "id": 2, - "streetName": "Calle de la plaza", - "streetKind": "Alargada", - "number": 12, - "postCode": "12345" - }, - "rooms": [ - { - "id": 3, - "roomNumber": "103", - "type": "SUITE", - "available": true - } - ] - } -] diff --git a/angular/RestClient/src/mocks/users.json b/angular/RestClient/src/mocks/users.json deleted file mode 100644 index 2f02d00125b0deaa2a86db47ea26f0c91f2e5fa3..0000000000000000000000000000000000000000 --- a/angular/RestClient/src/mocks/users.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "name": "John Doe", - "email": "john.doe@example.com", - "status": "NO_BOOKINGS" - }, - { - "name": "Pepe", - "email": "pepe@example.com", - "status": "WITH_ACTIVE_BOOKINGS" - } -] diff --git a/angular/RestClient/src/mocks/users.ts b/angular/RestClient/src/mocks/users.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6fafe67e13993f0e5b1a48c5443d43f6d3c2c3d --- /dev/null +++ b/angular/RestClient/src/mocks/users.ts @@ -0,0 +1,18 @@ +import { Client, User } from '../app/types'; + +export const users: Client[] = [ + { + id: 1, + name: 'John Doe', + email: 'jon@com', + rol: 'CLIENT', + status: 'NO_BOOKINGS', + }, + { + id: 2, + name: 'Angela Doe', + email: 'angle@com', + rol: 'CLIENT', + status: 'NO_BOOKINGS', + }, +]; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..6f86e46bad45d0d7f2a56bc0ce180ea343d0353e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,73 @@ +volumes: + kong_data: {} + users_data: {} + +networks: + kong-net: {} + +services: + Auth-API: + image: auth-api-image + hostname: ${AUTH_SERVICE_HOSTNAME} + build: + context: ./java/services/auth + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 8101:8101 + networks: + - kong-net + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://${DB_SERVICE_HOSTNAME}:3306/${DB_DATABASE_NAME}?createDatabaseIfNotExist=true + depends_on: + - RoomsBooking-database + + RoomsBooking-API: + image: rooms-booking-api-image + hostname: ${ROOMS_BOOKING_SERVICE_HOSTNAME} + build: + context: ./java/roomBooking + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 8080:8080 + networks: + - kong-net + environment: + 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} + cap_add: + - SYS_NICE + restart: unless-stopped + ports: + - "3307:3306" + networks: + - kong-net + volumes: + - users_data:/var/lib/mysql + environment: + MYSQL_ROOT_PASSWORD: ClaveRoot + MYSQL_USER: user + MYSQL_PASSWORD: password + MYSQL_DATABASE: RoomsBooking + MYSQL_ROOT_HOST: "%" + + RoomsBooking-Web: + image: roomsbooking-web-image + build: + context: ./angular/RestClient + dockerfile: ./Dockerfile + restart: unless-stopped + ports: + - 4200:80 + networks: + - kong-net + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://${DB_SERVICE_HOSTNAME}:3306/${DB_DATABASE_NAME}?createDatabaseIfNotExist=true + depends_on: + - RoomsBooking-database diff --git a/java/roomBooking/Dockerfile b/java/roomBooking/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8d0b79d6514534deddac782fc9367bcdbcaf89c3 --- /dev/null +++ b/java/roomBooking/Dockerfile @@ -0,0 +1,10 @@ +FROM maven:3-openjdk-17 AS maven +WORKDIR /app +COPY ./ ./ +RUN mvn -Dmaven.test.skip clean package +FROM openjdk:17-jdk-oracle +ARG JAR_FILE=/app/target/*.jar +COPY --from=maven ${JAR_FILE} app.jar +ENV PORT 8080 +EXPOSE $PORT +ENTRYPOINT ["java","-jar", "/app.jar"] \ No newline at end of file diff --git a/java/roomBooking/pom.xml b/java/roomBooking/pom.xml index 476f735b695a459292a258d72d89261c89dca993..f6fa75bdb21b812320db5afa07b770891fb9bd95 100644 --- a/java/roomBooking/pom.xml +++ b/java/roomBooking/pom.xml @@ -49,6 +49,32 @@ <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-api</artifactId> + <version>0.11.5</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-impl</artifactId> + <version>0.11.5</version> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-jackson</artifactId> + <version>0.11.5</version> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>jakarta.servlet</groupId> + <artifactId>jakarta.servlet-api</artifactId> + <scope>provided</scope> + </dependency> </dependencies> <build> @@ -60,4 +86,4 @@ </plugins> </build> -</project> +</project> \ No newline at end of file diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/RoomBookingApplication.java b/java/roomBooking/src/main/java/com/uva/monolith/RoomBookingApplication.java similarity index 90% rename from java/roomBooking/src/main/java/com/uva/roomBooking/RoomBookingApplication.java rename to java/roomBooking/src/main/java/com/uva/monolith/RoomBookingApplication.java index 1a5f312d49b2d1db4ffe587e4ae02c35e1e9a506..0a5db248da6e6be909302e323931f79151b0ed62 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/RoomBookingApplication.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/RoomBookingApplication.java @@ -1,4 +1,4 @@ -package com.uva.roomBooking; +package com.uva.monolith; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; 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 new file mode 100644 index 0000000000000000000000000000000000000000..8264f84a2e9f57dabaedc5892ea8a3ca0e931232 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/config/SecurityConfig.java @@ -0,0 +1,49 @@ +package com.uva.monolith.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import com.uva.monolith.filter.JwtAuthenticationFilter; +import com.uva.monolith.services.users.models.UserRol; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(authorize -> authorize + // Permitir OPTIONS sin autenticación + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + // Acceso restringido a usuarios y administradores + .requestMatchers("users", "users/**").hasAnyRole( + UserRol.CLIENT.toString(), UserRol.HOTEL_ADMIN.toString(), UserRol.ADMIN.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 + .requestMatchers("bookings", "bookings/**") + .hasAnyRole(UserRol.ADMIN.toString(), UserRol.HOTEL_ADMIN.toString(), UserRol.CLIENT.toString()) + // Rechazar el resto + .anyRequest().denyAll()) + // Registra el filtro antes del filtro estándar de autenticación + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/GlobalExceptionHandler.java b/java/roomBooking/src/main/java/com/uva/monolith/exceptions/GlobalExceptionHandler.java similarity index 97% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/GlobalExceptionHandler.java rename to java/roomBooking/src/main/java/com/uva/monolith/exceptions/GlobalExceptionHandler.java index 03b21e27ffa7e4e921cc9801187911d9f1be6a52..9428c51a9c63c3623d44752c9e3cbe6cf78ac19f 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/GlobalExceptionHandler.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/exceptions/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package com.uva.roomBooking.Exceptions; +package com.uva.monolith.exceptions; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/HotelNotFoundException.java b/java/roomBooking/src/main/java/com/uva/monolith/exceptions/HotelNotFoundException.java similarity index 68% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/HotelNotFoundException.java rename to java/roomBooking/src/main/java/com/uva/monolith/exceptions/HotelNotFoundException.java index 3d47f5efad71b6fbf3b3f5912493c9c0af1ca412..129a0b1086b4b78eb1f1725b9f241f51ce5540f8 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/HotelNotFoundException.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/exceptions/HotelNotFoundException.java @@ -1,9 +1,9 @@ -package com.uva.roomBooking.Exceptions; +package com.uva.monolith.exceptions; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) // Devuelve un 404 cuando se lanza la excepción +@ResponseStatus(HttpStatus.NOT_FOUND) // Devuelve un 404 cuando se lanza la excepción public class HotelNotFoundException extends RuntimeException { public HotelNotFoundException(int id) { super("Hotel not found with id: " + id); diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/InvalidDateRangeException.java b/java/roomBooking/src/main/java/com/uva/monolith/exceptions/InvalidDateRangeException.java similarity index 78% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/InvalidDateRangeException.java rename to java/roomBooking/src/main/java/com/uva/monolith/exceptions/InvalidDateRangeException.java index 17a8420453ef402411b61b965d001560a4dd51ce..5fea986ef1e9279c459bc5aff10932049f283333 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/InvalidDateRangeException.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/exceptions/InvalidDateRangeException.java @@ -1,8 +1,7 @@ -package com.uva.roomBooking.Exceptions; +package com.uva.monolith.exceptions; public class InvalidDateRangeException extends RuntimeException { public InvalidDateRangeException(String message) { super(message); } } - diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/InvalidRequestException.java b/java/roomBooking/src/main/java/com/uva/monolith/exceptions/InvalidRequestException.java similarity index 88% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/InvalidRequestException.java rename to java/roomBooking/src/main/java/com/uva/monolith/exceptions/InvalidRequestException.java index a8433b6f620da742dab87dae96a2e0f0193709ff..ca09e054420dd174c4d2c3424dcc8fe74b6c8576 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Exceptions/InvalidRequestException.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/exceptions/InvalidRequestException.java @@ -1,4 +1,4 @@ -package com.uva.roomBooking.Exceptions; +package com.uva.monolith.exceptions; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/java/roomBooking/src/main/java/com/uva/monolith/filter/JwtAuthenticationFilter.java b/java/roomBooking/src/main/java/com/uva/monolith/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..9b31d1a0e80f529075ef20bc4e9a955cfa825d30 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/filter/JwtAuthenticationFilter.java @@ -0,0 +1,124 @@ +package com.uva.monolith.filter; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; + +import com.uva.monolith.services.users.models.UserRol; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.Filter; +import java.io.IOException; +import java.security.Key; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Date; + +@Component +public class JwtAuthenticationFilter implements Filter { + + private final String SECRET_KEY = "3cfa76ef14937c1c0ea519f8fc057a80fcd04a7420f8e8bcd0a7567c272e007b"; + + private Key getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); + return Keys.hmacShaKeyFor(keyBytes); + } + + private String getTokenFromRequest(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) + return null; + return authHeader.substring(7); + } + + private Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private boolean validateToken(String token) { + if (token == null) + return false;// no token + try { + // Verifica y analiza el token + Claims claims = getClaimsFromToken(token); + + // Verifica que el token no esté expirado + return claims.getExpiration().after(new Date()); + } catch (ExpiredJwtException e) { + System.out.println("[" + LocalDateTime.now().toString() + "] Token expirado: " + e.getMessage()); + } catch (UnsupportedJwtException e) { + System.out.println("[" + LocalDateTime.now().toString() + "] Token no soportado: " + e.getMessage()); + } catch (MalformedJwtException e) { + System.out.println("[" + LocalDateTime.now().toString() + "] Token malformado: " + e.getMessage()); + } catch (SignatureException e) { + System.out.println("[" + LocalDateTime.now().toString() + "] Firma inválida: " + e.getMessage()); + } catch (IllegalArgumentException e) { + System.out.println("[" + LocalDateTime.now().toString() + "] Token vacÃo o nulo: " + e.getMessage()); + } + return false; // Si ocurre cualquier excepción, el token es inválido + + } + + private String getEmailFromToken(String token) { + return getClaimsFromToken(token).getSubject(); + } + + private UserRol getRoleFromToken(String token) { + String rol = getClaimsFromToken(token).get("rol", String.class); + return UserRol.valueOf(rol); + } + + public static String getRol(UserRol rol) { + return String.format("ROLE_%s", rol.toString()); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + String token = getTokenFromRequest(httpRequest); + + System.out.println("[" + LocalDateTime.now().toString() + "] TOKEN " + token); + + if (validateToken(token)) { + + String email = getEmailFromToken(token); + UserRol role = getRoleFromToken(token); // Extraer el rol del token + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + email, null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + // Agregar el rol como autoridad + SimpleGrantedAuthority authority = new SimpleGrantedAuthority(getRol(role)); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(email, null, + Collections.singletonList(authority)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + chain.doFilter(request, response); + } + +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/controllers/BookingController.java b/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/controllers/BookingController.java new file mode 100644 index 0000000000000000000000000000000000000000..602f16a2f96e27d3e855440136cbcb41f9b57ab0 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/controllers/BookingController.java @@ -0,0 +1,51 @@ +package com.uva.monolith.services.bookings.controllers; + +import com.uva.monolith.services.bookings.models.Booking; +import com.uva.monolith.services.bookings.services.BookingService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/bookings") +@CrossOrigin(origins = "*") +public class BookingController { + + private final BookingService bookingService; + + public BookingController(BookingService bookingService) { + this.bookingService = bookingService; + } + + @GetMapping + public List<Booking> getAllBookings( + @RequestParam(required = false) LocalDate start, + @RequestParam(required = false) LocalDate end, + @RequestParam(required = false) Integer roomId, + @RequestParam(required = false) Integer userId) { + return bookingService.getBookings(start, end, roomId, userId); + } + + @PostMapping + public Booking createBooking(@RequestBody Booking booking) { + return bookingService.createBooking(booking); + } + + @GetMapping("/{id}") + public Booking getBookingById(@PathVariable Integer id) { + return bookingService.getBookingById(id); + } + + @DeleteMapping("/{id}") + public ResponseEntity<Void> deleteBooking(@PathVariable Integer id) { + try { + bookingService.deleteBooking(id); + return new ResponseEntity<>(HttpStatus.ACCEPTED); + } catch (RuntimeException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } +} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Booking.java b/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/models/Booking.java similarity index 84% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Models/Booking.java rename to java/roomBooking/src/main/java/com/uva/monolith/services/bookings/models/Booking.java index c546d1916b1c29a10f95c97f03db34e64ffbe7d2..533ee0c7a1fa053dce449b855f1e46e08dabbe66 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Booking.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/models/Booking.java @@ -1,4 +1,4 @@ -package com.uva.roomBooking.Models; +package com.uva.monolith.services.bookings.models; import jakarta.persistence.Basic; import jakarta.persistence.CascadeType; @@ -13,6 +13,9 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.time.LocalDate; +import com.uva.monolith.services.hotels.models.Room; +import com.uva.monolith.services.users.models.Client; + @Entity @Table(name = "bookings") public class Booking { @@ -22,7 +25,7 @@ public class Booking { private int id; @JoinColumn(name = "user_id", referencedColumnName = "id") @ManyToOne(optional = false, fetch = FetchType.EAGER, cascade = CascadeType.MERGE) - private User userId; + private Client userId; @JoinColumn(name = "room_id", referencedColumnName = "id") @ManyToOne(optional = false, fetch = FetchType.EAGER, cascade = CascadeType.MERGE) private Room roomId; @@ -34,7 +37,7 @@ public class Booking { public Booking() { } - public Booking(int id, User userId, Room roomID, LocalDate startDate, LocalDate endDate) { + public Booking(int id, Client userId, Room roomID, LocalDate startDate, LocalDate endDate) { this.id = id; this.userId = userId; this.roomId = roomID; @@ -50,11 +53,11 @@ public class Booking { return this.id; } - public void setUserId(User userId) { + public void setUserId(Client userId) { this.userId = userId; } - public User getUserId() { + public Client getUserId() { return this.userId; } diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/repositories/BookingRepository.java b/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/repositories/BookingRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..b5ace65939b898798e6e5416fedda793388f2615 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/repositories/BookingRepository.java @@ -0,0 +1,41 @@ +// BookingRepository.java +package com.uva.monolith.services.bookings.repositories; + +import jakarta.transaction.Transactional; + +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.uva.monolith.services.bookings.models.Booking; + +public interface BookingRepository extends JpaRepository<Booking, Integer> { + @Query("SELECT b FROM Booking b WHERE b.userId.id = ?1") + List<Booking> findByUserId(int userId); + + @Query("SELECT b FROM Booking b WHERE b.roomId.id = ?1") + List<Booking> findByRoomId(int roomId); + + @Query("SELECT b FROM Booking b WHERE b.startDate >= ?1 AND b.endDate <= ?2") + List<Booking> findByDateRange(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + @Query("SELECT b FROM Booking b WHERE b.roomId.id = ?1 AND b.startDate < ?2 AND b.endDate > ?3") + List<Booking> findByRoomIdAndDateRange(@Param("roomId") int roomId, @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + @Transactional + @Modifying + @Query("DELETE FROM Booking b WHERE b.id = ?1") + void deleteBookingById(@Param("id") Integer id); + + @Transactional + @Modifying + @Query("DELETE FROM Booking b WHERE b.roomId.hotel.id = ?1") + void deleteAllByHotelId(int hotelId); + +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/services/BookingService.java b/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/services/BookingService.java new file mode 100644 index 0000000000000000000000000000000000000000..176f49dd951630c9118b24536e8a7dd6f5a15521 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/bookings/services/BookingService.java @@ -0,0 +1,89 @@ +package com.uva.monolith.services.bookings.services; + +import com.uva.monolith.services.bookings.models.Booking; +import com.uva.monolith.services.bookings.repositories.BookingRepository; +import com.uva.monolith.services.hotels.models.Room; +import com.uva.monolith.services.hotels.repositories.RoomRepository; +import com.uva.monolith.services.users.models.Client; +import com.uva.monolith.services.users.repositories.ClientRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class BookingService { + + @Autowired + private BookingRepository bookingRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private ClientRepository clientRepository; + + public List<Booking> getBookings(LocalDate start, LocalDate end, Integer roomId, Integer userId) { + List<Booking> bookings = null; + + if (start != null && end != null) { + bookings = bookingRepository.findByDateRange(start, end); + } + if (roomId != null) { + if (bookings == null) { + bookings = bookingRepository.findByRoomId(roomId); + } else { + bookings = bookings.stream() + .filter(booking -> booking.getRoomId().getId() == roomId) + .toList(); + } + } + if (userId != null) { + if (bookings == null) { + bookings = bookingRepository.findByUserId(userId); + } else { + bookings = bookings.stream() + .filter(booking -> booking.getUserId().getId() == userId) + .toList(); + } + } + if (start == null && end == null && roomId == null && userId == null) { + bookings = bookingRepository.findAll(); + } + + return bookings; + } + + public Booking createBooking(Booking booking) { + Client user = clientRepository.findById(booking.getUserId().getId()) + .orElseThrow(() -> new RuntimeException("User not found")); + Room room = roomRepository.findById(booking.getRoomId().getId()) + .orElseThrow(() -> new RuntimeException("Room not found")); + + // Check availability + List<Booking> existingBookings = bookingRepository.findByRoomIdAndDateRange( + room.getId(), booking.getStartDate(), booking.getEndDate()); + + if (!existingBookings.isEmpty()) { + throw new RuntimeException("Room is not available for the selected dates"); + } + + booking.setUserId(user); + booking.setRoomId(room); + return bookingRepository.save(booking); + } + + public Booking getBookingById(Integer id) { + return bookingRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Booking not found")); + } + + public void deleteBooking(Integer id) { + if (!bookingRepository.existsById(id)) { + throw new RuntimeException("Booking not found"); + } + bookingRepository.deleteBookingById(id); + } +} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/HotelController.java b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/controllers/HotelController.java similarity index 62% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/HotelController.java rename to java/roomBooking/src/main/java/com/uva/monolith/services/hotels/controllers/HotelController.java index 803edc35fdcbf2b5ec7fc96972e8ee2a7777391d..781cb6278b0b40d237b603d7cfc61a63b7b2af0f 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/HotelController.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/controllers/HotelController.java @@ -1,28 +1,33 @@ -package com.uva.roomBooking.Controllers; +package com.uva.monolith.services.hotels.controllers; import java.util.List; import java.util.Map; +import java.util.Optional; import java.time.LocalDate; -import com.uva.roomBooking.Exceptions.HotelNotFoundException; -import com.uva.roomBooking.Exceptions.InvalidDateRangeException; -import com.uva.roomBooking.Exceptions.InvalidRequestException; -import com.uva.roomBooking.Models.Booking; -import com.uva.roomBooking.Models.Hotel; -import com.uva.roomBooking.Models.Room; -import com.uva.roomBooking.Repositories.BookingRepository; -import com.uva.roomBooking.Repositories.HotelRepository; -import com.uva.roomBooking.Repositories.RoomRepository; - +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; +import com.uva.monolith.exceptions.HotelNotFoundException; +import com.uva.monolith.exceptions.InvalidDateRangeException; +import com.uva.monolith.exceptions.InvalidRequestException; +import com.uva.monolith.services.bookings.repositories.BookingRepository; +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; @@ -36,13 +41,36 @@ public class HotelController { // Obtener todos los hoteles @GetMapping - public List<Hotel> getAllHotels() { - return hotelRepository.findAll(); + public List<Hotel> getAllHotels( + @RequestParam(required = false) Integer managerId, + @RequestParam(required = false) LocalDate start, + @RequestParam(required = false) LocalDate end) { + List<Hotel> hotels = (managerId != null) + ? hotelRepository.findAllByHotelManager(managerId) + : hotelRepository.findAll(); + if (start != null && end != null) { + // Filtramos para los hoteles que + // tengan habitaciones disponibles para ese rango de fechas + System.out.println(start); + System.out.println(end); + hotels = hotels.stream().map(h -> { + if (h.getRooms().size() == 0) + return h; + h.setRooms(roomRepository.findAvailableRoomsByHotelAndDates(h.getId(), start, end)); + return h; + }).filter(h -> h.getRooms().size() >= 0).toList(); + } + return hotels; } // 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); } @@ -61,6 +89,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/roomBooking/Models/Address.java b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Address.java similarity index 97% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Models/Address.java rename to java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Address.java index 5e51f55e710c68ac38d4cb1c5769e574b06654bf..5f31a2a530da46c00460ad6cc6151b0769c1da61 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Address.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Address.java @@ -1,4 +1,4 @@ -package com.uva.roomBooking.Models; +package com.uva.monolith.services.hotels.models; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Hotel.java b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Hotel.java similarity index 76% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Models/Hotel.java rename to java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Hotel.java index 5a23005de99c022b5fff9570e2c515fcfc75e07e..21f5cec8b44f9fae1a566b7af92964d30b654546 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Hotel.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Hotel.java @@ -1,9 +1,8 @@ -package com.uva.roomBooking.Models; +package com.uva.monolith.services.hotels.models; import java.util.List; -import com.fasterxml.jackson.annotation.JsonIdentityInfo; -import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.uva.monolith.services.users.models.HotelManager; import jakarta.persistence.Basic; import jakarta.persistence.CascadeType; @@ -13,6 +12,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; @@ -38,14 +38,19 @@ public class Hotel { @OneToMany(mappedBy = "hotel", fetch = FetchType.EAGER, cascade = CascadeType.ALL) private List<Room> rooms; + @ManyToOne(optional = false) + @JoinColumn(name = "hotel_manager", referencedColumnName = "id") + private HotelManager hotelManager; + public Hotel() { } - public Hotel(int id, String name, Address address, List<Room> rooms) { + public Hotel(int id, String name, Address address, List<Room> rooms, HotelManager hotelManager) { setId(id); setName(name); setAddress(address); setRooms(rooms); + setHotelManager(hotelManager); } public int getId() { @@ -81,4 +86,11 @@ public class Hotel { rooms.forEach(room -> room.setHotel(this)); } + public void setHotelManager(HotelManager hotelManager) { + this.hotelManager = hotelManager; + } + + public HotelManager getHotelManager() { + return hotelManager; + } } diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Room.java b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Room.java similarity index 86% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Models/Room.java rename to java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Room.java index 639dfdb0bd3be629b16c719373fa51c9a7584898..72a6a728f729dc7b93b727606172de7cbe385ebb 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Room.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/Room.java @@ -1,10 +1,9 @@ -package com.uva.roomBooking.Models; +package com.uva.monolith.services.hotels.models; import java.util.List; -import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.uva.monolith.services.bookings.models.Booking; import jakarta.persistence.Basic; import jakarta.persistence.CascadeType; @@ -36,7 +35,7 @@ public class Room { @Column(name = "room_number", nullable = false) private String roomNumber; @Column(name = "type", nullable = false) - private Tipo type; + private RoomType type; @Column(name = "available", nullable = false) private boolean available; @JsonIgnore @@ -46,7 +45,7 @@ public class Room { public Room() { } - public Room(int id, Hotel hotelId, String roomNumber, Tipo type, boolean available, List<Booking> bookings) { + public Room(int id, Hotel hotelId, String roomNumber, RoomType type, boolean available, List<Booking> bookings) { this.id = id; this.hotel = hotelId; this.roomNumber = roomNumber; @@ -79,11 +78,11 @@ public class Room { return this.roomNumber; } - public void setType(Tipo type) { + public void setType(RoomType type) { this.type = type; } - public Tipo getType() { + public RoomType getType() { return this.type; } diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/RoomType.java b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/RoomType.java new file mode 100644 index 0000000000000000000000000000000000000000..b9e82584850795afa7c7392248e3a6472ce24ac0 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/models/RoomType.java @@ -0,0 +1,7 @@ +package com.uva.monolith.services.hotels.models; + +public enum RoomType { + SINGLE, + DOUBLE, + SUITE +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..eddb6640a275284cb70bde60a29afd33039ba454 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/repositories/HotelRepository.java @@ -0,0 +1,13 @@ +package com.uva.monolith.services.hotels.repositories; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.uva.monolith.services.hotels.models.Hotel; + +public interface HotelRepository extends JpaRepository<Hotel, Integer> { + @Query("SELECT h FROM Hotel h WHERE h.hotelManager.id = ?1") + List<Hotel> findAllByHotelManager(Integer hotelManager); +} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/RoomRepository.java b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/repositories/RoomRepository.java similarity index 83% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/RoomRepository.java rename to java/roomBooking/src/main/java/com/uva/monolith/services/hotels/repositories/RoomRepository.java index 3df9937f1ba7dc7c8bc0aa839a7a93ffd0baa8de..15cc3d370129e0753b9ac9b1eb24136c93bf5405 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/RoomRepository.java +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/hotels/repositories/RoomRepository.java @@ -1,9 +1,10 @@ -package com.uva.roomBooking.Repositories; +package com.uva.monolith.services.hotels.repositories; -import com.uva.roomBooking.Models.Room; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import com.uva.monolith.services.hotels.models.Room; + import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -24,9 +25,9 @@ public interface RoomRepository extends JpaRepository<Room, Integer> { SELECT b FROM Booking b WHERE b.roomId.id = r.id AND ( - b.endDate > ?2 + b.endDate >= ?2 OR - b.startDate > ?3 + ?3 >= b.startDate ) ) """) 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 new file mode 100644 index 0000000000000000000000000000000000000000..070fb5772a4f49f622617abf7d55958f1b6007c8 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/controllers/UserController.java @@ -0,0 +1,112 @@ +package com.uva.monolith.services.users.controllers; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; + +import com.uva.monolith.services.users.models.User; +import com.uva.monolith.services.users.models.UserStatus; +import com.uva.monolith.services.users.services.UserService; + +@RestController +@RequestMapping("users") +@CrossOrigin(origins = "*") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping + public ResponseEntity<List<User>> getAllUsers() { + List<User> users = userService.getAllUsers(); + return ResponseEntity.ok(users); + } + + @GetMapping(params = { "email" }) + public ResponseEntity<?> getUserByEmail(@RequestParam String email) { + try { + return ResponseEntity.ok(userService.getUserByEmail(email)); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) + return new ResponseEntity<String>(HttpStatus.NOT_FOUND); + throw e; + } + } + + @PostMapping + public ResponseEntity<?> addUser(@RequestBody User user) { + userService.addUser(user); + return new ResponseEntity<>(HttpStatus.ACCEPTED); + } + + @GetMapping("/{id}") + public ResponseEntity<?> getUserById(@PathVariable int id) { + return ResponseEntity.ok(userService.getUserById(id)); + } + + @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) { + return new ResponseEntity<String>("Missing required fields", HttpStatus.BAD_REQUEST); + } + try { + User user = userService.updateUserData(id, name, email); + return new ResponseEntity<User>(user, HttpStatus.OK); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) + return new ResponseEntity<String>(HttpStatus.NOT_FOUND); + throw e; + } + } + + @PatchMapping("/{id}") + public ResponseEntity<?> updateUserState(@PathVariable int id, @RequestBody Map<String, String> json) { + + String strStatus = json.get("status"); + if (strStatus == null) { + return new ResponseEntity<String>("Missing required fields", HttpStatus.BAD_REQUEST); + } + try { + UserStatus userStatus = UserStatus.valueOf(strStatus); + return ResponseEntity.ok(userService.updateUserStatus(id, userStatus)); + } catch (IllegalArgumentException e) { + return new ResponseEntity<String>("Unknown user state", HttpStatus.BAD_REQUEST); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) + return new ResponseEntity<String>(HttpStatus.NOT_FOUND); + throw e; + } + + } + + @DeleteMapping("/{id}") + public ResponseEntity<?> deleteUser(@PathVariable Integer id) { + try { + return ResponseEntity.ok(userService.deleteUserById(id)); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) + return new ResponseEntity<String>(HttpStatus.NOT_FOUND); + throw e; + } + } + +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/AuthResponse.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/AuthResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..8f334813beee8cb95caa275c66e46c9c539f2bd5 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/AuthResponse.java @@ -0,0 +1,51 @@ +package com.uva.monolith.services.users.models; + +public class AuthResponse { + + private int id; + private String username; + private String email; + private String password; + private UserRol rol; + + public int getId() { + return this.id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public UserRol getRol() { + return this.rol; + } + + public void setRol(UserRol rol) { + this.rol = rol; + } + +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/Client.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/Client.java new file mode 100644 index 0000000000000000000000000000000000000000..e106ecd3789a0237602e3194feacab7ddcbf4dfd --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/Client.java @@ -0,0 +1,65 @@ +package com.uva.monolith.services.users.models; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.uva.monolith.services.bookings.models.Booking; + +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 +@Table(name = "user_client") +public class Client extends User { + + @Basic(optional = false) + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private UserStatus status; + + @JsonIgnore + @OneToMany(mappedBy = "userId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + private List<Booking> bookings; + + public Client() { + super(); + bookings = new ArrayList<>(); + status = UserStatus.NO_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); + } + + public UserStatus getStatus() { + if (getBookings() == null || getBookings().isEmpty()) + return UserStatus.NO_BOOKINGS; + boolean activeBookings = getBookings().stream() + .anyMatch(booking -> !booking.getEndDate().isBefore(LocalDate.now())); // reserva >= ahora + return activeBookings ? UserStatus.WITH_ACTIVE_BOOKINGS : UserStatus.WITH_INACTIVE_BOOKINGS; + } + + public void setStatus(UserStatus status) { + this.status = status; + } + + public List<Booking> getBookings() { + return this.bookings; + } + + public void setBookings(List<Booking> bookings) { + this.bookings = bookings; + } +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/HotelManager.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/HotelManager.java new file mode 100644 index 0000000000000000000000000000000000000000..0e6f4b0aafa35ab8b23d202814c1fabefdcf86ed --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/HotelManager.java @@ -0,0 +1,38 @@ +package com.uva.monolith.services.users.models; + +import java.util.ArrayList; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.uva.monolith.services.hotels.models.Hotel; +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 { + + @JsonIgnore + @OneToMany(mappedBy = "hotelManager", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + private List<Hotel> hotels; + + public HotelManager() { + super(); + hotels = new ArrayList<>(); + } + + public HotelManager(int id, String name, String email, String password, List<Hotel> hotels) { + super(id, name, email, password, UserRol.HOTEL_ADMIN); + setHotels(hotels); + } + + public List<Hotel> getHotels() { + return this.hotels; + } + + public void setHotels(List<Hotel> hotels) { + this.hotels = hotels; + } +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/User.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/User.java new file mode 100644 index 0000000000000000000000000000000000000000..45decd686b3972058eb920f6c2e07cd4293f1e05 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/User.java @@ -0,0 +1,95 @@ +package com.uva.monolith.services.users.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + @Column(nullable = false) + private int id; + + @Basic(optional = false) + @Column(nullable = false) + private String name; + + @Basic(optional = false) + @Column(nullable = false, unique = true) + private String email; + + @JsonIgnore + @Basic(optional = false) + @Column(nullable = false) + private String password; + + @Basic(optional = false) + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private UserRol rol = UserRol.CLIENT; + + public User() { + } + + public User(int id, String name, String email, String password, UserRol rol) { + setId(id); + setName(name); + setEmail(email); + setRol(rol); + } + + public int getId() { + return this.id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String rawPassword) { + this.password = rawPassword; + } + + public UserRol getRol() { + return this.rol; + } + + public void setRol(UserRol rol) { + this.rol = rol; + } +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/UserRol.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/UserRol.java new file mode 100644 index 0000000000000000000000000000000000000000..f408ba5ef9d34d96c32d3c42a6c2c51b1c6f22b1 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/UserRol.java @@ -0,0 +1,5 @@ +package com.uva.monolith.services.users.models; + +public enum UserRol { + ADMIN, HOTEL_ADMIN, CLIENT +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/UserStatus.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/UserStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..362b8688260d4c13dc4a8eae205411c9d5533d79 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/models/UserStatus.java @@ -0,0 +1,5 @@ +package com.uva.monolith.services.users.models; + +public enum UserStatus { + NO_BOOKINGS, WITH_ACTIVE_BOOKINGS, WITH_INACTIVE_BOOKINGS; +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/ClientRepository.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/ClientRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..1c1b46fbe665075b8f817367ff14ee65cf69ff76 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/ClientRepository.java @@ -0,0 +1,10 @@ +package com.uva.monolith.services.users.repositories; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.uva.monolith.services.users.models.Client; + +public interface ClientRepository extends JpaRepository<Client, Integer> { + Optional<Client> findByEmail(String email); +} \ No newline at end of file diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/HotelManagerRepository.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/HotelManagerRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..092a251b199fdecd80a2654fc3e6c96d1b7eb7f4 --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/HotelManagerRepository.java @@ -0,0 +1,10 @@ +package com.uva.monolith.services.users.repositories; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.uva.monolith.services.users.models.HotelManager; + +public interface HotelManagerRepository extends JpaRepository<HotelManager, Integer> { + Optional<HotelManager> findByEmail(String email); +} \ No newline at end of file diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/UserRepository.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/UserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..e5b44c976f095719854aa8070abc843b898036fe --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/repositories/UserRepository.java @@ -0,0 +1,11 @@ +package com.uva.monolith.services.users.repositories; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.uva.monolith.services.users.models.User; + +public interface UserRepository extends JpaRepository<User, Integer> { + Optional<User> findByEmail(String email); +} diff --git a/java/roomBooking/src/main/java/com/uva/monolith/services/users/services/UserService.java b/java/roomBooking/src/main/java/com/uva/monolith/services/users/services/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..193d736853b1f0d84ff86a4b24cfd0f8db52172d --- /dev/null +++ b/java/roomBooking/src/main/java/com/uva/monolith/services/users/services/UserService.java @@ -0,0 +1,107 @@ +package com.uva.monolith.services.users.services; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; + +import com.uva.monolith.services.users.models.AuthResponse; +import com.uva.monolith.services.users.models.Client; +import com.uva.monolith.services.users.models.User; +import com.uva.monolith.services.users.models.UserStatus; +import com.uva.monolith.services.users.repositories.ClientRepository; +import com.uva.monolith.services.users.repositories.UserRepository; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private ClientRepository clientRepository; + + public List<User> getAllUsers() { + return userRepository.findAll(); + } + + private User assertUser(Optional<? extends User> opUser) { + return opUser.orElseThrow(() -> new HttpClientErrorException(HttpStatus.NOT_FOUND)); + } + + public User getUserById(int id) { + return assertUser(userRepository.findById(id)); + } + + public AuthResponse getUserByEmail(String email) { + User u = assertUser(userRepository.findByEmail(email)); + AuthResponse auth = new AuthResponse(); + BeanUtils.copyProperties(u, auth); + auth.setUsername(u.getName()); + return auth; + } + + public User addUser(User user) { + // Actualmente está en el servicio AUTH + // TODO adaptar adecuadamente + throw new HttpClientErrorException(HttpStatus.MOVED_PERMANENTLY, "servicio actual en http://localhost:8101/login"); + // user.setStatus(UserStatus.NO_BOOKINGS); + // if (user.getRol() == null) // Rol por defecto + // user.setRol(UserRol.CONSUMER); + // // Guardamos + // return userRepository.save(user); + } + + public User updateUserData(int id, String name, String email) { + User user = getUserById(id); + user.setName(name); + user.setEmail(email); + return userRepository.save(user); + } + + public User updateUserStatus(int id, UserStatus status) { + + Client user = (Client) assertUser(clientRepository.findById(id)); + + boolean activeBookings = user.getBookings().stream() + .anyMatch(booking -> !booking.getEndDate().isBefore(LocalDate.now())); // reserva >= ahora + boolean inactiveBookings = user.getBookings().stream() + .anyMatch(booking -> booking.getStartDate().isBefore(LocalDate.now())); // reserva < ahora + + switch (status) { + case NO_BOOKINGS: + if (!user.getBookings().isEmpty()) + throw new IllegalArgumentException("Invalid State: The user has at least one booking"); + break; + case WITH_ACTIVE_BOOKINGS: + if (user.getBookings().isEmpty()) + throw new IllegalArgumentException("Invalid State: The user don't has bookings"); + if (!activeBookings) + throw new IllegalArgumentException("Invalid State: The user don't has active bookings"); + break; + case WITH_INACTIVE_BOOKINGS: + if (user.getBookings().isEmpty()) + throw new IllegalArgumentException("Invalid State: The user don't has bookings"); + if (!inactiveBookings) + throw new IllegalArgumentException("Invalid State: The user don't has inactive bookings"); + break; + default: + break; + } + user.setStatus(status); + return userRepository.save(user); + } + + public User deleteUserById(int id) { + User user = getUserById(id); + // TODO eliminar reservas de usuario ahora mismo no por el modo cascada pero a + // futuro sÃ, después de la disgregación en microservicios + userRepository.deleteById(id); + return user; + } +} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/BookingController.java b/java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/BookingController.java deleted file mode 100644 index c52866c2ca28e056ad9551b42e007c0f2f487a80..0000000000000000000000000000000000000000 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/BookingController.java +++ /dev/null @@ -1,80 +0,0 @@ -// BookingController.java -package com.uva.roomBooking.Controllers; - -import com.uva.roomBooking.Models.Booking; -import com.uva.roomBooking.Models.Room; -import com.uva.roomBooking.Models.User; -import com.uva.roomBooking.Repositories.BookingRepository; -import com.uva.roomBooking.Repositories.RoomRepository; -import com.uva.roomBooking.Repositories.UserRepository; - -import jakarta.transaction.Transactional; - -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/bookings") -@CrossOrigin(origins = "http://localhost:4200") -public class BookingController { - - private final BookingRepository bookingRepository; - private final UserRepository userRepository; - private final RoomRepository roomRepository; - - public BookingController(BookingRepository bookingRepository, UserRepository userRepository, - RoomRepository roomRepository) { - this.bookingRepository = bookingRepository; - this.userRepository = userRepository; - this.roomRepository = roomRepository; - } - - @GetMapping - public List<Booking> getAllBookings() { - return bookingRepository.findAll(); - } - - @PostMapping - public Booking createBooking(@RequestBody Booking booking) { - User user = userRepository.findById(booking.getUserId().getId()) - .orElseThrow(() -> new RuntimeException("User not found")); - Room room = roomRepository.findById(booking.getRoomId().getId()) - .orElseThrow(() -> new RuntimeException("Room not found")); - - // Verificar disponibilidad - List<Booking> existingBookings = bookingRepository.findByRoomIdAndDateRange( - room.getId(), booking.getStartDate(), booking.getEndDate()); - - if (!existingBookings.isEmpty()) { - throw new RuntimeException("Room is not available for the selected dates"); - } - - booking.setUserId(user); - booking.setRoomId(room); - return bookingRepository.save(booking); - } - - @GetMapping("/{id}") - public Booking getBookingById(@PathVariable Integer id) { - return bookingRepository.findById(id) - .orElseThrow(() -> new RuntimeException("Booking not found")); - } - - @DeleteMapping("/{id}") - @Transactional - public ResponseEntity<Void> deleteBooking(@PathVariable Integer id) { - try { - if (!bookingRepository.existsById(id)) - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - - bookingRepository.deleteBookingById(id); - return new ResponseEntity<>(HttpStatus.ACCEPTED); - } catch (Exception e) { - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); - } - } -} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/UserController.java b/java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/UserController.java deleted file mode 100644 index a0e6397b9cabc191f7d5d24ecde162e8da6b2f14..0000000000000000000000000000000000000000 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Controllers/UserController.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.uva.roomBooking.Controllers; - -import java.time.LocalDate; -import java.util.List; -import java.util.Map; - -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.uva.roomBooking.Models.UserStatus; -import com.uva.roomBooking.Models.Booking; -import com.uva.roomBooking.Models.User; -import com.uva.roomBooking.Repositories.UserRepository; - -@RestController -@RequestMapping("users") -@CrossOrigin(origins = "*") -public class UserController { - private final UserRepository userRepository; - - public UserController(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @GetMapping - public List<User> getAllUsers() { - return userRepository.findAll(); - } - - @PostMapping - public User addUser(@RequestBody User user) { - // TODO revisar como se desea manejar estado por defecto - user.setStatus(UserStatus.NO_BOOKINGS); - // Aunque se asegure a lo mejor no es la forma de manejo esperada - return userRepository.save(user); - } - - @GetMapping("/{id}") - public User getUserById(@PathVariable int id) { - return userRepository.findById(id).orElseThrow(); - } - - @PutMapping("/{id}") - public User updateUserData(@PathVariable int id, @RequestBody Map<String, String> json) { - User target = userRepository.findById(id).orElseThrow(); - if (!json.containsKey("name") || !json.containsKey("email")) { - // TODO cambiar manejo - throw new RuntimeException("Missing required fields"); - } - target.setName(json.get("name")); - target.setEmail(json.get("email")); - return userRepository.save(target); - } - - @PatchMapping("/{id}") - public User updateUserState(@PathVariable int id, @RequestBody Map<String, String> json) { - User target = userRepository.findById(id).orElseThrow(); - String strStatus = json.get("status"); - if (strStatus == null) { - // TODO cambiar manejo - throw new RuntimeException("Missing required fields"); - } - UserStatus userStatus = UserStatus.valueOf(strStatus); - - boolean activeBookings = target.getBookings().stream() - .anyMatch(booking -> !booking.getEndDate().isBefore(LocalDate.now())); // reserva >= ahora - boolean inactiveBookings = target.getBookings().stream() - .anyMatch(booking -> booking.getStartDate().isBefore(LocalDate.now())); // reserva < ahora - - switch (userStatus) { - // TODO Buscar como validar las (in)active bookings - case NO_BOOKINGS: - if (!target.getBookings().isEmpty()) - throw new IllegalArgumentException("Invalid State: The user has at least one booking"); - break; - case WITH_ACTIVE_BOOKINGS: - if (target.getBookings().isEmpty()) - throw new IllegalArgumentException("Invalid State: The user don't has bookings"); - if (!activeBookings) - throw new IllegalArgumentException("Invalid State: The user don't has active bookings"); - break; - case WITH_INACTIVE_BOOKINGS: - if (target.getBookings().isEmpty()) - throw new IllegalArgumentException("Invalid State: The user don't has bookings"); - if (!inactiveBookings) - throw new IllegalArgumentException("Invalid State: The user don't has inactive bookings"); - break; - default: - break; - } - target.setStatus(userStatus); - return userRepository.save(target); - } - - @DeleteMapping("/{id}") - public User deleteUser(@PathVariable Integer id) { - User target; - if ((target = userRepository.findById(id).orElseThrow()) != null) { - userRepository.deleteById(id); - } - return target; - } - - @GetMapping("/{id}/bookings") - public List<Booking> getUserBookingsById(@PathVariable int id) { - User user = userRepository.findById(id).orElseThrow(); - return user.getBookings(); - } -} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Tipo.java b/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Tipo.java deleted file mode 100644 index e1a48b53450e1f87ae070b1bf70bd6a9afc1ea34..0000000000000000000000000000000000000000 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/Tipo.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.uva.roomBooking.Models; - -public enum Tipo { - SINGLE, - DOUBLE, - SUITE -} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/BookingRepository.java b/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/BookingRepository.java deleted file mode 100644 index 16c860a30fd168d4e03018ef02466dcd5adb7f99..0000000000000000000000000000000000000000 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/BookingRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -// BookingRepository.java -package com.uva.roomBooking.Repositories; - -import com.uva.roomBooking.Models.Booking; - -import jakarta.transaction.Transactional; - -import java.time.LocalDate; -import java.util.List; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface BookingRepository extends JpaRepository<Booking, Integer> { - - @Query("SELECT b FROM Booking b WHERE b.roomId.id = ?1 AND b.startDate < ?2 AND b.endDate > ?3") - List<Booking> findByRoomIdAndDateRange(@Param("roomId") int roomId, @Param("startDate") LocalDate startDate, - @Param("endDate") LocalDate endDate); - - @Transactional - @Modifying - @Query("DELETE FROM Booking b WHERE b.id = ?1") - void deleteBookingById(@Param("id") Integer id); - - @Transactional - @Modifying - @Query("DELETE FROM Booking b WHERE b.roomId.hotel.id = ?1") - void deleteAllByHotelId(int hotelId); - -} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/HotelRepository.java b/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/HotelRepository.java deleted file mode 100644 index 290f129a7be376279300062d4bf3d7a614abefcc..0000000000000000000000000000000000000000 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/HotelRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.uva.roomBooking.Repositories; - -import com.uva.roomBooking.Models.Hotel; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface HotelRepository extends JpaRepository<Hotel, Integer> { - -} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/UserRepository.java b/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/UserRepository.java deleted file mode 100644 index 5f02f6d4a3b2147d181b8db36736d85ccfd884b5..0000000000000000000000000000000000000000 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Repositories/UserRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.uva.roomBooking.Repositories; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.uva.roomBooking.Models.User; - -public interface UserRepository extends JpaRepository<User, Integer> { - -} diff --git a/java/roomBooking/src/main/resources/application.properties b/java/roomBooking/src/main/resources/application.properties index 563d4ad07ad10e7fb52bb3defcc3b736d889447b..e9a1304c8364b789ce008c161df0f98daf1238a9 100644 --- a/java/roomBooking/src/main/resources/application.properties +++ b/java/roomBooking/src/main/resources/application.properties @@ -7,4 +7,5 @@ spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # Usar esto para alternar entre las exposición del room repository ya que no es necesario su uso pero por defecto, al no cubrir su ruta, se expone -# spring.data.rest.base-path=false \ No newline at end of file +# spring.data.rest.base-path=false +external.services.auth.host=localhost:8101 \ No newline at end of file diff --git a/java/services/auth/.gitignore b/java/services/auth/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2 --- /dev/null +++ b/java/services/auth/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/java/services/auth/Dockerfile b/java/services/auth/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8d0b79d6514534deddac782fc9367bcdbcaf89c3 --- /dev/null +++ b/java/services/auth/Dockerfile @@ -0,0 +1,10 @@ +FROM maven:3-openjdk-17 AS maven +WORKDIR /app +COPY ./ ./ +RUN mvn -Dmaven.test.skip clean package +FROM openjdk:17-jdk-oracle +ARG JAR_FILE=/app/target/*.jar +COPY --from=maven ${JAR_FILE} app.jar +ENV PORT 8080 +EXPOSE $PORT +ENTRYPOINT ["java","-jar", "/app.jar"] \ No newline at end of file diff --git a/java/services/auth/mvnw b/java/services/auth/mvnw new file mode 100755 index 0000000000000000000000000000000000000000..19529ddf8c6eaa08c5c75ff80652d21ce4b72f8c --- /dev/null +++ b/java/services/auth/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash> +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/java/services/auth/mvnw.cmd b/java/services/auth/mvnw.cmd new file mode 100644 index 0000000000000000000000000000000000000000..249bdf3822221aa612d1da2605316cabd7b07e50 --- /dev/null +++ b/java/services/auth/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash> +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/java/services/auth/pom.xml b/java/services/auth/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..19b9923d7ad2a8b056e02681630234471de0a9d3 --- /dev/null +++ b/java/services/auth/pom.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>3.3.4</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + <groupId>com.uva</groupId> + <artifactId>authentication</artifactId> + <version>0.0.1-SNAPSHOT</version> + <name>authentication</name> + <description>Authentication microservicio</description> + <url/> + <licenses> + <license/> + </licenses> + <developers> + <developer/> + </developers> + <scm> + <connection/> + <developerConnection/> + <tag/> + <url/> + </scm> + <properties> + <java.version>17</java.version> + </properties> + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-rest</artifactId> + </dependency> + + <dependency> + <groupId>com.mysql</groupId> + <artifactId>mysql-connector-j</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core --> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-core</artifactId> + <version>6.4.1</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-api</artifactId> + <version>0.11.5</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-impl</artifactId> + <version>0.11.5</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-jackson</artifactId> + <version>0.11.5</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + </plugin> + </plugins> + </build> + +</project> \ No newline at end of file diff --git a/java/services/auth/src/main/java/com/uva/authentication/AuthenticationApplication.java b/java/services/auth/src/main/java/com/uva/authentication/AuthenticationApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..7b4cb3cd886885343249e97189a911ef42014655 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/AuthenticationApplication.java @@ -0,0 +1,13 @@ +package com.uva.authentication; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthenticationApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthenticationApplication.class, args); + } + +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/api/UserAPI.java b/java/services/auth/src/main/java/com/uva/authentication/api/UserAPI.java new file mode 100644 index 0000000000000000000000000000000000000000..d4aa44728a593f54275339d99c9e603b20b802e9 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/api/UserAPI.java @@ -0,0 +1,74 @@ +// TODO eliminar si realmente no necesitamos comunicar un servicio con otro +package com.uva.authentication.api; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import com.uva.authentication.models.RegisterRequest; +import com.uva.authentication.models.remote.User; +import com.uva.authentication.models.remote.UserRol; +import com.uva.authentication.utils.JwtUtil; + +@Component +public class UserAPI { + + @Autowired + private RestTemplate restTemplate; + + @Value("${external.services.users.baseurl}") + private String USER_API_URL; + + @Autowired + private JwtUtil jwtUtil; + + private String token; + private final User USER = new User(-1, "admin", null, null, UserRol.ADMIN); + + private String getAccessToken() { + if (token == null || token.isEmpty() || jwtUtil.isTokenValid(token, USER)) { + token = jwtUtil.generateToken(USER); + } + return token; + } + + public User getUserByEmail(String email) { + + // Implementación para acceder con autentificación + // String token = getAccessToken(); + // HttpHeaders headers = new HttpHeaders(); + // headers.set("Authorization", "Bearer " + token); + // HttpEntity<Void> entity = new HttpEntity<>(headers); + + String url = USER_API_URL + "?email={" + email + "}"; + try { + ResponseEntity<User> userResponse = restTemplate.getForEntity(url, User.class, email); + // restTemplate.exchange(url, HttpMethod.GET, entity, User.class); + return userResponse.getBody(); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() != HttpStatus.NOT_FOUND) + throw e; + return null; + } + } + + public User registerUser(RegisterRequest registerRequest) { + + String token = getAccessToken(); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + token); + + String url = USER_API_URL; + ResponseEntity<User> userResponse = restTemplate.postForEntity(url, registerRequest, User.class, headers); + if (!userResponse.getStatusCode().is2xxSuccessful()) + throw new RuntimeException("Failed to register user"); + + return userResponse.getBody(); + } + +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/config/RestTemplateConfig.java b/java/services/auth/src/main/java/com/uva/authentication/config/RestTemplateConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..ad00e740247bc6562bc0cadb41e417dcec238caa --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.uva.authentication.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/controllers/AuthController.java b/java/services/auth/src/main/java/com/uva/authentication/controllers/AuthController.java new file mode 100644 index 0000000000000000000000000000000000000000..23625a6e7824cadda471d847ecede0b94cb565da --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/controllers/AuthController.java @@ -0,0 +1,50 @@ +package com.uva.authentication.controllers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.HttpClientErrorException; +import com.uva.authentication.models.*; +import com.uva.authentication.services.AuthService; + +@RestController +@CrossOrigin(origins = "*") +public class AuthController { + + @Autowired + private AuthService authService; + + @PostMapping("/login") + public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { + try { + String token = authService.login(loginRequest); + return ResponseEntity.ok(new JwtAuthResponse(token)); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.FORBIDDEN) { + return new ResponseEntity<String>(e.getMessage(), HttpStatus.FORBIDDEN); + } + } + return new ResponseEntity<String>("Algo no fue bien", HttpStatus.UNAUTHORIZED); + } + + @PostMapping("/register") + public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) { + try { + LoginRequest loginRequest = new LoginRequest(); + loginRequest.setEmail(registerRequest.getEmail()); + loginRequest.setPassword(registerRequest.getPassword()); + + authService.register(registerRequest); + return login(loginRequest); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.CONFLICT) { + return new ResponseEntity<String>(e.getMessage(), HttpStatus.CONFLICT); + } + e.fillInStackTrace(); + } + + return new ResponseEntity<String>("Algo no fue bien", HttpStatus.UNAUTHORIZED); + } + +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/exceptions/GlobalExceptionHandler.java b/java/services/auth/src/main/java/com/uva/authentication/exceptions/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..6d4b8d58dbb5560de50b5b2149cbe3f181687446 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,51 @@ +package com.uva.authentication.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(HotelNotFoundException.class) + public ResponseEntity<Map<String, Object>> handleHotelNotFound(HotelNotFoundException ex) { + Map<String, Object> body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("message", ex.getMessage()); + + return new ResponseEntity<>(body, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(InvalidRequestException.class) + public ResponseEntity<Map<String, Object>> handleInvalidRequest(InvalidRequestException ex) { + Map<String, Object> body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("message", ex.getMessage()); + + return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(InvalidDateRangeException.class) + public ResponseEntity<Map<String, Object>> handleInvalidDateRange(InvalidDateRangeException ex) { + Map<String, Object> body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("message", ex.getMessage()); + + return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); + } + + // @ExceptionHandler(Exception.class) + // public ResponseEntity<Map<String, Object>> handleGeneralException(Exception + // ex) { + // Map<String, Object> body = new HashMap<>(); + // body.put("timestamp", LocalDateTime.now()); + // body.put("message", "An unexpected error occurred: " + ex.getMessage()); + + // return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR); + // } +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/exceptions/HotelNotFoundException.java b/java/services/auth/src/main/java/com/uva/authentication/exceptions/HotelNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..c642139b421a5cf864218fa2d0063f955335c5b7 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/exceptions/HotelNotFoundException.java @@ -0,0 +1,11 @@ +package com.uva.authentication.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) // Devuelve un 404 cuando se lanza la excepción +public class HotelNotFoundException extends RuntimeException { + public HotelNotFoundException(int id) { + super("Hotel not found with id: " + id); + } +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/exceptions/InvalidDateRangeException.java b/java/services/auth/src/main/java/com/uva/authentication/exceptions/InvalidDateRangeException.java new file mode 100644 index 0000000000000000000000000000000000000000..c3dc917fb03495480007365b117e185521cf7bf2 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/exceptions/InvalidDateRangeException.java @@ -0,0 +1,7 @@ +package com.uva.authentication.exceptions; + +public class InvalidDateRangeException extends RuntimeException { + public InvalidDateRangeException(String message) { + super(message); + } +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/exceptions/InvalidRequestException.java b/java/services/auth/src/main/java/com/uva/authentication/exceptions/InvalidRequestException.java new file mode 100644 index 0000000000000000000000000000000000000000..499a320e58ecd7576cbfff39101db14395f0edbe --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/exceptions/InvalidRequestException.java @@ -0,0 +1,11 @@ +package com.uva.authentication.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class InvalidRequestException extends RuntimeException { + public InvalidRequestException(String message) { + super(message); + } +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/AuthResponse.java b/java/services/auth/src/main/java/com/uva/authentication/models/AuthResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..e943a69108d5da38d4956509242935a6e4eb659e --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/AuthResponse.java @@ -0,0 +1,53 @@ +package com.uva.authentication.models; + +import com.uva.authentication.models.remote.UserRol; + +public class AuthResponse { + + private int id; + private String username; + private String email; + private String password; + private UserRol rol; + + public int getId() { + return this.id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public UserRol getRol() { + return this.rol; + } + + public void setRol(UserRol rol) { + this.rol = rol; + } + +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/JwtAuthResponse.java b/java/services/auth/src/main/java/com/uva/authentication/models/JwtAuthResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..a9566954c35ce71864881e320843e270a998b9da --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/JwtAuthResponse.java @@ -0,0 +1,14 @@ +package com.uva.authentication.models; + +public class JwtAuthResponse { + private String token; + + public JwtAuthResponse(String token) { + this.token = token; + } + + // Getter + public String getToken() { + return token; + } +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/LoginRequest.java b/java/services/auth/src/main/java/com/uva/authentication/models/LoginRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..d829de96fd9a45ed458976f03107f38e8933e797 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/LoginRequest.java @@ -0,0 +1,26 @@ +package com.uva.authentication.models; + +import java.util.Objects; + +public class LoginRequest { + private String email; + private String password = String.valueOf(Objects.hashCode("hi")); + + // Getters and setters + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/RegisterRequest.java b/java/services/auth/src/main/java/com/uva/authentication/models/RegisterRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..b3c98e8e71e7ff14f2bb90c9f1b4e0cecf7e8ae7 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/RegisterRequest.java @@ -0,0 +1,25 @@ +package com.uva.authentication.models; + +import com.uva.authentication.models.remote.UserRol; + +public class RegisterRequest extends LoginRequest { + private UserRol rol; + private String name; + + public UserRol getRol() { + return this.rol; + } + + public void setRol(UserRol rol) { + this.rol = rol; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..5ebf50a3b5b1135b6c6b1f7206486c95a9e95b92 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/Client.java @@ -0,0 +1,53 @@ +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 +@Table(name = "user_client") +public class Client extends User { + + @Basic(optional = false) + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private UserStatus status = UserStatus.NO_BOOKINGS; + + @OneToMany(mappedBy = "userId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + private List<Booking> bookings = new ArrayList<>(); + + public Client() { + super(); + } + + 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); + } + + public UserStatus getStatus() { + return this.status; + } + + public void setStatus(UserStatus status) { + this.status = status; + } + + public List<Booking> getBookings() { + return this.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 new file mode 100644 index 0000000000000000000000000000000000000000..dd58a508436be9e9e51a99d26038cf6efec373a6 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/HotelManager.java @@ -0,0 +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 { + + @OneToMany(mappedBy = "hotelManager", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + private List<Hotel> hotels = new ArrayList<>(); + + public HotelManager() { + super(); + } + + public HotelManager(int id, String name, String email, String password, List<Hotel> hotels) { + super(id, name, email, password, UserRol.HOTEL_ADMIN); + setHotels(hotels); + } + + public void setHotels(List<Hotel> hotels) { + this.hotels = hotels; + } + +} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/User.java b/java/services/auth/src/main/java/com/uva/authentication/models/remote/User.java similarity index 57% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Models/User.java rename to java/services/auth/src/main/java/com/uva/authentication/models/remote/User.java index c5373df6edc5e936d9c94f794ae36d3289d173d5..a72ecfa8ef8f896afda01b7b1ead124c09b81fab 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/User.java +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/User.java @@ -1,51 +1,56 @@ -package com.uva.roomBooking.Models; - -import java.util.List; +package com.uva.authentication.models.remote; import com.fasterxml.jackson.annotation.JsonIgnore; 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.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; import jakarta.persistence.Table; @Entity +@Inheritance(strategy = InheritanceType.JOINED) @Table(name = "users") public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) + @Column(nullable = false) private int id; @Basic(optional = false) + @Column(nullable = false) private String name; @Basic(optional = false) + @Column(nullable = false, unique = true) private String email; + @JsonIgnore @Basic(optional = false) - @Enumerated(EnumType.STRING) - private UserStatus status = UserStatus.NO_BOOKINGS; + @Column(nullable = false) + private String password; - @JsonIgnore - @OneToMany(mappedBy = "userId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) - private List<Booking> bookings; + @Basic(optional = false) + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private UserRol rol = UserRol.CLIENT; public User() { } - public User(int id, String name, String email, UserStatus status, List<Booking> bookings) { + public User(int id, String name, String email, String password, UserRol rol) { setId(id); + setName(name); setEmail(email); - setStatus(status); - setBookings(bookings); + setRol(rol); } public int getId() { @@ -72,19 +77,19 @@ public class User { this.email = email; } - public UserStatus getStatus() { - return this.status; + public String getPassword() { + return password; } - public void setStatus(UserStatus status) { - this.status = status; + public void setPassword(String rawPassword) { + this.password = rawPassword; } - public List<Booking> getBookings() { - return this.bookings; + public UserRol getRol() { + return this.rol; } - public void setBookings(List<Booking> bookings) { - this.bookings = bookings; + public void setRol(UserRol rol) { + this.rol = rol; } } diff --git a/java/services/auth/src/main/java/com/uva/authentication/models/remote/UserRol.java b/java/services/auth/src/main/java/com/uva/authentication/models/remote/UserRol.java new file mode 100644 index 0000000000000000000000000000000000000000..fe4d90dd1dd595f4c09ec699b452910352b406d5 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/UserRol.java @@ -0,0 +1,5 @@ +package com.uva.authentication.models.remote; + +public enum UserRol { + ADMIN, HOTEL_ADMIN, CLIENT +} diff --git a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/UserStatus.java b/java/services/auth/src/main/java/com/uva/authentication/models/remote/UserStatus.java similarity index 65% rename from java/roomBooking/src/main/java/com/uva/roomBooking/Models/UserStatus.java rename to java/services/auth/src/main/java/com/uva/authentication/models/remote/UserStatus.java index 41adce4ba6df6332286f0971b7f8e43b9126e401..5dd62bc5c75271eafa9daa58ad332aa8ad56d413 100644 --- a/java/roomBooking/src/main/java/com/uva/roomBooking/Models/UserStatus.java +++ b/java/services/auth/src/main/java/com/uva/authentication/models/remote/UserStatus.java @@ -1,4 +1,4 @@ -package com.uva.roomBooking.Models; +package com.uva.authentication.models.remote; public enum UserStatus { NO_BOOKINGS, WITH_ACTIVE_BOOKINGS, WITH_INACTIVE_BOOKINGS; 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 new file mode 100644 index 0000000000000000000000000000000000000000..6d2b8eeb777f2d3c9800c51dc34fe72ea504efd3 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/repositories/ClientRepository.java @@ -0,0 +1,10 @@ +package com.uva.authentication.repositories; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.uva.authentication.models.remote.Client; + +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 new file mode 100644 index 0000000000000000000000000000000000000000..c051ddd0425ff264337261645dbe6f17cfa55b51 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/repositories/HotelManagerRepository.java @@ -0,0 +1,10 @@ +package com.uva.authentication.repositories; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.uva.authentication.models.remote.HotelManager; + +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/repositories/UserRepository.java b/java/services/auth/src/main/java/com/uva/authentication/repositories/UserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..65f98edd7f2d0700de933199ae54c484eb8c71a3 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/repositories/UserRepository.java @@ -0,0 +1,10 @@ +package com.uva.authentication.repositories; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.uva.authentication.models.remote.User; + +public interface UserRepository extends JpaRepository<User, Integer> { + Optional<User> 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 new file mode 100644 index 0000000000000000000000000000000000000000..3ed67ececda2b0632e00f1b31ff04f3c05df866e --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/services/AuthService.java @@ -0,0 +1,98 @@ +package com.uva.authentication.services; + +import java.util.Optional; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; + +import com.uva.authentication.models.LoginRequest; +import com.uva.authentication.models.RegisterRequest; +import com.uva.authentication.models.remote.Client; +import com.uva.authentication.models.remote.HotelManager; +import com.uva.authentication.models.remote.User; +import com.uva.authentication.models.remote.UserRol; +import com.uva.authentication.repositories.ClientRepository; +import com.uva.authentication.repositories.HotelManagerRepository; +import com.uva.authentication.repositories.UserRepository; +import com.uva.authentication.utils.JwtUtil; +import com.uva.authentication.utils.SecurityUtils; + +@Service +public class AuthService { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private HotelManagerRepository hotelManagerRepository; + + @Autowired + private ClientRepository clientRepository; + + @Autowired + private UserRepository userRepository; + + private boolean authenticateUser(LoginRequest request, User user) { + if (user == null) + return false; + return SecurityUtils.checkPassword(request.getPassword(), user.getPassword()); + } + + public String login(LoginRequest loginRequest) { + User user = userRepository.findByEmail(loginRequest.getEmail()) + .orElseThrow(() -> new HttpClientErrorException(HttpStatus.FORBIDDEN, + "Invalid credentials")); + + if (!authenticateUser(loginRequest, user)) { + throw new HttpClientErrorException(HttpStatus.FORBIDDEN, "Invalid credentials"); + } + + return jwtUtil.generateToken(user); + } + + public User register(RegisterRequest registerRequest) { + Optional<User> user = userRepository.findByEmail(registerRequest.getEmail()); + if (user.isPresent()) + throw new HttpClientErrorException(HttpStatus.CONFLICT, "Email already in use"); + + return registerNewUser(registerRequest); + } + + private User registerNewUser(RegisterRequest registerRequest) { + User newUser; + + // Ciframos la contraseña + String hashPass = SecurityUtils.encrypt(registerRequest.getPassword()); + registerRequest.setPassword(hashPass); + + // Aseguramos que tenga un rol, por defecto es cliente + if (registerRequest.getRol() == null) + registerRequest.setRol(UserRol.CLIENT); + + switch (registerRequest.getRol()) { + case HOTEL_ADMIN: + HotelManager hm = new HotelManager(); + BeanUtils.copyProperties(registerRequest, hm); + newUser = hotelManagerRepository.save(hm); + break; + + case ADMIN: + User admin = new User(); + BeanUtils.copyProperties(registerRequest, admin); + newUser = userRepository.save(admin); + break; + + case CLIENT: // Por defecto cliente normal + default: + Client client = new Client(); + BeanUtils.copyProperties(registerRequest, client); + client.setRol(UserRol.CLIENT); + newUser = clientRepository.save(client); + break; + } + return newUser; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..e21efcdbabe51754bc2a2bf705eba0361f0696d6 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/utils/JwtUtil.java @@ -0,0 +1,85 @@ +package com.uva.authentication.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.uva.authentication.models.remote.User; + +@Component +public class JwtUtil { + + @Value("${security.jwt.secret-key}") + private String secretKey; + + @Value("${security.jwt.expiration-time}") + private long jwtExpiration; + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + public long getExpirationTime() { + return jwtExpiration; + } + + public String generateToken(User user) { + Map<String, Object> extraClaims = new HashMap<>(); + extraClaims.put("email", user.getEmail()); + extraClaims.put("rol", user.getRol()); + extraClaims.put("user", user); + long expiration = jwtExpiration; + + return Jwts + .builder() + .setClaims(extraClaims) + .setSubject(String.valueOf(user.getId())) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSignInKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public boolean isTokenValid(String token, User user) { + final String username = extractUsername(token); + return (username.equals(user.getName())) && !isTokenExpired(token); + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private Claims extractAllClaims(String token) { + return Jwts + .parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private Key getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/java/services/auth/src/main/java/com/uva/authentication/utils/SecurityUtils.java b/java/services/auth/src/main/java/com/uva/authentication/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..2df069eabbe53bdeaccbdbd58094bf8022ff35c7 --- /dev/null +++ b/java/services/auth/src/main/java/com/uva/authentication/utils/SecurityUtils.java @@ -0,0 +1,18 @@ +package com.uva.authentication.utils; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +public class SecurityUtils { + + private static BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + + public static String encrypt(String value) { + return encoder.encode(value); + } + + // Método para comparar la contraseña ingresada con el hash almacenado + public static boolean checkPassword(String rawPassword, String encodedPassword) { + return encoder.matches(rawPassword, encodedPassword); // Comparar la contraseña con el hash + } + +} diff --git a/java/services/auth/src/main/resources/application.properties b/java/services/auth/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..f08d9f6bd82253e53914fa3c4b695d4c25ba6f24 --- /dev/null +++ b/java/services/auth/src/main/resources/application.properties @@ -0,0 +1,16 @@ +spring.application.name=authService +server.port=8101 +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/RoomsBooking?createDatabaseIfNotExist=true +spring.datasource.username=user +spring.datasource.password=password +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# Usar esto para alternar entre las exposición del room repository ya que no es necesario su uso pero por defecto, al no cubrir su ruta, se expone +# spring.data.rest.base-path=false +security.jwt.secret-key=3cfa76ef14937c1c0ea519f8fc057a80fcd04a7420f8e8bcd0a7567c272e007b +# 1h in millisecond +security.jwt.expiration-time=3600000 + +external.services.users.baseurl=http://localhost:8080/users \ No newline at end of file diff --git a/java/services/auth/src/test/java/com/uva/roomBooking/RoomBookingApplicationTests.java b/java/services/auth/src/test/java/com/uva/roomBooking/RoomBookingApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..3b50599492c36017391c0dcabd81573f123a809a --- /dev/null +++ b/java/services/auth/src/test/java/com/uva/roomBooking/RoomBookingApplicationTests.java @@ -0,0 +1,13 @@ +package com.uva.roomBooking; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RoomBookingApplicationTests { + + @Test + void contextLoads() { + } + +}