From 3ba4e188b118e9c6dc1ed7d07aa528d78f661959 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Thu, 28 May 2026 22:26:11 +0530 Subject: [PATCH 01/59] =?UTF-8?q?Initial=20commit=20=E2=80=94=20Angular=20?= =?UTF-8?q?frontend=20under=20client/book-my-venue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Unified auth flow: single login/signup for users and vendors - Register as vendor checkbox on signup - Role-based redirect from JWT after login - Public venue listings (auth only required for booking) - Simplified landing page with Browse Venues CTA - Admin portal accessible via subtle footer link Co-Authored-By: Claude Sonnet 4.5 --- .claude/settings.local.json | 10 + .gitignore | 34 + BACKEND_API_CONTRACT.md | 906 ++ client/book-my-venue/.editorconfig | 17 + client/book-my-venue/.gitignore | 44 + client/book-my-venue/.postcssrc.json | 5 + client/book-my-venue/.prettierrc | 12 + client/book-my-venue/.vscode/extensions.json | 4 + client/book-my-venue/.vscode/launch.json | 20 + client/book-my-venue/.vscode/mcp.json | 9 + client/book-my-venue/.vscode/tasks.json | 42 + client/book-my-venue/README.md | 59 + client/book-my-venue/angular.json | 73 + client/book-my-venue/eslint.config.js | 16 + client/book-my-venue/package-lock.json | 10197 ++++++++++++++++ client/book-my-venue/package.json | 37 + client/book-my-venue/public/favicon.ico | Bin 0 -> 15086 bytes .../analytics/analytics.component.css | 1 + .../analytics/analytics.component.html | 30 + .../analytics/analytics.component.spec.ts | 7 + .../features/analytics/analytics.component.ts | 13 + .../features/bookings/bookings.component.css | 1 + .../features/bookings/bookings.component.html | 27 + .../bookings/bookings.component.spec.ts | 7 + .../features/bookings/bookings.component.ts | 21 + .../dashboard/dashboard.component.css | 1 + .../dashboard/dashboard.component.html | 27 + .../dashboard/dashboard.component.spec.ts | 11 + .../features/dashboard/dashboard.component.ts | 10 + .../forgot-password.component.css | 1 + .../forgot-password.component.html | 18 + .../forgot-password.component.spec.ts | 12 + .../forgot-password.component.ts | 24 + .../admin/features/login/login.component.css | 1 + .../admin/features/login/login.component.html | 22 + .../features/login/login.component.spec.ts | 12 + .../admin/features/login/login.component.ts | 30 + .../features/settings/settings.component.css | 1 + .../features/settings/settings.component.html | 15 + .../settings/settings.component.spec.ts | 7 + .../features/settings/settings.component.ts | 28 + .../admin/features/users/users.component.css | 1 + .../admin/features/users/users.component.html | 26 + .../features/users/users.component.spec.ts | 10 + .../admin/features/users/users.component.ts | 20 + .../features/vendors/vendors.component.css | 1 + .../features/vendors/vendors.component.html | 27 + .../vendors/vendors.component.spec.ts | 7 + .../features/vendors/vendors.component.ts | 19 + .../features/venues/venues.component.css | 1 + .../features/venues/venues.component.html | 26 + .../features/venues/venues.component.spec.ts | 7 + .../admin/features/venues/venues.component.ts | 13 + .../src/app/admin/guards/admin-auth.guard.ts | 15 + .../interceptors/admin-auth.interceptor.ts | 16 + .../admin-layout/admin-layout.component.css | 1 + .../admin-layout/admin-layout.component.html | 54 + .../admin-layout.component.spec.ts | 12 + .../admin-layout/admin-layout.component.ts | 30 + .../src/app/admin/models/index.ts | 4 + .../repositories/analytics.repository.ts | 27 + .../admin/repositories/booking.repository.ts | 18 + .../app/admin/repositories/user.repository.ts | 26 + .../admin/repositories/vendor.repository.ts | 30 + .../admin/repositories/venue.repository.ts | 22 + .../src/app/admin/routes/admin.routes.ts | 32 + .../app/admin/services/analytics.service.ts | 20 + .../src/app/admin/services/booking.service.ts | 22 + .../src/app/admin/services/user.service.ts | 22 + .../src/app/admin/services/vendor.service.ts | 22 + .../src/app/admin/services/venue.service.ts | 21 + client/book-my-venue/src/app/app.config.ts | 21 + client/book-my-venue/src/app/app.css | 0 client/book-my-venue/src/app/app.html | 1 + client/book-my-venue/src/app/app.routes.ts | 1 + client/book-my-venue/src/app/app.spec.ts | 23 + client/book-my-venue/src/app/app.ts | 11 + .../book-my-venue/src/app/core/app.routes.ts | 42 + .../src/app/core/config/environment.prod.ts | 6 + .../src/app/core/config/environment.ts | 6 + .../src/app/core/providers/app-providers.ts | 9 + .../src/app/core/tokens/injection-tokens.ts | 5 + .../src/app/landing/landing.component.css | 0 .../src/app/landing/landing.component.html | 242 + .../src/app/landing/landing.component.spec.ts | 22 + .../src/app/landing/landing.component.ts | 12 + .../components/button/button.component.css | 3 + .../components/button/button.component.html | 14 + .../button/button.component.spec.ts | 34 + .../components/button/button.component.ts | 44 + .../confirm-dialog.component.css | 3 + .../confirm-dialog.component.html | 38 + .../confirm-dialog.component.spec.ts | 26 + .../confirm-dialog.component.ts | 28 + .../empty-state/empty-state.component.css | 3 + .../empty-state/empty-state.component.html | 36 + .../empty-state/empty-state.component.spec.ts | 26 + .../empty-state/empty-state.component.ts | 21 + .../components/input/input.component.css | 3 + .../components/input/input.component.html | 26 + .../components/input/input.component.spec.ts | 35 + .../components/input/input.component.ts | 59 + .../components/loader/loader.component.css | 3 + .../components/loader/loader.component.html | 17 + .../loader/loader.component.spec.ts | 21 + .../components/loader/loader.component.ts | 23 + .../components/modal/modal.component.css | 3 + .../components/modal/modal.component.html | 23 + .../components/modal/modal.component.spec.ts | 34 + .../components/modal/modal.component.ts | 34 + .../pagination/pagination.component.css | 3 + .../pagination/pagination.component.html | 34 + .../pagination/pagination.component.spec.ts | 25 + .../pagination/pagination.component.ts | 59 + .../components/table/table.component.css | 3 + .../components/table/table.component.html | 54 + .../components/table/table.component.spec.ts | 33 + .../components/table/table.component.ts | 51 + .../constants/api-endpoints.constant.ts | 43 + .../src/app/shared/constants/index.ts | 2 + .../shared/constants/route-paths.constant.ts | 39 + .../directives/click-outside.directive.ts | 18 + .../app/shared/enums/booking-status.enum.ts | 7 + .../src/app/shared/enums/index.ts | 3 + .../src/app/shared/enums/user-role.enum.ts | 5 + .../src/app/shared/enums/venue-status.enum.ts | 6 + .../src/app/shared/guards/admin-auth.guard.ts | 16 + .../src/app/shared/guards/guest.guard.ts | 34 + .../src/app/shared/guards/user-auth.guard.ts | 16 + .../app/shared/guards/vendor-auth.guard.ts | 16 + .../shared/interceptors/error.interceptor.ts | 41 + .../app/shared/models/api-response.model.ts | 20 + .../app/shared/models/auth-response.model.ts | 38 + .../src/app/shared/models/booking.model.ts | 35 + .../src/app/shared/models/index.ts | 6 + .../src/app/shared/models/user.model.ts | 26 + .../src/app/shared/models/vendor.model.ts | 30 + .../src/app/shared/models/venue.model.ts | 64 + .../app/shared/pipes/currency-format.pipe.ts | 14 + .../src/app/shared/pipes/date-format.pipe.ts | 24 + .../app/shared/services/auth.repository.ts | 68 + .../src/app/shared/services/auth.service.ts | 229 + .../shared/services/notification.service.ts | 41 + .../app/shared/services/storage.service.ts | 20 + .../src/app/shared/utils/validators.ts | 38 + .../features/checkout/checkout.component.css | 3 + .../features/checkout/checkout.component.html | 95 + .../checkout/checkout.component.spec.ts | 24 + .../features/checkout/checkout.component.ts | 71 + .../forgot-password.component.css | 3 + .../forgot-password.component.html | 37 + .../forgot-password.component.spec.ts | 24 + .../forgot-password.component.ts | 33 + .../user/features/login/login.component.css | 3 + .../user/features/login/login.component.html | 50 + .../features/login/login.component.spec.ts | 28 + .../user/features/login/login.component.ts | 34 + .../my-bookings/my-bookings.component.css | 3 + .../my-bookings/my-bookings.component.html | 47 + .../my-bookings/my-bookings.component.spec.ts | 23 + .../my-bookings/my-bookings.component.ts | 46 + .../features/profile/profile.component.css | 3 + .../features/profile/profile.component.html | 52 + .../profile/profile.component.spec.ts | 24 + .../features/profile/profile.component.ts | 57 + .../user/features/signup/signup.component.css | 3 + .../features/signup/signup.component.html | 93 + .../features/signup/signup.component.spec.ts | 24 + .../user/features/signup/signup.component.ts | 41 + .../venue-details/venue-details.component.css | 3 + .../venue-details.component.html | 80 + .../venue-details.component.spec.ts | 24 + .../venue-details/venue-details.component.ts | 49 + .../venue-list/venue-list.component.css | 3 + .../venue-list/venue-list.component.html | 75 + .../venue-list/venue-list.component.spec.ts | 24 + .../venue-list/venue-list.component.ts | 42 + .../src/app/user/guards/user-auth.guard.ts | 16 + .../interceptors/user-auth.interceptor.ts | 19 + .../user-layout/user-layout.component.css | 4 + .../user-layout/user-layout.component.html | 96 + .../user-layout/user-layout.component.spec.ts | 24 + .../user-layout/user-layout.component.ts | 32 + .../src/app/user/models/index.ts | 3 + .../user/repositories/booking.repository.ts | 39 + .../app/user/repositories/user.repository.ts | 26 + .../app/user/repositories/venue.repository.ts | 41 + .../src/app/user/routes/user.routes.ts | 46 + .../src/app/user/services/booking.service.ts | 55 + .../src/app/user/services/user.service.ts | 30 + .../src/app/user/services/venue.service.ts | 38 + .../analytics/analytics.component.css | 1 + .../analytics/analytics.component.html | 41 + .../analytics/analytics.component.spec.ts | 10 + .../features/analytics/analytics.component.ts | 18 + .../features/bookings/bookings.component.css | 1 + .../features/bookings/bookings.component.html | 46 + .../bookings/bookings.component.spec.ts | 11 + .../features/bookings/bookings.component.ts | 25 + .../create-venue/create-venue.component.css | 1 + .../create-venue/create-venue.component.html | 39 + .../create-venue.component.spec.ts | 13 + .../create-venue/create-venue.component.ts | 55 + .../dashboard/dashboard.component.css | 1 + .../dashboard/dashboard.component.html | 97 + .../dashboard/dashboard.component.spec.ts | 19 + .../features/dashboard/dashboard.component.ts | 33 + .../edit-venue/edit-venue.component.css | 1 + .../edit-venue/edit-venue.component.html | 34 + .../edit-venue/edit-venue.component.spec.ts | 12 + .../edit-venue/edit-venue.component.ts | 73 + .../forgot-password.component.css | 1 + .../forgot-password.component.html | 20 + .../forgot-password.component.spec.ts | 19 + .../forgot-password.component.ts | 32 + .../vendor/features/login/login.component.css | 1 + .../features/login/login.component.html | 27 + .../features/login/login.component.spec.ts | 21 + .../vendor/features/login/login.component.ts | 34 + .../features/settings/settings.component.css | 1 + .../features/settings/settings.component.html | 17 + .../settings/settings.component.spec.ts | 10 + .../features/settings/settings.component.ts | 31 + .../features/signup/signup.component.css | 1 + .../features/signup/signup.component.html | 34 + .../features/signup/signup.component.spec.ts | 21 + .../features/signup/signup.component.ts | 40 + .../venue-list/venue-list.component.css | 1 + .../venue-list/venue-list.component.html | 39 + .../venue-list/venue-list.component.spec.ts | 13 + .../venue-list/venue-list.component.ts | 24 + .../app/vendor/guards/vendor-auth.guard.ts | 15 + .../interceptors/vendor-auth.interceptor.ts | 16 + .../vendor-layout/vendor-layout.component.css | 4 + .../vendor-layout.component.html | 91 + .../vendor-layout.component.spec.ts | 24 + .../vendor-layout/vendor-layout.component.ts | 34 + .../src/app/vendor/models/index.ts | 3 + .../repositories/analytics.repository.ts | 22 + .../vendor/repositories/booking.repository.ts | 21 + .../vendor/repositories/vendor.repository.ts | 27 + .../vendor/repositories/venue.repository.ts | 33 + .../src/app/vendor/routes/vendor.routes.ts | 24 + .../app/vendor/services/analytics.service.ts | 20 + .../app/vendor/services/booking.service.ts | 31 + .../src/app/vendor/services/vendor.service.ts | 12 + .../src/app/vendor/services/venue.service.ts | 35 + client/book-my-venue/src/index.html | 13 + client/book-my-venue/src/main.ts | 6 + client/book-my-venue/src/styles.css | 43 + client/book-my-venue/tsconfig.app.json | 16 + client/book-my-venue/tsconfig.json | 33 + client/book-my-venue/tsconfig.spec.json | 15 + 253 files changed, 17529 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 BACKEND_API_CONTRACT.md create mode 100644 client/book-my-venue/.editorconfig create mode 100644 client/book-my-venue/.gitignore create mode 100644 client/book-my-venue/.postcssrc.json create mode 100644 client/book-my-venue/.prettierrc create mode 100644 client/book-my-venue/.vscode/extensions.json create mode 100644 client/book-my-venue/.vscode/launch.json create mode 100644 client/book-my-venue/.vscode/mcp.json create mode 100644 client/book-my-venue/.vscode/tasks.json create mode 100644 client/book-my-venue/README.md create mode 100644 client/book-my-venue/angular.json create mode 100644 client/book-my-venue/eslint.config.js create mode 100644 client/book-my-venue/package-lock.json create mode 100644 client/book-my-venue/package.json create mode 100644 client/book-my-venue/public/favicon.ico create mode 100644 client/book-my-venue/src/app/admin/features/analytics/analytics.component.css create mode 100644 client/book-my-venue/src/app/admin/features/analytics/analytics.component.html create mode 100644 client/book-my-venue/src/app/admin/features/analytics/analytics.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/analytics/analytics.component.ts create mode 100644 client/book-my-venue/src/app/admin/features/bookings/bookings.component.css create mode 100644 client/book-my-venue/src/app/admin/features/bookings/bookings.component.html create mode 100644 client/book-my-venue/src/app/admin/features/bookings/bookings.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/bookings/bookings.component.ts create mode 100644 client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.css create mode 100644 client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.html create mode 100644 client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.ts create mode 100644 client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.css create mode 100644 client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.html create mode 100644 client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.ts create mode 100644 client/book-my-venue/src/app/admin/features/login/login.component.css create mode 100644 client/book-my-venue/src/app/admin/features/login/login.component.html create mode 100644 client/book-my-venue/src/app/admin/features/login/login.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/login/login.component.ts create mode 100644 client/book-my-venue/src/app/admin/features/settings/settings.component.css create mode 100644 client/book-my-venue/src/app/admin/features/settings/settings.component.html create mode 100644 client/book-my-venue/src/app/admin/features/settings/settings.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/settings/settings.component.ts create mode 100644 client/book-my-venue/src/app/admin/features/users/users.component.css create mode 100644 client/book-my-venue/src/app/admin/features/users/users.component.html create mode 100644 client/book-my-venue/src/app/admin/features/users/users.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/users/users.component.ts create mode 100644 client/book-my-venue/src/app/admin/features/vendors/vendors.component.css create mode 100644 client/book-my-venue/src/app/admin/features/vendors/vendors.component.html create mode 100644 client/book-my-venue/src/app/admin/features/vendors/vendors.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/vendors/vendors.component.ts create mode 100644 client/book-my-venue/src/app/admin/features/venues/venues.component.css create mode 100644 client/book-my-venue/src/app/admin/features/venues/venues.component.html create mode 100644 client/book-my-venue/src/app/admin/features/venues/venues.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/features/venues/venues.component.ts create mode 100644 client/book-my-venue/src/app/admin/guards/admin-auth.guard.ts create mode 100644 client/book-my-venue/src/app/admin/interceptors/admin-auth.interceptor.ts create mode 100644 client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.css create mode 100644 client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.html create mode 100644 client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts create mode 100644 client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.ts create mode 100644 client/book-my-venue/src/app/admin/models/index.ts create mode 100644 client/book-my-venue/src/app/admin/repositories/analytics.repository.ts create mode 100644 client/book-my-venue/src/app/admin/repositories/booking.repository.ts create mode 100644 client/book-my-venue/src/app/admin/repositories/user.repository.ts create mode 100644 client/book-my-venue/src/app/admin/repositories/vendor.repository.ts create mode 100644 client/book-my-venue/src/app/admin/repositories/venue.repository.ts create mode 100644 client/book-my-venue/src/app/admin/routes/admin.routes.ts create mode 100644 client/book-my-venue/src/app/admin/services/analytics.service.ts create mode 100644 client/book-my-venue/src/app/admin/services/booking.service.ts create mode 100644 client/book-my-venue/src/app/admin/services/user.service.ts create mode 100644 client/book-my-venue/src/app/admin/services/vendor.service.ts create mode 100644 client/book-my-venue/src/app/admin/services/venue.service.ts create mode 100644 client/book-my-venue/src/app/app.config.ts create mode 100644 client/book-my-venue/src/app/app.css create mode 100644 client/book-my-venue/src/app/app.html create mode 100644 client/book-my-venue/src/app/app.routes.ts create mode 100644 client/book-my-venue/src/app/app.spec.ts create mode 100644 client/book-my-venue/src/app/app.ts create mode 100644 client/book-my-venue/src/app/core/app.routes.ts create mode 100644 client/book-my-venue/src/app/core/config/environment.prod.ts create mode 100644 client/book-my-venue/src/app/core/config/environment.ts create mode 100644 client/book-my-venue/src/app/core/providers/app-providers.ts create mode 100644 client/book-my-venue/src/app/core/tokens/injection-tokens.ts create mode 100644 client/book-my-venue/src/app/landing/landing.component.css create mode 100644 client/book-my-venue/src/app/landing/landing.component.html create mode 100644 client/book-my-venue/src/app/landing/landing.component.spec.ts create mode 100644 client/book-my-venue/src/app/landing/landing.component.ts create mode 100644 client/book-my-venue/src/app/shared/components/button/button.component.css create mode 100644 client/book-my-venue/src/app/shared/components/button/button.component.html create mode 100644 client/book-my-venue/src/app/shared/components/button/button.component.spec.ts create mode 100644 client/book-my-venue/src/app/shared/components/button/button.component.ts create mode 100644 client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.css create mode 100644 client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.html create mode 100644 client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts create mode 100644 client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts create mode 100644 client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.css create mode 100644 client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.html create mode 100644 client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.spec.ts create mode 100644 client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.ts create mode 100644 client/book-my-venue/src/app/shared/components/input/input.component.css create mode 100644 client/book-my-venue/src/app/shared/components/input/input.component.html create mode 100644 client/book-my-venue/src/app/shared/components/input/input.component.spec.ts create mode 100644 client/book-my-venue/src/app/shared/components/input/input.component.ts create mode 100644 client/book-my-venue/src/app/shared/components/loader/loader.component.css create mode 100644 client/book-my-venue/src/app/shared/components/loader/loader.component.html create mode 100644 client/book-my-venue/src/app/shared/components/loader/loader.component.spec.ts create mode 100644 client/book-my-venue/src/app/shared/components/loader/loader.component.ts create mode 100644 client/book-my-venue/src/app/shared/components/modal/modal.component.css create mode 100644 client/book-my-venue/src/app/shared/components/modal/modal.component.html create mode 100644 client/book-my-venue/src/app/shared/components/modal/modal.component.spec.ts create mode 100644 client/book-my-venue/src/app/shared/components/modal/modal.component.ts create mode 100644 client/book-my-venue/src/app/shared/components/pagination/pagination.component.css create mode 100644 client/book-my-venue/src/app/shared/components/pagination/pagination.component.html create mode 100644 client/book-my-venue/src/app/shared/components/pagination/pagination.component.spec.ts create mode 100644 client/book-my-venue/src/app/shared/components/pagination/pagination.component.ts create mode 100644 client/book-my-venue/src/app/shared/components/table/table.component.css create mode 100644 client/book-my-venue/src/app/shared/components/table/table.component.html create mode 100644 client/book-my-venue/src/app/shared/components/table/table.component.spec.ts create mode 100644 client/book-my-venue/src/app/shared/components/table/table.component.ts create mode 100644 client/book-my-venue/src/app/shared/constants/api-endpoints.constant.ts create mode 100644 client/book-my-venue/src/app/shared/constants/index.ts create mode 100644 client/book-my-venue/src/app/shared/constants/route-paths.constant.ts create mode 100644 client/book-my-venue/src/app/shared/directives/click-outside.directive.ts create mode 100644 client/book-my-venue/src/app/shared/enums/booking-status.enum.ts create mode 100644 client/book-my-venue/src/app/shared/enums/index.ts create mode 100644 client/book-my-venue/src/app/shared/enums/user-role.enum.ts create mode 100644 client/book-my-venue/src/app/shared/enums/venue-status.enum.ts create mode 100644 client/book-my-venue/src/app/shared/guards/admin-auth.guard.ts create mode 100644 client/book-my-venue/src/app/shared/guards/guest.guard.ts create mode 100644 client/book-my-venue/src/app/shared/guards/user-auth.guard.ts create mode 100644 client/book-my-venue/src/app/shared/guards/vendor-auth.guard.ts create mode 100644 client/book-my-venue/src/app/shared/interceptors/error.interceptor.ts create mode 100644 client/book-my-venue/src/app/shared/models/api-response.model.ts create mode 100644 client/book-my-venue/src/app/shared/models/auth-response.model.ts create mode 100644 client/book-my-venue/src/app/shared/models/booking.model.ts create mode 100644 client/book-my-venue/src/app/shared/models/index.ts create mode 100644 client/book-my-venue/src/app/shared/models/user.model.ts create mode 100644 client/book-my-venue/src/app/shared/models/vendor.model.ts create mode 100644 client/book-my-venue/src/app/shared/models/venue.model.ts create mode 100644 client/book-my-venue/src/app/shared/pipes/currency-format.pipe.ts create mode 100644 client/book-my-venue/src/app/shared/pipes/date-format.pipe.ts create mode 100644 client/book-my-venue/src/app/shared/services/auth.repository.ts create mode 100644 client/book-my-venue/src/app/shared/services/auth.service.ts create mode 100644 client/book-my-venue/src/app/shared/services/notification.service.ts create mode 100644 client/book-my-venue/src/app/shared/services/storage.service.ts create mode 100644 client/book-my-venue/src/app/shared/utils/validators.ts create mode 100644 client/book-my-venue/src/app/user/features/checkout/checkout.component.css create mode 100644 client/book-my-venue/src/app/user/features/checkout/checkout.component.html create mode 100644 client/book-my-venue/src/app/user/features/checkout/checkout.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/features/checkout/checkout.component.ts create mode 100644 client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.css create mode 100644 client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.html create mode 100644 client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.ts create mode 100644 client/book-my-venue/src/app/user/features/login/login.component.css create mode 100644 client/book-my-venue/src/app/user/features/login/login.component.html create mode 100644 client/book-my-venue/src/app/user/features/login/login.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/features/login/login.component.ts create mode 100644 client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.css create mode 100644 client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.html create mode 100644 client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.ts create mode 100644 client/book-my-venue/src/app/user/features/profile/profile.component.css create mode 100644 client/book-my-venue/src/app/user/features/profile/profile.component.html create mode 100644 client/book-my-venue/src/app/user/features/profile/profile.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/features/profile/profile.component.ts create mode 100644 client/book-my-venue/src/app/user/features/signup/signup.component.css create mode 100644 client/book-my-venue/src/app/user/features/signup/signup.component.html create mode 100644 client/book-my-venue/src/app/user/features/signup/signup.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/features/signup/signup.component.ts create mode 100644 client/book-my-venue/src/app/user/features/venue-details/venue-details.component.css create mode 100644 client/book-my-venue/src/app/user/features/venue-details/venue-details.component.html create mode 100644 client/book-my-venue/src/app/user/features/venue-details/venue-details.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/features/venue-details/venue-details.component.ts create mode 100644 client/book-my-venue/src/app/user/features/venue-list/venue-list.component.css create mode 100644 client/book-my-venue/src/app/user/features/venue-list/venue-list.component.html create mode 100644 client/book-my-venue/src/app/user/features/venue-list/venue-list.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/features/venue-list/venue-list.component.ts create mode 100644 client/book-my-venue/src/app/user/guards/user-auth.guard.ts create mode 100644 client/book-my-venue/src/app/user/interceptors/user-auth.interceptor.ts create mode 100644 client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.css create mode 100644 client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.html create mode 100644 client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.spec.ts create mode 100644 client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.ts create mode 100644 client/book-my-venue/src/app/user/models/index.ts create mode 100644 client/book-my-venue/src/app/user/repositories/booking.repository.ts create mode 100644 client/book-my-venue/src/app/user/repositories/user.repository.ts create mode 100644 client/book-my-venue/src/app/user/repositories/venue.repository.ts create mode 100644 client/book-my-venue/src/app/user/routes/user.routes.ts create mode 100644 client/book-my-venue/src/app/user/services/booking.service.ts create mode 100644 client/book-my-venue/src/app/user/services/user.service.ts create mode 100644 client/book-my-venue/src/app/user/services/venue.service.ts create mode 100644 client/book-my-venue/src/app/vendor/features/analytics/analytics.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/analytics/analytics.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/analytics/analytics.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/analytics/analytics.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/bookings/bookings.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/bookings/bookings.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/bookings/bookings.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/bookings/bookings.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/login/login.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/login/login.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/login/login.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/login/login.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/settings/settings.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/settings/settings.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/settings/settings.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/settings/settings.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/signup/signup.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/signup/signup.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/signup/signup.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/signup/signup.component.ts create mode 100644 client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.css create mode 100644 client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.html create mode 100644 client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.ts create mode 100644 client/book-my-venue/src/app/vendor/guards/vendor-auth.guard.ts create mode 100644 client/book-my-venue/src/app/vendor/interceptors/vendor-auth.interceptor.ts create mode 100644 client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css create mode 100644 client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html create mode 100644 client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts create mode 100644 client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts create mode 100644 client/book-my-venue/src/app/vendor/models/index.ts create mode 100644 client/book-my-venue/src/app/vendor/repositories/analytics.repository.ts create mode 100644 client/book-my-venue/src/app/vendor/repositories/booking.repository.ts create mode 100644 client/book-my-venue/src/app/vendor/repositories/vendor.repository.ts create mode 100644 client/book-my-venue/src/app/vendor/repositories/venue.repository.ts create mode 100644 client/book-my-venue/src/app/vendor/routes/vendor.routes.ts create mode 100644 client/book-my-venue/src/app/vendor/services/analytics.service.ts create mode 100644 client/book-my-venue/src/app/vendor/services/booking.service.ts create mode 100644 client/book-my-venue/src/app/vendor/services/vendor.service.ts create mode 100644 client/book-my-venue/src/app/vendor/services/venue.service.ts create mode 100644 client/book-my-venue/src/index.html create mode 100644 client/book-my-venue/src/main.ts create mode 100644 client/book-my-venue/src/styles.css create mode 100644 client/book-my-venue/tsconfig.app.json create mode 100644 client/book-my-venue/tsconfig.json create mode 100644 client/book-my-venue/tsconfig.spec.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..93eeff778 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "PowerShell(node --version)", + "PowerShell(npm --version)", + "PowerShell(npx @angular/cli@latest version 2>$null)", + "PowerShell(if \\(-not $?\\) { \"Angular CLI not found globally\" })" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..2d5fa4443 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Old folder — contents moved to client/ +/book-my-venue/ + +# Mock server — not pushed to repo +mock-server/ + +# Node +node_modules/ +npm-debug.log +yarn-error.log + +# Angular +dist/ +.angular/cache/ +/tmp +/out-tsc + +# IDEs +.idea/ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/mcp.json +*.sublime-workspace + +# System +.DS_Store +Thumbs.db + +# Misc +coverage/ +*.log diff --git a/BACKEND_API_CONTRACT.md b/BACKEND_API_CONTRACT.md new file mode 100644 index 000000000..1ae484d75 --- /dev/null +++ b/BACKEND_API_CONTRACT.md @@ -0,0 +1,906 @@ +# Book My Venue - Backend API Contract + +> This document defines the complete API contract that the Angular frontend expects. All endpoints, request/response formats, headers, and enums are documented here. + +--- + +## Base Configuration + +- **Base URL (Dev):** `http://localhost:3000/api` +- **Base URL (Prod):** `https://api.bookmyvenue.com/api` +- **Content-Type:** `application/json` + +--- + +## Authentication + +### JWT Token Structure + +Tokens must be JWT and include a `role` field in the payload. + +```json +{ + "id": "user-id", + "email": "user@example.com", + "role": "user | vendor | admin", + "iat": 1234567890, + "exp": 1234567890 +} +``` + +### Auth Endpoints (Public - No Token Required) + +#### User Auth + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/auth/user/login` | User login | +| POST | `/auth/user/signup` | User registration | + +**Login Request:** +```json +{ + "email": "string (required)", + "password": "string (required)" +} +``` + +**Signup Request:** +```json +{ + "firstName": "string (required)", + "lastName": "string (required)", + "email": "string (required, valid email)", + "phone": "string (required, 10 digits)", + "password": "string (required, min 8 chars)" +} +``` + +#### Vendor Auth + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/auth/vendor/login` | Vendor login | +| POST | `/auth/vendor/signup` | Vendor registration | + +**Login Request:** +```json +{ + "email": "string (required)", + "password": "string (required)" +} +``` + +**Signup Request:** +```json +{ + "businessName": "string (required)", + "email": "string (required, valid email)", + "phone": "string (required, 10 digits)", + "password": "string (required, min 8 chars)" +} +``` + +#### Admin Auth + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/auth/admin/login` | Admin login (no signup - admins are seeded) | + +**Login Request:** +```json +{ + "email": "string (required)", + "password": "string (required)" +} +``` + +#### Common Auth + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/auth/logout` | Logout (invalidate token) | +| POST | `/auth/refresh-token` | Refresh access token | +| POST | `/auth/forgot-password` | Send password reset email | + +**Refresh Token Request:** +```json +{ + "refreshToken": "string (required)" +} +``` + +**Forgot Password Request:** +```json +{ + "email": "string (required)" +} +``` + +### Auth Response Format (Login & Signup) + +```json +{ + "token": "jwt-access-token", + "refreshToken": "refresh-token", + "user": { + "id": "string", + "email": "string", + "firstName": "string", + "lastName": "string", + "role": "user | vendor | admin" + } +} +``` + +--- + +## Headers on Protected Routes + +Every authenticated request from the frontend includes: + +``` +Authorization: Bearer +``` + +Additionally, portal-specific headers are sent: + +| Portal | Extra Header | +|--------|-------------| +| User | _(none)_ | +| Vendor | `X-Portal: vendor` | +| Admin | `X-Portal: admin` | + +**Backend must validate** that the JWT `role` matches the `X-Portal` header (where present) to prevent cross-portal access. + +--- + +## Standard Response Formats + +### Success - Single Item + +```json +{ + "data": { ... }, + "message": "Success" +} +``` + +### Success - Paginated List + +```json +{ + "data": [ ... ], + "total": 100, + "page": 1, + "limit": 10, + "totalPages": 10 +} +``` + +### Error + +```json +{ + "error": { + "message": "Human-readable error message", + "statusCode": 400 + } +} +``` + +### HTTP Status Codes Used + +| Code | Meaning | +|------|---------| +| 200 | Success | +| 201 | Created | +| 400 | Bad Request (validation errors) | +| 401 | Unauthorized (missing/expired token) | +| 403 | Forbidden (wrong role/portal) | +| 404 | Not Found | +| 422 | Unprocessable Entity | +| 500 | Internal Server Error | + +--- + +## Enums + +These values must be used consistently between frontend and backend. + +### UserRole + +| Value | Description | +|-------|-------------| +| `user` | Regular user | +| `vendor` | Venue vendor | +| `admin` | Platform admin | + +### BookingStatus + +| Value | Description | +|-------|-------------| +| `pending` | Awaiting vendor approval | +| `confirmed` | Approved by vendor | +| `cancelled` | Cancelled by user | +| `completed` | Event completed | +| `rejected` | Rejected by vendor | + +### VenueStatus + +| Value | Description | +|-------|-------------| +| `active` | Live and bookable | +| `inactive` | Hidden by vendor | +| `pending` | Awaiting admin approval | +| `rejected` | Rejected by admin | + +--- + +## API Endpoints by Portal + +--- + +### User Portal + +> All endpoints require `Authorization: Bearer ` with `role: user` + +#### Profile + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/users/profile` | Get current user's profile | +| PUT | `/users/profile` | Update profile | + +**GET `/users/profile` Response:** +```json +{ + "data": { + "id": "string", + "firstName": "string", + "lastName": "string", + "email": "string", + "phone": "string", + "role": "user", + "createdAt": "ISO 8601 date string", + "updatedAt": "ISO 8601 date string" + } +} +``` + +**PUT `/users/profile` Request:** +```json +{ + "firstName": "string (required)", + "lastName": "string (required)", + "phone": "string (required, 10 digits)" +} +``` + +#### Venues (Public Browsing) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/venues` | List all active venues (paginated, filterable) | +| GET | `/venues/:id` | Get venue details by ID | + +**GET `/venues` Query Parameters:** + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `page` | number | 1 | Page number | +| `limit` | number | 10 | Items per page | +| `search` | string | — | Search by name or description | +| `city` | string | — | Filter by city | +| `minPrice` | number | — | Minimum price per hour | +| `maxPrice` | number | — | Maximum price per hour | +| `capacity` | number | — | Minimum guest capacity | + +**GET `/venues` Response:** +```json +{ + "data": [ + { + "id": "string", + "name": "string", + "description": "string", + "address": "string", + "city": "string", + "state": "string", + "zipCode": "string", + "capacity": "number", + "pricePerHour": "number", + "amenities": ["string"], + "images": ["string (URL)"], + "status": "active", + "vendorId": "string", + "vendorName": "string", + "rating": "number (optional)", + "createdAt": "ISO 8601", + "updatedAt": "ISO 8601" + } + ], + "total": 100, + "page": 1, + "limit": 10, + "totalPages": 10 +} +``` + +**GET `/venues/:id` Response:** +```json +{ + "data": { + "id": "string", + "name": "string", + "description": "string", + "address": "string", + "city": "string", + "state": "string", + "zipCode": "string", + "capacity": "number", + "pricePerHour": "number", + "amenities": ["string"], + "images": ["string (URL)"], + "status": "active", + "vendorId": "string", + "vendorName": "string", + "rating": "number (optional)", + "createdAt": "ISO 8601", + "updatedAt": "ISO 8601" + } +} +``` + +#### Bookings + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/bookings/user` | Get current user's bookings | +| GET | `/bookings/:id` | Get booking details | +| POST | `/bookings` | Create a new booking | +| PUT | `/bookings/:id/cancel` | Cancel a booking | + +**GET `/bookings/user` Response:** +```json +{ + "data": [ + { + "id": "string", + "venueId": "string", + "venueName": "string", + "userId": "string", + "eventDate": "YYYY-MM-DD", + "startTime": "HH:mm", + "endTime": "HH:mm", + "guestCount": "number", + "totalAmount": "number", + "notes": "string (optional)", + "status": "pending | confirmed | cancelled | completed | rejected", + "createdAt": "ISO 8601", + "updatedAt": "ISO 8601" + } + ], + "total": 10, + "page": 1, + "limit": 10, + "totalPages": 1 +} +``` + +**POST `/bookings` Request:** +```json +{ + "venueId": "string (required)", + "eventDate": "YYYY-MM-DD (required, must be future date)", + "startTime": "HH:mm (required)", + "endTime": "HH:mm (required)", + "guestCount": "number (required, min 1)", + "notes": "string (optional)" +} +``` + +**POST `/bookings` Response:** +```json +{ + "data": { + "id": "string", + "venueId": "string", + "venueName": "string", + "userId": "string", + "eventDate": "YYYY-MM-DD", + "startTime": "HH:mm", + "endTime": "HH:mm", + "guestCount": "number", + "totalAmount": "number (calculated: hours x pricePerHour)", + "notes": "string", + "status": "pending", + "createdAt": "ISO 8601" + }, + "message": "Booking created successfully" +} +``` + +--- + +### Vendor Portal + +> All endpoints require `Authorization: Bearer ` with `role: vendor` and `X-Portal: vendor` header + +#### Profile + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/vendors/profile` | Get vendor profile | +| PUT | `/vendors/profile` | Update vendor profile | + +**GET `/vendors/profile` Response:** +```json +{ + "data": { + "id": "string", + "businessName": "string", + "email": "string", + "phone": "string", + "role": "vendor", + "isVerified": "boolean", + "createdAt": "ISO 8601", + "updatedAt": "ISO 8601" + } +} +``` + +**PUT `/vendors/profile` Request:** +```json +{ + "businessName": "string (required)", + "phone": "string (required)" +} +``` + +#### Venue Management + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/vendors/venues` | Get vendor's own venues | +| POST | `/vendors/venues` | Create a new venue | +| PUT | `/vendors/venues/:id` | Update a venue | +| DELETE | `/vendors/venues/:id` | Delete a venue | + +**POST `/vendors/venues` Request:** +```json +{ + "name": "string (required)", + "description": "string (required)", + "address": "string (required)", + "city": "string (required)", + "state": "string (required)", + "zipCode": "string (required)", + "capacity": "number (required, min 1)", + "pricePerHour": "number (required, min 0)", + "amenities": ["string (optional)"], + "images": ["string URL (optional)"] +} +``` + +**PUT `/vendors/venues/:id` Request:** +```json +{ + "name": "string", + "description": "string", + "address": "string", + "city": "string", + "state": "string", + "zipCode": "string", + "capacity": "number", + "pricePerHour": "number", + "amenities": ["string"], + "images": ["string URL"] +} +``` + +#### Booking Management + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/vendors/bookings` | Get bookings for vendor's venues | +| PUT | `/vendors/bookings/:id/approve` | Approve a pending booking | +| PUT | `/vendors/bookings/:id/reject` | Reject a pending booking | + +**GET `/vendors/bookings` Response:** +```json +{ + "data": [ + { + "id": "string", + "venueId": "string", + "venueName": "string", + "userId": "string", + "userName": "string", + "eventDate": "YYYY-MM-DD", + "startTime": "HH:mm", + "endTime": "HH:mm", + "guestCount": "number", + "totalAmount": "number", + "notes": "string", + "status": "pending | confirmed | cancelled | completed | rejected", + "createdAt": "ISO 8601" + } + ], + "total": 20, + "page": 1, + "limit": 10, + "totalPages": 2 +} +``` + +#### Analytics + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/vendors/analytics` | Get vendor analytics | + +**GET `/vendors/analytics` Query Parameters:** + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `period` | string | `month` | `week`, `month`, or `year` | + +**GET `/vendors/analytics` Response:** +```json +{ + "data": { + "totalVenues": "number", + "totalBookings": "number", + "totalRevenue": "number", + "pendingBookings": "number", + "revenueOverTime": [ + { "date": "string", "amount": "number" } + ], + "bookingTrends": [ + { "date": "string", "count": "number" } + ], + "topVenues": [ + { "venueId": "string", "name": "string", "bookings": "number", "revenue": "number" } + ], + "occupancyRate": "number (percentage)" + } +} +``` + +--- + +### Admin Portal + +> All endpoints require `Authorization: Bearer ` with `role: admin` and `X-Portal: admin` header + +#### User Management + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/admin/users` | List all users (paginated) | +| PUT | `/admin/users/:id/status` | Toggle user active/inactive | + +**GET `/admin/users` Query Parameters:** + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `page` | number | 1 | Page number | +| `limit` | number | 10 | Items per page | + +**GET `/admin/users` Response:** +```json +{ + "data": [ + { + "id": "string", + "firstName": "string", + "lastName": "string", + "email": "string", + "phone": "string", + "role": "user", + "isActive": "boolean", + "createdAt": "ISO 8601" + } + ], + "total": 50, + "page": 1, + "limit": 10, + "totalPages": 5 +} +``` + +**PUT `/admin/users/:id/status` Request:** +```json +{ + "isActive": "boolean" +} +``` + +#### Vendor Management + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/admin/vendors` | List all vendors (paginated) | +| PUT | `/admin/vendors/:id/status` | Toggle vendor status | + +**GET `/admin/vendors` Response:** +```json +{ + "data": [ + { + "id": "string", + "businessName": "string", + "email": "string", + "phone": "string", + "isVerified": "boolean", + "isActive": "boolean", + "totalVenues": "number", + "createdAt": "ISO 8601" + } + ], + "total": 20, + "page": 1, + "limit": 10, + "totalPages": 2 +} +``` + +**PUT `/admin/vendors/:id/status` Request:** +```json +{ + "isActive": "boolean", + "isVerified": "boolean" +} +``` + +#### Venue Management + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/admin/venues` | List all venues (paginated) | +| PUT | `/admin/venues/:id/status` | Approve/reject/deactivate venue | + +**GET `/admin/venues` Response:** +```json +{ + "data": [ + { + "id": "string", + "name": "string", + "vendorName": "string", + "city": "string", + "capacity": "number", + "pricePerHour": "number", + "status": "active | inactive | pending | rejected", + "createdAt": "ISO 8601" + } + ], + "total": 30, + "page": 1, + "limit": 10, + "totalPages": 3 +} +``` + +**PUT `/admin/venues/:id/status` Request:** +```json +{ + "status": "active | inactive | pending | rejected" +} +``` + +#### Booking Management + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/admin/bookings` | List all bookings (paginated) | + +**GET `/admin/bookings` Response:** +```json +{ + "data": [ + { + "id": "string", + "venueName": "string", + "userName": "string", + "vendorName": "string", + "eventDate": "YYYY-MM-DD", + "startTime": "HH:mm", + "endTime": "HH:mm", + "guestCount": "number", + "totalAmount": "number", + "status": "pending | confirmed | cancelled | completed | rejected", + "createdAt": "ISO 8601" + } + ], + "total": 100, + "page": 1, + "limit": 10, + "totalPages": 10 +} +``` + +#### Analytics + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/admin/analytics` | Platform-wide analytics | + +**GET `/admin/analytics` Query Parameters:** + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `period` | string | `month` | `week`, `month`, or `year` | + +**GET `/admin/analytics` Response:** +```json +{ + "data": { + "totalUsers": "number", + "totalVendors": "number", + "totalVenues": "number", + "totalBookings": "number", + "totalRevenue": "number", + "revenueOverTime": [ + { "date": "string", "amount": "number" } + ], + "userGrowth": [ + { "date": "string", "count": "number" } + ], + "bookingTrends": [ + { "date": "string", "count": "number" } + ], + "topVenues": [ + { "venueId": "string", "name": "string", "bookings": "number", "revenue": "number" } + ] + } +} +``` + +#### Platform Settings + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/admin/settings` | Get platform settings | +| PUT | `/admin/settings` | Update platform settings | + +**GET `/admin/settings` Response:** +```json +{ + "data": { + "siteName": "string", + "supportEmail": "string", + "commissionRate": "number (percentage, e.g. 10 for 10%)" + } +} +``` + +**PUT `/admin/settings` Request:** +```json +{ + "siteName": "string (required)", + "supportEmail": "string (required, valid email)", + "commissionRate": "number (required, 0-100)" +} +``` + +--- + +## CORS Configuration + +The backend must allow the following origins: + +- **Dev:** `http://localhost:4200` (Angular dev server) +- **Prod:** Your production frontend domain + +Required CORS headers: +``` +Access-Control-Allow-Origin: +Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS +Access-Control-Allow-Headers: Content-Type, Authorization, X-Portal +Access-Control-Allow-Credentials: true +``` + +--- + +## Database Schema Suggestions + +### Users Table +| Column | Type | Notes | +|--------|------|-------| +| id | UUID / String | Primary key | +| firstName | String | Required | +| lastName | String | Required | +| email | String | Unique, required | +| phone | String | Required | +| password | String | Hashed, required | +| role | Enum | `user`, `vendor`, `admin` | +| isActive | Boolean | Default: true | +| createdAt | DateTime | Auto-generated | +| updatedAt | DateTime | Auto-updated | + +### Vendors Table (extends or separate from Users) +| Column | Type | Notes | +|--------|------|-------| +| id | UUID / String | Primary key | +| businessName | String | Required | +| email | String | Unique, required | +| phone | String | Required | +| password | String | Hashed, required | +| isVerified | Boolean | Default: false | +| isActive | Boolean | Default: true | +| createdAt | DateTime | Auto-generated | +| updatedAt | DateTime | Auto-updated | + +### Venues Table +| Column | Type | Notes | +|--------|------|-------| +| id | UUID / String | Primary key | +| vendorId | UUID / String | Foreign key to Vendors | +| name | String | Required | +| description | Text | Required | +| address | String | Required | +| city | String | Required | +| state | String | Required | +| zipCode | String | Required | +| capacity | Integer | Required, min 1 | +| pricePerHour | Decimal | Required, min 0 | +| amenities | JSON / Array | Optional | +| images | JSON / Array | URLs | +| status | Enum | `active`, `inactive`, `pending`, `rejected` | +| createdAt | DateTime | Auto-generated | +| updatedAt | DateTime | Auto-updated | + +### Bookings Table +| Column | Type | Notes | +|--------|------|-------| +| id | UUID / String | Primary key | +| venueId | UUID / String | Foreign key to Venues | +| userId | UUID / String | Foreign key to Users | +| eventDate | Date | Required, future date | +| startTime | String | HH:mm format | +| endTime | String | HH:mm format | +| guestCount | Integer | Required, min 1 | +| totalAmount | Decimal | Calculated: hours x pricePerHour | +| notes | Text | Optional | +| status | Enum | `pending`, `confirmed`, `cancelled`, `completed`, `rejected` | +| createdAt | DateTime | Auto-generated | +| updatedAt | DateTime | Auto-updated | + +### Settings Table +| Column | Type | Notes | +|--------|------|-------| +| id | UUID / String | Primary key | +| siteName | String | Default: "Book My Venue" | +| supportEmail | String | Default: "support@bookmyvenue.com" | +| commissionRate | Decimal | Default: 10 (percentage) | + +--- + +## Seed Data + +Create at least one admin user on first run: + +```json +{ + "email": "admin@bookmyvenue.com", + "password": "Admin@123", + "role": "admin", + "firstName": "Super", + "lastName": "Admin" +} +``` + +--- + +## Notes + +- All dates should be in ISO 8601 format (`2025-01-15T10:30:00.000Z`) +- Event dates are `YYYY-MM-DD` format +- Times are `HH:mm` 24-hour format +- Passwords must be hashed (bcrypt recommended) +- Token expiry: access token ~15 min, refresh token ~7 days +- Pagination defaults: `page=1`, `limit=10` +- The frontend handles all validation on its end, but the backend should also validate +- Image handling: the frontend sends image URLs — implement file upload separately and return URLs diff --git a/client/book-my-venue/.editorconfig b/client/book-my-venue/.editorconfig new file mode 100644 index 000000000..f166060da --- /dev/null +++ b/client/book-my-venue/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/client/book-my-venue/.gitignore b/client/book-my-venue/.gitignore new file mode 100644 index 000000000..854acd5fc --- /dev/null +++ b/client/book-my-venue/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/client/book-my-venue/.postcssrc.json b/client/book-my-venue/.postcssrc.json new file mode 100644 index 000000000..e092dc7c1 --- /dev/null +++ b/client/book-my-venue/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/client/book-my-venue/.prettierrc b/client/book-my-venue/.prettierrc new file mode 100644 index 000000000..d6c16d7ee --- /dev/null +++ b/client/book-my-venue/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] +} diff --git a/client/book-my-venue/.vscode/extensions.json b/client/book-my-venue/.vscode/extensions.json new file mode 100644 index 000000000..77b374577 --- /dev/null +++ b/client/book-my-venue/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/client/book-my-venue/.vscode/launch.json b/client/book-my-venue/.vscode/launch.json new file mode 100644 index 000000000..925af8370 --- /dev/null +++ b/client/book-my-venue/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/client/book-my-venue/.vscode/mcp.json b/client/book-my-venue/.vscode/mcp.json new file mode 100644 index 000000000..956af8c62 --- /dev/null +++ b/client/book-my-venue/.vscode/mcp.json @@ -0,0 +1,9 @@ +{ + // For more information, visit: https://angular.dev/ai/mcp + "servers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} diff --git a/client/book-my-venue/.vscode/tasks.json b/client/book-my-venue/.vscode/tasks.json new file mode 100644 index 000000000..244306f98 --- /dev/null +++ b/client/book-my-venue/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/client/book-my-venue/README.md b/client/book-my-venue/README.md new file mode 100644 index 000000000..4fb03fa8a --- /dev/null +++ b/client/book-my-venue/README.md @@ -0,0 +1,59 @@ +# BookMyVenue + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.12. + +## Development server + +To start a local development server, run: + +```bash +ng serve +``` + +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. + +## Code scaffolding + +Angular CLI includes powerful code scaffolding tools. To generate a new component, run: + +```bash +ng generate component component-name +``` + +For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: + +```bash +ng generate --help +``` + +## Building + +To build the project run: + +```bash +ng build +``` + +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. + +## Running unit tests + +To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: + +```bash +ng test +``` + +## Running end-to-end tests + +For end-to-end (e2e) testing, run: + +```bash +ng e2e +``` + +Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. + +## Additional Resources + +For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/client/book-my-venue/angular.json b/client/book-my-venue/angular.json new file mode 100644 index 000000000..54e384264 --- /dev/null +++ b/client/book-my-venue/angular.json @@ -0,0 +1,73 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm" + }, + "newProjectRoot": "projects", + "projects": { + "book-my-venue": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "book-my-venue:build:production" + }, + "development": { + "buildTarget": "book-my-venue:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/client/book-my-venue/eslint.config.js b/client/book-my-venue/eslint.config.js new file mode 100644 index 000000000..097821b3b --- /dev/null +++ b/client/book-my-venue/eslint.config.js @@ -0,0 +1,16 @@ +const angular = require("@angular-eslint/schematics"); + +module.exports = [ + { + files: ["**/*.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/explicit-function-return-type": "warn", + "no-console": ["warn", { allow: ["warn", "error"] }], + }, + }, + { + files: ["**/*.html"], + rules: {}, + }, +]; diff --git a/client/book-my-venue/package-lock.json b/client/book-my-venue/package-lock.json new file mode 100644 index 000000000..e8cecbbf9 --- /dev/null +++ b/client/book-my-venue/package-lock.json @@ -0,0 +1,10197 @@ +{ + "name": "book-my-venue", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "book-my-venue", + "version": "0.0.0", + "dependencies": { + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular-eslint/schematics": "^21.4.0", + "@angular/build": "^21.2.12", + "@angular/cli": "^21.2.12", + "@angular/compiler-cli": "^21.2.0", + "@tailwindcss/postcss": "^4.3.0", + "eslint": "^10.4.0", + "jsdom": "^28.0.0", + "postcss": "^8.5.15", + "prettier": "^3.8.1", + "tailwindcss": "^4.3.0", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/abtesting": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", + "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", + "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", + "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", + "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", + "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", + "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", + "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", + "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", + "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", + "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", + "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", + "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", + "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", + "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2102.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.12.tgz", + "integrity": "sha512-w9FSMHYeeHkk0kRSAOCvNqEVyOHqpC1SUf3iN7tDnXBOA0dtc6JYvJU7O4joiwf7wMPZDK8LKc/6eu8/Tx87Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.12", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "21.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.12.tgz", + "integrity": "sha512-nXms0jVWwHOJK+z6vHvhw7HYFBelxh2gEnkij0OQMABXZN5hoUlTD0DDP1lYR7hQNi8Yb2Ar0UN9ihyUFVM5Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "21.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.12.tgz", + "integrity": "sha512-29xe6C9nwHejV9zBcu0js7NmzLWuCFzBGBTmL6eD4JN1NcxEZ/nO1JuaGINjPjzb/UDXPZIqEwHbnFNcGS5v1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.12", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.3.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.4.0.tgz", + "integrity": "sha512-/3H4BPbQ1BHJkkrUsfusZtmHc+qiFWBBZ9UDPWah4xZMjflexOK9U4GYeH7nMjcuyqFnIlMMeJJNwNLGt/hmdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.4.0.tgz", + "integrity": "sha512-mow2DMj+xBvGl5t7jzC34R8YfbHbaGNyCNFzpovtl9qc0JbuqLyg6htmt8xb05f8ZjATOr4nz0ESt6HV4c51hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.4.0", + "@angular-eslint/utils": "21.4.0", + "ts-api-utils": "^2.1.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.4.0.tgz", + "integrity": "sha512-sJEHx2WYnvOgPpzP1eHnUdRS06zgKmRxbiIR0JiCcaSen5iv1HlsMieXy//FS9TtNW+abHOy4UtDuGuSPflPFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.4.0", + "@angular-eslint/utils": "21.4.0", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" + }, + "peerDependencies": { + "@angular-eslint/template-parser": "21.4.0", + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.4.0.tgz", + "integrity": "sha512-crD6Hfxs7x5bN9FCqTZI7uVSiGvprfCS3MCPOpyIQl87bRr/9aNhnicJ3ROUHv+2A713BgPHIgiCII/bxzrfPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 21.0.0 < 22.0.0", + "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", + "@angular-eslint/eslint-plugin": "21.4.0", + "@angular-eslint/eslint-plugin-template": "21.4.0", + "ignore": "7.0.5", + "semver": "7.7.4", + "strip-json-comments": "3.1.1" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.4.0.tgz", + "integrity": "sha512-BaUSLSyS+43fzDoJkTMkGqNdCXq3fGnUZsfXTmrlZPJf5AYFbgAlAPGZXDJyoNWw43fux+DafdlrlKcYUSgSIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.4.0", + "eslint-scope": "9.1.2" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.4.0.tgz", + "integrity": "sha512-7pi+Ga7QmdH5Ig/diau6fR5L4yubgKr9TOjdCg7OeuE/zo0O3osTCNT6JOodzS/iQM1kSCJFDoIBKFeUOttiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.4.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/build": { + "version": "21.2.12", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.12.tgz", + "integrity": "sha512-zYfo21RuldDWXnshuPfWYtmh5ltlO9+XFHpNObdIInQTFxKD6grLNVNOblFFpi+oIIm4Km+CGSXvBHs/aH0ufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2102.12", + "@babel/core": "7.29.0", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.4", + "beasties": "0.4.1", + "browserslist": "^4.26.0", + "esbuild": "0.27.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.4", + "piscina": "5.1.4", + "rolldown": "1.0.0-rc.4", + "sass": "1.97.3", + "semver": "7.7.4", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.15", + "undici": "7.24.4", + "vite": "7.3.2", + "watchpack": "2.5.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.1" + }, + "peerDependencies": { + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.2.12", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^21.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "21.2.12", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.12.tgz", + "integrity": "sha512-oLEL1C1fI39b1eQo5f2cyQhQfE+QMv7dm8z2MmxbP7YR7jAdQPVfGU8CXECR5g7mrYi9WgvIRKB+9Oeq2aH6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2102.12", + "@angular-devkit/core": "21.2.12", + "@angular-devkit/schematics": "21.2.12", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.2.12", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.3.1", + "parse5-html-rewriting-stream": "8.0.0", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.3.6" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.14.tgz", + "integrity": "sha512-J6K7cE7uKOKmg4+sxLeGfsmaYDjP5l1XCiMMI0WPT0t68uxLk8g3MzV5Trqfb6ZnRxWcfp9c4c+XxAvMBB7ymA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.14.tgz", + "integrity": "sha512-8mqgwRYfn2Z1vg/5YVt60dDBattnZL45nNJd2vTMwAiDTzhWhgKgRWKOeVL0aj2JqHeHiwuIlrLnz46acJMulQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.14.tgz", + "integrity": "sha512-h+WQfPKFxaDfDhMqUUdOQ1TsDMccav8kLFERmKTRfD4MNOczSMpOMyeXJHCL0Rq4I8WDQvaBJGMG7DXRDefSog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.29.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.14", + "typescript": ">=5.9 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/core": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.14.tgz", + "integrity": "sha512-Z1Ivjh7L2lT//8LA7vQ3tj7Rg6wl2XRA5kPSAukgn8u0Yu0XxG8NE8KG0Eypb3v9CEcbwATwpgnxzbJFZ8TFcw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.14", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.14.tgz", + "integrity": "sha512-HQYIybyMt0CrI31rW6vXbiDsSM2DDtTcOVeT/nWDRNCoqBrREDg8rVsm2Y+fUMsiQVJNa6dCXPwvYhjzJ4r7ug==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.14", + "@angular/core": "21.2.14", + "@angular/platform-browser": "21.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.14.tgz", + "integrity": "sha512-34tBwxh86yN2YifBDhCesm6N+nn9WcbuXjRwfo0mTme15OZ/zt56yw7v1mcK3UFLegIIALtsIgpXXrPWWQoKkA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "21.2.14", + "@angular/common": "21.2.14", + "@angular/core": "21.2.14" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.14.tgz", + "integrity": "sha512-Yo3LdgcqkfMu2/Ycl8o/4QjCBqZhtA+a7B8JVdW5cWdrpFTxKCOrzm+YRUMuIFmH5nzSv9oGnUuz64uk1+7r5Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.14", + "@angular/core": "21.2.14", + "@angular/platform-browser": "21.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", + "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", + "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", + "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", + "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", + "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", + "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", + "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", + "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", + "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", + "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "21.2.12", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.12.tgz", + "integrity": "sha512-eHoAbxd6Kdw9YIQeZO/6lBXTmKKi10t4WTujY8CM5v4qv1zoJu9yiwVeQp9y3e7/Sybz5Ec3m4FmQ0Tw8iVDiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.12", + "@angular-devkit/schematics": "21.2.12", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz", + "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "postcss": "^8.5.10", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", + "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.0", + "@typescript-eslint/types": "^8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", + "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", + "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz", + "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", + "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/project-service": "8.60.0", + "@typescript-eslint/tsconfig-utils": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz", + "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", + "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "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": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", + "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", + "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", + "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", + "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.113.0", + "@rolldown/pluginutils": "1.0.0-rc.4" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-x64": "1.0.0-rc.4", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", + "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.0.tgz", + "integrity": "sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.0" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.0.tgz", + "integrity": "sha512-/mb9kRld+x1sIMXxWNOAp5m6C+D4GrAORWlJkOJ5dElvxdN1eutz/o7qHLp9gFvDF4Y3/L2xeScoxz6AbEo8rQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/client/book-my-venue/package.json b/client/book-my-venue/package.json new file mode 100644 index 000000000..ecf3608f2 --- /dev/null +++ b/client/book-my-venue/package.json @@ -0,0 +1,37 @@ +{ + "name": "book-my-venue", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "packageManager": "npm@10.9.2", + "dependencies": { + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular-eslint/schematics": "^21.4.0", + "@angular/build": "^21.2.12", + "@angular/cli": "^21.2.12", + "@angular/compiler-cli": "^21.2.0", + "@tailwindcss/postcss": "^4.3.0", + "eslint": "^10.4.0", + "jsdom": "^28.0.0", + "postcss": "^8.5.15", + "prettier": "^3.8.1", + "tailwindcss": "^4.3.0", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } +} diff --git a/client/book-my-venue/public/favicon.ico b/client/book-my-venue/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/client/book-my-venue/src/app/admin/features/analytics/analytics.component.css b/client/book-my-venue/src/app/admin/features/analytics/analytics.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/analytics/analytics.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/analytics/analytics.component.html b/client/book-my-venue/src/app/admin/features/analytics/analytics.component.html new file mode 100644 index 000000000..94ec01b71 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/analytics/analytics.component.html @@ -0,0 +1,30 @@ +
+
+

Platform Analytics

+
+ @for (p of ['week', 'month', 'year']; track p) { + + } +
+
+
+
+

Revenue Overview

+
Chart placeholder
+
+
+

User Growth

+
Chart placeholder
+
+
+

Booking Trends

+
Chart placeholder
+
+
+

Top Venues

+
Chart placeholder
+
+
+
diff --git a/client/book-my-venue/src/app/admin/features/analytics/analytics.component.spec.ts b/client/book-my-venue/src/app/admin/features/analytics/analytics.component.spec.ts new file mode 100644 index 000000000..1b74ea85d --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/analytics/analytics.component.spec.ts @@ -0,0 +1,7 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AnalyticsComponent } from './analytics.component'; +describe('Admin AnalyticsComponent', () => { + let fixture: ComponentFixture; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AnalyticsComponent] }).compileComponents(); fixture = TestBed.createComponent(AnalyticsComponent); fixture.detectChanges(); }); + it('should create', () => { expect(fixture.componentInstance).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/analytics/analytics.component.ts b/client/book-my-venue/src/app/admin/features/analytics/analytics.component.ts new file mode 100644 index 000000000..e6cab1d44 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/analytics/analytics.component.ts @@ -0,0 +1,13 @@ +import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { TitleCasePipe } from '@angular/common'; + +@Component({ + selector: 'app-admin-analytics', standalone: true, + imports: [TitleCasePipe], + templateUrl: './analytics.component.html', styleUrl: './analytics.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AnalyticsComponent { + readonly period = signal<'week' | 'month' | 'year'>('month'); + setPeriod(period: 'week' | 'month' | 'year'): void { this.period.set(period); } +} diff --git a/client/book-my-venue/src/app/admin/features/bookings/bookings.component.css b/client/book-my-venue/src/app/admin/features/bookings/bookings.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/bookings/bookings.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/bookings/bookings.component.html b/client/book-my-venue/src/app/admin/features/bookings/bookings.component.html new file mode 100644 index 000000000..bc0fb3deb --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/bookings/bookings.component.html @@ -0,0 +1,27 @@ +
+

Bookings Management

+ @if (loading()) { } @else if (bookings().length === 0) { + + } @else { +
+ + + + + + @for (booking of bookings(); track booking.id) { + + + + + + + + + } + +
VenueDateTimeGuestsAmountStatus
{{ booking.venueName }}{{ booking.eventDate | dateFormat }}{{ booking.startTime }} - {{ booking.endTime }}{{ booking.guestCount }}{{ booking.totalAmount | currencyFormat }}{{ booking.status }}
+
+ + } +
diff --git a/client/book-my-venue/src/app/admin/features/bookings/bookings.component.spec.ts b/client/book-my-venue/src/app/admin/features/bookings/bookings.component.spec.ts new file mode 100644 index 000000000..a6c6a12f7 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/bookings/bookings.component.spec.ts @@ -0,0 +1,7 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BookingsComponent } from './bookings.component'; +describe('Admin BookingsComponent', () => { + let fixture: ComponentFixture; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [BookingsComponent] }).compileComponents(); fixture = TestBed.createComponent(BookingsComponent); fixture.detectChanges(); }); + it('should create', () => { expect(fixture.componentInstance).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/bookings/bookings.component.ts b/client/book-my-venue/src/app/admin/features/bookings/bookings.component.ts new file mode 100644 index 000000000..bb26b75bf --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/bookings/bookings.component.ts @@ -0,0 +1,21 @@ +import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; +import { PaginationComponent } from '../../../shared/components/pagination/pagination.component'; +import { DateFormatPipe } from '../../../shared/pipes/date-format.pipe'; +import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; +import { Booking } from '../../../shared/models/booking.model'; + +@Component({ + selector: 'app-admin-bookings', standalone: true, + imports: [LoaderComponent, EmptyStateComponent, PaginationComponent, DateFormatPipe, CurrencyFormatPipe], + templateUrl: './bookings.component.html', styleUrl: './bookings.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BookingsComponent { + readonly bookings = signal([]); + readonly loading = signal(false); + readonly currentPage = signal(1); + readonly totalPages = signal(1); + onPageChange(page: number): void { this.currentPage.set(page); } +} diff --git a/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.css b/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.html b/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.html new file mode 100644 index 000000000..00c9df577 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.html @@ -0,0 +1,27 @@ +
+

Admin Dashboard

+
+ @for (stat of [ + { label: 'Users', value: stats().totalUsers, color: 'indigo' }, + { label: 'Vendors', value: stats().totalVendors, color: 'green' }, + { label: 'Venues', value: stats().totalVenues, color: 'blue' }, + { label: 'Bookings', value: stats().totalBookings, color: 'amber' }, + { label: 'Revenue', value: stats().totalRevenue, color: 'red' } + ]; track stat.label) { +
+

{{ stat.label }}

+

{{ stat.value }}

+
+ } +
+
+
+

Recent Activity

+

Activity feed placeholder

+
+
+

System Health

+

System metrics placeholder

+
+
+
diff --git a/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.spec.ts b/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.spec.ts new file mode 100644 index 000000000..a7fb79a79 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.spec.ts @@ -0,0 +1,11 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { DashboardComponent } from './dashboard.component'; +describe('Admin DashboardComponent', () => { + let component: DashboardComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [DashboardComponent], providers: [provideRouter([])] }).compileComponents(); + fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.ts b/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.ts new file mode 100644 index 000000000..5a4891bf5 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.ts @@ -0,0 +1,10 @@ +import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-admin-dashboard', standalone: true, + templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardComponent { + readonly stats = signal({ totalUsers: 0, totalVendors: 0, totalVenues: 0, totalBookings: 0, totalRevenue: 0 }); +} diff --git a/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.css b/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.html b/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.html new file mode 100644 index 000000000..31d8f5185 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.html @@ -0,0 +1,18 @@ +
+
+
+

Forgot Password

+

Reset your admin password

+
+
+
+ +
+ +

+ Back to login +

+
+
+
diff --git a/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.spec.ts b/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.spec.ts new file mode 100644 index 000000000..7be314c0b --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.spec.ts @@ -0,0 +1,12 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { ForgotPasswordComponent } from './forgot-password.component'; +describe('Admin ForgotPasswordComponent', () => { + let component: ForgotPasswordComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [ForgotPasswordComponent], providers: [provideRouter([]), provideHttpClient()] }).compileComponents(); + fixture = TestBed.createComponent(ForgotPasswordComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.ts b/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.ts new file mode 100644 index 000000000..995e82989 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.ts @@ -0,0 +1,24 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-admin-forgot-password', standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './forgot-password.component.html', styleUrl: './forgot-password.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ForgotPasswordComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + readonly loading = this.authService.loading; + readonly form = this.fb.nonNullable.group({ email: ['', [Validators.required, Validators.email]] }); + + onSubmit(): void { + if (this.form.valid) { this.authService.forgotPassword(this.form.getRawValue()); } + else { this.form.markAllAsTouched(); } + } +} diff --git a/client/book-my-venue/src/app/admin/features/login/login.component.css b/client/book-my-venue/src/app/admin/features/login/login.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/login/login.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/login/login.component.html b/client/book-my-venue/src/app/admin/features/login/login.component.html new file mode 100644 index 000000000..6c37dd312 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/login/login.component.html @@ -0,0 +1,22 @@ +
+
+
+

Admin Login

+

Access the admin control panel

+
+
+
+
+ + +
+ + + +
+
+
diff --git a/client/book-my-venue/src/app/admin/features/login/login.component.spec.ts b/client/book-my-venue/src/app/admin/features/login/login.component.spec.ts new file mode 100644 index 000000000..97e88177e --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/login/login.component.spec.ts @@ -0,0 +1,12 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { LoginComponent } from './login.component'; +describe('Admin LoginComponent', () => { + let component: LoginComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [LoginComponent], providers: [provideRouter([]), provideHttpClient()] }).compileComponents(); + fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/login/login.component.ts b/client/book-my-venue/src/app/admin/features/login/login.component.ts new file mode 100644 index 000000000..26693da56 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/login/login.component.ts @@ -0,0 +1,30 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-admin-login', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './login.component.html', + styleUrl: './login.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoginComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + readonly loading = this.authService.loading; + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]], + }); + + onSubmit(): void { + if (this.form.valid) { this.authService.adminLogin(this.form.getRawValue()); } + else { this.form.markAllAsTouched(); } + } +} diff --git a/client/book-my-venue/src/app/admin/features/settings/settings.component.css b/client/book-my-venue/src/app/admin/features/settings/settings.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/settings/settings.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/settings/settings.component.html b/client/book-my-venue/src/app/admin/features/settings/settings.component.html new file mode 100644 index 000000000..1912744bb --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/settings/settings.component.html @@ -0,0 +1,15 @@ +
+

Platform Settings

+
+
+
+ + + +
+
+ +
+
+
+
diff --git a/client/book-my-venue/src/app/admin/features/settings/settings.component.spec.ts b/client/book-my-venue/src/app/admin/features/settings/settings.component.spec.ts new file mode 100644 index 000000000..bfc8f97c6 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/settings/settings.component.spec.ts @@ -0,0 +1,7 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SettingsComponent } from './settings.component'; +describe('Admin SettingsComponent', () => { + let fixture: ComponentFixture; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SettingsComponent] }).compileComponents(); fixture = TestBed.createComponent(SettingsComponent); fixture.detectChanges(); }); + it('should create', () => { expect(fixture.componentInstance).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/settings/settings.component.ts b/client/book-my-venue/src/app/admin/features/settings/settings.component.ts new file mode 100644 index 000000000..a78519dd0 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/settings/settings.component.ts @@ -0,0 +1,28 @@ +import { Component, inject, signal, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-admin-settings', standalone: true, + imports: [ReactiveFormsModule, ButtonComponent, InputComponent], + templateUrl: './settings.component.html', styleUrl: './settings.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SettingsComponent { + private readonly fb = inject(FormBuilder); + readonly saving = signal(false); + + readonly form = this.fb.nonNullable.group({ + siteName: ['BookMyVenue', [Validators.required]], + supportEmail: ['support@bookmyvenue.com', [Validators.required, Validators.email]], + commissionRate: [10, [Validators.required, Validators.min(0), Validators.max(100)]], + }); + + onSubmit(): void { + if (this.form.valid) { + this.saving.set(true); + setTimeout(() => this.saving.set(false), 1000); + } + } +} diff --git a/client/book-my-venue/src/app/admin/features/users/users.component.css b/client/book-my-venue/src/app/admin/features/users/users.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/users/users.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/users/users.component.html b/client/book-my-venue/src/app/admin/features/users/users.component.html new file mode 100644 index 000000000..276025496 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/users/users.component.html @@ -0,0 +1,26 @@ +
+

Users Management

+ @if (loading()) { } @else if (users().length === 0) { + + } @else { +
+ + + + + + @for (user of users(); track user.id) { + + + + + + + + } + +
NameEmailPhoneStatusActions
{{ user.firstName }} {{ user.lastName }}{{ user.email }}{{ user.phone }}{{ user.isActive ? 'Active' : 'Inactive' }}
+
+ + } +
diff --git a/client/book-my-venue/src/app/admin/features/users/users.component.spec.ts b/client/book-my-venue/src/app/admin/features/users/users.component.spec.ts new file mode 100644 index 000000000..9e204b559 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/users/users.component.spec.ts @@ -0,0 +1,10 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UsersComponent } from './users.component'; +describe('Admin UsersComponent', () => { + let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [UsersComponent] }).compileComponents(); + fixture = TestBed.createComponent(UsersComponent); fixture.detectChanges(); + }); + it('should create', () => { expect(fixture.componentInstance).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/users/users.component.ts b/client/book-my-venue/src/app/admin/features/users/users.component.ts new file mode 100644 index 000000000..e0a30caaf --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/users/users.component.ts @@ -0,0 +1,20 @@ +import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; +import { PaginationComponent } from '../../../shared/components/pagination/pagination.component'; +import { User } from '../../../shared/models/user.model'; + +@Component({ + selector: 'app-admin-users', standalone: true, + imports: [LoaderComponent, EmptyStateComponent, PaginationComponent], + templateUrl: './users.component.html', styleUrl: './users.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UsersComponent { + readonly users = signal([]); + readonly loading = signal(false); + readonly currentPage = signal(1); + readonly totalPages = signal(1); + + onPageChange(page: number): void { this.currentPage.set(page); } +} diff --git a/client/book-my-venue/src/app/admin/features/vendors/vendors.component.css b/client/book-my-venue/src/app/admin/features/vendors/vendors.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/vendors/vendors.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/vendors/vendors.component.html b/client/book-my-venue/src/app/admin/features/vendors/vendors.component.html new file mode 100644 index 000000000..2748dbf87 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/vendors/vendors.component.html @@ -0,0 +1,27 @@ +
+

Vendors Management

+ @if (loading()) { } @else if (vendors().length === 0) { + + } @else { +
+ + + + + + @for (vendor of vendors(); track vendor.id) { + + + + + + + + + } + +
BusinessContactEmailVerifiedStatusActions
{{ vendor.businessName }}{{ vendor.contactPerson }}{{ vendor.email }}{{ vendor.isVerified ? 'Verified' : 'Pending' }}{{ vendor.isActive ? 'Active' : 'Inactive' }}
+
+ + } +
diff --git a/client/book-my-venue/src/app/admin/features/vendors/vendors.component.spec.ts b/client/book-my-venue/src/app/admin/features/vendors/vendors.component.spec.ts new file mode 100644 index 000000000..eec4337f5 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/vendors/vendors.component.spec.ts @@ -0,0 +1,7 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { VendorsComponent } from './vendors.component'; +describe('Admin VendorsComponent', () => { + let fixture: ComponentFixture; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [VendorsComponent] }).compileComponents(); fixture = TestBed.createComponent(VendorsComponent); fixture.detectChanges(); }); + it('should create', () => { expect(fixture.componentInstance).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/vendors/vendors.component.ts b/client/book-my-venue/src/app/admin/features/vendors/vendors.component.ts new file mode 100644 index 000000000..2043fac63 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/vendors/vendors.component.ts @@ -0,0 +1,19 @@ +import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; +import { PaginationComponent } from '../../../shared/components/pagination/pagination.component'; +import { Vendor } from '../../../shared/models/vendor.model'; + +@Component({ + selector: 'app-admin-vendors', standalone: true, + imports: [LoaderComponent, EmptyStateComponent, PaginationComponent], + templateUrl: './vendors.component.html', styleUrl: './vendors.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VendorsComponent { + readonly vendors = signal([]); + readonly loading = signal(false); + readonly currentPage = signal(1); + readonly totalPages = signal(1); + onPageChange(page: number): void { this.currentPage.set(page); } +} diff --git a/client/book-my-venue/src/app/admin/features/venues/venues.component.css b/client/book-my-venue/src/app/admin/features/venues/venues.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/venues/venues.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/admin/features/venues/venues.component.html b/client/book-my-venue/src/app/admin/features/venues/venues.component.html new file mode 100644 index 000000000..a57af32ce --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/venues/venues.component.html @@ -0,0 +1,26 @@ +
+

Venues Management

+ @if (loading()) { } @else if (venues().length === 0) { + + } @else { +
+ + + + + + @for (venue of venues(); track venue.id) { + + + + + + + + + } + +
NameCityCapacityPrice/HrStatusActions
{{ venue.name }}{{ venue.city }}{{ venue.capacity }}{{ venue.pricePerHour }}{{ venue.status }}
+
+ } +
diff --git a/client/book-my-venue/src/app/admin/features/venues/venues.component.spec.ts b/client/book-my-venue/src/app/admin/features/venues/venues.component.spec.ts new file mode 100644 index 000000000..030e205b4 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/venues/venues.component.spec.ts @@ -0,0 +1,7 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { VenuesComponent } from './venues.component'; +describe('Admin VenuesComponent', () => { + let fixture: ComponentFixture; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [VenuesComponent] }).compileComponents(); fixture = TestBed.createComponent(VenuesComponent); fixture.detectChanges(); }); + it('should create', () => { expect(fixture.componentInstance).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/features/venues/venues.component.ts b/client/book-my-venue/src/app/admin/features/venues/venues.component.ts new file mode 100644 index 000000000..08fec8086 --- /dev/null +++ b/client/book-my-venue/src/app/admin/features/venues/venues.component.ts @@ -0,0 +1,13 @@ +import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; +import { Venue } from '../../../shared/models/venue.model'; + +@Component({ + selector: 'app-admin-venues', standalone: true, imports: [LoaderComponent, EmptyStateComponent], + templateUrl: './venues.component.html', styleUrl: './venues.component.css', changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VenuesComponent { + readonly venues = signal([]); + readonly loading = signal(false); +} diff --git a/client/book-my-venue/src/app/admin/guards/admin-auth.guard.ts b/client/book-my-venue/src/app/admin/guards/admin-auth.guard.ts new file mode 100644 index 000000000..46df52d43 --- /dev/null +++ b/client/book-my-venue/src/app/admin/guards/admin-auth.guard.ts @@ -0,0 +1,15 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../../shared/services/auth.service'; +import { UserRole } from '../../shared/enums/user-role.enum'; + +export const adminAuthGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isPortalAuthenticated(UserRole.Admin)) { + return true; + } + router.navigate(['/admin/login']); + return false; +}; diff --git a/client/book-my-venue/src/app/admin/interceptors/admin-auth.interceptor.ts b/client/book-my-venue/src/app/admin/interceptors/admin-auth.interceptor.ts new file mode 100644 index 000000000..513ac9ee9 --- /dev/null +++ b/client/book-my-venue/src/app/admin/interceptors/admin-auth.interceptor.ts @@ -0,0 +1,16 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { AuthService } from '../../shared/services/auth.service'; + +export const adminAuthInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(AuthService); + const token = authService.getToken(); + + if (token) { + const cloned = req.clone({ + setHeaders: { Authorization: `Bearer ${token}`, 'X-Portal': 'admin' }, + }); + return next(cloned); + } + return next(req); +}; diff --git a/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.css b/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.css new file mode 100644 index 000000000..065254a3f --- /dev/null +++ b/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.css @@ -0,0 +1 @@ +:host { display: block; height: 100vh; } diff --git a/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.html b/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.html new file mode 100644 index 000000000..2ac7fe338 --- /dev/null +++ b/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.html @@ -0,0 +1,54 @@ +
+ +
+
+

Admin Panel

+
+ {{ currentUser()?.name }} +
+ {{ currentUser()?.name?.charAt(0) || 'A' }} +
+
+
+
+
+

© 2026 BookMyVenue Admin

+
+
+
diff --git a/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts b/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts new file mode 100644 index 000000000..5ecf25b2c --- /dev/null +++ b/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts @@ -0,0 +1,12 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { AdminLayoutComponent } from './admin-layout.component'; +describe('AdminLayoutComponent', () => { + let component: AdminLayoutComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [AdminLayoutComponent], providers: [provideRouter([]), provideHttpClient()] }).compileComponents(); + fixture = TestBed.createComponent(AdminLayoutComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.ts b/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.ts new file mode 100644 index 000000000..5b9e0efb6 --- /dev/null +++ b/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.ts @@ -0,0 +1,30 @@ +import { Component, inject, signal, ChangeDetectionStrategy } from '@angular/core'; +import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; + +@Component({ + selector: 'app-admin-layout', + standalone: true, + imports: [RouterOutlet, RouterLink, RouterLinkActive], + templateUrl: './admin-layout.component.html', + styleUrl: './admin-layout.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdminLayoutComponent { + private readonly authService = inject(AuthService); + readonly currentUser = this.authService.currentUser; + readonly sidebarOpen = signal(true); + + readonly navItems = [ + { label: 'Dashboard', path: '/admin/dashboard', icon: 'dashboard' }, + { label: 'Users', path: '/admin/users', icon: 'users' }, + { label: 'Vendors', path: '/admin/vendors', icon: 'vendors' }, + { label: 'Venues', path: '/admin/venues', icon: 'venues' }, + { label: 'Bookings', path: '/admin/bookings', icon: 'bookings' }, + { label: 'Analytics', path: '/admin/analytics', icon: 'analytics' }, + { label: 'Settings', path: '/admin/settings', icon: 'settings' }, + ]; + + toggleSidebar(): void { this.sidebarOpen.update(v => !v); } + logout(): void { this.authService.logout(); } +} diff --git a/client/book-my-venue/src/app/admin/models/index.ts b/client/book-my-venue/src/app/admin/models/index.ts new file mode 100644 index 000000000..4d233a546 --- /dev/null +++ b/client/book-my-venue/src/app/admin/models/index.ts @@ -0,0 +1,4 @@ +export * from '../../shared/models/user.model'; +export * from '../../shared/models/vendor.model'; +export * from '../../shared/models/venue.model'; +export * from '../../shared/models/booking.model'; diff --git a/client/book-my-venue/src/app/admin/repositories/analytics.repository.ts b/client/book-my-venue/src/app/admin/repositories/analytics.repository.ts new file mode 100644 index 000000000..64f939f24 --- /dev/null +++ b/client/book-my-venue/src/app/admin/repositories/analytics.repository.ts @@ -0,0 +1,27 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { ApiResponse } from '../../shared/models/api-response.model'; + +export interface AdminAnalyticsData { + totalRevenue: number; + userGrowth: { date: string; count: number }[]; + bookingTrends: { date: string; count: number }[]; + topVenues: { name: string; bookings: number }[]; +} + +@Injectable({ providedIn: 'root' }) +export class AdminAnalyticsRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getAnalytics(period: string): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.ANALYTICS}`, { params: { period } }); + } + + getDashboardStats(): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.DASHBOARD}`); + } +} diff --git a/client/book-my-venue/src/app/admin/repositories/booking.repository.ts b/client/book-my-venue/src/app/admin/repositories/booking.repository.ts new file mode 100644 index 000000000..df273a321 --- /dev/null +++ b/client/book-my-venue/src/app/admin/repositories/booking.repository.ts @@ -0,0 +1,18 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { Booking } from '../../shared/models/booking.model'; +import { PaginationResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class AdminBookingRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getBookings(page = 1, limit = 10): Observable> { + const params = new HttpParams().set('page', page).set('limit', limit); + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.BOOKINGS}`, { params }); + } +} diff --git a/client/book-my-venue/src/app/admin/repositories/user.repository.ts b/client/book-my-venue/src/app/admin/repositories/user.repository.ts new file mode 100644 index 000000000..188d09795 --- /dev/null +++ b/client/book-my-venue/src/app/admin/repositories/user.repository.ts @@ -0,0 +1,26 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { User } from '../../shared/models/user.model'; +import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class AdminUserRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getUsers(page = 1, limit = 10): Observable> { + const params = new HttpParams().set('page', page).set('limit', limit); + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.USERS}`, { params }); + } + + getUserById(id: string): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.USERS.BY_ID(id)}`); + } + + toggleUserStatus(id: string, isActive: boolean): Observable> { + return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.USERS.BY_ID(id)}`, { isActive }); + } +} diff --git a/client/book-my-venue/src/app/admin/repositories/vendor.repository.ts b/client/book-my-venue/src/app/admin/repositories/vendor.repository.ts new file mode 100644 index 000000000..048c7f692 --- /dev/null +++ b/client/book-my-venue/src/app/admin/repositories/vendor.repository.ts @@ -0,0 +1,30 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { Vendor } from '../../shared/models/vendor.model'; +import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class AdminVendorRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getVendors(page = 1, limit = 10): Observable> { + const params = new HttpParams().set('page', page).set('limit', limit); + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.VENDORS}`, { params }); + } + + getVendorById(id: string): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.BY_ID(id)}`); + } + + verifyVendor(id: string): Observable> { + return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.BY_ID(id)}`, { isVerified: true }); + } + + toggleVendorStatus(id: string, isActive: boolean): Observable> { + return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.BY_ID(id)}`, { isActive }); + } +} diff --git a/client/book-my-venue/src/app/admin/repositories/venue.repository.ts b/client/book-my-venue/src/app/admin/repositories/venue.repository.ts new file mode 100644 index 000000000..a0536af75 --- /dev/null +++ b/client/book-my-venue/src/app/admin/repositories/venue.repository.ts @@ -0,0 +1,22 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { Venue } from '../../shared/models/venue.model'; +import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class AdminVenueRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getVenues(page = 1, limit = 10): Observable> { + const params = new HttpParams().set('page', page).set('limit', limit); + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.VENUES}`, { params }); + } + + toggleVenueStatus(id: string, status: string): Observable> { + return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.VENUES.BY_ID(id)}`, { status }); + } +} diff --git a/client/book-my-venue/src/app/admin/routes/admin.routes.ts b/client/book-my-venue/src/app/admin/routes/admin.routes.ts new file mode 100644 index 000000000..31632077c --- /dev/null +++ b/client/book-my-venue/src/app/admin/routes/admin.routes.ts @@ -0,0 +1,32 @@ +import { Routes } from '@angular/router'; +import { ROUTE_PATHS } from '../../shared/constants/route-paths.constant'; +import { adminAuthGuard } from '../guards/admin-auth.guard'; +import { guestGuard } from '../../shared/guards/guest.guard'; + +export const ADMIN_ROUTES: Routes = [ + { + path: ROUTE_PATHS.ADMIN.LOGIN, + canActivate: [guestGuard], + loadComponent: () => import('../features/login/login.component').then(m => m.LoginComponent), + }, + { + path: ROUTE_PATHS.ADMIN.FORGOT_PASSWORD, + canActivate: [guestGuard], + loadComponent: () => import('../features/forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent), + }, + { + path: '', + loadComponent: () => import('../layouts/admin-layout/admin-layout.component').then(m => m.AdminLayoutComponent), + canActivate: [adminAuthGuard], + children: [ + { path: ROUTE_PATHS.ADMIN.DASHBOARD, loadComponent: () => import('../features/dashboard/dashboard.component').then(m => m.DashboardComponent) }, + { path: ROUTE_PATHS.ADMIN.USERS, loadComponent: () => import('../features/users/users.component').then(m => m.UsersComponent) }, + { path: ROUTE_PATHS.ADMIN.VENDORS, loadComponent: () => import('../features/vendors/vendors.component').then(m => m.VendorsComponent) }, + { path: ROUTE_PATHS.ADMIN.VENUES, loadComponent: () => import('../features/venues/venues.component').then(m => m.VenuesComponent) }, + { path: ROUTE_PATHS.ADMIN.BOOKINGS, loadComponent: () => import('../features/bookings/bookings.component').then(m => m.BookingsComponent) }, + { path: ROUTE_PATHS.ADMIN.ANALYTICS, loadComponent: () => import('../features/analytics/analytics.component').then(m => m.AnalyticsComponent) }, + { path: ROUTE_PATHS.ADMIN.SETTINGS, loadComponent: () => import('../features/settings/settings.component').then(m => m.SettingsComponent) }, + { path: '', redirectTo: ROUTE_PATHS.ADMIN.DASHBOARD, pathMatch: 'full' }, + ], + }, +]; diff --git a/client/book-my-venue/src/app/admin/services/analytics.service.ts b/client/book-my-venue/src/app/admin/services/analytics.service.ts new file mode 100644 index 000000000..8d838dd89 --- /dev/null +++ b/client/book-my-venue/src/app/admin/services/analytics.service.ts @@ -0,0 +1,20 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { AdminAnalyticsRepository, AdminAnalyticsData } from '../repositories/analytics.repository'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class AdminAnalyticsService { + private readonly analyticsRepository = inject(AdminAnalyticsRepository); + private readonly notification = inject(NotificationService); + + readonly data = signal(null); + readonly loading = signal(false); + + loadAnalytics(period: string): void { + this.loading.set(true); + this.analyticsRepository.getAnalytics(period).subscribe({ + next: (r) => { this.data.set(r.data); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load analytics'); this.loading.set(false); }, + }); + } +} diff --git a/client/book-my-venue/src/app/admin/services/booking.service.ts b/client/book-my-venue/src/app/admin/services/booking.service.ts new file mode 100644 index 000000000..1f86f139d --- /dev/null +++ b/client/book-my-venue/src/app/admin/services/booking.service.ts @@ -0,0 +1,22 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { AdminBookingRepository } from '../repositories/booking.repository'; +import { Booking } from '../../shared/models/booking.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class AdminBookingService { + private readonly bookingRepository = inject(AdminBookingRepository); + private readonly notification = inject(NotificationService); + + readonly bookings = signal([]); + readonly loading = signal(false); + readonly totalPages = signal(1); + + loadBookings(page = 1): void { + this.loading.set(true); + this.bookingRepository.getBookings(page).subscribe({ + next: (r) => { this.bookings.set(r.data); this.totalPages.set(r.totalPages); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load bookings'); this.loading.set(false); }, + }); + } +} diff --git a/client/book-my-venue/src/app/admin/services/user.service.ts b/client/book-my-venue/src/app/admin/services/user.service.ts new file mode 100644 index 000000000..78cc24bab --- /dev/null +++ b/client/book-my-venue/src/app/admin/services/user.service.ts @@ -0,0 +1,22 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { AdminUserRepository } from '../repositories/user.repository'; +import { User } from '../../shared/models/user.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class AdminUserService { + private readonly userRepository = inject(AdminUserRepository); + private readonly notification = inject(NotificationService); + + readonly users = signal([]); + readonly loading = signal(false); + readonly totalPages = signal(1); + + loadUsers(page = 1): void { + this.loading.set(true); + this.userRepository.getUsers(page).subscribe({ + next: (r) => { this.users.set(r.data); this.totalPages.set(r.totalPages); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load users'); this.loading.set(false); }, + }); + } +} diff --git a/client/book-my-venue/src/app/admin/services/vendor.service.ts b/client/book-my-venue/src/app/admin/services/vendor.service.ts new file mode 100644 index 000000000..97b72fb1d --- /dev/null +++ b/client/book-my-venue/src/app/admin/services/vendor.service.ts @@ -0,0 +1,22 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { AdminVendorRepository } from '../repositories/vendor.repository'; +import { Vendor } from '../../shared/models/vendor.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class AdminVendorService { + private readonly vendorRepository = inject(AdminVendorRepository); + private readonly notification = inject(NotificationService); + + readonly vendors = signal([]); + readonly loading = signal(false); + readonly totalPages = signal(1); + + loadVendors(page = 1): void { + this.loading.set(true); + this.vendorRepository.getVendors(page).subscribe({ + next: (r) => { this.vendors.set(r.data); this.totalPages.set(r.totalPages); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load vendors'); this.loading.set(false); }, + }); + } +} diff --git a/client/book-my-venue/src/app/admin/services/venue.service.ts b/client/book-my-venue/src/app/admin/services/venue.service.ts new file mode 100644 index 000000000..5e8d213f3 --- /dev/null +++ b/client/book-my-venue/src/app/admin/services/venue.service.ts @@ -0,0 +1,21 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { AdminVenueRepository } from '../repositories/venue.repository'; +import { Venue } from '../../shared/models/venue.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class AdminVenueService { + private readonly venueRepository = inject(AdminVenueRepository); + private readonly notification = inject(NotificationService); + + readonly venues = signal([]); + readonly loading = signal(false); + + loadVenues(page = 1): void { + this.loading.set(true); + this.venueRepository.getVenues(page).subscribe({ + next: (r) => { this.venues.set(r.data); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load venues'); this.loading.set(false); }, + }); + } +} diff --git a/client/book-my-venue/src/app/app.config.ts b/client/book-my-venue/src/app/app.config.ts new file mode 100644 index 000000000..4ec67409a --- /dev/null +++ b/client/book-my-venue/src/app/app.config.ts @@ -0,0 +1,21 @@ +import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { routes } from './core/app.routes'; +import { appProviders } from './core/providers/app-providers'; +import { errorInterceptor } from './shared/interceptors/error.interceptor'; +import { userAuthInterceptor } from './user/interceptors/user-auth.interceptor'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideRouter(routes, withComponentInputBinding()), + provideHttpClient( + withInterceptors([ + userAuthInterceptor, + errorInterceptor, + ]) + ), + ...appProviders, + ], +}; diff --git a/client/book-my-venue/src/app/app.css b/client/book-my-venue/src/app/app.css new file mode 100644 index 000000000..e69de29bb diff --git a/client/book-my-venue/src/app/app.html b/client/book-my-venue/src/app/app.html new file mode 100644 index 000000000..67e7bd4cd --- /dev/null +++ b/client/book-my-venue/src/app/app.html @@ -0,0 +1 @@ + diff --git a/client/book-my-venue/src/app/app.routes.ts b/client/book-my-venue/src/app/app.routes.ts new file mode 100644 index 000000000..b7d282fdc --- /dev/null +++ b/client/book-my-venue/src/app/app.routes.ts @@ -0,0 +1 @@ +export { routes } from './core/app.routes'; diff --git a/client/book-my-venue/src/app/app.spec.ts b/client/book-my-venue/src/app/app.spec.ts new file mode 100644 index 000000000..5427e1a58 --- /dev/null +++ b/client/book-my-venue/src/app/app.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; +import { App } from './app'; + +describe('App', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [App], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(App); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it('should render title', async () => { + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, book-my-venue'); + }); +}); diff --git a/client/book-my-venue/src/app/app.ts b/client/book-my-venue/src/app/app.ts new file mode 100644 index 000000000..547fb8c54 --- /dev/null +++ b/client/book-my-venue/src/app/app.ts @@ -0,0 +1,11 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + imports: [RouterOutlet], + templateUrl: './app.html', + styleUrl: './app.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App {} diff --git a/client/book-my-venue/src/app/core/app.routes.ts b/client/book-my-venue/src/app/core/app.routes.ts new file mode 100644 index 000000000..943c1b2fa --- /dev/null +++ b/client/book-my-venue/src/app/core/app.routes.ts @@ -0,0 +1,42 @@ +import { Routes } from '@angular/router'; +import { ROUTE_PATHS } from '../shared/constants/route-paths.constant'; +import { guestGuard } from '../shared/guards/guest.guard'; + +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('../landing/landing.component').then(m => m.LandingComponent), + pathMatch: 'full', + }, + { + path: 'login', + canActivate: [guestGuard], + loadComponent: () => import('../user/features/login/login.component').then(m => m.LoginComponent), + }, + { + path: 'signup', + canActivate: [guestGuard], + loadComponent: () => import('../user/features/signup/signup.component').then(m => m.SignupComponent), + }, + { + path: 'forgot-password', + canActivate: [guestGuard], + loadComponent: () => import('../user/features/forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent), + }, + { + path: ROUTE_PATHS.USER.BASE, + loadChildren: () => import('../user/routes/user.routes').then(m => m.USER_ROUTES), + }, + { + path: ROUTE_PATHS.VENDOR.BASE, + loadChildren: () => import('../vendor/routes/vendor.routes').then(m => m.VENDOR_ROUTES), + }, + { + path: ROUTE_PATHS.ADMIN.BASE, + loadChildren: () => import('../admin/routes/admin.routes').then(m => m.ADMIN_ROUTES), + }, + { + path: '**', + redirectTo: '', + }, +]; diff --git a/client/book-my-venue/src/app/core/config/environment.prod.ts b/client/book-my-venue/src/app/core/config/environment.prod.ts new file mode 100644 index 000000000..6d8ac42ca --- /dev/null +++ b/client/book-my-venue/src/app/core/config/environment.prod.ts @@ -0,0 +1,6 @@ +export const environment = { + production: true, + apiUrl: 'https://api.bookmyvenue.com/api', + tokenKey: 'bmv_token', + refreshTokenKey: 'bmv_refresh_token', +}; diff --git a/client/book-my-venue/src/app/core/config/environment.ts b/client/book-my-venue/src/app/core/config/environment.ts new file mode 100644 index 000000000..7cdf0fc87 --- /dev/null +++ b/client/book-my-venue/src/app/core/config/environment.ts @@ -0,0 +1,6 @@ +export const environment = { + production: false, + apiUrl: 'http://localhost:3000/api', + tokenKey: 'bmv_token', + refreshTokenKey: 'bmv_refresh_token', +}; diff --git a/client/book-my-venue/src/app/core/providers/app-providers.ts b/client/book-my-venue/src/app/core/providers/app-providers.ts new file mode 100644 index 000000000..98b634880 --- /dev/null +++ b/client/book-my-venue/src/app/core/providers/app-providers.ts @@ -0,0 +1,9 @@ +import { Provider } from '@angular/core'; +import { environment } from '../config/environment'; +import { API_URL, TOKEN_KEY, REFRESH_TOKEN_KEY } from '../tokens/injection-tokens'; + +export const appProviders: Provider[] = [ + { provide: API_URL, useValue: environment.apiUrl }, + { provide: TOKEN_KEY, useValue: environment.tokenKey }, + { provide: REFRESH_TOKEN_KEY, useValue: environment.refreshTokenKey }, +]; diff --git a/client/book-my-venue/src/app/core/tokens/injection-tokens.ts b/client/book-my-venue/src/app/core/tokens/injection-tokens.ts new file mode 100644 index 000000000..8a9185a86 --- /dev/null +++ b/client/book-my-venue/src/app/core/tokens/injection-tokens.ts @@ -0,0 +1,5 @@ +import { InjectionToken } from '@angular/core'; + +export const API_URL = new InjectionToken('API_URL'); +export const TOKEN_KEY = new InjectionToken('TOKEN_KEY'); +export const REFRESH_TOKEN_KEY = new InjectionToken('REFRESH_TOKEN_KEY'); diff --git a/client/book-my-venue/src/app/landing/landing.component.css b/client/book-my-venue/src/app/landing/landing.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/client/book-my-venue/src/app/landing/landing.component.html b/client/book-my-venue/src/app/landing/landing.component.html new file mode 100644 index 000000000..5e498becf --- /dev/null +++ b/client/book-my-venue/src/app/landing/landing.component.html @@ -0,0 +1,242 @@ + + + + +
+
+
+
+
+ +
+
+ + India's Premier Venue Booking Platform +
+ +

+ Find & Book Your + + Perfect Venue + + + + + +

+ +

+ Discover stunning venues for weddings, corporate events, and celebrations. + Connect directly with vendors, compare prices, and book instantly. +

+ + + + +
+
+

500+

+

Premium Venues

+
+
+

10K+

+

Events Hosted

+
+
+

50+

+

Cities Covered

+
+
+

4.8

+

Average Rating

+
+
+
+
+ + +
+
+
+

Features

+

Everything You Need to Plan Your Event

+

From discovery to booking, we've streamlined the entire venue selection process.

+
+ +
+
+
+ +
+

Smart Search

+

Filter venues by city, capacity, price range, and amenities to find your perfect match.

+
+ +
+
+ +
+

Instant Booking

+

Book your venue in minutes with our streamlined checkout process. No phone calls needed.

+
+ +
+
+ +
+

Transparent Pricing

+

See real-time pricing per hour with no hidden charges. Compare venues side by side.

+
+ +
+
+ +
+

Verified Venues

+

Every venue is verified by our team. Read genuine reviews from past event hosts.

+
+ +
+
+ +
+

Real-time Availability

+

Check venue availability instantly. No more back-and-forth with venue managers.

+
+ +
+
+ +
+

Vendor Dashboard

+

Vendors get powerful analytics, booking management, and revenue tracking tools.

+
+
+
+
+ + +
+
+
+

How It Works

+

Book Your Venue in 3 Simple Steps

+
+ +
+
+
1
+

Search & Discover

+

Browse through hundreds of venues. Filter by location, capacity, budget, and amenities.

+ +
+ +
+
2
+

Compare & Choose

+

View detailed photos, amenities, reviews, and pricing. Pick the venue that fits your vision.

+ +
+ +
+
3
+

Book & Celebrate

+

Confirm your date, complete the booking, and get ready for your event. It's that simple!

+
+
+
+
+ + + diff --git a/client/book-my-venue/src/app/landing/landing.component.spec.ts b/client/book-my-venue/src/app/landing/landing.component.spec.ts new file mode 100644 index 000000000..77211cb46 --- /dev/null +++ b/client/book-my-venue/src/app/landing/landing.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { LandingComponent } from './landing.component'; + +describe('LandingComponent', () => { + let component: LandingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LandingComponent, RouterTestingModule], + }).compileComponents(); + + fixture = TestBed.createComponent(LandingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/landing/landing.component.ts b/client/book-my-venue/src/app/landing/landing.component.ts new file mode 100644 index 000000000..433e3db08 --- /dev/null +++ b/client/book-my-venue/src/app/landing/landing.component.ts @@ -0,0 +1,12 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-landing', + standalone: true, + imports: [RouterLink], + templateUrl: './landing.component.html', + styleUrl: './landing.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LandingComponent {} diff --git a/client/book-my-venue/src/app/shared/components/button/button.component.css b/client/book-my-venue/src/app/shared/components/button/button.component.css new file mode 100644 index 000000000..48bb062ea --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/button/button.component.css @@ -0,0 +1,3 @@ +:host { + display: inline-block; +} diff --git a/client/book-my-venue/src/app/shared/components/button/button.component.html b/client/book-my-venue/src/app/shared/components/button/button.component.html new file mode 100644 index 000000000..a5463a4c1 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/button/button.component.html @@ -0,0 +1,14 @@ + diff --git a/client/book-my-venue/src/app/shared/components/button/button.component.spec.ts b/client/book-my-venue/src/app/shared/components/button/button.component.spec.ts new file mode 100644 index 000000000..463d2625c --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/button/button.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ButtonComponent } from './button.component'; + +describe('ButtonComponent', () => { + let component: ButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ButtonComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ButtonComponent); + fixture.componentRef.setInput('label', 'Test'); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the label', () => { + const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); + expect(button.textContent?.trim()).toContain('Test'); + }); + + it('should be disabled when disabled input is true', () => { + fixture.componentRef.setInput('disabled', true); + fixture.detectChanges(); + const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); + expect(button.disabled).toBeTrue(); + }); +}); diff --git a/client/book-my-venue/src/app/shared/components/button/button.component.ts b/client/book-my-venue/src/app/shared/components/button/button.component.ts new file mode 100644 index 000000000..ae0f3181d --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/button/button.component.ts @@ -0,0 +1,44 @@ +import { Component, input, output, computed, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-button', + standalone: true, + templateUrl: './button.component.html', + styleUrl: './button.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ButtonComponent { + readonly label = input.required(); + readonly type = input<'button' | 'submit' | 'reset'>('button'); + readonly variant = input<'primary' | 'secondary' | 'danger' | 'outline' | 'ghost'>('primary'); + readonly size = input<'sm' | 'md' | 'lg'>('md'); + readonly disabled = input(false); + readonly loading = input(false); + readonly fullWidth = input(false); + + readonly clicked = output(); + + readonly buttonClasses = computed(() => { + const base = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'; + const variants: Record = { + primary: 'bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-500', + secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500', + danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', + outline: 'border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-indigo-500', + ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-indigo-500', + }; + const sizes: Record = { + sm: 'px-3 py-1.5 text-xs', + md: 'px-4 py-2 text-sm', + lg: 'px-6 py-3 text-base', + }; + const width = this.fullWidth() ? 'w-full' : ''; + return `${base} ${variants[this.variant()]} ${sizes[this.size()]} ${width}`; + }); + + onClick(): void { + if (!this.disabled() && !this.loading()) { + this.clicked.emit(); + } + } +} diff --git a/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.css b/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.css new file mode 100644 index 000000000..92d692cdd --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.css @@ -0,0 +1,3 @@ +:host { + display: contents; +} diff --git a/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.html b/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.html new file mode 100644 index 000000000..1f4ad9b4b --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.html @@ -0,0 +1,38 @@ +@if (isOpen()) { +
+
+
+
+ @if (variant() === 'danger') { +
+ + + +
+ } @else { +
+ + + +
+ } +

{{ title() }}

+
+

{{ message() }}

+
+ + +
+
+
+} diff --git a/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts b/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts new file mode 100644 index 000000000..31146c197 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ConfirmDialogComponent } from './confirm-dialog.component'; + +describe('ConfirmDialogComponent', () => { + let component: ConfirmDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfirmDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ConfirmDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should not render when closed', () => { + const overlay = fixture.nativeElement.querySelector('.fixed'); + expect(overlay).toBeNull(); + }); +}); diff --git a/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts b/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts new file mode 100644 index 000000000..3f0a6a76d --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts @@ -0,0 +1,28 @@ +import { Component, input, output, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-confirm-dialog', + standalone: true, + templateUrl: './confirm-dialog.component.html', + styleUrl: './confirm-dialog.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConfirmDialogComponent { + readonly isOpen = input(false); + readonly title = input('Confirm Action'); + readonly message = input('Are you sure you want to proceed?'); + readonly confirmLabel = input('Confirm'); + readonly cancelLabel = input('Cancel'); + readonly variant = input<'danger' | 'warning' | 'info'>('danger'); + + readonly confirmed = output(); + readonly cancelled = output(); + + onConfirm(): void { + this.confirmed.emit(); + } + + onCancel(): void { + this.cancelled.emit(); + } +} diff --git a/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.css b/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.html b/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.html new file mode 100644 index 000000000..e1d3b912f --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.html @@ -0,0 +1,36 @@ +
+
+ @switch (icon()) { + @case ('search') { + + + + } + @case ('calendar') { + + + + } + @case ('users') { + + + + } + @default { + + + + } + } +
+

{{ title() }}

+

{{ message() }}

+ @if (actionLabel()) { + + } +
diff --git a/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.spec.ts b/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.spec.ts new file mode 100644 index 000000000..b98fc8b1a --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { EmptyStateComponent } from './empty-state.component'; + +describe('EmptyStateComponent', () => { + let component: EmptyStateComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmptyStateComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display default title', () => { + const title = fixture.nativeElement.querySelector('h3'); + expect(title.textContent).toContain('No data found'); + }); +}); diff --git a/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.ts b/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.ts new file mode 100644 index 000000000..c16f8e7ae --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.ts @@ -0,0 +1,21 @@ +import { Component, input, output, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-empty-state', + standalone: true, + templateUrl: './empty-state.component.html', + styleUrl: './empty-state.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EmptyStateComponent { + readonly title = input('No data found'); + readonly message = input('There are no items to display at the moment.'); + readonly icon = input<'search' | 'inbox' | 'calendar' | 'users'>('inbox'); + readonly actionLabel = input(''); + + readonly actionClicked = output(); + + onAction(): void { + this.actionClicked.emit(); + } +} diff --git a/client/book-my-venue/src/app/shared/components/input/input.component.css b/client/book-my-venue/src/app/shared/components/input/input.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/input/input.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/shared/components/input/input.component.html b/client/book-my-venue/src/app/shared/components/input/input.component.html new file mode 100644 index 000000000..be9e2ab97 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/input/input.component.html @@ -0,0 +1,26 @@ +
+ @if (label()) { + + } + + @if (error()) { +

{{ error() }}

+ } + @if (hint() && !error()) { +

{{ hint() }}

+ } +
diff --git a/client/book-my-venue/src/app/shared/components/input/input.component.spec.ts b/client/book-my-venue/src/app/shared/components/input/input.component.spec.ts new file mode 100644 index 000000000..f48a773d0 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/input/input.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { InputComponent } from './input.component'; + +describe('InputComponent', () => { + let component: InputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display label when provided', () => { + fixture.componentRef.setInput('label', 'Email'); + fixture.detectChanges(); + const label = fixture.nativeElement.querySelector('label'); + expect(label.textContent).toContain('Email'); + }); + + it('should show error message', () => { + fixture.componentRef.setInput('error', 'Required field'); + fixture.detectChanges(); + const error = fixture.nativeElement.querySelector('p'); + expect(error.textContent).toContain('Required field'); + }); +}); diff --git a/client/book-my-venue/src/app/shared/components/input/input.component.ts b/client/book-my-venue/src/app/shared/components/input/input.component.ts new file mode 100644 index 000000000..008cac10c --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/input/input.component.ts @@ -0,0 +1,59 @@ +import { Component, input, output, signal, ChangeDetectionStrategy, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'app-input', + standalone: true, + templateUrl: './input.component.html', + styleUrl: './input.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => InputComponent), + multi: true, + }, + ], +}) +export class InputComponent implements ControlValueAccessor { + readonly label = input(''); + readonly placeholder = input(''); + readonly type = input<'text' | 'email' | 'password' | 'number' | 'tel' | 'date' | 'time'>('text'); + readonly error = input(''); + readonly hint = input(''); + readonly required = input(false); + readonly disabled = input(false); + + readonly value = signal(''); + readonly focused = signal(false); + + private onChange: (value: string) => void = () => {}; + private onTouched: () => void = () => {}; + + writeValue(value: string): void { + this.value.set(value || ''); + } + + registerOnChange(fn: (value: string) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + onInput(event: Event): void { + const target = event.target as HTMLInputElement; + this.value.set(target.value); + this.onChange(target.value); + } + + onBlur(): void { + this.focused.set(false); + this.onTouched(); + } + + onFocus(): void { + this.focused.set(true); + } +} diff --git a/client/book-my-venue/src/app/shared/components/loader/loader.component.css b/client/book-my-venue/src/app/shared/components/loader/loader.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/loader/loader.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/shared/components/loader/loader.component.html b/client/book-my-venue/src/app/shared/components/loader/loader.component.html new file mode 100644 index 000000000..8eba81fa3 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/loader/loader.component.html @@ -0,0 +1,17 @@ +@if (overlay()) { +
+
+
+ @if (message()) { +

{{ message() }}

+ } +
+
+} @else { +
+
+ @if (message()) { +

{{ message() }}

+ } +
+} diff --git a/client/book-my-venue/src/app/shared/components/loader/loader.component.spec.ts b/client/book-my-venue/src/app/shared/components/loader/loader.component.spec.ts new file mode 100644 index 000000000..5f536fa25 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/loader/loader.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { LoaderComponent } from './loader.component'; + +describe('LoaderComponent', () => { + let component: LoaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoaderComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LoaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/shared/components/loader/loader.component.ts b/client/book-my-venue/src/app/shared/components/loader/loader.component.ts new file mode 100644 index 000000000..d6b9e0a86 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/loader/loader.component.ts @@ -0,0 +1,23 @@ +import { Component, input, computed, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-loader', + standalone: true, + templateUrl: './loader.component.html', + styleUrl: './loader.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoaderComponent { + readonly size = input<'sm' | 'md' | 'lg'>('md'); + readonly overlay = input(false); + readonly message = input(''); + + readonly spinnerClass = computed(() => { + const sizes: Record = { + sm: 'w-6 h-6', + md: 'w-10 h-10', + lg: 'w-16 h-16', + }; + return `${sizes[this.size()]} border-4 border-gray-200 border-t-indigo-600 rounded-full animate-spin`; + }); +} diff --git a/client/book-my-venue/src/app/shared/components/modal/modal.component.css b/client/book-my-venue/src/app/shared/components/modal/modal.component.css new file mode 100644 index 000000000..92d692cdd --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/modal/modal.component.css @@ -0,0 +1,3 @@ +:host { + display: contents; +} diff --git a/client/book-my-venue/src/app/shared/components/modal/modal.component.html b/client/book-my-venue/src/app/shared/components/modal/modal.component.html new file mode 100644 index 000000000..ef59ff470 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/modal/modal.component.html @@ -0,0 +1,23 @@ +@if (isOpen()) { +
+
+
+
+

{{ title() }}

+ +
+
+ +
+
+
+} diff --git a/client/book-my-venue/src/app/shared/components/modal/modal.component.spec.ts b/client/book-my-venue/src/app/shared/components/modal/modal.component.spec.ts new file mode 100644 index 000000000..afdabab00 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/modal/modal.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ModalComponent } from './modal.component'; + +describe('ModalComponent', () => { + let component: ModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ModalComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should not render when closed', () => { + const overlay = fixture.nativeElement.querySelector('.fixed'); + expect(overlay).toBeNull(); + }); + + it('should render when open', () => { + fixture.componentRef.setInput('isOpen', true); + fixture.componentRef.setInput('title', 'Test Modal'); + fixture.detectChanges(); + const title = fixture.nativeElement.querySelector('h3'); + expect(title.textContent).toContain('Test Modal'); + }); +}); diff --git a/client/book-my-venue/src/app/shared/components/modal/modal.component.ts b/client/book-my-venue/src/app/shared/components/modal/modal.component.ts new file mode 100644 index 000000000..4dc53372f --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/modal/modal.component.ts @@ -0,0 +1,34 @@ +import { Component, input, output, computed, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-modal', + standalone: true, + templateUrl: './modal.component.html', + styleUrl: './modal.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ModalComponent { + readonly isOpen = input(false); + readonly title = input(''); + readonly size = input<'sm' | 'md' | 'lg' | 'xl'>('md'); + + readonly closed = output(); + + readonly sizeClass = computed(() => { + const sizes: Record = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + }; + return sizes[this.size()]; + }); + + onBackdropClick(): void { + this.closed.emit(); + } + + onClose(): void { + this.closed.emit(); + } +} diff --git a/client/book-my-venue/src/app/shared/components/pagination/pagination.component.css b/client/book-my-venue/src/app/shared/components/pagination/pagination.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/pagination/pagination.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/shared/components/pagination/pagination.component.html b/client/book-my-venue/src/app/shared/components/pagination/pagination.component.html new file mode 100644 index 000000000..b865e5786 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/pagination/pagination.component.html @@ -0,0 +1,34 @@ +
+

+ Showing page {{ currentPage() }} of {{ totalPages() }} ({{ totalItems() }} items) +

+ +
diff --git a/client/book-my-venue/src/app/shared/components/pagination/pagination.component.spec.ts b/client/book-my-venue/src/app/shared/components/pagination/pagination.component.spec.ts new file mode 100644 index 000000000..7dec530c8 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/pagination/pagination.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PaginationComponent } from './pagination.component'; + +describe('PaginationComponent', () => { + let component: PaginationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PaginationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PaginationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should disable previous on first page', () => { + expect(component.hasPrevious()).toBeFalse(); + }); +}); diff --git a/client/book-my-venue/src/app/shared/components/pagination/pagination.component.ts b/client/book-my-venue/src/app/shared/components/pagination/pagination.component.ts new file mode 100644 index 000000000..d7e743cd8 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/pagination/pagination.component.ts @@ -0,0 +1,59 @@ +import { Component, input, output, computed, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-pagination', + standalone: true, + templateUrl: './pagination.component.html', + styleUrl: './pagination.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PaginationComponent { + readonly currentPage = input(1); + readonly totalPages = input(1); + readonly totalItems = input(0); + readonly pageSize = input(10); + + readonly pageChanged = output(); + + readonly pages = computed(() => { + const total = this.totalPages(); + const current = this.currentPage(); + const pages: (number | '...')[] = []; + + if (total <= 7) { + for (let i = 1; i <= total; i++) pages.push(i); + } else { + pages.push(1); + if (current > 3) pages.push('...'); + for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) { + pages.push(i); + } + if (current < total - 2) pages.push('...'); + pages.push(total); + } + + return pages; + }); + + readonly hasPrevious = computed(() => this.currentPage() > 1); + readonly hasNext = computed(() => this.currentPage() < this.totalPages()); + + goToPage(page: number | '...'): void { + if (page === '...') return; + if (page >= 1 && page <= this.totalPages()) { + this.pageChanged.emit(page); + } + } + + previousPage(): void { + if (this.hasPrevious()) { + this.pageChanged.emit(this.currentPage() - 1); + } + } + + nextPage(): void { + if (this.hasNext()) { + this.pageChanged.emit(this.currentPage() + 1); + } + } +} diff --git a/client/book-my-venue/src/app/shared/components/table/table.component.css b/client/book-my-venue/src/app/shared/components/table/table.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/table/table.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/shared/components/table/table.component.html b/client/book-my-venue/src/app/shared/components/table/table.component.html new file mode 100644 index 000000000..1b5c1ac79 --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/table/table.component.html @@ -0,0 +1,54 @@ +
+ + + + @for (col of columns(); track col.key) { + + } + + + + @if (loading()) { + + + + } @else if (data().length === 0) { + + + + } @else { + @for (item of data(); track $index; let odd = $odd) { + + @if (rowTemplate()) { + + } @else { + @for (col of columns(); track col.key) { + + } + } + + } + } + +
+ {{ col.label }} + @if (col.sortable) { + + + + } +
+ Loading... +
+ No data available +
{{ getCellValue(item, col.key) }}
+
diff --git a/client/book-my-venue/src/app/shared/components/table/table.component.spec.ts b/client/book-my-venue/src/app/shared/components/table/table.component.spec.ts new file mode 100644 index 000000000..e8d27b1be --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/table/table.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TableComponent, TableColumn } from './table.component'; + +describe('TableComponent', () => { + let component: TableComponent; + let fixture: ComponentFixture; + + const columns: TableColumn[] = [ + { key: 'name', label: 'Name' }, + { key: 'email', label: 'Email' }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TableComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TableComponent); + fixture.componentRef.setInput('columns', columns); + fixture.componentRef.setInput('data', []); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show no data message when empty', () => { + const cell = fixture.nativeElement.querySelector('td'); + expect(cell.textContent).toContain('No data available'); + }); +}); diff --git a/client/book-my-venue/src/app/shared/components/table/table.component.ts b/client/book-my-venue/src/app/shared/components/table/table.component.ts new file mode 100644 index 000000000..c3e91509c --- /dev/null +++ b/client/book-my-venue/src/app/shared/components/table/table.component.ts @@ -0,0 +1,51 @@ +import { Component, input, output, ChangeDetectionStrategy, TemplateRef, contentChild } from '@angular/core'; +import { NgTemplateOutlet } from '@angular/common'; + +export interface TableColumn { + key: string; + label: string; + sortable?: boolean; + width?: string; +} + +@Component({ + selector: 'app-table', + standalone: true, + imports: [NgTemplateOutlet], + templateUrl: './table.component.html', + styleUrl: './table.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TableComponent> { + readonly columns = input.required(); + readonly data = input.required(); + readonly loading = input(false); + readonly striped = input(true); + + readonly rowClicked = output(); + readonly sortChanged = output<{ key: string; direction: 'asc' | 'desc' }>(); + + readonly rowTemplate = contentChild>('rowTemplate'); + + private sortKey = ''; + private sortDirection: 'asc' | 'desc' = 'asc'; + + onSort(column: TableColumn): void { + if (!column.sortable) return; + if (this.sortKey === column.key) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortKey = column.key; + this.sortDirection = 'asc'; + } + this.sortChanged.emit({ key: this.sortKey, direction: this.sortDirection }); + } + + onRowClick(item: T): void { + this.rowClicked.emit(item); + } + + getCellValue(item: T, key: string): unknown { + return (item as Record)[key]; + } +} diff --git a/client/book-my-venue/src/app/shared/constants/api-endpoints.constant.ts b/client/book-my-venue/src/app/shared/constants/api-endpoints.constant.ts new file mode 100644 index 000000000..9d7d46947 --- /dev/null +++ b/client/book-my-venue/src/app/shared/constants/api-endpoints.constant.ts @@ -0,0 +1,43 @@ +export const API_ENDPOINTS = { + AUTH: { + LOGIN: '/auth/login', + SIGNUP: '/auth/signup', + LOGOUT: '/auth/logout', + REFRESH_TOKEN: '/auth/refresh-token', + FORGOT_PASSWORD: '/auth/forgot-password', + RESET_PASSWORD: '/auth/reset-password', + ADMIN_LOGIN: '/auth/admin/login', + }, + USERS: { + BASE: '/users', + PROFILE: '/users/profile', + BY_ID: (id: string) => `/users/${id}`, + }, + VENDORS: { + BASE: '/vendors', + PROFILE: '/vendors/profile', + BY_ID: (id: string) => `/vendors/${id}`, + ANALYTICS: '/vendors/analytics', + }, + VENUES: { + BASE: '/venues', + BY_ID: (id: string) => `/venues/${id}`, + BY_VENDOR: (vendorId: string) => `/vendors/${vendorId}/venues`, + SEARCH: '/venues/search', + }, + BOOKINGS: { + BASE: '/bookings', + BY_ID: (id: string) => `/bookings/${id}`, + BY_USER: (userId: string) => `/users/${userId}/bookings`, + BY_VENUE: (venueId: string) => `/venues/${venueId}/bookings`, + BY_VENDOR: '/vendor/bookings', + }, + ADMIN: { + DASHBOARD: '/admin/dashboard', + USERS: '/admin/users', + VENDORS: '/admin/vendors', + VENUES: '/admin/venues', + BOOKINGS: '/admin/bookings', + ANALYTICS: '/admin/analytics', + }, +} as const; diff --git a/client/book-my-venue/src/app/shared/constants/index.ts b/client/book-my-venue/src/app/shared/constants/index.ts new file mode 100644 index 000000000..dd98611f0 --- /dev/null +++ b/client/book-my-venue/src/app/shared/constants/index.ts @@ -0,0 +1,2 @@ +export * from './api-endpoints.constant'; +export * from './route-paths.constant'; diff --git a/client/book-my-venue/src/app/shared/constants/route-paths.constant.ts b/client/book-my-venue/src/app/shared/constants/route-paths.constant.ts new file mode 100644 index 000000000..f9176c517 --- /dev/null +++ b/client/book-my-venue/src/app/shared/constants/route-paths.constant.ts @@ -0,0 +1,39 @@ +export const ROUTE_PATHS = { + USER: { + BASE: 'user', + LOGIN: 'login', + SIGNUP: 'signup', + FORGOT_PASSWORD: 'forgot-password', + DASHBOARD: 'dashboard', + VENUES: 'venues', + VENUE_DETAILS: 'venues/:id', + CHECKOUT: 'checkout/:venueId', + MY_BOOKINGS: 'my-bookings', + PROFILE: 'profile', + }, + VENDOR: { + BASE: 'vendor', + LOGIN: 'login', + SIGNUP: 'signup', + FORGOT_PASSWORD: 'forgot-password', + DASHBOARD: 'dashboard', + VENUES: 'venues', + CREATE_VENUE: 'venues/create', + EDIT_VENUE: 'venues/:id/edit', + BOOKINGS: 'bookings', + ANALYTICS: 'analytics', + SETTINGS: 'settings', + }, + ADMIN: { + BASE: 'admin', + LOGIN: 'login', + FORGOT_PASSWORD: 'forgot-password', + DASHBOARD: 'dashboard', + USERS: 'users', + VENDORS: 'vendors', + VENUES: 'venues', + BOOKINGS: 'bookings', + ANALYTICS: 'analytics', + SETTINGS: 'settings', + }, +} as const; diff --git a/client/book-my-venue/src/app/shared/directives/click-outside.directive.ts b/client/book-my-venue/src/app/shared/directives/click-outside.directive.ts new file mode 100644 index 000000000..36e3e0c99 --- /dev/null +++ b/client/book-my-venue/src/app/shared/directives/click-outside.directive.ts @@ -0,0 +1,18 @@ +import { Directive, ElementRef, inject, output } from '@angular/core'; +import { fromEvent, filter } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Directive({ selector: '[appClickOutside]', standalone: true }) +export class ClickOutsideDirective { + private readonly elementRef = inject(ElementRef); + readonly appClickOutside = output(); + + constructor() { + fromEvent(document, 'click') + .pipe( + filter((event) => !this.elementRef.nativeElement.contains(event.target as Node)), + takeUntilDestroyed() + ) + .subscribe(() => this.appClickOutside.emit()); + } +} diff --git a/client/book-my-venue/src/app/shared/enums/booking-status.enum.ts b/client/book-my-venue/src/app/shared/enums/booking-status.enum.ts new file mode 100644 index 000000000..9343ad966 --- /dev/null +++ b/client/book-my-venue/src/app/shared/enums/booking-status.enum.ts @@ -0,0 +1,7 @@ +export enum BookingStatus { + Pending = 'pending', + Confirmed = 'confirmed', + Cancelled = 'cancelled', + Completed = 'completed', + Rejected = 'rejected', +} diff --git a/client/book-my-venue/src/app/shared/enums/index.ts b/client/book-my-venue/src/app/shared/enums/index.ts new file mode 100644 index 000000000..88dece146 --- /dev/null +++ b/client/book-my-venue/src/app/shared/enums/index.ts @@ -0,0 +1,3 @@ +export * from './user-role.enum'; +export * from './booking-status.enum'; +export * from './venue-status.enum'; diff --git a/client/book-my-venue/src/app/shared/enums/user-role.enum.ts b/client/book-my-venue/src/app/shared/enums/user-role.enum.ts new file mode 100644 index 000000000..28b2a328e --- /dev/null +++ b/client/book-my-venue/src/app/shared/enums/user-role.enum.ts @@ -0,0 +1,5 @@ +export enum UserRole { + User = 'user', + Vendor = 'vendor', + Admin = 'admin', +} diff --git a/client/book-my-venue/src/app/shared/enums/venue-status.enum.ts b/client/book-my-venue/src/app/shared/enums/venue-status.enum.ts new file mode 100644 index 000000000..801d8c60f --- /dev/null +++ b/client/book-my-venue/src/app/shared/enums/venue-status.enum.ts @@ -0,0 +1,6 @@ +export enum VenueStatus { + Active = 'active', + Inactive = 'inactive', + Pending = 'pending', + Rejected = 'rejected', +} diff --git a/client/book-my-venue/src/app/shared/guards/admin-auth.guard.ts b/client/book-my-venue/src/app/shared/guards/admin-auth.guard.ts new file mode 100644 index 000000000..136c53e02 --- /dev/null +++ b/client/book-my-venue/src/app/shared/guards/admin-auth.guard.ts @@ -0,0 +1,16 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { UserRole } from '../enums/user-role.enum'; + +export const adminAuthGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isAuthenticated() && authService.hasRole(UserRole.Admin)) { + return true; + } + + router.navigate(['/admin/login']); + return false; +}; diff --git a/client/book-my-venue/src/app/shared/guards/guest.guard.ts b/client/book-my-venue/src/app/shared/guards/guest.guard.ts new file mode 100644 index 000000000..fe4facdd5 --- /dev/null +++ b/client/book-my-venue/src/app/shared/guards/guest.guard.ts @@ -0,0 +1,34 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { UserRole } from '../enums/user-role.enum'; + +export const guestGuard: CanActivateFn = (route) => { + const authService = inject(AuthService); + const router = inject(Router); + + const url = route.pathFromRoot.map(r => r.url.map(s => s.path).join('/')).join('/'); + + if (url.includes('admin')) { + if (authService.isPortalAuthenticated(UserRole.Admin)) { + router.navigate(['/admin/dashboard']); + return false; + } + } else if (url.includes('vendor')) { + if (authService.isPortalAuthenticated(UserRole.Vendor)) { + router.navigate(['/vendor/dashboard']); + return false; + } + } else { + if (authService.isPortalAuthenticated(UserRole.Vendor)) { + router.navigate(['/vendor/dashboard']); + return false; + } + if (authService.isPortalAuthenticated(UserRole.User)) { + router.navigate(['/user/venues']); + return false; + } + } + + return true; +}; diff --git a/client/book-my-venue/src/app/shared/guards/user-auth.guard.ts b/client/book-my-venue/src/app/shared/guards/user-auth.guard.ts new file mode 100644 index 000000000..b7ebfc5e1 --- /dev/null +++ b/client/book-my-venue/src/app/shared/guards/user-auth.guard.ts @@ -0,0 +1,16 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { UserRole } from '../enums/user-role.enum'; + +export const userAuthGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isAuthenticated() && authService.hasRole(UserRole.User)) { + return true; + } + + router.navigate(['/login']); + return false; +}; diff --git a/client/book-my-venue/src/app/shared/guards/vendor-auth.guard.ts b/client/book-my-venue/src/app/shared/guards/vendor-auth.guard.ts new file mode 100644 index 000000000..e2285fa2c --- /dev/null +++ b/client/book-my-venue/src/app/shared/guards/vendor-auth.guard.ts @@ -0,0 +1,16 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { UserRole } from '../enums/user-role.enum'; + +export const vendorAuthGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isAuthenticated() && authService.hasRole(UserRole.Vendor)) { + return true; + } + + router.navigate(['/vendor/login']); + return false; +}; diff --git a/client/book-my-venue/src/app/shared/interceptors/error.interceptor.ts b/client/book-my-venue/src/app/shared/interceptors/error.interceptor.ts new file mode 100644 index 000000000..f839d2ebc --- /dev/null +++ b/client/book-my-venue/src/app/shared/interceptors/error.interceptor.ts @@ -0,0 +1,41 @@ +import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { catchError, throwError } from 'rxjs'; +import { NotificationService } from '../services/notification.service'; +import { AuthService } from '../services/auth.service'; + +export const errorInterceptor: HttpInterceptorFn = (req, next) => { + const notification = inject(NotificationService); + const authService = inject(AuthService); + + return next(req).pipe( + catchError((error: HttpErrorResponse) => { + let message = 'An unexpected error occurred'; + + switch (error.status) { + case 0: + message = 'Unable to connect to server'; + break; + case 401: + message = 'Session expired. Please login again.'; + authService.logout(); + break; + case 403: + message = 'You do not have permission to perform this action'; + break; + case 404: + message = 'Resource not found'; + break; + case 422: + message = error.error?.message || 'Validation error'; + break; + case 500: + message = 'Server error. Please try again later.'; + break; + } + + notification.error(message); + return throwError(() => error); + }) + ); +}; diff --git a/client/book-my-venue/src/app/shared/models/api-response.model.ts b/client/book-my-venue/src/app/shared/models/api-response.model.ts new file mode 100644 index 000000000..5a3792e66 --- /dev/null +++ b/client/book-my-venue/src/app/shared/models/api-response.model.ts @@ -0,0 +1,20 @@ +export interface ApiResponse { + success: boolean; + data: T; + message: string; +} + +export interface PaginationResponse { + success: boolean; + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +export interface ApiError { + success: false; + message: string; + errors?: Record; +} diff --git a/client/book-my-venue/src/app/shared/models/auth-response.model.ts b/client/book-my-venue/src/app/shared/models/auth-response.model.ts new file mode 100644 index 000000000..a5c282e19 --- /dev/null +++ b/client/book-my-venue/src/app/shared/models/auth-response.model.ts @@ -0,0 +1,38 @@ +export interface AuthResponse { + success: boolean; + token: string; + refreshToken: string; + expiresIn: number; + user: AuthUser; +} + +export interface AuthUser { + id: string; + email: string; + role: string; + name: string; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface SignupRequest { + email: string; + password: string; + firstName: string; + lastName: string; + phone: string; + isVendor?: boolean; +} + +export interface ForgotPasswordRequest { + email: string; +} + +export interface ResetPasswordRequest { + token: string; + password: string; + confirmPassword: string; +} diff --git a/client/book-my-venue/src/app/shared/models/booking.model.ts b/client/book-my-venue/src/app/shared/models/booking.model.ts new file mode 100644 index 000000000..2817a7d36 --- /dev/null +++ b/client/book-my-venue/src/app/shared/models/booking.model.ts @@ -0,0 +1,35 @@ +import { BookingStatus } from '../enums/booking-status.enum'; + +export interface Booking { + id: string; + userId: string; + venueId: string; + venueName: string; + eventDate: string; + startTime: string; + endTime: string; + guestCount: number; + totalAmount: number; + status: BookingStatus; + notes: string; + createdAt: string; + updatedAt: string; +} + +export interface CreateBookingRequest { + venueId: string; + eventDate: string; + startTime: string; + endTime: string; + guestCount: number; + notes?: string; +} + +export interface UpdateBookingRequest { + eventDate?: string; + startTime?: string; + endTime?: string; + guestCount?: number; + status?: BookingStatus; + notes?: string; +} diff --git a/client/book-my-venue/src/app/shared/models/index.ts b/client/book-my-venue/src/app/shared/models/index.ts new file mode 100644 index 000000000..1e81418f4 --- /dev/null +++ b/client/book-my-venue/src/app/shared/models/index.ts @@ -0,0 +1,6 @@ +export * from './user.model'; +export * from './vendor.model'; +export * from './venue.model'; +export * from './booking.model'; +export * from './api-response.model'; +export * from './auth-response.model'; diff --git a/client/book-my-venue/src/app/shared/models/user.model.ts b/client/book-my-venue/src/app/shared/models/user.model.ts new file mode 100644 index 000000000..6ddb8c6c6 --- /dev/null +++ b/client/book-my-venue/src/app/shared/models/user.model.ts @@ -0,0 +1,26 @@ +export interface User { + id: string; + email: string; + firstName: string; + lastName: string; + phone: string; + avatar: string; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export interface CreateUserRequest { + email: string; + password: string; + firstName: string; + lastName: string; + phone: string; +} + +export interface UpdateUserRequest { + firstName?: string; + lastName?: string; + phone?: string; + avatar?: string; +} diff --git a/client/book-my-venue/src/app/shared/models/vendor.model.ts b/client/book-my-venue/src/app/shared/models/vendor.model.ts new file mode 100644 index 000000000..1afcef267 --- /dev/null +++ b/client/book-my-venue/src/app/shared/models/vendor.model.ts @@ -0,0 +1,30 @@ +export interface Vendor { + id: string; + email: string; + businessName: string; + contactPerson: string; + phone: string; + address: string; + logo: string; + isVerified: boolean; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export interface CreateVendorRequest { + email: string; + password: string; + businessName: string; + contactPerson: string; + phone: string; + address: string; +} + +export interface UpdateVendorRequest { + businessName?: string; + contactPerson?: string; + phone?: string; + address?: string; + logo?: string; +} diff --git a/client/book-my-venue/src/app/shared/models/venue.model.ts b/client/book-my-venue/src/app/shared/models/venue.model.ts new file mode 100644 index 000000000..4fe2aa189 --- /dev/null +++ b/client/book-my-venue/src/app/shared/models/venue.model.ts @@ -0,0 +1,64 @@ +import { VenueStatus } from '../enums/venue-status.enum'; + +export interface Venue { + id: string; + vendorId: string; + name: string; + description: string; + address: string; + city: string; + state: string; + zipCode: string; + capacity: number; + pricePerHour: number; + pricePerDay: number; + amenities: string[]; + images: string[]; + status: VenueStatus; + rating: number; + totalReviews: number; + createdAt: string; + updatedAt: string; +} + +export interface CreateVenueRequest { + name: string; + description: string; + address: string; + city: string; + state: string; + zipCode: string; + capacity: number; + pricePerHour: number; + pricePerDay: number; + amenities: string[]; + images: string[]; +} + +export interface UpdateVenueRequest { + name?: string; + description?: string; + address?: string; + city?: string; + state?: string; + zipCode?: string; + capacity?: number; + pricePerHour?: number; + pricePerDay?: number; + amenities?: string[]; + images?: string[]; + status?: VenueStatus; +} + +export interface VenueFilter { + city?: string; + minCapacity?: number; + maxCapacity?: number; + minPrice?: number; + maxPrice?: number; + amenities?: string[]; + status?: VenueStatus; + search?: string; + page?: number; + limit?: number; +} diff --git a/client/book-my-venue/src/app/shared/pipes/currency-format.pipe.ts b/client/book-my-venue/src/app/shared/pipes/currency-format.pipe.ts new file mode 100644 index 000000000..038919dcd --- /dev/null +++ b/client/book-my-venue/src/app/shared/pipes/currency-format.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'currencyFormat', standalone: true }) +export class CurrencyFormatPipe implements PipeTransform { + transform(value: number, currency = 'INR', locale = 'en-IN'): string { + if (value === null || value === undefined) return ''; + return new Intl.NumberFormat(locale, { + style: 'currency', + currency, + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }).format(value); + } +} diff --git a/client/book-my-venue/src/app/shared/pipes/date-format.pipe.ts b/client/book-my-venue/src/app/shared/pipes/date-format.pipe.ts new file mode 100644 index 000000000..c615fe757 --- /dev/null +++ b/client/book-my-venue/src/app/shared/pipes/date-format.pipe.ts @@ -0,0 +1,24 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'dateFormat', standalone: true }) +export class DateFormatPipe implements PipeTransform { + transform(value: string | Date, format: 'short' | 'long' | 'date' | 'time' = 'short'): string { + if (!value) return ''; + const date = new Date(value); + + switch (format) { + case 'short': + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + case 'long': + return date.toLocaleDateString('en-US', { + weekday: 'long', month: 'long', day: 'numeric', year: 'numeric', + }); + case 'date': + return date.toLocaleDateString('en-US'); + case 'time': + return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); + default: + return date.toLocaleDateString('en-US'); + } + } +} diff --git a/client/book-my-venue/src/app/shared/services/auth.repository.ts b/client/book-my-venue/src/app/shared/services/auth.repository.ts new file mode 100644 index 000000000..e22589673 --- /dev/null +++ b/client/book-my-venue/src/app/shared/services/auth.repository.ts @@ -0,0 +1,68 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../constants/api-endpoints.constant'; +import { + AuthResponse, + LoginRequest, + SignupRequest, + ForgotPasswordRequest, + ResetPasswordRequest, +} from '../models/auth-response.model'; +import { ApiResponse } from '../models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class AuthRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + login(payload: LoginRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.AUTH.LOGIN}`, + payload + ); + } + + adminLogin(payload: LoginRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.AUTH.ADMIN_LOGIN}`, + payload + ); + } + + signup(payload: SignupRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.AUTH.SIGNUP}`, + payload + ); + } + + logout(): Observable> { + return this.http.post>( + `${this.apiUrl}${API_ENDPOINTS.AUTH.LOGOUT}`, + {} + ); + } + + refreshToken(refreshToken: string): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.AUTH.REFRESH_TOKEN}`, + { refreshToken } + ); + } + + forgotPassword(payload: ForgotPasswordRequest): Observable> { + return this.http.post>( + `${this.apiUrl}${API_ENDPOINTS.AUTH.FORGOT_PASSWORD}`, + payload + ); + } + + resetPassword(payload: ResetPasswordRequest): Observable> { + return this.http.post>( + `${this.apiUrl}${API_ENDPOINTS.AUTH.RESET_PASSWORD}`, + payload + ); + } +} diff --git a/client/book-my-venue/src/app/shared/services/auth.service.ts b/client/book-my-venue/src/app/shared/services/auth.service.ts new file mode 100644 index 000000000..e7c18902c --- /dev/null +++ b/client/book-my-venue/src/app/shared/services/auth.service.ts @@ -0,0 +1,229 @@ +import { Injectable, inject, signal, computed } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthRepository } from './auth.repository'; +import { StorageService } from './storage.service'; +import { NotificationService } from './notification.service'; +import { + AuthUser, + LoginRequest, + SignupRequest, + ForgotPasswordRequest, +} from '../models/auth-response.model'; +import { UserRole } from '../enums/user-role.enum'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private readonly authRepository = inject(AuthRepository); + private readonly storage = inject(StorageService); + private readonly notification = inject(NotificationService); + private readonly router = inject(Router); + + readonly userSession = signal(null); + readonly vendorSession = signal(null); + readonly adminSession = signal(null); + + readonly currentUser = computed(() => { + const portal = this.detectPortal(); + if (portal === 'admin') return this.adminSession(); + if (portal === 'vendor') return this.vendorSession(); + return this.userSession(); + }); + + readonly isAuthenticated = computed(() => this.currentUser() !== null); + readonly loading = signal(false); + + constructor() { + this.loadAllSessions(); + } + + private portalKeys(portal: string): { token: string; refresh: string; user: string } { + return { + token: `bmv_${portal}_token`, + refresh: `bmv_${portal}_refresh_token`, + user: `bmv_${portal}_user`, + }; + } + + private detectPortal(): string { + const url = this.router.url || window.location.pathname; + if (url.startsWith('/admin')) return 'admin'; + if (url.startsWith('/vendor')) return 'vendor'; + return 'user'; + } + + private loadAllSessions(): void { + this.userSession.set(this.loadSession('user')); + this.vendorSession.set(this.loadSession('vendor')); + this.adminSession.set(this.loadSession('admin')); + } + + private loadSession(portal: string): AuthUser | null { + const keys = this.portalKeys(portal); + const token = this.storage.get(keys.token); + const userJson = this.storage.get(keys.user); + if (token && userJson) { + try { + return JSON.parse(userJson); + } catch { + this.clearPortalSession(portal); + return null; + } + } + return null; + } + + private savePortalSession(portal: string, token: string, refreshToken: string, user: AuthUser): void { + const keys = this.portalKeys(portal); + this.storage.set(keys.token, token); + this.storage.set(keys.refresh, refreshToken); + this.storage.set(keys.user, JSON.stringify(user)); + + if (portal === 'admin') this.adminSession.set(user); + else if (portal === 'vendor') this.vendorSession.set(user); + else this.userSession.set(user); + } + + private clearPortalSession(portal: string): void { + const keys = this.portalKeys(portal); + this.storage.remove(keys.token); + this.storage.remove(keys.refresh); + this.storage.remove(keys.user); + + if (portal === 'admin') this.adminSession.set(null); + else if (portal === 'vendor') this.vendorSession.set(null); + else this.userSession.set(null); + } + + isPortalAuthenticated(role: UserRole): boolean { + switch (role) { + case UserRole.Admin: return this.adminSession() !== null; + case UserRole.Vendor: return this.vendorSession() !== null; + case UserRole.User: return this.userSession() !== null; + default: return false; + } + } + + getPortalUser(role: UserRole): AuthUser | null { + switch (role) { + case UserRole.Admin: return this.adminSession(); + case UserRole.Vendor: return this.vendorSession(); + case UserRole.User: return this.userSession(); + default: return null; + } + } + + getToken(): string | null { + const portal = this.detectPortal(); + return this.storage.get(this.portalKeys(portal).token); + } + + getTokenForPortal(portal: string): string | null { + return this.storage.get(this.portalKeys(portal).token); + } + + getRefreshToken(): string | null { + const portal = this.detectPortal(); + return this.storage.get(this.portalKeys(portal).refresh); + } + + hasRole(role: UserRole): boolean { + return this.currentUser()?.role === role; + } + + login(payload: LoginRequest): void { + this.loading.set(true); + this.authRepository.login(payload).subscribe({ + next: (response) => { + const portal = response.user.role === UserRole.Vendor ? 'vendor' : 'user'; + this.savePortalSession(portal, response.token, response.refreshToken, response.user); + this.notification.success('Login successful'); + this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); + this.loading.set(false); + }, + error: () => { + this.notification.error('Invalid credentials'); + this.loading.set(false); + }, + }); + } + + adminLogin(payload: LoginRequest): void { + this.loading.set(true); + this.authRepository.adminLogin(payload).subscribe({ + next: (response) => { + this.savePortalSession('admin', response.token, response.refreshToken, response.user); + this.notification.success('Login successful'); + this.router.navigate(['/admin/dashboard']); + this.loading.set(false); + }, + error: () => { + this.notification.error('Invalid credentials'); + this.loading.set(false); + }, + }); + } + + signup(payload: SignupRequest): void { + this.loading.set(true); + this.authRepository.signup(payload).subscribe({ + next: (response) => { + const portal = response.user.role === UserRole.Vendor ? 'vendor' : 'user'; + this.savePortalSession(portal, response.token, response.refreshToken, response.user); + this.notification.success(portal === 'vendor' ? 'Vendor account created successfully' : 'Account created successfully'); + this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); + this.loading.set(false); + }, + error: () => { + this.notification.error('Signup failed. Please try again.'); + this.loading.set(false); + }, + }); + } + + forgotPassword(payload: ForgotPasswordRequest): void { + this.loading.set(true); + this.authRepository.forgotPassword(payload).subscribe({ + next: () => { + this.notification.success('Password reset email sent'); + this.loading.set(false); + }, + error: () => { + this.notification.error('Failed to send reset email'); + this.loading.set(false); + }, + }); + } + + logout(): void { + const portal = this.detectPortal(); + this.authRepository.logout().subscribe({ + complete: () => { + this.clearPortalSession(portal); + this.router.navigate(['/']); + this.notification.info('Logged out successfully'); + }, + error: () => { + this.clearPortalSession(portal); + this.router.navigate(['/']); + }, + }); + } + + refreshToken(): void { + const portal = this.detectPortal(); + const refreshToken = this.getRefreshToken(); + if (!refreshToken) { + this.clearPortalSession(portal); + return; + } + this.authRepository.refreshToken(refreshToken).subscribe({ + next: (response) => { + this.savePortalSession(portal, response.token, response.refreshToken, response.user); + }, + error: () => { + this.clearPortalSession(portal); + this.router.navigate(['/']); + }, + }); + } +} diff --git a/client/book-my-venue/src/app/shared/services/notification.service.ts b/client/book-my-venue/src/app/shared/services/notification.service.ts new file mode 100644 index 000000000..acbe6c16e --- /dev/null +++ b/client/book-my-venue/src/app/shared/services/notification.service.ts @@ -0,0 +1,41 @@ +import { Injectable, signal } from '@angular/core'; + +export interface Notification { + id: string; + type: 'success' | 'error' | 'warning' | 'info'; + message: string; + duration?: number; +} + +@Injectable({ providedIn: 'root' }) +export class NotificationService { + readonly notifications = signal([]); + + show(type: Notification['type'], message: string, duration = 5000): void { + const id = crypto.randomUUID(); + this.notifications.update(list => [...list, { id, type, message, duration }]); + if (duration > 0) { + setTimeout(() => this.dismiss(id), duration); + } + } + + success(message: string): void { + this.show('success', message); + } + + error(message: string): void { + this.show('error', message); + } + + warning(message: string): void { + this.show('warning', message); + } + + info(message: string): void { + this.show('info', message); + } + + dismiss(id: string): void { + this.notifications.update(list => list.filter(n => n.id !== id)); + } +} diff --git a/client/book-my-venue/src/app/shared/services/storage.service.ts b/client/book-my-venue/src/app/shared/services/storage.service.ts new file mode 100644 index 000000000..ab824e2c8 --- /dev/null +++ b/client/book-my-venue/src/app/shared/services/storage.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class StorageService { + get(key: string): string | null { + return localStorage.getItem(key); + } + + set(key: string, value: string): void { + localStorage.setItem(key, value); + } + + remove(key: string): void { + localStorage.removeItem(key); + } + + clear(): void { + localStorage.clear(); + } +} diff --git a/client/book-my-venue/src/app/shared/utils/validators.ts b/client/book-my-venue/src/app/shared/utils/validators.ts new file mode 100644 index 000000000..955ceb498 --- /dev/null +++ b/client/book-my-venue/src/app/shared/utils/validators.ts @@ -0,0 +1,38 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export class AppValidators { + static email(control: AbstractControl): ValidationErrors | null { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + if (!control.value) return null; + return emailRegex.test(control.value) ? null : { email: true }; + } + + static phone(control: AbstractControl): ValidationErrors | null { + const phoneRegex = /^[0-9]{10}$/; + if (!control.value) return null; + return phoneRegex.test(control.value) ? null : { phone: true }; + } + + static matchField(fieldName: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const matchControl = control.parent?.get(fieldName); + if (!matchControl) return null; + return control.value === matchControl.value ? null : { mismatch: true }; + }; + } + + static minPrice(min: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + return control.value >= min ? null : { minPrice: { min, actual: control.value } }; + }; + } + + static futureDate(control: AbstractControl): ValidationErrors | null { + if (!control.value) return null; + const selectedDate = new Date(control.value); + const today = new Date(); + today.setHours(0, 0, 0, 0); + return selectedDate >= today ? null : { futureDate: true }; + } +} diff --git a/client/book-my-venue/src/app/user/features/checkout/checkout.component.css b/client/book-my-venue/src/app/user/features/checkout/checkout.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/checkout/checkout.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/user/features/checkout/checkout.component.html b/client/book-my-venue/src/app/user/features/checkout/checkout.component.html new file mode 100644 index 000000000..c513ffe58 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/checkout/checkout.component.html @@ -0,0 +1,95 @@ +@if (loading()) { + +} @else if (venue(); as v) { +
+

Complete Your Booking

+ +
+ +
+

Booking Details

+
+
+ + +
+ + +
+ + + +
+ + +
+
+ +
+ +
+
+
+ + +
+

Summary

+
+
+

{{ v.name }}

+

{{ v.city }}, {{ v.state }}

+
+
+
+ Price per hour + {{ v.pricePerHour | currencyFormat }} +
+
+ Price per day + {{ v.pricePerDay | currencyFormat }} +
+
+
+
+ Estimated Total + {{ estimatedTotal() | currencyFormat }} +
+
+
+
+
+
+} diff --git a/client/book-my-venue/src/app/user/features/checkout/checkout.component.spec.ts b/client/book-my-venue/src/app/user/features/checkout/checkout.component.spec.ts new file mode 100644 index 000000000..00aca7d82 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/checkout/checkout.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { CheckoutComponent } from './checkout.component'; + +describe('CheckoutComponent', () => { + let component: CheckoutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CheckoutComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(CheckoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/user/features/checkout/checkout.component.ts b/client/book-my-venue/src/app/user/features/checkout/checkout.component.ts new file mode 100644 index 000000000..7c633a181 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/checkout/checkout.component.ts @@ -0,0 +1,71 @@ +import { Component, inject, OnInit, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { VenueService } from '../../services/venue.service'; +import { BookingService } from '../../services/booking.service'; +import { Venue } from '../../../shared/models/venue.model'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; +import { AppValidators } from '../../../shared/utils/validators'; + +@Component({ + selector: 'app-checkout', + standalone: true, + imports: [ReactiveFormsModule, ButtonComponent, InputComponent, LoaderComponent, CurrencyFormatPipe], + templateUrl: './checkout.component.html', + styleUrl: './checkout.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CheckoutComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly fb = inject(FormBuilder); + private readonly venueService = inject(VenueService); + private readonly bookingService = inject(BookingService); + + readonly venue = signal(null); + readonly loading = signal(true); + readonly submitting = signal(false); + + readonly form = this.fb.nonNullable.group({ + eventDate: ['', [Validators.required, AppValidators.futureDate]], + startTime: ['', [Validators.required]], + endTime: ['', [Validators.required]], + guestCount: [1, [Validators.required, Validators.min(1)]], + notes: [''], + }); + + readonly estimatedTotal = computed(() => { + const v = this.venue(); + if (!v) return 0; + return v.pricePerHour; + }); + + ngOnInit(): void { + const venueId = this.route.snapshot.paramMap.get('venueId'); + if (venueId) { + this.venueService.loadVenueById(venueId).subscribe({ + next: (venue) => { + this.venue.set(venue); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } + } + + onSubmit(): void { + if (this.form.valid && this.venue()) { + this.submitting.set(true); + this.bookingService.createBooking({ + venueId: this.venue()!.id, + ...this.form.getRawValue(), + guestCount: Number(this.form.getRawValue().guestCount), + }); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.css b/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.html b/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.html new file mode 100644 index 000000000..025246149 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.html @@ -0,0 +1,37 @@ +
+
+
+

Forgot Password

+

Enter your email to receive a reset link

+
+ +
+
+
+ +
+ +
+ +
+
+ +

+ Remember your password? + Sign in +

+
+
+
diff --git a/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.spec.ts b/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.spec.ts new file mode 100644 index 000000000..f6ac6bd1e --- /dev/null +++ b/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { ForgotPasswordComponent } from './forgot-password.component'; + +describe('User ForgotPasswordComponent', () => { + let component: ForgotPasswordComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForgotPasswordComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(ForgotPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.ts b/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.ts new file mode 100644 index 000000000..efcfed4ce --- /dev/null +++ b/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.ts @@ -0,0 +1,33 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-user-forgot-password', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './forgot-password.component.html', + styleUrl: './forgot-password.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ForgotPasswordComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + + readonly loading = this.authService.loading; + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + }); + + onSubmit(): void { + if (this.form.valid) { + this.authService.forgotPassword(this.form.getRawValue()); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/user/features/login/login.component.css b/client/book-my-venue/src/app/user/features/login/login.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/login/login.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/user/features/login/login.component.html b/client/book-my-venue/src/app/user/features/login/login.component.html new file mode 100644 index 000000000..a604d99a1 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/login/login.component.html @@ -0,0 +1,50 @@ +
+
+
+

Welcome Back

+

Sign in to your account to continue

+
+ +
+
+
+ + + +
+ + + + + + +

+ Don't have an account? + Sign up +

+
+
+
diff --git a/client/book-my-venue/src/app/user/features/login/login.component.spec.ts b/client/book-my-venue/src/app/user/features/login/login.component.spec.ts new file mode 100644 index 000000000..872f265e9 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/login/login.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { LoginComponent } from './login.component'; + +describe('User LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have invalid form initially', () => { + expect(component.form.valid).toBeFalse(); + }); +}); diff --git a/client/book-my-venue/src/app/user/features/login/login.component.ts b/client/book-my-venue/src/app/user/features/login/login.component.ts new file mode 100644 index 000000000..176b46935 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/login/login.component.ts @@ -0,0 +1,34 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-user-login', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './login.component.html', + styleUrl: './login.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoginComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + + readonly loading = this.authService.loading; + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]], + }); + + onSubmit(): void { + if (this.form.valid) { + this.authService.login(this.form.getRawValue()); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.css b/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.html b/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.html new file mode 100644 index 000000000..40dc9bbbf --- /dev/null +++ b/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.html @@ -0,0 +1,47 @@ +
+

My Bookings

+ + @if (loading()) { + + } @else if (bookings().length === 0) { + + } @else { +
+
+ + + + + + + + + + + + + @for (booking of bookings(); track booking.id) { + + + + + + + + + } + +
VenueDateTimeGuestsAmountStatus
{{ booking.venueName }}{{ booking.eventDate | dateFormat }}{{ booking.startTime }} - {{ booking.endTime }}{{ booking.guestCount }}{{ booking.totalAmount | currencyFormat }} + + {{ booking.status }} + +
+
+
+ } +
diff --git a/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.spec.ts b/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.spec.ts new file mode 100644 index 000000000..2297fdd4a --- /dev/null +++ b/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { MyBookingsComponent } from './my-bookings.component'; + +describe('MyBookingsComponent', () => { + let component: MyBookingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MyBookingsComponent], + providers: [provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(MyBookingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.ts b/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.ts new file mode 100644 index 000000000..ba9b7ab26 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.ts @@ -0,0 +1,46 @@ +import { Component, inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BookingService } from '../../services/booking.service'; +import { BookingStatus } from '../../../shared/enums/booking-status.enum'; +import { TableColumn } from '../../../shared/components/table/table.component'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; +import { DateFormatPipe } from '../../../shared/pipes/date-format.pipe'; +import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; + +@Component({ + selector: 'app-my-bookings', + standalone: true, + imports: [LoaderComponent, EmptyStateComponent, DateFormatPipe, CurrencyFormatPipe], + templateUrl: './my-bookings.component.html', + styleUrl: './my-bookings.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyBookingsComponent implements OnInit { + private readonly bookingService = inject(BookingService); + + readonly bookings = this.bookingService.bookings; + readonly loading = this.bookingService.loading; + + readonly columns: TableColumn[] = [ + { key: 'venueName', label: 'Venue', sortable: true }, + { key: 'eventDate', label: 'Date', sortable: true }, + { key: 'startTime', label: 'Time' }, + { key: 'guestCount', label: 'Guests' }, + { key: 'totalAmount', label: 'Amount', sortable: true }, + { key: 'status', label: 'Status', sortable: true }, + ]; + + ngOnInit(): void { + this.bookingService.loadUserBookings(); + } + + getStatusClass(status: string): string { + switch (status) { + case BookingStatus.Confirmed: return 'bg-green-100 text-green-700'; + case BookingStatus.Pending: return 'bg-yellow-100 text-yellow-700'; + case BookingStatus.Cancelled: return 'bg-red-100 text-red-700'; + case BookingStatus.Completed: return 'bg-blue-100 text-blue-700'; + default: return 'bg-gray-100 text-gray-700'; + } + } +} diff --git a/client/book-my-venue/src/app/user/features/profile/profile.component.css b/client/book-my-venue/src/app/user/features/profile/profile.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/profile/profile.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/user/features/profile/profile.component.html b/client/book-my-venue/src/app/user/features/profile/profile.component.html new file mode 100644 index 000000000..d5fda1dee --- /dev/null +++ b/client/book-my-venue/src/app/user/features/profile/profile.component.html @@ -0,0 +1,52 @@ +
+

My Profile

+ +
+
+
+ + {{ authService.currentUser()?.name?.charAt(0) || 'U' }} + +
+
+

{{ authService.currentUser()?.name }}

+

{{ authService.currentUser()?.email }}

+
+
+ +
+
+
+ + +
+ + +
+ +
+ +
+
+
+
diff --git a/client/book-my-venue/src/app/user/features/profile/profile.component.spec.ts b/client/book-my-venue/src/app/user/features/profile/profile.component.spec.ts new file mode 100644 index 000000000..102bf8b02 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/profile/profile.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { ProfileComponent } from './profile.component'; + +describe('User ProfileComponent', () => { + let component: ProfileComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProfileComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(ProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/user/features/profile/profile.component.ts b/client/book-my-venue/src/app/user/features/profile/profile.component.ts new file mode 100644 index 000000000..1c079055a --- /dev/null +++ b/client/book-my-venue/src/app/user/features/profile/profile.component.ts @@ -0,0 +1,57 @@ +import { Component, inject, signal, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { UserService } from '../../services/user.service'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; +import { AppValidators } from '../../../shared/utils/validators'; + +@Component({ + selector: 'app-profile', + standalone: true, + imports: [ReactiveFormsModule, ButtonComponent, InputComponent], + templateUrl: './profile.component.html', + styleUrl: './profile.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProfileComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly userService = inject(UserService); + readonly authService = inject(AuthService); + + readonly loading = signal(false); + readonly saving = signal(false); + + readonly form = this.fb.nonNullable.group({ + firstName: ['', [Validators.required]], + lastName: ['', [Validators.required]], + phone: ['', [Validators.required, AppValidators.phone]], + }); + + ngOnInit(): void { + this.loading.set(true); + this.userService.loadProfile().subscribe({ + next: (user) => { + this.form.patchValue({ + firstName: user.firstName, + lastName: user.lastName, + phone: user.phone, + }); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } + + onSubmit(): void { + if (this.form.valid) { + this.saving.set(true); + this.userService.updateProfile(this.form.getRawValue()).subscribe({ + next: () => this.saving.set(false), + error: () => this.saving.set(false), + }); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/user/features/signup/signup.component.css b/client/book-my-venue/src/app/user/features/signup/signup.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/signup/signup.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/user/features/signup/signup.component.html b/client/book-my-venue/src/app/user/features/signup/signup.component.html new file mode 100644 index 000000000..63053f67c --- /dev/null +++ b/client/book-my-venue/src/app/user/features/signup/signup.component.html @@ -0,0 +1,93 @@ +
+
+
+

Create Account

+

Join BookMyVenue to find perfect venues

+
+ +
+
+
+
+ + +
+ + + + + + + + +
+ +
+ + +
+ +
+ +
+
+ +

+ Already have an account? + Sign in +

+
+
+
diff --git a/client/book-my-venue/src/app/user/features/signup/signup.component.spec.ts b/client/book-my-venue/src/app/user/features/signup/signup.component.spec.ts new file mode 100644 index 000000000..dfa81e137 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/signup/signup.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { SignupComponent } from './signup.component'; + +describe('User SignupComponent', () => { + let component: SignupComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SignupComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(SignupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/user/features/signup/signup.component.ts b/client/book-my-venue/src/app/user/features/signup/signup.component.ts new file mode 100644 index 000000000..e3cbce2ea --- /dev/null +++ b/client/book-my-venue/src/app/user/features/signup/signup.component.ts @@ -0,0 +1,41 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; +import { AppValidators } from '../../../shared/utils/validators'; + +@Component({ + selector: 'app-user-signup', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './signup.component.html', + styleUrl: './signup.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SignupComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + + readonly loading = this.authService.loading; + + readonly form = this.fb.nonNullable.group({ + firstName: ['', [Validators.required]], + lastName: ['', [Validators.required]], + email: ['', [Validators.required, Validators.email]], + phone: ['', [Validators.required, AppValidators.phone]], + password: ['', [Validators.required, Validators.minLength(6)]], + confirmPassword: ['', [Validators.required, AppValidators.matchField('password')]], + isVendor: [false], + }); + + onSubmit(): void { + if (this.form.valid) { + const { confirmPassword, ...payload } = this.form.getRawValue(); + this.authService.signup(payload); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.css b/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.html b/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.html new file mode 100644 index 000000000..112860bf2 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.html @@ -0,0 +1,80 @@ +@if (loading()) { + +} @else if (venue(); as v) { +
+ +
+
+ @if (v.images.length > 0) { + + } +
+ @if (v.images.length > 1) { +
+ @for (img of v.images; track $index) { + + } +
+ } +
+ +
+ +
+
+

{{ v.name }}

+

{{ v.address }}, {{ v.city }}, {{ v.state }} {{ v.zipCode }}

+
+ +
+
+ + + + {{ v.rating }} + ({{ v.totalReviews }} reviews) +
+ | + Up to {{ v.capacity }} guests +
+ +
+

About this venue

+

{{ v.description }}

+
+ +
+

Amenities

+
+ @for (amenity of v.amenities; track amenity) { + {{ amenity }} + } +
+
+
+ + +
+
+
{{ v.pricePerHour | currencyFormat }}
+

per hour

+
+
+
{{ v.pricePerDay | currencyFormat }}
+

per day

+
+ +
+
+
+} diff --git a/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.spec.ts b/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.spec.ts new file mode 100644 index 000000000..e072a7d82 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { VenueDetailsComponent } from './venue-details.component'; + +describe('VenueDetailsComponent', () => { + let component: VenueDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VenueDetailsComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(VenueDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.ts b/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.ts new file mode 100644 index 000000000..1fba131fa --- /dev/null +++ b/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.ts @@ -0,0 +1,49 @@ +import { Component, inject, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { VenueService } from '../../services/venue.service'; +import { Venue } from '../../../shared/models/venue.model'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; + +@Component({ + selector: 'app-venue-details', + standalone: true, + imports: [LoaderComponent, ButtonComponent, CurrencyFormatPipe], + templateUrl: './venue-details.component.html', + styleUrl: './venue-details.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VenueDetailsComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly venueService = inject(VenueService); + + readonly venue = signal(null); + readonly loading = signal(true); + readonly selectedImage = signal(0); + + ngOnInit(): void { + const id = this.route.snapshot.paramMap.get('id'); + if (id) { + this.venueService.loadVenueById(id).subscribe({ + next: (venue) => { + this.venue.set(venue); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } + } + + selectImage(index: number): void { + this.selectedImage.set(index); + } + + onBookNow(): void { + const venue = this.venue(); + if (venue) { + this.router.navigate(['/user/checkout', venue.id]); + } + } +} diff --git a/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.css b/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.html b/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.html new file mode 100644 index 000000000..da8f7ddac --- /dev/null +++ b/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.html @@ -0,0 +1,75 @@ +
+
+

Browse Venues

+
+ + + + +
+
+ + @if (loading()) { + + } @else if (venues().length === 0) { + + } @else { + + +
+ +
+ } +
diff --git a/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.spec.ts b/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.spec.ts new file mode 100644 index 000000000..00ea71791 --- /dev/null +++ b/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { VenueListComponent } from './venue-list.component'; + +describe('VenueListComponent', () => { + let component: VenueListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VenueListComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(VenueListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.ts b/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.ts new file mode 100644 index 000000000..0d167ae4c --- /dev/null +++ b/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.ts @@ -0,0 +1,42 @@ +import { Component, inject, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { VenueService } from '../../services/venue.service'; +import { Venue } from '../../../shared/models/venue.model'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; +import { PaginationComponent } from '../../../shared/components/pagination/pagination.component'; +import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; + +@Component({ + selector: 'app-venue-list', + standalone: true, + imports: [RouterLink, FormsModule, LoaderComponent, EmptyStateComponent, PaginationComponent, CurrencyFormatPipe], + templateUrl: './venue-list.component.html', + styleUrl: './venue-list.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VenueListComponent implements OnInit { + private readonly venueService = inject(VenueService); + + readonly venues = this.venueService.venues; + readonly loading = this.venueService.loading; + readonly totalPages = this.venueService.totalPages; + readonly currentPage = signal(1); + readonly searchQuery = signal(''); + + ngOnInit(): void { + this.venueService.loadVenues({ page: 1, limit: 12 }); + } + + onSearch(query: string): void { + this.searchQuery.set(query); + this.currentPage.set(1); + this.venueService.loadVenues({ search: query, page: 1, limit: 12 }); + } + + onPageChange(page: number): void { + this.currentPage.set(page); + this.venueService.loadVenues({ search: this.searchQuery(), page, limit: 12 }); + } +} diff --git a/client/book-my-venue/src/app/user/guards/user-auth.guard.ts b/client/book-my-venue/src/app/user/guards/user-auth.guard.ts new file mode 100644 index 000000000..2772e4cbd --- /dev/null +++ b/client/book-my-venue/src/app/user/guards/user-auth.guard.ts @@ -0,0 +1,16 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../../shared/services/auth.service'; +import { UserRole } from '../../shared/enums/user-role.enum'; + +export const userAuthGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isPortalAuthenticated(UserRole.User)) { + return true; + } + + router.navigate(['/login']); + return false; +}; diff --git a/client/book-my-venue/src/app/user/interceptors/user-auth.interceptor.ts b/client/book-my-venue/src/app/user/interceptors/user-auth.interceptor.ts new file mode 100644 index 000000000..b3ab97cbe --- /dev/null +++ b/client/book-my-venue/src/app/user/interceptors/user-auth.interceptor.ts @@ -0,0 +1,19 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { AuthService } from '../../shared/services/auth.service'; + +export const userAuthInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(AuthService); + const token = authService.getToken(); + + if (token) { + const cloned = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}`, + }, + }); + return next(cloned); + } + + return next(req); +}; diff --git a/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.css b/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.css new file mode 100644 index 000000000..ca6b14ddc --- /dev/null +++ b/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.css @@ -0,0 +1,4 @@ +:host { + display: block; + height: 100vh; +} diff --git a/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.html b/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.html new file mode 100644 index 000000000..75d6db5e3 --- /dev/null +++ b/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.html @@ -0,0 +1,96 @@ +
+ + + + +
+ +
+
+

Welcome back

+

{{ currentUser()?.name }}

+
+
+ +
+ + {{ currentUser()?.name?.charAt(0) || 'U' }} + +
+
+
+ + +
+ +
+ + +
+

© 2026 BookMyVenue. All rights reserved.

+
+
+
diff --git a/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.spec.ts b/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.spec.ts new file mode 100644 index 000000000..159e0a0b4 --- /dev/null +++ b/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { UserLayoutComponent } from './user-layout.component'; + +describe('UserLayoutComponent', () => { + let component: UserLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserLayoutComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(UserLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.ts b/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.ts new file mode 100644 index 000000000..1a244932c --- /dev/null +++ b/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.ts @@ -0,0 +1,32 @@ +import { Component, inject, signal, ChangeDetectionStrategy } from '@angular/core'; +import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; + +@Component({ + selector: 'app-user-layout', + standalone: true, + imports: [RouterOutlet, RouterLink, RouterLinkActive], + templateUrl: './user-layout.component.html', + styleUrl: './user-layout.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UserLayoutComponent { + private readonly authService = inject(AuthService); + + readonly currentUser = this.authService.currentUser; + readonly sidebarOpen = signal(true); + + readonly navItems = [ + { label: 'Browse Venues', path: '/user/venues', icon: 'search' }, + { label: 'My Bookings', path: '/user/my-bookings', icon: 'calendar' }, + { label: 'Profile', path: '/user/profile', icon: 'user' }, + ]; + + toggleSidebar(): void { + this.sidebarOpen.update(v => !v); + } + + logout(): void { + this.authService.logout(); + } +} diff --git a/client/book-my-venue/src/app/user/models/index.ts b/client/book-my-venue/src/app/user/models/index.ts new file mode 100644 index 000000000..43919a261 --- /dev/null +++ b/client/book-my-venue/src/app/user/models/index.ts @@ -0,0 +1,3 @@ +export * from '../../shared/models/user.model'; +export * from '../../shared/models/venue.model'; +export * from '../../shared/models/booking.model'; diff --git a/client/book-my-venue/src/app/user/repositories/booking.repository.ts b/client/book-my-venue/src/app/user/repositories/booking.repository.ts new file mode 100644 index 000000000..19da54613 --- /dev/null +++ b/client/book-my-venue/src/app/user/repositories/booking.repository.ts @@ -0,0 +1,39 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { Booking, CreateBookingRequest } from '../../shared/models/booking.model'; +import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class BookingRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getUserBookings(): Observable> { + return this.http.get>( + `${this.apiUrl}${API_ENDPOINTS.BOOKINGS.BASE}` + ); + } + + getBookingById(id: string): Observable> { + return this.http.get>( + `${this.apiUrl}${API_ENDPOINTS.BOOKINGS.BY_ID(id)}` + ); + } + + createBooking(payload: CreateBookingRequest): Observable> { + return this.http.post>( + `${this.apiUrl}${API_ENDPOINTS.BOOKINGS.BASE}`, + payload + ); + } + + cancelBooking(id: string): Observable> { + return this.http.patch>( + `${this.apiUrl}${API_ENDPOINTS.BOOKINGS.BY_ID(id)}`, + { status: 'cancelled' } + ); + } +} diff --git a/client/book-my-venue/src/app/user/repositories/user.repository.ts b/client/book-my-venue/src/app/user/repositories/user.repository.ts new file mode 100644 index 000000000..654376e4e --- /dev/null +++ b/client/book-my-venue/src/app/user/repositories/user.repository.ts @@ -0,0 +1,26 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { User, UpdateUserRequest } from '../../shared/models/user.model'; +import { ApiResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class UserRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getProfile(): Observable> { + return this.http.get>( + `${this.apiUrl}${API_ENDPOINTS.USERS.PROFILE}` + ); + } + + updateProfile(payload: UpdateUserRequest): Observable> { + return this.http.put>( + `${this.apiUrl}${API_ENDPOINTS.USERS.PROFILE}`, + payload + ); + } +} diff --git a/client/book-my-venue/src/app/user/repositories/venue.repository.ts b/client/book-my-venue/src/app/user/repositories/venue.repository.ts new file mode 100644 index 000000000..fa5468746 --- /dev/null +++ b/client/book-my-venue/src/app/user/repositories/venue.repository.ts @@ -0,0 +1,41 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { Venue, VenueFilter } from '../../shared/models/venue.model'; +import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class VenueRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getVenues(filter?: VenueFilter): Observable> { + let params = new HttpParams(); + if (filter) { + Object.entries(filter).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + params = params.set(key, String(value)); + } + }); + } + return this.http.get>( + `${this.apiUrl}${API_ENDPOINTS.VENUES.BASE}`, + { params } + ); + } + + getVenueById(id: string): Observable> { + return this.http.get>( + `${this.apiUrl}${API_ENDPOINTS.VENUES.BY_ID(id)}` + ); + } + + searchVenues(query: string): Observable> { + return this.http.get>( + `${this.apiUrl}${API_ENDPOINTS.VENUES.SEARCH}`, + { params: new HttpParams().set('q', query) } + ); + } +} diff --git a/client/book-my-venue/src/app/user/routes/user.routes.ts b/client/book-my-venue/src/app/user/routes/user.routes.ts new file mode 100644 index 000000000..3facbbd58 --- /dev/null +++ b/client/book-my-venue/src/app/user/routes/user.routes.ts @@ -0,0 +1,46 @@ +import { Routes } from '@angular/router'; +import { ROUTE_PATHS } from '../../shared/constants/route-paths.constant'; +import { userAuthGuard } from '../guards/user-auth.guard'; + +export const USER_ROUTES: Routes = [ + { + path: '', + loadComponent: () => + import('../layouts/user-layout/user-layout.component').then(m => m.UserLayoutComponent), + children: [ + { + path: ROUTE_PATHS.USER.VENUES, + loadComponent: () => + import('../features/venue-list/venue-list.component').then(m => m.VenueListComponent), + }, + { + path: ROUTE_PATHS.USER.VENUE_DETAILS, + loadComponent: () => + import('../features/venue-details/venue-details.component').then(m => m.VenueDetailsComponent), + }, + { + path: ROUTE_PATHS.USER.CHECKOUT, + canActivate: [userAuthGuard], + loadComponent: () => + import('../features/checkout/checkout.component').then(m => m.CheckoutComponent), + }, + { + path: ROUTE_PATHS.USER.MY_BOOKINGS, + canActivate: [userAuthGuard], + loadComponent: () => + import('../features/my-bookings/my-bookings.component').then(m => m.MyBookingsComponent), + }, + { + path: ROUTE_PATHS.USER.PROFILE, + canActivate: [userAuthGuard], + loadComponent: () => + import('../features/profile/profile.component').then(m => m.ProfileComponent), + }, + { + path: '', + redirectTo: ROUTE_PATHS.USER.VENUES, + pathMatch: 'full', + }, + ], + }, +]; diff --git a/client/book-my-venue/src/app/user/services/booking.service.ts b/client/book-my-venue/src/app/user/services/booking.service.ts new file mode 100644 index 000000000..ad32b89ce --- /dev/null +++ b/client/book-my-venue/src/app/user/services/booking.service.ts @@ -0,0 +1,55 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { BookingRepository } from '../repositories/booking.repository'; +import { Booking, CreateBookingRequest } from '../../shared/models/booking.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class BookingService { + private readonly bookingRepository = inject(BookingRepository); + private readonly notification = inject(NotificationService); + private readonly router = inject(Router); + + readonly bookings = signal([]); + readonly loading = signal(false); + + loadUserBookings(): void { + this.loading.set(true); + this.bookingRepository.getUserBookings().subscribe({ + next: (response) => { + this.bookings.set(response.data); + this.loading.set(false); + }, + error: () => { + this.notification.error('Failed to load bookings'); + this.loading.set(false); + }, + }); + } + + createBooking(payload: CreateBookingRequest): void { + this.bookingRepository.createBooking(payload).subscribe({ + next: () => { + this.notification.success('Booking created successfully!'); + this.router.navigate(['/user/my-bookings']); + }, + error: () => { + this.notification.error('Failed to create booking'); + }, + }); + } + + cancelBooking(id: string): void { + this.bookingRepository.cancelBooking(id).subscribe({ + next: () => { + this.bookings.update(list => + list.map(b => b.id === id ? { ...b, status: 'cancelled' as Booking['status'] } : b) + ); + this.notification.success('Booking cancelled'); + }, + error: () => { + this.notification.error('Failed to cancel booking'); + }, + }); + } +} diff --git a/client/book-my-venue/src/app/user/services/user.service.ts b/client/book-my-venue/src/app/user/services/user.service.ts new file mode 100644 index 000000000..6972c1a04 --- /dev/null +++ b/client/book-my-venue/src/app/user/services/user.service.ts @@ -0,0 +1,30 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { Observable, map, tap } from 'rxjs'; +import { UserRepository } from '../repositories/user.repository'; +import { User, UpdateUserRequest } from '../../shared/models/user.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class UserService { + private readonly userRepository = inject(UserRepository); + private readonly notification = inject(NotificationService); + + readonly profile = signal(null); + + loadProfile(): Observable { + return this.userRepository.getProfile().pipe( + map((response) => response.data), + tap((user) => this.profile.set(user)) + ); + } + + updateProfile(payload: UpdateUserRequest): Observable { + return this.userRepository.updateProfile(payload).pipe( + map((response) => response.data), + tap((user) => { + this.profile.set(user); + this.notification.success('Profile updated successfully'); + }) + ); + } +} diff --git a/client/book-my-venue/src/app/user/services/venue.service.ts b/client/book-my-venue/src/app/user/services/venue.service.ts new file mode 100644 index 000000000..09663bd59 --- /dev/null +++ b/client/book-my-venue/src/app/user/services/venue.service.ts @@ -0,0 +1,38 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { Observable, map, tap } from 'rxjs'; +import { VenueRepository } from '../repositories/venue.repository'; +import { Venue, VenueFilter } from '../../shared/models/venue.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class VenueService { + private readonly venueRepository = inject(VenueRepository); + private readonly notification = inject(NotificationService); + + readonly venues = signal([]); + readonly loading = signal(false); + readonly totalPages = signal(1); + readonly totalItems = signal(0); + + loadVenues(filter?: VenueFilter): void { + this.loading.set(true); + this.venueRepository.getVenues(filter).subscribe({ + next: (response) => { + this.venues.set(response.data); + this.totalPages.set(response.totalPages); + this.totalItems.set(response.total); + this.loading.set(false); + }, + error: () => { + this.notification.error('Failed to load venues'); + this.loading.set(false); + }, + }); + } + + loadVenueById(id: string): Observable { + return this.venueRepository.getVenueById(id).pipe( + map((response) => response.data) + ); + } +} diff --git a/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.css b/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.html b/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.html new file mode 100644 index 000000000..5d19c8be0 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.html @@ -0,0 +1,41 @@ +
+
+

Analytics

+
+ @for (p of ['week', 'month', 'year']; track p) { + + } +
+
+ +
+
+

Revenue Overview

+
+

Chart placeholder — integrate with a charting library

+
+
+
+

Booking Trends

+
+

Chart placeholder — integrate with a charting library

+
+
+
+

Top Venues

+
+

Chart placeholder — integrate with a charting library

+
+
+
+

Occupancy Rate

+
+

Chart placeholder — integrate with a charting library

+
+
+
+
diff --git a/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.spec.ts b/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.spec.ts new file mode 100644 index 000000000..cc4b08170 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.spec.ts @@ -0,0 +1,10 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AnalyticsComponent } from './analytics.component'; +describe('Vendor AnalyticsComponent', () => { + let component: AnalyticsComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [AnalyticsComponent] }).compileComponents(); + fixture = TestBed.createComponent(AnalyticsComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.ts b/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.ts new file mode 100644 index 000000000..7c577eaad --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.ts @@ -0,0 +1,18 @@ +import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { TitleCasePipe } from '@angular/common'; + +@Component({ + selector: 'app-vendor-analytics', + standalone: true, + imports: [TitleCasePipe], + templateUrl: './analytics.component.html', + styleUrl: './analytics.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AnalyticsComponent { + readonly period = signal<'week' | 'month' | 'year'>('month'); + + setPeriod(period: 'week' | 'month' | 'year'): void { + this.period.set(period); + } +} diff --git a/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.css b/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.html b/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.html new file mode 100644 index 000000000..7a62f736a --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.html @@ -0,0 +1,46 @@ +
+

Bookings

+ @if (loading()) { + + } @else if (bookings().length === 0) { + + } @else { +
+ + + + + + + + + + + + + + @for (booking of bookings(); track booking.id) { + + + + + + + + + + } + +
VenueDateTimeGuestsAmountStatusActions
{{ booking.venueName }}{{ booking.eventDate | dateFormat }}{{ booking.startTime }} - {{ booking.endTime }}{{ booking.guestCount }}{{ booking.totalAmount | currencyFormat }} + {{ booking.status }} + + @if (booking.status === 'pending') { +
+ + +
+ } +
+
+ } +
diff --git a/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.spec.ts b/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.spec.ts new file mode 100644 index 000000000..c864062b3 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.spec.ts @@ -0,0 +1,11 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { BookingsComponent } from './bookings.component'; +describe('Vendor BookingsComponent', () => { + let component: BookingsComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [BookingsComponent], providers: [provideHttpClient()] }).compileComponents(); + fixture = TestBed.createComponent(BookingsComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.ts b/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.ts new file mode 100644 index 000000000..4e0609eb9 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.ts @@ -0,0 +1,25 @@ +import { Component, inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BookingService } from '../../services/booking.service'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; +import { DateFormatPipe } from '../../../shared/pipes/date-format.pipe'; +import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; + +@Component({ + selector: 'app-vendor-bookings', + standalone: true, + imports: [LoaderComponent, EmptyStateComponent, DateFormatPipe, CurrencyFormatPipe], + templateUrl: './bookings.component.html', + styleUrl: './bookings.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BookingsComponent implements OnInit { + private readonly bookingService = inject(BookingService); + readonly bookings = this.bookingService.bookings; + readonly loading = this.bookingService.loading; + + ngOnInit(): void { this.bookingService.loadVendorBookings(); } + + onApprove(id: string): void { this.bookingService.updateBookingStatus(id, 'confirmed'); } + onReject(id: string): void { this.bookingService.updateBookingStatus(id, 'rejected'); } +} diff --git a/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.css b/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.html b/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.html new file mode 100644 index 000000000..ef9ba3d7c --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.html @@ -0,0 +1,39 @@ +
+

Create New Venue

+ +
+
+
+ + +
+ + +
+ + + +
+ + + +
+ +
+ + + +
+ + +
+ +
+ + +
+
+
+
diff --git a/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.spec.ts b/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.spec.ts new file mode 100644 index 000000000..2bfade49d --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.spec.ts @@ -0,0 +1,13 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { CreateVenueComponent } from './create-venue.component'; +describe('CreateVenueComponent', () => { + let component: CreateVenueComponent; + let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [CreateVenueComponent], providers: [provideRouter([]), provideHttpClient()] }).compileComponents(); + fixture = TestBed.createComponent(CreateVenueComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.ts b/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.ts new file mode 100644 index 000000000..ae0ee6585 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.ts @@ -0,0 +1,55 @@ +import { Component, inject, signal, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { VenueService } from '../../services/venue.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-create-venue', + standalone: true, + imports: [ReactiveFormsModule, ButtonComponent, InputComponent], + templateUrl: './create-venue.component.html', + styleUrl: './create-venue.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CreateVenueComponent { + private readonly fb = inject(FormBuilder); + private readonly venueService = inject(VenueService); + readonly router = inject(Router); + + readonly saving = signal(false); + + readonly form = this.fb.nonNullable.group({ + name: ['', [Validators.required]], + description: ['', [Validators.required, Validators.minLength(20)]], + address: ['', [Validators.required]], + city: ['', [Validators.required]], + state: ['', [Validators.required]], + zipCode: ['', [Validators.required]], + capacity: [0, [Validators.required, Validators.min(1)]], + pricePerHour: [0, [Validators.required, Validators.min(1)]], + pricePerDay: [0, [Validators.required, Validators.min(1)]], + amenities: [''], + }); + + onSubmit(): void { + if (this.form.valid) { + this.saving.set(true); + const raw = this.form.getRawValue(); + this.venueService.createVenue({ + ...raw, + amenities: raw.amenities.split(',').map(a => a.trim()).filter(Boolean), + images: [], + }).subscribe({ + next: () => { + this.saving.set(false); + this.router.navigate(['/vendor/venues']); + }, + error: () => this.saving.set(false), + }); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.css b/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.html b/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.html new file mode 100644 index 000000000..e76951395 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.html @@ -0,0 +1,97 @@ +
+
+

Dashboard

+ + + Add Venue + +
+ +
+
+
+ Total Venues +
+ + + +
+
+

{{ stats().totalVenues }}

+
+ +
+
+ Total Bookings +
+ + + +
+
+

{{ stats().totalBookings }}

+
+ +
+
+ Revenue +
+ + + +
+
+

{{ stats().totalRevenue }}

+
+ +
+
+ Pending +
+ + + +
+
+

{{ stats().pendingBookings }}

+
+
+ + +
diff --git a/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.spec.ts b/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.spec.ts new file mode 100644 index 000000000..2ace629f6 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.spec.ts @@ -0,0 +1,19 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { DashboardComponent } from './dashboard.component'; + +describe('Vendor DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DashboardComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.ts b/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.ts new file mode 100644 index 000000000..5972372eb --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.ts @@ -0,0 +1,33 @@ +import { Component, inject, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { VendorService } from '../../services/vendor.service'; + +@Component({ + selector: 'app-vendor-dashboard', + standalone: true, + imports: [RouterLink], + templateUrl: './dashboard.component.html', + styleUrl: './dashboard.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardComponent implements OnInit { + private readonly vendorService = inject(VendorService); + + readonly stats = signal({ + totalVenues: 0, + totalBookings: 0, + totalRevenue: 0, + pendingBookings: 0, + }); + readonly loading = signal(true); + + ngOnInit(): void { + this.vendorService.loadDashboardStats().subscribe({ + next: (data) => { + this.stats.set(data); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } +} diff --git a/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.css b/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.html b/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.html new file mode 100644 index 000000000..5715c6ae9 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.html @@ -0,0 +1,34 @@ +@if (loading()) { + +} @else { +
+

Edit Venue

+
+
+
+ +
+ + +
+ +
+ + + +
+
+ + + +
+ +
+
+ + +
+
+
+
+} diff --git a/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts b/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts new file mode 100644 index 000000000..3e932620e --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts @@ -0,0 +1,12 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { EditVenueComponent } from './edit-venue.component'; +describe('EditVenueComponent', () => { + let component: EditVenueComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [EditVenueComponent], providers: [provideRouter([]), provideHttpClient()] }).compileComponents(); + fixture = TestBed.createComponent(EditVenueComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.ts b/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.ts new file mode 100644 index 000000000..3401a7e07 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.ts @@ -0,0 +1,73 @@ +import { Component, inject, signal, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { VenueService } from '../../services/venue.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; + +@Component({ + selector: 'app-edit-venue', + standalone: true, + imports: [ReactiveFormsModule, ButtonComponent, InputComponent, LoaderComponent], + templateUrl: './edit-venue.component.html', + styleUrl: './edit-venue.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EditVenueComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + readonly router = inject(Router); + private readonly fb = inject(FormBuilder); + private readonly venueService = inject(VenueService); + + readonly loading = signal(true); + readonly saving = signal(false); + + readonly form = this.fb.nonNullable.group({ + name: ['', [Validators.required]], + description: ['', [Validators.required]], + address: ['', [Validators.required]], + city: ['', [Validators.required]], + state: ['', [Validators.required]], + zipCode: ['', [Validators.required]], + capacity: [0, [Validators.required, Validators.min(1)]], + pricePerHour: [0, [Validators.required, Validators.min(1)]], + pricePerDay: [0, [Validators.required, Validators.min(1)]], + amenities: [''], + }); + + private venueId = ''; + + ngOnInit(): void { + this.venueId = this.route.snapshot.paramMap.get('id') || ''; + if (this.venueId) { + this.venueService.loadVenueById(this.venueId).subscribe({ + next: (venue) => { + this.form.patchValue({ + ...venue, + amenities: venue.amenities.join(', '), + }); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } + } + + onSubmit(): void { + if (this.form.valid) { + this.saving.set(true); + const raw = this.form.getRawValue(); + this.venueService.updateVenue(this.venueId, { + ...raw, + amenities: raw.amenities.split(',').map(a => a.trim()).filter(Boolean), + }).subscribe({ + next: () => { + this.saving.set(false); + this.router.navigate(['/vendor/venues']); + }, + error: () => this.saving.set(false), + }); + } + } +} diff --git a/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.css b/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.html b/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.html new file mode 100644 index 000000000..01d970ca9 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.html @@ -0,0 +1,20 @@ +
+
+
+

Forgot Password

+

We'll send a reset link to your email

+
+
+
+ +
+ +
+ +

+ Back to login +

+
+
+
diff --git a/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts b/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts new file mode 100644 index 000000000..6614c9376 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts @@ -0,0 +1,19 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { ForgotPasswordComponent } from './forgot-password.component'; + +describe('Vendor ForgotPasswordComponent', () => { + let component: ForgotPasswordComponent; + let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForgotPasswordComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + fixture = TestBed.createComponent(ForgotPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.ts b/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.ts new file mode 100644 index 000000000..e35fd3ae6 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.ts @@ -0,0 +1,32 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-vendor-forgot-password', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './forgot-password.component.html', + styleUrl: './forgot-password.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ForgotPasswordComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + readonly loading = this.authService.loading; + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + }); + + onSubmit(): void { + if (this.form.valid) { + this.authService.forgotPassword(this.form.getRawValue()); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/vendor/features/login/login.component.css b/client/book-my-venue/src/app/vendor/features/login/login.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/login/login.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/login/login.component.html b/client/book-my-venue/src/app/vendor/features/login/login.component.html new file mode 100644 index 000000000..5b6b2ec88 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/login/login.component.html @@ -0,0 +1,27 @@ +
+
+
+

Vendor Login

+

Access your vendor dashboard

+
+ +
+
+
+ + +
+ + + +

+ Don't have a vendor account? + Register here +

+
+
+
diff --git a/client/book-my-venue/src/app/vendor/features/login/login.component.spec.ts b/client/book-my-venue/src/app/vendor/features/login/login.component.spec.ts new file mode 100644 index 000000000..17952ab3e --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/login/login.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { LoginComponent } from './login.component'; + +describe('Vendor LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/login/login.component.ts b/client/book-my-venue/src/app/vendor/features/login/login.component.ts new file mode 100644 index 000000000..9bcdbc0c4 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/login/login.component.ts @@ -0,0 +1,34 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-vendor-login', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './login.component.html', + styleUrl: './login.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoginComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + + readonly loading = this.authService.loading; + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]], + }); + + onSubmit(): void { + if (this.form.valid) { + this.authService.login(this.form.getRawValue()); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/vendor/features/settings/settings.component.css b/client/book-my-venue/src/app/vendor/features/settings/settings.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/settings/settings.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/settings/settings.component.html b/client/book-my-venue/src/app/vendor/features/settings/settings.component.html new file mode 100644 index 000000000..b7d01e3d0 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/settings/settings.component.html @@ -0,0 +1,17 @@ +
+

Settings

+
+

Business Information

+
+
+ + + + +
+
+ +
+
+
+
diff --git a/client/book-my-venue/src/app/vendor/features/settings/settings.component.spec.ts b/client/book-my-venue/src/app/vendor/features/settings/settings.component.spec.ts new file mode 100644 index 000000000..123e538e0 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/settings/settings.component.spec.ts @@ -0,0 +1,10 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SettingsComponent } from './settings.component'; +describe('Vendor SettingsComponent', () => { + let component: SettingsComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [SettingsComponent] }).compileComponents(); + fixture = TestBed.createComponent(SettingsComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/settings/settings.component.ts b/client/book-my-venue/src/app/vendor/features/settings/settings.component.ts new file mode 100644 index 000000000..cd14b5cff --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/settings/settings.component.ts @@ -0,0 +1,31 @@ +import { Component, inject, signal, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-vendor-settings', + standalone: true, + imports: [ReactiveFormsModule, ButtonComponent, InputComponent], + templateUrl: './settings.component.html', + styleUrl: './settings.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SettingsComponent { + private readonly fb = inject(FormBuilder); + readonly saving = signal(false); + + readonly form = this.fb.nonNullable.group({ + businessName: ['', [Validators.required]], + contactPerson: ['', [Validators.required]], + phone: ['', [Validators.required]], + address: ['', [Validators.required]], + }); + + onSubmit(): void { + if (this.form.valid) { + this.saving.set(true); + setTimeout(() => this.saving.set(false), 1000); + } + } +} diff --git a/client/book-my-venue/src/app/vendor/features/signup/signup.component.css b/client/book-my-venue/src/app/vendor/features/signup/signup.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/signup/signup.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/signup/signup.component.html b/client/book-my-venue/src/app/vendor/features/signup/signup.component.html new file mode 100644 index 000000000..cfdc9338a --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/signup/signup.component.html @@ -0,0 +1,34 @@ +
+
+
+

Vendor Registration

+

List your venues on BookMyVenue

+
+
+
+
+
+ + +
+ + + + +
+
+ +
+
+

+ Already have an account? Sign in +

+
+
+
diff --git a/client/book-my-venue/src/app/vendor/features/signup/signup.component.spec.ts b/client/book-my-venue/src/app/vendor/features/signup/signup.component.spec.ts new file mode 100644 index 000000000..4f8d8dc0b --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/signup/signup.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { SignupComponent } from './signup.component'; + +describe('Vendor SignupComponent', () => { + let component: SignupComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SignupComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + fixture = TestBed.createComponent(SignupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/signup/signup.component.ts b/client/book-my-venue/src/app/vendor/features/signup/signup.component.ts new file mode 100644 index 000000000..74ed5edba --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/signup/signup.component.ts @@ -0,0 +1,40 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; +import { AppValidators } from '../../../shared/utils/validators'; + +@Component({ + selector: 'app-vendor-signup', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './signup.component.html', + styleUrl: './signup.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SignupComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + + readonly loading = this.authService.loading; + + readonly form = this.fb.nonNullable.group({ + firstName: ['', [Validators.required]], + lastName: ['', [Validators.required]], + email: ['', [Validators.required, Validators.email]], + phone: ['', [Validators.required, AppValidators.phone]], + password: ['', [Validators.required, Validators.minLength(6)]], + confirmPassword: ['', [Validators.required, AppValidators.matchField('password')]], + }); + + onSubmit(): void { + if (this.form.valid) { + const { confirmPassword, ...payload } = this.form.getRawValue(); + this.authService.signup({ ...payload, isVendor: true }); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.css b/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.css new file mode 100644 index 000000000..5804c2c02 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.css @@ -0,0 +1 @@ +:host { display: block; } diff --git a/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.html b/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.html new file mode 100644 index 000000000..aecc78e89 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.html @@ -0,0 +1,39 @@ +
+
+

My Venues

+ + + Add Venue + +
+ + @if (loading()) { + + } @else if (venues().length === 0) { + + } @else { +
+ @for (venue of venues(); track venue.id) { +
+
+ @if (venue.images.length > 0) { + + } +
+
+
+

{{ venue.name }}

+ + {{ venue.status }} + +
+

{{ venue.city }}, {{ venue.state }}

+
+ {{ venue.pricePerHour }}/hr + Edit +
+
+
+ } +
+ } +
diff --git a/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.spec.ts b/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.spec.ts new file mode 100644 index 000000000..a7a12fa2c --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.spec.ts @@ -0,0 +1,13 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { VenueListComponent } from './venue-list.component'; +describe('Vendor VenueListComponent', () => { + let component: VenueListComponent; + let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [VenueListComponent], providers: [provideRouter([]), provideHttpClient()] }).compileComponents(); + fixture = TestBed.createComponent(VenueListComponent); component = fixture.componentInstance; fixture.detectChanges(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); +}); diff --git a/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.ts b/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.ts new file mode 100644 index 000000000..f20f4d0a0 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.ts @@ -0,0 +1,24 @@ +import { Component, inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { VenueService } from '../../services/venue.service'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; + +@Component({ + selector: 'app-vendor-venue-list', + standalone: true, + imports: [RouterLink, LoaderComponent, EmptyStateComponent], + templateUrl: './venue-list.component.html', + styleUrl: './venue-list.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VenueListComponent implements OnInit { + private readonly venueService = inject(VenueService); + + readonly venues = this.venueService.venues; + readonly loading = this.venueService.loading; + + ngOnInit(): void { + this.venueService.loadVendorVenues(); + } +} diff --git a/client/book-my-venue/src/app/vendor/guards/vendor-auth.guard.ts b/client/book-my-venue/src/app/vendor/guards/vendor-auth.guard.ts new file mode 100644 index 000000000..9ae8e0b07 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/guards/vendor-auth.guard.ts @@ -0,0 +1,15 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../../shared/services/auth.service'; +import { UserRole } from '../../shared/enums/user-role.enum'; + +export const vendorAuthGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isPortalAuthenticated(UserRole.Vendor)) { + return true; + } + router.navigate(['/login']); + return false; +}; diff --git a/client/book-my-venue/src/app/vendor/interceptors/vendor-auth.interceptor.ts b/client/book-my-venue/src/app/vendor/interceptors/vendor-auth.interceptor.ts new file mode 100644 index 000000000..2096baad5 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/interceptors/vendor-auth.interceptor.ts @@ -0,0 +1,16 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { AuthService } from '../../shared/services/auth.service'; + +export const vendorAuthInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(AuthService); + const token = authService.getToken(); + + if (token) { + const cloned = req.clone({ + setHeaders: { Authorization: `Bearer ${token}`, 'X-Portal': 'vendor' }, + }); + return next(cloned); + } + return next(req); +}; diff --git a/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css b/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css new file mode 100644 index 000000000..ca6b14ddc --- /dev/null +++ b/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css @@ -0,0 +1,4 @@ +:host { + display: block; + height: 100vh; +} diff --git a/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html b/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html new file mode 100644 index 000000000..bca58bd79 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html @@ -0,0 +1,91 @@ +
+ + + + +
+
+

Vendor Dashboard

+
+ {{ currentUser()?.name }} +
+ {{ currentUser()?.name?.charAt(0) || 'V' }} +
+
+
+ +
+ +
+ +
+

© 2026 BookMyVenue Vendor Portal

+
+
+
diff --git a/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts b/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts new file mode 100644 index 000000000..1a0a41eda --- /dev/null +++ b/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { VendorLayoutComponent } from './vendor-layout.component'; + +describe('VendorLayoutComponent', () => { + let component: VendorLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VendorLayoutComponent], + providers: [provideRouter([]), provideHttpClient()], + }).compileComponents(); + + fixture = TestBed.createComponent(VendorLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts b/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts new file mode 100644 index 000000000..5b56f70af --- /dev/null +++ b/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts @@ -0,0 +1,34 @@ +import { Component, inject, signal, ChangeDetectionStrategy } from '@angular/core'; +import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; + +@Component({ + selector: 'app-vendor-layout', + standalone: true, + imports: [RouterOutlet, RouterLink, RouterLinkActive], + templateUrl: './vendor-layout.component.html', + styleUrl: './vendor-layout.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VendorLayoutComponent { + private readonly authService = inject(AuthService); + + readonly currentUser = this.authService.currentUser; + readonly sidebarOpen = signal(true); + + readonly navItems = [ + { label: 'Dashboard', path: '/vendor/dashboard', icon: 'dashboard' }, + { label: 'My Venues', path: '/vendor/venues', icon: 'venues' }, + { label: 'Bookings', path: '/vendor/bookings', icon: 'bookings' }, + { label: 'Analytics', path: '/vendor/analytics', icon: 'analytics' }, + { label: 'Settings', path: '/vendor/settings', icon: 'settings' }, + ]; + + toggleSidebar(): void { + this.sidebarOpen.update(v => !v); + } + + logout(): void { + this.authService.logout(); + } +} diff --git a/client/book-my-venue/src/app/vendor/models/index.ts b/client/book-my-venue/src/app/vendor/models/index.ts new file mode 100644 index 000000000..d9811af81 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/models/index.ts @@ -0,0 +1,3 @@ +export * from '../../shared/models/vendor.model'; +export * from '../../shared/models/venue.model'; +export * from '../../shared/models/booking.model'; diff --git a/client/book-my-venue/src/app/vendor/repositories/analytics.repository.ts b/client/book-my-venue/src/app/vendor/repositories/analytics.repository.ts new file mode 100644 index 000000000..4f2992e46 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/repositories/analytics.repository.ts @@ -0,0 +1,22 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { ApiResponse } from '../../shared/models/api-response.model'; + +export interface AnalyticsData { + revenue: { date: string; amount: number }[]; + bookings: { date: string; count: number }[]; + topVenues: { name: string; bookings: number; revenue: number }[]; +} + +@Injectable({ providedIn: 'root' }) +export class VendorAnalyticsRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getAnalytics(period: string): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.ANALYTICS}`, { params: { period } }); + } +} diff --git a/client/book-my-venue/src/app/vendor/repositories/booking.repository.ts b/client/book-my-venue/src/app/vendor/repositories/booking.repository.ts new file mode 100644 index 000000000..6070448a6 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/repositories/booking.repository.ts @@ -0,0 +1,21 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { Booking } from '../../shared/models/booking.model'; +import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class VendorBookingRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getVendorBookings(): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.BOOKINGS.BY_VENDOR}`); + } + + updateBookingStatus(id: string, status: string): Observable> { + return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.BOOKINGS.BY_ID(id)}`, { status }); + } +} diff --git a/client/book-my-venue/src/app/vendor/repositories/vendor.repository.ts b/client/book-my-venue/src/app/vendor/repositories/vendor.repository.ts new file mode 100644 index 000000000..169d5b133 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/repositories/vendor.repository.ts @@ -0,0 +1,27 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { Vendor, UpdateVendorRequest } from '../../shared/models/vendor.model'; +import { ApiResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class VendorRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getProfile(): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.PROFILE}`); + } + + updateProfile(payload: UpdateVendorRequest): Observable> { + return this.http.put>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.PROFILE}`, payload); + } + + getDashboardStats(): Observable> { + return this.http.get>( + `${this.apiUrl}${API_ENDPOINTS.VENDORS.ANALYTICS}` + ); + } +} diff --git a/client/book-my-venue/src/app/vendor/repositories/venue.repository.ts b/client/book-my-venue/src/app/vendor/repositories/venue.repository.ts new file mode 100644 index 000000000..6198fe99b --- /dev/null +++ b/client/book-my-venue/src/app/vendor/repositories/venue.repository.ts @@ -0,0 +1,33 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { Venue, CreateVenueRequest, UpdateVenueRequest } from '../../shared/models/venue.model'; +import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class VendorVenueRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getVendorVenues(): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.BASE}/venues`); + } + + getVenueById(id: string): Observable> { + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.VENUES.BY_ID(id)}`); + } + + createVenue(payload: CreateVenueRequest): Observable> { + return this.http.post>(`${this.apiUrl}${API_ENDPOINTS.VENUES.BASE}`, payload); + } + + updateVenue(id: string, payload: UpdateVenueRequest): Observable> { + return this.http.put>(`${this.apiUrl}${API_ENDPOINTS.VENUES.BY_ID(id)}`, payload); + } + + deleteVenue(id: string): Observable> { + return this.http.delete>(`${this.apiUrl}${API_ENDPOINTS.VENUES.BY_ID(id)}`); + } +} diff --git a/client/book-my-venue/src/app/vendor/routes/vendor.routes.ts b/client/book-my-venue/src/app/vendor/routes/vendor.routes.ts new file mode 100644 index 000000000..70a1f5863 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/routes/vendor.routes.ts @@ -0,0 +1,24 @@ +import { Routes } from '@angular/router'; +import { ROUTE_PATHS } from '../../shared/constants/route-paths.constant'; +import { vendorAuthGuard } from '../guards/vendor-auth.guard'; + +export const VENDOR_ROUTES: Routes = [ + { path: ROUTE_PATHS.VENDOR.LOGIN, redirectTo: '/login', pathMatch: 'full' }, + { path: ROUTE_PATHS.VENDOR.SIGNUP, redirectTo: '/signup', pathMatch: 'full' }, + { path: ROUTE_PATHS.VENDOR.FORGOT_PASSWORD, redirectTo: '/forgot-password', pathMatch: 'full' }, + { + path: '', + loadComponent: () => import('../layouts/vendor-layout/vendor-layout.component').then(m => m.VendorLayoutComponent), + canActivate: [vendorAuthGuard], + children: [ + { path: ROUTE_PATHS.VENDOR.DASHBOARD, loadComponent: () => import('../features/dashboard/dashboard.component').then(m => m.DashboardComponent) }, + { path: ROUTE_PATHS.VENDOR.VENUES, loadComponent: () => import('../features/venue-list/venue-list.component').then(m => m.VenueListComponent) }, + { path: ROUTE_PATHS.VENDOR.CREATE_VENUE, loadComponent: () => import('../features/create-venue/create-venue.component').then(m => m.CreateVenueComponent) }, + { path: ROUTE_PATHS.VENDOR.EDIT_VENUE, loadComponent: () => import('../features/edit-venue/edit-venue.component').then(m => m.EditVenueComponent) }, + { path: ROUTE_PATHS.VENDOR.BOOKINGS, loadComponent: () => import('../features/bookings/bookings.component').then(m => m.BookingsComponent) }, + { path: ROUTE_PATHS.VENDOR.ANALYTICS, loadComponent: () => import('../features/analytics/analytics.component').then(m => m.AnalyticsComponent) }, + { path: ROUTE_PATHS.VENDOR.SETTINGS, loadComponent: () => import('../features/settings/settings.component').then(m => m.SettingsComponent) }, + { path: '', redirectTo: ROUTE_PATHS.VENDOR.DASHBOARD, pathMatch: 'full' }, + ], + }, +]; diff --git a/client/book-my-venue/src/app/vendor/services/analytics.service.ts b/client/book-my-venue/src/app/vendor/services/analytics.service.ts new file mode 100644 index 000000000..c8824bc75 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/services/analytics.service.ts @@ -0,0 +1,20 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { VendorAnalyticsRepository, AnalyticsData } from '../repositories/analytics.repository'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class AnalyticsService { + private readonly analyticsRepository = inject(VendorAnalyticsRepository); + private readonly notification = inject(NotificationService); + + readonly data = signal(null); + readonly loading = signal(false); + + loadAnalytics(period: string): void { + this.loading.set(true); + this.analyticsRepository.getAnalytics(period).subscribe({ + next: (response) => { this.data.set(response.data); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load analytics'); this.loading.set(false); }, + }); + } +} diff --git a/client/book-my-venue/src/app/vendor/services/booking.service.ts b/client/book-my-venue/src/app/vendor/services/booking.service.ts new file mode 100644 index 000000000..92856b7e4 --- /dev/null +++ b/client/book-my-venue/src/app/vendor/services/booking.service.ts @@ -0,0 +1,31 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { VendorBookingRepository } from '../repositories/booking.repository'; +import { Booking } from '../../shared/models/booking.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class BookingService { + private readonly bookingRepository = inject(VendorBookingRepository); + private readonly notification = inject(NotificationService); + + readonly bookings = signal([]); + readonly loading = signal(false); + + loadVendorBookings(): void { + this.loading.set(true); + this.bookingRepository.getVendorBookings().subscribe({ + next: (response) => { this.bookings.set(response.data); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load bookings'); this.loading.set(false); }, + }); + } + + updateBookingStatus(id: string, status: string): void { + this.bookingRepository.updateBookingStatus(id, status).subscribe({ + next: (response) => { + this.bookings.update(list => list.map(b => b.id === id ? response.data : b)); + this.notification.success(`Booking ${status}`); + }, + error: () => this.notification.error('Failed to update booking'), + }); + } +} diff --git a/client/book-my-venue/src/app/vendor/services/vendor.service.ts b/client/book-my-venue/src/app/vendor/services/vendor.service.ts new file mode 100644 index 000000000..b3ff7ae4f --- /dev/null +++ b/client/book-my-venue/src/app/vendor/services/vendor.service.ts @@ -0,0 +1,12 @@ +import { Injectable, inject } from '@angular/core'; +import { Observable, map } from 'rxjs'; +import { VendorRepository } from '../repositories/vendor.repository'; + +@Injectable({ providedIn: 'root' }) +export class VendorService { + private readonly vendorRepository = inject(VendorRepository); + + loadDashboardStats(): Observable<{ totalVenues: number; totalBookings: number; totalRevenue: number; pendingBookings: number }> { + return this.vendorRepository.getDashboardStats().pipe(map(r => r.data)); + } +} diff --git a/client/book-my-venue/src/app/vendor/services/venue.service.ts b/client/book-my-venue/src/app/vendor/services/venue.service.ts new file mode 100644 index 000000000..8ebb711fb --- /dev/null +++ b/client/book-my-venue/src/app/vendor/services/venue.service.ts @@ -0,0 +1,35 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { Observable, map } from 'rxjs'; +import { VendorVenueRepository } from '../repositories/venue.repository'; +import { Venue, CreateVenueRequest, UpdateVenueRequest } from '../../shared/models/venue.model'; +import { NotificationService } from '../../shared/services/notification.service'; +import { ApiResponse } from '../../shared/models/api-response.model'; + +@Injectable({ providedIn: 'root' }) +export class VenueService { + private readonly venueRepository = inject(VendorVenueRepository); + private readonly notification = inject(NotificationService); + + readonly venues = signal([]); + readonly loading = signal(false); + + loadVendorVenues(): void { + this.loading.set(true); + this.venueRepository.getVendorVenues().subscribe({ + next: (response) => { this.venues.set(response.data); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load venues'); this.loading.set(false); }, + }); + } + + loadVenueById(id: string): Observable { + return this.venueRepository.getVenueById(id).pipe(map(r => r.data)); + } + + createVenue(payload: CreateVenueRequest): Observable> { + return this.venueRepository.createVenue(payload); + } + + updateVenue(id: string, payload: UpdateVenueRequest): Observable> { + return this.venueRepository.updateVenue(id, payload); + } +} diff --git a/client/book-my-venue/src/index.html b/client/book-my-venue/src/index.html new file mode 100644 index 000000000..b1f267beb --- /dev/null +++ b/client/book-my-venue/src/index.html @@ -0,0 +1,13 @@ + + + + + BookMyVenue + + + + + + + + diff --git a/client/book-my-venue/src/main.ts b/client/book-my-venue/src/main.ts new file mode 100644 index 000000000..5df75f9c8 --- /dev/null +++ b/client/book-my-venue/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig) + .catch((err) => console.error(err)); diff --git a/client/book-my-venue/src/styles.css b/client/book-my-venue/src/styles.css new file mode 100644 index 000000000..90e42a541 --- /dev/null +++ b/client/book-my-venue/src/styles.css @@ -0,0 +1,43 @@ +@import "tailwindcss"; + +:root { + --color-primary: #4f46e5; + --color-primary-dark: #4338ca; + --color-primary-light: #818cf8; + --color-secondary: #0ea5e9; + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --color-neutral-50: #f8fafc; + --color-neutral-100: #f1f5f9; + --color-neutral-200: #e2e8f0; + --color-neutral-300: #cbd5e1; + --color-neutral-400: #94a3b8; + --color-neutral-500: #64748b; + --color-neutral-600: #475569; + --color-neutral-700: #334155; + --color-neutral-800: #1e293b; + --color-neutral-900: #0f172a; +} + +@layer base { + body { + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: var(--color-neutral-800); + background-color: var(--color-neutral-50); + } + + h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.25; + } + + a { + color: var(--color-primary); + text-decoration: none; + } + + a:hover { + color: var(--color-primary-dark); + } +} diff --git a/client/book-my-venue/tsconfig.app.json b/client/book-my-venue/tsconfig.app.json new file mode 100644 index 000000000..e4dd97c65 --- /dev/null +++ b/client/book-my-venue/tsconfig.app.json @@ -0,0 +1,16 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "rootDir": "./src", + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/client/book-my-venue/tsconfig.json b/client/book-my-venue/tsconfig.json new file mode 100644 index 000000000..2ab744275 --- /dev/null +++ b/client/book-my-venue/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/client/book-my-venue/tsconfig.spec.json b/client/book-my-venue/tsconfig.spec.json new file mode 100644 index 000000000..d38370633 --- /dev/null +++ b/client/book-my-venue/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "vitest/globals" + ] + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.spec.ts" + ] +} From 8e020f0a9775c59b1df60e37c9dcd093b0f930ac Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Thu, 28 May 2026 22:29:59 +0530 Subject: [PATCH 02/59] Remove .claude folder from repo and add to gitignore --- .claude/settings.local.json | 10 ---------- .gitignore | 3 +++ 2 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 93eeff778..000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "PowerShell(node --version)", - "PowerShell(npm --version)", - "PowerShell(npx @angular/cli@latest version 2>$null)", - "PowerShell(if \\(-not $?\\) { \"Angular CLI not found globally\" })" - ] - } -} diff --git a/.gitignore b/.gitignore index 2d5fa4443..271918ff8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Claude Code +.claude/ + # Old folder — contents moved to client/ /book-my-venue/ From 707f0097ee0ec34c9cdf48b9bd27c3095deff767 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Thu, 28 May 2026 23:27:58 +0530 Subject: [PATCH 03/59] chore: initialize spring boot backend structure via spring initializr --- server/.gitattributes | 2 + server/.gitignore | 33 ++ server/.mvn/wrapper/maven-wrapper.properties | 3 + server/mvnw | 295 ++++++++++++++++++ server/mvnw.cmd | 189 +++++++++++ server/pom.xml | 121 +++++++ .../bookmyvenue/server/ServerApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../server/ServerApplicationTests.java | 13 + 9 files changed, 670 insertions(+) create mode 100644 server/.gitattributes create mode 100644 server/.gitignore create mode 100644 server/.mvn/wrapper/maven-wrapper.properties create mode 100755 server/mvnw create mode 100644 server/mvnw.cmd create mode 100644 server/pom.xml create mode 100644 server/src/main/java/com/bookmyvenue/server/ServerApplication.java create mode 100644 server/src/main/resources/application.properties create mode 100644 server/src/test/java/com/bookmyvenue/server/ServerApplicationTests.java diff --git a/server/.gitattributes b/server/.gitattributes new file mode 100644 index 000000000..3b41682ac --- /dev/null +++ b/server/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 000000000..667aaef0c --- /dev/null +++ b/server/.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/server/.mvn/wrapper/maven-wrapper.properties b/server/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..216df0589 --- /dev/null +++ b/server/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip diff --git a/server/mvnw b/server/mvnw new file mode 100755 index 000000000..bd8896bf2 --- /dev/null +++ b/server/mvnw @@ -0,0 +1,295 @@ +#!/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.4 +# +# 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:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/server/mvnw.cmd b/server/mvnw.cmd new file mode 100644 index 000000000..92450f932 --- /dev/null +++ b/server/mvnw.cmd @@ -0,0 +1,189 @@ +<# : 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.4 +@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-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/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_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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/server/pom.xml b/server/pom.xml new file mode 100644 index 000000000..3519904d5 --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.14 + + + com.bookmyvenue + server + 0.0.1-SNAPSHOT + + + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + compile + + compile + + + + + org.projectlombok + lombok + + + + + + default-testCompile + test-compile + + testCompile + + + + + org.projectlombok + lombok + + + + + + + + + + diff --git a/server/src/main/java/com/bookmyvenue/server/ServerApplication.java b/server/src/main/java/com/bookmyvenue/server/ServerApplication.java new file mode 100644 index 000000000..c77811908 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/ServerApplication.java @@ -0,0 +1,13 @@ +package com.bookmyvenue.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ServerApplication { + + public static void main(String[] args) { + SpringApplication.run(ServerApplication.class, args); + } + +} diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties new file mode 100644 index 000000000..df243b894 --- /dev/null +++ b/server/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=server diff --git a/server/src/test/java/com/bookmyvenue/server/ServerApplicationTests.java b/server/src/test/java/com/bookmyvenue/server/ServerApplicationTests.java new file mode 100644 index 000000000..2aa945b37 --- /dev/null +++ b/server/src/test/java/com/bookmyvenue/server/ServerApplicationTests.java @@ -0,0 +1,13 @@ +package com.bookmyvenue.server; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ServerApplicationTests { + + @Test + void contextLoads() { + } + +} From faa27767ead9bf1a84473e42695011f8182a5aae Mon Sep 17 00:00:00 2001 From: AJAY V Date: Thu, 28 May 2026 23:34:23 +0530 Subject: [PATCH 04/59] Update .gitignore to include build and IDE files Added various build and IDE configuration files to .gitignore. --- .gitignore | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.gitignore b/.gitignore index 271918ff8..a163efa49 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,42 @@ Thumbs.db # Misc coverage/ *.log + + +# ========================================== +# ☕ Spring Boot & Maven Base Defaults +# ========================================== +**/target/ +**.target/ +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# Maven wrapper files (Kept ignored to prevent build rule conflicts) +**/mvnw +**/mvnw.cmd +**/.mvn/wrapper/maven-wrapper.properties +**/.mvn/wrapper/maven-wrapper.jar + +# ========================================== +# 💻 IDE Configurations (IntelliJ / VS Code / Eclipse) +# ========================================== +**/.idea/ +**/*.iml +**/out/ +**/.vscode/ +**/.project +**/.classpath +**/.settings/ +**/.factorypath + +# ========================================== +# 🔒 Environment & OS files +# ========================================== +**/.env +**/.env.local +**/.DS_Store +**/Thumbs.db From 2eaa8549197410afba58ee989c5c7b211fc39023 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Fri, 29 May 2026 01:23:14 +0530 Subject: [PATCH 05/59] infrastructure: complete database setup, env configs, and backend documentation --- server/.env | 3 + server/.env.example | 3 + server/README.md | 146 ++++++++++++++++++ server/docker-compose.yml | 17 ++ .../src/main/resources/application.properties | 13 +- 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 server/.env create mode 100644 server/.env.example create mode 100644 server/README.md create mode 100644 server/docker-compose.yml diff --git a/server/.env b/server/.env new file mode 100644 index 000000000..eecef2f50 --- /dev/null +++ b/server/.env @@ -0,0 +1,3 @@ +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_NAME=bookmyvenue_db \ No newline at end of file diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 000000000..ff1cc3d27 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,3 @@ +DB_USERNAME=postgres +DB_PASSWORD=type_your_password_here +DB_NAME=bookmyvenue_db \ No newline at end of file diff --git a/server/README.md b/server/README.md new file mode 100644 index 000000000..44a76f3ba --- /dev/null +++ b/server/README.md @@ -0,0 +1,146 @@ +# BookMyVenue - Backend Server + +Backend service for **BookMyVenue**, built with **Spring Boot**, **Java 21**, and **Maven**. +The project uses a **PostgreSQL** database running inside Docker for local development. + +--- + +# Tech Stack + +* **Language:** Java 21 +* **Framework:** Spring Boot 3.5.14 +* **Build Tool:** Apache Maven +* **Database:** PostgreSQL +* **Containerization:** Docker & Docker Compose + +--- + +# Prerequisites + +Before running the project, make sure the following are installed on your machine. + +## 1. Java 21 + +Check installed version: + +```bash +java -version +``` + +--- + +## 2. Docker & Docker Compose + +Check installation: + +```bash +docker --version +docker compose version +``` + +--- + +## 3. Ensure Port 5432 is Free + +If PostgreSQL is already running locally, stop it before starting Docker: + +```bash +sudo systemctl stop postgresql +``` + +--- + +# Running the Project Locally + +## Step 1: Configure Environment Variables + +Inside the `server/` directory, create a local `.env` file from the example template: + +```bash +cp .env.example .env +``` + +Open the `.env` file and verify the values: + +```env +DB_USERNAME=postgres +DB_PASSWORD=your password +DB_NAME=bookmyvenue_db +``` + + + +--- + +## Step 2: Start PostgreSQL with Docker + +From inside the `server/` directory, start the database container: + +```bash +docker compose up -d +``` + +Verify the container is running: + +```bash +docker ps +``` + +--- + +## Step 3: Run the Spring Boot Application + +You can either: + +* Click the **Run/Play** button in your IDE (IntelliJ IDEA / VS Code) + +OR + +Run the Maven wrapper command: + +```bash +./mvnw spring-boot:run +``` + +When you see the following log: + +```text +Started BookMyVenueApplication +``` + +the backend server is successfully running. + +--- + +# Stopping the Docker Container + +To stop the PostgreSQL container: + +```bash +docker compose down +``` + +> Your database data will remain safe because Docker volumes are persisted. + +--- + +# Project Structure + +```text +server/ +├── src/ +├── docker-compose.yml +├── .env.example +├── pom.xml +└── mvnw +``` + +--- + +# Notes + +* Ensure Docker Desktop/service is running before starting the project. +* Use Java 21 for compatibility. +* PostgreSQL runs inside Docker to keep local setup consistent across all developers. + +--- diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 000000000..66baf6376 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: bmv-postgres-db + environment: + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index df243b894..07a2999e9 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1 +1,12 @@ -spring.application.name=server +# =================================================================== +#DOCKERIZED POSTGRESQL CONFIGURATION +# =================================================================== +spring.datasource.url=jdbc:postgresql://localhost:5432/${DB_NAME} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=org.postgresql.Driver + +# JPA / Hibernate Properties +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file From bb472d6c5ff468437e19cece42206fdf74d88a54 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Sat, 30 May 2026 11:02:29 +0530 Subject: [PATCH 06/59] git commit -m "feature: implement JWT authentication module" --- server/.env | 8 +- server/.env.example | 8 +- server/pom.xml | 99 ++++++++++ .../auth/controller/AuthController.java | 79 ++++++++ .../server/auth/dto/request/LoginRequest.java | 30 ++++ .../auth/dto/request/RefreshTokenRequest.java | 21 +++ .../auth/dto/request/RegisterRequest.java | 40 +++++ .../auth/dto/response/AuthResponse.java | 34 ++++ .../dto/response/RefreshTokenResponse.java | 23 +++ .../server/auth/security/JwtProperties.java | 34 ++++ .../server/auth/security/JwtService.java | 121 +++++++++++++ .../server/auth/security/SecurityConfig.java | 45 +++++ .../server/auth/service/AuthService.java | 26 +++ .../server/auth/service/AuthServiceImpl.java | 130 ++++++++++++++ .../common/exception/BadRequestException.java | 8 + .../exception/GlobalExceptionHandler.java | 170 ++++++++++++++++++ .../InvalidCredentialsException.java | 8 + .../exception/ResourceNotFoundException.java | 8 + .../exception/UserAlreadyExistsException.java | 8 + .../common/response/ApiErrorResponse.java | 23 +++ .../bookmyvenue/server/user/entity/User.java | 64 +++++++ .../bookmyvenue/server/user/enums/Role.java | 10 ++ .../user/repository/UserRepository.java | 28 +++ .../src/main/resources/application.properties | 8 +- 24 files changed, 1029 insertions(+), 4 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/request/LoginRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/request/RefreshTokenRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/JwtProperties.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/JwtService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/entity/User.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/enums/Role.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java diff --git a/server/.env b/server/.env index eecef2f50..c765791db 100644 --- a/server/.env +++ b/server/.env @@ -1,3 +1,7 @@ DB_USERNAME=postgres -DB_PASSWORD=postgres -DB_NAME=bookmyvenue_db \ No newline at end of file +DB_PASSWORD=postgres123 +DB_NAME=bookmyvenue_db + +JWT_SECRET=M1hQ5gL4vM8e4uVh1sL3gXzN0V8i8cR4gG8kN2jP7sW9mT6aY3qF1rH5uK2xB9nL +JWT_ACCESS_TOKEN_EXPIRATION=900000 +JWT_REFRESH_TOKEN_EXPIRATION=604800000 \ No newline at end of file diff --git a/server/.env.example b/server/.env.example index ff1cc3d27..64a129c91 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,3 +1,9 @@ +# PostgreSQL DB_USERNAME=postgres DB_PASSWORD=type_your_password_here -DB_NAME=bookmyvenue_db \ No newline at end of file +DB_NAME=bookmyvenue_db + +# JWT +JWT_SECRET=replace_with_your_jwt_secret +JWT_ACCESS_TOKEN_EXPIRATION=900000 +JWT_REFRESH_TOKEN_EXPIRATION=604800000 \ No newline at end of file diff --git a/server/pom.xml b/server/pom.xml index 3519904d5..f8a5359b4 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -28,6 +28,7 @@ 21 + 2.3.10 @@ -43,6 +44,42 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-validation + + + io.jsonwebtoken + jjwt-api + 0.12.7 + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.9 + + + + me.paulschwarz + spring-dotenv + 4.0.0 + + + + + io.jsonwebtoken + jjwt-impl + 0.12.7 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.7 + runtime + org.postgresql postgresql @@ -63,6 +100,17 @@ spring-security-test test + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + @@ -79,6 +127,43 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + true + + + compile + compile + + compile + + + + src/main/java + target/generated-sources/annotations + + + + + test-compile + test-compile + + test-compile + + + + src/test/java + target/generated-test-sources/test-annotations + + + + + + 1.8 + + org.apache.maven.plugins maven-compiler-plugin @@ -113,6 +198,20 @@ + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + diff --git a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java new file mode 100644 index 000000000..00d5479d8 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java @@ -0,0 +1,79 @@ +package com.bookmyvenue.server.auth.controller; + +import com.bookmyvenue.server.auth.dto.request.LoginRequest; +import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; +import com.bookmyvenue.server.auth.dto.request.RegisterRequest; +import com.bookmyvenue.server.auth.dto.response.AuthResponse; +import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; +import com.bookmyvenue.server.auth.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@Tag(name = "Authentication", description = "Authentication and account management APIs") +public class AuthController { + + private final AuthService authService; + + @Operation( + summary = "Register a new account", + description = "Creates a new USER or VENDOR account and returns JWT tokens." + ) + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Registration successful"), + @ApiResponse(responseCode = "400", description = "Invalid request or ADMIN registration attempted"), + @ApiResponse(responseCode = "409", description = "Email or phone already exists") + }) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + AuthResponse response = authService.register(request); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(response); + } + + + + @Operation( + summary = "Authenticate user", + description = "Authenticates a user and returns JWT tokens." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Login successful"), + @ApiResponse(responseCode = "401", description = "Invalid credentials") + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + AuthResponse response = authService.login(request); + return ResponseEntity.ok(response); + } + + + + @Operation( + summary = "Refresh JWT tokens", + description = "Generates new access and refresh tokens using a valid refresh token." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Tokens refreshed successfully"), + @ApiResponse(responseCode = "401", description = "Invalid or expired refresh token") + }) + @PostMapping("/refresh-token") + public ResponseEntity refreshToken(@Valid @RequestBody RefreshTokenRequest request) { + + RefreshTokenResponse response = authService.refreshToken(request); + return ResponseEntity.ok(response); + } + + + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/LoginRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/LoginRequest.java new file mode 100644 index 000000000..16a1df4f0 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/LoginRequest.java @@ -0,0 +1,30 @@ +package com.bookmyvenue.server.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "User login request") +public class LoginRequest { + + @Schema( + description = "Registered email address", + example = "ajay@gmail.com" + ) + @Email(message = "Invalid email format") + @NotBlank(message = "Email is required") + private String email; + + @Schema( + description = "Account password", + example = "password123" + ) + @NotBlank(message = "Password is required") + private String password; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RefreshTokenRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RefreshTokenRequest.java new file mode 100644 index 000000000..dab88bad5 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RefreshTokenRequest.java @@ -0,0 +1,21 @@ +package com.bookmyvenue.server.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Refresh token request") +public class RefreshTokenRequest { + + @Schema( + description = "JWT refresh token", + example = "eyJhbGciOiJIUzM4NCJ9..." + ) + @NotBlank(message = "Refresh token is required") + private String refreshToken; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java new file mode 100644 index 000000000..9d289286e --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java @@ -0,0 +1,40 @@ +package com.bookmyvenue.server.auth.dto.request; + +import com.bookmyvenue.server.user.enums.Role; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "User registration request") +public class RegisterRequest { + + @Schema(description = "Full name of the user", example = "Ajay") + @NotBlank(message = "Name is required") + private String name; + + @Schema(description = "Unique email address", example = "ajay@gmail.com") + @Email(message = "Invalid email format") + @NotBlank(message = "Email is required") + private String email; + + @Schema(description = "10 digit phone number", example = "9876543210") + @Pattern(regexp = "^[0-9]{10}$", message = "Phone number must be exactly 10 digits") + @NotBlank(message = "Phone number is required") + private String phone; + + @Schema(description = "Account password", example = "password123") + @NotBlank(message = "Password is required") + private String password; + + @Schema(description = "Account role", example = "USER", allowableValues = {"USER", "VENDOR"}) + @NotNull(message = "Role is required") + private Role role; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java new file mode 100644 index 000000000..ba45f1515 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java @@ -0,0 +1,34 @@ +package com.bookmyvenue.server.auth.dto.response; + +import com.bookmyvenue.server.user.enums.Role; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Authentication response returned after successful login or registration") +public class AuthResponse { + + @Schema(description = "Unique identifier of the authenticated user", example = "82b45eec-c37b-4ef5-b5fb-921ab399cb13") + private UUID userId; + + @Schema(description = "Full name of the user", example = "Ajay") + private String name; + + @Schema(description = "Registered email address", example = "ajay@gmail.com") + private String email; + + @Schema(description = "Role assigned to the user", example = "USER") + private Role role; + + @Schema(description = "JWT access token used to access protected APIs") + private String accessToken; + + @Schema(description = "JWT refresh token used to obtain a new access token") + private String refreshToken; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java new file mode 100644 index 000000000..5b7c259c3 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java @@ -0,0 +1,23 @@ +package com.bookmyvenue.server.auth.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +/**for later scaling we are + returning both access and refreshtoken . for token rotation **/ + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Refresh token response") + +public class RefreshTokenResponse { + + @Schema(description = "New JWT access token") + private String accessToken; + + @Schema(description = "New JWT refresh token") + private String refreshToken; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/JwtProperties.java b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtProperties.java new file mode 100644 index 000000000..a9726cea8 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtProperties.java @@ -0,0 +1,34 @@ +package com.bookmyvenue.server.auth.security; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Stores JWT-related configuration loaded from application properties. + * + * Values are sourced from environment variables to keep secrets + * and token settings outside the codebase. + */ +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "app.jwt") +public class JwtProperties { + + /** + * Secret key used to sign and verify JWT tokens. + */ + private String secret; + + /** + * Access token validity duration in milliseconds. + */ + private long accessTokenExpiration; + + /** + * Refresh token validity duration in milliseconds. + */ + private long refreshTokenExpiration; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/JwtService.java b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtService.java new file mode 100644 index 000000000..feb84530c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtService.java @@ -0,0 +1,121 @@ +package com.bookmyvenue.server.auth.security; + +import com.bookmyvenue.server.user.enums.Role; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.function.Function; + +/** + * Handles JWT token generation, validation, and claim extraction. + */ +@Service +@RequiredArgsConstructor +public class JwtService { + + private final JwtProperties jwtProperties; + + /** + * Generates an access token for the given user. + */ + public String generateAccessToken(String email, Role role) { + return generateToken(email, role, jwtProperties.getAccessTokenExpiration()); + } + + /** + * Generates a refresh token for the given user. + */ + public String generateRefreshToken(String email, Role role) { + return generateToken(email, role, jwtProperties.getRefreshTokenExpiration()); + } + + /** + * Common token generation logic. + */ + private String generateToken(String email, Role role, long expiration) { + return Jwts.builder() + .subject(email) + .claim("role", role.name()) + .issuedAt(new Date()) + .expiration( + new Date( + System.currentTimeMillis() + expiration + ) + ) + .signWith(getSigningKey()) + .compact(); + } + + /** + * Extracts the email (subject) from a JWT token. + */ + public String extractEmail(String token) { + return extractClaim(token, Claims::getSubject); + } + + /** + * Extracts the role from a JWT token. + */ + public String extractRole(String token) { + return extractClaim( + token, + claims -> claims.get("role", String.class) + ); + } + + /** + * Extracts a specific claim from a token. + */ + public T extractClaim( + String token, + Function claimsResolver + ) { + Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + /** + * Validates whether a token belongs to the expected user + * and is not expired. + */ + public boolean isTokenValid(String token, String email) { + return email.equals(extractEmail(token)) + && !isTokenExpired(token); + } + + /** + * Checks whether a token has expired. + */ + private boolean isTokenExpired(String token) { + return extractClaim(token, Claims::getExpiration) + .before(new Date()); + } + + /** + * Extracts all claims from the token. + */ + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + /** + * Creates the signing key from the configured JWT secret. + */ + private SecretKey getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode( + jwtProperties.getSecret() + ); + + return Keys.hmacShaKeyFor(keyBytes); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java new file mode 100644 index 000000000..c6bc75133 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -0,0 +1,45 @@ +package com.bookmyvenue.server.auth.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + /** + * Password encoder bean used to hash and verify passwords. + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Security configuration for MVP authentication endpoints. + */ + @Bean + public SecurityFilterChain securityFilterChain( + HttpSecurity http + ) throws Exception { + + http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/api/auth/**", + "/swagger-ui/**", + "/v3/api-docs/**" + ).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form.disable()) + .httpBasic(basic -> basic.disable()); + + return http.build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java new file mode 100644 index 000000000..ad11ec32a --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java @@ -0,0 +1,26 @@ +package com.bookmyvenue.server.auth.service; + +import com.bookmyvenue.server.auth.dto.request.LoginRequest; +import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; +import com.bookmyvenue.server.auth.dto.request.RegisterRequest; +import com.bookmyvenue.server.auth.dto.response.AuthResponse; +import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; + +public interface AuthService { + + /** + * Registers a new USER or VENDOR account. + */ + AuthResponse register(RegisterRequest request); + + /** + * Authenticates a user and returns JWT tokens. + */ + AuthResponse login(LoginRequest request); + + + /** + * Generates a new access token using a valid refresh token. + */ + RefreshTokenResponse refreshToken(RefreshTokenRequest request); +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java new file mode 100644 index 000000000..06abc361d --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java @@ -0,0 +1,130 @@ +package com.bookmyvenue.server.auth.service; + +import com.bookmyvenue.server.auth.dto.request.LoginRequest; +import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; +import com.bookmyvenue.server.auth.dto.request.RegisterRequest; +import com.bookmyvenue.server.auth.dto.response.AuthResponse; +import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; +import com.bookmyvenue.server.auth.security.JwtService; +import com.bookmyvenue.server.common.exception.BadRequestException; +import com.bookmyvenue.server.common.exception.InvalidCredentialsException; +import com.bookmyvenue.server.common.exception.UserAlreadyExistsException; +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.user.enums.Role; +import com.bookmyvenue.server.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthServiceImpl implements AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + + + @Override + public AuthResponse register(RegisterRequest request) { + + log.info("Attempting registration for email: {}", request.getEmail()); + + if (userRepository.existsByEmail(request.getEmail())) { + log.warn("Registration failed. Email already exists: {}", request.getEmail()); + throw new UserAlreadyExistsException("Email already registered"); + } + + if (userRepository.existsByPhone(request.getPhone())) { + log.warn("Registration failed. Phone already exists: {}", request.getPhone()); + throw new UserAlreadyExistsException("Phone number already registered"); + } + + if (request.getRole() == Role.ADMIN) { + log.warn("Attempted ADMIN registration for email: {}", request.getEmail()); + throw new BadRequestException("Admin registration is not allowed"); + } + User user = User.builder() + .name(request.getName()) + .email(request.getEmail()) + .phone(request.getPhone()) + .password(passwordEncoder.encode(request.getPassword())) + .role(request.getRole()) + .build(); + + User savedUser = userRepository.save(user); + + String accessToken = jwtService.generateAccessToken(savedUser.getEmail(), savedUser.getRole()); + String refreshToken = jwtService.generateRefreshToken(savedUser.getEmail(), savedUser.getRole()); + + log.info("User registered successfully: {}", savedUser.getEmail()); + + return AuthResponse.builder() + .userId(savedUser.getId()) + .name(savedUser.getName()) + .email(savedUser.getEmail()) + .role(savedUser.getRole()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + + + @Override + public AuthResponse login(LoginRequest request) { + log.info("Login attempt for email: {}", request.getEmail()); + User user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> { + log.warn("Login failed. User not found: {}", request.getEmail()); + return new InvalidCredentialsException("Invalid email or password"); + }); + + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + log.warn("Login failed. Invalid password for email: {}", request.getEmail()); + throw new InvalidCredentialsException("Invalid email or password"); + } + + String accessToken = jwtService.generateAccessToken(user.getEmail(), user.getRole()); + String refreshToken = jwtService.generateRefreshToken(user.getEmail(), user.getRole()); + + log.info("User logged in successfully: {}", user.getEmail()); + + return AuthResponse.builder() + .userId(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .role(user.getRole()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + + @Override + public RefreshTokenResponse refreshToken(RefreshTokenRequest request) { + + String email = jwtService.extractEmail(request.getRefreshToken()); + User user = userRepository.findByEmail(email) + .orElseThrow(() -> + new InvalidCredentialsException( + "Invalid refresh token" + ) + ); + + if (!jwtService.isTokenValid(request.getRefreshToken(), user.getEmail() + )) { + throw new InvalidCredentialsException("Invalid refresh token"); + } + String accessToken = jwtService.generateAccessToken(user.getEmail(), user.getRole()); + String refreshToken = jwtService.generateRefreshToken(user.getEmail(), user.getRole()); + + return RefreshTokenResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java new file mode 100644 index 000000000..82789c5ac --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.common.exception; + +public class BadRequestException extends RuntimeException { + + public BadRequestException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..4f6ce6228 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,170 @@ +package com.bookmyvenue.server.common.exception; + +import com.bookmyvenue.server.common.response.ApiErrorResponse; +import io.jsonwebtoken.JwtException; +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.LocalDateTime; + +/** + * Centralized exception handler for the entire application. + * + * Converts application exceptions into consistent API error responses, + * ensuring clients receive meaningful HTTP status codes and messages. + */ +@Hidden +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * Handles attempts to create a user with an email or phone number + * that already exists in the system. + * + * Returns HTTP 409 (Conflict). + */ + @ExceptionHandler(UserAlreadyExistsException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiErrorResponse handleUserAlreadyExistsException( + UserAlreadyExistsException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.CONFLICT, + ex.getMessage(), + request + ); + } + + /** + * Handles authentication failures such as invalid email or password. + * + * Returns HTTP 401 (Unauthorized). + */ + @ExceptionHandler(InvalidCredentialsException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ApiErrorResponse handleInvalidCredentialsException( + InvalidCredentialsException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.UNAUTHORIZED, + ex.getMessage(), + request + ); + } + + /** + * Handles requests for resources that do not exist. + * + * Examples: + * - User not found + * - Venue not found + * - Booking not found + * + * Returns HTTP 404 (Not Found). + */ + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiErrorResponse handleResourceNotFoundException( + ResourceNotFoundException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.NOT_FOUND, + ex.getMessage(), + request + ); + } + + /** + * Fallback handler for unexpected application errors. + * + * Prevents internal stack traces from being exposed to API consumers + * and returns a generic error response. + * + * Returns HTTP 500 (Internal Server Error). + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiErrorResponse handleGenericException( + Exception ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "An unexpected error occurred", + request + ); + } + + /** + * Handles requests that fail business validation rules. + * + * Examples: + * - Admin self-registration attempted + * - Invalid booking request + * - Invalid venue configuration + * + * Returns HTTP 400 (Bad Request). + */ + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiErrorResponse handleBadRequestException( + BadRequestException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + ex.getMessage(), + request + ); + } + + + /** + * Handles JWT-related authentication failures. + * + * Examples: + * - Expired token + * - Invalid token signature + * - Malformed token + * - Tampered token + * + * Returns HTTP 401 (Unauthorized). + */ + @ExceptionHandler(JwtException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ApiErrorResponse handleJwtException( + JwtException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.UNAUTHORIZED, + "Invalid or expired token", + request + ); + } + + + /** + * Creates a standardized error response object used across the API. + */ + private ApiErrorResponse buildErrorResponse( + HttpStatus status, + String message, + HttpServletRequest request + ) { + return ApiErrorResponse.builder() + .status(status.value()) + .error(status.getReasonPhrase()) + .message(message) + .path(request.getRequestURI()) + .timestamp(LocalDateTime.now()) + .build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java new file mode 100644 index 000000000..358149b10 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.common.exception; + +public class InvalidCredentialsException extends RuntimeException { + + public InvalidCredentialsException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java new file mode 100644 index 000000000..0046a7a37 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.common.exception; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java new file mode 100644 index 000000000..44b2ab437 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.common.exception; + +public class UserAlreadyExistsException extends RuntimeException { + + public UserAlreadyExistsException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java b/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java new file mode 100644 index 000000000..40e6aeef1 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java @@ -0,0 +1,23 @@ +package com.bookmyvenue.server.common.response; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ApiErrorResponse { + + private int status; + + private String error; + + private String message; + + private String path; + + private LocalDateTime timestamp; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/user/entity/User.java b/server/src/main/java/com/bookmyvenue/server/user/entity/User.java new file mode 100644 index 000000000..d8e79f169 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/entity/User.java @@ -0,0 +1,64 @@ +package com.bookmyvenue.server.user.entity; + +import com.bookmyvenue.server.user.enums.Role; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "users") // Maps this entity to the 'users' table in PostgreSQL +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) // Auto-generates a unique UUID for each user + private UUID id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false, unique = true) + private String phone; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; // USER, VENDOR, or ADMIN + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; // Record creation timestamp + + @Column(nullable = false) + private LocalDateTime updatedAt; // Last update timestamp + + /** + * Automatically sets creation and update timestamps + * before the entity is inserted into the database. + */ + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + this.createdAt = now; + this.updatedAt = now; + } + + /** + * Automatically updates the updatedAt timestamp + * whenever the entity is modified. + */ + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/server/src/main/java/com/bookmyvenue/server/user/enums/Role.java b/server/src/main/java/com/bookmyvenue/server/user/enums/Role.java new file mode 100644 index 000000000..d53c13957 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/enums/Role.java @@ -0,0 +1,10 @@ +package com.bookmyvenue.server.user.enums; + +/** + * Defines the roles available in the BookMyVenue platform. + */ +public enum Role { + USER, + VENDOR, + ADMIN +} diff --git a/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java b/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java new file mode 100644 index 000000000..819174b88 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java @@ -0,0 +1,28 @@ +package com.bookmyvenue.server.user.repository; + +import com.bookmyvenue.server.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface UserRepository extends JpaRepository { + + /** + * Finds a user by email address. + * Used during authentication and login. + */ + Optional findByEmail(String email); + + /** + * Checks whether an email is already registered. + * Used during user registration. + */ + boolean existsByEmail(String email); + + /** + * Checks whether a phone number is already registered. + * Used during user registration. + */ + boolean existsByPhone(String phone); +} \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 07a2999e9..5a4accccf 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -9,4 +9,10 @@ spring.datasource.driver-class-name=org.postgresql.Driver # JPA / Hibernate Properties spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + + + +app.jwt.secret=${JWT_SECRET} +app.jwt.access-token-expiration=${JWT_ACCESS_TOKEN_EXPIRATION} +app.jwt.refresh-token-expiration=${JWT_REFRESH_TOKEN_EXPIRATION} \ No newline at end of file From 8d487316343437d5930ed0b77afcedf3b24b6aa3 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Sat, 30 May 2026 11:02:29 +0530 Subject: [PATCH 07/59] feat(auth): implement authentication module --- server/.env | 8 +- server/.env.example | 8 +- server/pom.xml | 99 ++++++++++ .../auth/controller/AuthController.java | 79 ++++++++ .../server/auth/dto/request/LoginRequest.java | 30 ++++ .../auth/dto/request/RefreshTokenRequest.java | 21 +++ .../auth/dto/request/RegisterRequest.java | 40 +++++ .../auth/dto/response/AuthResponse.java | 34 ++++ .../dto/response/RefreshTokenResponse.java | 23 +++ .../server/auth/security/JwtProperties.java | 34 ++++ .../server/auth/security/JwtService.java | 121 +++++++++++++ .../server/auth/security/SecurityConfig.java | 45 +++++ .../server/auth/service/AuthService.java | 26 +++ .../server/auth/service/AuthServiceImpl.java | 130 ++++++++++++++ .../common/exception/BadRequestException.java | 8 + .../exception/GlobalExceptionHandler.java | 170 ++++++++++++++++++ .../InvalidCredentialsException.java | 8 + .../exception/ResourceNotFoundException.java | 8 + .../exception/UserAlreadyExistsException.java | 8 + .../common/response/ApiErrorResponse.java | 23 +++ .../bookmyvenue/server/user/entity/User.java | 64 +++++++ .../bookmyvenue/server/user/enums/Role.java | 10 ++ .../user/repository/UserRepository.java | 28 +++ .../src/main/resources/application.properties | 8 +- 24 files changed, 1029 insertions(+), 4 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/request/LoginRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/request/RefreshTokenRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/JwtProperties.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/JwtService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/entity/User.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/enums/Role.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java diff --git a/server/.env b/server/.env index eecef2f50..c765791db 100644 --- a/server/.env +++ b/server/.env @@ -1,3 +1,7 @@ DB_USERNAME=postgres -DB_PASSWORD=postgres -DB_NAME=bookmyvenue_db \ No newline at end of file +DB_PASSWORD=postgres123 +DB_NAME=bookmyvenue_db + +JWT_SECRET=M1hQ5gL4vM8e4uVh1sL3gXzN0V8i8cR4gG8kN2jP7sW9mT6aY3qF1rH5uK2xB9nL +JWT_ACCESS_TOKEN_EXPIRATION=900000 +JWT_REFRESH_TOKEN_EXPIRATION=604800000 \ No newline at end of file diff --git a/server/.env.example b/server/.env.example index ff1cc3d27..64a129c91 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,3 +1,9 @@ +# PostgreSQL DB_USERNAME=postgres DB_PASSWORD=type_your_password_here -DB_NAME=bookmyvenue_db \ No newline at end of file +DB_NAME=bookmyvenue_db + +# JWT +JWT_SECRET=replace_with_your_jwt_secret +JWT_ACCESS_TOKEN_EXPIRATION=900000 +JWT_REFRESH_TOKEN_EXPIRATION=604800000 \ No newline at end of file diff --git a/server/pom.xml b/server/pom.xml index 3519904d5..f8a5359b4 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -28,6 +28,7 @@ 21 + 2.3.10 @@ -43,6 +44,42 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-validation + + + io.jsonwebtoken + jjwt-api + 0.12.7 + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.9 + + + + me.paulschwarz + spring-dotenv + 4.0.0 + + + + + io.jsonwebtoken + jjwt-impl + 0.12.7 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.7 + runtime + org.postgresql postgresql @@ -63,6 +100,17 @@ spring-security-test test + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + @@ -79,6 +127,43 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + true + + + compile + compile + + compile + + + + src/main/java + target/generated-sources/annotations + + + + + test-compile + test-compile + + test-compile + + + + src/test/java + target/generated-test-sources/test-annotations + + + + + + 1.8 + + org.apache.maven.plugins maven-compiler-plugin @@ -113,6 +198,20 @@ + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + diff --git a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java new file mode 100644 index 000000000..00d5479d8 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java @@ -0,0 +1,79 @@ +package com.bookmyvenue.server.auth.controller; + +import com.bookmyvenue.server.auth.dto.request.LoginRequest; +import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; +import com.bookmyvenue.server.auth.dto.request.RegisterRequest; +import com.bookmyvenue.server.auth.dto.response.AuthResponse; +import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; +import com.bookmyvenue.server.auth.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@Tag(name = "Authentication", description = "Authentication and account management APIs") +public class AuthController { + + private final AuthService authService; + + @Operation( + summary = "Register a new account", + description = "Creates a new USER or VENDOR account and returns JWT tokens." + ) + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Registration successful"), + @ApiResponse(responseCode = "400", description = "Invalid request or ADMIN registration attempted"), + @ApiResponse(responseCode = "409", description = "Email or phone already exists") + }) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + AuthResponse response = authService.register(request); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(response); + } + + + + @Operation( + summary = "Authenticate user", + description = "Authenticates a user and returns JWT tokens." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Login successful"), + @ApiResponse(responseCode = "401", description = "Invalid credentials") + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + AuthResponse response = authService.login(request); + return ResponseEntity.ok(response); + } + + + + @Operation( + summary = "Refresh JWT tokens", + description = "Generates new access and refresh tokens using a valid refresh token." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Tokens refreshed successfully"), + @ApiResponse(responseCode = "401", description = "Invalid or expired refresh token") + }) + @PostMapping("/refresh-token") + public ResponseEntity refreshToken(@Valid @RequestBody RefreshTokenRequest request) { + + RefreshTokenResponse response = authService.refreshToken(request); + return ResponseEntity.ok(response); + } + + + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/LoginRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/LoginRequest.java new file mode 100644 index 000000000..16a1df4f0 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/LoginRequest.java @@ -0,0 +1,30 @@ +package com.bookmyvenue.server.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "User login request") +public class LoginRequest { + + @Schema( + description = "Registered email address", + example = "ajay@gmail.com" + ) + @Email(message = "Invalid email format") + @NotBlank(message = "Email is required") + private String email; + + @Schema( + description = "Account password", + example = "password123" + ) + @NotBlank(message = "Password is required") + private String password; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RefreshTokenRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RefreshTokenRequest.java new file mode 100644 index 000000000..dab88bad5 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RefreshTokenRequest.java @@ -0,0 +1,21 @@ +package com.bookmyvenue.server.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Refresh token request") +public class RefreshTokenRequest { + + @Schema( + description = "JWT refresh token", + example = "eyJhbGciOiJIUzM4NCJ9..." + ) + @NotBlank(message = "Refresh token is required") + private String refreshToken; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java new file mode 100644 index 000000000..9d289286e --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java @@ -0,0 +1,40 @@ +package com.bookmyvenue.server.auth.dto.request; + +import com.bookmyvenue.server.user.enums.Role; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "User registration request") +public class RegisterRequest { + + @Schema(description = "Full name of the user", example = "Ajay") + @NotBlank(message = "Name is required") + private String name; + + @Schema(description = "Unique email address", example = "ajay@gmail.com") + @Email(message = "Invalid email format") + @NotBlank(message = "Email is required") + private String email; + + @Schema(description = "10 digit phone number", example = "9876543210") + @Pattern(regexp = "^[0-9]{10}$", message = "Phone number must be exactly 10 digits") + @NotBlank(message = "Phone number is required") + private String phone; + + @Schema(description = "Account password", example = "password123") + @NotBlank(message = "Password is required") + private String password; + + @Schema(description = "Account role", example = "USER", allowableValues = {"USER", "VENDOR"}) + @NotNull(message = "Role is required") + private Role role; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java new file mode 100644 index 000000000..ba45f1515 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java @@ -0,0 +1,34 @@ +package com.bookmyvenue.server.auth.dto.response; + +import com.bookmyvenue.server.user.enums.Role; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Authentication response returned after successful login or registration") +public class AuthResponse { + + @Schema(description = "Unique identifier of the authenticated user", example = "82b45eec-c37b-4ef5-b5fb-921ab399cb13") + private UUID userId; + + @Schema(description = "Full name of the user", example = "Ajay") + private String name; + + @Schema(description = "Registered email address", example = "ajay@gmail.com") + private String email; + + @Schema(description = "Role assigned to the user", example = "USER") + private Role role; + + @Schema(description = "JWT access token used to access protected APIs") + private String accessToken; + + @Schema(description = "JWT refresh token used to obtain a new access token") + private String refreshToken; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java new file mode 100644 index 000000000..5b7c259c3 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java @@ -0,0 +1,23 @@ +package com.bookmyvenue.server.auth.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +/**for later scaling we are + returning both access and refreshtoken . for token rotation **/ + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Refresh token response") + +public class RefreshTokenResponse { + + @Schema(description = "New JWT access token") + private String accessToken; + + @Schema(description = "New JWT refresh token") + private String refreshToken; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/JwtProperties.java b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtProperties.java new file mode 100644 index 000000000..a9726cea8 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtProperties.java @@ -0,0 +1,34 @@ +package com.bookmyvenue.server.auth.security; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Stores JWT-related configuration loaded from application properties. + * + * Values are sourced from environment variables to keep secrets + * and token settings outside the codebase. + */ +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "app.jwt") +public class JwtProperties { + + /** + * Secret key used to sign and verify JWT tokens. + */ + private String secret; + + /** + * Access token validity duration in milliseconds. + */ + private long accessTokenExpiration; + + /** + * Refresh token validity duration in milliseconds. + */ + private long refreshTokenExpiration; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/JwtService.java b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtService.java new file mode 100644 index 000000000..feb84530c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtService.java @@ -0,0 +1,121 @@ +package com.bookmyvenue.server.auth.security; + +import com.bookmyvenue.server.user.enums.Role; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.function.Function; + +/** + * Handles JWT token generation, validation, and claim extraction. + */ +@Service +@RequiredArgsConstructor +public class JwtService { + + private final JwtProperties jwtProperties; + + /** + * Generates an access token for the given user. + */ + public String generateAccessToken(String email, Role role) { + return generateToken(email, role, jwtProperties.getAccessTokenExpiration()); + } + + /** + * Generates a refresh token for the given user. + */ + public String generateRefreshToken(String email, Role role) { + return generateToken(email, role, jwtProperties.getRefreshTokenExpiration()); + } + + /** + * Common token generation logic. + */ + private String generateToken(String email, Role role, long expiration) { + return Jwts.builder() + .subject(email) + .claim("role", role.name()) + .issuedAt(new Date()) + .expiration( + new Date( + System.currentTimeMillis() + expiration + ) + ) + .signWith(getSigningKey()) + .compact(); + } + + /** + * Extracts the email (subject) from a JWT token. + */ + public String extractEmail(String token) { + return extractClaim(token, Claims::getSubject); + } + + /** + * Extracts the role from a JWT token. + */ + public String extractRole(String token) { + return extractClaim( + token, + claims -> claims.get("role", String.class) + ); + } + + /** + * Extracts a specific claim from a token. + */ + public T extractClaim( + String token, + Function claimsResolver + ) { + Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + /** + * Validates whether a token belongs to the expected user + * and is not expired. + */ + public boolean isTokenValid(String token, String email) { + return email.equals(extractEmail(token)) + && !isTokenExpired(token); + } + + /** + * Checks whether a token has expired. + */ + private boolean isTokenExpired(String token) { + return extractClaim(token, Claims::getExpiration) + .before(new Date()); + } + + /** + * Extracts all claims from the token. + */ + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + /** + * Creates the signing key from the configured JWT secret. + */ + private SecretKey getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode( + jwtProperties.getSecret() + ); + + return Keys.hmacShaKeyFor(keyBytes); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java new file mode 100644 index 000000000..c6bc75133 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -0,0 +1,45 @@ +package com.bookmyvenue.server.auth.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + /** + * Password encoder bean used to hash and verify passwords. + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Security configuration for MVP authentication endpoints. + */ + @Bean + public SecurityFilterChain securityFilterChain( + HttpSecurity http + ) throws Exception { + + http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/api/auth/**", + "/swagger-ui/**", + "/v3/api-docs/**" + ).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form.disable()) + .httpBasic(basic -> basic.disable()); + + return http.build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java new file mode 100644 index 000000000..ad11ec32a --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java @@ -0,0 +1,26 @@ +package com.bookmyvenue.server.auth.service; + +import com.bookmyvenue.server.auth.dto.request.LoginRequest; +import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; +import com.bookmyvenue.server.auth.dto.request.RegisterRequest; +import com.bookmyvenue.server.auth.dto.response.AuthResponse; +import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; + +public interface AuthService { + + /** + * Registers a new USER or VENDOR account. + */ + AuthResponse register(RegisterRequest request); + + /** + * Authenticates a user and returns JWT tokens. + */ + AuthResponse login(LoginRequest request); + + + /** + * Generates a new access token using a valid refresh token. + */ + RefreshTokenResponse refreshToken(RefreshTokenRequest request); +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java new file mode 100644 index 000000000..06abc361d --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java @@ -0,0 +1,130 @@ +package com.bookmyvenue.server.auth.service; + +import com.bookmyvenue.server.auth.dto.request.LoginRequest; +import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; +import com.bookmyvenue.server.auth.dto.request.RegisterRequest; +import com.bookmyvenue.server.auth.dto.response.AuthResponse; +import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; +import com.bookmyvenue.server.auth.security.JwtService; +import com.bookmyvenue.server.common.exception.BadRequestException; +import com.bookmyvenue.server.common.exception.InvalidCredentialsException; +import com.bookmyvenue.server.common.exception.UserAlreadyExistsException; +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.user.enums.Role; +import com.bookmyvenue.server.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthServiceImpl implements AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + + + @Override + public AuthResponse register(RegisterRequest request) { + + log.info("Attempting registration for email: {}", request.getEmail()); + + if (userRepository.existsByEmail(request.getEmail())) { + log.warn("Registration failed. Email already exists: {}", request.getEmail()); + throw new UserAlreadyExistsException("Email already registered"); + } + + if (userRepository.existsByPhone(request.getPhone())) { + log.warn("Registration failed. Phone already exists: {}", request.getPhone()); + throw new UserAlreadyExistsException("Phone number already registered"); + } + + if (request.getRole() == Role.ADMIN) { + log.warn("Attempted ADMIN registration for email: {}", request.getEmail()); + throw new BadRequestException("Admin registration is not allowed"); + } + User user = User.builder() + .name(request.getName()) + .email(request.getEmail()) + .phone(request.getPhone()) + .password(passwordEncoder.encode(request.getPassword())) + .role(request.getRole()) + .build(); + + User savedUser = userRepository.save(user); + + String accessToken = jwtService.generateAccessToken(savedUser.getEmail(), savedUser.getRole()); + String refreshToken = jwtService.generateRefreshToken(savedUser.getEmail(), savedUser.getRole()); + + log.info("User registered successfully: {}", savedUser.getEmail()); + + return AuthResponse.builder() + .userId(savedUser.getId()) + .name(savedUser.getName()) + .email(savedUser.getEmail()) + .role(savedUser.getRole()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + + + @Override + public AuthResponse login(LoginRequest request) { + log.info("Login attempt for email: {}", request.getEmail()); + User user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> { + log.warn("Login failed. User not found: {}", request.getEmail()); + return new InvalidCredentialsException("Invalid email or password"); + }); + + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + log.warn("Login failed. Invalid password for email: {}", request.getEmail()); + throw new InvalidCredentialsException("Invalid email or password"); + } + + String accessToken = jwtService.generateAccessToken(user.getEmail(), user.getRole()); + String refreshToken = jwtService.generateRefreshToken(user.getEmail(), user.getRole()); + + log.info("User logged in successfully: {}", user.getEmail()); + + return AuthResponse.builder() + .userId(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .role(user.getRole()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + + @Override + public RefreshTokenResponse refreshToken(RefreshTokenRequest request) { + + String email = jwtService.extractEmail(request.getRefreshToken()); + User user = userRepository.findByEmail(email) + .orElseThrow(() -> + new InvalidCredentialsException( + "Invalid refresh token" + ) + ); + + if (!jwtService.isTokenValid(request.getRefreshToken(), user.getEmail() + )) { + throw new InvalidCredentialsException("Invalid refresh token"); + } + String accessToken = jwtService.generateAccessToken(user.getEmail(), user.getRole()); + String refreshToken = jwtService.generateRefreshToken(user.getEmail(), user.getRole()); + + return RefreshTokenResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java new file mode 100644 index 000000000..82789c5ac --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.common.exception; + +public class BadRequestException extends RuntimeException { + + public BadRequestException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..4f6ce6228 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,170 @@ +package com.bookmyvenue.server.common.exception; + +import com.bookmyvenue.server.common.response.ApiErrorResponse; +import io.jsonwebtoken.JwtException; +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.LocalDateTime; + +/** + * Centralized exception handler for the entire application. + * + * Converts application exceptions into consistent API error responses, + * ensuring clients receive meaningful HTTP status codes and messages. + */ +@Hidden +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * Handles attempts to create a user with an email or phone number + * that already exists in the system. + * + * Returns HTTP 409 (Conflict). + */ + @ExceptionHandler(UserAlreadyExistsException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiErrorResponse handleUserAlreadyExistsException( + UserAlreadyExistsException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.CONFLICT, + ex.getMessage(), + request + ); + } + + /** + * Handles authentication failures such as invalid email or password. + * + * Returns HTTP 401 (Unauthorized). + */ + @ExceptionHandler(InvalidCredentialsException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ApiErrorResponse handleInvalidCredentialsException( + InvalidCredentialsException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.UNAUTHORIZED, + ex.getMessage(), + request + ); + } + + /** + * Handles requests for resources that do not exist. + * + * Examples: + * - User not found + * - Venue not found + * - Booking not found + * + * Returns HTTP 404 (Not Found). + */ + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiErrorResponse handleResourceNotFoundException( + ResourceNotFoundException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.NOT_FOUND, + ex.getMessage(), + request + ); + } + + /** + * Fallback handler for unexpected application errors. + * + * Prevents internal stack traces from being exposed to API consumers + * and returns a generic error response. + * + * Returns HTTP 500 (Internal Server Error). + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiErrorResponse handleGenericException( + Exception ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "An unexpected error occurred", + request + ); + } + + /** + * Handles requests that fail business validation rules. + * + * Examples: + * - Admin self-registration attempted + * - Invalid booking request + * - Invalid venue configuration + * + * Returns HTTP 400 (Bad Request). + */ + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiErrorResponse handleBadRequestException( + BadRequestException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + ex.getMessage(), + request + ); + } + + + /** + * Handles JWT-related authentication failures. + * + * Examples: + * - Expired token + * - Invalid token signature + * - Malformed token + * - Tampered token + * + * Returns HTTP 401 (Unauthorized). + */ + @ExceptionHandler(JwtException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ApiErrorResponse handleJwtException( + JwtException ex, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.UNAUTHORIZED, + "Invalid or expired token", + request + ); + } + + + /** + * Creates a standardized error response object used across the API. + */ + private ApiErrorResponse buildErrorResponse( + HttpStatus status, + String message, + HttpServletRequest request + ) { + return ApiErrorResponse.builder() + .status(status.value()) + .error(status.getReasonPhrase()) + .message(message) + .path(request.getRequestURI()) + .timestamp(LocalDateTime.now()) + .build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java new file mode 100644 index 000000000..358149b10 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.common.exception; + +public class InvalidCredentialsException extends RuntimeException { + + public InvalidCredentialsException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java new file mode 100644 index 000000000..0046a7a37 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.common.exception; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java new file mode 100644 index 000000000..44b2ab437 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.common.exception; + +public class UserAlreadyExistsException extends RuntimeException { + + public UserAlreadyExistsException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java b/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java new file mode 100644 index 000000000..40e6aeef1 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java @@ -0,0 +1,23 @@ +package com.bookmyvenue.server.common.response; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ApiErrorResponse { + + private int status; + + private String error; + + private String message; + + private String path; + + private LocalDateTime timestamp; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/user/entity/User.java b/server/src/main/java/com/bookmyvenue/server/user/entity/User.java new file mode 100644 index 000000000..d8e79f169 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/entity/User.java @@ -0,0 +1,64 @@ +package com.bookmyvenue.server.user.entity; + +import com.bookmyvenue.server.user.enums.Role; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "users") // Maps this entity to the 'users' table in PostgreSQL +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) // Auto-generates a unique UUID for each user + private UUID id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false, unique = true) + private String phone; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; // USER, VENDOR, or ADMIN + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; // Record creation timestamp + + @Column(nullable = false) + private LocalDateTime updatedAt; // Last update timestamp + + /** + * Automatically sets creation and update timestamps + * before the entity is inserted into the database. + */ + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + this.createdAt = now; + this.updatedAt = now; + } + + /** + * Automatically updates the updatedAt timestamp + * whenever the entity is modified. + */ + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/server/src/main/java/com/bookmyvenue/server/user/enums/Role.java b/server/src/main/java/com/bookmyvenue/server/user/enums/Role.java new file mode 100644 index 000000000..d53c13957 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/enums/Role.java @@ -0,0 +1,10 @@ +package com.bookmyvenue.server.user.enums; + +/** + * Defines the roles available in the BookMyVenue platform. + */ +public enum Role { + USER, + VENDOR, + ADMIN +} diff --git a/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java b/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java new file mode 100644 index 000000000..819174b88 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java @@ -0,0 +1,28 @@ +package com.bookmyvenue.server.user.repository; + +import com.bookmyvenue.server.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface UserRepository extends JpaRepository { + + /** + * Finds a user by email address. + * Used during authentication and login. + */ + Optional findByEmail(String email); + + /** + * Checks whether an email is already registered. + * Used during user registration. + */ + boolean existsByEmail(String email); + + /** + * Checks whether a phone number is already registered. + * Used during user registration. + */ + boolean existsByPhone(String phone); +} \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 07a2999e9..5a4accccf 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -9,4 +9,10 @@ spring.datasource.driver-class-name=org.postgresql.Driver # JPA / Hibernate Properties spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + + + +app.jwt.secret=${JWT_SECRET} +app.jwt.access-token-expiration=${JWT_ACCESS_TOKEN_EXPIRATION} +app.jwt.refresh-token-expiration=${JWT_REFRESH_TOKEN_EXPIRATION} \ No newline at end of file From 42b1921cc40c5062d96fadfd190e65fd7d3c35f1 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Sat, 30 May 2026 11:24:58 +0530 Subject: [PATCH 08/59] chore: stop tracking .env file --- server/.env | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 server/.env diff --git a/server/.env b/server/.env deleted file mode 100644 index c765791db..000000000 --- a/server/.env +++ /dev/null @@ -1,7 +0,0 @@ -DB_USERNAME=postgres -DB_PASSWORD=postgres123 -DB_NAME=bookmyvenue_db - -JWT_SECRET=M1hQ5gL4vM8e4uVh1sL3gXzN0V8i8cR4gG8kN2jP7sW9mT6aY3qF1rH5uK2xB9nL -JWT_ACCESS_TOKEN_EXPIRATION=900000 -JWT_REFRESH_TOKEN_EXPIRATION=604800000 \ No newline at end of file From e5d1af3f98a9e1fe5babb4e7728317499fc10aff Mon Sep 17 00:00:00 2001 From: AJAY V Date: Sat, 30 May 2026 11:51:07 +0530 Subject: [PATCH 09/59] Delete server/.env --- server/.env | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 server/.env diff --git a/server/.env b/server/.env deleted file mode 100644 index c765791db..000000000 --- a/server/.env +++ /dev/null @@ -1,7 +0,0 @@ -DB_USERNAME=postgres -DB_PASSWORD=postgres123 -DB_NAME=bookmyvenue_db - -JWT_SECRET=M1hQ5gL4vM8e4uVh1sL3gXzN0V8i8cR4gG8kN2jP7sW9mT6aY3qF1rH5uK2xB9nL -JWT_ACCESS_TOKEN_EXPIRATION=900000 -JWT_REFRESH_TOKEN_EXPIRATION=604800000 \ No newline at end of file From 9ea78d6dc4a1547e0eeab1ecfafeac13c78c4e0d Mon Sep 17 00:00:00 2001 From: AJAY V Date: Sat, 30 May 2026 12:00:18 +0530 Subject: [PATCH 10/59] Refactor .gitignore for improved clarity and coverage Updated .gitignore to include additional build, environment, and IDE files while removing redundant entries. --- server/.gitignore | 105 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/server/.gitignore b/server/.gitignore index 667aaef0c..a642c0b25 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,10 +1,58 @@ HELP.md + +# ========================================== + +# Build Output + +# ========================================== + target/ -.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ +build/ + +# ========================================== + +# Environment Files + +# ========================================== + +.env +.env.* +!.env.example + +# ========================================== + +# Logs + +# ========================================== + +*.log + +# ========================================== + +# IntelliJ IDEA + +# ========================================== + +.idea/ +*.iml +*.ipr +*.iws +out/ + +# ========================================== + +# VS Code + +# ========================================== + +.vscode/ + +# ========================================== + +# Spring Tool Suite (STS) + +# ========================================== -### STS ### .apt_generated .classpath .factorypath @@ -13,21 +61,48 @@ target/ .springBeans .sts4-cache -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr +# ========================================== + +# NetBeans + +# ========================================== -### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ -### VS Code ### -.vscode/ +# ========================================== + +# OS Files + +# ========================================== + +.DS_Store +Thumbs.db + +# ========================================== + +# Coverage Reports + +# ========================================== + +coverage/ + +# ========================================== + +# Temporary Files + +# ========================================== + +tmp/ +temp/ + +# ========================================== + +# Docker + +# ========================================== + +*.pid From b7675fc087a07f385794f002207399b2bef30d2a Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 30 May 2026 22:54:12 +0530 Subject: [PATCH 11/59] feat: setup flyway and initial database schema --- server/.gitignore | 1 + server/pom.xml | 9 ++ .../bookmyvenue/server/ServerApplication.java | 1 + .../src/main/resources/application.properties | 11 +- .../db/migration/V1__init_schema.sql | 135 ++++++++++++++++++ 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 server/src/main/resources/db/migration/V1__init_schema.sql diff --git a/server/.gitignore b/server/.gitignore index a642c0b25..ba43720e9 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -106,3 +106,4 @@ temp/ # ========================================== *.pid + diff --git a/server/pom.xml b/server/pom.xml index f8a5359b4..fbd2c92aa 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -111,6 +111,15 @@ ${kotlin.version} test + + org.flywaydb + flyway-core + compile + + + org.flywaydb + flyway-database-postgresql + diff --git a/server/src/main/java/com/bookmyvenue/server/ServerApplication.java b/server/src/main/java/com/bookmyvenue/server/ServerApplication.java index c77811908..3dbd46e1d 100644 --- a/server/src/main/java/com/bookmyvenue/server/ServerApplication.java +++ b/server/src/main/java/com/bookmyvenue/server/ServerApplication.java @@ -6,6 +6,7 @@ @SpringBootApplication public class ServerApplication { + public static void main(String[] args) { SpringApplication.run(ServerApplication.class, args); } diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 5a4accccf..66b4ab8ab 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1,18 +1,23 @@ # =================================================================== #DOCKERIZED POSTGRESQL CONFIGURATION # =================================================================== +spring.config.import=optional:file:server/.env[.properties] spring.datasource.url=jdbc:postgresql://localhost:5432/${DB_NAME} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # JPA / Hibernate Properties -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=validate spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect - +# jwt app.jwt.secret=${JWT_SECRET} app.jwt.access-token-expiration=${JWT_ACCESS_TOKEN_EXPIRATION} -app.jwt.refresh-token-expiration=${JWT_REFRESH_TOKEN_EXPIRATION} \ No newline at end of file +app.jwt.refresh-token-expiration=${JWT_REFRESH_TOKEN_EXPIRATION} + +# Flyway +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration diff --git a/server/src/main/resources/db/migration/V1__init_schema.sql b/server/src/main/resources/db/migration/V1__init_schema.sql new file mode 100644 index 000000000..bc8980d16 --- /dev/null +++ b/server/src/main/resources/db/migration/V1__init_schema.sql @@ -0,0 +1,135 @@ +CREATE TABLE users ( + id UUID PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + phone VARCHAR(20) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); + + +CREATE TABLE venue_category ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE venue ( + id BIGSERIAL PRIMARY KEY, + + owner_id UUID NOT NULL, + category_id BIGINT NOT NULL, + + name VARCHAR(255) NOT NULL, + description TEXT, + address TEXT NOT NULL, + district VARCHAR(100), + capacity INTEGER NOT NULL, + + price_per_slot DECIMAL(10,2) NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_venue_owner + FOREIGN KEY (owner_id) + REFERENCES users(id), + + CONSTRAINT fk_venue_category + FOREIGN KEY (category_id) + REFERENCES venue_category(id) +); + + + +CREATE TABLE venue_image ( + id BIGSERIAL PRIMARY KEY, + + venue_id BIGINT NOT NULL, + + image_url VARCHAR(1000) NOT NULL, + + is_primary BOOLEAN NOT NULL DEFAULT FALSE, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_venue_image_venue + FOREIGN KEY (venue_id) + REFERENCES venue(id) +); + + +CREATE TABLE slot ( + id BIGSERIAL PRIMARY KEY, + + venue_id BIGINT NOT NULL, + + date DATE NOT NULL, + + start_time TIME NOT NULL, + end_time TIME NOT NULL, + + status VARCHAR(50) NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_slot_venue + FOREIGN KEY (venue_id) + REFERENCES venue(id) +); + + +CREATE TABLE booking ( + id BIGSERIAL PRIMARY KEY, + + user_id UUID NOT NULL, + venue_id BIGINT NOT NULL, + slot_id BIGINT NOT NULL, + + status VARCHAR(50) NOT NULL, + + total_amount DECIMAL(10,2) NOT NULL, + + expires_at TIMESTAMP, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_booking_user + FOREIGN KEY (user_id) + REFERENCES users(id), + + CONSTRAINT fk_booking_venue + FOREIGN KEY (venue_id) + REFERENCES venue(id), + + CONSTRAINT fk_booking_slot + FOREIGN KEY (slot_id) + REFERENCES slot(id) +); + + +CREATE TABLE payment ( + id BIGSERIAL PRIMARY KEY, + + booking_id BIGINT NOT NULL, + + amount DECIMAL(10,2) NOT NULL, + + status VARCHAR(50) NOT NULL, + + gateway_payment_id VARCHAR(255), + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_payment_booking + FOREIGN KEY (booking_id) + REFERENCES booking(id) +); \ No newline at end of file From 2cee909878a9ccac15fb411ba2f4ec527b0f4e92 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sun, 31 May 2026 12:55:48 +0530 Subject: [PATCH 12/59] feat: implement venue creation api --- .../server/auth/security/SecurityConfig.java | 3 +- .../user/repository/UserRepository.java | 2 + .../venue/controller/VenueController.java | 24 +++++++ .../venue/dto/request/CreateVenueRequest.java | 30 ++++++++ .../venue/dto/response/VenueResponse.java | 35 +++++++++ .../server/venue/entity/Venue.java | 71 +++++++++++++++++++ .../server/venue/entity/VenueCategory.java | 36 ++++++++++ .../server/venue/entity/VenueImage.java | 35 +++++++++ .../server/venue/entity/VenueStatus.java | 8 +++ .../repository/VenueCategoryRepository.java | 9 +++ .../venue/repository/VenueRepository.java | 7 ++ .../server/venue/service/VenueService.java | 8 +++ .../venue/service/VenueServiceImpl.java | 65 +++++++++++++++++ .../db/migration/V2__add_status_to_venue.sql | 2 + 14 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/entity/VenueCategory.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/entity/VenueImage.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/entity/VenueStatus.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/repository/VenueCategoryRepository.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java create mode 100644 server/src/main/resources/db/migration/V2__add_status_to_venue.sql diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java index c6bc75133..c1f893af1 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -33,7 +33,8 @@ public SecurityFilterChain securityFilterChain( .requestMatchers( "/api/auth/**", "/swagger-ui/**", - "/v3/api-docs/**" + "/v3/api-docs/**", + "/api/v1/venues/**" // TODO: Restrict to VENDOR role after JWT authorization is implemented ).permitAll() .anyRequest().authenticated() ) diff --git a/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java b/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java index 819174b88..429a3e951 100644 --- a/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java +++ b/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java @@ -25,4 +25,6 @@ public interface UserRepository extends JpaRepository { * Used during user registration. */ boolean existsByPhone(String phone); + + UUID id(UUID id); } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java new file mode 100644 index 000000000..3e35e455c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -0,0 +1,24 @@ +package com.bookmyvenue.server.venue.controller; + +import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; +import com.bookmyvenue.server.venue.dto.response.VenueResponse; +import com.bookmyvenue.server.venue.service.VenueService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/venues") +@RequiredArgsConstructor +public class VenueController { + + private final VenueService venueService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public VenueResponse createVenue( + @RequestBody CreateVenueRequest request + ) { + return venueService.createVenue(request); + } +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java new file mode 100644 index 000000000..6f18ed899 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java @@ -0,0 +1,30 @@ +package com.bookmyvenue.server.venue.dto.request; + +import lombok.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CreateVenueRequest { + + private String name; + private String description; + private String address; + private String district; + + private Integer capacity; + + private BigDecimal pricePerSlot; + + private Long categoryId; + // TODO: Temporary field for development. Remove after JWT authentication is integrated. + private UUID ownerId; + + private List imageUrls; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueResponse.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueResponse.java new file mode 100644 index 000000000..bc03a217b --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueResponse.java @@ -0,0 +1,35 @@ +package com.bookmyvenue.server.venue.dto.response; + +import com.bookmyvenue.server.venue.entity.VenueStatus; +import lombok.*; + +import java.math.BigDecimal; +import java.util.List; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class VenueResponse { + + private Long id; + + private String name; + + private String description; + + private String address; + + private String district; + + private Integer capacity; + + private BigDecimal pricePerSlot; + + private String category; + + private VenueStatus status; + + private List imageUrls; +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java b/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java new file mode 100644 index 000000000..09ecda7cb --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java @@ -0,0 +1,71 @@ +package com.bookmyvenue.server.venue.entity; + +import com.bookmyvenue.server.user.entity.User; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "venue") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Venue { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id", nullable = false) + private User owner; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id", nullable = false) + private VenueCategory category; + + @Column(nullable = false) + private String name; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(nullable = false, columnDefinition = "TEXT") + private String address; + + @Column(length = 100) + private String district; + + @Column(nullable = false) + private Integer capacity; + + @Column(name = "price_per_slot", nullable = false, precision = 10, scale = 2) + private BigDecimal pricePerSlot; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private VenueStatus status; + + @OneToMany( + mappedBy = "venue", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List images = new ArrayList<>(); + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueCategory.java b/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueCategory.java new file mode 100644 index 000000000..217bcae44 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueCategory.java @@ -0,0 +1,36 @@ +package com.bookmyvenue.server.venue.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "venue_category") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VenueCategory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 100) + private String name; + + @Column(columnDefinition = "TEXT") + private String description; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueImage.java b/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueImage.java new file mode 100644 index 000000000..0a1e9799e --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueImage.java @@ -0,0 +1,35 @@ +package com.bookmyvenue.server.venue.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "venue_image") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VenueImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "venue_id", nullable = false) + private Venue venue; + + @Column(name = "image_url", nullable = false, length = 1000) + private String imageUrl; + + @Column(name = "is_primary", nullable = false) + private Boolean isPrimary = false; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueStatus.java b/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueStatus.java new file mode 100644 index 000000000..0c1ee948c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueStatus.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.venue.entity; + +public enum VenueStatus { + PENDING_APPROVAL, + APPROVED, + REJECTED, + INACTIVE +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueCategoryRepository.java b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueCategoryRepository.java new file mode 100644 index 000000000..f4cfc55a7 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueCategoryRepository.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.server.venue.repository; + +import com.bookmyvenue.server.venue.entity.VenueCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface VenueCategoryRepository extends JpaRepository { +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java new file mode 100644 index 000000000..948ae965e --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java @@ -0,0 +1,7 @@ +package com.bookmyvenue.server.venue.repository; + +import com.bookmyvenue.server.venue.entity.Venue; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VenueRepository extends JpaRepository { +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java new file mode 100644 index 000000000..2f4ef8492 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.server.venue.service; + +import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; +import com.bookmyvenue.server.venue.dto.response.VenueResponse; + +public interface VenueService { + public VenueResponse createVenue(CreateVenueRequest request); +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java new file mode 100644 index 000000000..18dfa5eb4 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -0,0 +1,65 @@ +package com.bookmyvenue.server.venue.service; + +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.user.repository.UserRepository; +import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; +import com.bookmyvenue.server.venue.dto.response.VenueResponse; +import com.bookmyvenue.server.venue.entity.Venue; +import com.bookmyvenue.server.venue.entity.VenueCategory; +import com.bookmyvenue.server.venue.entity.VenueStatus; +import com.bookmyvenue.server.venue.repository.VenueCategoryRepository; +import com.bookmyvenue.server.venue.repository.VenueRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +@Transactional +public class VenueServiceImpl implements VenueService { + + private final VenueRepository venueRepository; + private final VenueCategoryRepository venueCategoryRepository; + private final UserRepository userRepository; + + @Override + public VenueResponse createVenue(CreateVenueRequest request) { + + VenueCategory category = venueCategoryRepository + .findById(request.getCategoryId()) + .orElseThrow(() -> + new RuntimeException("Venue category not found")); + + // TODO: Replace ownerId with authenticated user from SecurityContext once JWT integration is completed. + User owner = userRepository.findById(request.getOwnerId()) + .orElseThrow(()-> + new RuntimeException("user not found")); + + Venue venue = Venue.builder() + .name(request.getName()) + .description(request.getDescription()) + .address(request.getAddress()) + .district(request.getDistrict()) + .capacity(request.getCapacity()) + .pricePerSlot(request.getPricePerSlot()) + .category(category) + .status(VenueStatus.PENDING_APPROVAL) + .owner(owner) + .build(); + + Venue savedVenue = venueRepository.save(venue); + + return VenueResponse.builder() + .id(savedVenue.getId()) + .name(savedVenue.getName()) + .description(savedVenue.getDescription()) + .address(savedVenue.getAddress()) + .district(savedVenue.getDistrict()) + .capacity(savedVenue.getCapacity()) + .pricePerSlot(savedVenue.getPricePerSlot()) + .category(savedVenue.getCategory().getName()) + .status(savedVenue.getStatus()) + .build(); + } +} \ No newline at end of file diff --git a/server/src/main/resources/db/migration/V2__add_status_to_venue.sql b/server/src/main/resources/db/migration/V2__add_status_to_venue.sql new file mode 100644 index 000000000..5327660b7 --- /dev/null +++ b/server/src/main/resources/db/migration/V2__add_status_to_venue.sql @@ -0,0 +1,2 @@ +ALTER TABLE venue + ADD COLUMN status VARCHAR(30) NOT NULL DEFAULT 'PENDING_APPROVAL'; \ No newline at end of file From 7059ec3a93a4ae1259a16bcb45b4711970adeca1 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Mon, 1 Jun 2026 20:08:00 +0530 Subject: [PATCH 13/59] fix:updated directory hierarchy --- client/{book-my-venue => }/.editorconfig | 0 client/{book-my-venue => }/.gitignore | 0 client/{book-my-venue => }/.postcssrc.json | 0 client/{book-my-venue => }/.prettierrc | 0 client/{book-my-venue => }/README.md | 0 client/{book-my-venue => }/angular.json | 0 client/book-my-venue/.vscode/extensions.json | 4 -- client/book-my-venue/.vscode/launch.json | 20 --------- client/book-my-venue/.vscode/mcp.json | 9 ---- client/book-my-venue/.vscode/tasks.json | 42 ------------------ client/{book-my-venue => }/eslint.config.js | 0 client/{book-my-venue => }/package-lock.json | 0 client/{book-my-venue => }/package.json | 0 client/{book-my-venue => }/public/favicon.ico | Bin .../analytics/analytics.component.css | 0 .../analytics/analytics.component.html | 0 .../analytics/analytics.component.spec.ts | 0 .../features/analytics/analytics.component.ts | 0 .../features/bookings/bookings.component.css | 0 .../features/bookings/bookings.component.html | 0 .../bookings/bookings.component.spec.ts | 0 .../features/bookings/bookings.component.ts | 0 .../dashboard/dashboard.component.css | 0 .../dashboard/dashboard.component.html | 0 .../dashboard/dashboard.component.spec.ts | 0 .../features/dashboard/dashboard.component.ts | 0 .../forgot-password.component.css | 0 .../forgot-password.component.html | 0 .../forgot-password.component.spec.ts | 0 .../forgot-password.component.ts | 0 .../admin/features/login/login.component.css | 0 .../admin/features/login/login.component.html | 0 .../features/login/login.component.spec.ts | 0 .../admin/features/login/login.component.ts | 0 .../features/settings/settings.component.css | 0 .../features/settings/settings.component.html | 0 .../settings/settings.component.spec.ts | 0 .../features/settings/settings.component.ts | 0 .../admin/features/users/users.component.css | 0 .../admin/features/users/users.component.html | 0 .../features/users/users.component.spec.ts | 0 .../admin/features/users/users.component.ts | 0 .../features/vendors/vendors.component.css | 0 .../features/vendors/vendors.component.html | 0 .../vendors/vendors.component.spec.ts | 0 .../features/vendors/vendors.component.ts | 0 .../features/venues/venues.component.css | 0 .../features/venues/venues.component.html | 0 .../features/venues/venues.component.spec.ts | 0 .../admin/features/venues/venues.component.ts | 0 .../src/app/admin/guards/admin-auth.guard.ts | 0 .../interceptors/admin-auth.interceptor.ts | 0 .../admin-layout/admin-layout.component.css | 0 .../admin-layout/admin-layout.component.html | 0 .../admin-layout.component.spec.ts | 0 .../admin-layout/admin-layout.component.ts | 0 .../src/app/admin/models/index.ts | 0 .../repositories/analytics.repository.ts | 0 .../admin/repositories/booking.repository.ts | 0 .../app/admin/repositories/user.repository.ts | 0 .../admin/repositories/vendor.repository.ts | 0 .../admin/repositories/venue.repository.ts | 0 .../src/app/admin/routes/admin.routes.ts | 0 .../app/admin/services/analytics.service.ts | 0 .../src/app/admin/services/booking.service.ts | 0 .../src/app/admin/services/user.service.ts | 0 .../src/app/admin/services/vendor.service.ts | 0 .../src/app/admin/services/venue.service.ts | 0 .../{book-my-venue => }/src/app/app.config.ts | 0 client/{book-my-venue => }/src/app/app.css | 0 client/{book-my-venue => }/src/app/app.html | 0 .../{book-my-venue => }/src/app/app.routes.ts | 0 .../{book-my-venue => }/src/app/app.spec.ts | 0 client/{book-my-venue => }/src/app/app.ts | 0 .../src/app/core/app.routes.ts | 0 .../src/app/core/config/environment.prod.ts | 0 .../src/app/core/config/environment.ts | 0 .../src/app/core/providers/app-providers.ts | 0 .../src/app/core/tokens/injection-tokens.ts | 0 .../src/app/landing/landing.component.css | 0 .../src/app/landing/landing.component.html | 0 .../src/app/landing/landing.component.spec.ts | 0 .../src/app/landing/landing.component.ts | 0 .../components/button/button.component.css | 0 .../components/button/button.component.html | 0 .../button/button.component.spec.ts | 0 .../components/button/button.component.ts | 0 .../confirm-dialog.component.css | 0 .../confirm-dialog.component.html | 0 .../confirm-dialog.component.spec.ts | 0 .../confirm-dialog.component.ts | 0 .../empty-state/empty-state.component.css | 0 .../empty-state/empty-state.component.html | 0 .../empty-state/empty-state.component.spec.ts | 0 .../empty-state/empty-state.component.ts | 0 .../components/input/input.component.css | 0 .../components/input/input.component.html | 0 .../components/input/input.component.spec.ts | 0 .../components/input/input.component.ts | 0 .../components/loader/loader.component.css | 0 .../components/loader/loader.component.html | 0 .../loader/loader.component.spec.ts | 0 .../components/loader/loader.component.ts | 0 .../components/modal/modal.component.css | 0 .../components/modal/modal.component.html | 0 .../components/modal/modal.component.spec.ts | 0 .../components/modal/modal.component.ts | 0 .../pagination/pagination.component.css | 0 .../pagination/pagination.component.html | 0 .../pagination/pagination.component.spec.ts | 0 .../pagination/pagination.component.ts | 0 .../components/table/table.component.css | 0 .../components/table/table.component.html | 0 .../components/table/table.component.spec.ts | 0 .../components/table/table.component.ts | 0 .../constants/api-endpoints.constant.ts | 0 .../src/app/shared/constants/index.ts | 0 .../shared/constants/route-paths.constant.ts | 0 .../directives/click-outside.directive.ts | 0 .../app/shared/enums/booking-status.enum.ts | 0 .../src/app/shared/enums/index.ts | 0 .../src/app/shared/enums/user-role.enum.ts | 0 .../src/app/shared/enums/venue-status.enum.ts | 0 .../src/app/shared/guards/admin-auth.guard.ts | 0 .../src/app/shared/guards/guest.guard.ts | 0 .../src/app/shared/guards/user-auth.guard.ts | 0 .../app/shared/guards/vendor-auth.guard.ts | 0 .../shared/interceptors/error.interceptor.ts | 0 .../app/shared/models/api-response.model.ts | 0 .../app/shared/models/auth-response.model.ts | 0 .../src/app/shared/models/booking.model.ts | 0 .../src/app/shared/models/index.ts | 0 .../src/app/shared/models/user.model.ts | 0 .../src/app/shared/models/vendor.model.ts | 0 .../src/app/shared/models/venue.model.ts | 0 .../app/shared/pipes/currency-format.pipe.ts | 0 .../src/app/shared/pipes/date-format.pipe.ts | 0 .../app/shared/services/auth.repository.ts | 0 .../src/app/shared/services/auth.service.ts | 0 .../shared/services/notification.service.ts | 0 .../app/shared/services/storage.service.ts | 0 .../src/app/shared/utils/validators.ts | 0 .../features/checkout/checkout.component.css | 0 .../features/checkout/checkout.component.html | 0 .../checkout/checkout.component.spec.ts | 0 .../features/checkout/checkout.component.ts | 0 .../forgot-password.component.css | 0 .../forgot-password.component.html | 0 .../forgot-password.component.spec.ts | 0 .../forgot-password.component.ts | 0 .../user/features/login/login.component.css | 0 .../user/features/login/login.component.html | 0 .../features/login/login.component.spec.ts | 0 .../user/features/login/login.component.ts | 0 .../my-bookings/my-bookings.component.css | 0 .../my-bookings/my-bookings.component.html | 0 .../my-bookings/my-bookings.component.spec.ts | 0 .../my-bookings/my-bookings.component.ts | 0 .../features/profile/profile.component.css | 0 .../features/profile/profile.component.html | 0 .../profile/profile.component.spec.ts | 0 .../features/profile/profile.component.ts | 0 .../user/features/signup/signup.component.css | 0 .../features/signup/signup.component.html | 0 .../features/signup/signup.component.spec.ts | 0 .../user/features/signup/signup.component.ts | 0 .../venue-details/venue-details.component.css | 0 .../venue-details.component.html | 0 .../venue-details.component.spec.ts | 0 .../venue-details/venue-details.component.ts | 0 .../venue-list/venue-list.component.css | 0 .../venue-list/venue-list.component.html | 0 .../venue-list/venue-list.component.spec.ts | 0 .../venue-list/venue-list.component.ts | 0 .../src/app/user/guards/user-auth.guard.ts | 0 .../interceptors/user-auth.interceptor.ts | 0 .../user-layout/user-layout.component.css | 0 .../user-layout/user-layout.component.html | 0 .../user-layout/user-layout.component.spec.ts | 0 .../user-layout/user-layout.component.ts | 0 .../src/app/user/models/index.ts | 0 .../user/repositories/booking.repository.ts | 0 .../app/user/repositories/user.repository.ts | 0 .../app/user/repositories/venue.repository.ts | 0 .../src/app/user/routes/user.routes.ts | 0 .../src/app/user/services/booking.service.ts | 0 .../src/app/user/services/user.service.ts | 0 .../src/app/user/services/venue.service.ts | 0 .../analytics/analytics.component.css | 0 .../analytics/analytics.component.html | 0 .../analytics/analytics.component.spec.ts | 0 .../features/analytics/analytics.component.ts | 0 .../features/bookings/bookings.component.css | 0 .../features/bookings/bookings.component.html | 0 .../bookings/bookings.component.spec.ts | 0 .../features/bookings/bookings.component.ts | 0 .../create-venue/create-venue.component.css | 0 .../create-venue/create-venue.component.html | 0 .../create-venue.component.spec.ts | 0 .../create-venue/create-venue.component.ts | 0 .../dashboard/dashboard.component.css | 0 .../dashboard/dashboard.component.html | 0 .../dashboard/dashboard.component.spec.ts | 0 .../features/dashboard/dashboard.component.ts | 0 .../edit-venue/edit-venue.component.css | 0 .../edit-venue/edit-venue.component.html | 0 .../edit-venue/edit-venue.component.spec.ts | 0 .../edit-venue/edit-venue.component.ts | 0 .../forgot-password.component.css | 0 .../forgot-password.component.html | 0 .../forgot-password.component.spec.ts | 0 .../forgot-password.component.ts | 0 .../vendor/features/login/login.component.css | 0 .../features/login/login.component.html | 0 .../features/login/login.component.spec.ts | 0 .../vendor/features/login/login.component.ts | 0 .../features/settings/settings.component.css | 0 .../features/settings/settings.component.html | 0 .../settings/settings.component.spec.ts | 0 .../features/settings/settings.component.ts | 0 .../features/signup/signup.component.css | 0 .../features/signup/signup.component.html | 0 .../features/signup/signup.component.spec.ts | 0 .../features/signup/signup.component.ts | 0 .../venue-list/venue-list.component.css | 0 .../venue-list/venue-list.component.html | 0 .../venue-list/venue-list.component.spec.ts | 0 .../venue-list/venue-list.component.ts | 0 .../app/vendor/guards/vendor-auth.guard.ts | 0 .../interceptors/vendor-auth.interceptor.ts | 0 .../vendor-layout/vendor-layout.component.css | 0 .../vendor-layout.component.html | 0 .../vendor-layout.component.spec.ts | 0 .../vendor-layout/vendor-layout.component.ts | 0 .../src/app/vendor/models/index.ts | 0 .../repositories/analytics.repository.ts | 0 .../vendor/repositories/booking.repository.ts | 0 .../vendor/repositories/vendor.repository.ts | 0 .../vendor/repositories/venue.repository.ts | 0 .../src/app/vendor/routes/vendor.routes.ts | 0 .../app/vendor/services/analytics.service.ts | 0 .../app/vendor/services/booking.service.ts | 0 .../src/app/vendor/services/vendor.service.ts | 0 .../src/app/vendor/services/venue.service.ts | 0 client/{book-my-venue => }/src/index.html | 0 client/{book-my-venue => }/src/main.ts | 0 client/{book-my-venue => }/src/styles.css | 0 client/{book-my-venue => }/tsconfig.app.json | 0 client/{book-my-venue => }/tsconfig.json | 0 client/{book-my-venue => }/tsconfig.spec.json | 0 250 files changed, 75 deletions(-) rename client/{book-my-venue => }/.editorconfig (100%) rename client/{book-my-venue => }/.gitignore (100%) rename client/{book-my-venue => }/.postcssrc.json (100%) rename client/{book-my-venue => }/.prettierrc (100%) rename client/{book-my-venue => }/README.md (100%) rename client/{book-my-venue => }/angular.json (100%) delete mode 100644 client/book-my-venue/.vscode/extensions.json delete mode 100644 client/book-my-venue/.vscode/launch.json delete mode 100644 client/book-my-venue/.vscode/mcp.json delete mode 100644 client/book-my-venue/.vscode/tasks.json rename client/{book-my-venue => }/eslint.config.js (100%) rename client/{book-my-venue => }/package-lock.json (100%) rename client/{book-my-venue => }/package.json (100%) rename client/{book-my-venue => }/public/favicon.ico (100%) rename client/{book-my-venue => }/src/app/admin/features/analytics/analytics.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/analytics/analytics.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/analytics/analytics.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/analytics/analytics.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/bookings/bookings.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/bookings/bookings.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/bookings/bookings.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/bookings/bookings.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/dashboard/dashboard.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/dashboard/dashboard.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/dashboard/dashboard.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/dashboard/dashboard.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/forgot-password/forgot-password.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/forgot-password/forgot-password.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/forgot-password/forgot-password.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/forgot-password/forgot-password.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/login/login.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/login/login.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/login/login.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/login/login.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/settings/settings.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/settings/settings.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/settings/settings.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/settings/settings.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/users/users.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/users/users.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/users/users.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/users/users.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/vendors/vendors.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/vendors/vendors.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/vendors/vendors.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/vendors/vendors.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/venues/venues.component.css (100%) rename client/{book-my-venue => }/src/app/admin/features/venues/venues.component.html (100%) rename client/{book-my-venue => }/src/app/admin/features/venues/venues.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/features/venues/venues.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/guards/admin-auth.guard.ts (100%) rename client/{book-my-venue => }/src/app/admin/interceptors/admin-auth.interceptor.ts (100%) rename client/{book-my-venue => }/src/app/admin/layouts/admin-layout/admin-layout.component.css (100%) rename client/{book-my-venue => }/src/app/admin/layouts/admin-layout/admin-layout.component.html (100%) rename client/{book-my-venue => }/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/admin/layouts/admin-layout/admin-layout.component.ts (100%) rename client/{book-my-venue => }/src/app/admin/models/index.ts (100%) rename client/{book-my-venue => }/src/app/admin/repositories/analytics.repository.ts (100%) rename client/{book-my-venue => }/src/app/admin/repositories/booking.repository.ts (100%) rename client/{book-my-venue => }/src/app/admin/repositories/user.repository.ts (100%) rename client/{book-my-venue => }/src/app/admin/repositories/vendor.repository.ts (100%) rename client/{book-my-venue => }/src/app/admin/repositories/venue.repository.ts (100%) rename client/{book-my-venue => }/src/app/admin/routes/admin.routes.ts (100%) rename client/{book-my-venue => }/src/app/admin/services/analytics.service.ts (100%) rename client/{book-my-venue => }/src/app/admin/services/booking.service.ts (100%) rename client/{book-my-venue => }/src/app/admin/services/user.service.ts (100%) rename client/{book-my-venue => }/src/app/admin/services/vendor.service.ts (100%) rename client/{book-my-venue => }/src/app/admin/services/venue.service.ts (100%) rename client/{book-my-venue => }/src/app/app.config.ts (100%) rename client/{book-my-venue => }/src/app/app.css (100%) rename client/{book-my-venue => }/src/app/app.html (100%) rename client/{book-my-venue => }/src/app/app.routes.ts (100%) rename client/{book-my-venue => }/src/app/app.spec.ts (100%) rename client/{book-my-venue => }/src/app/app.ts (100%) rename client/{book-my-venue => }/src/app/core/app.routes.ts (100%) rename client/{book-my-venue => }/src/app/core/config/environment.prod.ts (100%) rename client/{book-my-venue => }/src/app/core/config/environment.ts (100%) rename client/{book-my-venue => }/src/app/core/providers/app-providers.ts (100%) rename client/{book-my-venue => }/src/app/core/tokens/injection-tokens.ts (100%) rename client/{book-my-venue => }/src/app/landing/landing.component.css (100%) rename client/{book-my-venue => }/src/app/landing/landing.component.html (100%) rename client/{book-my-venue => }/src/app/landing/landing.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/landing/landing.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/button/button.component.css (100%) rename client/{book-my-venue => }/src/app/shared/components/button/button.component.html (100%) rename client/{book-my-venue => }/src/app/shared/components/button/button.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/button/button.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/confirm-dialog/confirm-dialog.component.css (100%) rename client/{book-my-venue => }/src/app/shared/components/confirm-dialog/confirm-dialog.component.html (100%) rename client/{book-my-venue => }/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/empty-state/empty-state.component.css (100%) rename client/{book-my-venue => }/src/app/shared/components/empty-state/empty-state.component.html (100%) rename client/{book-my-venue => }/src/app/shared/components/empty-state/empty-state.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/empty-state/empty-state.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/input/input.component.css (100%) rename client/{book-my-venue => }/src/app/shared/components/input/input.component.html (100%) rename client/{book-my-venue => }/src/app/shared/components/input/input.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/input/input.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/loader/loader.component.css (100%) rename client/{book-my-venue => }/src/app/shared/components/loader/loader.component.html (100%) rename client/{book-my-venue => }/src/app/shared/components/loader/loader.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/loader/loader.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/modal/modal.component.css (100%) rename client/{book-my-venue => }/src/app/shared/components/modal/modal.component.html (100%) rename client/{book-my-venue => }/src/app/shared/components/modal/modal.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/modal/modal.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/pagination/pagination.component.css (100%) rename client/{book-my-venue => }/src/app/shared/components/pagination/pagination.component.html (100%) rename client/{book-my-venue => }/src/app/shared/components/pagination/pagination.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/pagination/pagination.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/table/table.component.css (100%) rename client/{book-my-venue => }/src/app/shared/components/table/table.component.html (100%) rename client/{book-my-venue => }/src/app/shared/components/table/table.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/shared/components/table/table.component.ts (100%) rename client/{book-my-venue => }/src/app/shared/constants/api-endpoints.constant.ts (100%) rename client/{book-my-venue => }/src/app/shared/constants/index.ts (100%) rename client/{book-my-venue => }/src/app/shared/constants/route-paths.constant.ts (100%) rename client/{book-my-venue => }/src/app/shared/directives/click-outside.directive.ts (100%) rename client/{book-my-venue => }/src/app/shared/enums/booking-status.enum.ts (100%) rename client/{book-my-venue => }/src/app/shared/enums/index.ts (100%) rename client/{book-my-venue => }/src/app/shared/enums/user-role.enum.ts (100%) rename client/{book-my-venue => }/src/app/shared/enums/venue-status.enum.ts (100%) rename client/{book-my-venue => }/src/app/shared/guards/admin-auth.guard.ts (100%) rename client/{book-my-venue => }/src/app/shared/guards/guest.guard.ts (100%) rename client/{book-my-venue => }/src/app/shared/guards/user-auth.guard.ts (100%) rename client/{book-my-venue => }/src/app/shared/guards/vendor-auth.guard.ts (100%) rename client/{book-my-venue => }/src/app/shared/interceptors/error.interceptor.ts (100%) rename client/{book-my-venue => }/src/app/shared/models/api-response.model.ts (100%) rename client/{book-my-venue => }/src/app/shared/models/auth-response.model.ts (100%) rename client/{book-my-venue => }/src/app/shared/models/booking.model.ts (100%) rename client/{book-my-venue => }/src/app/shared/models/index.ts (100%) rename client/{book-my-venue => }/src/app/shared/models/user.model.ts (100%) rename client/{book-my-venue => }/src/app/shared/models/vendor.model.ts (100%) rename client/{book-my-venue => }/src/app/shared/models/venue.model.ts (100%) rename client/{book-my-venue => }/src/app/shared/pipes/currency-format.pipe.ts (100%) rename client/{book-my-venue => }/src/app/shared/pipes/date-format.pipe.ts (100%) rename client/{book-my-venue => }/src/app/shared/services/auth.repository.ts (100%) rename client/{book-my-venue => }/src/app/shared/services/auth.service.ts (100%) rename client/{book-my-venue => }/src/app/shared/services/notification.service.ts (100%) rename client/{book-my-venue => }/src/app/shared/services/storage.service.ts (100%) rename client/{book-my-venue => }/src/app/shared/utils/validators.ts (100%) rename client/{book-my-venue => }/src/app/user/features/checkout/checkout.component.css (100%) rename client/{book-my-venue => }/src/app/user/features/checkout/checkout.component.html (100%) rename client/{book-my-venue => }/src/app/user/features/checkout/checkout.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/features/checkout/checkout.component.ts (100%) rename client/{book-my-venue => }/src/app/user/features/forgot-password/forgot-password.component.css (100%) rename client/{book-my-venue => }/src/app/user/features/forgot-password/forgot-password.component.html (100%) rename client/{book-my-venue => }/src/app/user/features/forgot-password/forgot-password.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/features/forgot-password/forgot-password.component.ts (100%) rename client/{book-my-venue => }/src/app/user/features/login/login.component.css (100%) rename client/{book-my-venue => }/src/app/user/features/login/login.component.html (100%) rename client/{book-my-venue => }/src/app/user/features/login/login.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/features/login/login.component.ts (100%) rename client/{book-my-venue => }/src/app/user/features/my-bookings/my-bookings.component.css (100%) rename client/{book-my-venue => }/src/app/user/features/my-bookings/my-bookings.component.html (100%) rename client/{book-my-venue => }/src/app/user/features/my-bookings/my-bookings.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/features/my-bookings/my-bookings.component.ts (100%) rename client/{book-my-venue => }/src/app/user/features/profile/profile.component.css (100%) rename client/{book-my-venue => }/src/app/user/features/profile/profile.component.html (100%) rename client/{book-my-venue => }/src/app/user/features/profile/profile.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/features/profile/profile.component.ts (100%) rename client/{book-my-venue => }/src/app/user/features/signup/signup.component.css (100%) rename client/{book-my-venue => }/src/app/user/features/signup/signup.component.html (100%) rename client/{book-my-venue => }/src/app/user/features/signup/signup.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/features/signup/signup.component.ts (100%) rename client/{book-my-venue => }/src/app/user/features/venue-details/venue-details.component.css (100%) rename client/{book-my-venue => }/src/app/user/features/venue-details/venue-details.component.html (100%) rename client/{book-my-venue => }/src/app/user/features/venue-details/venue-details.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/features/venue-details/venue-details.component.ts (100%) rename client/{book-my-venue => }/src/app/user/features/venue-list/venue-list.component.css (100%) rename client/{book-my-venue => }/src/app/user/features/venue-list/venue-list.component.html (100%) rename client/{book-my-venue => }/src/app/user/features/venue-list/venue-list.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/features/venue-list/venue-list.component.ts (100%) rename client/{book-my-venue => }/src/app/user/guards/user-auth.guard.ts (100%) rename client/{book-my-venue => }/src/app/user/interceptors/user-auth.interceptor.ts (100%) rename client/{book-my-venue => }/src/app/user/layouts/user-layout/user-layout.component.css (100%) rename client/{book-my-venue => }/src/app/user/layouts/user-layout/user-layout.component.html (100%) rename client/{book-my-venue => }/src/app/user/layouts/user-layout/user-layout.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/user/layouts/user-layout/user-layout.component.ts (100%) rename client/{book-my-venue => }/src/app/user/models/index.ts (100%) rename client/{book-my-venue => }/src/app/user/repositories/booking.repository.ts (100%) rename client/{book-my-venue => }/src/app/user/repositories/user.repository.ts (100%) rename client/{book-my-venue => }/src/app/user/repositories/venue.repository.ts (100%) rename client/{book-my-venue => }/src/app/user/routes/user.routes.ts (100%) rename client/{book-my-venue => }/src/app/user/services/booking.service.ts (100%) rename client/{book-my-venue => }/src/app/user/services/user.service.ts (100%) rename client/{book-my-venue => }/src/app/user/services/venue.service.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/analytics/analytics.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/analytics/analytics.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/analytics/analytics.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/analytics/analytics.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/bookings/bookings.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/bookings/bookings.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/bookings/bookings.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/bookings/bookings.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/create-venue/create-venue.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/create-venue/create-venue.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/create-venue/create-venue.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/create-venue/create-venue.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/dashboard/dashboard.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/dashboard/dashboard.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/dashboard/dashboard.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/dashboard/dashboard.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/edit-venue/edit-venue.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/edit-venue/edit-venue.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/edit-venue/edit-venue.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/forgot-password/forgot-password.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/forgot-password/forgot-password.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/forgot-password/forgot-password.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/login/login.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/login/login.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/login/login.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/login/login.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/settings/settings.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/settings/settings.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/settings/settings.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/settings/settings.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/signup/signup.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/signup/signup.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/signup/signup.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/signup/signup.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/venue-list/venue-list.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/features/venue-list/venue-list.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/features/venue-list/venue-list.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/features/venue-list/venue-list.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/guards/vendor-auth.guard.ts (100%) rename client/{book-my-venue => }/src/app/vendor/interceptors/vendor-auth.interceptor.ts (100%) rename client/{book-my-venue => }/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css (100%) rename client/{book-my-venue => }/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html (100%) rename client/{book-my-venue => }/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts (100%) rename client/{book-my-venue => }/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts (100%) rename client/{book-my-venue => }/src/app/vendor/models/index.ts (100%) rename client/{book-my-venue => }/src/app/vendor/repositories/analytics.repository.ts (100%) rename client/{book-my-venue => }/src/app/vendor/repositories/booking.repository.ts (100%) rename client/{book-my-venue => }/src/app/vendor/repositories/vendor.repository.ts (100%) rename client/{book-my-venue => }/src/app/vendor/repositories/venue.repository.ts (100%) rename client/{book-my-venue => }/src/app/vendor/routes/vendor.routes.ts (100%) rename client/{book-my-venue => }/src/app/vendor/services/analytics.service.ts (100%) rename client/{book-my-venue => }/src/app/vendor/services/booking.service.ts (100%) rename client/{book-my-venue => }/src/app/vendor/services/vendor.service.ts (100%) rename client/{book-my-venue => }/src/app/vendor/services/venue.service.ts (100%) rename client/{book-my-venue => }/src/index.html (100%) rename client/{book-my-venue => }/src/main.ts (100%) rename client/{book-my-venue => }/src/styles.css (100%) rename client/{book-my-venue => }/tsconfig.app.json (100%) rename client/{book-my-venue => }/tsconfig.json (100%) rename client/{book-my-venue => }/tsconfig.spec.json (100%) diff --git a/client/book-my-venue/.editorconfig b/client/.editorconfig similarity index 100% rename from client/book-my-venue/.editorconfig rename to client/.editorconfig diff --git a/client/book-my-venue/.gitignore b/client/.gitignore similarity index 100% rename from client/book-my-venue/.gitignore rename to client/.gitignore diff --git a/client/book-my-venue/.postcssrc.json b/client/.postcssrc.json similarity index 100% rename from client/book-my-venue/.postcssrc.json rename to client/.postcssrc.json diff --git a/client/book-my-venue/.prettierrc b/client/.prettierrc similarity index 100% rename from client/book-my-venue/.prettierrc rename to client/.prettierrc diff --git a/client/book-my-venue/README.md b/client/README.md similarity index 100% rename from client/book-my-venue/README.md rename to client/README.md diff --git a/client/book-my-venue/angular.json b/client/angular.json similarity index 100% rename from client/book-my-venue/angular.json rename to client/angular.json diff --git a/client/book-my-venue/.vscode/extensions.json b/client/book-my-venue/.vscode/extensions.json deleted file mode 100644 index 77b374577..000000000 --- a/client/book-my-venue/.vscode/extensions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 - "recommendations": ["angular.ng-template"] -} diff --git a/client/book-my-venue/.vscode/launch.json b/client/book-my-venue/.vscode/launch.json deleted file mode 100644 index 925af8370..000000000 --- a/client/book-my-venue/.vscode/launch.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "ng serve", - "type": "chrome", - "request": "launch", - "preLaunchTask": "npm: start", - "url": "http://localhost:4200/" - }, - { - "name": "ng test", - "type": "chrome", - "request": "launch", - "preLaunchTask": "npm: test", - "url": "http://localhost:9876/debug.html" - } - ] -} diff --git a/client/book-my-venue/.vscode/mcp.json b/client/book-my-venue/.vscode/mcp.json deleted file mode 100644 index 956af8c62..000000000 --- a/client/book-my-venue/.vscode/mcp.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - // For more information, visit: https://angular.dev/ai/mcp - "servers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] - } - } -} diff --git a/client/book-my-venue/.vscode/tasks.json b/client/book-my-venue/.vscode/tasks.json deleted file mode 100644 index 244306f98..000000000 --- a/client/book-my-venue/.vscode/tasks.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "start", - "isBackground": true, - "problemMatcher": { - "owner": "typescript", - "pattern": "$tsc", - "background": { - "activeOnStart": true, - "beginsPattern": { - "regexp": "Changes detected" - }, - "endsPattern": { - "regexp": "bundle generation (complete|failed)" - } - } - } - }, - { - "type": "npm", - "script": "test", - "isBackground": true, - "problemMatcher": { - "owner": "typescript", - "pattern": "$tsc", - "background": { - "activeOnStart": true, - "beginsPattern": { - "regexp": "Changes detected" - }, - "endsPattern": { - "regexp": "bundle generation (complete|failed)" - } - } - } - } - ] -} diff --git a/client/book-my-venue/eslint.config.js b/client/eslint.config.js similarity index 100% rename from client/book-my-venue/eslint.config.js rename to client/eslint.config.js diff --git a/client/book-my-venue/package-lock.json b/client/package-lock.json similarity index 100% rename from client/book-my-venue/package-lock.json rename to client/package-lock.json diff --git a/client/book-my-venue/package.json b/client/package.json similarity index 100% rename from client/book-my-venue/package.json rename to client/package.json diff --git a/client/book-my-venue/public/favicon.ico b/client/public/favicon.ico similarity index 100% rename from client/book-my-venue/public/favicon.ico rename to client/public/favicon.ico diff --git a/client/book-my-venue/src/app/admin/features/analytics/analytics.component.css b/client/src/app/admin/features/analytics/analytics.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/analytics/analytics.component.css rename to client/src/app/admin/features/analytics/analytics.component.css diff --git a/client/book-my-venue/src/app/admin/features/analytics/analytics.component.html b/client/src/app/admin/features/analytics/analytics.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/analytics/analytics.component.html rename to client/src/app/admin/features/analytics/analytics.component.html diff --git a/client/book-my-venue/src/app/admin/features/analytics/analytics.component.spec.ts b/client/src/app/admin/features/analytics/analytics.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/analytics/analytics.component.spec.ts rename to client/src/app/admin/features/analytics/analytics.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/analytics/analytics.component.ts b/client/src/app/admin/features/analytics/analytics.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/analytics/analytics.component.ts rename to client/src/app/admin/features/analytics/analytics.component.ts diff --git a/client/book-my-venue/src/app/admin/features/bookings/bookings.component.css b/client/src/app/admin/features/bookings/bookings.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/bookings/bookings.component.css rename to client/src/app/admin/features/bookings/bookings.component.css diff --git a/client/book-my-venue/src/app/admin/features/bookings/bookings.component.html b/client/src/app/admin/features/bookings/bookings.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/bookings/bookings.component.html rename to client/src/app/admin/features/bookings/bookings.component.html diff --git a/client/book-my-venue/src/app/admin/features/bookings/bookings.component.spec.ts b/client/src/app/admin/features/bookings/bookings.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/bookings/bookings.component.spec.ts rename to client/src/app/admin/features/bookings/bookings.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/bookings/bookings.component.ts b/client/src/app/admin/features/bookings/bookings.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/bookings/bookings.component.ts rename to client/src/app/admin/features/bookings/bookings.component.ts diff --git a/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.css b/client/src/app/admin/features/dashboard/dashboard.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.css rename to client/src/app/admin/features/dashboard/dashboard.component.css diff --git a/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.html b/client/src/app/admin/features/dashboard/dashboard.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.html rename to client/src/app/admin/features/dashboard/dashboard.component.html diff --git a/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.spec.ts b/client/src/app/admin/features/dashboard/dashboard.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.spec.ts rename to client/src/app/admin/features/dashboard/dashboard.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.ts b/client/src/app/admin/features/dashboard/dashboard.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/dashboard/dashboard.component.ts rename to client/src/app/admin/features/dashboard/dashboard.component.ts diff --git a/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.css b/client/src/app/admin/features/forgot-password/forgot-password.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.css rename to client/src/app/admin/features/forgot-password/forgot-password.component.css diff --git a/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.html b/client/src/app/admin/features/forgot-password/forgot-password.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.html rename to client/src/app/admin/features/forgot-password/forgot-password.component.html diff --git a/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.spec.ts b/client/src/app/admin/features/forgot-password/forgot-password.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.spec.ts rename to client/src/app/admin/features/forgot-password/forgot-password.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.ts b/client/src/app/admin/features/forgot-password/forgot-password.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/forgot-password/forgot-password.component.ts rename to client/src/app/admin/features/forgot-password/forgot-password.component.ts diff --git a/client/book-my-venue/src/app/admin/features/login/login.component.css b/client/src/app/admin/features/login/login.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/login/login.component.css rename to client/src/app/admin/features/login/login.component.css diff --git a/client/book-my-venue/src/app/admin/features/login/login.component.html b/client/src/app/admin/features/login/login.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/login/login.component.html rename to client/src/app/admin/features/login/login.component.html diff --git a/client/book-my-venue/src/app/admin/features/login/login.component.spec.ts b/client/src/app/admin/features/login/login.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/login/login.component.spec.ts rename to client/src/app/admin/features/login/login.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/login/login.component.ts b/client/src/app/admin/features/login/login.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/login/login.component.ts rename to client/src/app/admin/features/login/login.component.ts diff --git a/client/book-my-venue/src/app/admin/features/settings/settings.component.css b/client/src/app/admin/features/settings/settings.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/settings/settings.component.css rename to client/src/app/admin/features/settings/settings.component.css diff --git a/client/book-my-venue/src/app/admin/features/settings/settings.component.html b/client/src/app/admin/features/settings/settings.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/settings/settings.component.html rename to client/src/app/admin/features/settings/settings.component.html diff --git a/client/book-my-venue/src/app/admin/features/settings/settings.component.spec.ts b/client/src/app/admin/features/settings/settings.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/settings/settings.component.spec.ts rename to client/src/app/admin/features/settings/settings.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/settings/settings.component.ts b/client/src/app/admin/features/settings/settings.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/settings/settings.component.ts rename to client/src/app/admin/features/settings/settings.component.ts diff --git a/client/book-my-venue/src/app/admin/features/users/users.component.css b/client/src/app/admin/features/users/users.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/users/users.component.css rename to client/src/app/admin/features/users/users.component.css diff --git a/client/book-my-venue/src/app/admin/features/users/users.component.html b/client/src/app/admin/features/users/users.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/users/users.component.html rename to client/src/app/admin/features/users/users.component.html diff --git a/client/book-my-venue/src/app/admin/features/users/users.component.spec.ts b/client/src/app/admin/features/users/users.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/users/users.component.spec.ts rename to client/src/app/admin/features/users/users.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/users/users.component.ts b/client/src/app/admin/features/users/users.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/users/users.component.ts rename to client/src/app/admin/features/users/users.component.ts diff --git a/client/book-my-venue/src/app/admin/features/vendors/vendors.component.css b/client/src/app/admin/features/vendors/vendors.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/vendors/vendors.component.css rename to client/src/app/admin/features/vendors/vendors.component.css diff --git a/client/book-my-venue/src/app/admin/features/vendors/vendors.component.html b/client/src/app/admin/features/vendors/vendors.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/vendors/vendors.component.html rename to client/src/app/admin/features/vendors/vendors.component.html diff --git a/client/book-my-venue/src/app/admin/features/vendors/vendors.component.spec.ts b/client/src/app/admin/features/vendors/vendors.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/vendors/vendors.component.spec.ts rename to client/src/app/admin/features/vendors/vendors.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/vendors/vendors.component.ts b/client/src/app/admin/features/vendors/vendors.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/vendors/vendors.component.ts rename to client/src/app/admin/features/vendors/vendors.component.ts diff --git a/client/book-my-venue/src/app/admin/features/venues/venues.component.css b/client/src/app/admin/features/venues/venues.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/features/venues/venues.component.css rename to client/src/app/admin/features/venues/venues.component.css diff --git a/client/book-my-venue/src/app/admin/features/venues/venues.component.html b/client/src/app/admin/features/venues/venues.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/features/venues/venues.component.html rename to client/src/app/admin/features/venues/venues.component.html diff --git a/client/book-my-venue/src/app/admin/features/venues/venues.component.spec.ts b/client/src/app/admin/features/venues/venues.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/venues/venues.component.spec.ts rename to client/src/app/admin/features/venues/venues.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/features/venues/venues.component.ts b/client/src/app/admin/features/venues/venues.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/features/venues/venues.component.ts rename to client/src/app/admin/features/venues/venues.component.ts diff --git a/client/book-my-venue/src/app/admin/guards/admin-auth.guard.ts b/client/src/app/admin/guards/admin-auth.guard.ts similarity index 100% rename from client/book-my-venue/src/app/admin/guards/admin-auth.guard.ts rename to client/src/app/admin/guards/admin-auth.guard.ts diff --git a/client/book-my-venue/src/app/admin/interceptors/admin-auth.interceptor.ts b/client/src/app/admin/interceptors/admin-auth.interceptor.ts similarity index 100% rename from client/book-my-venue/src/app/admin/interceptors/admin-auth.interceptor.ts rename to client/src/app/admin/interceptors/admin-auth.interceptor.ts diff --git a/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.css b/client/src/app/admin/layouts/admin-layout/admin-layout.component.css similarity index 100% rename from client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.css rename to client/src/app/admin/layouts/admin-layout/admin-layout.component.css diff --git a/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.html b/client/src/app/admin/layouts/admin-layout/admin-layout.component.html similarity index 100% rename from client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.html rename to client/src/app/admin/layouts/admin-layout/admin-layout.component.html diff --git a/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts b/client/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts rename to client/src/app/admin/layouts/admin-layout/admin-layout.component.spec.ts diff --git a/client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.ts b/client/src/app/admin/layouts/admin-layout/admin-layout.component.ts similarity index 100% rename from client/book-my-venue/src/app/admin/layouts/admin-layout/admin-layout.component.ts rename to client/src/app/admin/layouts/admin-layout/admin-layout.component.ts diff --git a/client/book-my-venue/src/app/admin/models/index.ts b/client/src/app/admin/models/index.ts similarity index 100% rename from client/book-my-venue/src/app/admin/models/index.ts rename to client/src/app/admin/models/index.ts diff --git a/client/book-my-venue/src/app/admin/repositories/analytics.repository.ts b/client/src/app/admin/repositories/analytics.repository.ts similarity index 100% rename from client/book-my-venue/src/app/admin/repositories/analytics.repository.ts rename to client/src/app/admin/repositories/analytics.repository.ts diff --git a/client/book-my-venue/src/app/admin/repositories/booking.repository.ts b/client/src/app/admin/repositories/booking.repository.ts similarity index 100% rename from client/book-my-venue/src/app/admin/repositories/booking.repository.ts rename to client/src/app/admin/repositories/booking.repository.ts diff --git a/client/book-my-venue/src/app/admin/repositories/user.repository.ts b/client/src/app/admin/repositories/user.repository.ts similarity index 100% rename from client/book-my-venue/src/app/admin/repositories/user.repository.ts rename to client/src/app/admin/repositories/user.repository.ts diff --git a/client/book-my-venue/src/app/admin/repositories/vendor.repository.ts b/client/src/app/admin/repositories/vendor.repository.ts similarity index 100% rename from client/book-my-venue/src/app/admin/repositories/vendor.repository.ts rename to client/src/app/admin/repositories/vendor.repository.ts diff --git a/client/book-my-venue/src/app/admin/repositories/venue.repository.ts b/client/src/app/admin/repositories/venue.repository.ts similarity index 100% rename from client/book-my-venue/src/app/admin/repositories/venue.repository.ts rename to client/src/app/admin/repositories/venue.repository.ts diff --git a/client/book-my-venue/src/app/admin/routes/admin.routes.ts b/client/src/app/admin/routes/admin.routes.ts similarity index 100% rename from client/book-my-venue/src/app/admin/routes/admin.routes.ts rename to client/src/app/admin/routes/admin.routes.ts diff --git a/client/book-my-venue/src/app/admin/services/analytics.service.ts b/client/src/app/admin/services/analytics.service.ts similarity index 100% rename from client/book-my-venue/src/app/admin/services/analytics.service.ts rename to client/src/app/admin/services/analytics.service.ts diff --git a/client/book-my-venue/src/app/admin/services/booking.service.ts b/client/src/app/admin/services/booking.service.ts similarity index 100% rename from client/book-my-venue/src/app/admin/services/booking.service.ts rename to client/src/app/admin/services/booking.service.ts diff --git a/client/book-my-venue/src/app/admin/services/user.service.ts b/client/src/app/admin/services/user.service.ts similarity index 100% rename from client/book-my-venue/src/app/admin/services/user.service.ts rename to client/src/app/admin/services/user.service.ts diff --git a/client/book-my-venue/src/app/admin/services/vendor.service.ts b/client/src/app/admin/services/vendor.service.ts similarity index 100% rename from client/book-my-venue/src/app/admin/services/vendor.service.ts rename to client/src/app/admin/services/vendor.service.ts diff --git a/client/book-my-venue/src/app/admin/services/venue.service.ts b/client/src/app/admin/services/venue.service.ts similarity index 100% rename from client/book-my-venue/src/app/admin/services/venue.service.ts rename to client/src/app/admin/services/venue.service.ts diff --git a/client/book-my-venue/src/app/app.config.ts b/client/src/app/app.config.ts similarity index 100% rename from client/book-my-venue/src/app/app.config.ts rename to client/src/app/app.config.ts diff --git a/client/book-my-venue/src/app/app.css b/client/src/app/app.css similarity index 100% rename from client/book-my-venue/src/app/app.css rename to client/src/app/app.css diff --git a/client/book-my-venue/src/app/app.html b/client/src/app/app.html similarity index 100% rename from client/book-my-venue/src/app/app.html rename to client/src/app/app.html diff --git a/client/book-my-venue/src/app/app.routes.ts b/client/src/app/app.routes.ts similarity index 100% rename from client/book-my-venue/src/app/app.routes.ts rename to client/src/app/app.routes.ts diff --git a/client/book-my-venue/src/app/app.spec.ts b/client/src/app/app.spec.ts similarity index 100% rename from client/book-my-venue/src/app/app.spec.ts rename to client/src/app/app.spec.ts diff --git a/client/book-my-venue/src/app/app.ts b/client/src/app/app.ts similarity index 100% rename from client/book-my-venue/src/app/app.ts rename to client/src/app/app.ts diff --git a/client/book-my-venue/src/app/core/app.routes.ts b/client/src/app/core/app.routes.ts similarity index 100% rename from client/book-my-venue/src/app/core/app.routes.ts rename to client/src/app/core/app.routes.ts diff --git a/client/book-my-venue/src/app/core/config/environment.prod.ts b/client/src/app/core/config/environment.prod.ts similarity index 100% rename from client/book-my-venue/src/app/core/config/environment.prod.ts rename to client/src/app/core/config/environment.prod.ts diff --git a/client/book-my-venue/src/app/core/config/environment.ts b/client/src/app/core/config/environment.ts similarity index 100% rename from client/book-my-venue/src/app/core/config/environment.ts rename to client/src/app/core/config/environment.ts diff --git a/client/book-my-venue/src/app/core/providers/app-providers.ts b/client/src/app/core/providers/app-providers.ts similarity index 100% rename from client/book-my-venue/src/app/core/providers/app-providers.ts rename to client/src/app/core/providers/app-providers.ts diff --git a/client/book-my-venue/src/app/core/tokens/injection-tokens.ts b/client/src/app/core/tokens/injection-tokens.ts similarity index 100% rename from client/book-my-venue/src/app/core/tokens/injection-tokens.ts rename to client/src/app/core/tokens/injection-tokens.ts diff --git a/client/book-my-venue/src/app/landing/landing.component.css b/client/src/app/landing/landing.component.css similarity index 100% rename from client/book-my-venue/src/app/landing/landing.component.css rename to client/src/app/landing/landing.component.css diff --git a/client/book-my-venue/src/app/landing/landing.component.html b/client/src/app/landing/landing.component.html similarity index 100% rename from client/book-my-venue/src/app/landing/landing.component.html rename to client/src/app/landing/landing.component.html diff --git a/client/book-my-venue/src/app/landing/landing.component.spec.ts b/client/src/app/landing/landing.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/landing/landing.component.spec.ts rename to client/src/app/landing/landing.component.spec.ts diff --git a/client/book-my-venue/src/app/landing/landing.component.ts b/client/src/app/landing/landing.component.ts similarity index 100% rename from client/book-my-venue/src/app/landing/landing.component.ts rename to client/src/app/landing/landing.component.ts diff --git a/client/book-my-venue/src/app/shared/components/button/button.component.css b/client/src/app/shared/components/button/button.component.css similarity index 100% rename from client/book-my-venue/src/app/shared/components/button/button.component.css rename to client/src/app/shared/components/button/button.component.css diff --git a/client/book-my-venue/src/app/shared/components/button/button.component.html b/client/src/app/shared/components/button/button.component.html similarity index 100% rename from client/book-my-venue/src/app/shared/components/button/button.component.html rename to client/src/app/shared/components/button/button.component.html diff --git a/client/book-my-venue/src/app/shared/components/button/button.component.spec.ts b/client/src/app/shared/components/button/button.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/button/button.component.spec.ts rename to client/src/app/shared/components/button/button.component.spec.ts diff --git a/client/book-my-venue/src/app/shared/components/button/button.component.ts b/client/src/app/shared/components/button/button.component.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/button/button.component.ts rename to client/src/app/shared/components/button/button.component.ts diff --git a/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.css b/client/src/app/shared/components/confirm-dialog/confirm-dialog.component.css similarity index 100% rename from client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.css rename to client/src/app/shared/components/confirm-dialog/confirm-dialog.component.css diff --git a/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.html b/client/src/app/shared/components/confirm-dialog/confirm-dialog.component.html similarity index 100% rename from client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.html rename to client/src/app/shared/components/confirm-dialog/confirm-dialog.component.html diff --git a/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts b/client/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts rename to client/src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts diff --git a/client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts b/client/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts rename to client/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts diff --git a/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.css b/client/src/app/shared/components/empty-state/empty-state.component.css similarity index 100% rename from client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.css rename to client/src/app/shared/components/empty-state/empty-state.component.css diff --git a/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.html b/client/src/app/shared/components/empty-state/empty-state.component.html similarity index 100% rename from client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.html rename to client/src/app/shared/components/empty-state/empty-state.component.html diff --git a/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.spec.ts b/client/src/app/shared/components/empty-state/empty-state.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.spec.ts rename to client/src/app/shared/components/empty-state/empty-state.component.spec.ts diff --git a/client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.ts b/client/src/app/shared/components/empty-state/empty-state.component.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/empty-state/empty-state.component.ts rename to client/src/app/shared/components/empty-state/empty-state.component.ts diff --git a/client/book-my-venue/src/app/shared/components/input/input.component.css b/client/src/app/shared/components/input/input.component.css similarity index 100% rename from client/book-my-venue/src/app/shared/components/input/input.component.css rename to client/src/app/shared/components/input/input.component.css diff --git a/client/book-my-venue/src/app/shared/components/input/input.component.html b/client/src/app/shared/components/input/input.component.html similarity index 100% rename from client/book-my-venue/src/app/shared/components/input/input.component.html rename to client/src/app/shared/components/input/input.component.html diff --git a/client/book-my-venue/src/app/shared/components/input/input.component.spec.ts b/client/src/app/shared/components/input/input.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/input/input.component.spec.ts rename to client/src/app/shared/components/input/input.component.spec.ts diff --git a/client/book-my-venue/src/app/shared/components/input/input.component.ts b/client/src/app/shared/components/input/input.component.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/input/input.component.ts rename to client/src/app/shared/components/input/input.component.ts diff --git a/client/book-my-venue/src/app/shared/components/loader/loader.component.css b/client/src/app/shared/components/loader/loader.component.css similarity index 100% rename from client/book-my-venue/src/app/shared/components/loader/loader.component.css rename to client/src/app/shared/components/loader/loader.component.css diff --git a/client/book-my-venue/src/app/shared/components/loader/loader.component.html b/client/src/app/shared/components/loader/loader.component.html similarity index 100% rename from client/book-my-venue/src/app/shared/components/loader/loader.component.html rename to client/src/app/shared/components/loader/loader.component.html diff --git a/client/book-my-venue/src/app/shared/components/loader/loader.component.spec.ts b/client/src/app/shared/components/loader/loader.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/loader/loader.component.spec.ts rename to client/src/app/shared/components/loader/loader.component.spec.ts diff --git a/client/book-my-venue/src/app/shared/components/loader/loader.component.ts b/client/src/app/shared/components/loader/loader.component.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/loader/loader.component.ts rename to client/src/app/shared/components/loader/loader.component.ts diff --git a/client/book-my-venue/src/app/shared/components/modal/modal.component.css b/client/src/app/shared/components/modal/modal.component.css similarity index 100% rename from client/book-my-venue/src/app/shared/components/modal/modal.component.css rename to client/src/app/shared/components/modal/modal.component.css diff --git a/client/book-my-venue/src/app/shared/components/modal/modal.component.html b/client/src/app/shared/components/modal/modal.component.html similarity index 100% rename from client/book-my-venue/src/app/shared/components/modal/modal.component.html rename to client/src/app/shared/components/modal/modal.component.html diff --git a/client/book-my-venue/src/app/shared/components/modal/modal.component.spec.ts b/client/src/app/shared/components/modal/modal.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/modal/modal.component.spec.ts rename to client/src/app/shared/components/modal/modal.component.spec.ts diff --git a/client/book-my-venue/src/app/shared/components/modal/modal.component.ts b/client/src/app/shared/components/modal/modal.component.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/modal/modal.component.ts rename to client/src/app/shared/components/modal/modal.component.ts diff --git a/client/book-my-venue/src/app/shared/components/pagination/pagination.component.css b/client/src/app/shared/components/pagination/pagination.component.css similarity index 100% rename from client/book-my-venue/src/app/shared/components/pagination/pagination.component.css rename to client/src/app/shared/components/pagination/pagination.component.css diff --git a/client/book-my-venue/src/app/shared/components/pagination/pagination.component.html b/client/src/app/shared/components/pagination/pagination.component.html similarity index 100% rename from client/book-my-venue/src/app/shared/components/pagination/pagination.component.html rename to client/src/app/shared/components/pagination/pagination.component.html diff --git a/client/book-my-venue/src/app/shared/components/pagination/pagination.component.spec.ts b/client/src/app/shared/components/pagination/pagination.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/pagination/pagination.component.spec.ts rename to client/src/app/shared/components/pagination/pagination.component.spec.ts diff --git a/client/book-my-venue/src/app/shared/components/pagination/pagination.component.ts b/client/src/app/shared/components/pagination/pagination.component.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/pagination/pagination.component.ts rename to client/src/app/shared/components/pagination/pagination.component.ts diff --git a/client/book-my-venue/src/app/shared/components/table/table.component.css b/client/src/app/shared/components/table/table.component.css similarity index 100% rename from client/book-my-venue/src/app/shared/components/table/table.component.css rename to client/src/app/shared/components/table/table.component.css diff --git a/client/book-my-venue/src/app/shared/components/table/table.component.html b/client/src/app/shared/components/table/table.component.html similarity index 100% rename from client/book-my-venue/src/app/shared/components/table/table.component.html rename to client/src/app/shared/components/table/table.component.html diff --git a/client/book-my-venue/src/app/shared/components/table/table.component.spec.ts b/client/src/app/shared/components/table/table.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/table/table.component.spec.ts rename to client/src/app/shared/components/table/table.component.spec.ts diff --git a/client/book-my-venue/src/app/shared/components/table/table.component.ts b/client/src/app/shared/components/table/table.component.ts similarity index 100% rename from client/book-my-venue/src/app/shared/components/table/table.component.ts rename to client/src/app/shared/components/table/table.component.ts diff --git a/client/book-my-venue/src/app/shared/constants/api-endpoints.constant.ts b/client/src/app/shared/constants/api-endpoints.constant.ts similarity index 100% rename from client/book-my-venue/src/app/shared/constants/api-endpoints.constant.ts rename to client/src/app/shared/constants/api-endpoints.constant.ts diff --git a/client/book-my-venue/src/app/shared/constants/index.ts b/client/src/app/shared/constants/index.ts similarity index 100% rename from client/book-my-venue/src/app/shared/constants/index.ts rename to client/src/app/shared/constants/index.ts diff --git a/client/book-my-venue/src/app/shared/constants/route-paths.constant.ts b/client/src/app/shared/constants/route-paths.constant.ts similarity index 100% rename from client/book-my-venue/src/app/shared/constants/route-paths.constant.ts rename to client/src/app/shared/constants/route-paths.constant.ts diff --git a/client/book-my-venue/src/app/shared/directives/click-outside.directive.ts b/client/src/app/shared/directives/click-outside.directive.ts similarity index 100% rename from client/book-my-venue/src/app/shared/directives/click-outside.directive.ts rename to client/src/app/shared/directives/click-outside.directive.ts diff --git a/client/book-my-venue/src/app/shared/enums/booking-status.enum.ts b/client/src/app/shared/enums/booking-status.enum.ts similarity index 100% rename from client/book-my-venue/src/app/shared/enums/booking-status.enum.ts rename to client/src/app/shared/enums/booking-status.enum.ts diff --git a/client/book-my-venue/src/app/shared/enums/index.ts b/client/src/app/shared/enums/index.ts similarity index 100% rename from client/book-my-venue/src/app/shared/enums/index.ts rename to client/src/app/shared/enums/index.ts diff --git a/client/book-my-venue/src/app/shared/enums/user-role.enum.ts b/client/src/app/shared/enums/user-role.enum.ts similarity index 100% rename from client/book-my-venue/src/app/shared/enums/user-role.enum.ts rename to client/src/app/shared/enums/user-role.enum.ts diff --git a/client/book-my-venue/src/app/shared/enums/venue-status.enum.ts b/client/src/app/shared/enums/venue-status.enum.ts similarity index 100% rename from client/book-my-venue/src/app/shared/enums/venue-status.enum.ts rename to client/src/app/shared/enums/venue-status.enum.ts diff --git a/client/book-my-venue/src/app/shared/guards/admin-auth.guard.ts b/client/src/app/shared/guards/admin-auth.guard.ts similarity index 100% rename from client/book-my-venue/src/app/shared/guards/admin-auth.guard.ts rename to client/src/app/shared/guards/admin-auth.guard.ts diff --git a/client/book-my-venue/src/app/shared/guards/guest.guard.ts b/client/src/app/shared/guards/guest.guard.ts similarity index 100% rename from client/book-my-venue/src/app/shared/guards/guest.guard.ts rename to client/src/app/shared/guards/guest.guard.ts diff --git a/client/book-my-venue/src/app/shared/guards/user-auth.guard.ts b/client/src/app/shared/guards/user-auth.guard.ts similarity index 100% rename from client/book-my-venue/src/app/shared/guards/user-auth.guard.ts rename to client/src/app/shared/guards/user-auth.guard.ts diff --git a/client/book-my-venue/src/app/shared/guards/vendor-auth.guard.ts b/client/src/app/shared/guards/vendor-auth.guard.ts similarity index 100% rename from client/book-my-venue/src/app/shared/guards/vendor-auth.guard.ts rename to client/src/app/shared/guards/vendor-auth.guard.ts diff --git a/client/book-my-venue/src/app/shared/interceptors/error.interceptor.ts b/client/src/app/shared/interceptors/error.interceptor.ts similarity index 100% rename from client/book-my-venue/src/app/shared/interceptors/error.interceptor.ts rename to client/src/app/shared/interceptors/error.interceptor.ts diff --git a/client/book-my-venue/src/app/shared/models/api-response.model.ts b/client/src/app/shared/models/api-response.model.ts similarity index 100% rename from client/book-my-venue/src/app/shared/models/api-response.model.ts rename to client/src/app/shared/models/api-response.model.ts diff --git a/client/book-my-venue/src/app/shared/models/auth-response.model.ts b/client/src/app/shared/models/auth-response.model.ts similarity index 100% rename from client/book-my-venue/src/app/shared/models/auth-response.model.ts rename to client/src/app/shared/models/auth-response.model.ts diff --git a/client/book-my-venue/src/app/shared/models/booking.model.ts b/client/src/app/shared/models/booking.model.ts similarity index 100% rename from client/book-my-venue/src/app/shared/models/booking.model.ts rename to client/src/app/shared/models/booking.model.ts diff --git a/client/book-my-venue/src/app/shared/models/index.ts b/client/src/app/shared/models/index.ts similarity index 100% rename from client/book-my-venue/src/app/shared/models/index.ts rename to client/src/app/shared/models/index.ts diff --git a/client/book-my-venue/src/app/shared/models/user.model.ts b/client/src/app/shared/models/user.model.ts similarity index 100% rename from client/book-my-venue/src/app/shared/models/user.model.ts rename to client/src/app/shared/models/user.model.ts diff --git a/client/book-my-venue/src/app/shared/models/vendor.model.ts b/client/src/app/shared/models/vendor.model.ts similarity index 100% rename from client/book-my-venue/src/app/shared/models/vendor.model.ts rename to client/src/app/shared/models/vendor.model.ts diff --git a/client/book-my-venue/src/app/shared/models/venue.model.ts b/client/src/app/shared/models/venue.model.ts similarity index 100% rename from client/book-my-venue/src/app/shared/models/venue.model.ts rename to client/src/app/shared/models/venue.model.ts diff --git a/client/book-my-venue/src/app/shared/pipes/currency-format.pipe.ts b/client/src/app/shared/pipes/currency-format.pipe.ts similarity index 100% rename from client/book-my-venue/src/app/shared/pipes/currency-format.pipe.ts rename to client/src/app/shared/pipes/currency-format.pipe.ts diff --git a/client/book-my-venue/src/app/shared/pipes/date-format.pipe.ts b/client/src/app/shared/pipes/date-format.pipe.ts similarity index 100% rename from client/book-my-venue/src/app/shared/pipes/date-format.pipe.ts rename to client/src/app/shared/pipes/date-format.pipe.ts diff --git a/client/book-my-venue/src/app/shared/services/auth.repository.ts b/client/src/app/shared/services/auth.repository.ts similarity index 100% rename from client/book-my-venue/src/app/shared/services/auth.repository.ts rename to client/src/app/shared/services/auth.repository.ts diff --git a/client/book-my-venue/src/app/shared/services/auth.service.ts b/client/src/app/shared/services/auth.service.ts similarity index 100% rename from client/book-my-venue/src/app/shared/services/auth.service.ts rename to client/src/app/shared/services/auth.service.ts diff --git a/client/book-my-venue/src/app/shared/services/notification.service.ts b/client/src/app/shared/services/notification.service.ts similarity index 100% rename from client/book-my-venue/src/app/shared/services/notification.service.ts rename to client/src/app/shared/services/notification.service.ts diff --git a/client/book-my-venue/src/app/shared/services/storage.service.ts b/client/src/app/shared/services/storage.service.ts similarity index 100% rename from client/book-my-venue/src/app/shared/services/storage.service.ts rename to client/src/app/shared/services/storage.service.ts diff --git a/client/book-my-venue/src/app/shared/utils/validators.ts b/client/src/app/shared/utils/validators.ts similarity index 100% rename from client/book-my-venue/src/app/shared/utils/validators.ts rename to client/src/app/shared/utils/validators.ts diff --git a/client/book-my-venue/src/app/user/features/checkout/checkout.component.css b/client/src/app/user/features/checkout/checkout.component.css similarity index 100% rename from client/book-my-venue/src/app/user/features/checkout/checkout.component.css rename to client/src/app/user/features/checkout/checkout.component.css diff --git a/client/book-my-venue/src/app/user/features/checkout/checkout.component.html b/client/src/app/user/features/checkout/checkout.component.html similarity index 100% rename from client/book-my-venue/src/app/user/features/checkout/checkout.component.html rename to client/src/app/user/features/checkout/checkout.component.html diff --git a/client/book-my-venue/src/app/user/features/checkout/checkout.component.spec.ts b/client/src/app/user/features/checkout/checkout.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/checkout/checkout.component.spec.ts rename to client/src/app/user/features/checkout/checkout.component.spec.ts diff --git a/client/book-my-venue/src/app/user/features/checkout/checkout.component.ts b/client/src/app/user/features/checkout/checkout.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/checkout/checkout.component.ts rename to client/src/app/user/features/checkout/checkout.component.ts diff --git a/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.css b/client/src/app/user/features/forgot-password/forgot-password.component.css similarity index 100% rename from client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.css rename to client/src/app/user/features/forgot-password/forgot-password.component.css diff --git a/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.html b/client/src/app/user/features/forgot-password/forgot-password.component.html similarity index 100% rename from client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.html rename to client/src/app/user/features/forgot-password/forgot-password.component.html diff --git a/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.spec.ts b/client/src/app/user/features/forgot-password/forgot-password.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.spec.ts rename to client/src/app/user/features/forgot-password/forgot-password.component.spec.ts diff --git a/client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.ts b/client/src/app/user/features/forgot-password/forgot-password.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/forgot-password/forgot-password.component.ts rename to client/src/app/user/features/forgot-password/forgot-password.component.ts diff --git a/client/book-my-venue/src/app/user/features/login/login.component.css b/client/src/app/user/features/login/login.component.css similarity index 100% rename from client/book-my-venue/src/app/user/features/login/login.component.css rename to client/src/app/user/features/login/login.component.css diff --git a/client/book-my-venue/src/app/user/features/login/login.component.html b/client/src/app/user/features/login/login.component.html similarity index 100% rename from client/book-my-venue/src/app/user/features/login/login.component.html rename to client/src/app/user/features/login/login.component.html diff --git a/client/book-my-venue/src/app/user/features/login/login.component.spec.ts b/client/src/app/user/features/login/login.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/login/login.component.spec.ts rename to client/src/app/user/features/login/login.component.spec.ts diff --git a/client/book-my-venue/src/app/user/features/login/login.component.ts b/client/src/app/user/features/login/login.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/login/login.component.ts rename to client/src/app/user/features/login/login.component.ts diff --git a/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.css b/client/src/app/user/features/my-bookings/my-bookings.component.css similarity index 100% rename from client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.css rename to client/src/app/user/features/my-bookings/my-bookings.component.css diff --git a/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.html b/client/src/app/user/features/my-bookings/my-bookings.component.html similarity index 100% rename from client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.html rename to client/src/app/user/features/my-bookings/my-bookings.component.html diff --git a/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.spec.ts b/client/src/app/user/features/my-bookings/my-bookings.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.spec.ts rename to client/src/app/user/features/my-bookings/my-bookings.component.spec.ts diff --git a/client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.ts b/client/src/app/user/features/my-bookings/my-bookings.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/my-bookings/my-bookings.component.ts rename to client/src/app/user/features/my-bookings/my-bookings.component.ts diff --git a/client/book-my-venue/src/app/user/features/profile/profile.component.css b/client/src/app/user/features/profile/profile.component.css similarity index 100% rename from client/book-my-venue/src/app/user/features/profile/profile.component.css rename to client/src/app/user/features/profile/profile.component.css diff --git a/client/book-my-venue/src/app/user/features/profile/profile.component.html b/client/src/app/user/features/profile/profile.component.html similarity index 100% rename from client/book-my-venue/src/app/user/features/profile/profile.component.html rename to client/src/app/user/features/profile/profile.component.html diff --git a/client/book-my-venue/src/app/user/features/profile/profile.component.spec.ts b/client/src/app/user/features/profile/profile.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/profile/profile.component.spec.ts rename to client/src/app/user/features/profile/profile.component.spec.ts diff --git a/client/book-my-venue/src/app/user/features/profile/profile.component.ts b/client/src/app/user/features/profile/profile.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/profile/profile.component.ts rename to client/src/app/user/features/profile/profile.component.ts diff --git a/client/book-my-venue/src/app/user/features/signup/signup.component.css b/client/src/app/user/features/signup/signup.component.css similarity index 100% rename from client/book-my-venue/src/app/user/features/signup/signup.component.css rename to client/src/app/user/features/signup/signup.component.css diff --git a/client/book-my-venue/src/app/user/features/signup/signup.component.html b/client/src/app/user/features/signup/signup.component.html similarity index 100% rename from client/book-my-venue/src/app/user/features/signup/signup.component.html rename to client/src/app/user/features/signup/signup.component.html diff --git a/client/book-my-venue/src/app/user/features/signup/signup.component.spec.ts b/client/src/app/user/features/signup/signup.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/signup/signup.component.spec.ts rename to client/src/app/user/features/signup/signup.component.spec.ts diff --git a/client/book-my-venue/src/app/user/features/signup/signup.component.ts b/client/src/app/user/features/signup/signup.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/signup/signup.component.ts rename to client/src/app/user/features/signup/signup.component.ts diff --git a/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.css b/client/src/app/user/features/venue-details/venue-details.component.css similarity index 100% rename from client/book-my-venue/src/app/user/features/venue-details/venue-details.component.css rename to client/src/app/user/features/venue-details/venue-details.component.css diff --git a/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.html b/client/src/app/user/features/venue-details/venue-details.component.html similarity index 100% rename from client/book-my-venue/src/app/user/features/venue-details/venue-details.component.html rename to client/src/app/user/features/venue-details/venue-details.component.html diff --git a/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.spec.ts b/client/src/app/user/features/venue-details/venue-details.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/venue-details/venue-details.component.spec.ts rename to client/src/app/user/features/venue-details/venue-details.component.spec.ts diff --git a/client/book-my-venue/src/app/user/features/venue-details/venue-details.component.ts b/client/src/app/user/features/venue-details/venue-details.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/venue-details/venue-details.component.ts rename to client/src/app/user/features/venue-details/venue-details.component.ts diff --git a/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.css b/client/src/app/user/features/venue-list/venue-list.component.css similarity index 100% rename from client/book-my-venue/src/app/user/features/venue-list/venue-list.component.css rename to client/src/app/user/features/venue-list/venue-list.component.css diff --git a/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.html b/client/src/app/user/features/venue-list/venue-list.component.html similarity index 100% rename from client/book-my-venue/src/app/user/features/venue-list/venue-list.component.html rename to client/src/app/user/features/venue-list/venue-list.component.html diff --git a/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.spec.ts b/client/src/app/user/features/venue-list/venue-list.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/venue-list/venue-list.component.spec.ts rename to client/src/app/user/features/venue-list/venue-list.component.spec.ts diff --git a/client/book-my-venue/src/app/user/features/venue-list/venue-list.component.ts b/client/src/app/user/features/venue-list/venue-list.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/features/venue-list/venue-list.component.ts rename to client/src/app/user/features/venue-list/venue-list.component.ts diff --git a/client/book-my-venue/src/app/user/guards/user-auth.guard.ts b/client/src/app/user/guards/user-auth.guard.ts similarity index 100% rename from client/book-my-venue/src/app/user/guards/user-auth.guard.ts rename to client/src/app/user/guards/user-auth.guard.ts diff --git a/client/book-my-venue/src/app/user/interceptors/user-auth.interceptor.ts b/client/src/app/user/interceptors/user-auth.interceptor.ts similarity index 100% rename from client/book-my-venue/src/app/user/interceptors/user-auth.interceptor.ts rename to client/src/app/user/interceptors/user-auth.interceptor.ts diff --git a/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.css b/client/src/app/user/layouts/user-layout/user-layout.component.css similarity index 100% rename from client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.css rename to client/src/app/user/layouts/user-layout/user-layout.component.css diff --git a/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.html b/client/src/app/user/layouts/user-layout/user-layout.component.html similarity index 100% rename from client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.html rename to client/src/app/user/layouts/user-layout/user-layout.component.html diff --git a/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.spec.ts b/client/src/app/user/layouts/user-layout/user-layout.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.spec.ts rename to client/src/app/user/layouts/user-layout/user-layout.component.spec.ts diff --git a/client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.ts b/client/src/app/user/layouts/user-layout/user-layout.component.ts similarity index 100% rename from client/book-my-venue/src/app/user/layouts/user-layout/user-layout.component.ts rename to client/src/app/user/layouts/user-layout/user-layout.component.ts diff --git a/client/book-my-venue/src/app/user/models/index.ts b/client/src/app/user/models/index.ts similarity index 100% rename from client/book-my-venue/src/app/user/models/index.ts rename to client/src/app/user/models/index.ts diff --git a/client/book-my-venue/src/app/user/repositories/booking.repository.ts b/client/src/app/user/repositories/booking.repository.ts similarity index 100% rename from client/book-my-venue/src/app/user/repositories/booking.repository.ts rename to client/src/app/user/repositories/booking.repository.ts diff --git a/client/book-my-venue/src/app/user/repositories/user.repository.ts b/client/src/app/user/repositories/user.repository.ts similarity index 100% rename from client/book-my-venue/src/app/user/repositories/user.repository.ts rename to client/src/app/user/repositories/user.repository.ts diff --git a/client/book-my-venue/src/app/user/repositories/venue.repository.ts b/client/src/app/user/repositories/venue.repository.ts similarity index 100% rename from client/book-my-venue/src/app/user/repositories/venue.repository.ts rename to client/src/app/user/repositories/venue.repository.ts diff --git a/client/book-my-venue/src/app/user/routes/user.routes.ts b/client/src/app/user/routes/user.routes.ts similarity index 100% rename from client/book-my-venue/src/app/user/routes/user.routes.ts rename to client/src/app/user/routes/user.routes.ts diff --git a/client/book-my-venue/src/app/user/services/booking.service.ts b/client/src/app/user/services/booking.service.ts similarity index 100% rename from client/book-my-venue/src/app/user/services/booking.service.ts rename to client/src/app/user/services/booking.service.ts diff --git a/client/book-my-venue/src/app/user/services/user.service.ts b/client/src/app/user/services/user.service.ts similarity index 100% rename from client/book-my-venue/src/app/user/services/user.service.ts rename to client/src/app/user/services/user.service.ts diff --git a/client/book-my-venue/src/app/user/services/venue.service.ts b/client/src/app/user/services/venue.service.ts similarity index 100% rename from client/book-my-venue/src/app/user/services/venue.service.ts rename to client/src/app/user/services/venue.service.ts diff --git a/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.css b/client/src/app/vendor/features/analytics/analytics.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/analytics/analytics.component.css rename to client/src/app/vendor/features/analytics/analytics.component.css diff --git a/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.html b/client/src/app/vendor/features/analytics/analytics.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/analytics/analytics.component.html rename to client/src/app/vendor/features/analytics/analytics.component.html diff --git a/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.spec.ts b/client/src/app/vendor/features/analytics/analytics.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/analytics/analytics.component.spec.ts rename to client/src/app/vendor/features/analytics/analytics.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/analytics/analytics.component.ts b/client/src/app/vendor/features/analytics/analytics.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/analytics/analytics.component.ts rename to client/src/app/vendor/features/analytics/analytics.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.css b/client/src/app/vendor/features/bookings/bookings.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/bookings/bookings.component.css rename to client/src/app/vendor/features/bookings/bookings.component.css diff --git a/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.html b/client/src/app/vendor/features/bookings/bookings.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/bookings/bookings.component.html rename to client/src/app/vendor/features/bookings/bookings.component.html diff --git a/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.spec.ts b/client/src/app/vendor/features/bookings/bookings.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/bookings/bookings.component.spec.ts rename to client/src/app/vendor/features/bookings/bookings.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/bookings/bookings.component.ts b/client/src/app/vendor/features/bookings/bookings.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/bookings/bookings.component.ts rename to client/src/app/vendor/features/bookings/bookings.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.css b/client/src/app/vendor/features/create-venue/create-venue.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.css rename to client/src/app/vendor/features/create-venue/create-venue.component.css diff --git a/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.html b/client/src/app/vendor/features/create-venue/create-venue.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.html rename to client/src/app/vendor/features/create-venue/create-venue.component.html diff --git a/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.spec.ts b/client/src/app/vendor/features/create-venue/create-venue.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.spec.ts rename to client/src/app/vendor/features/create-venue/create-venue.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.ts b/client/src/app/vendor/features/create-venue/create-venue.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/create-venue/create-venue.component.ts rename to client/src/app/vendor/features/create-venue/create-venue.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.css b/client/src/app/vendor/features/dashboard/dashboard.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.css rename to client/src/app/vendor/features/dashboard/dashboard.component.css diff --git a/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.html b/client/src/app/vendor/features/dashboard/dashboard.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.html rename to client/src/app/vendor/features/dashboard/dashboard.component.html diff --git a/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.spec.ts b/client/src/app/vendor/features/dashboard/dashboard.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.spec.ts rename to client/src/app/vendor/features/dashboard/dashboard.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.ts b/client/src/app/vendor/features/dashboard/dashboard.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/dashboard/dashboard.component.ts rename to client/src/app/vendor/features/dashboard/dashboard.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.css b/client/src/app/vendor/features/edit-venue/edit-venue.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.css rename to client/src/app/vendor/features/edit-venue/edit-venue.component.css diff --git a/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.html b/client/src/app/vendor/features/edit-venue/edit-venue.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.html rename to client/src/app/vendor/features/edit-venue/edit-venue.component.html diff --git a/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts b/client/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts rename to client/src/app/vendor/features/edit-venue/edit-venue.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.ts b/client/src/app/vendor/features/edit-venue/edit-venue.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/edit-venue/edit-venue.component.ts rename to client/src/app/vendor/features/edit-venue/edit-venue.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.css b/client/src/app/vendor/features/forgot-password/forgot-password.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.css rename to client/src/app/vendor/features/forgot-password/forgot-password.component.css diff --git a/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.html b/client/src/app/vendor/features/forgot-password/forgot-password.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.html rename to client/src/app/vendor/features/forgot-password/forgot-password.component.html diff --git a/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts b/client/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts rename to client/src/app/vendor/features/forgot-password/forgot-password.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.ts b/client/src/app/vendor/features/forgot-password/forgot-password.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/forgot-password/forgot-password.component.ts rename to client/src/app/vendor/features/forgot-password/forgot-password.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/login/login.component.css b/client/src/app/vendor/features/login/login.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/login/login.component.css rename to client/src/app/vendor/features/login/login.component.css diff --git a/client/book-my-venue/src/app/vendor/features/login/login.component.html b/client/src/app/vendor/features/login/login.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/login/login.component.html rename to client/src/app/vendor/features/login/login.component.html diff --git a/client/book-my-venue/src/app/vendor/features/login/login.component.spec.ts b/client/src/app/vendor/features/login/login.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/login/login.component.spec.ts rename to client/src/app/vendor/features/login/login.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/login/login.component.ts b/client/src/app/vendor/features/login/login.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/login/login.component.ts rename to client/src/app/vendor/features/login/login.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/settings/settings.component.css b/client/src/app/vendor/features/settings/settings.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/settings/settings.component.css rename to client/src/app/vendor/features/settings/settings.component.css diff --git a/client/book-my-venue/src/app/vendor/features/settings/settings.component.html b/client/src/app/vendor/features/settings/settings.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/settings/settings.component.html rename to client/src/app/vendor/features/settings/settings.component.html diff --git a/client/book-my-venue/src/app/vendor/features/settings/settings.component.spec.ts b/client/src/app/vendor/features/settings/settings.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/settings/settings.component.spec.ts rename to client/src/app/vendor/features/settings/settings.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/settings/settings.component.ts b/client/src/app/vendor/features/settings/settings.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/settings/settings.component.ts rename to client/src/app/vendor/features/settings/settings.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/signup/signup.component.css b/client/src/app/vendor/features/signup/signup.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/signup/signup.component.css rename to client/src/app/vendor/features/signup/signup.component.css diff --git a/client/book-my-venue/src/app/vendor/features/signup/signup.component.html b/client/src/app/vendor/features/signup/signup.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/signup/signup.component.html rename to client/src/app/vendor/features/signup/signup.component.html diff --git a/client/book-my-venue/src/app/vendor/features/signup/signup.component.spec.ts b/client/src/app/vendor/features/signup/signup.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/signup/signup.component.spec.ts rename to client/src/app/vendor/features/signup/signup.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/signup/signup.component.ts b/client/src/app/vendor/features/signup/signup.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/signup/signup.component.ts rename to client/src/app/vendor/features/signup/signup.component.ts diff --git a/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.css b/client/src/app/vendor/features/venue-list/venue-list.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.css rename to client/src/app/vendor/features/venue-list/venue-list.component.css diff --git a/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.html b/client/src/app/vendor/features/venue-list/venue-list.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.html rename to client/src/app/vendor/features/venue-list/venue-list.component.html diff --git a/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.spec.ts b/client/src/app/vendor/features/venue-list/venue-list.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.spec.ts rename to client/src/app/vendor/features/venue-list/venue-list.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.ts b/client/src/app/vendor/features/venue-list/venue-list.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/features/venue-list/venue-list.component.ts rename to client/src/app/vendor/features/venue-list/venue-list.component.ts diff --git a/client/book-my-venue/src/app/vendor/guards/vendor-auth.guard.ts b/client/src/app/vendor/guards/vendor-auth.guard.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/guards/vendor-auth.guard.ts rename to client/src/app/vendor/guards/vendor-auth.guard.ts diff --git a/client/book-my-venue/src/app/vendor/interceptors/vendor-auth.interceptor.ts b/client/src/app/vendor/interceptors/vendor-auth.interceptor.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/interceptors/vendor-auth.interceptor.ts rename to client/src/app/vendor/interceptors/vendor-auth.interceptor.ts diff --git a/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css b/client/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css similarity index 100% rename from client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css rename to client/src/app/vendor/layouts/vendor-layout/vendor-layout.component.css diff --git a/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html b/client/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html similarity index 100% rename from client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html rename to client/src/app/vendor/layouts/vendor-layout/vendor-layout.component.html diff --git a/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts b/client/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts rename to client/src/app/vendor/layouts/vendor-layout/vendor-layout.component.spec.ts diff --git a/client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts b/client/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts rename to client/src/app/vendor/layouts/vendor-layout/vendor-layout.component.ts diff --git a/client/book-my-venue/src/app/vendor/models/index.ts b/client/src/app/vendor/models/index.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/models/index.ts rename to client/src/app/vendor/models/index.ts diff --git a/client/book-my-venue/src/app/vendor/repositories/analytics.repository.ts b/client/src/app/vendor/repositories/analytics.repository.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/repositories/analytics.repository.ts rename to client/src/app/vendor/repositories/analytics.repository.ts diff --git a/client/book-my-venue/src/app/vendor/repositories/booking.repository.ts b/client/src/app/vendor/repositories/booking.repository.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/repositories/booking.repository.ts rename to client/src/app/vendor/repositories/booking.repository.ts diff --git a/client/book-my-venue/src/app/vendor/repositories/vendor.repository.ts b/client/src/app/vendor/repositories/vendor.repository.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/repositories/vendor.repository.ts rename to client/src/app/vendor/repositories/vendor.repository.ts diff --git a/client/book-my-venue/src/app/vendor/repositories/venue.repository.ts b/client/src/app/vendor/repositories/venue.repository.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/repositories/venue.repository.ts rename to client/src/app/vendor/repositories/venue.repository.ts diff --git a/client/book-my-venue/src/app/vendor/routes/vendor.routes.ts b/client/src/app/vendor/routes/vendor.routes.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/routes/vendor.routes.ts rename to client/src/app/vendor/routes/vendor.routes.ts diff --git a/client/book-my-venue/src/app/vendor/services/analytics.service.ts b/client/src/app/vendor/services/analytics.service.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/services/analytics.service.ts rename to client/src/app/vendor/services/analytics.service.ts diff --git a/client/book-my-venue/src/app/vendor/services/booking.service.ts b/client/src/app/vendor/services/booking.service.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/services/booking.service.ts rename to client/src/app/vendor/services/booking.service.ts diff --git a/client/book-my-venue/src/app/vendor/services/vendor.service.ts b/client/src/app/vendor/services/vendor.service.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/services/vendor.service.ts rename to client/src/app/vendor/services/vendor.service.ts diff --git a/client/book-my-venue/src/app/vendor/services/venue.service.ts b/client/src/app/vendor/services/venue.service.ts similarity index 100% rename from client/book-my-venue/src/app/vendor/services/venue.service.ts rename to client/src/app/vendor/services/venue.service.ts diff --git a/client/book-my-venue/src/index.html b/client/src/index.html similarity index 100% rename from client/book-my-venue/src/index.html rename to client/src/index.html diff --git a/client/book-my-venue/src/main.ts b/client/src/main.ts similarity index 100% rename from client/book-my-venue/src/main.ts rename to client/src/main.ts diff --git a/client/book-my-venue/src/styles.css b/client/src/styles.css similarity index 100% rename from client/book-my-venue/src/styles.css rename to client/src/styles.css diff --git a/client/book-my-venue/tsconfig.app.json b/client/tsconfig.app.json similarity index 100% rename from client/book-my-venue/tsconfig.app.json rename to client/tsconfig.app.json diff --git a/client/book-my-venue/tsconfig.json b/client/tsconfig.json similarity index 100% rename from client/book-my-venue/tsconfig.json rename to client/tsconfig.json diff --git a/client/book-my-venue/tsconfig.spec.json b/client/tsconfig.spec.json similarity index 100% rename from client/book-my-venue/tsconfig.spec.json rename to client/tsconfig.spec.json From f0cc9518e5a4651f2c94ebf869f6b7c4882deef5 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Mon, 1 Jun 2026 20:27:19 +0530 Subject: [PATCH 14/59] feat: implement venue retrieval apis --- .../server/auth/security/SecurityConfig.java | 1 + .../venue/controller/VenueController.java | 32 ++++++++++- .../server/venue/mapper/VenueMapper.java | 31 +++++++++++ .../venue/repository/VenueRepository.java | 9 ++++ .../server/venue/service/VenueService.java | 9 +++- .../venue/service/VenueServiceImpl.java | 53 +++++++++++++++---- 6 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java index c1f893af1..ce943ad72 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -34,6 +34,7 @@ public SecurityFilterChain securityFilterChain( "/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**", + "/api/v1/vendor/venues/**", "/api/v1/venues/**" // TODO: Restrict to VENDOR role after JWT authorization is implemented ).permitAll() .anyRequest().authenticated() diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java index 3e35e455c..0c9d05035 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -7,18 +7,46 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController -@RequestMapping("/api/v1/venues") +@RequestMapping("/api/v1") @RequiredArgsConstructor public class VenueController { private final VenueService venueService; - @PostMapping + @PostMapping("/vendor/venues") @ResponseStatus(HttpStatus.CREATED) public VenueResponse createVenue( @RequestBody CreateVenueRequest request ) { return venueService.createVenue(request); } + + @GetMapping("vendor/venues") + @ResponseStatus(HttpStatus.OK) + public List getAllVenues(){ + return venueService.getAllVenues(); + } + + @GetMapping("vendor/venues/{venueId}") + @ResponseStatus(HttpStatus.OK) + public VenueResponse getVenue(@PathVariable Long venueId){ + return venueService.getVenue(venueId); + } + + @GetMapping("venues") + @ResponseStatus(HttpStatus.OK) + public List getApprovedVenues() { + return venueService.getApprovedVenues(); + } + + @GetMapping("venues/{id}") + @ResponseStatus(HttpStatus.OK) + public VenueResponse getApprovedVenue( + @PathVariable Long id + ) { + return venueService.getApprovedVenue(id); + } } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java b/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java new file mode 100644 index 000000000..cf01a8899 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java @@ -0,0 +1,31 @@ +package com.bookmyvenue.server.venue.mapper; + +import com.bookmyvenue.server.venue.dto.response.VenueResponse; +import com.bookmyvenue.server.venue.entity.Venue; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class VenueMapper { + + public VenueResponse toResponse(Venue venue) { + return VenueResponse.builder() + .id(venue.getId()) + .name(venue.getName()) + .description(venue.getDescription()) + .address(venue.getAddress()) + .district(venue.getDistrict()) + .capacity(venue.getCapacity()) + .pricePerSlot(venue.getPricePerSlot()) + .category(venue.getCategory().getName()) + .status(venue.getStatus()) + .build(); + } + + public List toResponse(List venues) { + return venues.stream() + .map(this::toResponse) + .toList(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java index 948ae965e..51fb80a43 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java @@ -1,7 +1,16 @@ package com.bookmyvenue.server.venue.repository; import com.bookmyvenue.server.venue.entity.Venue; +import com.bookmyvenue.server.venue.entity.VenueStatus; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + public interface VenueRepository extends JpaRepository { + List findByStatus(VenueStatus status); + Optional findByIdAndStatus( + Long id, + VenueStatus status + ); } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java index 2f4ef8492..bfa7de752 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java @@ -3,6 +3,13 @@ import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; import com.bookmyvenue.server.venue.dto.response.VenueResponse; +import java.util.List; + public interface VenueService { - public VenueResponse createVenue(CreateVenueRequest request); + VenueResponse createVenue(CreateVenueRequest request); + List getAllVenues(); + VenueResponse getVenue(Long venueId); + List getApprovedVenues(); + VenueResponse getApprovedVenue(Long id); + } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index 18dfa5eb4..d1463e0f3 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -7,12 +7,15 @@ import com.bookmyvenue.server.venue.entity.Venue; import com.bookmyvenue.server.venue.entity.VenueCategory; import com.bookmyvenue.server.venue.entity.VenueStatus; +import com.bookmyvenue.server.venue.mapper.VenueMapper; import com.bookmyvenue.server.venue.repository.VenueCategoryRepository; import com.bookmyvenue.server.venue.repository.VenueRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor @@ -22,6 +25,7 @@ public class VenueServiceImpl implements VenueService { private final VenueRepository venueRepository; private final VenueCategoryRepository venueCategoryRepository; private final UserRepository userRepository; + private final VenueMapper venueMapper; @Override public VenueResponse createVenue(CreateVenueRequest request) { @@ -50,16 +54,43 @@ public VenueResponse createVenue(CreateVenueRequest request) { Venue savedVenue = venueRepository.save(venue); - return VenueResponse.builder() - .id(savedVenue.getId()) - .name(savedVenue.getName()) - .description(savedVenue.getDescription()) - .address(savedVenue.getAddress()) - .district(savedVenue.getDistrict()) - .capacity(savedVenue.getCapacity()) - .pricePerSlot(savedVenue.getPricePerSlot()) - .category(savedVenue.getCategory().getName()) - .status(savedVenue.getStatus()) - .build(); + return venueMapper.toResponse(savedVenue); + } + + @Override + public List getAllVenues() { + List venues = venueRepository.findAll() + .stream().toList(); + return venueMapper.toResponse(venues); + } + + @Override + public VenueResponse getVenue(Long venueId) { + Venue venue = venueRepository.findById(venueId) + .orElseThrow(()-> + new RuntimeException("venue not found")); + return venueMapper.toResponse(venue); + } + + @Override + public List getApprovedVenues() { + + List venues = venueRepository + .findByStatus(VenueStatus.APPROVED); + + return venues.stream() + .map(venueMapper::toResponse) + .toList(); + } + + @Override + public VenueResponse getApprovedVenue(Long id) { + + Venue venue = venueRepository + .findByIdAndStatus(id, VenueStatus.APPROVED) + .orElseThrow(() -> + new RuntimeException("Venue not found")); + + return venueMapper.toResponse(venue); } } \ No newline at end of file From 422945eb320cf70d3ba12494f2422d30cbdefc4a Mon Sep 17 00:00:00 2001 From: Anzyll Date: Mon, 1 Jun 2026 21:37:13 +0530 Subject: [PATCH 15/59] feat: implement venue updation api --- .../venue/controller/VenueController.java | 10 ++++ .../venue/dto/request/UpdateVenueRequest.java | 22 ++++++++ .../server/venue/service/VenueService.java | 5 ++ .../venue/service/VenueServiceImpl.java | 55 +++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java index 0c9d05035..d1e6754e1 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -1,6 +1,7 @@ package com.bookmyvenue.server.venue.controller; import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; +import com.bookmyvenue.server.venue.dto.request.UpdateVenueRequest; import com.bookmyvenue.server.venue.dto.response.VenueResponse; import com.bookmyvenue.server.venue.service.VenueService; import lombok.RequiredArgsConstructor; @@ -49,4 +50,13 @@ public VenueResponse getApprovedVenue( ) { return venueService.getApprovedVenue(id); } + + @PatchMapping("venues/{id}") + @ResponseStatus(HttpStatus.OK) + public VenueResponse updateVenue( + @PathVariable Long id, + @RequestBody UpdateVenueRequest request + ) { + return venueService.updateVenue(id, request); + } } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java new file mode 100644 index 000000000..a1840c00b --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java @@ -0,0 +1,22 @@ +package com.bookmyvenue.server.venue.dto.request; + +import lombok.*; + +import java.math.BigDecimal; +import java.util.UUID; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UpdateVenueRequest { + private UUID ownerId; // TODO: Remove after JWT integration + private String name; + private String description; + private String address; + private String district; + private Integer capacity; + private BigDecimal pricePerSlot; + private Long categoryId; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java index bfa7de752..ab60a5c9e 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java @@ -1,6 +1,7 @@ package com.bookmyvenue.server.venue.service; import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; +import com.bookmyvenue.server.venue.dto.request.UpdateVenueRequest; import com.bookmyvenue.server.venue.dto.response.VenueResponse; import java.util.List; @@ -11,5 +12,9 @@ public interface VenueService { VenueResponse getVenue(Long venueId); List getApprovedVenues(); VenueResponse getApprovedVenue(Long id); + VenueResponse updateVenue( + Long venueId, + UpdateVenueRequest request + ); } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index d1463e0f3..c6ee7fd16 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -3,6 +3,7 @@ import com.bookmyvenue.server.user.entity.User; import com.bookmyvenue.server.user.repository.UserRepository; import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; +import com.bookmyvenue.server.venue.dto.request.UpdateVenueRequest; import com.bookmyvenue.server.venue.dto.response.VenueResponse; import com.bookmyvenue.server.venue.entity.Venue; import com.bookmyvenue.server.venue.entity.VenueCategory; @@ -93,4 +94,58 @@ public VenueResponse getApprovedVenue(Long id) { return venueMapper.toResponse(venue); } + + @Override + public VenueResponse updateVenue( + Long venueId, + UpdateVenueRequest request + ) { + + Venue venue = venueRepository.findById(venueId) + .orElseThrow(() -> + new RuntimeException("Venue not found")); + + // TODO: Replace with SecurityContext authentication + if (!venue.getOwner().getId().equals(request.getOwnerId())) { + throw new RuntimeException("You are not the owner of this venue"); + } + + if (request.getName() != null) { + venue.setName(request.getName()); + } + + if (request.getDescription() != null) { + venue.setDescription(request.getDescription()); + } + + if (request.getAddress() != null) { + venue.setAddress(request.getAddress()); + } + + if (request.getDistrict() != null) { + venue.setDistrict(request.getDistrict()); + } + + if (request.getCapacity() != null) { + venue.setCapacity(request.getCapacity()); + } + + if (request.getPricePerSlot() != null) { + venue.setPricePerSlot(request.getPricePerSlot()); + } + + if (request.getCategoryId() != null) { + + VenueCategory category = venueCategoryRepository + .findById(request.getCategoryId()) + .orElseThrow(() -> + new RuntimeException("Venue category not found")); + + venue.setCategory(category); + } + + Venue updatedVenue = venueRepository.save(venue); + + return venueMapper.toResponse(updatedVenue); + } } \ No newline at end of file From 1d1030b52f7f79cf09c42ff468ba8b13c2bec0ab Mon Sep 17 00:00:00 2001 From: Anzyll Date: Mon, 1 Jun 2026 21:47:07 +0530 Subject: [PATCH 16/59] feat: implement venue deletion api --- .../venue/controller/VenueController.java | 10 ++++++++++ .../server/venue/service/VenueService.java | 5 +++++ .../venue/service/VenueServiceImpl.java | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java index d1e6754e1..3fad3c6eb 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.UUID; @RestController @RequestMapping("/api/v1") @@ -59,4 +60,13 @@ public VenueResponse updateVenue( ) { return venueService.updateVenue(id, request); } + + @DeleteMapping("venues/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteVenue( + @PathVariable Long id, + @RequestParam UUID ownerId + ) { + venueService.deleteVenue(id, ownerId); + } } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java index ab60a5c9e..94d05d39e 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java @@ -5,6 +5,7 @@ import com.bookmyvenue.server.venue.dto.response.VenueResponse; import java.util.List; +import java.util.UUID; public interface VenueService { VenueResponse createVenue(CreateVenueRequest request); @@ -16,5 +17,9 @@ VenueResponse updateVenue( Long venueId, UpdateVenueRequest request ); + void deleteVenue( + Long venueId, + UUID ownerId + ); } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index c6ee7fd16..b2ce74906 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.UUID; @Service @@ -148,4 +149,22 @@ public VenueResponse updateVenue( return venueMapper.toResponse(updatedVenue); } + + @Override + public void deleteVenue( + Long venueId, + UUID ownerId + ) { + + Venue venue = venueRepository.findById(venueId) + .orElseThrow(() -> + new RuntimeException("Venue not found")); + + // TODO: Replace with SecurityContext authentication + if (!venue.getOwner().getId().equals(ownerId)) { + throw new RuntimeException("You are not the owner of this venue"); + } + + venueRepository.delete(venue); + } } \ No newline at end of file From ef37e27417a40f4014013851dc3bb5f792fff580 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Tue, 2 Jun 2026 01:11:27 +0530 Subject: [PATCH 17/59] dockerrized spring and postgres --- server/.dockerignore | 3 + server/Dockerfile | 17 +++ server/README.md | 127 ++++++++++++------ server/docker-compose.yml | 18 +++ server/openapi.json | 1 + .../src/main/resources/application.properties | 15 ++- 6 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile create mode 100644 server/openapi.json diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 000000000..698905e5c --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,3 @@ +target/ +.git/ +.idea/ \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 000000000..eaa2dee8d --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,17 @@ +FROM maven:3.9.11-eclipse-temurin-21 AS builder + +WORKDIR /app + +COPY . . + +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:21-jre + +WORKDIR /app + +COPY --from=builder /app/target/*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java","-jar","app.jar"] \ No newline at end of file diff --git a/server/README.md b/server/README.md index 44a76f3ba..114dbc8e2 100644 --- a/server/README.md +++ b/server/README.md @@ -1,7 +1,8 @@ # BookMyVenue - Backend Server Backend service for **BookMyVenue**, built with **Spring Boot**, **Java 21**, and **Maven**. -The project uses a **PostgreSQL** database running inside Docker for local development. + +The project uses **PostgreSQL** running inside Docker. The backend application can be run either from an IDE (recommended for backend developers) or inside Docker (recommended for frontend developers and deployment). --- @@ -17,7 +18,7 @@ The project uses a **PostgreSQL** database running inside Docker for local devel # Prerequisites -Before running the project, make sure the following are installed on your machine. +Before running the project, make sure the following are installed. ## 1. Java 21 @@ -40,17 +41,7 @@ docker compose version --- -## 3. Ensure Port 5432 is Free - -If PostgreSQL is already running locally, stop it before starting Docker: - -```bash -sudo systemctl stop postgresql -``` - ---- - -# Running the Project Locally +# Running the Project ## Step 1: Configure Environment Variables @@ -60,67 +51,104 @@ Inside the `server/` directory, create a local `.env` file from the example temp cp .env.example .env ``` -Open the `.env` file and verify the values: +Verify the values: ```env DB_USERNAME=postgres -DB_PASSWORD=your password +DB_PASSWORD=postgres123 DB_NAME=bookmyvenue_db -``` - +JWT_SECRET= +JWT_ACCESS_TOKEN_EXPIRATION=900000 +JWT_REFRESH_TOKEN_EXPIRATION=604800000 +``` --- -## Step 2: Start PostgreSQL with Docker +# Backend Developer Setup (Recommended) + +Backend developers should run PostgreSQL in Docker and run Spring Boot from IntelliJ IDEA. -From inside the `server/` directory, start the database container: +## Step 1: Start PostgreSQL ```bash -docker compose up -d +docker compose up -d postgres ``` -Verify the container is running: +Verify: ```bash docker ps ``` ---- +You should see: -## Step 3: Run the Spring Boot Application - -You can either: - -* Click the **Run/Play** button in your IDE (IntelliJ IDEA / VS Code) +```text +bmv-postgres-db +``` -OR +## Step 2: Run Spring Boot -Run the Maven wrapper command: +Run the application from IntelliJ IDEA or: ```bash ./mvnw spring-boot:run ``` -When you see the following log: +When you see: + +```text +Started ServerApplication +``` + +the backend server is running. + +The application will be available at: ```text -Started BookMyVenueApplication +http://localhost:8080 +``` + +--- + +# Frontend Developer Setup + +Frontend developers do not need Java, Maven, or IntelliJ. + +Start the entire backend stack using Docker: + +```bash +docker compose up --build ``` -the backend server is successfully running. +This starts: + +* PostgreSQL +* Spring Boot Backend + +The API will be available at: + +```text +http://localhost:8080 +``` --- -# Stopping the Docker Container +# Stopping Containers -To stop the PostgreSQL container: +Stop all services: ```bash docker compose down ``` -> Your database data will remain safe because Docker volumes are persisted. +Database data will remain safe because Docker volumes are persisted. + +To remove all database data and start fresh: + +```bash +docker compose down -v +``` --- @@ -130,17 +158,36 @@ docker compose down server/ ├── src/ ├── docker-compose.yml +├── Dockerfile ├── .env.example ├── pom.xml -└── mvnw +├── mvnw +└── README.md ``` --- -# Notes +# Deployment -* Ensure Docker Desktop/service is running before starting the project. -* Use Java 21 for compatibility. -* PostgreSQL runs inside Docker to keep local setup consistent across all developers. +Build and start all services: + +```bash +docker compose up -d --build +``` + +This will: + +1. Start PostgreSQL +2. Build the Spring Boot Docker image +3. Start the backend container +4. Apply Flyway migrations --- + +# Notes + +* Ensure Docker is running before starting the project. +* Use Java 21 for compatibility. +* PostgreSQL runs inside Docker for consistency across environments. +* Backend developers are encouraged to run Spring Boot from IntelliJ for easier debugging and faster development. +* Frontend developers can run the full backend stack using Docker only. diff --git a/server/docker-compose.yml b/server/docker-compose.yml index 66baf6376..ee3c1067e 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -13,5 +13,23 @@ services: volumes: - postgres_data:/var/lib/postgresql/data + backend: + build: + context: . + dockerfile: Dockerfile + container_name: bmv-backend + depends_on: + - postgres + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${DB_NAME} + SPRING_DATASOURCE_USERNAME: ${DB_USERNAME} + SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} + + JWT_SECRET: ${JWT_SECRET} + JWT_ACCESS_TOKEN_EXPIRATION: ${JWT_ACCESS_TOKEN_EXPIRATION} + JWT_REFRESH_TOKEN_EXPIRATION: ${JWT_REFRESH_TOKEN_EXPIRATION} + ports: + - "8080:8080" + volumes: postgres_data: \ No newline at end of file diff --git a/server/openapi.json b/server/openapi.json new file mode 100644 index 000000000..2e87870e4 --- /dev/null +++ b/server/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"tags":[{"name":"Authentication","description":"Authentication and account management APIs"}],"paths":{"/api/auth/register":{"post":{"tags":["Authentication"],"summary":"Register a new account","description":"Creates a new USER or VENDOR account and returns JWT tokens.","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Registration successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"400":{"description":"Invalid request or ADMIN registration attempted","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"409":{"description":"Email or phone already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}}}}},"/api/auth/refresh-token":{"post":{"tags":["Authentication"],"summary":"Refresh JWT tokens","description":"Generates new access and refresh tokens using a valid refresh token.","operationId":"refreshToken","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"200":{"description":"Tokens refreshed successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RefreshTokenResponse"}}}},"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RefreshTokenResponse"}}}}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"Authenticate user","description":"Authenticates a user and returns JWT tokens.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"401":{"description":"Invalid credentials","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}}}}}},"components":{"schemas":{"RegisterRequest":{"type":"object","description":"User registration request","properties":{"name":{"type":"string","description":"Full name of the user","example":"Ajay","minLength":1},"email":{"type":"string","description":"Unique email address","example":"ajay@gmail.com","minLength":1},"phone":{"type":"string","description":"10 digit phone number","example":9876543210,"minLength":1,"pattern":"^[0-9]{10}$"},"password":{"type":"string","description":"Account password","example":"password123","minLength":1},"role":{"type":"string","description":"Account role","enum":["USER","VENDOR"],"example":"USER"}},"required":["email","name","password","phone","role"]},"AuthResponse":{"type":"object","description":"Authentication response returned after successful login or registration","properties":{"userId":{"type":"string","format":"uuid","description":"Unique identifier of the authenticated user","example":"82b45eec-c37b-4ef5-b5fb-921ab399cb13"},"name":{"type":"string","description":"Full name of the user","example":"Ajay"},"email":{"type":"string","description":"Registered email address","example":"ajay@gmail.com"},"role":{"type":"string","description":"Role assigned to the user","enum":["USER","VENDOR","ADMIN"],"example":"USER"},"accessToken":{"type":"string","description":"JWT access token used to access protected APIs"},"refreshToken":{"type":"string","description":"JWT refresh token used to obtain a new access token"}}},"RefreshTokenRequest":{"type":"object","description":"Refresh token request","properties":{"refreshToken":{"type":"string","description":"JWT refresh token","example":"eyJhbGciOiJIUzM4NCJ9...","minLength":1}},"required":["refreshToken"]},"RefreshTokenResponse":{"type":"object","description":"Refresh token response","properties":{"accessToken":{"type":"string","description":"New JWT access token"},"refreshToken":{"type":"string","description":"New JWT refresh token"}}},"LoginRequest":{"type":"object","description":"User login request","properties":{"email":{"type":"string","description":"Registered email address","example":"ajay@gmail.com","minLength":1},"password":{"type":"string","description":"Account password","example":"password123","minLength":1}},"required":["email","password"]}}}} \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 66b4ab8ab..9df7e44fc 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1,10 +1,12 @@ # =================================================================== -#DOCKERIZED POSTGRESQL CONFIGURATION +# CONFIGURATION # =================================================================== + spring.config.import=optional:file:server/.env[.properties] -spring.datasource.url=jdbc:postgresql://localhost:5432/${DB_NAME} -spring.datasource.username=${DB_USERNAME} -spring.datasource.password=${DB_PASSWORD} + +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/${DB_NAME}} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:${DB_USERNAME}} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:${DB_PASSWORD}} spring.datasource.driver-class-name=org.postgresql.Driver # JPA / Hibernate Properties @@ -12,12 +14,11 @@ spring.jpa.hibernate.ddl-auto=validate spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect - -# jwt +# JWT app.jwt.secret=${JWT_SECRET} app.jwt.access-token-expiration=${JWT_ACCESS_TOKEN_EXPIRATION} app.jwt.refresh-token-expiration=${JWT_REFRESH_TOKEN_EXPIRATION} # Flyway spring.flyway.enabled=true -spring.flyway.locations=classpath:db/migration +spring.flyway.locations=classpath:db/migration \ No newline at end of file From d5a08384ccec5d69e5f2e5df1bf29e4b6078f36c Mon Sep 17 00:00:00 2001 From: Anzyll Date: Tue, 2 Jun 2026 23:49:51 +0530 Subject: [PATCH 18/59] feat: implement venue search and filtering --- .../venue/controller/VenueController.java | 7 ++- .../venue/repository/VenueRepository.java | 19 ++++++++ .../server/venue/service/VenueService.java | 2 +- .../venue/service/VenueServiceImpl.java | 48 +++++++++++++++++-- 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java index 3fad3c6eb..90b6017eb 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -40,8 +40,11 @@ public VenueResponse getVenue(@PathVariable Long venueId){ @GetMapping("venues") @ResponseStatus(HttpStatus.OK) - public List getApprovedVenues() { - return venueService.getApprovedVenues(); + public List getApprovedVenues( + @RequestParam(required = false) String search, + @RequestParam(required = false) String district, + @RequestParam(required = false) Long categoryId) { + return venueService.getApprovedVenues(search,district,categoryId); } @GetMapping("venues/{id}") diff --git a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java index 51fb80a43..5a8135ec1 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java @@ -13,4 +13,23 @@ Optional findByIdAndStatus( Long id, VenueStatus status ); + List findByStatusAndDistrictContainingIgnoreCase( + VenueStatus status, + String district + ); + + List findByStatusAndCategoryId( + VenueStatus status, + Long categoryId + ); + + List findByStatusAndDistrictIgnoreCaseAndCategoryId( + VenueStatus status, + String district, + Long categoryId + ); + List findByStatusAndNameContainingIgnoreCase( + VenueStatus status, + String keyword + ); } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java index 94d05d39e..689dc73f0 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java @@ -11,7 +11,7 @@ public interface VenueService { VenueResponse createVenue(CreateVenueRequest request); List getAllVenues(); VenueResponse getVenue(Long venueId); - List getApprovedVenues(); + List getApprovedVenues(String keyword, String district,Long categoryId); VenueResponse getApprovedVenue(Long id); VenueResponse updateVenue( Long venueId, diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index b2ce74906..1c0fb6353 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -75,10 +75,52 @@ public VenueResponse getVenue(Long venueId) { } @Override - public List getApprovedVenues() { + public List getApprovedVenues( + String keyword, + String district, + Long categoryId + ) { + + List venues; + + if (keyword != null && !keyword.isBlank()) { + + venues = venueRepository.findByStatusAndNameContainingIgnoreCase( + VenueStatus.APPROVED, + keyword + ); + + } else if (district != null && categoryId != null) { + + venues = venueRepository + .findByStatusAndDistrictIgnoreCaseAndCategoryId( + VenueStatus.APPROVED, + district, + categoryId + ); - List venues = venueRepository - .findByStatus(VenueStatus.APPROVED); + } else if (district != null) { + + venues = venueRepository + .findByStatusAndDistrictContainingIgnoreCase( + VenueStatus.APPROVED, + district + ); + + } else if (categoryId != null) { + + venues = venueRepository + .findByStatusAndCategoryId( + VenueStatus.APPROVED, + categoryId + ); + + } else { + + venues = venueRepository.findByStatus( + VenueStatus.APPROVED + ); + } return venues.stream() .map(venueMapper::toResponse) From 18b6f1473dd580ef5a6af9cf3d43636b632cfa29 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Thu, 4 Jun 2026 01:44:34 +0530 Subject: [PATCH 19/59] cookie migration done --- server/openapi.json | 311 +++++++++++++++++- .../auth/controller/AuthController.java | 87 +++-- .../auth/dto/response/AuthResponse.java | 5 - .../server/auth/dto/response/AuthResult.java | 11 + .../dto/response/RefreshTokenResponse.java | 23 -- .../server/auth/security/CookieUtils.java | 89 +++++ .../server/auth/service/AuthService.java | 11 +- .../server/auth/service/AuthServiceImpl.java | 48 ++- .../common/response/ApiErrorResponse.java | 10 + 9 files changed, 515 insertions(+), 80 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResult.java delete mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/CookieUtils.java diff --git a/server/openapi.json b/server/openapi.json index 2e87870e4..9e6613cbc 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -1 +1,310 @@ -{"openapi":"3.1.0","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"tags":[{"name":"Authentication","description":"Authentication and account management APIs"}],"paths":{"/api/auth/register":{"post":{"tags":["Authentication"],"summary":"Register a new account","description":"Creates a new USER or VENDOR account and returns JWT tokens.","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Registration successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"400":{"description":"Invalid request or ADMIN registration attempted","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"409":{"description":"Email or phone already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}}}}},"/api/auth/refresh-token":{"post":{"tags":["Authentication"],"summary":"Refresh JWT tokens","description":"Generates new access and refresh tokens using a valid refresh token.","operationId":"refreshToken","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"200":{"description":"Tokens refreshed successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RefreshTokenResponse"}}}},"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RefreshTokenResponse"}}}}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"Authenticate user","description":"Authenticates a user and returns JWT tokens.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"401":{"description":"Invalid credentials","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}}}}}},"components":{"schemas":{"RegisterRequest":{"type":"object","description":"User registration request","properties":{"name":{"type":"string","description":"Full name of the user","example":"Ajay","minLength":1},"email":{"type":"string","description":"Unique email address","example":"ajay@gmail.com","minLength":1},"phone":{"type":"string","description":"10 digit phone number","example":9876543210,"minLength":1,"pattern":"^[0-9]{10}$"},"password":{"type":"string","description":"Account password","example":"password123","minLength":1},"role":{"type":"string","description":"Account role","enum":["USER","VENDOR"],"example":"USER"}},"required":["email","name","password","phone","role"]},"AuthResponse":{"type":"object","description":"Authentication response returned after successful login or registration","properties":{"userId":{"type":"string","format":"uuid","description":"Unique identifier of the authenticated user","example":"82b45eec-c37b-4ef5-b5fb-921ab399cb13"},"name":{"type":"string","description":"Full name of the user","example":"Ajay"},"email":{"type":"string","description":"Registered email address","example":"ajay@gmail.com"},"role":{"type":"string","description":"Role assigned to the user","enum":["USER","VENDOR","ADMIN"],"example":"USER"},"accessToken":{"type":"string","description":"JWT access token used to access protected APIs"},"refreshToken":{"type":"string","description":"JWT refresh token used to obtain a new access token"}}},"RefreshTokenRequest":{"type":"object","description":"Refresh token request","properties":{"refreshToken":{"type":"string","description":"JWT refresh token","example":"eyJhbGciOiJIUzM4NCJ9...","minLength":1}},"required":["refreshToken"]},"RefreshTokenResponse":{"type":"object","description":"Refresh token response","properties":{"accessToken":{"type":"string","description":"New JWT access token"},"refreshToken":{"type":"string","description":"New JWT refresh token"}}},"LoginRequest":{"type":"object","description":"User login request","properties":{"email":{"type":"string","description":"Registered email address","example":"ajay@gmail.com","minLength":1},"password":{"type":"string","description":"Account password","example":"password123","minLength":1}},"required":["email","password"]}}}} \ No newline at end of file +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "tags": [ + { + "name": "Authentication", + "description": "Authentication and account management APIs" + } + ], + "paths": { + "/api/auth/register": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Register a new account", + "description": "Creates a new USER or VENDOR account and sets authentication cookies.", + "operationId": "register", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Registration successful", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AuthResponse" + } + } + } + }, + "400": { + "description": "Invalid request or ADMIN registration attempted", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "409": { + "description": "Email or phone already exists", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + } + } + }, + "/api/auth/refresh-token": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Refresh JWT tokens", + "description": "Generates new authentication cookies using a valid refresh token.", + "operationId": "refreshToken", + "parameters": [ + { + "name": "refreshToken", + "in": "cookie", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Tokens refreshed successfully", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AuthResponse" + } + } + } + }, + "401": { + "description": "Missing, invalid or expired refresh token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + } + } + }, + "/api/auth/logout": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Logout user", + "description": "Clears authentication cookies.", + "operationId": "logout", + "responses": { + "204": { + "description": "Logout successful" + } + } + } + }, + "/api/auth/login": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Authenticate user", + "description": "Authenticates a user and sets authentication cookies.", + "operationId": "login", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Login successful", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AuthResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "RegisterRequest": { + "type": "object", + "description": "User registration request", + "properties": { + "name": { + "type": "string", + "description": "Full name of the user", + "example": "Ajay", + "minLength": 1 + }, + "email": { + "type": "string", + "description": "Unique email address", + "example": "ajay@gmail.com", + "minLength": 1 + }, + "phone": { + "type": "string", + "description": "10 digit phone number", + "example": 9876543210, + "minLength": 1, + "pattern": "^[0-9]{10}$" + }, + "password": { + "type": "string", + "description": "Account password", + "example": "password123", + "minLength": 1 + }, + "role": { + "type": "string", + "description": "Account role", + "enum": [ + "USER", + "VENDOR" + ], + "example": "USER" + } + }, + "required": [ + "email", + "name", + "password", + "phone", + "role" + ] + }, + "ApiErrorResponse": { + "type": "object", + "description": "Standard API error response", + "properties": { + "status": { + "type": "integer", + "format": "int32", + "description": "HTTP status code", + "example": 401 + }, + "error": { + "type": "string", + "description": "Error type", + "example": "Unauthorized" + }, + "message": { + "type": "string", + "description": "Detailed error message", + "example": "Invalid email or password" + }, + "path": { + "type": "string", + "description": "Request path", + "example": "/api/auth/specific path" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Timestamp of the error", + "example": "2026-06-04T01:22:49" + } + } + }, + "AuthResponse": { + "type": "object", + "description": "Authentication response returned after successful login or registration", + "properties": { + "userId": { + "type": "string", + "format": "uuid", + "description": "Unique identifier of the authenticated user", + "example": "82b45eec-c37b-4ef5-b5fb-921ab399cb13" + }, + "name": { + "type": "string", + "description": "Full name of the user", + "example": "Ajay" + }, + "email": { + "type": "string", + "description": "Registered email address", + "example": "ajay@gmail.com" + }, + "role": { + "type": "string", + "description": "Role assigned to the user", + "enum": [ + "USER", + "VENDOR", + "ADMIN" + ], + "example": "USER" + } + } + }, + "LoginRequest": { + "type": "object", + "description": "User login request", + "properties": { + "email": { + "type": "string", + "description": "Registered email address", + "example": "ajay@gmail.com", + "minLength": 1 + }, + "password": { + "type": "string", + "description": "Account password", + "example": "password123", + "minLength": 1 + } + }, + "required": [ + "email", + "password" + ] + } + } + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java index 00d5479d8..51190ac4f 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java @@ -1,18 +1,24 @@ package com.bookmyvenue.server.auth.controller; import com.bookmyvenue.server.auth.dto.request.LoginRequest; -import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; import com.bookmyvenue.server.auth.dto.request.RegisterRequest; import com.bookmyvenue.server.auth.dto.response.AuthResponse; -import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; +import com.bookmyvenue.server.auth.dto.response.AuthResult; +import com.bookmyvenue.server.auth.security.CookieUtils; import com.bookmyvenue.server.auth.service.AuthService; +import com.bookmyvenue.server.common.exception.InvalidCredentialsException; +import com.bookmyvenue.server.common.response.ApiErrorResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -23,57 +29,82 @@ public class AuthController { private final AuthService authService; + private final CookieUtils cookieUtils; - @Operation( - summary = "Register a new account", - description = "Creates a new USER or VENDOR account and returns JWT tokens." - ) + @Operation(summary = "Register a new account", description = "Creates a new USER or VENDOR account and sets authentication cookies.") @ApiResponses({ @ApiResponse(responseCode = "201", description = "Registration successful"), - @ApiResponse(responseCode = "400", description = "Invalid request or ADMIN registration attempted"), - @ApiResponse(responseCode = "409", description = "Email or phone already exists") + @ApiResponse(responseCode = "400", description = "Invalid request or ADMIN registration attempted", + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "409", description = "Email or phone already exists", + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))) }) @PostMapping("/register") public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - AuthResponse response = authService.register(request); + + AuthResult result = authService.register(request); + ResponseCookie accessCookie = cookieUtils.accessTokenCookie(result.accessToken()); + ResponseCookie refreshCookie = cookieUtils.refreshTokenCookie(result.refreshToken()); + + System.out.println(accessCookie); + System.out.println(refreshCookie); return ResponseEntity .status(HttpStatus.CREATED) - .body(response); + .header(HttpHeaders.SET_COOKIE, accessCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) + .body(result.response()); } - @Operation( - summary = "Authenticate user", - description = "Authenticates a user and returns JWT tokens." - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Login successful"), - @ApiResponse(responseCode = "401", description = "Invalid credentials") + @Operation(summary = "Authenticate user", description = "Authenticates a user and sets authentication cookies.") + @ApiResponses({@ApiResponse(responseCode = "200", description = "Login successful"), + @ApiResponse(responseCode = "401", description = "Invalid credentials", + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))) }) @PostMapping("/login") public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - AuthResponse response = authService.login(request); - return ResponseEntity.ok(response); + + AuthResult result = authService.login(request); + ResponseCookie accessCookie = cookieUtils.accessTokenCookie(result.accessToken()); + ResponseCookie refreshCookie = cookieUtils.refreshTokenCookie(result.refreshToken()); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, accessCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) + .body(result.response()); } - @Operation( - summary = "Refresh JWT tokens", - description = "Generates new access and refresh tokens using a valid refresh token." - ) + @Operation(summary = "Refresh JWT tokens", description = "Generates new authentication cookies using a valid refresh token.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "Tokens refreshed successfully"), - @ApiResponse(responseCode = "401", description = "Invalid or expired refresh token") + @ApiResponse(responseCode = "401", description = "Missing, invalid or expired refresh token", + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))) }) @PostMapping("/refresh-token") - public ResponseEntity refreshToken(@Valid @RequestBody RefreshTokenRequest request) { + public ResponseEntity refreshToken(@CookieValue(value=CookieUtils.REFRESH_TOKEN,required = false) String refreshToken) { - RefreshTokenResponse response = authService.refreshToken(request); - return ResponseEntity.ok(response); + if (refreshToken == null) { + throw new InvalidCredentialsException("Refresh token not found"); + } + AuthResult result = authService.refreshToken(refreshToken); + ResponseCookie accessCookie = cookieUtils.accessTokenCookie(result.accessToken()); + ResponseCookie refreshCookie = cookieUtils.refreshTokenCookie(result.refreshToken()); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, accessCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) + .body(result.response()); } - + @Operation(summary = "Logout user", description = "Clears authentication cookies.") + @ApiResponse(responseCode = "204", description = "Logout successful") + @PostMapping("/logout") + public ResponseEntity logout() { + return ResponseEntity.noContent() + .header(HttpHeaders.SET_COOKIE, cookieUtils.clearAccessTokenCookie().toString()) + .header(HttpHeaders.SET_COOKIE, cookieUtils.clearRefreshTokenCookie().toString()) + .build(); + } } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java index ba45f1515..b80d4b026 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResponse.java @@ -26,9 +26,4 @@ public class AuthResponse { @Schema(description = "Role assigned to the user", example = "USER") private Role role; - @Schema(description = "JWT access token used to access protected APIs") - private String accessToken; - - @Schema(description = "JWT refresh token used to obtain a new access token") - private String refreshToken; } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResult.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResult.java new file mode 100644 index 000000000..597f897b3 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/AuthResult.java @@ -0,0 +1,11 @@ +package com.bookmyvenue.server.auth.dto.response; + +/** + * Authentication result containing user details and generated tokens. + */ +public record AuthResult( + AuthResponse response, + String accessToken, + String refreshToken +) { +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java deleted file mode 100644 index 5b7c259c3..000000000 --- a/server/src/main/java/com/bookmyvenue/server/auth/dto/response/RefreshTokenResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.bookmyvenue.server.auth.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.*; - -/**for later scaling we are - returning both access and refreshtoken . for token rotation **/ - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Schema(description = "Refresh token response") - -public class RefreshTokenResponse { - - @Schema(description = "New JWT access token") - private String accessToken; - - @Schema(description = "New JWT refresh token") - private String refreshToken; -} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/CookieUtils.java b/server/src/main/java/com/bookmyvenue/server/auth/security/CookieUtils.java new file mode 100644 index 000000000..f9363206c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/CookieUtils.java @@ -0,0 +1,89 @@ +package com.bookmyvenue.server.auth.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +/** + * Utility methods for authentication cookies. + */ +@Component +@RequiredArgsConstructor +public class CookieUtils { + + /** + * Access token cookie name. + */ + public static final String ACCESS_TOKEN = "accessToken"; + + /** + * Refresh token cookie name. + */ + public static final String REFRESH_TOKEN = "refreshToken"; + + /** + * Cookie path used throughout the application. + */ + private static final String COOKIE_PATH = "/"; + + /** + * SameSite policy for authentication cookies. + */ + private static final String SAME_SITE_POLICY = "Lax"; + + + + private final JwtProperties jwtProperties; + + /** + * Creates an access token cookie. + */ + public ResponseCookie accessTokenCookie(String token) { + return ResponseCookie.from(ACCESS_TOKEN, token) + .httpOnly(true) + .secure(false) // Use true in production. + .path(COOKIE_PATH) + .sameSite(SAME_SITE_POLICY) + .maxAge(jwtProperties.getAccessTokenExpiration() / 1000) + .build(); + } + + /** + * Creates a refresh token cookie. + */ + public ResponseCookie refreshTokenCookie(String token) { + return ResponseCookie.from(REFRESH_TOKEN, token) + .httpOnly(true) + .secure(false) // Use true in production. + .path(COOKIE_PATH) + .sameSite(SAME_SITE_POLICY) + .maxAge(jwtProperties.getRefreshTokenExpiration() / 1000) + .build(); + } + + /** + * Creates an expired access token cookie. + */ + public ResponseCookie clearAccessTokenCookie() { + return ResponseCookie.from(ACCESS_TOKEN, "") + .httpOnly(true) + .secure(false) // Use true in production. + .path(COOKIE_PATH) + .sameSite(SAME_SITE_POLICY) + .maxAge(0) + .build(); + } + + /** + * Creates an expired refresh token cookie. + */ + public ResponseCookie clearRefreshTokenCookie() { + return ResponseCookie.from(REFRESH_TOKEN, "") + .httpOnly(true) + .secure(false) // Use true in production. + .path(COOKIE_PATH) + .sameSite(SAME_SITE_POLICY) + .maxAge(0) + .build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java index ad11ec32a..40b507107 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java @@ -1,26 +1,25 @@ package com.bookmyvenue.server.auth.service; import com.bookmyvenue.server.auth.dto.request.LoginRequest; -import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; import com.bookmyvenue.server.auth.dto.request.RegisterRequest; -import com.bookmyvenue.server.auth.dto.response.AuthResponse; -import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; +import com.bookmyvenue.server.auth.dto.response.AuthResult; + public interface AuthService { /** * Registers a new USER or VENDOR account. */ - AuthResponse register(RegisterRequest request); + AuthResult register(RegisterRequest request); /** * Authenticates a user and returns JWT tokens. */ - AuthResponse login(LoginRequest request); + AuthResult login(LoginRequest request); /** * Generates a new access token using a valid refresh token. */ - RefreshTokenResponse refreshToken(RefreshTokenRequest request); + AuthResult refreshToken(String refreshToken); } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java index 06abc361d..4fead1074 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java @@ -1,10 +1,9 @@ package com.bookmyvenue.server.auth.service; import com.bookmyvenue.server.auth.dto.request.LoginRequest; -import com.bookmyvenue.server.auth.dto.request.RefreshTokenRequest; import com.bookmyvenue.server.auth.dto.request.RegisterRequest; import com.bookmyvenue.server.auth.dto.response.AuthResponse; -import com.bookmyvenue.server.auth.dto.response.RefreshTokenResponse; +import com.bookmyvenue.server.auth.dto.response.AuthResult; import com.bookmyvenue.server.auth.security.JwtService; import com.bookmyvenue.server.common.exception.BadRequestException; import com.bookmyvenue.server.common.exception.InvalidCredentialsException; @@ -29,7 +28,7 @@ public class AuthServiceImpl implements AuthService { @Override - public AuthResponse register(RegisterRequest request) { + public AuthResult register(RegisterRequest request) { log.info("Attempting registration for email: {}", request.getEmail()); @@ -62,20 +61,24 @@ public AuthResponse register(RegisterRequest request) { log.info("User registered successfully: {}", savedUser.getEmail()); - return AuthResponse.builder() + AuthResponse response = AuthResponse.builder() .userId(savedUser.getId()) .name(savedUser.getName()) .email(savedUser.getEmail()) .role(savedUser.getRole()) - .accessToken(accessToken) - .refreshToken(refreshToken) .build(); + + return new AuthResult( + response, + accessToken, + refreshToken + ); } @Override - public AuthResponse login(LoginRequest request) { + public AuthResult login(LoginRequest request) { log.info("Login attempt for email: {}", request.getEmail()); User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> { @@ -93,21 +96,25 @@ public AuthResponse login(LoginRequest request) { log.info("User logged in successfully: {}", user.getEmail()); - return AuthResponse.builder() + AuthResponse response = AuthResponse.builder() .userId(user.getId()) .name(user.getName()) .email(user.getEmail()) .role(user.getRole()) - .accessToken(accessToken) - .refreshToken(refreshToken) .build(); + + return new AuthResult( + response, + accessToken, + refreshToken + ); } @Override - public RefreshTokenResponse refreshToken(RefreshTokenRequest request) { + public AuthResult refreshToken(String refreshToken) { - String email = jwtService.extractEmail(request.getRefreshToken()); + String email = jwtService.extractEmail(refreshToken); User user = userRepository.findByEmail(email) .orElseThrow(() -> new InvalidCredentialsException( @@ -115,16 +122,23 @@ public RefreshTokenResponse refreshToken(RefreshTokenRequest request) { ) ); - if (!jwtService.isTokenValid(request.getRefreshToken(), user.getEmail() + if (!jwtService.isTokenValid(refreshToken, user.getEmail() )) { throw new InvalidCredentialsException("Invalid refresh token"); } String accessToken = jwtService.generateAccessToken(user.getEmail(), user.getRole()); - String refreshToken = jwtService.generateRefreshToken(user.getEmail(), user.getRole()); + String newRefreshToken = jwtService.generateRefreshToken(user.getEmail(), user.getRole()); - return RefreshTokenResponse.builder() - .accessToken(accessToken) - .refreshToken(refreshToken) + AuthResponse response = AuthResponse.builder() + .userId(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .role(user.getRole()) .build(); + return new AuthResult( + response, + accessToken, + newRefreshToken + ); } } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java b/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java index 40e6aeef1..0c309d667 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java +++ b/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java @@ -1,23 +1,33 @@ package com.bookmyvenue.server.common.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; +/** + * Standard API error response. + */ @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder +@Schema(description = "Standard API error response") public class ApiErrorResponse { + @Schema(description = "HTTP status code", example = "401") private int status; + @Schema(description = "Error type", example = "Unauthorized") private String error; + @Schema(description = "Detailed error message", example = "Invalid credentials") private String message; + @Schema(description = "Request path that triggered the error", example = "/api/resource") private String path; + @Schema(description = "Timestamp of the error", example = "2026-06-04T01:22:49") private LocalDateTime timestamp; } \ No newline at end of file From cc150d9ce72f8040d0a0395420d0455bb92e1ec2 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Thu, 4 Jun 2026 21:22:39 +0530 Subject: [PATCH 20/59] refactor: prepare SecurityContext-based user retrieval --- .../server/auth/security/SecurityConfig.java | 6 +--- .../service/AuthenticatedUserService.java | 26 ++++++++++++++++ .../venue/controller/VenueController.java | 7 +++-- .../venue/dto/request/CreateVenueRequest.java | 2 -- .../venue/dto/request/UpdateVenueRequest.java | 1 - .../server/venue/service/VenueService.java | 5 +--- .../venue/service/VenueServiceImpl.java | 30 ++++++++----------- 7 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/service/AuthenticatedUserService.java diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java index 8ceb18c5e..0184c2887 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -32,7 +32,6 @@ public PasswordEncoder passwordEncoder() { public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception { - http .csrf(csrf -> csrf.disable()) .cors(Customizer.withDefaults()) @@ -41,17 +40,14 @@ public SecurityFilterChain securityFilterChain( "/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**", - "/api/v1/vendor/venues/**", - "/api/v1/venues/**" // TODO: Restrict to VENDOR role after JWT authorization is implemented + "/api/v1/venues/**" ).permitAll() .anyRequest().authenticated() ) .formLogin(form -> form.disable()) .httpBasic(basic -> basic.disable()); - return http.build(); } - @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthenticatedUserService.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthenticatedUserService.java new file mode 100644 index 000000000..faec26c6f --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthenticatedUserService.java @@ -0,0 +1,26 @@ +package com.bookmyvenue.server.auth.service; + +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthenticatedUserService { + + private final UserRepository userRepository; + + public User getCurrentUser() { + + String email = SecurityContextHolder + .getContext() + .getAuthentication() + .getName(); + + return userRepository.findByEmail(email) + .orElseThrow(() -> + new RuntimeException("User not found")); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java index 90b6017eb..e7750096c 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -6,6 +6,7 @@ import com.bookmyvenue.server.venue.service.VenueService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -27,6 +28,7 @@ public VenueResponse createVenue( } @GetMapping("vendor/venues") + @PreAuthorize("hasRole('USER')") @ResponseStatus(HttpStatus.OK) public List getAllVenues(){ return venueService.getAllVenues(); @@ -67,9 +69,8 @@ public VenueResponse updateVenue( @DeleteMapping("venues/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteVenue( - @PathVariable Long id, - @RequestParam UUID ownerId + @PathVariable Long id ) { - venueService.deleteVenue(id, ownerId); + venueService.deleteVenue(id); } } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java index 6f18ed899..2b8dff9f9 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java @@ -23,8 +23,6 @@ public class CreateVenueRequest { private BigDecimal pricePerSlot; private Long categoryId; - // TODO: Temporary field for development. Remove after JWT authentication is integrated. - private UUID ownerId; private List imageUrls; } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java index a1840c00b..99a07b9bb 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java @@ -11,7 +11,6 @@ @AllArgsConstructor @NoArgsConstructor public class UpdateVenueRequest { - private UUID ownerId; // TODO: Remove after JWT integration private String name; private String description; private String address; diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java index 689dc73f0..40916d037 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java @@ -17,9 +17,6 @@ VenueResponse updateVenue( Long venueId, UpdateVenueRequest request ); - void deleteVenue( - Long venueId, - UUID ownerId - ); + void deleteVenue(Long venueId); } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index 1c0fb6353..896701fae 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -1,5 +1,6 @@ package com.bookmyvenue.server.venue.service; +import com.bookmyvenue.server.auth.service.AuthenticatedUserService; import com.bookmyvenue.server.user.entity.User; import com.bookmyvenue.server.user.repository.UserRepository; import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; @@ -26,8 +27,8 @@ public class VenueServiceImpl implements VenueService { private final VenueRepository venueRepository; private final VenueCategoryRepository venueCategoryRepository; - private final UserRepository userRepository; private final VenueMapper venueMapper; + private final AuthenticatedUserService authenticatedUserService; @Override public VenueResponse createVenue(CreateVenueRequest request) { @@ -37,10 +38,8 @@ public VenueResponse createVenue(CreateVenueRequest request) { .orElseThrow(() -> new RuntimeException("Venue category not found")); - // TODO: Replace ownerId with authenticated user from SecurityContext once JWT integration is completed. - User owner = userRepository.findById(request.getOwnerId()) - .orElseThrow(()-> - new RuntimeException("user not found")); + User currentUser = + authenticatedUserService.getCurrentUser(); Venue venue = Venue.builder() .name(request.getName()) @@ -51,7 +50,7 @@ public VenueResponse createVenue(CreateVenueRequest request) { .pricePerSlot(request.getPricePerSlot()) .category(category) .status(VenueStatus.PENDING_APPROVAL) - .owner(owner) + .owner(currentUser) .build(); Venue savedVenue = venueRepository.save(venue); @@ -148,11 +147,10 @@ public VenueResponse updateVenue( .orElseThrow(() -> new RuntimeException("Venue not found")); - // TODO: Replace with SecurityContext authentication - if (!venue.getOwner().getId().equals(request.getOwnerId())) { + User owner = authenticatedUserService.getCurrentUser(); + if (!venue.getOwner().getId().equals(owner.getId())) { throw new RuntimeException("You are not the owner of this venue"); } - if (request.getName() != null) { venue.setName(request.getName()); } @@ -193,20 +191,16 @@ public VenueResponse updateVenue( } @Override - public void deleteVenue( - Long venueId, - UUID ownerId - ) { - + public void deleteVenue(Long venueId){ Venue venue = venueRepository.findById(venueId) .orElseThrow(() -> new RuntimeException("Venue not found")); - // TODO: Replace with SecurityContext authentication - if (!venue.getOwner().getId().equals(ownerId)) { - throw new RuntimeException("You are not the owner of this venue"); - } + User currentUser = authenticatedUserService.getCurrentUser(); + if (!venue.getOwner().getId().equals(currentUser.getId())) { + throw new RuntimeException("You are not the owner of this venue"); + } venueRepository.delete(venue); } } \ No newline at end of file From ae17f7ba9211b8527765bb40bde2a24b72c4ea26 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Fri, 5 Jun 2026 05:52:04 +0530 Subject: [PATCH 21/59] feat:implemented cookie based authentication in frontend --- .../interceptors/admin-auth.interceptor.ts | 16 ----- client/src/app/app.config.ts | 8 ++- .../app/shared/models/auth-response.model.ts | 7 --- .../app/shared/services/auth.repository.ts | 11 ++-- .../src/app/shared/services/auth.service.ts | 58 +++++-------------- .../interceptors/user-auth.interceptor.ts | 19 ------ .../interceptors/vendor-auth.interceptor.ts | 16 ----- 7 files changed, 25 insertions(+), 110 deletions(-) delete mode 100644 client/src/app/admin/interceptors/admin-auth.interceptor.ts delete mode 100644 client/src/app/user/interceptors/user-auth.interceptor.ts delete mode 100644 client/src/app/vendor/interceptors/vendor-auth.interceptor.ts diff --git a/client/src/app/admin/interceptors/admin-auth.interceptor.ts b/client/src/app/admin/interceptors/admin-auth.interceptor.ts deleted file mode 100644 index 513ac9ee9..000000000 --- a/client/src/app/admin/interceptors/admin-auth.interceptor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { HttpInterceptorFn } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { AuthService } from '../../shared/services/auth.service'; - -export const adminAuthInterceptor: HttpInterceptorFn = (req, next) => { - const authService = inject(AuthService); - const token = authService.getToken(); - - if (token) { - const cloned = req.clone({ - setHeaders: { Authorization: `Bearer ${token}`, 'X-Portal': 'admin' }, - }); - return next(cloned); - } - return next(req); -}; diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index 4ec67409a..ffa3acdde 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -1,10 +1,12 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; -import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { provideHttpClient, withInterceptors,HttpInterceptorFn } from '@angular/common/http'; import { routes } from './core/app.routes'; import { appProviders } from './core/providers/app-providers'; import { errorInterceptor } from './shared/interceptors/error.interceptor'; -import { userAuthInterceptor } from './user/interceptors/user-auth.interceptor'; + +const credentialsInterceptor:HttpInterceptorFn = (req,next) => + next(req.clone({withCredentials:true})); export const appConfig: ApplicationConfig = { providers: [ @@ -12,7 +14,7 @@ export const appConfig: ApplicationConfig = { provideRouter(routes, withComponentInputBinding()), provideHttpClient( withInterceptors([ - userAuthInterceptor, + credentialsInterceptor, errorInterceptor, ]) ), diff --git a/client/src/app/shared/models/auth-response.model.ts b/client/src/app/shared/models/auth-response.model.ts index ba28b0b33..3036a76c5 100644 --- a/client/src/app/shared/models/auth-response.model.ts +++ b/client/src/app/shared/models/auth-response.model.ts @@ -3,13 +3,6 @@ export interface AuthResponse { name: string; email: string; role: string; - accessToken: string; - refreshToken: string; -} - -export interface RefreshTokenApiResponse { - accessToken: string; - refreshToken: string; } export interface AuthUser { diff --git a/client/src/app/shared/services/auth.repository.ts b/client/src/app/shared/services/auth.repository.ts index 95929a3cf..901e6be94 100644 --- a/client/src/app/shared/services/auth.repository.ts +++ b/client/src/app/shared/services/auth.repository.ts @@ -5,7 +5,6 @@ import { environment } from '../../core/config/environment'; import { API_ENDPOINTS } from '../constants/api-endpoints.constant'; import { AuthResponse, - RefreshTokenApiResponse, LoginRequest, SignupRequest, ForgotPasswordRequest, @@ -47,17 +46,17 @@ signup(payload: SignupRequest): Observable { } - logout(): Observable> { - return this.http.post>( + logout(): Observable { + return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.LOGOUT}`, {} ); } - refreshToken(refreshToken: string): Observable { - return this.http.post( + refreshToken(): Observable { + return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.REFRESH_TOKEN}`, - { refreshToken } + {} ); } diff --git a/client/src/app/shared/services/auth.service.ts b/client/src/app/shared/services/auth.service.ts index de28a14ab..c253a7687 100644 --- a/client/src/app/shared/services/auth.service.ts +++ b/client/src/app/shared/services/auth.service.ts @@ -37,10 +37,8 @@ export class AuthService { this.loadAllSessions(); } - private portalKeys(portal: string): { token: string; refresh: string; user: string } { + private portalKeys(portal: string): { user: string } { return { - token: `bmv_${portal}_token`, - refresh: `bmv_${portal}_refresh_token`, user: `bmv_${portal}_user`, }; } @@ -59,10 +57,9 @@ export class AuthService { } private loadSession(portal: string): AuthUser | null { - const keys = this.portalKeys(portal); - const token = this.storage.get(keys.token); - const userJson = this.storage.get(keys.user); - if (token && userJson) { + const user = this.portalKeys(portal).user; + const userJson = this.storage.get(user); + if (userJson) { try { return JSON.parse(userJson); } catch { @@ -73,11 +70,9 @@ export class AuthService { return null; } - private savePortalSession(portal: string, token: string, refreshToken: string, user: AuthUser): void { - const keys = this.portalKeys(portal); - this.storage.set(keys.token, token); - this.storage.set(keys.refresh, refreshToken); - this.storage.set(keys.user, JSON.stringify(user)); + private savePortalSession(portal: string, user: AuthUser): void { + const userToStore = this.portalKeys(portal).user; + this.storage.set(userToStore, JSON.stringify(user)); if (portal === 'admin') this.adminSession.set(user); else if (portal === 'vendor') this.vendorSession.set(user); @@ -85,10 +80,8 @@ export class AuthService { } private clearPortalSession(portal: string): void { - const keys = this.portalKeys(portal); - this.storage.remove(keys.token); - this.storage.remove(keys.refresh); - this.storage.remove(keys.user); + const user = this.portalKeys(portal).user; + this.storage.remove(user); if (portal === 'admin') this.adminSession.set(null); else if (portal === 'vendor') this.vendorSession.set(null); @@ -117,20 +110,6 @@ export class AuthService { } } - getToken(): string | null { - const portal = this.detectPortal(); - return this.storage.get(this.portalKeys(portal).token); - } - - getTokenForPortal(portal: string): string | null { - return this.storage.get(this.portalKeys(portal).token); - } - - getRefreshToken(): string | null { - const portal = this.detectPortal(); - return this.storage.get(this.portalKeys(portal).refresh); - } - hasRole(role: UserRole): boolean { return this.currentUser()?.role === role; } @@ -141,7 +120,7 @@ export class AuthService { next: (response) => { const user = this.toAuthUser(response); const portal = response.role === UserRole.Vendor ? 'vendor' : 'user'; - this.savePortalSession(portal, response.accessToken, response.refreshToken, user); + this.savePortalSession(portal, user); this.notification.success('Login successful'); this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); this.loading.set(false); @@ -158,7 +137,7 @@ export class AuthService { this.authRepository.adminLogin(payload).subscribe({ next: (response) => { const user = this.toAuthUser(response); - this.savePortalSession('admin', response.accessToken, response.refreshToken, user); + this.savePortalSession('admin', user); this.notification.success('Login successful'); this.router.navigate(['/admin/dashboard']); this.loading.set(false); @@ -176,7 +155,7 @@ export class AuthService { next: (response) => { const user = this.toAuthUser(response); const portal = response.role === UserRole.Vendor ? 'vendor' : 'user'; - this.savePortalSession(portal, response.accessToken, response.refreshToken, user); + this.savePortalSession(portal, user); this.notification.success(portal === 'vendor' ? 'Vendor account created successfully' : 'Account created successfully'); this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); this.loading.set(false); @@ -205,7 +184,7 @@ export class AuthService { logout(): void { const portal = this.detectPortal(); this.authRepository.logout().subscribe({ - complete: () => { + next: () => { this.clearPortalSession(portal); this.router.navigate(['/']); this.notification.info('Logged out successfully'); @@ -219,16 +198,9 @@ export class AuthService { refreshToken(): void { const portal = this.detectPortal(); - const refreshToken = this.getRefreshToken(); - if (!refreshToken) { - this.clearPortalSession(portal); - return; - } - this.authRepository.refreshToken(refreshToken).subscribe({ + this.authRepository.refreshToken().subscribe({ next: (response) => { - const keys = this.portalKeys(portal); - this.storage.set(keys.token, response.accessToken); - this.storage.set(keys.refresh, response.refreshToken); + this.savePortalSession(portal,this.toAuthUser(response)) }, error: () => { this.clearPortalSession(portal); diff --git a/client/src/app/user/interceptors/user-auth.interceptor.ts b/client/src/app/user/interceptors/user-auth.interceptor.ts deleted file mode 100644 index b3ab97cbe..000000000 --- a/client/src/app/user/interceptors/user-auth.interceptor.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { HttpInterceptorFn } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { AuthService } from '../../shared/services/auth.service'; - -export const userAuthInterceptor: HttpInterceptorFn = (req, next) => { - const authService = inject(AuthService); - const token = authService.getToken(); - - if (token) { - const cloned = req.clone({ - setHeaders: { - Authorization: `Bearer ${token}`, - }, - }); - return next(cloned); - } - - return next(req); -}; diff --git a/client/src/app/vendor/interceptors/vendor-auth.interceptor.ts b/client/src/app/vendor/interceptors/vendor-auth.interceptor.ts deleted file mode 100644 index 2096baad5..000000000 --- a/client/src/app/vendor/interceptors/vendor-auth.interceptor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { HttpInterceptorFn } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { AuthService } from '../../shared/services/auth.service'; - -export const vendorAuthInterceptor: HttpInterceptorFn = (req, next) => { - const authService = inject(AuthService); - const token = authService.getToken(); - - if (token) { - const cloned = req.clone({ - setHeaders: { Authorization: `Bearer ${token}`, 'X-Portal': 'vendor' }, - }); - return next(cloned); - } - return next(req); -}; From 99d98ee47b225e19131236f257f481557fcc5f69 Mon Sep 17 00:00:00 2001 From: syamjith45 Date: Fri, 5 Jun 2026 16:27:13 +0530 Subject: [PATCH 22/59] feat: implement refresh token functionality and add user authentication interceptor --- client/angular.json | 3 +- client/src/app/app.config.ts | 8 ++- .../shared/interceptors/error.interceptor.ts | 4 +- .../app/shared/models/auth-response.model.ts | 7 +++ .../app/shared/services/auth.repository.ts | 5 ++ .../src/app/shared/services/auth.service.ts | 49 ++++++++++++++----- .../interceptors/user-auth.interceptor.ts | 8 +++ 7 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 client/src/app/user/interceptors/user-auth.interceptor.ts diff --git a/client/angular.json b/client/angular.json index 54e384264..40e2d5843 100644 --- a/client/angular.json +++ b/client/angular.json @@ -2,7 +2,8 @@ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { - "packageManager": "npm" + "packageManager": "npm", + "analytics": false }, "newProjectRoot": "projects", "projects": { diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index ffa3acdde..dc32d726e 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -1,6 +1,6 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; -import { provideHttpClient, withInterceptors,HttpInterceptorFn } from '@angular/common/http'; +import { provideHttpClient, withInterceptors, withXsrfConfiguration } from '@angular/common/http'; import { routes } from './core/app.routes'; import { appProviders } from './core/providers/app-providers'; import { errorInterceptor } from './shared/interceptors/error.interceptor'; @@ -16,7 +16,11 @@ export const appConfig: ApplicationConfig = { withInterceptors([ credentialsInterceptor, errorInterceptor, - ]) + ]), + withXsrfConfiguration({ + cookieName: 'XSRF-TOKEN', + headerName: 'X-XSRF-TOKEN', + }) ), ...appProviders, ], diff --git a/client/src/app/shared/interceptors/error.interceptor.ts b/client/src/app/shared/interceptors/error.interceptor.ts index f839d2ebc..037dd1123 100644 --- a/client/src/app/shared/interceptors/error.interceptor.ts +++ b/client/src/app/shared/interceptors/error.interceptor.ts @@ -17,8 +17,8 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { message = 'Unable to connect to server'; break; case 401: - message = 'Session expired. Please login again.'; - authService.logout(); + message = 'Session expired. Refreshing...'; + authService.refreshToken(); break; case 403: message = 'You do not have permission to perform this action'; diff --git a/client/src/app/shared/models/auth-response.model.ts b/client/src/app/shared/models/auth-response.model.ts index 3036a76c5..85cffd662 100644 --- a/client/src/app/shared/models/auth-response.model.ts +++ b/client/src/app/shared/models/auth-response.model.ts @@ -35,3 +35,10 @@ export interface ResetPasswordRequest { password: string; confirmPassword: string; } + +export interface RefreshTokenApiResponse { + userId: string; + name: string; + email: string; + role: string; +} diff --git a/client/src/app/shared/services/auth.repository.ts b/client/src/app/shared/services/auth.repository.ts index 901e6be94..925cbe14f 100644 --- a/client/src/app/shared/services/auth.repository.ts +++ b/client/src/app/shared/services/auth.repository.ts @@ -53,8 +53,13 @@ signup(payload: SignupRequest): Observable { ); } +<<<<<<< Updated upstream refreshToken(): Observable { return this.http.post( +======= + refreshToken(): Observable { + return this.http.post( +>>>>>>> Stashed changes `${this.apiUrl}${API_ENDPOINTS.AUTH.REFRESH_TOKEN}`, {} ); diff --git a/client/src/app/shared/services/auth.service.ts b/client/src/app/shared/services/auth.service.ts index c253a7687..3df72c1a5 100644 --- a/client/src/app/shared/services/auth.service.ts +++ b/client/src/app/shared/services/auth.service.ts @@ -37,7 +37,7 @@ export class AuthService { this.loadAllSessions(); } - private portalKeys(portal: string): { user: string } { + private portalKeys(portal: string): { token: string; refresh: string; user: string } { return { user: `bmv_${portal}_user`, }; @@ -57,9 +57,10 @@ export class AuthService { } private loadSession(portal: string): AuthUser | null { - const user = this.portalKeys(portal).user; - const userJson = this.storage.get(user); - if (userJson) { + const keys = this.portalKeys(portal); + const token = this.storage.get(keys.token); + const userJson = this.storage.get(keys.user); + if (token && userJson) { try { return JSON.parse(userJson); } catch { @@ -70,9 +71,11 @@ export class AuthService { return null; } - private savePortalSession(portal: string, user: AuthUser): void { - const userToStore = this.portalKeys(portal).user; - this.storage.set(userToStore, JSON.stringify(user)); + private savePortalSession(portal: string, token: string, refreshToken: string, user: AuthUser): void { + const keys = this.portalKeys(portal); + this.storage.set(keys.token, token); + this.storage.set(keys.refresh, refreshToken); + this.storage.set(keys.user, JSON.stringify(user)); if (portal === 'admin') this.adminSession.set(user); else if (portal === 'vendor') this.vendorSession.set(user); @@ -80,8 +83,10 @@ export class AuthService { } private clearPortalSession(portal: string): void { - const user = this.portalKeys(portal).user; - this.storage.remove(user); + const keys = this.portalKeys(portal); + this.storage.remove(keys.token); + this.storage.remove(keys.refresh); + this.storage.remove(keys.user); if (portal === 'admin') this.adminSession.set(null); else if (portal === 'vendor') this.vendorSession.set(null); @@ -110,6 +115,20 @@ export class AuthService { } } + getToken(): string | null { + const portal = this.detectPortal(); + return this.storage.get(this.portalKeys(portal).token); + } + + getTokenForPortal(portal: string): string | null { + return this.storage.get(this.portalKeys(portal).token); + } + + getRefreshToken(): string | null { + const portal = this.detectPortal(); + return this.storage.get(this.portalKeys(portal).refresh); + } + hasRole(role: UserRole): boolean { return this.currentUser()?.role === role; } @@ -198,11 +217,19 @@ export class AuthService { refreshToken(): void { const portal = this.detectPortal(); - this.authRepository.refreshToken().subscribe({ + const refreshToken = this.getRefreshToken(); + if (!refreshToken) { + this.clearPortalSession(portal); + return; + } + this.authRepository.refreshToken(refreshToken).subscribe({ next: (response) => { - this.savePortalSession(portal,this.toAuthUser(response)) + const keys = this.portalKeys(portal); + this.storage.set(keys.token, response.accessToken); + this.storage.set(keys.refresh, response.refreshToken); }, error: () => { + const portal = this.detectPortal(); this.clearPortalSession(portal); this.router.navigate(['/']); }, diff --git a/client/src/app/user/interceptors/user-auth.interceptor.ts b/client/src/app/user/interceptors/user-auth.interceptor.ts new file mode 100644 index 000000000..ebe9f5ad5 --- /dev/null +++ b/client/src/app/user/interceptors/user-auth.interceptor.ts @@ -0,0 +1,8 @@ +import { HttpInterceptorFn } from '@angular/common/http'; + +export const userAuthInterceptor: HttpInterceptorFn = (req, next) => { + const cloned = req.clone({ + withCredentials: true, + }); + return next(cloned); +}; From 244eb9d3b1e14cfb12aff1dc300c12aeb9036c84 Mon Sep 17 00:00:00 2001 From: syamjith45 Date: Fri, 5 Jun 2026 16:29:04 +0530 Subject: [PATCH 23/59] feat: update authentication flow to include access and refresh tokens in responses --- client/src/app/app.config.ts | 6 +++--- .../app/shared/models/auth-response.model.ts | 4 ++++ .../app/shared/services/auth.repository.ts | 6 +----- .../src/app/shared/services/auth.service.ts | 19 ++++++++----------- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index dc32d726e..a2583ce5d 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -1,12 +1,12 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; -import { provideHttpClient, withInterceptors, withXsrfConfiguration } from '@angular/common/http'; +import { provideHttpClient, withInterceptors, withXsrfConfiguration, HttpInterceptorFn } from '@angular/common/http'; import { routes } from './core/app.routes'; import { appProviders } from './core/providers/app-providers'; import { errorInterceptor } from './shared/interceptors/error.interceptor'; -const credentialsInterceptor:HttpInterceptorFn = (req,next) => - next(req.clone({withCredentials:true})); +const credentialsInterceptor: HttpInterceptorFn = (req, next) => + next(req.clone({withCredentials:true})); export const appConfig: ApplicationConfig = { providers: [ diff --git a/client/src/app/shared/models/auth-response.model.ts b/client/src/app/shared/models/auth-response.model.ts index 85cffd662..ee04c4537 100644 --- a/client/src/app/shared/models/auth-response.model.ts +++ b/client/src/app/shared/models/auth-response.model.ts @@ -3,6 +3,8 @@ export interface AuthResponse { name: string; email: string; role: string; + accessToken?: string; + refreshToken?: string; } export interface AuthUser { @@ -41,4 +43,6 @@ export interface RefreshTokenApiResponse { name: string; email: string; role: string; + accessToken: string; + refreshToken: string; } diff --git a/client/src/app/shared/services/auth.repository.ts b/client/src/app/shared/services/auth.repository.ts index 925cbe14f..3a8214bf8 100644 --- a/client/src/app/shared/services/auth.repository.ts +++ b/client/src/app/shared/services/auth.repository.ts @@ -9,6 +9,7 @@ import { SignupRequest, ForgotPasswordRequest, ResetPasswordRequest, + RefreshTokenApiResponse, } from '../models/auth-response.model'; import { ApiResponse } from '../models/api-response.model'; @@ -53,13 +54,8 @@ signup(payload: SignupRequest): Observable { ); } -<<<<<<< Updated upstream - refreshToken(): Observable { - return this.http.post( -======= refreshToken(): Observable { return this.http.post( ->>>>>>> Stashed changes `${this.apiUrl}${API_ENDPOINTS.AUTH.REFRESH_TOKEN}`, {} ); diff --git a/client/src/app/shared/services/auth.service.ts b/client/src/app/shared/services/auth.service.ts index 3df72c1a5..99764d651 100644 --- a/client/src/app/shared/services/auth.service.ts +++ b/client/src/app/shared/services/auth.service.ts @@ -39,6 +39,8 @@ export class AuthService { private portalKeys(portal: string): { token: string; refresh: string; user: string } { return { + token: `bmv_${portal}_token`, + refresh: `bmv_${portal}_refresh`, user: `bmv_${portal}_user`, }; } @@ -139,7 +141,7 @@ export class AuthService { next: (response) => { const user = this.toAuthUser(response); const portal = response.role === UserRole.Vendor ? 'vendor' : 'user'; - this.savePortalSession(portal, user); + this.savePortalSession(portal, response.accessToken || '', response.refreshToken || '', user); this.notification.success('Login successful'); this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); this.loading.set(false); @@ -156,7 +158,7 @@ export class AuthService { this.authRepository.adminLogin(payload).subscribe({ next: (response) => { const user = this.toAuthUser(response); - this.savePortalSession('admin', user); + this.savePortalSession('admin', response.accessToken || '', response.refreshToken || '', user); this.notification.success('Login successful'); this.router.navigate(['/admin/dashboard']); this.loading.set(false); @@ -174,7 +176,7 @@ export class AuthService { next: (response) => { const user = this.toAuthUser(response); const portal = response.role === UserRole.Vendor ? 'vendor' : 'user'; - this.savePortalSession(portal, user); + this.savePortalSession(portal, response.accessToken || '', response.refreshToken || '', user); this.notification.success(portal === 'vendor' ? 'Vendor account created successfully' : 'Account created successfully'); this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); this.loading.set(false); @@ -217,16 +219,11 @@ export class AuthService { refreshToken(): void { const portal = this.detectPortal(); - const refreshToken = this.getRefreshToken(); - if (!refreshToken) { - this.clearPortalSession(portal); - return; - } - this.authRepository.refreshToken(refreshToken).subscribe({ + this.authRepository.refreshToken().subscribe({ next: (response) => { const keys = this.portalKeys(portal); - this.storage.set(keys.token, response.accessToken); - this.storage.set(keys.refresh, response.refreshToken); + this.storage.set(keys.token, response.accessToken || ''); + this.storage.set(keys.refresh, response.refreshToken || ''); }, error: () => { const portal = this.detectPortal(); From 0203b540dd8f2421ff7bf045936a61224d357c93 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Mon, 8 Jun 2026 17:09:36 +0530 Subject: [PATCH 24/59] refactor: Add ErrorCode enum and BusinessException to standardize exception handling --- .../common/exception/BusinessException.java | 41 +++++++ .../server/common/exception/ErrorCode.java | 78 ++++++++++++ .../exception/GlobalExceptionHandler.java | 116 ++++-------------- .../common/response/ApiErrorResponse.java | 3 + .../venue/service/VenueServiceImpl.java | 42 ++++++- 5 files changed, 187 insertions(+), 93 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/BusinessException.java create mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/BusinessException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/BusinessException.java new file mode 100644 index 000000000..5e512a637 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/BusinessException.java @@ -0,0 +1,41 @@ +package com.bookmyvenue.server.common.exception; + + + +import lombok.Getter; + +/** + * Base exception for business and domain rule violations. + * + * Used when a request cannot be processed because it violates + * application-specific rules or constraints. + * + * Examples: + * - User already exists + * - Invalid credentials + * - Venue not found + * - Booking slot unavailable + * - Access denied + * + * Each exception is associated with an {@link ErrorCode}, + * which determines the HTTP status, error code, + * and default error message returned to API clients. + */ +@Getter +public class BusinessException extends RuntimeException { + + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public BusinessException( + ErrorCode errorCode, + String message + ) { + super(message); + this.errorCode = errorCode; + } +} diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java new file mode 100644 index 000000000..c32e3c6b1 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java @@ -0,0 +1,78 @@ +package com.bookmyvenue.server.common.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +/** + * Defines all application-specific error codes. + * + * Each error code contains: + * - HTTP status to return + * - Unique error code for clients + * - Default error message + */ +@Getter +public enum ErrorCode { + + USER_ALREADY_EXISTS( + HttpStatus.CONFLICT, + "USER_ALREADY_EXISTS", + "User already exists" + ), + + INVALID_CREDENTIALS( + HttpStatus.UNAUTHORIZED, + "INVALID_CREDENTIALS", + "Invalid credentials" + ), + + VENUE_NOT_FOUND( + HttpStatus.NOT_FOUND, + "VENUE_NOT_FOUND", + "Venue not found" + ), + + VENUE_CATEGORY_NOT_FOUND( + HttpStatus.NOT_FOUND, + "VENUE_CATEGORY_NOT_FOUND", + "Venue category not found" + ), + + BOOKING_NOT_FOUND( + HttpStatus.NOT_FOUND, + "BOOKING_NOT_FOUND", + "Booking not found" + ), + + ACCESS_DENIED( + HttpStatus.FORBIDDEN, + "ACCESS_DENIED", + "Access denied" + ), + + BAD_REQUEST( + HttpStatus.BAD_REQUEST, + "BAD_REQUEST", + "Invalid request" + ), + + INVALID_TOKEN( + HttpStatus.UNAUTHORIZED, + "INVALID_TOKEN", + "Invalid or expired token" + ); + + private final HttpStatus status; + private final String code; + private final String message; + + ErrorCode( + HttpStatus status, + String code, + String message + ) { + this.status = status; + this.code = code; + this.message = message; + } +} diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java index 4f6ce6228..dd63cc0b6 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import io.jsonwebtoken.JwtException; import io.swagger.v3.oas.annotations.Hidden; import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -18,111 +19,46 @@ * ensuring clients receive meaningful HTTP status codes and messages. */ @Hidden +@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** - * Handles attempts to create a user with an email or phone number - * that already exists in the system. - * - * Returns HTTP 409 (Conflict). - */ - @ExceptionHandler(UserAlreadyExistsException.class) - @ResponseStatus(HttpStatus.CONFLICT) - public ApiErrorResponse handleUserAlreadyExistsException( - UserAlreadyExistsException ex, - HttpServletRequest request - ) { - return buildErrorResponse( - HttpStatus.CONFLICT, - ex.getMessage(), - request - ); - } - - /** - * Handles authentication failures such as invalid email or password. - * - * Returns HTTP 401 (Unauthorized). - */ - @ExceptionHandler(InvalidCredentialsException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - public ApiErrorResponse handleInvalidCredentialsException( - InvalidCredentialsException ex, - HttpServletRequest request - ) { - return buildErrorResponse( - HttpStatus.UNAUTHORIZED, - ex.getMessage(), - request - ); - } - - /** - * Handles requests for resources that do not exist. + * Handles business and domain rule violations. * * Examples: - * - User not found + * - User already exists + * - Invalid credentials * - Venue not found - * - Booking not found + * - Booking slot unavailable + * - Access denied * - * Returns HTTP 404 (Not Found). + * The HTTP status and error details are determined + * by the associated {@link ErrorCode}. */ - @ExceptionHandler(ResourceNotFoundException.class) - @ResponseStatus(HttpStatus.NOT_FOUND) - public ApiErrorResponse handleResourceNotFoundException( - ResourceNotFoundException ex, + @ExceptionHandler(BusinessException.class) + public ApiErrorResponse handleBusinessException( + BusinessException ex, HttpServletRequest request ) { - return buildErrorResponse( - HttpStatus.NOT_FOUND, - ex.getMessage(), - request - ); - } - /** - * Fallback handler for unexpected application errors. - * - * Prevents internal stack traces from being exposed to API consumers - * and returns a generic error response. - * - * Returns HTTP 500 (Internal Server Error). - */ - @ExceptionHandler(Exception.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public ApiErrorResponse handleGenericException( - Exception ex, - HttpServletRequest request - ) { - return buildErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR, - "An unexpected error occurred", - request - ); - } + ErrorCode errorCode = ex.getErrorCode(); - /** - * Handles requests that fail business validation rules. - * - * Examples: - * - Admin self-registration attempted - * - Invalid booking request - * - Invalid venue configuration - * - * Returns HTTP 400 (Bad Request). - */ - @ExceptionHandler(BadRequestException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ApiErrorResponse handleBadRequestException( - BadRequestException ex, - HttpServletRequest request - ) { - return buildErrorResponse( - HttpStatus.BAD_REQUEST, + log.warn( + "Business exception. code={}, message={}, path={}", + errorCode.getCode(), ex.getMessage(), - request + request.getRequestURI() ); + + return ApiErrorResponse.builder() + .status(errorCode.getStatus().value()) + .code(errorCode.getCode()) + .error(errorCode.getStatus().getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .timestamp(LocalDateTime.now()) + .build(); } diff --git a/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java b/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java index 0c309d667..3b0df2d25 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java +++ b/server/src/main/java/com/bookmyvenue/server/common/response/ApiErrorResponse.java @@ -19,6 +19,9 @@ public class ApiErrorResponse { @Schema(description = "HTTP status code", example = "401") private int status; + @Schema(description = "Application error code", example = "RESOURCE_NOT_FOUND") + private String code; + @Schema(description = "Error type", example = "Unauthorized") private String error; diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index 896701fae..53b6bb9fa 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -14,6 +14,7 @@ import com.bookmyvenue.server.venue.repository.VenueRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; @@ -23,6 +24,7 @@ @Service @RequiredArgsConstructor @Transactional +@Slf4j public class VenueServiceImpl implements VenueService { private final VenueRepository venueRepository; @@ -32,15 +34,20 @@ public class VenueServiceImpl implements VenueService { @Override public VenueResponse createVenue(CreateVenueRequest request) { - + log.info("Creating venue with name: {}, categoryId: {}", + request.getName(), + request.getCategoryId()); VenueCategory category = venueCategoryRepository .findById(request.getCategoryId()) .orElseThrow(() -> new RuntimeException("Venue category not found")); + // Get currently authenticated venue owner User currentUser = authenticatedUserService.getCurrentUser(); + + // New venues require admin approval before becoming visible Venue venue = Venue.builder() .name(request.getName()) .description(request.getDescription()) @@ -54,6 +61,9 @@ public VenueResponse createVenue(CreateVenueRequest request) { .build(); Venue savedVenue = venueRepository.save(venue); + log.info("Venue created successfully. venueId={}, ownerId={}", + savedVenue.getId(), + currentUser.getId()); return venueMapper.toResponse(savedVenue); } @@ -147,10 +157,17 @@ public VenueResponse updateVenue( .orElseThrow(() -> new RuntimeException("Venue not found")); - User owner = authenticatedUserService.getCurrentUser(); - if (!venue.getOwner().getId().equals(owner.getId())) { + // Ensure only the venue owner can modify venue details + User currentUser = authenticatedUserService.getCurrentUser(); + if (!venue.getOwner().getId().equals(currentUser.getId())) { throw new RuntimeException("You are not the owner of this venue"); } + log.warn( + "Unauthorized venue update attempt. venueId={}, ownerId={}, requesterId={}", + venueId, + venue.getOwner().getId(), + currentUser.getId() + ); if (request.getName() != null) { venue.setName(request.getName()); } @@ -187,6 +204,12 @@ public VenueResponse updateVenue( Venue updatedVenue = venueRepository.save(venue); + log.info( + "Venue updated. venueId={}, ownerId={}", + updatedVenue.getId(), + currentUser.getId() + ); + return venueMapper.toResponse(updatedVenue); } @@ -198,9 +221,22 @@ public void deleteVenue(Long venueId){ User currentUser = authenticatedUserService.getCurrentUser(); + // Ensure only the venue owner can delete the venue if (!venue.getOwner().getId().equals(currentUser.getId())) { + log.warn( + "Unauthorized venue delete attempt. venueId={}, ownerId={}, requesterId={}", + venueId, + venue.getOwner().getId(), + currentUser.getId() + ); throw new RuntimeException("You are not the owner of this venue"); } venueRepository.delete(venue); + + log.info( + "Venue deleted. venueId={}, ownerId={}", + venueId, + currentUser.getId() + ); } } \ No newline at end of file From 30a63e31ec2aef4b5a0a975ce75c4231efa1cf65 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Tue, 9 Jun 2026 15:42:32 +0530 Subject: [PATCH 25/59] refactor: implement cleaner exception handling --- .../auth/controller/AuthController.java | 5 +++-- .../server/auth/service/AuthServiceImpl.java | 22 +++++++++---------- .../service/AuthenticatedUserService.java | 4 +++- .../common/exception/BadRequestException.java | 8 ------- .../server/common/exception/ErrorCode.java | 19 ++++++++++++++-- .../exception/GlobalExceptionHandler.java | 9 ++++++-- .../InvalidCredentialsException.java | 8 ------- .../exception/ResourceNotFoundException.java | 8 ------- .../exception/UserAlreadyExistsException.java | 8 ------- .../venue/service/VenueServiceImpl.java | 22 ++++++++++--------- 10 files changed, 52 insertions(+), 61 deletions(-) delete mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java delete mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java delete mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java delete mode 100644 server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java diff --git a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java index 51190ac4f..3144d932d 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java @@ -6,7 +6,8 @@ import com.bookmyvenue.server.auth.dto.response.AuthResult; import com.bookmyvenue.server.auth.security.CookieUtils; import com.bookmyvenue.server.auth.service.AuthService; -import com.bookmyvenue.server.common.exception.InvalidCredentialsException; +import com.bookmyvenue.server.common.exception.BusinessException; +import com.bookmyvenue.server.common.exception.ErrorCode; import com.bookmyvenue.server.common.response.ApiErrorResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -86,7 +87,7 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest reque public ResponseEntity refreshToken(@CookieValue(value=CookieUtils.REFRESH_TOKEN,required = false) String refreshToken) { if (refreshToken == null) { - throw new InvalidCredentialsException("Refresh token not found"); + throw new BusinessException(ErrorCode.INVALID_REFRESH_TOKEN); } AuthResult result = authService.refreshToken(refreshToken); ResponseCookie accessCookie = cookieUtils.accessTokenCookie(result.accessToken()); diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java index 4fead1074..6aa4b6b58 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java @@ -5,9 +5,7 @@ import com.bookmyvenue.server.auth.dto.response.AuthResponse; import com.bookmyvenue.server.auth.dto.response.AuthResult; import com.bookmyvenue.server.auth.security.JwtService; -import com.bookmyvenue.server.common.exception.BadRequestException; -import com.bookmyvenue.server.common.exception.InvalidCredentialsException; -import com.bookmyvenue.server.common.exception.UserAlreadyExistsException; +import com.bookmyvenue.server.common.exception.*; import com.bookmyvenue.server.user.entity.User; import com.bookmyvenue.server.user.enums.Role; import com.bookmyvenue.server.user.repository.UserRepository; @@ -34,17 +32,19 @@ public AuthResult register(RegisterRequest request) { if (userRepository.existsByEmail(request.getEmail())) { log.warn("Registration failed. Email already exists: {}", request.getEmail()); - throw new UserAlreadyExistsException("Email already registered"); + throw new BusinessException(ErrorCode.USER_ALREADY_EXISTS); } if (userRepository.existsByPhone(request.getPhone())) { log.warn("Registration failed. Phone already exists: {}", request.getPhone()); - throw new UserAlreadyExistsException("Phone number already registered"); + throw new BusinessException(ErrorCode.PHONE_ALREADY_EXISTS); } if (request.getRole() == Role.ADMIN) { log.warn("Attempted ADMIN registration for email: {}", request.getEmail()); - throw new BadRequestException("Admin registration is not allowed"); + throw new BusinessException( + ErrorCode.ADMIN_REGISTRATION_NOT_ALLOWED + ); } User user = User.builder() .name(request.getName()) @@ -83,12 +83,12 @@ public AuthResult login(LoginRequest request) { User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> { log.warn("Login failed. User not found: {}", request.getEmail()); - return new InvalidCredentialsException("Invalid email or password"); + return new BusinessException(ErrorCode.INVALID_CREDENTIALS); }); if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { log.warn("Login failed. Invalid password for email: {}", request.getEmail()); - throw new InvalidCredentialsException("Invalid email or password"); + throw new BusinessException(ErrorCode.INVALID_CREDENTIALS); } String accessToken = jwtService.generateAccessToken(user.getEmail(), user.getRole()); @@ -117,14 +117,12 @@ public AuthResult refreshToken(String refreshToken) { String email = jwtService.extractEmail(refreshToken); User user = userRepository.findByEmail(email) .orElseThrow(() -> - new InvalidCredentialsException( - "Invalid refresh token" - ) + new BusinessException(ErrorCode.INVALID_REFRESH_TOKEN) ); if (!jwtService.isTokenValid(refreshToken, user.getEmail() )) { - throw new InvalidCredentialsException("Invalid refresh token"); + throw new BusinessException(ErrorCode.INVALID_REFRESH_TOKEN); } String accessToken = jwtService.generateAccessToken(user.getEmail(), user.getRole()); String newRefreshToken = jwtService.generateRefreshToken(user.getEmail(), user.getRole()); diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthenticatedUserService.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthenticatedUserService.java index faec26c6f..0809e839e 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthenticatedUserService.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthenticatedUserService.java @@ -1,5 +1,7 @@ package com.bookmyvenue.server.auth.service; +import com.bookmyvenue.server.common.exception.BusinessException; +import com.bookmyvenue.server.common.exception.ErrorCode; import com.bookmyvenue.server.user.entity.User; import com.bookmyvenue.server.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -21,6 +23,6 @@ public User getCurrentUser() { return userRepository.findByEmail(email) .orElseThrow(() -> - new RuntimeException("User not found")); + new BusinessException(ErrorCode.USER_NOT_FOUND)); } } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java deleted file mode 100644 index 82789c5ac..000000000 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/BadRequestException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.bookmyvenue.server.common.exception; - -public class BadRequestException extends RuntimeException { - - public BadRequestException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java index c32e3c6b1..687fac8bc 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java @@ -19,6 +19,16 @@ public enum ErrorCode { "USER_ALREADY_EXISTS", "User already exists" ), + PHONE_ALREADY_EXISTS( + HttpStatus.CONFLICT, + "PHONE_ALREADY_EXISTS", + "phone number already registered" + ), + ADMIN_REGISTRATION_NOT_ALLOWED( + HttpStatus.BAD_REQUEST, + "ADMIN_REGISTRATION_NOT_ALLOWED", + "admin registration not allowed" + ), INVALID_CREDENTIALS( HttpStatus.UNAUTHORIZED, @@ -55,10 +65,15 @@ public enum ErrorCode { "BAD_REQUEST", "Invalid request" ), + USER_NOT_FOUND( + HttpStatus.NOT_FOUND, + "USER NOT FOUND", + "user not found" + ), - INVALID_TOKEN( + INVALID_REFRESH_TOKEN( HttpStatus.UNAUTHORIZED, - "INVALID_TOKEN", + "INVALID_REFRESH_TOKEN", "Invalid or expired token" ); diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java index dd63cc0b6..295327163 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -37,7 +38,7 @@ public class GlobalExceptionHandler { * by the associated {@link ErrorCode}. */ @ExceptionHandler(BusinessException.class) - public ApiErrorResponse handleBusinessException( + public ResponseEntity handleBusinessException( BusinessException ex, HttpServletRequest request ) { @@ -51,7 +52,7 @@ public ApiErrorResponse handleBusinessException( request.getRequestURI() ); - return ApiErrorResponse.builder() + ApiErrorResponse response = ApiErrorResponse.builder() .status(errorCode.getStatus().value()) .code(errorCode.getCode()) .error(errorCode.getStatus().getReasonPhrase()) @@ -59,6 +60,10 @@ public ApiErrorResponse handleBusinessException( .path(request.getRequestURI()) .timestamp(LocalDateTime.now()) .build(); + + return ResponseEntity + .status(errorCode.getStatus()) + .body(response); } diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java deleted file mode 100644 index 358149b10..000000000 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/InvalidCredentialsException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.bookmyvenue.server.common.exception; - -public class InvalidCredentialsException extends RuntimeException { - - public InvalidCredentialsException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java deleted file mode 100644 index 0046a7a37..000000000 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/ResourceNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.bookmyvenue.server.common.exception; - -public class ResourceNotFoundException extends RuntimeException { - - public ResourceNotFoundException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java b/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java deleted file mode 100644 index 44b2ab437..000000000 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/UserAlreadyExistsException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.bookmyvenue.server.common.exception; - -public class UserAlreadyExistsException extends RuntimeException { - - public UserAlreadyExistsException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index 53b6bb9fa..9c490f0f9 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -1,6 +1,8 @@ package com.bookmyvenue.server.venue.service; import com.bookmyvenue.server.auth.service.AuthenticatedUserService; +import com.bookmyvenue.server.common.exception.BusinessException; +import com.bookmyvenue.server.common.exception.ErrorCode; import com.bookmyvenue.server.user.entity.User; import com.bookmyvenue.server.user.repository.UserRepository; import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; @@ -40,7 +42,7 @@ public VenueResponse createVenue(CreateVenueRequest request) { VenueCategory category = venueCategoryRepository .findById(request.getCategoryId()) .orElseThrow(() -> - new RuntimeException("Venue category not found")); + new BusinessException(ErrorCode.VENUE_CATEGORY_NOT_FOUND)); // Get currently authenticated venue owner User currentUser = @@ -79,7 +81,7 @@ public List getAllVenues() { public VenueResponse getVenue(Long venueId) { Venue venue = venueRepository.findById(venueId) .orElseThrow(()-> - new RuntimeException("venue not found")); + new BusinessException(ErrorCode.VENUE_NOT_FOUND)); return venueMapper.toResponse(venue); } @@ -141,8 +143,8 @@ public VenueResponse getApprovedVenue(Long id) { Venue venue = venueRepository .findByIdAndStatus(id, VenueStatus.APPROVED) - .orElseThrow(() -> - new RuntimeException("Venue not found")); + .orElseThrow(()-> + new BusinessException(ErrorCode.VENUE_NOT_FOUND)); return venueMapper.toResponse(venue); } @@ -154,13 +156,13 @@ public VenueResponse updateVenue( ) { Venue venue = venueRepository.findById(venueId) - .orElseThrow(() -> - new RuntimeException("Venue not found")); + .orElseThrow(()-> + new BusinessException(ErrorCode.VENUE_NOT_FOUND)); // Ensure only the venue owner can modify venue details User currentUser = authenticatedUserService.getCurrentUser(); if (!venue.getOwner().getId().equals(currentUser.getId())) { - throw new RuntimeException("You are not the owner of this venue"); + throw new BusinessException(ErrorCode.ACCESS_DENIED); } log.warn( "Unauthorized venue update attempt. venueId={}, ownerId={}, requesterId={}", @@ -197,7 +199,7 @@ public VenueResponse updateVenue( VenueCategory category = venueCategoryRepository .findById(request.getCategoryId()) .orElseThrow(() -> - new RuntimeException("Venue category not found")); + new BusinessException(ErrorCode.VENUE_CATEGORY_NOT_FOUND)); venue.setCategory(category); } @@ -217,7 +219,7 @@ public VenueResponse updateVenue( public void deleteVenue(Long venueId){ Venue venue = venueRepository.findById(venueId) .orElseThrow(() -> - new RuntimeException("Venue not found")); + new BusinessException(ErrorCode.VENUE_NOT_FOUND)); User currentUser = authenticatedUserService.getCurrentUser(); @@ -229,7 +231,7 @@ public void deleteVenue(Long venueId){ venue.getOwner().getId(), currentUser.getId() ); - throw new RuntimeException("You are not the owner of this venue"); + throw new BusinessException(ErrorCode.ACCESS_DENIED); } venueRepository.delete(venue); From dc2e0e77fb4cdae11377a6db3cae89994bc6c616 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Wed, 10 Jun 2026 00:37:04 +0530 Subject: [PATCH 26/59] filter class added --- .../auth/controller/AuthController.java | 3 + .../security/CustomUserDetailsService.java | 35 ++++++++ .../auth/security/CustomUserPrincipal.java | 45 +++++++++++ .../security/JwtAuthenticationFilter.java | 81 +++++++++++++++++++ .../server/auth/security/SecurityConfig.java | 64 ++++++++++----- .../server/venue/service/VenueService.java | 12 ++- 6 files changed, 218 insertions(+), 22 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/CustomUserDetailsService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/CustomUserPrincipal.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/security/JwtAuthenticationFilter.java diff --git a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java index 51190ac4f..957423f2c 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java @@ -97,6 +97,9 @@ public ResponseEntity refreshToken(@CookieValue(value=CookieUtils. .body(result.response()); } + + + @Operation(summary = "Logout user", description = "Clears authentication cookies.") @ApiResponse(responseCode = "204", description = "Logout successful") @PostMapping("/logout") diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/CustomUserDetailsService.java b/server/src/main/java/com/bookmyvenue/server/auth/security/CustomUserDetailsService.java new file mode 100644 index 000000000..db8f2d421 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/CustomUserDetailsService.java @@ -0,0 +1,35 @@ +package com.bookmyvenue.server.auth.security; + +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) + throws UsernameNotFoundException { + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> + new UsernameNotFoundException( + "User not found with email: " + email + ) + ); + + return new CustomUserPrincipal( + user.getId(), + user.getEmail(), + user.getPassword(), + user.getRole() + ); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/CustomUserPrincipal.java b/server/src/main/java/com/bookmyvenue/server/auth/security/CustomUserPrincipal.java new file mode 100644 index 000000000..fe168ed26 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/CustomUserPrincipal.java @@ -0,0 +1,45 @@ +package com.bookmyvenue.server.auth.security; + +import com.bookmyvenue.server.user.enums.Role; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +/** + * Custom authenticated user object stored in Spring Security's SecurityContext. + * Holds the user's identity and role information after successful authentication. + * Inorder to avoid the repeated db check. + */ +@Getter +@RequiredArgsConstructor +public class CustomUserPrincipal implements UserDetails { + + private final UUID id; + private final String email; + private final String password; + private final Role role; + + // Returns the user's role as a Spring Security authority + @Override + public Collection getAuthorities() { + return List.of( + new SimpleGrantedAuthority("ROLE_" + role.name()) + ); + } + + @Override + public String getUsername() { + return email; + } + + @Override + public String getPassword() { + return password; + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/JwtAuthenticationFilter.java b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtAuthenticationFilter.java new file mode 100644 index 000000000..55406c6ae --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtAuthenticationFilter.java @@ -0,0 +1,81 @@ +package com.bookmyvenue.server.auth.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * Authenticates incoming requests using the JWT access token stored in cookies. + */ +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final CustomUserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + try { + + // Extract access token from cookie + String token = extractAccessToken(request); + // No token -> continue as anonymous request + if (token == null) { + filterChain.doFilter(request, response); + return; + } + // Extract email from JWT + String email = jwtService.extractEmail(token); + // If email exists and user is not already authenticated + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + + // Validate JWT + if (jwtService.isTokenValid(token, userDetails.getUsername())) { + // Create authenticated user + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + // Store authentication in SecurityContext + SecurityContextHolder.getContext() + .setAuthentication(authentication); + } + } + + } catch (Exception ex) { + // Invalid token -> request remains unauthenticated + SecurityContextHolder.clearContext(); + } + filterChain.doFilter(request, response); + } + + /** + * Extracts access token from cookies. + */ + private String extractAccessToken(HttpServletRequest request) { + + if (request.getCookies() == null) { + return null; + } + + for (Cookie cookie : request.getCookies()) { + + if (CookieUtils.ACCESS_TOKEN.equals(cookie.getName())) { + return cookie.getValue(); + } + } + + return null; + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java index 0184c2887..99b82c974 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -1,24 +1,29 @@ package com.bookmyvenue.server.auth.security; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; - +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.List; +import java.util.List; @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + /** - * Password encoder bean used to hash and verify passwords. + * Password encoder used for hashing and verifying passwords. */ @Bean public PasswordEncoder passwordEncoder() { @@ -26,38 +31,61 @@ public PasswordEncoder passwordEncoder() { } /** - * Security configuration for MVP authentication endpoints. + * Main Spring Security configuration. */ @Bean - public SecurityFilterChain securityFilterChain( - HttpSecurity http - ) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // Disable CSRF because authentication is JWT-based .csrf(csrf -> csrf.disable()) - .cors(Customizer.withDefaults()) + // Enable configured CORS settings + .cors(Customizer.withDefaults()) + // No HTTP session should be created + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // Endpoint authorization rules .authorizeHttpRequests(auth -> auth + // Public endpoints .requestMatchers( "/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/api/v1/venues/**" ).permitAll() + // Everything else requires authentication .anyRequest().authenticated() ) + // Disable default Spring Security login mechanisms .formLogin(form -> form.disable()) - .httpBasic(basic -> basic.disable()); + .httpBasic(basic -> basic.disable()) + // Register JWT filter before Spring's authentication filter + .addFilterBefore( + jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class); return http.build(); } + + /** + * CORS configuration for frontend applications. + */ @Bean public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(List.of("http://localhost:4200")); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); - config.setAllowedHeaders(List.of("*")); - config.setAllowCredentials(true); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; -} + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("http://localhost:4200")); + config.setAllowedMethods(List.of( + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + // Required for sending cookies + config.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java index 40916d037..7b607363c 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueService.java @@ -8,15 +8,19 @@ import java.util.UUID; public interface VenueService { + VenueResponse createVenue(CreateVenueRequest request); + List getAllVenues(); + VenueResponse getVenue(Long venueId); + List getApprovedVenues(String keyword, String district,Long categoryId); + VenueResponse getApprovedVenue(Long id); - VenueResponse updateVenue( - Long venueId, - UpdateVenueRequest request - ); + + VenueResponse updateVenue(Long venueId, UpdateVenueRequest request); + void deleteVenue(Long venueId); } From d66d6c246e0bfe59b82a39df0e9fa9888fc12e79 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Wed, 10 Jun 2026 21:14:46 +0530 Subject: [PATCH 27/59] feat(slot): implement venue availability template CRUD --- .../server/common/exception/ErrorCode.java | 14 +- .../controller/SlotTemplateController.java | 68 ++++++++ .../request/CreateSlotTemplateRequest.java | 11 ++ .../dto/response/SlotTemplateResponse.java | 19 ++ .../server/slot/entity/SlotTemplate.java | 39 +++++ .../repository/SlotTemplateRepository.java | 16 ++ .../slot/service/SlotTemplateService.java | 22 +++ .../slot/service/SlotTemplateServiceImpl.java | 164 ++++++++++++++++++ .../db/migration/V1__init_schema.sql | 37 ++-- 9 files changed, 371 insertions(+), 19 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/slot/dto/request/CreateSlotTemplateRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/slot/dto/response/SlotTemplateResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/slot/entity/SlotTemplate.java create mode 100644 server/src/main/java/com/bookmyvenue/server/slot/repository/SlotTemplateRepository.java create mode 100644 server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateServiceImpl.java diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java index 687fac8bc..e91deceee 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java @@ -75,7 +75,19 @@ public enum ErrorCode { HttpStatus.UNAUTHORIZED, "INVALID_REFRESH_TOKEN", "Invalid or expired token" - ); + ), + INVALID_TIME_RANGE(HttpStatus.BAD_REQUEST, + " INVALID_TIME_RANGE", + "End time must be after start time"), + + SLOT_TEMPLATE_NOT_FOUND(HttpStatus.NOT_FOUND, + " SLOT_TEMPLATE_NOT_FOUND", + "Slot template not found"), + + OVERLAPPING_SLOT_TEMPLATE(HttpStatus.BAD_REQUEST, + "OVERLAPPING_SLOT_TEMPLATE", + "Slot template overlaps with existing schedule"); + private final HttpStatus status; private final String code; diff --git a/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java b/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java new file mode 100644 index 000000000..d521017ef --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java @@ -0,0 +1,68 @@ +package com.bookmyvenue.server.slot.controller; + +import com.bookmyvenue.server.slot.dto.request.CreateSlotTemplateRequest; +import com.bookmyvenue.server.slot.dto.response.SlotTemplateResponse; +import com.bookmyvenue.server.slot.service.SlotTemplateService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/venues") +@RequiredArgsConstructor +public class SlotTemplateController { + + private final SlotTemplateService slotTemplateService; + + /** + * Creates a new slot template for a venue. + */ + @PostMapping("/{venueId}/slots") + public ResponseEntity createTemplate( + @PathVariable Long venueId, + @Valid @RequestBody CreateSlotTemplateRequest request + ) { + + SlotTemplateResponse response = + slotTemplateService.createTemplate( + venueId, + request + ); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(response); + } + + /** + * Returns all slot templates configured for a venue. + */ + @GetMapping("/{venueId}/slots") + public ResponseEntity> getVenueTemplates( + @PathVariable Long venueId + ) { + + return ResponseEntity.ok( + slotTemplateService.getVenueTemplates( + venueId + ) + ); + } + + /** + * Deletes a slot template. + */ + @DeleteMapping("/slots/{templateId}") + public ResponseEntity deleteTemplate( + @PathVariable Long templateId + ) { + + slotTemplateService.deleteTemplate(templateId); + + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/slot/dto/request/CreateSlotTemplateRequest.java b/server/src/main/java/com/bookmyvenue/server/slot/dto/request/CreateSlotTemplateRequest.java new file mode 100644 index 000000000..f0ad6ecb9 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/slot/dto/request/CreateSlotTemplateRequest.java @@ -0,0 +1,11 @@ +package com.bookmyvenue.server.slot.dto.request; + +import java.time.DayOfWeek; +import java.time.LocalTime; + +public record CreateSlotTemplateRequest( + DayOfWeek dayOfWeek, + LocalTime startTime, + LocalTime endTime +) { +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/slot/dto/response/SlotTemplateResponse.java b/server/src/main/java/com/bookmyvenue/server/slot/dto/response/SlotTemplateResponse.java new file mode 100644 index 000000000..39d73adef --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/slot/dto/response/SlotTemplateResponse.java @@ -0,0 +1,19 @@ +package com.bookmyvenue.server.slot.dto.response; + +import java.time.DayOfWeek; +import java.time.LocalTime; + +public record SlotTemplateResponse( + + Long id, + + DayOfWeek dayOfWeek, + + LocalTime startTime, + + LocalTime endTime, + + boolean active + +) { +} diff --git a/server/src/main/java/com/bookmyvenue/server/slot/entity/SlotTemplate.java b/server/src/main/java/com/bookmyvenue/server/slot/entity/SlotTemplate.java new file mode 100644 index 000000000..c26b64f03 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/slot/entity/SlotTemplate.java @@ -0,0 +1,39 @@ +package com.bookmyvenue.server.slot.entity; + +import com.bookmyvenue.server.venue.entity.Venue; +import jakarta.persistence.*; +import lombok.*; + +import java.time.DayOfWeek; +import java.time.LocalTime; + +@Entity +@Table(name = "venue_availability_template") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SlotTemplate { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "day_of_week", nullable = false) + private DayOfWeek dayOfWeek; + + @Column(name = "start_time", nullable = false) + private LocalTime startTime; + + @Column(name = "end_time", nullable = false) + private LocalTime endTime; + + @Column(nullable = false) + private boolean active = true; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "venue_id", nullable = false) + private Venue venue; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/slot/repository/SlotTemplateRepository.java b/server/src/main/java/com/bookmyvenue/server/slot/repository/SlotTemplateRepository.java new file mode 100644 index 000000000..33e02027c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/slot/repository/SlotTemplateRepository.java @@ -0,0 +1,16 @@ +package com.bookmyvenue.server.slot.repository; + +import com.bookmyvenue.server.slot.entity.SlotTemplate; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.DayOfWeek; +import java.util.List; + +public interface SlotTemplateRepository extends JpaRepository { +List findByVenueId(Long venueId); + +List findByVenueIdAndDayOfWeek( + Long venueId, + DayOfWeek dayOfWeek +); +} diff --git a/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateService.java b/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateService.java new file mode 100644 index 000000000..9fa63a8f6 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateService.java @@ -0,0 +1,22 @@ +package com.bookmyvenue.server.slot.service; + +import com.bookmyvenue.server.slot.dto.request.CreateSlotTemplateRequest; +import com.bookmyvenue.server.slot.dto.response.SlotTemplateResponse; + +import java.util.List; + +public interface SlotTemplateService { + + SlotTemplateResponse createTemplate( + Long venueId, + CreateSlotTemplateRequest request + ); + + List getVenueTemplates( + Long venueId + ); + + void deleteTemplate( + Long templateId + ); +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateServiceImpl.java new file mode 100644 index 000000000..f90496feb --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateServiceImpl.java @@ -0,0 +1,164 @@ +package com.bookmyvenue.server.slot.service; + +import com.bookmyvenue.server.auth.service.AuthenticatedUserService; +import com.bookmyvenue.server.common.exception.BusinessException; +import com.bookmyvenue.server.common.exception.ErrorCode; +import com.bookmyvenue.server.slot.dto.request.CreateSlotTemplateRequest; +import com.bookmyvenue.server.slot.dto.response.SlotTemplateResponse; +import com.bookmyvenue.server.slot.entity.SlotTemplate; +import com.bookmyvenue.server.slot.repository.SlotTemplateRepository; +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.venue.entity.Venue; +import com.bookmyvenue.server.venue.repository.VenueRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SlotTemplateServiceImpl + implements SlotTemplateService { + + private final SlotTemplateRepository slotTemplateRepository; + private final VenueRepository venueRepository; + private final AuthenticatedUserService authenticatedUserService; + + @Override + public SlotTemplateResponse createTemplate( + Long venueId, + CreateSlotTemplateRequest request + ) { + + log.info("Creating slot template for venueId={}", venueId); + + Venue venue = venueRepository.findById(venueId) + .orElseThrow(() -> { + log.warn("Venue not found. venueId={}", venueId); + return new BusinessException(ErrorCode.VENUE_NOT_FOUND); + }); + + User currentUser = + authenticatedUserService.getCurrentUser(); + + if (!venue.getOwner().getId().equals(currentUser.getId())) { + + log.warn( + "Unauthorized slot template creation attempt. venueId={}, userId={}", + venueId, + currentUser.getId() + ); + + throw new BusinessException(ErrorCode.ACCESS_DENIED); + } + + if (!request.endTime().isAfter(request.startTime())) { + + log.warn( + "Invalid time range. startTime={}, endTime={}", + request.startTime(), + request.endTime() + ); + + throw new BusinessException(ErrorCode.INVALID_TIME_RANGE); + } + + SlotTemplate slotTemplate = + SlotTemplate.builder() + .venue(venue) + .dayOfWeek(request.dayOfWeek()) + .startTime(request.startTime()) + .endTime(request.endTime()) + .active(true) + .build(); + + slotTemplateRepository.save(slotTemplate); + + log.info( + "Slot template created successfully. templateId={}, venueId={}", + slotTemplate.getId(), + venueId + ); + + return new SlotTemplateResponse( + slotTemplate.getId(), + slotTemplate.getDayOfWeek(), + slotTemplate.getStartTime(), + slotTemplate.getEndTime(), + slotTemplate.isActive() + ); + } + + @Override + public List getVenueTemplates( + Long venueId + ) { + + log.info( + "Fetching slot templates for venueId={}", + venueId + ); + + return slotTemplateRepository.findByVenueId(venueId) + .stream() + .map(template -> + new SlotTemplateResponse( + template.getId(), + template.getDayOfWeek(), + template.getStartTime(), + template.getEndTime(), + template.isActive() + ) + ) + .toList(); + } + + @Override + public void deleteTemplate(Long templateId) { + + log.info( + "Deleting slot template. templateId={}", + templateId + ); + + SlotTemplate template = + slotTemplateRepository.findById(templateId) + .orElseThrow(() -> { + log.warn( + "Slot template not found. templateId={}", + templateId + ); + return new BusinessException( + ErrorCode.SLOT_TEMPLATE_NOT_FOUND + ); + }); + + User currentUser = + authenticatedUserService.getCurrentUser(); + + if (!template.getVenue() + .getOwner() + .getId() + .equals(currentUser.getId())) { + + log.warn( + "Unauthorized slot template deletion attempt. templateId={}, userId={}", + templateId, + currentUser.getId() + ); + + throw new BusinessException( + ErrorCode.ACCESS_DENIED + ); + } + + slotTemplateRepository.delete(template); + + log.info( + "Slot template deleted successfully. templateId={}", + templateId + ); + } +} diff --git a/server/src/main/resources/db/migration/V1__init_schema.sql b/server/src/main/resources/db/migration/V1__init_schema.sql index bc8980d16..4cfb547b0 100644 --- a/server/src/main/resources/db/migration/V1__init_schema.sql +++ b/server/src/main/resources/db/migration/V1__init_schema.sql @@ -64,24 +64,25 @@ CREATE TABLE venue_image ( ); -CREATE TABLE slot ( - id BIGSERIAL PRIMARY KEY, +CREATE TABLE venue_availability_template ( + id BIGSERIAL PRIMARY KEY, - venue_id BIGINT NOT NULL, + venue_id BIGINT NOT NULL, - date DATE NOT NULL, + day_of_week VARCHAR(20) NOT NULL, - start_time TIME NOT NULL, - end_time TIME NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, - status VARCHAR(50) NOT NULL, + active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_slot_venue - FOREIGN KEY (venue_id) - REFERENCES venue(id) + CONSTRAINT fk_template_venue + FOREIGN KEY (venue_id) + REFERENCES venue(id) + ON DELETE CASCADE ); @@ -90,7 +91,11 @@ CREATE TABLE booking ( user_id UUID NOT NULL, venue_id BIGINT NOT NULL, - slot_id BIGINT NOT NULL, + + booking_date DATE NOT NULL, + + start_time TIME NOT NULL, + end_time TIME NOT NULL, status VARCHAR(50) NOT NULL, @@ -107,11 +112,7 @@ CREATE TABLE booking ( CONSTRAINT fk_booking_venue FOREIGN KEY (venue_id) - REFERENCES venue(id), - - CONSTRAINT fk_booking_slot - FOREIGN KEY (slot_id) - REFERENCES slot(id) + REFERENCES venue(id) ); From 1bf7ab42ed07257911e82e934866bc8104ec2c65 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 13 Jun 2026 10:47:18 +0530 Subject: [PATCH 28/59] fix: prevent overlap slot template creation --- .../server/auth/security/SecurityConfig.java | 2 + .../controller/SlotTemplateController.java | 2 + .../slot/service/SlotTemplateServiceImpl.java | 48 ++++++++++++++++++- .../venue/controller/VenueController.java | 19 ++++---- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java index 99b82c974..4f78e7893 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -17,6 +18,7 @@ import java.util.List; @Configuration +@EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { diff --git a/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java b/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java index d521017ef..eb82c98a1 100644 --- a/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java +++ b/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java @@ -7,11 +7,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController +@PreAuthorize("hasRole('VENDOR')") @RequestMapping("/api/v1/venues") @RequiredArgsConstructor public class SlotTemplateController { diff --git a/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateServiceImpl.java index f90496feb..02ce84d20 100644 --- a/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/slot/service/SlotTemplateServiceImpl.java @@ -61,10 +61,38 @@ public SlotTemplateResponse createTemplate( request.startTime(), request.endTime() ); - throw new BusinessException(ErrorCode.INVALID_TIME_RANGE); } + List existingTemplates = + slotTemplateRepository.findByVenueIdAndDayOfWeek( + venueId, + request.dayOfWeek() + ); + + // Ensure the new slot does not overlap with any existing slot + // for the same venue and day of the week. + boolean hasOverlap = existingTemplates.stream() + .anyMatch(template -> + request.startTime().isBefore(template.getEndTime()) + && request.endTime().isAfter(template.getStartTime()) + ); + + if (hasOverlap) { + + log.warn( + "Overlapping slot template detected. venueId={}, day={}, startTime={}, endTime={}", + venueId, + request.dayOfWeek(), + request.startTime(), + request.endTime() + ); + + throw new BusinessException( + ErrorCode.OVERLAPPING_SLOT_TEMPLATE + ); + } + SlotTemplate slotTemplate = SlotTemplate.builder() .venue(venue) @@ -100,7 +128,25 @@ public List getVenueTemplates( "Fetching slot templates for venueId={}", venueId ); + Venue venue = venueRepository.findById(venueId) + .orElseThrow(() -> + new BusinessException(ErrorCode.VENUE_NOT_FOUND)); + + User currentUser = authenticatedUserService.getCurrentUser(); + if (!venue.getOwner().getId().equals(currentUser.getId())) { + log.warn( + "Unauthorized slot template access. venueId={}, userId={}", + venueId, + currentUser.getId() + ); + throw new BusinessException(ErrorCode.ACCESS_DENIED); + } + + log.info( + "Successfully fetched slot templates for venueId={}", + venueId + ); return slotTemplateRepository.findByVenueId(venueId) .stream() .map(template -> diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java index e7750096c..0fbd035dd 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.UUID; @RestController @RequestMapping("/api/v1") @@ -20,6 +19,7 @@ public class VenueController { private final VenueService venueService; @PostMapping("/vendor/venues") + @PreAuthorize("hasRole('VENDOR')") @ResponseStatus(HttpStatus.CREATED) public VenueResponse createVenue( @RequestBody CreateVenueRequest request @@ -27,20 +27,21 @@ public VenueResponse createVenue( return venueService.createVenue(request); } - @GetMapping("vendor/venues") - @PreAuthorize("hasRole('USER')") + @GetMapping("/vendor/venues") + @PreAuthorize("hasRole('VENDOR')") @ResponseStatus(HttpStatus.OK) public List getAllVenues(){ return venueService.getAllVenues(); } - @GetMapping("vendor/venues/{venueId}") + @GetMapping("/vendor/venues/{venueId}") @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasRole('VENDOR')") public VenueResponse getVenue(@PathVariable Long venueId){ return venueService.getVenue(venueId); } - @GetMapping("venues") + @GetMapping("/venues") @ResponseStatus(HttpStatus.OK) public List getApprovedVenues( @RequestParam(required = false) String search, @@ -49,7 +50,7 @@ public List getApprovedVenues( return venueService.getApprovedVenues(search,district,categoryId); } - @GetMapping("venues/{id}") + @GetMapping("/venues/{id}") @ResponseStatus(HttpStatus.OK) public VenueResponse getApprovedVenue( @PathVariable Long id @@ -57,7 +58,8 @@ public VenueResponse getApprovedVenue( return venueService.getApprovedVenue(id); } - @PatchMapping("venues/{id}") + @PatchMapping("/venues/{id}") + @PreAuthorize("hasRole('VENDOR')") @ResponseStatus(HttpStatus.OK) public VenueResponse updateVenue( @PathVariable Long id, @@ -66,7 +68,8 @@ public VenueResponse updateVenue( return venueService.updateVenue(id, request); } - @DeleteMapping("venues/{id}") + @DeleteMapping("/venues/{id}") + @PreAuthorize("hasRole('VENDOR')") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteVenue( @PathVariable Long id From e747d804bffd9da543c38e056659d86ebe16873d Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 13 Jun 2026 12:11:42 +0530 Subject: [PATCH 29/59] feat(availability): implement booking availability api --- .../controller/AvailabilityController.java | 33 ++++++++++ .../booking/dto/AvailabilityResponse.java | 9 +++ .../booking/service/AvailabilityService.java | 14 ++++ .../service/AvailabilityServiceImpl.java | 64 +++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/dto/AvailabilityResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java diff --git a/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java b/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java new file mode 100644 index 000000000..b79eabc2b --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java @@ -0,0 +1,33 @@ +package com.bookmyvenue.server.booking.controller; + +import com.bookmyvenue.server.booking.dto.AvailabilityResponse; +import com.bookmyvenue.server.booking.service.AvailabilityService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/venues") +@RequiredArgsConstructor +public class AvailabilityController { + + private final AvailabilityService availabilityService; + + @GetMapping("/{venueId}/availability") + public ResponseEntity> + getAvailability( + @PathVariable Long venueId, + @RequestParam LocalDate date + ) { + + return ResponseEntity.ok( + availabilityService.getAvailability( + venueId, + date + ) + ); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/dto/AvailabilityResponse.java b/server/src/main/java/com/bookmyvenue/server/booking/dto/AvailabilityResponse.java new file mode 100644 index 000000000..687d73816 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/dto/AvailabilityResponse.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.server.booking.dto; + +import java.time.LocalTime; + +public record AvailabilityResponse( + LocalTime startTime, + LocalTime endTime +) { +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityService.java b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityService.java new file mode 100644 index 000000000..0ae32211b --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityService.java @@ -0,0 +1,14 @@ +package com.bookmyvenue.server.booking.service; + +import com.bookmyvenue.server.booking.dto.AvailabilityResponse; + +import java.time.LocalDate; +import java.util.List; + +public interface AvailabilityService { + + List getAvailability( + Long venueId, + LocalDate date + ); +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java new file mode 100644 index 000000000..76716f8c1 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java @@ -0,0 +1,64 @@ +package com.bookmyvenue.server.booking.service; + +import com.bookmyvenue.server.booking.dto.AvailabilityResponse; +import com.bookmyvenue.server.common.exception.BusinessException; +import com.bookmyvenue.server.common.exception.ErrorCode; +import com.bookmyvenue.server.slot.entity.SlotTemplate; +import com.bookmyvenue.server.slot.repository.SlotTemplateRepository; +import com.bookmyvenue.server.venue.entity.Venue; +import com.bookmyvenue.server.venue.repository.VenueRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AvailabilityServiceImpl + implements AvailabilityService { + + private final VenueRepository venueRepository; + private final SlotTemplateRepository slotTemplateRepository; + + @Override + public List getAvailability( + Long venueId, + LocalDate date + ) { + + log.info( + "Fetching availability for venueId={} date={}", + venueId, + date + ); + + Venue venue = venueRepository.findById(venueId) + .orElseThrow(() -> + new BusinessException( + ErrorCode.VENUE_NOT_FOUND + )); + + DayOfWeek dayOfWeek = date.getDayOfWeek(); + + List templates = + slotTemplateRepository + .findByVenueIdAndDayOfWeek( + venue.getId(), + dayOfWeek + ); + + return templates.stream() + .filter(SlotTemplate::isActive) + .map(template -> + new AvailabilityResponse( + template.getStartTime(), + template.getEndTime() + ) + ) + .toList(); + } +} \ No newline at end of file From 94e5021874db28ff39c6f08e370a9ee17cf89667 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 13 Jun 2026 14:53:13 +0530 Subject: [PATCH 30/59] feat(Booking): implement booking workflow with pending reservations --- .../controller/AvailabilityController.java | 2 +- .../booking/controller/BookingController.java | 53 +++++ .../booking/dto/request/BookingRequest.java | 9 + .../{ => response}/AvailabilityResponse.java | 2 +- .../booking/dto/response/BookingResponse.java | 18 ++ .../server/booking/entity/Booking.java | 47 +++++ .../server/booking/enums/BookingStatus.java | 9 + .../booking/repository/BookingRepository.java | 24 +++ .../booking/service/AvailabilityService.java | 2 +- .../service/AvailabilityServiceImpl.java | 2 +- .../booking/service/BookingService.java | 19 ++ .../booking/service/BookingServiceImpl.java | 193 ++++++++++++++++++ .../server/common/exception/ErrorCode.java | 12 +- .../db/migration/V3__edit_booking_table.sql | 20 ++ 14 files changed, 407 insertions(+), 5 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/controller/BookingController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/dto/request/BookingRequest.java rename server/src/main/java/com/bookmyvenue/server/booking/dto/{ => response}/AvailabilityResponse.java (70%) create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/dto/response/BookingResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/entity/Booking.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/enums/BookingStatus.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/service/BookingService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java create mode 100644 server/src/main/resources/db/migration/V3__edit_booking_table.sql diff --git a/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java b/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java index b79eabc2b..fc6ea708c 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java @@ -1,6 +1,6 @@ package com.bookmyvenue.server.booking.controller; -import com.bookmyvenue.server.booking.dto.AvailabilityResponse; +import com.bookmyvenue.server.booking.dto.response.AvailabilityResponse; import com.bookmyvenue.server.booking.service.AvailabilityService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; diff --git a/server/src/main/java/com/bookmyvenue/server/booking/controller/BookingController.java b/server/src/main/java/com/bookmyvenue/server/booking/controller/BookingController.java new file mode 100644 index 000000000..45e6d8fa5 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/controller/BookingController.java @@ -0,0 +1,53 @@ +package com.bookmyvenue.server.booking.controller; + +import com.bookmyvenue.server.booking.dto.request.BookingRequest; +import com.bookmyvenue.server.booking.dto.response.BookingResponse; +import com.bookmyvenue.server.booking.service.BookingService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class BookingController { + + private final BookingService bookingService; + + @PostMapping( + "/venues/{venueId}/slots/{slotTemplateId}/bookings" + ) + @ResponseStatus(HttpStatus.CREATED) + public BookingResponse createBooking( + @PathVariable Long venueId, + @PathVariable Long slotTemplateId, + @RequestBody BookingRequest request + ) { + + return bookingService.createBooking( + venueId, + slotTemplateId, + request + ); + } + + @GetMapping("/bookings/my-bookings") + @ResponseStatus(HttpStatus.OK) + public List getMyBookings() { + + return bookingService.getMyBookings(); + } + + @PostMapping("/bookings/{bookingId}/cancel") + @ResponseStatus(HttpStatus.OK) + public void cancelBooking( + @PathVariable Long bookingId + ) { + + bookingService.cancelBooking( + bookingId + ); + } +} diff --git a/server/src/main/java/com/bookmyvenue/server/booking/dto/request/BookingRequest.java b/server/src/main/java/com/bookmyvenue/server/booking/dto/request/BookingRequest.java new file mode 100644 index 000000000..c214e834c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/dto/request/BookingRequest.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.server.booking.dto.request; + +import java.time.LocalDate; + +public record BookingRequest( + LocalDate bookingDate +) { + +} diff --git a/server/src/main/java/com/bookmyvenue/server/booking/dto/AvailabilityResponse.java b/server/src/main/java/com/bookmyvenue/server/booking/dto/response/AvailabilityResponse.java similarity index 70% rename from server/src/main/java/com/bookmyvenue/server/booking/dto/AvailabilityResponse.java rename to server/src/main/java/com/bookmyvenue/server/booking/dto/response/AvailabilityResponse.java index 687d73816..26d62502f 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/dto/AvailabilityResponse.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/dto/response/AvailabilityResponse.java @@ -1,4 +1,4 @@ -package com.bookmyvenue.server.booking.dto; +package com.bookmyvenue.server.booking.dto.response; import java.time.LocalTime; diff --git a/server/src/main/java/com/bookmyvenue/server/booking/dto/response/BookingResponse.java b/server/src/main/java/com/bookmyvenue/server/booking/dto/response/BookingResponse.java new file mode 100644 index 000000000..21a126f90 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/dto/response/BookingResponse.java @@ -0,0 +1,18 @@ +package com.bookmyvenue.server.booking.dto.response; + +import com.bookmyvenue.server.booking.enums.BookingStatus; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +public record BookingResponse( + Long id, + Long venueId, + Long slotTemplateId, + LocalDate bookingDate, + BookingStatus status, + BigDecimal totalAmount, + LocalDateTime expiresAt +) { +} diff --git a/server/src/main/java/com/bookmyvenue/server/booking/entity/Booking.java b/server/src/main/java/com/bookmyvenue/server/booking/entity/Booking.java new file mode 100644 index 000000000..166202cd7 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/entity/Booking.java @@ -0,0 +1,47 @@ +package com.bookmyvenue.server.booking.entity; + +import com.bookmyvenue.server.booking.enums.BookingStatus; +import com.bookmyvenue.server.slot.entity.SlotTemplate; +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.venue.entity.Venue; +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Table(name = "booking") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Booking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private LocalDate bookingDate; + + @Enumerated(EnumType.STRING) + private BookingStatus status; + + private BigDecimal totalAmount; + + private LocalDateTime expiresAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "venue_id") + private Venue venue; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "slot_template_id") + private SlotTemplate slotTemplate; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/enums/BookingStatus.java b/server/src/main/java/com/bookmyvenue/server/booking/enums/BookingStatus.java new file mode 100644 index 000000000..16f3efd1c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/enums/BookingStatus.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.server.booking.enums; + +public enum BookingStatus { + + PENDING, // Reserved, waiting for advance payment + CONFIRMED, // Advance payment successful + CANCELLED, // Cancelled by user/admin + EXPIRED // Reservation timeout +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java b/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java new file mode 100644 index 000000000..b570192db --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java @@ -0,0 +1,24 @@ +package com.bookmyvenue.server.booking.repository; + +import com.bookmyvenue.server.booking.entity.Booking; +import com.bookmyvenue.server.booking.enums.BookingStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +@Repository +public interface BookingRepository + extends JpaRepository { + + boolean existsBySlotTemplateIdAndBookingDateAndStatusIn( + Long slotTemplateId, + LocalDate bookingDate, + List statuses + ); + + List findByUserId(UUID userId); + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityService.java b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityService.java index 0ae32211b..ccbf9fd86 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityService.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityService.java @@ -1,6 +1,6 @@ package com.bookmyvenue.server.booking.service; -import com.bookmyvenue.server.booking.dto.AvailabilityResponse; +import com.bookmyvenue.server.booking.dto.response.AvailabilityResponse; import java.time.LocalDate; import java.util.List; diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java index 76716f8c1..2628e0ea7 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java @@ -1,6 +1,6 @@ package com.bookmyvenue.server.booking.service; -import com.bookmyvenue.server.booking.dto.AvailabilityResponse; +import com.bookmyvenue.server.booking.dto.response.AvailabilityResponse; import com.bookmyvenue.server.common.exception.BusinessException; import com.bookmyvenue.server.common.exception.ErrorCode; import com.bookmyvenue.server.slot.entity.SlotTemplate; diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/BookingService.java b/server/src/main/java/com/bookmyvenue/server/booking/service/BookingService.java new file mode 100644 index 000000000..ec6e90ef2 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/BookingService.java @@ -0,0 +1,19 @@ +package com.bookmyvenue.server.booking.service; + +import com.bookmyvenue.server.booking.dto.request.BookingRequest; +import com.bookmyvenue.server.booking.dto.response.BookingResponse; + +import java.util.List; + +public interface BookingService { + + BookingResponse createBooking( + Long venueId, + Long slotTemplateId, + BookingRequest request + ); + + List getMyBookings(); + + void cancelBooking(Long bookingId); +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java new file mode 100644 index 000000000..bde3d36df --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java @@ -0,0 +1,193 @@ +package com.bookmyvenue.server.booking.service; + +import com.bookmyvenue.server.auth.service.AuthenticatedUserService; +import com.bookmyvenue.server.booking.dto.request.BookingRequest; +import com.bookmyvenue.server.booking.dto.response.BookingResponse; +import com.bookmyvenue.server.booking.entity.Booking; +import com.bookmyvenue.server.booking.enums.BookingStatus; +import com.bookmyvenue.server.booking.repository.BookingRepository; +import com.bookmyvenue.server.common.exception.BusinessException; +import com.bookmyvenue.server.common.exception.ErrorCode; +import com.bookmyvenue.server.slot.entity.SlotTemplate; +import com.bookmyvenue.server.slot.repository.SlotTemplateRepository; +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.venue.entity.Venue; +import com.bookmyvenue.server.venue.repository.VenueRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional +public class BookingServiceImpl implements BookingService { + + private final BookingRepository bookingRepository; + private final VenueRepository venueRepository; + private final SlotTemplateRepository slotTemplateRepository; + private final AuthenticatedUserService authenticatedUserService; + + @Override + public BookingResponse createBooking( + Long venueId, + Long slotTemplateId, + BookingRequest request + ) { + + log.info( + "Creating booking. venueId={}, slotTemplateId={}, date={}", + venueId, + slotTemplateId, + request.bookingDate() + ); + + User currentUser = + authenticatedUserService.getCurrentUser(); + + Venue venue = venueRepository.findById(venueId) + .orElseThrow(() -> + new BusinessException( + ErrorCode.VENUE_NOT_FOUND + )); + + SlotTemplate slotTemplate = + slotTemplateRepository.findById(slotTemplateId) + .orElseThrow(() -> + new BusinessException( + ErrorCode.SLOT_TEMPLATE_NOT_FOUND + )); + if (!slotTemplate.getVenue().getId().equals(venueId)) { + throw new BusinessException( + ErrorCode.ACCESS_DENIED + ); + } + if (!slotTemplate.isActive()) { + throw new BusinessException( + ErrorCode.SLOT_TEMPLATE_NOT_FOUND + ); + } + + DayOfWeek bookingDay = + request.bookingDate().getDayOfWeek(); + + if (!slotTemplate.getDayOfWeek().equals(bookingDay)) { + + throw new BusinessException( + ErrorCode.INVALID_BOOKING_DATE + ); + } + + + boolean alreadyBooked = + bookingRepository + .existsBySlotTemplateIdAndBookingDateAndStatusIn( + slotTemplateId, + request.bookingDate(), + List.of( + BookingStatus.PENDING, + BookingStatus.CONFIRMED + ) + ); + + if (alreadyBooked) { + + log.warn( + "Slot already booked. slotTemplateId={}, date={}", + slotTemplateId, + request.bookingDate() + ); + + throw new BusinessException( + ErrorCode.SLOT_ALREADY_BOOKED + ); + } + + Booking booking = Booking.builder() + .user(currentUser) + .venue(venue) + .slotTemplate(slotTemplate) + .bookingDate(request.bookingDate()) + .status(BookingStatus.PENDING) + .totalAmount(BigDecimal.ZERO) + .expiresAt( + LocalDateTime.now().plusMinutes(10) + ) + .build(); + + bookingRepository.save(booking); + + log.info( + "Booking created successfully. bookingId={}", + booking.getId() + ); + + return mapToResponse(booking); + } + + @Override + public List getMyBookings() { + + User currentUser = + authenticatedUserService.getCurrentUser(); + + return bookingRepository + .findByUserId(currentUser.getId()) + .stream() + .map(this::mapToResponse) + .toList(); + } + + @Override + public void cancelBooking(Long bookingId) { + + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> + new BusinessException( + ErrorCode.BOOKING_NOT_FOUND + )); + + User currentUser = + authenticatedUserService.getCurrentUser(); + + if (!booking.getUser().getId() + .equals(currentUser.getId())) { + + throw new BusinessException( + ErrorCode.ACCESS_DENIED + ); + } + + booking.setStatus( + BookingStatus.CANCELLED + ); + + bookingRepository.save(booking); + + log.info( + "Booking cancelled. bookingId={}", + bookingId + ); + } + + private BookingResponse mapToResponse( + Booking booking + ) { + + return new BookingResponse( + booking.getId(), + booking.getVenue().getId(), + booking.getSlotTemplate().getId(), + booking.getBookingDate(), + booking.getStatus(), + booking.getTotalAmount(), + booking.getExpiresAt() + ); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java index e91deceee..98066263b 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java @@ -86,7 +86,17 @@ public enum ErrorCode { OVERLAPPING_SLOT_TEMPLATE(HttpStatus.BAD_REQUEST, "OVERLAPPING_SLOT_TEMPLATE", - "Slot template overlaps with existing schedule"); + "Slot template overlaps with existing schedule"), + SLOT_ALREADY_BOOKED( + HttpStatus.CONFLICT, + "SLOT_ALREADY_BOOKED", + "Selected slot is already booked" + ), + INVALID_BOOKING_DATE( + HttpStatus.BAD_REQUEST, + "INVALID_BOOKING_DATE", + "Selected date does not match the slot template schedule" + ); private final HttpStatus status; diff --git a/server/src/main/resources/db/migration/V3__edit_booking_table.sql b/server/src/main/resources/db/migration/V3__edit_booking_table.sql new file mode 100644 index 000000000..84e1f8dc2 --- /dev/null +++ b/server/src/main/resources/db/migration/V3__edit_booking_table.sql @@ -0,0 +1,20 @@ +ALTER TABLE booking +DROP COLUMN start_time; + +ALTER TABLE booking +DROP COLUMN end_time; + +ALTER TABLE booking + ADD COLUMN slot_template_id BIGINT NOT NULL; + +ALTER TABLE booking + ADD CONSTRAINT fk_booking_slot_template + FOREIGN KEY (slot_template_id) + REFERENCES venue_availability_template(id); + +ALTER TABLE booking + ADD CONSTRAINT uk_booking_slot + UNIQUE ( + booking_date, + slot_template_id + ); \ No newline at end of file From 321fc2964018713f3bb06d42439dcba8a460c2ab Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 13 Jun 2026 15:38:02 +0530 Subject: [PATCH 31/59] feat: filter booked slots from availability --- .../booking/repository/BookingRepository.java | 6 +++ .../service/AvailabilityServiceImpl.java | 41 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java b/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java index b570192db..181d90594 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java @@ -21,4 +21,10 @@ boolean existsBySlotTemplateIdAndBookingDateAndStatusIn( List findByUserId(UUID userId); + List findByVenueIdAndBookingDateAndStatusIn( + Long venueId, + LocalDate bookingDate, + List statuses + ); + } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java index 2628e0ea7..3de03f957 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java @@ -1,6 +1,9 @@ package com.bookmyvenue.server.booking.service; import com.bookmyvenue.server.booking.dto.response.AvailabilityResponse; +import com.bookmyvenue.server.booking.entity.Booking; +import com.bookmyvenue.server.booking.enums.BookingStatus; +import com.bookmyvenue.server.booking.repository.BookingRepository; import com.bookmyvenue.server.common.exception.BusinessException; import com.bookmyvenue.server.common.exception.ErrorCode; import com.bookmyvenue.server.slot.entity.SlotTemplate; @@ -14,6 +17,8 @@ import java.time.DayOfWeek; import java.time.LocalDate; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -23,6 +28,7 @@ public class AvailabilityServiceImpl private final VenueRepository venueRepository; private final SlotTemplateRepository slotTemplateRepository; + private final BookingRepository bookingRepository; @Override public List getAvailability( @@ -44,6 +50,7 @@ public List getAvailability( DayOfWeek dayOfWeek = date.getDayOfWeek(); + // Get all configured slots for the selected day List templates = slotTemplateRepository .findByVenueIdAndDayOfWeek( @@ -51,8 +58,40 @@ public List getAvailability( dayOfWeek ); + // Active bookings block slot availability + List activeBookings = + bookingRepository + .findByVenueIdAndBookingDateAndStatusIn( + venueId, + date, + List.of( + BookingStatus.PENDING, + BookingStatus.CONFIRMED + ) + ); + + // Collect booked slot template ids for filtering + Set bookedTemplateIds = + activeBookings.stream() + .map(booking -> + booking.getSlotTemplate().getId() + ) + .collect(Collectors.toSet()); + + log.info( + "Found {} configured slots and {} active bookings for venueId={} date={}", + templates.size(), + activeBookings.size(), + venueId, + date + ); + return templates.stream() - .filter(SlotTemplate::isActive) + .filter(template -> + !bookedTemplateIds.contains( + template.getId() + ) + ) .map(template -> new AvailabilityResponse( template.getStartTime(), From 36ba1791f2e83f83f0214c5f1dbf54800c63c421 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 13 Jun 2026 19:04:15 +0530 Subject: [PATCH 32/59] feat: add scheduler to expired the pending bookings --- .../bookmyvenue/server/ServerApplication.java | 2 + .../booking/repository/BookingRepository.java | 6 +++ .../scheduler/BookingExpiryScheduler.java | 49 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 server/src/main/java/com/bookmyvenue/server/booking/scheduler/BookingExpiryScheduler.java diff --git a/server/src/main/java/com/bookmyvenue/server/ServerApplication.java b/server/src/main/java/com/bookmyvenue/server/ServerApplication.java index 3dbd46e1d..212da88f1 100644 --- a/server/src/main/java/com/bookmyvenue/server/ServerApplication.java +++ b/server/src/main/java/com/bookmyvenue/server/ServerApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class ServerApplication { diff --git a/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java b/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java index 181d90594..e94387d2b 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/repository/BookingRepository.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Repository; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -27,4 +28,9 @@ List findByVenueIdAndBookingDateAndStatusIn( List statuses ); + List findByStatusAndExpiresAtBefore( + BookingStatus status, + LocalDateTime time + ); + } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/booking/scheduler/BookingExpiryScheduler.java b/server/src/main/java/com/bookmyvenue/server/booking/scheduler/BookingExpiryScheduler.java new file mode 100644 index 000000000..13ff93637 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/booking/scheduler/BookingExpiryScheduler.java @@ -0,0 +1,49 @@ +package com.bookmyvenue.server.booking.scheduler; + +import com.bookmyvenue.server.booking.entity.Booking; +import com.bookmyvenue.server.booking.enums.BookingStatus; +import com.bookmyvenue.server.booking.repository.BookingRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class BookingExpiryScheduler { + + private final BookingRepository bookingRepository; + + @Scheduled(fixedRate = 60000) + @Transactional + public void expireBookings() { + + List expiredBookings = + bookingRepository + .findByStatusAndExpiresAtBefore( + BookingStatus.PENDING, + LocalDateTime.now() + ); + + expiredBookings.forEach( + booking -> + booking.setStatus( + BookingStatus.EXPIRED + ) + ); + + bookingRepository.saveAll(expiredBookings); + + if (!expiredBookings.isEmpty()) { + log.info( + "Expired {} bookings", + expiredBookings.size() + ); + } + } +} \ No newline at end of file From b344f0c05db577725329ff76c2af34ff1c4659ac Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 13 Jun 2026 19:25:34 +0530 Subject: [PATCH 33/59] docs: add swagger operation summaries --- .../booking/controller/AvailabilityController.java | 4 ++++ .../server/booking/controller/BookingController.java | 6 ++++++ .../server/slot/controller/SlotTemplateController.java | 7 +++++++ .../server/venue/controller/VenueController.java | 9 +++++++++ 4 files changed, 26 insertions(+) diff --git a/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java b/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java index fc6ea708c..ef2bc3abc 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java @@ -2,6 +2,8 @@ import com.bookmyvenue.server.booking.dto.response.AvailabilityResponse; import com.bookmyvenue.server.booking.service.AvailabilityService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -10,6 +12,7 @@ import java.util.List; @RestController +@Tag(name = "Availability") @RequestMapping("/api/v1/venues") @RequiredArgsConstructor public class AvailabilityController { @@ -17,6 +20,7 @@ public class AvailabilityController { private final AvailabilityService availabilityService; @GetMapping("/{venueId}/availability") + @Operation(summary = "Get Venue Availability") public ResponseEntity> getAvailability( @PathVariable Long venueId, diff --git a/server/src/main/java/com/bookmyvenue/server/booking/controller/BookingController.java b/server/src/main/java/com/bookmyvenue/server/booking/controller/BookingController.java index 45e6d8fa5..86561b6cb 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/controller/BookingController.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/controller/BookingController.java @@ -3,6 +3,8 @@ import com.bookmyvenue.server.booking.dto.request.BookingRequest; import com.bookmyvenue.server.booking.dto.response.BookingResponse; import com.bookmyvenue.server.booking.service.BookingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -10,6 +12,7 @@ import java.util.List; @RestController +@Tag(name = "Booking Management") @RequestMapping("/api/v1") @RequiredArgsConstructor public class BookingController { @@ -19,6 +22,7 @@ public class BookingController { @PostMapping( "/venues/{venueId}/slots/{slotTemplateId}/bookings" ) + @Operation(summary = "Create Booking") @ResponseStatus(HttpStatus.CREATED) public BookingResponse createBooking( @PathVariable Long venueId, @@ -34,6 +38,7 @@ public BookingResponse createBooking( } @GetMapping("/bookings/my-bookings") + @Operation(summary = "Get My Bookings") @ResponseStatus(HttpStatus.OK) public List getMyBookings() { @@ -41,6 +46,7 @@ public List getMyBookings() { } @PostMapping("/bookings/{bookingId}/cancel") + @Operation(summary = "Cancel Booking") @ResponseStatus(HttpStatus.OK) public void cancelBooking( @PathVariable Long bookingId diff --git a/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java b/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java index eb82c98a1..7fec8ff1b 100644 --- a/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java +++ b/server/src/main/java/com/bookmyvenue/server/slot/controller/SlotTemplateController.java @@ -3,6 +3,8 @@ import com.bookmyvenue.server.slot.dto.request.CreateSlotTemplateRequest; import com.bookmyvenue.server.slot.dto.response.SlotTemplateResponse; import com.bookmyvenue.server.slot.service.SlotTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -13,6 +15,7 @@ import java.util.List; @RestController +@Tag(name = "Slot Management") @PreAuthorize("hasRole('VENDOR')") @RequestMapping("/api/v1/venues") @RequiredArgsConstructor @@ -24,6 +27,7 @@ public class SlotTemplateController { * Creates a new slot template for a venue. */ @PostMapping("/{venueId}/slots") + @Operation(summary = "Create Slot Template") public ResponseEntity createTemplate( @PathVariable Long venueId, @Valid @RequestBody CreateSlotTemplateRequest request @@ -44,6 +48,7 @@ public ResponseEntity createTemplate( * Returns all slot templates configured for a venue. */ @GetMapping("/{venueId}/slots") + @Operation(summary = "Get Slot Templates") public ResponseEntity> getVenueTemplates( @PathVariable Long venueId ) { @@ -58,7 +63,9 @@ public ResponseEntity> getVenueTemplates( /** * Deletes a slot template. */ + @DeleteMapping("/slots/{templateId}") + @Operation(summary = "Delete Slot Template") public ResponseEntity deleteTemplate( @PathVariable Long templateId ) { diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java index 0fbd035dd..b4d7ef237 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -4,6 +4,8 @@ import com.bookmyvenue.server.venue.dto.request.UpdateVenueRequest; import com.bookmyvenue.server.venue.dto.response.VenueResponse; import com.bookmyvenue.server.venue.service.VenueService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -12,6 +14,7 @@ import java.util.List; @RestController +@Tag(name = "Venue Management") @RequestMapping("/api/v1") @RequiredArgsConstructor public class VenueController { @@ -19,6 +22,7 @@ public class VenueController { private final VenueService venueService; @PostMapping("/vendor/venues") + @Operation(summary = "Create Venue") @PreAuthorize("hasRole('VENDOR')") @ResponseStatus(HttpStatus.CREATED) public VenueResponse createVenue( @@ -29,12 +33,14 @@ public VenueResponse createVenue( @GetMapping("/vendor/venues") @PreAuthorize("hasRole('VENDOR')") + @Operation(summary = "Get Vendor Venues") @ResponseStatus(HttpStatus.OK) public List getAllVenues(){ return venueService.getAllVenues(); } @GetMapping("/vendor/venues/{venueId}") + @Operation(summary = "Get Vendor Venue") @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasRole('VENDOR')") public VenueResponse getVenue(@PathVariable Long venueId){ @@ -51,6 +57,7 @@ public List getApprovedVenues( } @GetMapping("/venues/{id}") + @Operation(summary = "Browse Venues") @ResponseStatus(HttpStatus.OK) public VenueResponse getApprovedVenue( @PathVariable Long id @@ -59,6 +66,7 @@ public VenueResponse getApprovedVenue( } @PatchMapping("/venues/{id}") + @Operation(summary = "Update Venue") @PreAuthorize("hasRole('VENDOR')") @ResponseStatus(HttpStatus.OK) public VenueResponse updateVenue( @@ -69,6 +77,7 @@ public VenueResponse updateVenue( } @DeleteMapping("/venues/{id}") + @Operation(summary = "Delete Venue") @PreAuthorize("hasRole('VENDOR')") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteVenue( From 78db7c34ac150b7125c1ec278eba0b4fc4bf3746 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 13 Jun 2026 20:46:39 +0530 Subject: [PATCH 34/59] refactor(Booking): add partial unique index --- .../server/booking/controller/AvailabilityController.java | 1 - .../db/migration/V4__add_unique_index_for_booking.sql | 3 +++ .../db/migration/V5__delete_unique_constraint_in_booking.sql | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 server/src/main/resources/db/migration/V4__add_unique_index_for_booking.sql create mode 100644 server/src/main/resources/db/migration/V5__delete_unique_constraint_in_booking.sql diff --git a/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java b/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java index ef2bc3abc..e71a7cf68 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/controller/AvailabilityController.java @@ -18,7 +18,6 @@ public class AvailabilityController { private final AvailabilityService availabilityService; - @GetMapping("/{venueId}/availability") @Operation(summary = "Get Venue Availability") public ResponseEntity> diff --git a/server/src/main/resources/db/migration/V4__add_unique_index_for_booking.sql b/server/src/main/resources/db/migration/V4__add_unique_index_for_booking.sql new file mode 100644 index 000000000..8a71fc13f --- /dev/null +++ b/server/src/main/resources/db/migration/V4__add_unique_index_for_booking.sql @@ -0,0 +1,3 @@ +CREATE UNIQUE INDEX uk_booking_active_slot + ON booking(booking_date, slot_template_id) + WHERE status IN ('PENDING', 'CONFIRMED'); \ No newline at end of file diff --git a/server/src/main/resources/db/migration/V5__delete_unique_constraint_in_booking.sql b/server/src/main/resources/db/migration/V5__delete_unique_constraint_in_booking.sql new file mode 100644 index 000000000..71165ba0f --- /dev/null +++ b/server/src/main/resources/db/migration/V5__delete_unique_constraint_in_booking.sql @@ -0,0 +1,2 @@ +ALTER TABLE booking +DROP CONSTRAINT uk_booking_slot; \ No newline at end of file From c21cc51f9dd607b9a299870acd9eee51db6e8f2b Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sat, 13 Jun 2026 23:45:23 +0530 Subject: [PATCH 35/59] test: add concurrency test for booking --- .../server/BookingConcurrencyTest.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 server/src/test/java/com/bookmyvenue/server/BookingConcurrencyTest.java diff --git a/server/src/test/java/com/bookmyvenue/server/BookingConcurrencyTest.java b/server/src/test/java/com/bookmyvenue/server/BookingConcurrencyTest.java new file mode 100644 index 000000000..f748b4d48 --- /dev/null +++ b/server/src/test/java/com/bookmyvenue/server/BookingConcurrencyTest.java @@ -0,0 +1,139 @@ +package com.bookmyvenue.server; + +import com.bookmyvenue.server.booking.entity.Booking; +import com.bookmyvenue.server.booking.enums.BookingStatus; +import com.bookmyvenue.server.booking.repository.BookingRepository; +import com.bookmyvenue.server.slot.entity.SlotTemplate; +import com.bookmyvenue.server.slot.repository.SlotTemplateRepository; +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.user.repository.UserRepository; +import com.bookmyvenue.server.venue.entity.Venue; +import com.bookmyvenue.server.venue.repository.VenueRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@Transactional(propagation = Propagation.NOT_SUPPORTED) +class BookingConcurrencyTest { + + @Autowired + private BookingRepository bookingRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private VenueRepository venueRepository; + + @Autowired + private SlotTemplateRepository slotTemplateRepository; + + @Test + void shouldPreventDoubleBooking() throws Exception { + + Venue venue = + venueRepository.findById(1L) + .orElseThrow(); + + SlotTemplate slotTemplate = + slotTemplateRepository.findById(1L) + .orElseThrow(); + + User user = + userRepository.findAll() + .getFirst(); + + LocalDate bookingDate = + LocalDate.of(2026, 6, 23); + + int threadCount = 100; + + ExecutorService executorService = + Executors.newFixedThreadPool(threadCount); + + CountDownLatch startLatch = + new CountDownLatch(1); + + CountDownLatch finishLatch = + new CountDownLatch(threadCount); + + AtomicInteger successCount = + new AtomicInteger(); + + AtomicInteger failureCount = + new AtomicInteger(); + + for (int i = 0; i < threadCount; i++) { + + executorService.submit(() -> { + + try { + + startLatch.await(); + + Booking booking = Booking.builder() + .user(user) + .venue(venue) + .slotTemplate(slotTemplate) + .bookingDate(bookingDate) + .status(BookingStatus.PENDING) + .totalAmount(BigDecimal.ZERO) + .expiresAt( + LocalDateTime.now() + .plusMinutes(10) + ) + .build(); + + bookingRepository.saveAndFlush( + booking + ); + + successCount.incrementAndGet(); + + } catch (Exception e) { + + failureCount.incrementAndGet(); + + } finally { + + finishLatch.countDown(); + + } + }); + } + + startLatch.countDown(); + + finishLatch.await(); + + executorService.shutdown(); + + System.out.println( + "Success Count = " + + successCount.get() + ); + + System.out.println( + "Failure Count = " + + failureCount.get() + ); + + assertEquals( + 1, + successCount.get() + ); + } +} \ No newline at end of file From 188c0e3ac11c036c414a86a71fd99968f7d9f19a Mon Sep 17 00:00:00 2001 From: Anzyll Date: Sun, 14 Jun 2026 00:00:37 +0530 Subject: [PATCH 36/59] refactor: price added in booking creation --- .../bookmyvenue/server/booking/service/BookingServiceImpl.java | 2 +- .../java/com/bookmyvenue/server/BookingConcurrencyTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java index bde3d36df..855385353 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java @@ -115,7 +115,7 @@ public BookingResponse createBooking( .slotTemplate(slotTemplate) .bookingDate(request.bookingDate()) .status(BookingStatus.PENDING) - .totalAmount(BigDecimal.ZERO) + .totalAmount(venue.getPricePerSlot()) .expiresAt( LocalDateTime.now().plusMinutes(10) ) diff --git a/server/src/test/java/com/bookmyvenue/server/BookingConcurrencyTest.java b/server/src/test/java/com/bookmyvenue/server/BookingConcurrencyTest.java index f748b4d48..e59153f43 100644 --- a/server/src/test/java/com/bookmyvenue/server/BookingConcurrencyTest.java +++ b/server/src/test/java/com/bookmyvenue/server/BookingConcurrencyTest.java @@ -90,7 +90,7 @@ void shouldPreventDoubleBooking() throws Exception { .slotTemplate(slotTemplate) .bookingDate(bookingDate) .status(BookingStatus.PENDING) - .totalAmount(BigDecimal.ZERO) + .totalAmount(venue.getPricePerSlot()) .expiresAt( LocalDateTime.now() .plusMinutes(10) From ad43eed7595d9ed0aef4244391ab145e209b4561 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Sun, 14 Jun 2026 19:30:48 +0530 Subject: [PATCH 37/59] feat:updated Ui theme and added create and edit venue options --- .../admin/features/login/login.component.html | 77 +++- .../features/venues/venues.component.html | 6 +- .../admin/repositories/venue.repository.ts | 2 +- client/src/app/core/config/environment.ts | 4 + client/src/app/landing/landing.component.html | 391 ++++++++++-------- client/src/app/landing/landing.component.ts | 56 ++- .../components/button/button.component.ts | 6 +- .../components/input/input.component.html | 2 +- .../constants/api-endpoints.constant.ts | 14 +- client/src/app/shared/enums/user-role.enum.ts | 6 +- .../src/app/shared/enums/venue-status.enum.ts | 8 +- .../shared/interceptors/error.interceptor.ts | 44 +- client/src/app/shared/models/venue.model.ts | 50 +-- .../src/app/shared/services/auth.service.ts | 31 +- .../app/shared/services/cloudinary.service.ts | 19 + .../features/checkout/checkout.component.html | 377 +++++++++++++---- .../features/checkout/checkout.component.ts | 137 ++++-- .../user/features/login/login.component.html | 108 ++++- .../user/features/login/login.component.ts | 6 + .../features/signup/signup.component.html | 145 +++++-- .../user/features/signup/signup.component.ts | 2 + .../venue-details.component.html | 294 ++++++++++--- .../venue-details/venue-details.component.ts | 99 ++++- .../venue-list/venue-list.component.html | 154 ++++--- .../venue-list/venue-list.component.ts | 40 +- .../user-layout/user-layout.component.html | 173 +++++--- .../user-layout/user-layout.component.ts | 6 +- .../app/user/repositories/venue.repository.ts | 21 +- client/src/app/user/services/venue.service.ts | 12 +- .../create-venue/create-venue.component.html | 265 ++++++++++-- .../create-venue/create-venue.component.ts | 119 +++++- .../dashboard/dashboard.component.html | 212 ++++++---- .../features/dashboard/dashboard.component.ts | 41 +- .../edit-venue/edit-venue.component.html | 300 ++++++++++++-- .../edit-venue/edit-venue.component.ts | 142 +++++-- .../venue-list/venue-list.component.html | 15 +- .../venue-list/venue-list.component.ts | 17 +- .../vendor-layout.component.html | 118 ++++-- .../vendor-layout/vendor-layout.component.ts | 3 +- .../vendor/repositories/venue.repository.ts | 21 +- .../src/app/vendor/services/venue.service.ts | 18 +- client/src/index.html | 3 + client/src/styles.css | 187 +++++++-- 43 files changed, 2837 insertions(+), 914 deletions(-) create mode 100644 client/src/app/shared/services/cloudinary.service.ts diff --git a/client/src/app/admin/features/login/login.component.html b/client/src/app/admin/features/login/login.component.html index 6c37dd312..610fab88a 100644 --- a/client/src/app/admin/features/login/login.component.html +++ b/client/src/app/admin/features/login/login.component.html @@ -1,22 +1,71 @@ -
-
+
+ + +
+ + +
+ + +
+ +
-

Admin Login

-

Access the admin control panel

+
+ + + +
+

BookMyVenue

+

Admin Control Panel

-
-
-
- - + + +
+ + +
+ +
-
- Forgot password? + +
+ +
- + + + +
+ + +

+ © 2026 BookMyVenue · All activity is logged and monitored +

diff --git a/client/src/app/admin/features/venues/venues.component.html b/client/src/app/admin/features/venues/venues.component.html index a57af32ce..794442737 100644 --- a/client/src/app/admin/features/venues/venues.component.html +++ b/client/src/app/admin/features/venues/venues.component.html @@ -6,15 +6,15 @@

Venues Management

- + @for (venue of venues(); track venue.id) { - + - + diff --git a/client/src/app/admin/repositories/venue.repository.ts b/client/src/app/admin/repositories/venue.repository.ts index a0536af75..ad3e2b74c 100644 --- a/client/src/app/admin/repositories/venue.repository.ts +++ b/client/src/app/admin/repositories/venue.repository.ts @@ -17,6 +17,6 @@ export class AdminVenueRepository { } toggleVenueStatus(id: string, status: string): Observable> { - return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.VENUES.BY_ID(id)}`, { status }); + return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.VENUES.PUBLIC_BY_ID(id)}`, { status }); } } diff --git a/client/src/app/core/config/environment.ts b/client/src/app/core/config/environment.ts index 728eb3bfc..ee79ab393 100644 --- a/client/src/app/core/config/environment.ts +++ b/client/src/app/core/config/environment.ts @@ -3,4 +3,8 @@ export const environment = { apiUrl: 'http://localhost:8080/api', tokenKey: 'bmv_token', refreshTokenKey: 'bmv_refresh_token', + cloudinary:{ + cloudName:'dbigsiz1p', + uploadPreset:'book_my_venue', + }, }; diff --git a/client/src/app/landing/landing.component.html b/client/src/app/landing/landing.component.html index 5e498becf..74cd137b8 100644 --- a/client/src/app/landing/landing.component.html +++ b/client/src/app/landing/landing.component.html @@ -1,242 +1,287 @@ -
NameCityCapacityPrice/HrStatusActions
NameDistrictCapacityPrice/SlotStatusActions
{{ venue.name }}{{ venue.city }}{{ venue.district }} {{ venue.capacity }}{{ venue.pricePerHour }}{{ venue.pricePerSlot }} {{ venue.status }}
- - - - - @for (booking of bookings(); track booking.id) { - - - - - - - - - } - -
VenueDateTimeGuestsAmountStatus
{{ booking.venueName }}{{ booking.eventDate | dateFormat }}{{ booking.startTime }} - {{ booking.endTime }}{{ booking.guestCount }}{{ booking.totalAmount | currencyFormat }}{{ booking.status }}
-
- +
+ + + + + + + + + + + + + @for (booking of bookings(); track booking.id) { + + + + + + + + + } + +
Venue IDDateSlot IDAmountStatusActions
{{ booking.venueId }}{{ booking.bookingDate | dateFormat }}{{ booking.slotTemplateId }}{{ booking.totalAmount | currencyFormat }} + {{ booking.status }} + + + +
+
} -
+
\ No newline at end of file diff --git a/client/src/app/admin/features/bookings/bookings.component.ts b/client/src/app/admin/features/bookings/bookings.component.ts index bb26b75bf..ca623d175 100644 --- a/client/src/app/admin/features/bookings/bookings.component.ts +++ b/client/src/app/admin/features/bookings/bookings.component.ts @@ -1,21 +1,38 @@ -import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; +import { BookingService } from '../../../vendor/services/booking.service'; import { LoaderComponent } from '../../../shared/components/loader/loader.component'; import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; -import { PaginationComponent } from '../../../shared/components/pagination/pagination.component'; import { DateFormatPipe } from '../../../shared/pipes/date-format.pipe'; import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; import { Booking } from '../../../shared/models/booking.model'; @Component({ selector: 'app-admin-bookings', standalone: true, - imports: [LoaderComponent, EmptyStateComponent, PaginationComponent, DateFormatPipe, CurrencyFormatPipe], + imports: [LoaderComponent, EmptyStateComponent, DateFormatPipe, CurrencyFormatPipe], templateUrl: './bookings.component.html', styleUrl: './bookings.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class BookingsComponent { - readonly bookings = signal([]); - readonly loading = signal(false); +export class BookingsComponent implements OnInit { + private readonly bookingService = inject(BookingService); + + readonly bookings = this.bookingService.bookings; + readonly loading = this.bookingService.loading; readonly currentPage = signal(1); readonly totalPages = signal(1); - onPageChange(page: number): void { this.currentPage.set(page); } -} + + ngOnInit(): void { + this.bookingService.loadVendorBookings(); + } + + onPageChange(page: number): void { + this.currentPage.set(page); + } + + onApprove(id: number): void { + this.bookingService.updateBookingStatus(id, 'confirmed'); + } + + onReject(id: number): void { + this.bookingService.updateBookingStatus(id, 'rejected'); + } +} \ No newline at end of file diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index a2583ce5d..c694b7dbe 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -1,12 +1,10 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; -import { provideHttpClient, withInterceptors, withXsrfConfiguration, HttpInterceptorFn } from '@angular/common/http'; +import { provideHttpClient, withInterceptors, withXsrfConfiguration } from '@angular/common/http'; import { routes } from './core/app.routes'; import { appProviders } from './core/providers/app-providers'; import { errorInterceptor } from './shared/interceptors/error.interceptor'; -const credentialsInterceptor: HttpInterceptorFn = (req, next) => - next(req.clone({withCredentials:true})); export const appConfig: ApplicationConfig = { providers: [ @@ -14,7 +12,6 @@ export const appConfig: ApplicationConfig = { provideRouter(routes, withComponentInputBinding()), provideHttpClient( withInterceptors([ - credentialsInterceptor, errorInterceptor, ]), withXsrfConfiguration({ diff --git a/client/src/app/shared/constants/api-endpoints.constant.ts b/client/src/app/shared/constants/api-endpoints.constant.ts index d1d7be98d..eba4a6431 100644 --- a/client/src/app/shared/constants/api-endpoints.constant.ts +++ b/client/src/app/shared/constants/api-endpoints.constant.ts @@ -23,7 +23,7 @@ export const API_ENDPOINTS = { // Public endpoints (no auth required) PUBLIC_BASE:'/v1/venues', PUBLIC_BY_ID:(id:string) => `/v1/venues/${id}`, - SLOTS: (venueId: string) => `/v1/venues/${venueId}/slots`, + AVAILABILITY: (venueId: string) => `/v1/venues/${venueId}/availability`, // Vendor endpoints (require VENDOR role) diff --git a/client/src/app/shared/models/booking.model.ts b/client/src/app/shared/models/booking.model.ts index 2e3dc8a82..2f004e643 100644 --- a/client/src/app/shared/models/booking.model.ts +++ b/client/src/app/shared/models/booking.model.ts @@ -1,25 +1,19 @@ import { BookingStatus } from '../enums/booking-status.enum'; export interface Booking { - id: string; - userId: string; - venueId: string; - venueName: string; - eventDate: string; - startTime: string; - endTime: string; - guestCount: number; - totalAmount: number; + id: number; + venueId: number; + slotTemplateId: number; + bookingDate: string; status: BookingStatus; - notes: string; - createdAt: string; - updatedAt: string; + totalAmount: number; + expiresAt: string; } export interface CreateBookingRequest { venueId: string; slotTemplateId: number; - eventDate: string; + bookingDate: string; startTime: string; endTime: string; guestCount: number; diff --git a/client/src/app/user/features/checkout/checkout.component.ts b/client/src/app/user/features/checkout/checkout.component.ts index 0bd126ddc..9b4a9620e 100644 --- a/client/src/app/user/features/checkout/checkout.component.ts +++ b/client/src/app/user/features/checkout/checkout.component.ts @@ -101,23 +101,31 @@ export class CheckoutComponent implements OnInit { ngOnInit(): void { const venueId = this.route.snapshot.paramMap.get('venueId') ?? ''; - // ✅ Switch between dummy and real data HERE only - const USE_DUMMY_DATA = false; // Set to false when backend ready - - if (USE_DUMMY_DATA) { - this.slots.set(this.slotService.getDummySlots()); - } else { - this.slotService.loadSlotsForVenue(venueId).subscribe({ - next: (response) => this.slots.set(response.data), - error: () => console.error('Failed to load slots'), - }); - } - - // SWITCH: use dummyVenues for design, venueService for real data - // this.venue.set(this.dummyVenues[venueId] ?? null); - // this.loading.set(false); + // Watch for date changes to fetch available slots + this.form.controls.eventDate.valueChanges.subscribe({ + next: (date) => { + this.selectedSlotId.set(null); + if (!date || this.form.controls.eventDate.invalid) { + this.slots.set([]); + return; + } + + const USE_DUMMY_DATA = false; // Set to false when backend ready + + if (USE_DUMMY_DATA) { + this.slots.set(this.slotService.getDummySlots()); + } else { + this.slotService.loadSlotsForVenue(venueId, date).subscribe({ + next: (slots) => this.slots.set(slots), + error: () => { + console.error('Failed to load slots'); + this.slots.set([]); + }, + }); + } + } + }); - // Uncomment when switching to real data: if (venueId) { this.venueService.loadVenueById(venueId).subscribe({ next: (venue) => { this.venue.set(venue); this.loading.set(false); }, @@ -155,9 +163,9 @@ export class CheckoutComponent implements OnInit { const payload: CreateBookingRequest = { venueId: this.venue()!.id, slotTemplateId: selectedSlot.id, - eventDate: this.form.value.eventDate!, + bookingDate: this.form.value.eventDate!, startTime: selectedSlot.startTime, - endTime: selectedSlot.endTime, + endTime: selectedSlot.endTime, guestCount: Number(this.form.value.guestCount), notes: this.form.value.notes, }; diff --git a/client/src/app/user/features/my-bookings/my-bookings.component.html b/client/src/app/user/features/my-bookings/my-bookings.component.html index 40dc9bbbf..6901f95da 100644 --- a/client/src/app/user/features/my-bookings/my-bookings.component.html +++ b/client/src/app/user/features/my-bookings/my-bookings.component.html @@ -2,46 +2,59 @@

My Bookings

@if (loading()) { - - } @else if (bookings().length === 0) { - + + } @else if (!bookings() || bookings().length === 0) { + } @else { -
-
- - - - - - - - - - - - - @for (booking of bookings(); track booking.id) { - - - - - - - - - } - -
VenueDateTimeGuestsAmountStatus
{{ booking.venueName }}{{ booking.eventDate | dateFormat }}{{ booking.startTime }} - {{ booking.endTime }}{{ booking.guestCount }}{{ booking.totalAmount | currencyFormat }} - - {{ booking.status }} - -
-
+
+
+ + + + + + + + + + + + + @for (booking of bookings(); track booking.id) { + + + + + + + + + } + +
Venue IDDateSlot IDAmountStatusActions
+ {{ booking.venueId }} + + {{ booking.bookingDate | dateFormat }} + + {{ booking.slotTemplateId }} + + {{ booking.totalAmount | currencyFormat }} + + + {{ booking.status }} + + + + @if (booking.status === BookingStatus.Confirmed || booking.status === BookingStatus.Pending) { + + } +
+
} -
+
\ No newline at end of file diff --git a/client/src/app/user/features/my-bookings/my-bookings.component.ts b/client/src/app/user/features/my-bookings/my-bookings.component.ts index ba9b7ab26..c1397d98e 100644 --- a/client/src/app/user/features/my-bookings/my-bookings.component.ts +++ b/client/src/app/user/features/my-bookings/my-bookings.component.ts @@ -1,5 +1,7 @@ import { Component, inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { Router } from '@angular/router'; import { BookingService } from '../../services/booking.service'; +import { Booking } from '../../../shared/models/booking.model'; import { BookingStatus } from '../../../shared/enums/booking-status.enum'; import { TableColumn } from '../../../shared/components/table/table.component'; import { LoaderComponent } from '../../../shared/components/loader/loader.component'; @@ -17,15 +19,18 @@ import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; }) export class MyBookingsComponent implements OnInit { private readonly bookingService = inject(BookingService); + private readonly router = inject(Router); readonly bookings = this.bookingService.bookings; readonly loading = this.bookingService.loading; + // Expose enum to the template so we can compare booking.status against it + readonly BookingStatus = BookingStatus; + readonly columns: TableColumn[] = [ - { key: 'venueName', label: 'Venue', sortable: true }, - { key: 'eventDate', label: 'Date', sortable: true }, - { key: 'startTime', label: 'Time' }, - { key: 'guestCount', label: 'Guests' }, + { key: 'venueId', label: 'Venue ID', sortable: true }, + { key: 'bookingDate', label: 'Date', sortable: true }, + { key: 'slotTemplateId', label: 'Slot' }, { key: 'totalAmount', label: 'Amount', sortable: true }, { key: 'status', label: 'Status', sortable: true }, ]; @@ -34,6 +39,14 @@ export class MyBookingsComponent implements OnInit { this.bookingService.loadUserBookings(); } + onView(booking: Booking): void { + this.router.navigate(['/user/my-bookings', booking.id]); + } + + onCancel(booking: Booking): void { + this.bookingService.cancelBooking(booking.id); + } + getStatusClass(status: string): string { switch (status) { case BookingStatus.Confirmed: return 'bg-green-100 text-green-700'; @@ -43,4 +56,4 @@ export class MyBookingsComponent implements OnInit { default: return 'bg-gray-100 text-gray-700'; } } -} +} \ No newline at end of file diff --git a/client/src/app/user/repositories/booking.repository.ts b/client/src/app/user/repositories/booking.repository.ts index 17480e407..9353cdfbd 100644 --- a/client/src/app/user/repositories/booking.repository.ts +++ b/client/src/app/user/repositories/booking.repository.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, map } from 'rxjs'; import { environment } from '../../core/config/environment'; import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; import { Booking, CreateBookingRequest } from '../../shared/models/booking.model'; @@ -12,8 +12,18 @@ export class BookingRepository { private readonly apiUrl = environment.apiUrl; getUserBookings(): Observable> { - return this.http.get>( + // Backend returns plain array, wrap it in pagination format + return this.http.get( `${this.apiUrl}${API_ENDPOINTS.BOOKINGS.MY_BOOKINGS}` + ).pipe( + map(data => ({ + success: true, + data: data, + total: data.length, + page: 1, + limit: data.length, + totalPages: 1 + })) ); } diff --git a/client/src/app/user/services/booking.service.ts b/client/src/app/user/services/booking.service.ts index ad32b89ce..3732df83f 100644 --- a/client/src/app/user/services/booking.service.ts +++ b/client/src/app/user/services/booking.service.ts @@ -39,8 +39,8 @@ export class BookingService { }); } - cancelBooking(id: string): void { - this.bookingRepository.cancelBooking(id).subscribe({ + cancelBooking(id: number): void { + this.bookingRepository.cancelBooking(String(id)).subscribe({ next: () => { this.bookings.update(list => list.map(b => b.id === id ? { ...b, status: 'cancelled' as Booking['status'] } : b) @@ -52,4 +52,4 @@ export class BookingService { }, }); } -} +} \ No newline at end of file diff --git a/client/src/app/user/services/slot.services.ts b/client/src/app/user/services/slot.services.ts index 39979ed51..62ca211f4 100644 --- a/client/src/app/user/services/slot.services.ts +++ b/client/src/app/user/services/slot.services.ts @@ -1,11 +1,17 @@ // src/app/user/services/slot.service.ts import { Injectable, inject, signal } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable, map } from 'rxjs'; import { environment } from '../../core/config/environment'; import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; import { TimeSlot } from '../../shared/models/slot.model'; +export interface AvailableSlotResponse { + slotTemplateId: number; + startTime: string; + endTime: string; +} + @Injectable({ providedIn: 'root' }) export class SlotService { private readonly http = inject(HttpClient); @@ -14,12 +20,52 @@ export class SlotService { readonly slots = signal([]); readonly loading = signal(false); - loadSlotsForVenue(venueId: string): Observable<{ data: TimeSlot[] }> { - return this.http.get<{ data: TimeSlot[] }>( - `${this.apiUrl}${API_ENDPOINTS.VENUES.SLOTS(venueId)}` + loadSlotsForVenue(venueId: string, date: string): Observable { + const params = new HttpParams().set('date', date); + return this.http.get( + `${this.apiUrl}${API_ENDPOINTS.VENUES.AVAILABILITY(venueId)}`, + { params } + ).pipe( + map(slots => (slots || []).map(slot => ({ + id: slot.slotTemplateId, + label: this.getLabelForSlot(slot.startTime), + startTime: slot.startTime.substring(0, 5), + endTime: slot.endTime.substring(0, 5), + duration: this.formatDuration(slot.startTime, slot.endTime), + available: true, + surcharge: 0 + }))) ); } + private getLabelForSlot(startTime: string): string { + const parts = startTime.split(':').map(Number); + const hour = parts[0] || 0; + if (hour < 12) return 'Morning'; + if (hour < 16) return 'Afternoon'; + if (hour < 19) return 'Evening'; + return 'Night'; + } + + private parseTimeToMinutes(timeStr: string): number { + const parts = timeStr.split(':').map(Number); + const h = parts[0] || 0; + const m = parts[1] || 0; + return h * 60 + m; + } + + private formatDuration(start: string, end: string): string { + const startMins = this.parseTimeToMinutes(start); + const endMins = this.parseTimeToMinutes(end); + let diff = endMins - startMins; + if (diff < 0) { + diff += 24 * 60; // Handle overnight slots + } + const hours = Math.floor(diff / 60); + const mins = diff % 60; + return mins > 0 ? `${hours} hrs ${mins} mins` : `${hours} hrs`; + } + getDummySlots(): TimeSlot[] { return [ diff --git a/client/src/app/vendor/features/bookings/bookings.component.html b/client/src/app/vendor/features/bookings/bookings.component.html index 7a62f736a..18b21e9bd 100644 --- a/client/src/app/vendor/features/bookings/bookings.component.html +++ b/client/src/app/vendor/features/bookings/bookings.component.html @@ -1,46 +1,48 @@

Bookings

@if (loading()) { - + } @else if (bookings().length === 0) { - + } @else { -
- - - - - - - - - - - - - - @for (booking of bookings(); track booking.id) { - - - - - - - - - - } - -
VenueDateTimeGuestsAmountStatusActions
{{ booking.venueName }}{{ booking.eventDate | dateFormat }}{{ booking.startTime }} - {{ booking.endTime }}{{ booking.guestCount }}{{ booking.totalAmount | currencyFormat }} - {{ booking.status }} - - @if (booking.status === 'pending') { -
- - -
- } -
-
+
+ + + + + + + + + + + + + @for (booking of bookings(); track booking.id) { + + + + + + + + + } + +
Venue IDDateSlot IDAmountStatusActions
{{ booking.venueId }}{{ booking.bookingDate | dateFormat }}{{ booking.slotTemplateId }}{{ booking.totalAmount | currencyFormat }} + {{ booking.status + }} + + @if (booking.status === BookingStatus.Pending) { +
+ + +
+ } +
+
} -
+
\ No newline at end of file diff --git a/client/src/app/vendor/features/bookings/bookings.component.ts b/client/src/app/vendor/features/bookings/bookings.component.ts index 4e0609eb9..9dc81f384 100644 --- a/client/src/app/vendor/features/bookings/bookings.component.ts +++ b/client/src/app/vendor/features/bookings/bookings.component.ts @@ -1,5 +1,6 @@ import { Component, inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { BookingService } from '../../services/booking.service'; +import { BookingStatus } from '../../../shared/enums/booking-status.enum'; import { LoaderComponent } from '../../../shared/components/loader/loader.component'; import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; import { DateFormatPipe } from '../../../shared/pipes/date-format.pipe'; @@ -17,9 +18,10 @@ export class BookingsComponent implements OnInit { private readonly bookingService = inject(BookingService); readonly bookings = this.bookingService.bookings; readonly loading = this.bookingService.loading; + readonly BookingStatus = BookingStatus; ngOnInit(): void { this.bookingService.loadVendorBookings(); } - onApprove(id: string): void { this.bookingService.updateBookingStatus(id, 'confirmed'); } - onReject(id: string): void { this.bookingService.updateBookingStatus(id, 'rejected'); } -} + onApprove(id: number): void { this.bookingService.updateBookingStatus(id, 'confirmed'); } + onReject(id: number): void { this.bookingService.updateBookingStatus(id, 'rejected'); } +} \ No newline at end of file diff --git a/client/src/app/vendor/services/booking.service.ts b/client/src/app/vendor/services/booking.service.ts index 92856b7e4..b57bb9f29 100644 --- a/client/src/app/vendor/services/booking.service.ts +++ b/client/src/app/vendor/services/booking.service.ts @@ -19,8 +19,8 @@ export class BookingService { }); } - updateBookingStatus(id: string, status: string): void { - this.bookingRepository.updateBookingStatus(id, status).subscribe({ + updateBookingStatus(id: number, status: string): void { + this.bookingRepository.updateBookingStatus(String(id), status).subscribe({ next: (response) => { this.bookings.update(list => list.map(b => b.id === id ? response.data : b)); this.notification.success(`Booking ${status}`); @@ -28,4 +28,4 @@ export class BookingService { error: () => this.notification.error('Failed to update booking'), }); } -} +} \ No newline at end of file From 4351be3684b3c7e15b14f268f568c356941c2b13 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Sun, 21 Jun 2026 12:46:54 +0530 Subject: [PATCH 44/59] completed venue creation --- .../dashboard/dashboard.component.html | 50 ++++++++++++----- .../features/dashboard/dashboard.component.ts | 17 ++++-- .../features/venues/venues.component.html | 41 +++++++++++--- .../admin/features/venues/venues.component.ts | 35 +++++++++--- .../admin/repositories/venue.repository.ts | 54 ++++++++++++++++--- .../src/app/admin/services/venue.service.ts | 43 ++++++++++++--- client/src/app/app.config.ts | 2 + .../constants/api-endpoints.constant.ts | 15 +++--- .../interceptors/user-auth.interceptor.ts | 5 +- 9 files changed, 207 insertions(+), 55 deletions(-) diff --git a/client/src/app/admin/features/dashboard/dashboard.component.html b/client/src/app/admin/features/dashboard/dashboard.component.html index 00c9df577..d79ccb874 100644 --- a/client/src/app/admin/features/dashboard/dashboard.component.html +++ b/client/src/app/admin/features/dashboard/dashboard.component.html @@ -1,19 +1,41 @@

Admin Dashboard

-
- @for (stat of [ - { label: 'Users', value: stats().totalUsers, color: 'indigo' }, - { label: 'Vendors', value: stats().totalVendors, color: 'green' }, - { label: 'Venues', value: stats().totalVenues, color: 'blue' }, - { label: 'Bookings', value: stats().totalBookings, color: 'amber' }, - { label: 'Revenue', value: stats().totalRevenue, color: 'red' } - ]; track stat.label) { -
-

{{ stat.label }}

-

{{ stat.value }}

-
- } -
+ @if (venueService.dashboard(); as stats) { +
+ @for (stat of [ + { label: 'Users', value: stats.totalUsers, color: 'indigo' }, + { label: 'Vendors', value: stats.totalVendors, color: 'green' }, + { label: 'Total Venues', value: stats.totalVenues, color: 'blue' }, + { label: 'Bookings', value: stats.totalBookings, color: 'amber' } + ]; track stat.label) { +
+

{{ stat.label }}

+

{{ stat.value }}

+
+ } +
+
+ @for (stat of [ + { label: 'Pending Approval', value: stats.pendingVenues, color: 'yellow' }, + { label: 'Approved Venues', value: stats.approvedVenues, color: 'green' }, + { label: 'Rejected Venues', value: stats.rejectedVenues, color: 'red' } + ]; track stat.label) { +
+

{{ stat.label }}

+

{{ stat.value }}

+
+ } +
+ } @else { +
+ @for (i of [1,2,3,4]; track i) { +
+
+
+
+ } +
+ }

Recent Activity

diff --git a/client/src/app/admin/features/dashboard/dashboard.component.ts b/client/src/app/admin/features/dashboard/dashboard.component.ts index 5a4891bf5..6bc520d23 100644 --- a/client/src/app/admin/features/dashboard/dashboard.component.ts +++ b/client/src/app/admin/features/dashboard/dashboard.component.ts @@ -1,10 +1,17 @@ -import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { AdminVenueService } from '../../services/venue.service'; @Component({ - selector: 'app-admin-dashboard', standalone: true, - templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.css', + selector: 'app-admin-dashboard', + standalone: true, + templateUrl: './dashboard.component.html', + styleUrl: './dashboard.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DashboardComponent { - readonly stats = signal({ totalUsers: 0, totalVendors: 0, totalVenues: 0, totalBookings: 0, totalRevenue: 0 }); +export class DashboardComponent implements OnInit { + readonly venueService = inject(AdminVenueService); + + ngOnInit(): void { + this.venueService.loadDashboard(); + } } diff --git a/client/src/app/admin/features/venues/venues.component.html b/client/src/app/admin/features/venues/venues.component.html index 794442737..b4d95b308 100644 --- a/client/src/app/admin/features/venues/venues.component.html +++ b/client/src/app/admin/features/venues/venues.component.html @@ -1,22 +1,51 @@

Venues Management

- @if (loading()) { } @else if (venues().length === 0) { + @if (venueService.loading()) { + + } @else if (venueService.venues().length === 0) { } @else {
- + + + + + + + + - @for (venue of venues(); track venue.id) { + @for (venue of venueService.venues(); track venue.id) { - - - + + + } diff --git a/client/src/app/admin/features/venues/venues.component.ts b/client/src/app/admin/features/venues/venues.component.ts index 08fec8086..173762f75 100644 --- a/client/src/app/admin/features/venues/venues.component.ts +++ b/client/src/app/admin/features/venues/venues.component.ts @@ -1,13 +1,34 @@ -import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; import { LoaderComponent } from '../../../shared/components/loader/loader.component'; import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; -import { Venue } from '../../../shared/models/venue.model'; +import { AdminVenueService } from '../../services/venue.service'; @Component({ - selector: 'app-admin-venues', standalone: true, imports: [LoaderComponent, EmptyStateComponent], - templateUrl: './venues.component.html', styleUrl: './venues.component.css', changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-admin-venues', + standalone: true, + imports: [LoaderComponent, EmptyStateComponent], + templateUrl: './venues.component.html', + styleUrl: './venues.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class VenuesComponent { - readonly venues = signal([]); - readonly loading = signal(false); +export class VenuesComponent implements OnInit { + readonly venueService = inject(AdminVenueService); + readonly actingOnId = signal(null); + + ngOnInit(): void { + this.venueService.loadAllVenues(); + } + + approve(id: number): void { + console.log('id',id); + if (this.actingOnId() !== null) return; + this.actingOnId.set(id); + this.venueService.approveVenue(id, () => this.actingOnId.set(null)); + } + + reject(id: number): void { + if (this.actingOnId() !== null) return; + this.actingOnId.set(id); + this.venueService.rejectVenue(id, () => this.actingOnId.set(null)); + } } diff --git a/client/src/app/admin/repositories/venue.repository.ts b/client/src/app/admin/repositories/venue.repository.ts index ad3e2b74c..fb200a21c 100644 --- a/client/src/app/admin/repositories/venue.repository.ts +++ b/client/src/app/admin/repositories/venue.repository.ts @@ -3,20 +3,60 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../core/config/environment'; import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; -import { Venue } from '../../shared/models/venue.model'; -import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; + +export interface AdminVenue { + id: number; + name: string; + district: string; + address: string; + capacity: number; + ownerName: string; + ownerEmail: string; + status?: string; +} + +export interface DashboardStats { + totalUsers: number; + totalVendors: number; + totalVenues: number; + pendingVenues: number; + approvedVenues: number; + rejectedVenues: number; + totalBookings: number; +} + +export interface SpringPage { + content: T[]; + totalElements: number; + totalPages: number; + number: number; + size: number; +} @Injectable({ providedIn: 'root' }) export class AdminVenueRepository { private readonly http = inject(HttpClient); private readonly apiUrl = environment.apiUrl; - getVenues(page = 1, limit = 10): Observable> { - const params = new HttpParams().set('page', page).set('limit', limit); - return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.VENUES}`, { params }); + getDashboard(): Observable { + return this.http.get(`${this.apiUrl}${API_ENDPOINTS.ADMIN.DASHBOARD}`); + } + + getAllVenues(page = 0, size = 10): Observable> { + const params = new HttpParams().set('page', page).set('size', size); + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.VENUES}`, { params }); + } + + getPendingVenues(page = 0, size = 10): Observable> { + const params = new HttpParams().set('page', page).set('size', size); + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.PENDING_VENUES}`, { params }); + } + + approveVenue(id: number): Observable { + return this.http.patch(`${this.apiUrl}${API_ENDPOINTS.ADMIN.VENUE_APPROVE(id)}`, {}); } - toggleVenueStatus(id: string, status: string): Observable> { - return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.VENUES.PUBLIC_BY_ID(id)}`, { status }); + rejectVenue(id: number): Observable { + return this.http.patch(`${this.apiUrl}${API_ENDPOINTS.ADMIN.VENUE_REJECT(id)}`, {}); } } diff --git a/client/src/app/admin/services/venue.service.ts b/client/src/app/admin/services/venue.service.ts index 5e8d213f3..eaba6b469 100644 --- a/client/src/app/admin/services/venue.service.ts +++ b/client/src/app/admin/services/venue.service.ts @@ -1,6 +1,5 @@ import { Injectable, inject, signal } from '@angular/core'; -import { AdminVenueRepository } from '../repositories/venue.repository'; -import { Venue } from '../../shared/models/venue.model'; +import { AdminVenueRepository, AdminVenue, DashboardStats } from '../repositories/venue.repository'; import { NotificationService } from '../../shared/services/notification.service'; @Injectable({ providedIn: 'root' }) @@ -8,14 +7,46 @@ export class AdminVenueService { private readonly venueRepository = inject(AdminVenueRepository); private readonly notification = inject(NotificationService); - readonly venues = signal([]); + readonly dashboard = signal(null); + readonly venues = signal([]); + readonly pendingVenues = signal([]); readonly loading = signal(false); + readonly totalPages = signal(1); - loadVenues(page = 1): void { + loadDashboard(): void { + this.venueRepository.getDashboard().subscribe({ + next: (stats) => this.dashboard.set(stats), + error: () => this.notification.error('Failed to load dashboard'), + }); + } + + loadAllVenues(page = 0): void { this.loading.set(true); - this.venueRepository.getVenues(page).subscribe({ - next: (r) => { this.venues.set(r.data); this.loading.set(false); }, + this.venueRepository.getAllVenues(page).subscribe({ + next: (r) => { this.venues.set(r.content); this.totalPages.set(r.totalPages); this.loading.set(false); }, error: () => { this.notification.error('Failed to load venues'); this.loading.set(false); }, }); } + + loadPendingVenues(page = 0): void { + this.loading.set(true); + this.venueRepository.getPendingVenues(page).subscribe({ + next: (r) => { this.pendingVenues.set(r.content); this.totalPages.set(r.totalPages); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load pending venues'); this.loading.set(false); }, + }); + } + + approveVenue(id: number, onDone?: () => void): void { + this.venueRepository.approveVenue(id).subscribe({ + next: () => { this.notification.success('Venue approved'); this.loadAllVenues(); onDone?.(); }, + error: () => { this.notification.error('Failed to approve venue'); onDone?.(); }, + }); + } + + rejectVenue(id: number, onDone?: () => void): void { + this.venueRepository.rejectVenue(id).subscribe({ + next: () => { this.notification.success('Venue rejected'); this.loadAllVenues(); onDone?.(); }, + error: () => { this.notification.error('Failed to reject venue'); onDone?.(); }, + }); + } } diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index c694b7dbe..871355b8a 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -4,6 +4,7 @@ import { provideHttpClient, withInterceptors, withXsrfConfiguration } from '@ang import { routes } from './core/app.routes'; import { appProviders } from './core/providers/app-providers'; import { errorInterceptor } from './shared/interceptors/error.interceptor'; +import { userAuthInterceptor } from './user/interceptors/user-auth.interceptor'; export const appConfig: ApplicationConfig = { @@ -12,6 +13,7 @@ export const appConfig: ApplicationConfig = { provideRouter(routes, withComponentInputBinding()), provideHttpClient( withInterceptors([ + userAuthInterceptor, errorInterceptor, ]), withXsrfConfiguration({ diff --git a/client/src/app/shared/constants/api-endpoints.constant.ts b/client/src/app/shared/constants/api-endpoints.constant.ts index eba4a6431..557408a88 100644 --- a/client/src/app/shared/constants/api-endpoints.constant.ts +++ b/client/src/app/shared/constants/api-endpoints.constant.ts @@ -42,11 +42,14 @@ export const API_ENDPOINTS = { }, ADMIN: { - DASHBOARD: '/admin/dashboard', - USERS: '/admin/users', - VENDORS: '/admin/vendors', - VENUES: '/admin/venues', - BOOKINGS: '/admin/bookings', - ANALYTICS: '/admin/analytics', + DASHBOARD: '/v1/admin/dashboard', + USERS: '/v1/admin/users', + VENDORS: '/v1/admin/vendors', + VENUES: '/v1/admin/venues', + PENDING_VENUES: '/v1/admin/venues/pending', + VENUE_APPROVE: (id: number) => `/v1/admin/venues/${id}/approve`, + VENUE_REJECT: (id: number) => `/v1/admin/venues/${id}/reject`, + BOOKINGS: '/v1/admin/bookings', + ANALYTICS: '/v1/admin/analytics', }, } as const; diff --git a/client/src/app/user/interceptors/user-auth.interceptor.ts b/client/src/app/user/interceptors/user-auth.interceptor.ts index ebe9f5ad5..aa03949d3 100644 --- a/client/src/app/user/interceptors/user-auth.interceptor.ts +++ b/client/src/app/user/interceptors/user-auth.interceptor.ts @@ -1,8 +1,5 @@ import { HttpInterceptorFn } from '@angular/common/http'; export const userAuthInterceptor: HttpInterceptorFn = (req, next) => { - const cloned = req.clone({ - withCredentials: true, - }); - return next(cloned); + return next(req.clone({ withCredentials: true })); }; From 2513a12720c7e535e60257bc9c2c96c82ad06b73 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Mon, 22 Jun 2026 02:10:18 +0530 Subject: [PATCH 45/59] fix admin module bugs. --- .../admin/controller/AdminController.java | 30 +++++++++++++++++ .../admin/dto/response/AdminUserResponse.java | 18 ++++++++++ .../server/admin/service/AdminService.java | 5 +++ .../admin/service/AdminServiceImpl.java | 33 +++++++++++++++++++ .../user/repository/UserRepository.java | 5 +++ 5 files changed, 91 insertions(+) create mode 100644 server/src/main/java/com/bookmyvenue/server/admin/dto/response/AdminUserResponse.java diff --git a/server/src/main/java/com/bookmyvenue/server/admin/controller/AdminController.java b/server/src/main/java/com/bookmyvenue/server/admin/controller/AdminController.java index 183aa58d2..cc5217410 100644 --- a/server/src/main/java/com/bookmyvenue/server/admin/controller/AdminController.java +++ b/server/src/main/java/com/bookmyvenue/server/admin/controller/AdminController.java @@ -1,5 +1,6 @@ package com.bookmyvenue.server.admin.controller; +import com.bookmyvenue.server.admin.dto.response.AdminUserResponse; import com.bookmyvenue.server.admin.dto.response.AdminVenueResponse; import com.bookmyvenue.server.admin.dto.response.DashboardResponse; import com.bookmyvenue.server.admin.service.AdminService; @@ -106,4 +107,33 @@ public void approveVenue(@PathVariable Long venueId) { public void rejectVenue(@PathVariable Long venueId) { adminService.rejectVenue(venueId); } + + + @Operation(summary = "Get all users", description = "Returns a paginated list of all registered users.") + @ApiResponses({@ApiResponse(responseCode = "200",description = "Users retrieved successfully"), + @ApiResponse(responseCode = "403", description = "Access denied") + }) + @GetMapping("/users") + public Page getUsers( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + return adminService.getUsers(page, size); + } + + + @Operation(summary = "Get all vendors", description = "Returns a paginated list of all registered venue vendors.") + @ApiResponses({@ApiResponse(responseCode = "200", description = "Vendors retrieved successfully"), + @ApiResponse(responseCode = "403", description = "Access denied") + }) + @GetMapping("/vendors") + public Page getVendors( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + return adminService.getVendors(page, size); + } + + + } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/admin/dto/response/AdminUserResponse.java b/server/src/main/java/com/bookmyvenue/server/admin/dto/response/AdminUserResponse.java new file mode 100644 index 000000000..923b30a42 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/admin/dto/response/AdminUserResponse.java @@ -0,0 +1,18 @@ +package com.bookmyvenue.server.admin.dto.response; + +import com.bookmyvenue.server.user.enums.Role; +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +public class AdminUserResponse { + + private UUID id; + private String name; + private String email; + private String phone; + private Role role; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/admin/service/AdminService.java b/server/src/main/java/com/bookmyvenue/server/admin/service/AdminService.java index 09da375ac..5f8621dd3 100644 --- a/server/src/main/java/com/bookmyvenue/server/admin/service/AdminService.java +++ b/server/src/main/java/com/bookmyvenue/server/admin/service/AdminService.java @@ -1,5 +1,6 @@ package com.bookmyvenue.server.admin.service; +import com.bookmyvenue.server.admin.dto.response.AdminUserResponse; import com.bookmyvenue.server.admin.dto.response.AdminVenueResponse; import com.bookmyvenue.server.admin.dto.response.DashboardResponse; import com.bookmyvenue.server.venue.dto.response.VenueResponse; @@ -26,4 +27,8 @@ public interface AdminService { Page getAllVenues(int page, int size); + + Page getUsers(int page, int size); + + Page getVendors(int page, int size); } diff --git a/server/src/main/java/com/bookmyvenue/server/admin/service/AdminServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/admin/service/AdminServiceImpl.java index b412f5af0..93f2cfab2 100644 --- a/server/src/main/java/com/bookmyvenue/server/admin/service/AdminServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/admin/service/AdminServiceImpl.java @@ -1,5 +1,6 @@ package com.bookmyvenue.server.admin.service; +import com.bookmyvenue.server.admin.dto.response.AdminUserResponse; import com.bookmyvenue.server.admin.dto.response.AdminVenueResponse; import com.bookmyvenue.server.admin.dto.response.DashboardResponse; import com.bookmyvenue.server.booking.repository.BookingRepository; @@ -72,11 +73,13 @@ public Page getPendingVenues(int page, int size) { .capacity(venue.getCapacity()) .ownerName(venue.getOwner().getName()) .ownerEmail(venue.getOwner().getEmail()) + .status(venue.getStatus()) .build()); } @Override + @Transactional public void approveVenue(Long venueId) { Venue venue = venueRepository.findById(venueId) @@ -91,6 +94,7 @@ public void approveVenue(Long venueId) { @Override + @Transactional public void rejectVenue(Long venueId) { Venue venue = venueRepository.findById(venueId) @@ -133,4 +137,33 @@ public Page getAllVenues(int page, int size) { } + @Override + public Page getUsers(int page, int size) { + + Pageable pageable = PageRequest.of(page, size); + return userRepository + .findByRole(Role.USER, pageable) + .map(user -> AdminUserResponse.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .phone(user.getPhone()) + .role(user.getRole()) + .build()); + } + + @Override + public Page getVendors(int page, int size) { + + Pageable pageable = PageRequest.of(page, size); + return userRepository + .findByRole(Role.VENDOR, pageable) + .map(user -> AdminUserResponse.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .phone(user.getPhone()) + .role(user.getRole()) + .build()); + } } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java b/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java index 3b5a34e3e..67f7f12d8 100644 --- a/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java +++ b/server/src/main/java/com/bookmyvenue/server/user/repository/UserRepository.java @@ -2,6 +2,8 @@ import com.bookmyvenue.server.user.entity.User; import com.bookmyvenue.server.user.enums.Role; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; @@ -35,4 +37,7 @@ public interface UserRepository extends JpaRepository { * @return */ long countByRole(Role role); + + + Page findByRole(Role role, Pageable pageable); } \ No newline at end of file From 3d6a6063fa5eaa67cbede3a15707648a1b7e26c4 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Wed, 24 Jun 2026 07:39:51 +0530 Subject: [PATCH 46/59] feat: complete frontend integration for admin, checkout, bookings, and vendor slot management --- .../admin/features/users/users.component.html | 21 +- .../admin/features/users/users.component.ts | 18 +- .../features/vendors/vendors.component.html | 22 ++- .../features/vendors/vendors.component.ts | 19 +- client/src/app/admin/models/index.ts | 9 + .../app/admin/repositories/user.repository.ts | 18 +- .../admin/repositories/vendor.repository.ts | 22 +-- client/src/app/admin/services/user.service.ts | 8 +- .../src/app/admin/services/vendor.service.ts | 8 +- .../constants/api-endpoints.constant.ts | 6 + .../shared/constants/route-paths.constant.ts | 1 + .../app/shared/enums/booking-status.enum.ts | 9 +- .../app/shared/models/api-response.model.ts | 9 + client/src/app/shared/models/booking.model.ts | 15 ++ client/src/app/shared/models/slot.model.ts | 17 +- .../features/checkout/checkout.component.ts | 180 ++++++++---------- .../my-bookings/my-bookings.component.html | 2 +- .../my-bookings/my-bookings.component.ts | 17 +- .../venue-details/venue-details.component.ts | 83 +------- .../venue-list/venue-list.component.ts | 18 -- .../user/repositories/booking.repository.ts | 10 +- .../user/repositories/payment.repository.ts | 26 +++ .../src/app/user/services/booking.service.ts | 32 +--- .../features/bookings/bookings.component.html | 2 +- .../slot-management.component.css | 0 .../slot-management.component.html | 78 ++++++++ .../slot-management.component.spec.ts | 28 +++ .../slot-management.component.ts | 45 +++++ .../venue-list/venue-list.component.html | 1 + .../vendor/repositories/slot.repository.ts | 31 +++ client/src/app/vendor/routes/vendor.routes.ts | 1 + .../src/app/vendor/services/slot.service.ts | 48 +++++ client/src/index.html | 1 + 33 files changed, 482 insertions(+), 323 deletions(-) create mode 100644 client/src/app/user/repositories/payment.repository.ts create mode 100644 client/src/app/vendor/features/slot-management/slot-management.component.css create mode 100644 client/src/app/vendor/features/slot-management/slot-management.component.html create mode 100644 client/src/app/vendor/features/slot-management/slot-management.component.spec.ts create mode 100644 client/src/app/vendor/features/slot-management/slot-management.component.ts create mode 100644 client/src/app/vendor/repositories/slot.repository.ts create mode 100644 client/src/app/vendor/services/slot.service.ts diff --git a/client/src/app/admin/features/users/users.component.html b/client/src/app/admin/features/users/users.component.html index 276025496..cf1edb501 100644 --- a/client/src/app/admin/features/users/users.component.html +++ b/client/src/app/admin/features/users/users.component.html @@ -1,26 +1,31 @@

Users Management

- @if (loading()) { } @else if (users().length === 0) { - + @if (userService.loading()) { } + @else if (userService.users().length === 0) { + } @else {
NameDistrictCapacityPrice/SlotStatusActions
NameDistrictCapacityOwnerStatusActions
{{ venue.name }} {{ venue.district }} {{ venue.capacity }}{{ venue.pricePerSlot }}{{ venue.status }}{{ venue.ownerName }} + + {{ venue.status }} + + + @if (venue.status === 'PENDING_APPROVAL') { + + + } +
- + + + + + + - @for (user of users(); track user.id) { + @for (user of userService.users(); track user.id) { - + - - + }
NameEmailPhoneStatusActions
NameEmailPhoneJoined
{{ user.firstName }} {{ user.lastName }}{{ user.name }} {{ user.email }} {{ user.phone }}{{ user.isActive ? 'Active' : 'Inactive' }}{{ user.createdAt | date:'mediumDate' }}
- + }
diff --git a/client/src/app/admin/features/users/users.component.ts b/client/src/app/admin/features/users/users.component.ts index e0a30caaf..d086bea3c 100644 --- a/client/src/app/admin/features/users/users.component.ts +++ b/client/src/app/admin/features/users/users.component.ts @@ -1,20 +1,20 @@ -import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { LoaderComponent } from '../../../shared/components/loader/loader.component'; import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; import { PaginationComponent } from '../../../shared/components/pagination/pagination.component'; -import { User } from '../../../shared/models/user.model'; +import { AdminUserService } from '../../services/user.service'; +import { DatePipe } from '@angular/common'; @Component({ selector: 'app-admin-users', standalone: true, - imports: [LoaderComponent, EmptyStateComponent, PaginationComponent], + imports: [LoaderComponent, EmptyStateComponent, PaginationComponent,DatePipe], templateUrl: './users.component.html', styleUrl: './users.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class UsersComponent { - readonly users = signal([]); - readonly loading = signal(false); - readonly currentPage = signal(1); - readonly totalPages = signal(1); +export class UsersComponent implements OnInit { + readonly userService = inject(AdminUserService); - onPageChange(page: number): void { this.currentPage.set(page); } + ngOnInit(): void { this.userService.loadUsers(); } + + onPageChange(page: number): void { this.userService.loadUsers(page - 1); } } diff --git a/client/src/app/admin/features/vendors/vendors.component.html b/client/src/app/admin/features/vendors/vendors.component.html index 2748dbf87..94748cfe9 100644 --- a/client/src/app/admin/features/vendors/vendors.component.html +++ b/client/src/app/admin/features/vendors/vendors.component.html @@ -1,27 +1,31 @@

Vendors Management

- @if (loading()) { } @else if (vendors().length === 0) { + @if (vendorService.loading()) { } + @else if (vendorService.vendors().length === 0) { } @else {
- + + + + + + - @for (vendor of vendors(); track vendor.id) { + @for (vendor of vendorService.vendors(); track vendor.id) { - - + - - - + + }
BusinessContactEmailVerifiedStatusActions
NameEmailPhoneJoined
{{ vendor.businessName }}{{ vendor.contactPerson }}{{ vendor.name }} {{ vendor.email }}{{ vendor.isVerified ? 'Verified' : 'Pending' }}{{ vendor.isActive ? 'Active' : 'Inactive' }}{{ vendor.phone }}{{ vendor.createdAt | date:'mediumDate' }}
- + }
diff --git a/client/src/app/admin/features/vendors/vendors.component.ts b/client/src/app/admin/features/vendors/vendors.component.ts index 2043fac63..c2366609b 100644 --- a/client/src/app/admin/features/vendors/vendors.component.ts +++ b/client/src/app/admin/features/vendors/vendors.component.ts @@ -1,19 +1,20 @@ -import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { LoaderComponent } from '../../../shared/components/loader/loader.component'; import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; import { PaginationComponent } from '../../../shared/components/pagination/pagination.component'; -import { Vendor } from '../../../shared/models/vendor.model'; +import { AdminVendorService } from '../../services/vendor.service'; +import { DatePipe } from '@angular/common'; @Component({ selector: 'app-admin-vendors', standalone: true, - imports: [LoaderComponent, EmptyStateComponent, PaginationComponent], + imports: [LoaderComponent, EmptyStateComponent, PaginationComponent,DatePipe], templateUrl: './vendors.component.html', styleUrl: './vendors.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class VendorsComponent { - readonly vendors = signal([]); - readonly loading = signal(false); - readonly currentPage = signal(1); - readonly totalPages = signal(1); - onPageChange(page: number): void { this.currentPage.set(page); } +export class VendorsComponent implements OnInit { + readonly vendorService = inject(AdminVendorService); + + ngOnInit(): void { this.vendorService.loadVendors(); } + + onPageChange(page: number): void { this.vendorService.loadVendors(page - 1); } } diff --git a/client/src/app/admin/models/index.ts b/client/src/app/admin/models/index.ts index 4d233a546..e43d566fc 100644 --- a/client/src/app/admin/models/index.ts +++ b/client/src/app/admin/models/index.ts @@ -2,3 +2,12 @@ export * from '../../shared/models/user.model'; export * from '../../shared/models/vendor.model'; export * from '../../shared/models/venue.model'; export * from '../../shared/models/booking.model'; + +export interface AdminUser { + id: string; + name: string; + email: string; + phone: string; + role: string; + createdAt: string; +} diff --git a/client/src/app/admin/repositories/user.repository.ts b/client/src/app/admin/repositories/user.repository.ts index 188d09795..66543efbf 100644 --- a/client/src/app/admin/repositories/user.repository.ts +++ b/client/src/app/admin/repositories/user.repository.ts @@ -3,24 +3,16 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../core/config/environment'; import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; -import { User } from '../../shared/models/user.model'; -import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; +import { SpringPage } from '../../shared/models/api-response.model'; +import { AdminUser } from '../models'; @Injectable({ providedIn: 'root' }) export class AdminUserRepository { private readonly http = inject(HttpClient); private readonly apiUrl = environment.apiUrl; - getUsers(page = 1, limit = 10): Observable> { - const params = new HttpParams().set('page', page).set('limit', limit); - return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.USERS}`, { params }); - } - - getUserById(id: string): Observable> { - return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.USERS.BY_ID(id)}`); - } - - toggleUserStatus(id: string, isActive: boolean): Observable> { - return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.USERS.BY_ID(id)}`, { isActive }); + getUsers(page = 0, size = 10): Observable> { + const params = new HttpParams().set('page', page).set('size', size); + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.USERS}`, { params }); } } diff --git a/client/src/app/admin/repositories/vendor.repository.ts b/client/src/app/admin/repositories/vendor.repository.ts index 048c7f692..2aae0ccc2 100644 --- a/client/src/app/admin/repositories/vendor.repository.ts +++ b/client/src/app/admin/repositories/vendor.repository.ts @@ -3,28 +3,16 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../core/config/environment'; import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; -import { Vendor } from '../../shared/models/vendor.model'; -import { ApiResponse, PaginationResponse } from '../../shared/models/api-response.model'; +import { SpringPage } from '../../shared/models/api-response.model'; +import { AdminUser } from '../models'; @Injectable({ providedIn: 'root' }) export class AdminVendorRepository { private readonly http = inject(HttpClient); private readonly apiUrl = environment.apiUrl; - getVendors(page = 1, limit = 10): Observable> { - const params = new HttpParams().set('page', page).set('limit', limit); - return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.VENDORS}`, { params }); - } - - getVendorById(id: string): Observable> { - return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.BY_ID(id)}`); - } - - verifyVendor(id: string): Observable> { - return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.BY_ID(id)}`, { isVerified: true }); - } - - toggleVendorStatus(id: string, isActive: boolean): Observable> { - return this.http.patch>(`${this.apiUrl}${API_ENDPOINTS.VENDORS.BY_ID(id)}`, { isActive }); + getVendors(page = 0, size = 10): Observable> { + const params = new HttpParams().set('page', page).set('size', size); + return this.http.get>(`${this.apiUrl}${API_ENDPOINTS.ADMIN.VENDORS}`, { params }); } } diff --git a/client/src/app/admin/services/user.service.ts b/client/src/app/admin/services/user.service.ts index 78cc24bab..440d0a5ee 100644 --- a/client/src/app/admin/services/user.service.ts +++ b/client/src/app/admin/services/user.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject, signal } from '@angular/core'; import { AdminUserRepository } from '../repositories/user.repository'; -import { User } from '../../shared/models/user.model'; +import { AdminUser } from '../models'; import { NotificationService } from '../../shared/services/notification.service'; @Injectable({ providedIn: 'root' }) @@ -8,14 +8,14 @@ export class AdminUserService { private readonly userRepository = inject(AdminUserRepository); private readonly notification = inject(NotificationService); - readonly users = signal([]); + readonly users = signal([]); readonly loading = signal(false); readonly totalPages = signal(1); - loadUsers(page = 1): void { + loadUsers(page = 0): void { this.loading.set(true); this.userRepository.getUsers(page).subscribe({ - next: (r) => { this.users.set(r.data); this.totalPages.set(r.totalPages); this.loading.set(false); }, + next: (r) => { this.users.set(r.content); this.totalPages.set(r.totalPages); this.loading.set(false); }, error: () => { this.notification.error('Failed to load users'); this.loading.set(false); }, }); } diff --git a/client/src/app/admin/services/vendor.service.ts b/client/src/app/admin/services/vendor.service.ts index 97b72fb1d..620460d5f 100644 --- a/client/src/app/admin/services/vendor.service.ts +++ b/client/src/app/admin/services/vendor.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject, signal } from '@angular/core'; import { AdminVendorRepository } from '../repositories/vendor.repository'; -import { Vendor } from '../../shared/models/vendor.model'; +import { AdminUser } from '../models'; import { NotificationService } from '../../shared/services/notification.service'; @Injectable({ providedIn: 'root' }) @@ -8,14 +8,14 @@ export class AdminVendorService { private readonly vendorRepository = inject(AdminVendorRepository); private readonly notification = inject(NotificationService); - readonly vendors = signal([]); + readonly vendors = signal([]); readonly loading = signal(false); readonly totalPages = signal(1); - loadVendors(page = 1): void { + loadVendors(page = 0): void { this.loading.set(true); this.vendorRepository.getVendors(page).subscribe({ - next: (r) => { this.vendors.set(r.data); this.totalPages.set(r.totalPages); this.loading.set(false); }, + next: (r) => { this.vendors.set(r.content); this.totalPages.set(r.totalPages); this.loading.set(false); }, error: () => { this.notification.error('Failed to load vendors'); this.loading.set(false); }, }); } diff --git a/client/src/app/shared/constants/api-endpoints.constant.ts b/client/src/app/shared/constants/api-endpoints.constant.ts index 557408a88..4ff0951c5 100644 --- a/client/src/app/shared/constants/api-endpoints.constant.ts +++ b/client/src/app/shared/constants/api-endpoints.constant.ts @@ -29,6 +29,8 @@ export const API_ENDPOINTS = { VENDOR_BASE: '/v1/vendor/venues', VENDOR_BY_ID: (id: string) => `/v1/vendor/venues/${id}`, + SLOTS_BY_VENUE: (venueId: string) => `/v1/venues/${venueId}/slots`, + DELETE_SLOT: (templateId: number) => `/v1/venues/slots/${templateId}`, }, BOOKINGS: { BASE: '/bookings', @@ -52,4 +54,8 @@ export const API_ENDPOINTS = { BOOKINGS: '/v1/admin/bookings', ANALYTICS: '/v1/admin/analytics', }, + PAYMENTS: { + CREATE: (bookingId: number) => `/v1/payments/${bookingId}`, + VERIFY: '/v1/payments/verify', + }, } as const; diff --git a/client/src/app/shared/constants/route-paths.constant.ts b/client/src/app/shared/constants/route-paths.constant.ts index 0976250cd..cd2d64ddd 100644 --- a/client/src/app/shared/constants/route-paths.constant.ts +++ b/client/src/app/shared/constants/route-paths.constant.ts @@ -23,6 +23,7 @@ export const ROUTE_PATHS = { BOOKINGS: 'bookings', ANALYTICS: 'analytics', SETTINGS: 'settings', + VENUE_SLOTS: 'venues/:id/slots', }, ADMIN: { BASE: 'admin', diff --git a/client/src/app/shared/enums/booking-status.enum.ts b/client/src/app/shared/enums/booking-status.enum.ts index 9343ad966..bc31ecbcb 100644 --- a/client/src/app/shared/enums/booking-status.enum.ts +++ b/client/src/app/shared/enums/booking-status.enum.ts @@ -1,7 +1,6 @@ export enum BookingStatus { - Pending = 'pending', - Confirmed = 'confirmed', - Cancelled = 'cancelled', - Completed = 'completed', - Rejected = 'rejected', + PENDING = 'PENDING', + CONFIRMED = 'CONFIRMED', + CANCELLED = 'CANCELLED', + EXPIRED = 'EXPIRED', } diff --git a/client/src/app/shared/models/api-response.model.ts b/client/src/app/shared/models/api-response.model.ts index 5a3792e66..3c31789ab 100644 --- a/client/src/app/shared/models/api-response.model.ts +++ b/client/src/app/shared/models/api-response.model.ts @@ -18,3 +18,12 @@ export interface ApiError { message: string; errors?: Record; } + + +export interface SpringPage { + content: T[]; + totalPages: number; + totalElements: number; + number: number; + size: number; +} diff --git a/client/src/app/shared/models/booking.model.ts b/client/src/app/shared/models/booking.model.ts index 2f004e643..9a524e6bb 100644 --- a/client/src/app/shared/models/booking.model.ts +++ b/client/src/app/shared/models/booking.model.ts @@ -28,3 +28,18 @@ export interface UpdateBookingRequest { status?: BookingStatus; notes?: string; } + +export interface PaymentOrder { + paymentId: number; + bookingId: number; + razorpayOrderId: string; + amount: number; + status: string; +} + +export interface VerifyPaymentRequest { + razorpayOrderId: string; + razorpayPaymentId: string; + razorpaySignature: string; +} + diff --git a/client/src/app/shared/models/slot.model.ts b/client/src/app/shared/models/slot.model.ts index 36f8f0ccd..1f5b23bae 100644 --- a/client/src/app/shared/models/slot.model.ts +++ b/client/src/app/shared/models/slot.model.ts @@ -1,4 +1,3 @@ -// src/app/shared/models/slot.model.ts export interface TimeSlot { id: number; label: string; @@ -7,4 +6,18 @@ export interface TimeSlot { duration: string; available: boolean; surcharge: number; -} \ No newline at end of file +} + +export interface SlotTemplate { + id: number; + dayOfWeek: string; + startTime: string; + endTime: string; + active: boolean; +} + +export interface CreateSlotTemplateRequest { + dayOfWeek: string; + startTime: string; + endTime: string; +} diff --git a/client/src/app/user/features/checkout/checkout.component.ts b/client/src/app/user/features/checkout/checkout.component.ts index 9b4a9620e..aede8d383 100644 --- a/client/src/app/user/features/checkout/checkout.component.ts +++ b/client/src/app/user/features/checkout/checkout.component.ts @@ -3,17 +3,18 @@ import { ActivatedRoute, Router } from '@angular/router'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { VenueService } from '../../services/venue.service'; import { BookingService } from '../../services/booking.service'; +import { PaymentRepository } from '../../repositories/payment.repository'; import { Venue } from '../../../shared/models/venue.model'; -import { VenueStatus } from '../../../shared/enums/venue-status.enum'; import { ButtonComponent } from '../../../shared/components/button/button.component'; import { InputComponent } from '../../../shared/components/input/input.component'; import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; import { AppValidators } from '../../../shared/utils/validators'; import { SlotService } from '../../services/slot.services'; import { TimeSlot } from '../../../shared/models/slot.model'; -import { CreateBookingRequest } from '../../../shared/models/booking.model'; - +import { CreateBookingRequest, VerifyPaymentRequest } from '../../../shared/models/booking.model'; +import { NotificationService } from '../../../shared/services/notification.service'; +declare var Razorpay: any; export interface Amenity { id: string; @@ -37,93 +38,48 @@ export class CheckoutComponent implements OnInit { private readonly fb = inject(FormBuilder); private readonly venueService = inject(VenueService); private readonly bookingService = inject(BookingService); + private readonly paymentRepository = inject(PaymentRepository); private readonly slotService = inject(SlotService); + private readonly notification = inject(NotificationService); readonly venue = signal(null); readonly loading = signal(true); readonly submitting = signal(false); - readonly selectedSlotId = signal(null); readonly selectedAmenityIds = signal>(new Set()); - - // ── DUMMY VENUES (remove when switching to real backend) ──────────────── - private readonly dummyVenues: Record = { - '1': { id: '1', name: 'The Grand Ballroom', category: 'Banquet Hall', address: '12 Marine Drive', district: 'Ernakulam', capacity: 500, pricePerSlot: 25000, status: VenueStatus.APPROVED, description: '', imageUrls: ['https://images.unsplash.com/photo-1519167758481-83f550bb49b3?w=800&q=80'] }, - '2': { id: '2', name: 'Skyline Rooftop Terrace', category: 'Rooftop', address: '8 MG Road', district: 'Thrissur', capacity: 150, pricePerSlot: 12000, status: VenueStatus.APPROVED, description: '', imageUrls: ['https://images.unsplash.com/photo-1533174072545-7a4b6ad7a6c3?w=800&q=80'] }, - '3': { id: '3', name: 'Garden Pavilion', category: 'Outdoor', address: '34 NH Bypass', district: 'Kozhikode', capacity: 300, pricePerSlot: 18000, status: VenueStatus.APPROVED, description: '', imageUrls: ['https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?w=800&q=80'] }, - '4': { id: '4', name: 'Heritage Haveli', category: 'Heritage', address: '5 Fort Road', district: 'Thiruvananthapuram', capacity: 200, pricePerSlot: 22000, status: VenueStatus.APPROVED, description: '', imageUrls: ['https://images.unsplash.com/photo-1505236858219-8359eb29e329?w=800&q=80'] }, - '5': { id: '5', name: 'The Conference Hub', category: 'Conference', address: '17 Technopark', district: 'Thiruvananthapuram', capacity: 80, pricePerSlot: 8000, status: VenueStatus.APPROVED, description: '', imageUrls: ['https://images.unsplash.com/photo-1517457373958-b7bdd4587205?w=800&q=80'] }, - '6': { id: '6', name: 'Lakeside Retreat', category: 'Resort', address: '2 Lake View Road', district: 'Alappuzha', capacity: 250, pricePerSlot: 20000, status: VenueStatus.APPROVED, description: '', imageUrls: ['https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800&q=80'] }, - }; - // ──────────────────────────────────────────────────────────────────────── - readonly slots = signal([]); + readonly amenities: Amenity[] = [ - { id: 'a1', name: 'Catering Service', desc: 'Full catering for all guests (per head)', price: 500, icon: 'M3 3h2l.4 2M7 13h10l4-4H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z' }, - { id: 'a2', name: 'AV & Sound System', desc: 'Professional PA, lighting & projector', price: 3000, icon: 'M15.536 8.464a5 5 0 010 7.072M12 6a7 7 0 010 12M8.464 8.464a5 5 0 000 7.072M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, - { id: 'a3', name: 'Valet Parking', desc: 'Dedicated parking for up to 100 cars', price: 1500, icon: 'M8 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM15 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM3 4h13l4 4v4H3V4z' }, - { id: 'a4', name: 'Event Decoration', desc: 'Floral & thematic decoration setup', price: 8000, icon: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z' }, - { id: 'a5', name: 'Security Team', desc: '4 trained security personnel', price: 2000, icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' }, - { id: 'a6', name: 'Photography Setup', desc: 'Photo booth + candid photography', price: 5000, icon: 'M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z M15 13a3 3 0 11-6 0 3 3 0 016 0z' }, + { id: 'a1', name: 'Catering Service', desc: 'Full catering for all guests (per head)', price: 500, icon: 'M3 3h2l.4 2M7 13h10l4-4H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z' }, + { id: 'a2', name: 'AV & Sound System', desc: 'Professional PA, lighting & projector', price: 3000, icon: 'M15.536 8.464a5 5 0 010 7.072M12 6a7 7 0 010 12M8.464 8.464a5 5 0 000 7.072M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, + { id: 'a3', name: 'Valet Parking', desc: 'Dedicated parking for up to 100 cars', price: 1500, icon: 'M8 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM15 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM3 4h13l4 4v4H3V4z' }, + { id: 'a4', name: 'Event Decoration', desc: 'Floral & thematic decoration setup', price: 8000, icon: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z' }, + { id: 'a5', name: 'Security Team', desc: '4 trained security personnel', price: 2000, icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' }, + { id: 'a6', name: 'Photography Setup', desc: 'Photo booth + candid photography', price: 5000, icon: 'M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z M15 13a3 3 0 11-6 0 3 3 0 016 0z' }, ]; readonly form = this.fb.nonNullable.group({ - eventDate: ['', [Validators.required, AppValidators.futureDate]], + eventDate: ['', [Validators.required, AppValidators.futureDate]], guestCount: ['', [Validators.required, Validators.min(1)]], - notes: [''], - }); - - // ── Computed totals ────────────────────────────────────────────────────── - readonly selectedSlot = computed(() => - this.slots().find(s => s.id === this.selectedSlotId()) ?? null - ); - - readonly amenitiesTotal = computed(() => - this.amenities - .filter(a => this.selectedAmenityIds().has(a.id)) - .reduce((sum, a) => sum + a.price, 0) - ); - - readonly slotTotal = computed(() => { - const v = this.venue(); - const slot = this.selectedSlot(); - if (!v || !slot) return 0; - return v.pricePerSlot + slot.surcharge; + notes: [''], }); + readonly selectedSlot = computed(() => this.slots().find(s => s.id === this.selectedSlotId()) ?? null); + readonly amenitiesTotal = computed(() => this.amenities.filter(a => this.selectedAmenityIds().has(a.id)).reduce((sum, a) => sum + a.price, 0)); + readonly slotTotal = computed(() => { const v = this.venue(); const slot = this.selectedSlot(); if (!v || !slot) return 0; return v.pricePerSlot + slot.surcharge; }); readonly grandTotal = computed(() => this.slotTotal() + this.amenitiesTotal()); - - readonly selectedAmenitiesList = computed(() => - this.amenities.filter(a => this.selectedAmenityIds().has(a.id)) - ); - // ──────────────────────────────────────────────────────────────────────── + readonly selectedAmenitiesList = computed(() => this.amenities.filter(a => this.selectedAmenityIds().has(a.id))); ngOnInit(): void { const venueId = this.route.snapshot.paramMap.get('venueId') ?? ''; - // Watch for date changes to fetch available slots - this.form.controls.eventDate.valueChanges.subscribe({ - next: (date) => { - this.selectedSlotId.set(null); - if (!date || this.form.controls.eventDate.invalid) { - this.slots.set([]); - return; - } - - const USE_DUMMY_DATA = false; // Set to false when backend ready - - if (USE_DUMMY_DATA) { - this.slots.set(this.slotService.getDummySlots()); - } else { - this.slotService.loadSlotsForVenue(venueId, date).subscribe({ - next: (slots) => this.slots.set(slots), - error: () => { - console.error('Failed to load slots'); - this.slots.set([]); - }, - }); - } - } + this.form.controls.eventDate.valueChanges.subscribe(date => { + this.selectedSlotId.set(null); + if (!date || this.form.controls.eventDate.invalid) { this.slots.set([]); return; } + this.slotService.loadSlotsForVenue(venueId, date).subscribe({ + next: (slots) => this.slots.set(slots), + error: () => this.slots.set([]), + }); }); if (venueId) { @@ -134,43 +90,65 @@ export class CheckoutComponent implements OnInit { } } - selectSlot(slot: TimeSlot): void { - if (!slot.available) return; - this.selectedSlotId.set(slot.id); - } + selectSlot(slot: TimeSlot): void { if (!slot.available) return; this.selectedSlotId.set(slot.id); } toggleAmenity(id: string): void { - this.selectedAmenityIds.update(set => { - const next = new Set(set); - next.has(id) ? next.delete(id) : next.add(id); - return next; + this.selectedAmenityIds.update(set => { const next = new Set(set); next.has(id) ? next.delete(id) : next.add(id); return next; }); + } + + isAmenitySelected(id: string): boolean { return this.selectedAmenityIds().has(id); } + + onSubmit(): void { + const selectedSlot = this.selectedSlot(); + if (!this.form.valid || !this.venue() || !selectedSlot) return; + + this.submitting.set(true); + + const payload: CreateBookingRequest = { + venueId: this.venue()!.id, + slotTemplateId: selectedSlot.id, + bookingDate: this.form.value.eventDate!, + startTime: selectedSlot.startTime, + endTime: selectedSlot.endTime, + guestCount: Number(this.form.value.guestCount), + notes: this.form.value.notes, + }; + + this.bookingService.createBooking(payload).subscribe({ + next: (booking) => this.initiatePayment(booking.id, booking.totalAmount), + error: () => { this.notification.error('Failed to create booking'); this.submitting.set(false); }, }); } - isAmenitySelected(id: string): boolean { - return this.selectedAmenityIds().has(id); + private initiatePayment(bookingId: number, amount: number): void { + this.paymentRepository.createPaymentOrder(bookingId).subscribe({ + next: (order) => { + const options = { + key: '', // Leave empty — Razorpay key comes from backend via order + amount: order.amount * 100, + currency: 'INR', + order_id: order.razorpayOrderId, + name: 'BookMyVenue', + description: 'Venue Booking Payment', + handler: (response: any) => this.verifyPayment(response), + modal: { ondismiss: () => this.submitting.set(false) }, + }; + const rzp = new Razorpay(options); + rzp.open(); + }, + error: () => { this.notification.error('Failed to initiate payment'); this.submitting.set(false); }, + }); } - onSubmit(): void { - if (!this.selectedSlotId()) return; - if (this.form.valid && this.venue()) { - this.submitting.set(true); - - const selectedSlot = this.slots().find(s => s.id === this.selectedSlotId()); - - if (!selectedSlot) return; - - const payload: CreateBookingRequest = { - venueId: this.venue()!.id, - slotTemplateId: selectedSlot.id, - bookingDate: this.form.value.eventDate!, - startTime: selectedSlot.startTime, - endTime: selectedSlot.endTime, - guestCount: Number(this.form.value.guestCount), - notes: this.form.value.notes, - }; - - this.bookingService.createBooking(payload); - } + private verifyPayment(response: any): void { + const request: VerifyPaymentRequest = { + razorpayOrderId: response.razorpay_order_id, + razorpayPaymentId: response.razorpay_payment_id, + razorpaySignature: response.razorpay_signature, + }; + this.paymentRepository.verifyPayment(request).subscribe({ + next: () => { this.notification.success('Payment successful! Booking confirmed.'); this.router.navigate(['/user/my-bookings']); }, + error: () => { this.notification.error('Payment verification failed'); this.submitting.set(false); }, + }); } } diff --git a/client/src/app/user/features/my-bookings/my-bookings.component.html b/client/src/app/user/features/my-bookings/my-bookings.component.html index 6901f95da..25b8ead24 100644 --- a/client/src/app/user/features/my-bookings/my-bookings.component.html +++ b/client/src/app/user/features/my-bookings/my-bookings.component.html @@ -44,7 +44,7 @@

My Bookings

- @if (booking.status === BookingStatus.Confirmed || booking.status === BookingStatus.Pending) { + @if (booking.status === BookingStatus.CONFIRMED || booking.status === BookingStatus.PENDING) { diff --git a/client/src/app/user/features/my-bookings/my-bookings.component.ts b/client/src/app/user/features/my-bookings/my-bookings.component.ts index c1397d98e..72f2aea65 100644 --- a/client/src/app/user/features/my-bookings/my-bookings.component.ts +++ b/client/src/app/user/features/my-bookings/my-bookings.component.ts @@ -3,7 +3,6 @@ import { Router } from '@angular/router'; import { BookingService } from '../../services/booking.service'; import { Booking } from '../../../shared/models/booking.model'; import { BookingStatus } from '../../../shared/enums/booking-status.enum'; -import { TableColumn } from '../../../shared/components/table/table.component'; import { LoaderComponent } from '../../../shared/components/loader/loader.component'; import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; import { DateFormatPipe } from '../../../shared/pipes/date-format.pipe'; @@ -27,14 +26,6 @@ export class MyBookingsComponent implements OnInit { // Expose enum to the template so we can compare booking.status against it readonly BookingStatus = BookingStatus; - readonly columns: TableColumn[] = [ - { key: 'venueId', label: 'Venue ID', sortable: true }, - { key: 'bookingDate', label: 'Date', sortable: true }, - { key: 'slotTemplateId', label: 'Slot' }, - { key: 'totalAmount', label: 'Amount', sortable: true }, - { key: 'status', label: 'Status', sortable: true }, - ]; - ngOnInit(): void { this.bookingService.loadUserBookings(); } @@ -49,10 +40,10 @@ export class MyBookingsComponent implements OnInit { getStatusClass(status: string): string { switch (status) { - case BookingStatus.Confirmed: return 'bg-green-100 text-green-700'; - case BookingStatus.Pending: return 'bg-yellow-100 text-yellow-700'; - case BookingStatus.Cancelled: return 'bg-red-100 text-red-700'; - case BookingStatus.Completed: return 'bg-blue-100 text-blue-700'; + case BookingStatus.CONFIRMED: return 'bg-green-100 text-green-700'; + case BookingStatus.PENDING: return 'bg-yellow-100 text-yellow-700'; + case BookingStatus.CANCELLED: return 'bg-red-100 text-red-700'; + case BookingStatus.EXPIRED: return 'bg-red-100 text-red-700'; default: return 'bg-gray-100 text-gray-700'; } } diff --git a/client/src/app/user/features/venue-details/venue-details.component.ts b/client/src/app/user/features/venue-details/venue-details.component.ts index fbf5ade58..2f54e2ffd 100644 --- a/client/src/app/user/features/venue-details/venue-details.component.ts +++ b/client/src/app/user/features/venue-details/venue-details.component.ts @@ -2,7 +2,6 @@ import { Component, inject, OnInit, signal, ChangeDetectionStrategy } from '@ang import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { VenueService } from '../../services/venue.service'; import { Venue } from '../../../shared/models/venue.model'; -import { VenueStatus } from '../../../shared/enums/venue-status.enum'; import { ButtonComponent } from '../../../shared/components/button/button.component'; import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; @@ -23,82 +22,8 @@ export class VenueDetailsComponent implements OnInit { readonly loading = signal(true); readonly selectedImage = signal(0); - // ── DUMMY DATA (remove when switching to real backend) ────────────────── - private readonly dummyVenues: Record = { - '1': { - id: '1', name: 'The Grand Ballroom', category: 'Banquet Hall', - address: '12 Marine Drive', district: 'Ernakulam', - capacity: 500, pricePerSlot: 25000, status: VenueStatus.APPROVED, - description: 'A magnificent ballroom with soaring ceilings, crystal chandeliers, and a grand stage. Perfect for large weddings, gala dinners, and corporate galas. The space features state-of-the-art sound and lighting systems, a dedicated catering kitchen, and an elegant pre-function foyer for cocktail receptions.', - imageUrls: [ - 'https://images.unsplash.com/photo-1519167758481-83f550bb49b3?w=1200&q=80', - 'https://images.unsplash.com/photo-1542314831-068cd1dbfeeb?w=1200&q=80', - 'https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?w=1200&q=80', - ], - }, - '2': { - id: '2', name: 'Skyline Rooftop Terrace', category: 'Rooftop', - address: '8 MG Road', district: 'Thrissur', - capacity: 150, pricePerSlot: 12000, status: VenueStatus.APPROVED, - description: 'An open-air rooftop venue with breathtaking panoramic city views. Features include a built-in bar, lounge seating, and fairy-light canopy. Ideal for cocktail parties, product launches, and intimate celebrations under the stars.', - imageUrls: [ - 'https://images.unsplash.com/photo-1533174072545-7a4b6ad7a6c3?w=1200&q=80', - 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200&q=80', - ], - }, - '3': { - id: '3', name: 'Garden Pavilion', category: 'Outdoor', - address: '34 NH Bypass', district: 'Kozhikode', - capacity: 300, pricePerSlot: 18000, status: VenueStatus.APPROVED, - description: 'A lush outdoor garden pavilion surrounded by manicured lawns and tropical foliage. The venue includes a covered pavilion, open-air stage, and ambient garden lighting. Perfect for outdoor weddings, cultural events, and family celebrations.', - imageUrls: [ - 'https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?w=1200&q=80', - 'https://images.unsplash.com/photo-1519167758481-83f550bb49b3?w=1200&q=80', - 'https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=1200&q=80', - ], - }, - '4': { - id: '4', name: 'Heritage Haveli', category: 'Heritage', - address: '5 Fort Road', district: 'Thiruvananthapuram', - capacity: 200, pricePerSlot: 22000, status: VenueStatus.APPROVED, - description: 'A restored 19th-century heritage building featuring traditional Kerala architecture, ornate woodwork, and antique furnishings. The venue offers a unique blend of history and luxury, complete with a traditional courtyard, pillared halls, and a serene garden.', - imageUrls: [ - 'https://images.unsplash.com/photo-1505236858219-8359eb29e329?w=1200&q=80', - 'https://images.unsplash.com/photo-1542314831-068cd1dbfeeb?w=1200&q=80', - ], - }, - '5': { - id: '5', name: 'The Conference Hub', category: 'Conference', - address: '17 Technopark', district: 'Thiruvananthapuram', - capacity: 80, pricePerSlot: 8000, status: VenueStatus.APPROVED, - description: 'A modern conference centre equipped with the latest AV technology, high-speed Wi-Fi, and ergonomic seating. Includes a main auditorium, breakout rooms, and a catering lounge. Ideal for corporate meetings, seminars, workshops, and training sessions.', - imageUrls: [ - 'https://images.unsplash.com/photo-1517457373958-b7bdd4587205?w=1200&q=80', - ], - }, - '6': { - id: '6', name: 'Lakeside Retreat', category: 'Resort', - address: '2 Lake View Road', district: 'Alappuzha', - capacity: 250, pricePerSlot: 20000, status: VenueStatus.APPROVED, - description: 'A stunning lakeside resort venue with private waterfront access, infinity pool, and lush gardens. Features a grand event lawn, covered banquet hall, and floating stage on the lake. The perfect backdrop for destination weddings and luxury private events.', - imageUrls: [ - 'https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=1200&q=80', - 'https://images.unsplash.com/photo-1505236858219-8359eb29e329?w=1200&q=80', - 'https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?w=1200&q=80', - ], - }, - }; - // ──────────────────────────────────────────────────────────────────────── - ngOnInit(): void { const id = this.route.snapshot.paramMap.get('id') ?? ''; - - // SWITCH: use dummyVenues for design, venueService for real data - // const venue = this.dummyVenues[id] ?? null; - // this.venue.set(venue); - // this.loading.set(false); - - // Uncomment below and remove the two lines above when switching to real data: if (id) { this.venueService.loadVenueById(id).subscribe({ next: (venue) => { this.venue.set(venue); this.loading.set(false); }, @@ -107,14 +32,10 @@ export class VenueDetailsComponent implements OnInit { } } - selectImage(index: number): void { - this.selectedImage.set(index); - } + selectImage(index: number): void { this.selectedImage.set(index); } onBookNow(): void { const venue = this.venue(); - if (venue) { - this.router.navigate(['/user/checkout', venue.id]); - } + if (venue) this.router.navigate(['/user/checkout', venue.id]); } } diff --git a/client/src/app/user/features/venue-list/venue-list.component.ts b/client/src/app/user/features/venue-list/venue-list.component.ts index fdbe7d6ac..4f015519a 100644 --- a/client/src/app/user/features/venue-list/venue-list.component.ts +++ b/client/src/app/user/features/venue-list/venue-list.component.ts @@ -3,8 +3,6 @@ import { RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { VenueService } from '../../services/venue.service'; import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; -import { Venue } from '../../../shared/models/venue.model'; -import { VenueStatus } from '../../../shared/enums/venue-status.enum'; @Component({ selector: 'app-venue-list', @@ -17,27 +15,11 @@ import { VenueStatus } from '../../../shared/enums/venue-status.enum'; export class VenueListComponent implements OnInit { private readonly venueService = inject(VenueService); - // ── DUMMY DATA (comment out venues = dummyVenues and uncomment service line to use real data) ── - readonly dummyVenues = signal([ - { id: '1', name: 'The Grand Ballroom', description: '', address: '12 Marine Drive', district: 'Ernakulam', capacity: 500, pricePerSlot: 25000, category: 'Banquet Hall', status: VenueStatus.APPROVED, imageUrls: ['https://images.unsplash.com/photo-1519167758481-83f550bb49b3?w=600&q=80'] }, - { id: '2', name: 'Skyline Rooftop Terrace', description: '', address: '8 MG Road', district: 'Thrissur', capacity: 150, pricePerSlot: 12000, category: 'Rooftop', status: VenueStatus.APPROVED, imageUrls: ['https://images.unsplash.com/photo-1533174072545-7a4b6ad7a6c3?w=600&q=80'] }, - { id: '3', name: 'Garden Pavilion', description: '', address: '34 NH Bypass', district: 'Kozhikode', capacity: 300, pricePerSlot: 18000, category: 'Outdoor', status: VenueStatus.APPROVED, imageUrls: ['https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?w=600&q=80'] }, - { id: '4', name: 'Heritage Haveli', description: '', address: '5 Fort Road', district: 'Thiruvananthapuram', capacity: 200, pricePerSlot: 22000, category: 'Heritage', status: VenueStatus.APPROVED, imageUrls: ['https://images.unsplash.com/photo-1505236858219-8359eb29e329?w=600&q=80'] }, - { id: '5', name: 'The Conference Hub', description: '', address: '17 Technopark', district: 'Thiruvananthapuram', capacity: 80, pricePerSlot: 8000, category: 'Conference', status: VenueStatus.APPROVED, imageUrls: ['https://images.unsplash.com/photo-1517457373958-b7bdd4587205?w=600&q=80'] }, - { id: '6', name: 'Lakeside Retreat', description: '', address: '2 Lake View Road', district: 'Alappuzha', capacity: 250, pricePerSlot: 20000, category: 'Resort', status: VenueStatus.APPROVED, imageUrls: ['https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=600&q=80'] }, - ]); - - // SWITCH: use dummyVenues for design, venueService.venues for real data - // readonly venues = this.dummyVenues; readonly venues = this.venueService.venues; - - // readonly loading = signal(false); readonly loading = this.venueService.loading; - readonly searchQuery = signal(''); ngOnInit(): void { - // Comment this in when switching to real data: this.venueService.loadVenues(); } diff --git a/client/src/app/user/repositories/booking.repository.ts b/client/src/app/user/repositories/booking.repository.ts index 9353cdfbd..9cd98e440 100644 --- a/client/src/app/user/repositories/booking.repository.ts +++ b/client/src/app/user/repositories/booking.repository.ts @@ -33,17 +33,17 @@ export class BookingRepository { ); } - createBooking(payload: CreateBookingRequest): Observable> { - return this.http.post>( + createBooking(payload: CreateBookingRequest): Observable { + return this.http.post( `${this.apiUrl}${API_ENDPOINTS.BOOKINGS.CREATE_WITH_SLOT(payload.venueId, payload.slotTemplateId)}`, payload ); } - cancelBooking(id: string): Observable> { - return this.http.post>( + cancelBooking(id: string): Observable { + return this.http.post( `${this.apiUrl}${API_ENDPOINTS.BOOKINGS.CANCEL(id)}`, - { status: 'cancelled' } + {} ); } } diff --git a/client/src/app/user/repositories/payment.repository.ts b/client/src/app/user/repositories/payment.repository.ts new file mode 100644 index 000000000..55ae1dcfc --- /dev/null +++ b/client/src/app/user/repositories/payment.repository.ts @@ -0,0 +1,26 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { PaymentOrder, VerifyPaymentRequest } from '../../shared/models/booking.model'; + +@Injectable({ providedIn: 'root' }) +export class PaymentRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + createPaymentOrder(bookingId: number): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.PAYMENTS.CREATE(bookingId)}`, + {} + ); + } + + verifyPayment(request: VerifyPaymentRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.PAYMENTS.VERIFY}`, + request + ); + } +} diff --git a/client/src/app/user/services/booking.service.ts b/client/src/app/user/services/booking.service.ts index 3732df83f..a0e791112 100644 --- a/client/src/app/user/services/booking.service.ts +++ b/client/src/app/user/services/booking.service.ts @@ -3,6 +3,8 @@ import { Router } from '@angular/router'; import { BookingRepository } from '../repositories/booking.repository'; import { Booking, CreateBookingRequest } from '../../shared/models/booking.model'; import { NotificationService } from '../../shared/services/notification.service'; +import { Observable } from 'rxjs'; +import { BookingStatus } from '../../shared/enums'; @Injectable({ providedIn: 'root' }) export class BookingService { @@ -16,40 +18,24 @@ export class BookingService { loadUserBookings(): void { this.loading.set(true); this.bookingRepository.getUserBookings().subscribe({ - next: (response) => { - this.bookings.set(response.data); - this.loading.set(false); - }, - error: () => { - this.notification.error('Failed to load bookings'); - this.loading.set(false); - }, + next: (response) => { this.bookings.set(response.data); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load bookings'); this.loading.set(false); }, }); } - createBooking(payload: CreateBookingRequest): void { - this.bookingRepository.createBooking(payload).subscribe({ - next: () => { - this.notification.success('Booking created successfully!'); - this.router.navigate(['/user/my-bookings']); - }, - error: () => { - this.notification.error('Failed to create booking'); - }, - }); + createBooking(payload: CreateBookingRequest): Observable { + return this.bookingRepository.createBooking(payload); } cancelBooking(id: number): void { this.bookingRepository.cancelBooking(String(id)).subscribe({ next: () => { this.bookings.update(list => - list.map(b => b.id === id ? { ...b, status: 'cancelled' as Booking['status'] } : b) + list.map(b => b.id === id ? { ...b, status: BookingStatus.CANCELLED } : b) ); this.notification.success('Booking cancelled'); }, - error: () => { - this.notification.error('Failed to cancel booking'); - }, + error: () => this.notification.error('Failed to cancel booking'), }); } -} \ No newline at end of file +} diff --git a/client/src/app/vendor/features/bookings/bookings.component.html b/client/src/app/vendor/features/bookings/bookings.component.html index 18b21e9bd..d38553cfd 100644 --- a/client/src/app/vendor/features/bookings/bookings.component.html +++ b/client/src/app/vendor/features/bookings/bookings.component.html @@ -30,7 +30,7 @@

Bookings

}} - @if (booking.status === BookingStatus.Pending) { + @if (booking.status === BookingStatus.PENDING) {
diff --git a/client/src/app/vendor/features/slot-management/slot-management.component.css b/client/src/app/vendor/features/slot-management/slot-management.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/vendor/features/slot-management/slot-management.component.html b/client/src/app/vendor/features/slot-management/slot-management.component.html new file mode 100644 index 000000000..71d9b13f5 --- /dev/null +++ b/client/src/app/vendor/features/slot-management/slot-management.component.html @@ -0,0 +1,78 @@ +
+

Manage Slots

+ + +
+

Add New Slot Template

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + + @if (slotService.loading()) { + + } @else if (slotService.slots().length === 0) { + + } @else { +
+ + + + + + + + + + + + @for (slot of slotService.slots(); track slot.id) { + + + + + + + + } + +
DayStartEndStatusAction
{{ slot.dayOfWeek }}{{ slot.startTime }}{{ slot.endTime }} + + {{ slot.active ? 'Active' : 'Inactive' }} + + + +
+
+ } +
diff --git a/client/src/app/vendor/features/slot-management/slot-management.component.spec.ts b/client/src/app/vendor/features/slot-management/slot-management.component.spec.ts new file mode 100644 index 000000000..d1c738af5 --- /dev/null +++ b/client/src/app/vendor/features/slot-management/slot-management.component.spec.ts @@ -0,0 +1,28 @@ +/* tslint:disable:no-unused-variable */ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { SlotManagementComponent } from './slot-management.component'; + +describe('SlotManagementComponent', () => { + let component: SlotManagementComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SlotManagementComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SlotManagementComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/vendor/features/slot-management/slot-management.component.ts b/client/src/app/vendor/features/slot-management/slot-management.component.ts new file mode 100644 index 000000000..7a3e4359e --- /dev/null +++ b/client/src/app/vendor/features/slot-management/slot-management.component.ts @@ -0,0 +1,45 @@ +import { Component, inject, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { VendorSlotService } from '../../services/slot.service'; +import { LoaderComponent } from '../../../shared/components/loader/loader.component'; +import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component'; + +const DAYS = ['MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY','SUNDAY']; + +@Component({ + selector: 'app-slot-management', + standalone: true, + imports: [ReactiveFormsModule, LoaderComponent, EmptyStateComponent], + templateUrl: './slot-management.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SlotManagementComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + private readonly fb = inject(FormBuilder); + readonly slotService = inject(VendorSlotService); + + readonly venueId = signal(''); + readonly days = DAYS; + + readonly form = this.fb.nonNullable.group({ + dayOfWeek: ['', Validators.required], + startTime: ['', Validators.required], + endTime: ['', Validators.required], + }); + + ngOnInit(): void { + const id = this.route.snapshot.paramMap.get('id') ?? ''; + this.venueId.set(id); + this.slotService.loadSlots(id); + } + + onSubmit(): void { + if (this.form.invalid) return; + this.slotService.createSlot(this.venueId(), this.form.getRawValue(), () => this.form.reset()); + } + + deleteSlot(id: number): void { + this.slotService.deleteSlot(id); + } +} diff --git a/client/src/app/vendor/features/venue-list/venue-list.component.html b/client/src/app/vendor/features/venue-list/venue-list.component.html index 95bcb34fb..860249ec3 100644 --- a/client/src/app/vendor/features/venue-list/venue-list.component.html +++ b/client/src/app/vendor/features/venue-list/venue-list.component.html @@ -32,6 +32,7 @@

{{ venue.name }}

Edit + Slots
diff --git a/client/src/app/vendor/repositories/slot.repository.ts b/client/src/app/vendor/repositories/slot.repository.ts new file mode 100644 index 000000000..08a96795c --- /dev/null +++ b/client/src/app/vendor/repositories/slot.repository.ts @@ -0,0 +1,31 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../../shared/constants/api-endpoints.constant'; +import { SlotTemplate, CreateSlotTemplateRequest } from '../../shared/models/slot.model'; + +@Injectable({ providedIn: 'root' }) +export class VendorSlotRepository { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getSlots(venueId: string): Observable { + return this.http.get( + `${this.apiUrl}${API_ENDPOINTS.VENUES.SLOTS_BY_VENUE(venueId)}` + ); + } + + createSlot(venueId: string, payload: CreateSlotTemplateRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.VENUES.SLOTS_BY_VENUE(venueId)}`, + payload + ); + } + + deleteSlot(templateId: number): Observable { + return this.http.delete( + `${this.apiUrl}${API_ENDPOINTS.VENUES.DELETE_SLOT(templateId)}` + ); + } +} diff --git a/client/src/app/vendor/routes/vendor.routes.ts b/client/src/app/vendor/routes/vendor.routes.ts index 70a1f5863..afad29c48 100644 --- a/client/src/app/vendor/routes/vendor.routes.ts +++ b/client/src/app/vendor/routes/vendor.routes.ts @@ -18,6 +18,7 @@ export const VENDOR_ROUTES: Routes = [ { path: ROUTE_PATHS.VENDOR.BOOKINGS, loadComponent: () => import('../features/bookings/bookings.component').then(m => m.BookingsComponent) }, { path: ROUTE_PATHS.VENDOR.ANALYTICS, loadComponent: () => import('../features/analytics/analytics.component').then(m => m.AnalyticsComponent) }, { path: ROUTE_PATHS.VENDOR.SETTINGS, loadComponent: () => import('../features/settings/settings.component').then(m => m.SettingsComponent) }, + { path: ROUTE_PATHS.VENDOR.VENUE_SLOTS, loadComponent: () => import('../features/slot-management/slot-management.component').then(m => m.SlotManagementComponent) }, { path: '', redirectTo: ROUTE_PATHS.VENDOR.DASHBOARD, pathMatch: 'full' }, ], }, diff --git a/client/src/app/vendor/services/slot.service.ts b/client/src/app/vendor/services/slot.service.ts new file mode 100644 index 000000000..444c9e072 --- /dev/null +++ b/client/src/app/vendor/services/slot.service.ts @@ -0,0 +1,48 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { VendorSlotRepository } from '../repositories/slot.repository'; +import { SlotTemplate, CreateSlotTemplateRequest } from '../../shared/models/slot.model'; +import { NotificationService } from '../../shared/services/notification.service'; + +@Injectable({ providedIn: 'root' }) +export class VendorSlotService { + private readonly repo = inject(VendorSlotRepository); + private readonly notification = inject(NotificationService); + + readonly slots = signal([]); + readonly loading = signal(false); + readonly saving = signal(false); + readonly deletingId = signal(null); + + loadSlots(venueId: string): void { + this.loading.set(true); + this.repo.getSlots(venueId).subscribe({ + next: (slots) => { this.slots.set(slots); this.loading.set(false); }, + error: () => { this.notification.error('Failed to load slots'); this.loading.set(false); }, + }); + } + + createSlot(venueId: string, payload: CreateSlotTemplateRequest, onDone: () => void): void { + this.saving.set(true); + this.repo.createSlot(venueId, payload).subscribe({ + next: (slot) => { + this.slots.update(list => [...list, slot]); + this.notification.success('Slot created'); + this.saving.set(false); + onDone(); + }, + error: () => { this.notification.error('Failed to create slot'); this.saving.set(false); }, + }); + } + + deleteSlot(templateId: number): void { + this.deletingId.set(templateId); + this.repo.deleteSlot(templateId).subscribe({ + next: () => { + this.slots.update(list => list.filter(s => s.id !== templateId)); + this.notification.success('Slot deleted'); + this.deletingId.set(null); + }, + error: () => { this.notification.error('Failed to delete slot'); this.deletingId.set(null); }, + }); + } +} diff --git a/client/src/index.html b/client/src/index.html index d722d786f..e575830e7 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -9,6 +9,7 @@ + From aebebc37973d53c3d5176be7f6bab190a0dcb0f3 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Sat, 27 Jun 2026 16:38:08 +0530 Subject: [PATCH 47/59] added user module --- .../user/controller/UserController.java | 27 +++++++++++++++++ .../dto/response/UserProfileResponse.java | 18 ++++++++++++ .../server/user/service/UserService.java | 9 ++++++ .../server/user/service/UserServiceImpl.java | 29 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 server/src/main/java/com/bookmyvenue/server/user/controller/UserController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/dto/response/UserProfileResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/service/UserService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/user/service/UserServiceImpl.java diff --git a/server/src/main/java/com/bookmyvenue/server/user/controller/UserController.java b/server/src/main/java/com/bookmyvenue/server/user/controller/UserController.java new file mode 100644 index 000000000..d193424a4 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/controller/UserController.java @@ -0,0 +1,27 @@ +package com.bookmyvenue.server.user.controller; + +import com.bookmyvenue.server.user.dto.response.UserProfileResponse; +import com.bookmyvenue.server.user.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +@PreAuthorize("isAuthenticated()") +public class UserController { + + private final UserService userService; + + @Operation(summary = "Get user profile", description = "Returns the profile details of the authenticated user.") + @ApiResponse(responseCode = "200", description = "Profile retrieved successfully") + @GetMapping("/profile") + public UserProfileResponse getProfile() { + return userService.getCurrentUserProfile(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/user/dto/response/UserProfileResponse.java b/server/src/main/java/com/bookmyvenue/server/user/dto/response/UserProfileResponse.java new file mode 100644 index 000000000..1b3c82aac --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/dto/response/UserProfileResponse.java @@ -0,0 +1,18 @@ +package com.bookmyvenue.server.user.dto.response; + +import com.bookmyvenue.server.user.enums.Role; +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Builder +public record UserProfileResponse( + UUID id, + String name, + String email, + String phone, + Role role, + LocalDateTime createdAt +) { +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/user/service/UserService.java b/server/src/main/java/com/bookmyvenue/server/user/service/UserService.java new file mode 100644 index 000000000..4c12d9fd7 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/service/UserService.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.server.user.service; + +import com.bookmyvenue.server.user.dto.response.UserProfileResponse; + +public interface UserService { + + UserProfileResponse getCurrentUserProfile(); + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/user/service/UserServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/user/service/UserServiceImpl.java new file mode 100644 index 000000000..1536d5da8 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/user/service/UserServiceImpl.java @@ -0,0 +1,29 @@ +package com.bookmyvenue.server.user.service; + +import com.bookmyvenue.server.auth.service.AuthenticatedUserService; +import com.bookmyvenue.server.user.dto.response.UserProfileResponse; +import com.bookmyvenue.server.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final AuthenticatedUserService authenticatedUserService; + + @Override + public UserProfileResponse getCurrentUserProfile() { + + User user = authenticatedUserService.getCurrentUser(); + + return UserProfileResponse.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .phone(user.getPhone()) + .role(user.getRole()) + .createdAt(user.getCreatedAt()) + .build(); + } +} \ No newline at end of file From 3f226a1424250b416a1588e499db4c55cc7a5d63 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Sun, 28 Jun 2026 13:38:54 +0530 Subject: [PATCH 48/59] integrated razorpay successfully --- client/src/app/core/config/environment.ts | 1 + .../constants/api-endpoints.constant.ts | 6 +-- .../shared/constants/route-paths.constant.ts | 1 + client/src/app/shared/models/user.model.ts | 11 ++-- .../booking-detail.component.html | 52 +++++++++++++++++++ .../booking-detail.component.ts | 49 +++++++++++++++++ .../features/checkout/checkout.component.ts | 3 +- .../features/profile/profile.component.html | 20 +++---- .../features/profile/profile.component.ts | 6 +-- client/src/app/user/routes/user.routes.ts | 6 +++ 10 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 client/src/app/user/features/booking-detail/booking-detail.component.html create mode 100644 client/src/app/user/features/booking-detail/booking-detail.component.ts diff --git a/client/src/app/core/config/environment.ts b/client/src/app/core/config/environment.ts index ee79ab393..2c1d93ceb 100644 --- a/client/src/app/core/config/environment.ts +++ b/client/src/app/core/config/environment.ts @@ -3,6 +3,7 @@ export const environment = { apiUrl: 'http://localhost:8080/api', tokenKey: 'bmv_token', refreshTokenKey: 'bmv_refresh_token', + razorpayKeyId: 'rzp_test_T3DBrC7nq10MPn', cloudinary:{ cloudName:'dbigsiz1p', uploadPreset:'book_my_venue', diff --git a/client/src/app/shared/constants/api-endpoints.constant.ts b/client/src/app/shared/constants/api-endpoints.constant.ts index 4ff0951c5..3ac75a144 100644 --- a/client/src/app/shared/constants/api-endpoints.constant.ts +++ b/client/src/app/shared/constants/api-endpoints.constant.ts @@ -9,9 +9,9 @@ export const API_ENDPOINTS = { ADMIN_LOGIN: '/auth/login', }, USERS: { - BASE: '/users', - PROFILE: '/users/profile', - BY_ID: (id: string) => `/users/${id}`, + BASE: '/v1/users', + PROFILE: '/v1/users/profile', + BY_ID: (id: string) => `/v1/users/${id}`, }, VENDORS: { BASE: '/vendors', diff --git a/client/src/app/shared/constants/route-paths.constant.ts b/client/src/app/shared/constants/route-paths.constant.ts index cd2d64ddd..28413d787 100644 --- a/client/src/app/shared/constants/route-paths.constant.ts +++ b/client/src/app/shared/constants/route-paths.constant.ts @@ -9,6 +9,7 @@ export const ROUTE_PATHS = { VENUE_DETAILS: 'venues/:id', CHECKOUT: 'checkout/:venueId', MY_BOOKINGS: 'my-bookings', + MY_BOOKING_DETAIL: 'my-bookings/:id', PROFILE: 'profile', }, VENDOR: { diff --git a/client/src/app/shared/models/user.model.ts b/client/src/app/shared/models/user.model.ts index 6ddb8c6c6..8172a8283 100644 --- a/client/src/app/shared/models/user.model.ts +++ b/client/src/app/shared/models/user.model.ts @@ -1,13 +1,10 @@ export interface User { id: string; + name: string; email: string; - firstName: string; - lastName: string; phone: string; - avatar: string; - isActive: boolean; + role: string; createdAt: string; - updatedAt: string; } export interface CreateUserRequest { @@ -19,8 +16,6 @@ export interface CreateUserRequest { } export interface UpdateUserRequest { - firstName?: string; - lastName?: string; + name?: string; phone?: string; - avatar?: string; } diff --git a/client/src/app/user/features/booking-detail/booking-detail.component.html b/client/src/app/user/features/booking-detail/booking-detail.component.html new file mode 100644 index 000000000..d028b6785 --- /dev/null +++ b/client/src/app/user/features/booking-detail/booking-detail.component.html @@ -0,0 +1,52 @@ +
+ + + @if (booking()) { +
+
+

Booking #{{ booking()!.id }}

+ + {{ booking()!.status }} + +
+ +
+
+

Booking Date

+

{{ booking()!.bookingDate | dateFormat }}

+
+
+

Venue ID

+

{{ booking()!.venueId }}

+
+
+

Slot ID

+

{{ booking()!.slotTemplateId }}

+
+
+

Total Amount

+

{{ booking()!.totalAmount | currencyFormat }}

+
+ @if (booking()!.expiresAt) { +
+

Expires At

+

{{ booking()!.expiresAt | dateFormat }}

+
+ } +
+ + @if (booking()!.status === BookingStatus.CONFIRMED || booking()!.status === BookingStatus.PENDING) { +
+ +
+ } +
+ } @else { +
Booking not found.
+ } +
diff --git a/client/src/app/user/features/booking-detail/booking-detail.component.ts b/client/src/app/user/features/booking-detail/booking-detail.component.ts new file mode 100644 index 000000000..3191712ad --- /dev/null +++ b/client/src/app/user/features/booking-detail/booking-detail.component.ts @@ -0,0 +1,49 @@ +import { Component, inject, computed, ChangeDetectionStrategy } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BookingService } from '../../services/booking.service'; +import { BookingStatus } from '../../../shared/enums/booking-status.enum'; +import { DateFormatPipe } from '../../../shared/pipes/date-format.pipe'; +import { CurrencyFormatPipe } from '../../../shared/pipes/currency-format.pipe'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; + +@Component({ + selector: 'app-booking-detail', + standalone: true, + imports: [DateFormatPipe, CurrencyFormatPipe, ButtonComponent], + templateUrl: './booking-detail.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BookingDetailComponent { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly bookingService = inject(BookingService); + + readonly BookingStatus = BookingStatus; + + readonly booking = computed(() => { + const id = Number(this.route.snapshot.paramMap.get('id')); + return this.bookingService.bookings().find(b => b.id === id) ?? null; + }); + + getStatusClass(status: string): string { + switch (status) { + case BookingStatus.CONFIRMED: return 'bg-green-100 text-green-700'; + case BookingStatus.PENDING: return 'bg-yellow-100 text-yellow-700'; + case BookingStatus.CANCELLED: return 'bg-red-100 text-red-700'; + case BookingStatus.EXPIRED: return 'bg-red-100 text-red-700'; + default: return 'bg-gray-100 text-gray-700'; + } + } + + goBack(): void { + this.router.navigate(['/user/my-bookings']); + } + + onCancel(): void { + const booking = this.booking(); + if (booking) { + this.bookingService.cancelBooking(booking.id); + this.router.navigate(['/user/my-bookings']); + } + } +} diff --git a/client/src/app/user/features/checkout/checkout.component.ts b/client/src/app/user/features/checkout/checkout.component.ts index aede8d383..e7450edbe 100644 --- a/client/src/app/user/features/checkout/checkout.component.ts +++ b/client/src/app/user/features/checkout/checkout.component.ts @@ -13,6 +13,7 @@ import { SlotService } from '../../services/slot.services'; import { TimeSlot } from '../../../shared/models/slot.model'; import { CreateBookingRequest, VerifyPaymentRequest } from '../../../shared/models/booking.model'; import { NotificationService } from '../../../shared/services/notification.service'; +import { environment } from '../../../core/config/environment'; declare var Razorpay: any; @@ -124,7 +125,7 @@ export class CheckoutComponent implements OnInit { this.paymentRepository.createPaymentOrder(bookingId).subscribe({ next: (order) => { const options = { - key: '', // Leave empty — Razorpay key comes from backend via order + key: environment.razorpayKeyId, amount: order.amount * 100, currency: 'INR', order_id: order.razorpayOrderId, diff --git a/client/src/app/user/features/profile/profile.component.html b/client/src/app/user/features/profile/profile.component.html index d5fda1dee..f813c0244 100644 --- a/client/src/app/user/features/profile/profile.component.html +++ b/client/src/app/user/features/profile/profile.component.html @@ -16,20 +16,12 @@

{{ authService.currentUser()?.na
-
- - -
+ { this.form.patchValue({ - firstName: user.firstName, - lastName: user.lastName, + name: user.name, phone: user.phone, }); this.loading.set(false); diff --git a/client/src/app/user/routes/user.routes.ts b/client/src/app/user/routes/user.routes.ts index 3facbbd58..818a79ead 100644 --- a/client/src/app/user/routes/user.routes.ts +++ b/client/src/app/user/routes/user.routes.ts @@ -30,6 +30,12 @@ export const USER_ROUTES: Routes = [ loadComponent: () => import('../features/my-bookings/my-bookings.component').then(m => m.MyBookingsComponent), }, + { + path: ROUTE_PATHS.USER.MY_BOOKING_DETAIL, + canActivate: [userAuthGuard], + loadComponent: () => + import('../features/booking-detail/booking-detail.component').then(m => m.BookingDetailComponent), + }, { path: ROUTE_PATHS.USER.PROFILE, canActivate: [userAuthGuard], From 38c89fd8222089794db48ca552ce563689fc55ed Mon Sep 17 00:00:00 2001 From: Anzyll Date: Mon, 29 Jun 2026 15:26:38 +0530 Subject: [PATCH 49/59] feat(venue): add Cloudinary image upload, category API, and vendor ownership checks --- client/package-lock.json | 59 ++++++++-------- .../security/JwtAuthenticationFilter.java | 1 - .../server/auth/security/SecurityConfig.java | 3 +- .../service/AvailabilityServiceImpl.java | 2 - .../controller/VenueCategoryController.java | 24 +++++++ .../venue/controller/VenueController.java | 1 + .../dto/response/VenueCategoryResponse.java | 17 +++++ .../server/venue/entity/Venue.java | 1 + .../server/venue/entity/VenueImage.java | 1 + .../server/venue/mapper/VenueMapper.java | 7 ++ .../repository/VenueCategoryRepository.java | 3 + .../repository/VenueImageRepository.java | 9 +++ .../venue/repository/VenueRepository.java | 5 ++ .../venue/service/VenueCategoryService.java | 10 +++ .../service/VenueCategoryServiceImpl.java | 31 +++++++++ .../venue/service/VenueServiceImpl.java | 68 +++++++++---------- .../src/main/resources/application.properties | 3 +- .../migration/V7__seed_venue_categories.sql | 14 ++++ 18 files changed, 187 insertions(+), 72 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/controller/VenueCategoryController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueCategoryResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/repository/VenueImageRepository.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/service/VenueCategoryService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/venue/service/VenueCategoryServiceImpl.java create mode 100644 server/src/main/resources/db/migration/V7__seed_venue_categories.sql diff --git a/client/package-lock.json b/client/package-lock.json index e8cecbbf9..31b966c53 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -541,6 +541,7 @@ "integrity": "sha512-oLEL1C1fI39b1eQo5f2cyQhQfE+QMv7dm8z2MmxbP7YR7jAdQPVfGU8CXECR5g7mrYi9WgvIRKB+9Oeq2aH6Jw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@angular-devkit/architect": "0.2102.12", "@angular-devkit/core": "21.2.12", @@ -575,6 +576,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.14.tgz", "integrity": "sha512-J6K7cE7uKOKmg4+sxLeGfsmaYDjP5l1XCiMMI0WPT0t68uxLk8g3MzV5Trqfb6ZnRxWcfp9c4c+XxAvMBB7ymA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -591,6 +593,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.14.tgz", "integrity": "sha512-8mqgwRYfn2Z1vg/5YVt60dDBattnZL45nNJd2vTMwAiDTzhWhgKgRWKOeVL0aj2JqHeHiwuIlrLnz46acJMulQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -604,6 +607,7 @@ "integrity": "sha512-h+WQfPKFxaDfDhMqUUdOQ1TsDMccav8kLFERmKTRfD4MNOczSMpOMyeXJHCL0Rq4I8WDQvaBJGMG7DXRDefSog==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.29.0", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -636,6 +640,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.14.tgz", "integrity": "sha512-Z1Ivjh7L2lT//8LA7vQ3tj7Rg6wl2XRA5kPSAukgn8u0Yu0XxG8NE8KG0Eypb3v9CEcbwATwpgnxzbJFZ8TFcw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -680,6 +685,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.14.tgz", "integrity": "sha512-34tBwxh86yN2YifBDhCesm6N+nn9WcbuXjRwfo0mTme15OZ/zt56yw7v1mcK3UFLegIIALtsIgpXXrPWWQoKkA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -804,6 +810,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1167,6 +1174,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1215,33 +1223,11 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -2140,6 +2126,7 @@ "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", @@ -4477,7 +4464,6 @@ "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.0", "@typescript-eslint/types": "^8.60.0", @@ -4500,7 +4486,6 @@ "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" @@ -4519,7 +4504,6 @@ "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4552,7 +4536,6 @@ "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/project-service": "8.60.0", "@typescript-eslint/tsconfig-utils": "8.60.0", @@ -4606,7 +4589,6 @@ "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" @@ -4625,7 +4607,6 @@ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, @@ -4803,6 +4784,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5082,6 +5064,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5233,6 +5216,7 @@ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^5.0.0" }, @@ -5887,6 +5871,7 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -6166,6 +6151,7 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6572,6 +6558,7 @@ "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -6941,6 +6928,7 @@ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", @@ -7348,6 +7336,7 @@ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -8531,6 +8520,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -8854,6 +8844,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -9365,7 +9356,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.3", @@ -9525,7 +9517,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "4.1.0", @@ -9594,6 +9587,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9689,6 +9683,7 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9764,6 +9759,7 @@ "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.7", "@vitest/mocker": "4.1.7", @@ -10179,6 +10175,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/JwtAuthenticationFilter.java b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtAuthenticationFilter.java index 55406c6ae..fae915227 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/security/JwtAuthenticationFilter.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/JwtAuthenticationFilter.java @@ -26,7 +26,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - try { // Extract access token from cookie diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java index 4f78e7893..dab154ffe 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -53,7 +53,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**", - "/api/v1/venues/**" + "/api/v1/venues/**", + "/api/v1/categories/**" ).permitAll() // Everything else requires authentication .anyRequest().authenticated() diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java index 77bf35f71..e877b14dd 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/AvailabilityServiceImpl.java @@ -35,13 +35,11 @@ public List getAvailability( Long venueId, LocalDate date ) { - log.info( "Fetching availability for venueId={} date={}", venueId, date ); - Venue venue = venueRepository.findById(venueId) .orElseThrow(() -> new BusinessException( diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueCategoryController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueCategoryController.java new file mode 100644 index 000000000..3c0c90442 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueCategoryController.java @@ -0,0 +1,24 @@ +package com.bookmyvenue.server.venue.controller; + + +import com.bookmyvenue.server.venue.dto.response.VenueCategoryResponse; +import com.bookmyvenue.server.venue.service.VenueCategoryService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/categories") +@RequiredArgsConstructor +public class VenueCategoryController { + + private final VenueCategoryService venueCategoryService; + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public List getCategories() { + return venueCategoryService.getCategories(); + } +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java index b4d7ef237..9151b8273 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/controller/VenueController.java @@ -28,6 +28,7 @@ public class VenueController { public VenueResponse createVenue( @RequestBody CreateVenueRequest request ) { + System.out.println("CREATE VENUE CONTROLLER HIT"); return venueService.createVenue(request); } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueCategoryResponse.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueCategoryResponse.java new file mode 100644 index 000000000..f424c1ecb --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueCategoryResponse.java @@ -0,0 +1,17 @@ +package com.bookmyvenue.server.venue.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VenueCategoryResponse { + + private Long id; + private String name; + private String description; +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java b/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java index 09ecda7cb..b3a0bc51e 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java @@ -59,6 +59,7 @@ public class Venue { cascade = CascadeType.ALL, orphanRemoval = true ) + @Builder.Default private List images = new ArrayList<>(); @CreationTimestamp diff --git a/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueImage.java b/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueImage.java index 0a1e9799e..2f670cdb5 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueImage.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/entity/VenueImage.java @@ -26,6 +26,7 @@ public class VenueImage { @Column(name = "image_url", nullable = false, length = 1000) private String imageUrl; + @Builder.Default @Column(name = "is_primary", nullable = false) private Boolean isPrimary = false; diff --git a/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java b/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java index cf01a8899..6add80020 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java @@ -2,6 +2,7 @@ import com.bookmyvenue.server.venue.dto.response.VenueResponse; import com.bookmyvenue.server.venue.entity.Venue; +import com.bookmyvenue.server.venue.entity.VenueImage; import org.springframework.stereotype.Component; import java.util.List; @@ -20,6 +21,12 @@ public VenueResponse toResponse(Venue venue) { .pricePerSlot(venue.getPricePerSlot()) .category(venue.getCategory().getName()) .status(venue.getStatus()) + .imageUrls( + venue.getImages() + .stream() + .map(VenueImage::getImageUrl) + .toList() + ) .build(); } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueCategoryRepository.java b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueCategoryRepository.java index f4cfc55a7..3e917dbf4 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueCategoryRepository.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueCategoryRepository.java @@ -4,6 +4,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface VenueCategoryRepository extends JpaRepository { + List findAllByOrderByNameAsc(); } diff --git a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueImageRepository.java b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueImageRepository.java new file mode 100644 index 000000000..8d4971913 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueImageRepository.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.server.venue.repository; + +import com.bookmyvenue.server.venue.entity.VenueImage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface VenueImageRepository extends JpaRepository { +} diff --git a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java index 63a913242..e210d1363 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/repository/VenueRepository.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; public interface VenueRepository extends JpaRepository { List findByStatus(VenueStatus status); @@ -35,6 +36,10 @@ List findByStatusAndNameContainingIgnoreCase( String keyword ); + List findByOwnerId(UUID ownerId); + + Optional findByIdAndOwnerId(Long id, UUID ownerId); + long countByStatus(VenueStatus status); diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueCategoryService.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueCategoryService.java new file mode 100644 index 000000000..a8fa44d2d --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueCategoryService.java @@ -0,0 +1,10 @@ +package com.bookmyvenue.server.venue.service; + +import com.bookmyvenue.server.venue.dto.response.VenueCategoryResponse; + +import java.util.List; + +public interface VenueCategoryService { + + List getCategories(); +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueCategoryServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueCategoryServiceImpl.java new file mode 100644 index 000000000..f7eff23c5 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueCategoryServiceImpl.java @@ -0,0 +1,31 @@ +package com.bookmyvenue.server.venue.service; + +import com.bookmyvenue.server.venue.dto.response.VenueCategoryResponse; +import com.bookmyvenue.server.venue.repository.VenueCategoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class VenueCategoryServiceImpl + implements VenueCategoryService { + + private final VenueCategoryRepository venueCategoryRepository; + + @Override + public List getCategories() { + + return venueCategoryRepository.findAllByOrderByNameAsc() + .stream() + .map(category -> + VenueCategoryResponse.builder() + .id(category.getId()) + .name(category.getName()) + .description(category.getDescription()) + .build() + ) + .toList(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index 9c490f0f9..32b539a2d 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -4,15 +4,16 @@ import com.bookmyvenue.server.common.exception.BusinessException; import com.bookmyvenue.server.common.exception.ErrorCode; import com.bookmyvenue.server.user.entity.User; -import com.bookmyvenue.server.user.repository.UserRepository; import com.bookmyvenue.server.venue.dto.request.CreateVenueRequest; import com.bookmyvenue.server.venue.dto.request.UpdateVenueRequest; import com.bookmyvenue.server.venue.dto.response.VenueResponse; import com.bookmyvenue.server.venue.entity.Venue; import com.bookmyvenue.server.venue.entity.VenueCategory; +import com.bookmyvenue.server.venue.entity.VenueImage; import com.bookmyvenue.server.venue.entity.VenueStatus; import com.bookmyvenue.server.venue.mapper.VenueMapper; import com.bookmyvenue.server.venue.repository.VenueCategoryRepository; +import com.bookmyvenue.server.venue.repository.VenueImageRepository; import com.bookmyvenue.server.venue.repository.VenueRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -20,7 +21,6 @@ import org.springframework.stereotype.Service; import java.util.List; -import java.util.UUID; @Service @@ -33,6 +33,7 @@ public class VenueServiceImpl implements VenueService { private final VenueCategoryRepository venueCategoryRepository; private final VenueMapper venueMapper; private final AuthenticatedUserService authenticatedUserService; + private final VenueImageRepository venueImageRepository; @Override public VenueResponse createVenue(CreateVenueRequest request) { @@ -47,6 +48,7 @@ public VenueResponse createVenue(CreateVenueRequest request) { // Get currently authenticated venue owner User currentUser = authenticatedUserService.getCurrentUser(); + System.out.println("CURRENT USER = " + currentUser.getEmail()); // New venues require admin approval before becoming visible @@ -62,25 +64,37 @@ public VenueResponse createVenue(CreateVenueRequest request) { .owner(currentUser) .build(); + if (request.getImageUrls() != null) { + request.getImageUrls().forEach(url -> { + VenueImage image = VenueImage.builder() + .venue(venue) + .imageUrl(url) + .build(); + + venue.getImages().add(image); + }); + } + Venue savedVenue = venueRepository.save(venue); - log.info("Venue created successfully. venueId={}, ownerId={}", - savedVenue.getId(), - currentUser.getId()); return venueMapper.toResponse(savedVenue); } @Override public List getAllVenues() { - List venues = venueRepository.findAll() - .stream().toList(); + User currentUser = authenticatedUserService.getCurrentUser(); + List venues = venueRepository.findByOwnerId(currentUser.getId()); + return venueMapper.toResponse(venues); } @Override public VenueResponse getVenue(Long venueId) { - Venue venue = venueRepository.findById(venueId) - .orElseThrow(()-> + User currentUser = authenticatedUserService.getCurrentUser(); + + Venue venue = venueRepository + .findByIdAndOwnerId(venueId, currentUser.getId()) + .orElseThrow(() -> new BusinessException(ErrorCode.VENUE_NOT_FOUND)); return venueMapper.toResponse(venue); } @@ -155,21 +169,13 @@ public VenueResponse updateVenue( UpdateVenueRequest request ) { - Venue venue = venueRepository.findById(venueId) - .orElseThrow(()-> - new BusinessException(ErrorCode.VENUE_NOT_FOUND)); - // Ensure only the venue owner can modify venue details User currentUser = authenticatedUserService.getCurrentUser(); - if (!venue.getOwner().getId().equals(currentUser.getId())) { - throw new BusinessException(ErrorCode.ACCESS_DENIED); - } - log.warn( - "Unauthorized venue update attempt. venueId={}, ownerId={}, requesterId={}", - venueId, - venue.getOwner().getId(), - currentUser.getId() - ); + Venue venue = venueRepository + .findByIdAndOwnerId(venueId, currentUser.getId()) + .orElseThrow(() -> + new BusinessException(ErrorCode.VENUE_NOT_FOUND)); + if (request.getName() != null) { venue.setName(request.getName()); } @@ -217,22 +223,12 @@ public VenueResponse updateVenue( @Override public void deleteVenue(Long venueId){ - Venue venue = venueRepository.findById(venueId) - .orElseThrow(() -> - new BusinessException(ErrorCode.VENUE_NOT_FOUND)); User currentUser = authenticatedUserService.getCurrentUser(); - - // Ensure only the venue owner can delete the venue - if (!venue.getOwner().getId().equals(currentUser.getId())) { - log.warn( - "Unauthorized venue delete attempt. venueId={}, ownerId={}, requesterId={}", - venueId, - venue.getOwner().getId(), - currentUser.getId() - ); - throw new BusinessException(ErrorCode.ACCESS_DENIED); - } + Venue venue = venueRepository + .findByIdAndOwnerId(venueId, currentUser.getId()) + .orElseThrow(() -> + new BusinessException(ErrorCode.VENUE_NOT_FOUND)); venueRepository.delete(venue); log.info( diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 18a7a2424..3ffdd00ce 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -25,4 +25,5 @@ spring.flyway.locations=classpath:db/migration app.razorpay.key-id=${RAZORPAY_KEY_ID} -app.razorpay.key-secret=${RAZORPAY_KEY_SECRET} \ No newline at end of file +app.razorpay.key-secret=${RAZORPAY_KEY_SECRET} + diff --git a/server/src/main/resources/db/migration/V7__seed_venue_categories.sql b/server/src/main/resources/db/migration/V7__seed_venue_categories.sql new file mode 100644 index 000000000..e8e5abb2c --- /dev/null +++ b/server/src/main/resources/db/migration/V7__seed_venue_categories.sql @@ -0,0 +1,14 @@ +INSERT INTO venue_category (name, description) +VALUES + ('Wedding Hall', 'Indoor halls for weddings and receptions'), + ('Convention Center', 'Large venues for conferences and corporate events'), + ('Resort', 'Resorts suitable for destination weddings and events'), + ('Banquet Hall', 'Banquet halls for celebrations and gatherings'), + ('Party Hall', 'Small and medium party venues'), + ('Auditorium', 'Auditoriums for cultural and public events'), + ('Conference Hall', 'Business meetings and seminars'), + ('Sports Turf', 'Football, cricket and other sports grounds'), + ('Farm House', 'Farm houses for private events'), + ('Beach Venue', 'Beachside venues for weddings and parties'), + ('Rooftop Venue', 'Open rooftop event spaces'), + ('Community Hall', 'Community-owned multipurpose halls'); \ No newline at end of file From 97f48eda511ddcfd76c33cda3b219f14a47306b3 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Mon, 29 Jun 2026 19:34:24 +0530 Subject: [PATCH 50/59] feat: support configurable venue advance payments --- .../server/booking/service/BookingServiceImpl.java | 14 +++++++++++++- .../bookmyvenue/server/payment/entity/Payment.java | 5 +++++ .../server/payment/enums/PaymentType.java | 6 ++++++ .../venue/dto/request/CreateVenueRequest.java | 6 +++++- .../venue/dto/request/UpdateVenueRequest.java | 5 +++++ .../server/venue/dto/response/VenueResponse.java | 2 ++ .../com/bookmyvenue/server/venue/entity/Venue.java | 8 ++++++++ .../server/venue/mapper/VenueMapper.java | 3 +++ .../server/venue/service/VenueServiceImpl.java | 12 +++++++++++- .../db/migration/V8__add_payment_type.sql | 3 +++ .../V9__add_advance_percentage_to_venue.sql | 2 ++ 11 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/payment/enums/PaymentType.java create mode 100644 server/src/main/resources/db/migration/V8__add_payment_type.sql create mode 100644 server/src/main/resources/db/migration/V9__add_advance_percentage_to_venue.sql diff --git a/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java index 855385353..c0960d4e9 100644 --- a/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/booking/service/BookingServiceImpl.java @@ -8,6 +8,8 @@ import com.bookmyvenue.server.booking.repository.BookingRepository; import com.bookmyvenue.server.common.exception.BusinessException; import com.bookmyvenue.server.common.exception.ErrorCode; +import com.bookmyvenue.server.payment.enums.PaymentType; +import com.bookmyvenue.server.payment.service.PaymentService; import com.bookmyvenue.server.slot.entity.SlotTemplate; import com.bookmyvenue.server.slot.repository.SlotTemplateRepository; import com.bookmyvenue.server.user.entity.User; @@ -57,6 +59,15 @@ public BookingResponse createBooking( ErrorCode.VENUE_NOT_FOUND )); + BigDecimal totalAmount = venue.getPricePerSlot(); + + BigDecimal advanceAmount = + totalAmount + .multiply( + venue.getAdvancePercentage() + ) + .divide(BigDecimal.valueOf(100)); + SlotTemplate slotTemplate = slotTemplateRepository.findById(slotTemplateId) .orElseThrow(() -> @@ -115,7 +126,7 @@ public BookingResponse createBooking( .slotTemplate(slotTemplate) .bookingDate(request.bookingDate()) .status(BookingStatus.PENDING) - .totalAmount(venue.getPricePerSlot()) + .totalAmount(totalAmount) .expiresAt( LocalDateTime.now().plusMinutes(10) ) @@ -128,6 +139,7 @@ public BookingResponse createBooking( booking.getId() ); + return mapToResponse(booking); } diff --git a/server/src/main/java/com/bookmyvenue/server/payment/entity/Payment.java b/server/src/main/java/com/bookmyvenue/server/payment/entity/Payment.java index 4af7b6689..568935877 100644 --- a/server/src/main/java/com/bookmyvenue/server/payment/entity/Payment.java +++ b/server/src/main/java/com/bookmyvenue/server/payment/entity/Payment.java @@ -2,6 +2,7 @@ import com.bookmyvenue.server.booking.entity.Booking; import com.bookmyvenue.server.payment.enums.PaymentStatus; +import com.bookmyvenue.server.payment.enums.PaymentType; import jakarta.persistence.*; import lombok.*; @@ -36,6 +37,10 @@ public class Payment { @Column(nullable = false) private PaymentStatus status; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PaymentType paymentType; + private String razorpayOrderId; private String razorpayPaymentId; diff --git a/server/src/main/java/com/bookmyvenue/server/payment/enums/PaymentType.java b/server/src/main/java/com/bookmyvenue/server/payment/enums/PaymentType.java new file mode 100644 index 000000000..64149b792 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/payment/enums/PaymentType.java @@ -0,0 +1,6 @@ +package com.bookmyvenue.server.payment.enums; + +public enum PaymentType { + ADVANCE, + BALANCE +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java index 2b8dff9f9..8988f7242 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/CreateVenueRequest.java @@ -1,5 +1,7 @@ package com.bookmyvenue.server.venue.dto.request; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; import lombok.*; import java.math.BigDecimal; @@ -21,7 +23,9 @@ public class CreateVenueRequest { private Integer capacity; private BigDecimal pricePerSlot; - + @DecimalMin("0.00") + @DecimalMax("100.00") + private BigDecimal advancePercentage; private Long categoryId; private List imageUrls; diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java index 99a07b9bb..fe9da4d00 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/request/UpdateVenueRequest.java @@ -1,5 +1,7 @@ package com.bookmyvenue.server.venue.dto.request; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; import lombok.*; import java.math.BigDecimal; @@ -17,5 +19,8 @@ public class UpdateVenueRequest { private String district; private Integer capacity; private BigDecimal pricePerSlot; + @DecimalMin("0.00") + @DecimalMax("100.00") + private BigDecimal advancePercentage; private Long categoryId; } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueResponse.java b/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueResponse.java index bc03a217b..5d45775d6 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueResponse.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/dto/response/VenueResponse.java @@ -27,6 +27,8 @@ public class VenueResponse { private BigDecimal pricePerSlot; + private BigDecimal advancePercentage; + private String category; private VenueStatus status; diff --git a/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java b/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java index b3a0bc51e..dea727a5d 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/entity/Venue.java @@ -62,6 +62,14 @@ public class Venue { @Builder.Default private List images = new ArrayList<>(); + @Column( + name = "advance_percentage", + nullable = false, + precision = 5, + scale = 2 + ) + private BigDecimal advancePercentage; + @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; diff --git a/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java b/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java index 6add80020..7ee0742ec 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/mapper/VenueMapper.java @@ -19,6 +19,9 @@ public VenueResponse toResponse(Venue venue) { .district(venue.getDistrict()) .capacity(venue.getCapacity()) .pricePerSlot(venue.getPricePerSlot()) + .advancePercentage( + venue.getAdvancePercentage() + ) .category(venue.getCategory().getName()) .status(venue.getStatus()) .imageUrls( diff --git a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java index 32b539a2d..b99c290a9 100644 --- a/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/venue/service/VenueServiceImpl.java @@ -59,6 +59,7 @@ public VenueResponse createVenue(CreateVenueRequest request) { .district(request.getDistrict()) .capacity(request.getCapacity()) .pricePerSlot(request.getPricePerSlot()) + .advancePercentage(request.getAdvancePercentage()) .category(category) .status(VenueStatus.PENDING_APPROVAL) .owner(currentUser) @@ -140,7 +141,10 @@ public List getApprovedVenues( categoryId ); - } else { + } + + + else { venues = venueRepository.findByStatus( VenueStatus.APPROVED @@ -200,6 +204,12 @@ public VenueResponse updateVenue( venue.setPricePerSlot(request.getPricePerSlot()); } + if (request.getAdvancePercentage() != null) { + venue.setAdvancePercentage( + request.getAdvancePercentage() + ); + } + if (request.getCategoryId() != null) { VenueCategory category = venueCategoryRepository diff --git a/server/src/main/resources/db/migration/V8__add_payment_type.sql b/server/src/main/resources/db/migration/V8__add_payment_type.sql new file mode 100644 index 000000000..767ddb7d9 --- /dev/null +++ b/server/src/main/resources/db/migration/V8__add_payment_type.sql @@ -0,0 +1,3 @@ +ALTER TABLE payment + ADD COLUMN payment_type VARCHAR(20) + NOT NULL DEFAULT 'ADVANCE'; \ No newline at end of file diff --git a/server/src/main/resources/db/migration/V9__add_advance_percentage_to_venue.sql b/server/src/main/resources/db/migration/V9__add_advance_percentage_to_venue.sql new file mode 100644 index 000000000..5ebf10511 --- /dev/null +++ b/server/src/main/resources/db/migration/V9__add_advance_percentage_to_venue.sql @@ -0,0 +1,2 @@ +ALTER TABLE venue + ADD COLUMN advance_percentage NUMERIC(5,2) NOT NULL DEFAULT 20.00; \ No newline at end of file From dec3b3e97dd95efb5729acc70c0bf991c0741638 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Wed, 1 Jul 2026 09:02:59 +0530 Subject: [PATCH 51/59] add email verification --- server/.env.example | 7 +- server/docker-compose.yml | 12 +++ server/pom.xml | 9 +++ .../auth/controller/AuthController.java | 19 ++--- .../server/auth/security/SecurityConfig.java | 2 +- .../server/auth/service/AuthService.java | 3 +- .../server/auth/service/AuthServiceImpl.java | 32 ++++---- .../server/common/exception/ErrorCode.java | 22 ++++++ .../common/response/MessageResponse.java | 14 ++++ .../bookmyvenue/server/user/entity/User.java | 6 ++ .../controller/VerificationController.java | 34 +++++++++ .../dto/request/EmailRequest.java | 18 +++++ .../dto/request/VerifyEmailRequest.java | 23 ++++++ .../verification/mail/client/BrevoClient.java | 56 ++++++++++++++ .../mail/config/BrevoProperties.java | 19 +++++ .../mail/config/RestClientConfig.java | 15 ++++ .../mail/dto/BrevoEmailRequest.java | 20 +++++ .../verification/mail/dto/BrevoResponse.java | 4 + .../verification/mail/dto/Recipient.java | 12 +++ .../server/verification/mail/dto/Sender.java | 13 ++++ .../mail/service/MailService.java | 11 +++ .../mail/service/MailServiceImpl.java | 27 +++++++ .../verification/service/OtpRedisService.java | 14 ++++ .../service/OtpRedisServiceImpl.java | 34 +++++++++ .../service/VerificationService.java | 10 +++ .../service/VerificationServiceImpl.java | 75 +++++++++++++++++++ .../verification/util/OtpGenerator.java | 27 +++++++ .../server/verification/util/RedisKeys.java | 16 ++++ .../src/main/resources/application.properties | 15 +++- .../V7__add_user_verification_fields.sql | 5 ++ 30 files changed, 540 insertions(+), 34 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/common/response/MessageResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/controller/VerificationController.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/dto/request/EmailRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/dto/request/VerifyEmailRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/client/BrevoClient.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/config/BrevoProperties.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/config/RestClientConfig.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/dto/BrevoEmailRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/dto/BrevoResponse.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/dto/Recipient.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/dto/Sender.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailServiceImpl.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/service/OtpRedisService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/service/OtpRedisServiceImpl.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/service/VerificationService.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/service/VerificationServiceImpl.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/util/OtpGenerator.java create mode 100644 server/src/main/java/com/bookmyvenue/server/verification/util/RedisKeys.java create mode 100644 server/src/main/resources/db/migration/V7__add_user_verification_fields.sql diff --git a/server/.env.example b/server/.env.example index 12038ce45..049c92de3 100644 --- a/server/.env.example +++ b/server/.env.example @@ -10,4 +10,9 @@ JWT_REFRESH_TOKEN_EXPIRATION=604800000 RAZORPAY_KEY_ID=rzp_test_T3DBrC7nq10MPn -RAZORPAY_KEY_SECRET=pGHlSR1p2mm1g2m02AAjwl2W \ No newline at end of file +RAZORPAY_KEY_SECRET=pGHlSR1p2mm1g2m02AAjwl2W + + +BREVO_API_KEY=your-api-key +BREVO_SENDER_EMAIL=noreply@yourdomain.com +BREVO_SENDER_NAME=BookMyVenue \ No newline at end of file diff --git a/server/docker-compose.yml b/server/docker-compose.yml index 788c4a65e..a8d5e3089 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -13,6 +13,14 @@ services: volumes: - postgres_data:/var/lib/postgresql/data + redis: + image: redis:7-alpine + container_name: bmv-redis + restart: unless-stopped + ports: + - "6379:6379" + + backend: build: context: . @@ -20,6 +28,7 @@ services: container_name: bmv-backend depends_on: - postgres + - redis environment: SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${DB_NAME} SPRING_DATASOURCE_USERNAME: ${DB_USERNAME} @@ -30,6 +39,9 @@ services: JWT_REFRESH_TOKEN_EXPIRATION: ${JWT_REFRESH_TOKEN_EXPIRATION} RAZORPAY_KEY_ID: ${RAZORPAY_KEY_ID} RAZORPAY_KEY_SECRET: ${RAZORPAY_KEY_SECRET} + BREVO_API_KEY: ${BREVO_API_KEY} + BREVO_SENDER_EMAIL: ${BREVO_SENDER_EMAIL} + BREVO_SENDER_NAME: ${BREVO_SENDER_NAME} ports: - "8080:8080" diff --git a/server/pom.xml b/server/pom.xml index 027c179d5..24935a193 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -125,6 +125,15 @@ razorpay-java 1.4.8 + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-mail + diff --git a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java index 55dd65374..61cebab4c 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java @@ -9,6 +9,7 @@ import com.bookmyvenue.server.common.exception.BusinessException; import com.bookmyvenue.server.common.exception.ErrorCode; import com.bookmyvenue.server.common.response.ApiErrorResponse; +import com.bookmyvenue.server.common.response.MessageResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -24,7 +25,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/auth") +@RequestMapping("/api/v1/auth") @RequiredArgsConstructor @Tag(name = "Authentication", description = "Authentication and account management APIs") public class AuthController { @@ -32,7 +33,7 @@ public class AuthController { private final AuthService authService; private final CookieUtils cookieUtils; - @Operation(summary = "Register a new account", description = "Creates a new USER or VENDOR account and sets authentication cookies.") + @Operation(summary = "Register a new account", description = "Creates a new USER or VENDOR account and sends a verification email.") @ApiResponses({ @ApiResponse(responseCode = "201", description = "Registration successful"), @ApiResponse(responseCode = "400", description = "Invalid request or ADMIN registration attempted", @@ -41,26 +42,20 @@ public class AuthController { content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))) }) @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - AuthResult result = authService.register(request); - ResponseCookie accessCookie = cookieUtils.accessTokenCookie(result.accessToken()); - ResponseCookie refreshCookie = cookieUtils.refreshTokenCookie(result.refreshToken()); + MessageResponse response = authService.register(request); - System.out.println(accessCookie); - System.out.println(refreshCookie); return ResponseEntity .status(HttpStatus.CREATED) - .header(HttpHeaders.SET_COOKIE, accessCookie.toString()) - .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) - .body(result.response()); + .body(response); } @Operation(summary = "Authenticate user", description = "Authenticates a user and sets authentication cookies.") @ApiResponses({@ApiResponse(responseCode = "200", description = "Login successful"), - @ApiResponse(responseCode = "401", description = "Invalid credentials", + @ApiResponse(responseCode = "401", description = "Invalid credentials or email not verified", content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))) }) @PostMapping("/login") diff --git a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java index 4f78e7893..01141a969 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/security/SecurityConfig.java @@ -50,7 +50,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth // Public endpoints .requestMatchers( - "/api/auth/**", + "/api/v1/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/api/v1/venues/**" diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java index 40b507107..056f6cecf 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java @@ -3,6 +3,7 @@ import com.bookmyvenue.server.auth.dto.request.LoginRequest; import com.bookmyvenue.server.auth.dto.request.RegisterRequest; import com.bookmyvenue.server.auth.dto.response.AuthResult; +import com.bookmyvenue.server.common.response.MessageResponse; public interface AuthService { @@ -10,7 +11,7 @@ public interface AuthService { /** * Registers a new USER or VENDOR account. */ - AuthResult register(RegisterRequest request); + MessageResponse register(RegisterRequest request); /** * Authenticates a user and returns JWT tokens. diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java index 6aa4b6b58..dbcba31d6 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java @@ -6,9 +6,11 @@ import com.bookmyvenue.server.auth.dto.response.AuthResult; import com.bookmyvenue.server.auth.security.JwtService; import com.bookmyvenue.server.common.exception.*; +import com.bookmyvenue.server.common.response.MessageResponse; import com.bookmyvenue.server.user.entity.User; import com.bookmyvenue.server.user.enums.Role; import com.bookmyvenue.server.user.repository.UserRepository; +import com.bookmyvenue.server.verification.service.VerificationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; @@ -23,10 +25,10 @@ public class AuthServiceImpl implements AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtService jwtService; - + private final VerificationService verificationService; @Override - public AuthResult register(RegisterRequest request) { + public MessageResponse register(RegisterRequest request) { log.info("Attempting registration for email: {}", request.getEmail()); @@ -52,27 +54,18 @@ public AuthResult register(RegisterRequest request) { .phone(request.getPhone()) .password(passwordEncoder.encode(request.getPassword())) .role(request.getRole()) + .emailVerified(false) + .phoneVerified(false) .build(); User savedUser = userRepository.save(user); - String accessToken = jwtService.generateAccessToken(savedUser.getEmail(), savedUser.getRole()); - String refreshToken = jwtService.generateRefreshToken(savedUser.getEmail(), savedUser.getRole()); - - log.info("User registered successfully: {}", savedUser.getEmail()); + verificationService.sendEmailVerificationOtp(savedUser.getEmail()); - AuthResponse response = AuthResponse.builder() - .userId(savedUser.getId()) - .name(savedUser.getName()) - .email(savedUser.getEmail()) - .role(savedUser.getRole()) + log.info("Verification email sent to {}", savedUser.getEmail()); + return MessageResponse.builder() + .message("Registration successful. Please verify your email.") .build(); - - return new AuthResult( - response, - accessToken, - refreshToken - ); } @@ -86,6 +79,11 @@ public AuthResult login(LoginRequest request) { return new BusinessException(ErrorCode.INVALID_CREDENTIALS); }); + if (!user.isEmailVerified()) { + log.warn("Login failed. Email not verified: {}", request.getEmail()); + throw new BusinessException(ErrorCode.EMAIL_NOT_VERIFIED); + } + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { log.warn("Login failed. Invalid password for email: {}", request.getEmail()); throw new BusinessException(ErrorCode.INVALID_CREDENTIALS); diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java index 4ace92c45..7574b478b 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/ErrorCode.java @@ -117,6 +117,28 @@ public enum ErrorCode { "INVALID_PAYMENT_SIGNATURE", "Invalid payment signature" ), + INVALID_OTP( + HttpStatus.BAD_REQUEST, + "INVALID_OTP", + "Invalid OTP." + ), + + OTP_EXPIRED( + HttpStatus.BAD_REQUEST, + "OTP_EXPIRED", + "OTP has expired. Please request a new one." + ), + + EMAIL_ALREADY_VERIFIED( + HttpStatus.BAD_REQUEST, + "EMAIL_ALREADY_VERIFIED", + "Email is already verified." + ), + EMAIL_NOT_VERIFIED( + HttpStatus.FORBIDDEN, + "EMAIL_NOT_VERIFIED", + "Email is not verified." + ), INVALID_VENUE_STATUS( HttpStatus.BAD_REQUEST, "INVALID_VENUE_STATUS", diff --git a/server/src/main/java/com/bookmyvenue/server/common/response/MessageResponse.java b/server/src/main/java/com/bookmyvenue/server/common/response/MessageResponse.java new file mode 100644 index 000000000..2fdc5ebb4 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/common/response/MessageResponse.java @@ -0,0 +1,14 @@ +package com.bookmyvenue.server.common.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class MessageResponse { + + private String message; + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/user/entity/User.java b/server/src/main/java/com/bookmyvenue/server/user/entity/User.java index d8e79f169..640293667 100644 --- a/server/src/main/java/com/bookmyvenue/server/user/entity/User.java +++ b/server/src/main/java/com/bookmyvenue/server/user/entity/User.java @@ -42,6 +42,12 @@ public class User { @Column(nullable = false) private LocalDateTime updatedAt; // Last update timestamp + @Column(nullable = false) + private boolean emailVerified = false; + + @Column(nullable = false) + private boolean phoneVerified = false; + /** * Automatically sets creation and update timestamps * before the entity is inserted into the database. diff --git a/server/src/main/java/com/bookmyvenue/server/verification/controller/VerificationController.java b/server/src/main/java/com/bookmyvenue/server/verification/controller/VerificationController.java new file mode 100644 index 000000000..f16dd3b2e --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/controller/VerificationController.java @@ -0,0 +1,34 @@ +package com.bookmyvenue.server.verification.controller; + +import com.bookmyvenue.server.verification.dto.request.EmailRequest; +import com.bookmyvenue.server.verification.dto.request.VerifyEmailRequest; +import com.bookmyvenue.server.verification.service.VerificationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class VerificationController { + + private final VerificationService verificationService; + + @Operation(summary = "Verify email", description = "Verifies the user's email using the OTP sent to their email address.") + @ApiResponse(responseCode = "200", description = "Email verified successfully") + @PostMapping("/verify-email") + public void verifyEmail(@Valid @RequestBody VerifyEmailRequest request) + { + verificationService.verifyEmail(request.getEmail(), request.getOtp()); + } + + @Operation(summary = "Resend verification email", description = "Generates a new OTP and sends it to the user's registered email.") + @ApiResponse(responseCode = "200", description = "Verification email sent successfully") + @PostMapping("/resend-verification") + public void resendVerification(@Valid @RequestBody EmailRequest request) + { + verificationService.sendEmailVerificationOtp(request.getEmail()); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/dto/request/EmailRequest.java b/server/src/main/java/com/bookmyvenue/server/verification/dto/request/EmailRequest.java new file mode 100644 index 000000000..b230ecbf0 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/dto/request/EmailRequest.java @@ -0,0 +1,18 @@ +package com.bookmyvenue.server.verification.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EmailRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + private String email; + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/dto/request/VerifyEmailRequest.java b/server/src/main/java/com/bookmyvenue/server/verification/dto/request/VerifyEmailRequest.java new file mode 100644 index 000000000..c71679719 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/dto/request/VerifyEmailRequest.java @@ -0,0 +1,23 @@ +package com.bookmyvenue.server.verification.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VerifyEmailRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + private String email; + + @NotBlank(message = "OTP is required") + @Pattern(regexp = "\\d{6}", message = "OTP must be a 6-digit number") + private String otp; + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/client/BrevoClient.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/client/BrevoClient.java new file mode 100644 index 000000000..c0c0b2c6d --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/client/BrevoClient.java @@ -0,0 +1,56 @@ +package com.bookmyvenue.server.verification.mail.client; + + +import com.bookmyvenue.server.verification.mail.config.BrevoProperties; +import com.bookmyvenue.server.verification.mail.dto.BrevoEmailRequest; +import com.bookmyvenue.server.verification.mail.dto.BrevoResponse; +import com.bookmyvenue.server.verification.mail.dto.Recipient; +import com.bookmyvenue.server.verification.mail.dto.Sender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.util.List; + +@Component +@Slf4j +@RequiredArgsConstructor +public class BrevoClient { + + private static final String BREVO_URL = "https://api.brevo.com/v3/smtp/email"; + + private final RestClient restClient; + private final BrevoProperties brevoProperties; + + public void sendEmail(String to, String subject, String htmlContent) { + + BrevoEmailRequest request = BrevoEmailRequest.builder() + .sender(new Sender( + brevoProperties.getSenderName(), + brevoProperties.getSenderEmail())) + .to(List.of(new Recipient(to))) + .subject(subject) + .htmlContent(htmlContent) + .build(); + + try { + BrevoResponse response = restClient.post() + .uri(BREVO_URL) + .header("api-key", brevoProperties.getApiKey()) + .contentType(MediaType.APPLICATION_JSON) + .body(request) + .retrieve() + .body(BrevoResponse.class); + + log.info("Brevo Response: {}", response); + + } catch (Exception e) { + log.error("Brevo email sending failed", e); + throw e; + } + + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/config/BrevoProperties.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/config/BrevoProperties.java new file mode 100644 index 000000000..b1c4d49e5 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/config/BrevoProperties.java @@ -0,0 +1,19 @@ +package com.bookmyvenue.server.verification.mail.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "app.brevo") +public class BrevoProperties { + + private String apiKey; + + private String senderEmail; + + private String senderName; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/config/RestClientConfig.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/config/RestClientConfig.java new file mode 100644 index 000000000..db6c210bb --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/config/RestClientConfig.java @@ -0,0 +1,15 @@ +package com.bookmyvenue.server.verification.mail.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient() { + + return RestClient.builder().build(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/BrevoEmailRequest.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/BrevoEmailRequest.java new file mode 100644 index 000000000..2a14adeeb --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/BrevoEmailRequest.java @@ -0,0 +1,20 @@ +package com.bookmyvenue.server.verification.mail.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class BrevoEmailRequest { + + private Sender sender; + + private List to; + + private String subject; + + private String htmlContent; + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/BrevoResponse.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/BrevoResponse.java new file mode 100644 index 000000000..159027b4c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/BrevoResponse.java @@ -0,0 +1,4 @@ +package com.bookmyvenue.server.verification.mail.dto; + +public class BrevoResponse { +} diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/Recipient.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/Recipient.java new file mode 100644 index 000000000..1ec2df2ff --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/Recipient.java @@ -0,0 +1,12 @@ +package com.bookmyvenue.server.verification.mail.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Recipient { + + private String email; + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/Sender.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/Sender.java new file mode 100644 index 000000000..005797391 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/dto/Sender.java @@ -0,0 +1,13 @@ +package com.bookmyvenue.server.verification.mail.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Sender { + + private String name; + private String email; + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailService.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailService.java new file mode 100644 index 000000000..4cf02d97b --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailService.java @@ -0,0 +1,11 @@ +package com.bookmyvenue.server.verification.mail.service; + +public interface MailService { + + + void sendVerificationOtp(String email, String otp); + + + + +} diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailServiceImpl.java new file mode 100644 index 000000000..d14974af4 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailServiceImpl.java @@ -0,0 +1,27 @@ +package com.bookmyvenue.server.verification.mail.service; + +import com.bookmyvenue.server.verification.mail.client.BrevoClient; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MailServiceImpl implements MailService { + + private final BrevoClient brevoClient; + + @Override + public void sendVerificationOtp(String email, String otp) { + + String subject = "Verify your BookMyVenue account"; + + String html = """ +

Email Verification

+

Your verification code is:

+

%s

+

This code will expire in 5 minutes.

+ """.formatted(otp); + + brevoClient.sendEmail(email, subject, html); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/service/OtpRedisService.java b/server/src/main/java/com/bookmyvenue/server/verification/service/OtpRedisService.java new file mode 100644 index 000000000..e1aa41e43 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/service/OtpRedisService.java @@ -0,0 +1,14 @@ +package com.bookmyvenue.server.verification.service; + +public interface OtpRedisService { + + void saveOtp(String key, String otp); + + String getOtp(String key); + + void deleteOtp(String key); + + + + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/service/OtpRedisServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/verification/service/OtpRedisServiceImpl.java new file mode 100644 index 000000000..3317d06db --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/service/OtpRedisServiceImpl.java @@ -0,0 +1,34 @@ +package com.bookmyvenue.server.verification.service; + +import com.bookmyvenue.server.verification.util.RedisKeys; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +@Service +@RequiredArgsConstructor +public class OtpRedisServiceImpl implements OtpRedisService { + + private static final Duration OTP_EXPIRY = Duration.ofMinutes(5); + private final StringRedisTemplate redisTemplate; + + @Override + public void saveOtp(String key, String otp) { + redisTemplate.opsForValue() + .set(key, otp, OTP_EXPIRY); + } + + @Override + public String getOtp(String key) { + return redisTemplate.opsForValue().get(key); + } + + @Override + public void deleteOtp(String key) { + redisTemplate.delete(key); + } + + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/service/VerificationService.java b/server/src/main/java/com/bookmyvenue/server/verification/service/VerificationService.java new file mode 100644 index 000000000..c2e82de7e --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/service/VerificationService.java @@ -0,0 +1,10 @@ +package com.bookmyvenue.server.verification.service; + +public interface VerificationService { + + void sendEmailVerificationOtp(String email); + + void verifyEmail(String email, String otp); + + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/service/VerificationServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/verification/service/VerificationServiceImpl.java new file mode 100644 index 000000000..e7098c2d7 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/service/VerificationServiceImpl.java @@ -0,0 +1,75 @@ +package com.bookmyvenue.server.verification.service; + + +import com.bookmyvenue.server.common.exception.BusinessException; +import com.bookmyvenue.server.common.exception.ErrorCode; +import com.bookmyvenue.server.user.entity.User; +import com.bookmyvenue.server.user.repository.UserRepository; +import com.bookmyvenue.server.verification.mail.service.MailService; +import com.bookmyvenue.server.verification.util.OtpGenerator; +import com.bookmyvenue.server.verification.util.RedisKeys; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class VerificationServiceImpl implements VerificationService { + + private final OtpGenerator otpGenerator; + private final OtpRedisService otpRedisService; + private final MailService mailService; + private final UserRepository userRepository; + + + + @Override + public void sendEmailVerificationOtp(String email) { + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> + new BusinessException(ErrorCode.USER_NOT_FOUND)); + + if (user.isEmailVerified()) { + throw new BusinessException(ErrorCode.EMAIL_ALREADY_VERIFIED); + } + String otp = otpGenerator.generateOtp(); + String key = RedisKeys.emailVerification(email); + otpRedisService.saveOtp(key, otp); + log.info("Sending verification email to {}", email); + mailService.sendVerificationOtp(email, otp); + log.info("Verification email sent successfully to {}", email); + } + + @Transactional + @Override + public void verifyEmail(String email, String otp) { + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> + new BusinessException(ErrorCode.USER_NOT_FOUND)); + + + if (user.isEmailVerified()) { + throw new BusinessException(ErrorCode.EMAIL_ALREADY_VERIFIED); + } + + String key = RedisKeys.emailVerification(email); + String savedOtp = otpRedisService.getOtp(key); + + if (savedOtp == null) { + throw new BusinessException(ErrorCode.OTP_EXPIRED); + } + + if (!savedOtp.equals(otp)) { + throw new BusinessException(ErrorCode.INVALID_OTP); + } + user.setEmailVerified(true); + userRepository.save(user); + otpRedisService.deleteOtp(key); + log.info("Email verified successfully: {}", email); + } + +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/util/OtpGenerator.java b/server/src/main/java/com/bookmyvenue/server/verification/util/OtpGenerator.java new file mode 100644 index 000000000..26b925b5c --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/util/OtpGenerator.java @@ -0,0 +1,27 @@ +package com.bookmyvenue.server.verification.util; + +import org.springframework.stereotype.Component; + +import java.security.SecureRandom; + +/** + * Utility class for generating secure 6-digit OTPs. + * Used for email and phone verification. + */ +@Component +public class OtpGenerator { + + // Cryptographically secure random number generator + private static final SecureRandom RANDOM = new SecureRandom(); + + /** + * Generates a random 6-digit OTP. + * Leading zeros are preserved (e.g., 000123). + * + * @return 6-digit OTP as a String + */ + public String generateOtp() { + + return String.format("%06d", RANDOM.nextInt(1_000_000)); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/util/RedisKeys.java b/server/src/main/java/com/bookmyvenue/server/verification/util/RedisKeys.java new file mode 100644 index 000000000..ae79ad5aa --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/verification/util/RedisKeys.java @@ -0,0 +1,16 @@ +package com.bookmyvenue.server.verification.util; + +public final class RedisKeys { + + private RedisKeys() { + } + + public static String emailVerification(String email) { + return "verify:email:" + email; + } + + public static String verifiedEmail(String email) { + return "verified:email:" + email; + } + +} \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 18a7a2424..9c9ba4f78 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -23,6 +23,17 @@ app.jwt.refresh-token-expiration=${JWT_REFRESH_TOKEN_EXPIRATION} spring.flyway.enabled=true spring.flyway.locations=classpath:db/migration - +# Razorpay app.razorpay.key-id=${RAZORPAY_KEY_ID} -app.razorpay.key-secret=${RAZORPAY_KEY_SECRET} \ No newline at end of file +app.razorpay.key-secret=${RAZORPAY_KEY_SECRET} + + +# Redis +spring.data.redis.host=${SPRING_DATA_REDIS_HOST:localhost} +spring.data.redis.port=${SPRING_DATA_REDIS_PORT:6379} + +# Brevo +app.brevo.api-key=${BREVO_API_KEY} +app.brevo.sender-email=${BREVO_SENDER_EMAIL} +app.brevo.sender-name=${BREVO_SENDER_NAME} + diff --git a/server/src/main/resources/db/migration/V7__add_user_verification_fields.sql b/server/src/main/resources/db/migration/V7__add_user_verification_fields.sql new file mode 100644 index 000000000..a232d5f71 --- /dev/null +++ b/server/src/main/resources/db/migration/V7__add_user_verification_fields.sql @@ -0,0 +1,5 @@ +ALTER TABLE users + ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE; + +ALTER TABLE users + ADD COLUMN phone_verified BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file From fd826561ca5ae857729ce1fd885c619aa01efd4f Mon Sep 17 00:00:00 2001 From: syamjith45 Date: Fri, 3 Jul 2026 14:40:03 +0530 Subject: [PATCH 52/59] feat(auth): implement OTP-based email verification and resend functionality --- .../admin/features/login/login.component.html | 11 ++ .../admin/features/login/login.component.ts | 22 ++- client/src/app/core/app.routes.ts | 5 + .../constants/api-endpoints.constant.ts | 32 ++-- .../shared/constants/auth-errors.constant.ts | 8 + client/src/app/shared/constants/index.ts | 1 + .../verify-email/verify-email.component.css | 13 ++ .../verify-email/verify-email.component.html | 114 ++++++++++++++ .../verify-email/verify-email.component.ts | 144 ++++++++++++++++++ .../app/shared/models/auth-response.model.ts | 20 +++ .../app/shared/services/auth.repository.ts | 17 +++ .../src/app/shared/services/auth.service.ts | 74 +++++++-- .../user/features/login/login.component.html | 12 ++ .../user/features/login/login.component.ts | 22 ++- .../user/features/signup/signup.component.ts | 12 +- 15 files changed, 470 insertions(+), 37 deletions(-) create mode 100644 client/src/app/shared/constants/auth-errors.constant.ts create mode 100644 client/src/app/shared/features/verify-email/verify-email.component.css create mode 100644 client/src/app/shared/features/verify-email/verify-email.component.html create mode 100644 client/src/app/shared/features/verify-email/verify-email.component.ts diff --git a/client/src/app/admin/features/login/login.component.html b/client/src/app/admin/features/login/login.component.html index 610fab88a..64741c735 100644 --- a/client/src/app/admin/features/login/login.component.html +++ b/client/src/app/admin/features/login/login.component.html @@ -60,6 +60,17 @@

BookMyVenu

+ + @if (showVerifyButton()) { + + }

diff --git a/client/src/app/admin/features/login/login.component.ts b/client/src/app/admin/features/login/login.component.ts index 26693da56..0016cfcc6 100644 --- a/client/src/app/admin/features/login/login.component.ts +++ b/client/src/app/admin/features/login/login.component.ts @@ -1,6 +1,6 @@ -import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, ChangeDetectionStrategy, computed } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../../shared/services/auth.service'; import { ButtonComponent } from '../../../shared/components/button/button.component'; import { InputComponent } from '../../../shared/components/input/input.component'; @@ -15,8 +15,12 @@ import { InputComponent } from '../../../shared/components/input/input.component }) export class LoginComponent { private readonly fb = inject(FormBuilder); + private readonly router = inject(Router); private readonly authService = inject(AuthService); readonly loading = this.authService.loading; + readonly pendingVerificationEmail = this.authService.pendingVerificationEmail; + + readonly showVerifyButton = computed(() => this.pendingVerificationEmail() !== null); readonly form = this.fb.nonNullable.group({ email: ['', [Validators.required, Validators.email]], @@ -27,4 +31,18 @@ export class LoginComponent { if (this.form.valid) { this.authService.adminLogin(this.form.getRawValue()); } else { this.form.markAllAsTouched(); } } + + onVerify(): void { + const email = this.pendingVerificationEmail(); + if (email) { + this.authService.clearPendingVerification(); + this.authService.resendOtp({ email }).subscribe({ + next: () => { + this.router.navigate(['/verify-email'], { + queryParams: { email, portal: 'admin' }, + }); + }, + }); + } + } } diff --git a/client/src/app/core/app.routes.ts b/client/src/app/core/app.routes.ts index 943c1b2fa..2b7760a9a 100644 --- a/client/src/app/core/app.routes.ts +++ b/client/src/app/core/app.routes.ts @@ -23,6 +23,11 @@ export const routes: Routes = [ canActivate: [guestGuard], loadComponent: () => import('../user/features/forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent), }, + { + path: 'verify-email', + canActivate: [guestGuard], + loadComponent: () => import('../shared/features/verify-email/verify-email.component').then(m => m.VerifyEmailComponent), + }, { path: ROUTE_PATHS.USER.BASE, loadChildren: () => import('../user/routes/user.routes').then(m => m.USER_ROUTES), diff --git a/client/src/app/shared/constants/api-endpoints.constant.ts b/client/src/app/shared/constants/api-endpoints.constant.ts index 3ac75a144..4493dd399 100644 --- a/client/src/app/shared/constants/api-endpoints.constant.ts +++ b/client/src/app/shared/constants/api-endpoints.constant.ts @@ -1,12 +1,14 @@ export const API_ENDPOINTS = { AUTH: { - LOGIN: '/auth/login', - SIGNUP: '/auth/register', - LOGOUT: '/auth/logout', - REFRESH_TOKEN: '/auth/refresh-token', - FORGOT_PASSWORD: '/auth/forgot-password', - RESET_PASSWORD: '/auth/reset-password', - ADMIN_LOGIN: '/auth/login', + LOGIN: '/v1/auth/login', + SIGNUP: '/v1/auth/register', + LOGOUT: '/v1/auth/logout', + REFRESH_TOKEN: '/v1/auth/refresh-token', + FORGOT_PASSWORD: '/v1/auth/forgot-password', + RESET_PASSWORD: '/v1/auth/reset-password', + ADMIN_LOGIN: '/v1/auth/login', + VERIFY_EMAIL: '/v1/auth/verify-email', + RESEND_VERIFICATION: '/v1/auth/resend-verification', }, USERS: { BASE: '/v1/users', @@ -21,8 +23,8 @@ export const API_ENDPOINTS = { }, VENUES: { // Public endpoints (no auth required) - PUBLIC_BASE:'/v1/venues', - PUBLIC_BY_ID:(id:string) => `/v1/venues/${id}`, + PUBLIC_BASE: '/v1/venues', + PUBLIC_BY_ID: (id: string) => `/v1/venues/${id}`, AVAILABILITY: (venueId: string) => `/v1/venues/${venueId}/availability`, // Vendor endpoints (require VENDOR role) @@ -39,9 +41,9 @@ export const API_ENDPOINTS = { BY_VENUE: (venueId: string) => `/venues/${venueId}/bookings`, BY_VENDOR: '/vendor/bookings', MY_BOOKINGS: '/v1/bookings/my-bookings', - CREATE_WITH_SLOT:(venueId: string, slotId: number) => `/v1/venues/${venueId}/slots/${slotId}/bookings`, - CANCEL:(bookingId: string) => `/v1/bookings/${bookingId}/cancel`, - + CREATE_WITH_SLOT: (venueId: string, slotId: number) => `/v1/venues/${venueId}/slots/${slotId}/bookings`, + CANCEL: (bookingId: string) => `/v1/bookings/${bookingId}/cancel`, + }, ADMIN: { DASHBOARD: '/v1/admin/dashboard', @@ -50,12 +52,12 @@ export const API_ENDPOINTS = { VENUES: '/v1/admin/venues', PENDING_VENUES: '/v1/admin/venues/pending', VENUE_APPROVE: (id: number) => `/v1/admin/venues/${id}/approve`, - VENUE_REJECT: (id: number) => `/v1/admin/venues/${id}/reject`, + VENUE_REJECT: (id: number) => `/v1/admin/venues/${id}/reject`, BOOKINGS: '/v1/admin/bookings', ANALYTICS: '/v1/admin/analytics', }, PAYMENTS: { - CREATE: (bookingId: number) => `/v1/payments/${bookingId}`, - VERIFY: '/v1/payments/verify', + CREATE: (bookingId: number) => `/v1/payments/${bookingId}`, + VERIFY: '/v1/payments/verify', }, } as const; diff --git a/client/src/app/shared/constants/auth-errors.constant.ts b/client/src/app/shared/constants/auth-errors.constant.ts new file mode 100644 index 000000000..9eb1de783 --- /dev/null +++ b/client/src/app/shared/constants/auth-errors.constant.ts @@ -0,0 +1,8 @@ +export const AUTH_ERRORS = { + INVALID_OTP: 'INVALID_OTP', + EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED', + OTP_EXPIRED: 'OTP_EXPIRED', + EMAIL_ALREADY_EXISTS: 'EMAIL_ALREADY_EXISTS', +} as const; + +export type AuthErrorCode = typeof AUTH_ERRORS[keyof typeof AUTH_ERRORS]; \ No newline at end of file diff --git a/client/src/app/shared/constants/index.ts b/client/src/app/shared/constants/index.ts index dd98611f0..de6edb234 100644 --- a/client/src/app/shared/constants/index.ts +++ b/client/src/app/shared/constants/index.ts @@ -1,2 +1,3 @@ export * from './api-endpoints.constant'; export * from './route-paths.constant'; +export * from './auth-errors.constant'; diff --git a/client/src/app/shared/features/verify-email/verify-email.component.css b/client/src/app/shared/features/verify-email/verify-email.component.css new file mode 100644 index 000000000..0511dbbfc --- /dev/null +++ b/client/src/app/shared/features/verify-email/verify-email.component.css @@ -0,0 +1,13 @@ +:host { + display: block; +} + +input[type="text"] { + -moz-appearance: textfield; +} + +input[type="text"]::-webkit-outer-spin-button, +input[type="text"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} \ No newline at end of file diff --git a/client/src/app/shared/features/verify-email/verify-email.component.html b/client/src/app/shared/features/verify-email/verify-email.component.html new file mode 100644 index 000000000..a28931af2 --- /dev/null +++ b/client/src/app/shared/features/verify-email/verify-email.component.html @@ -0,0 +1,114 @@ +
+ + + + + +
+ + +
+
+ + + +
+ BookMyVenue +
+ +
+ + + + + + + Back to signup + + + +
+

Verify your email

+

Enter the 6-digit code sent to {{ email() }}

+
+ + + @if (error()) { +
+

{{ error() }}

+
+ } + + +
+
+ @for (control of ['otp1', 'otp2', 'otp3', 'otp4', 'otp5', 'otp6']; track control; let i = $index) { + + } +
+ + + + + +
+ @if (countdown() > 0) { +

Resend code in {{ countdown() }}s

+ } @else { + + } +
+
+
+ +
\ No newline at end of file diff --git a/client/src/app/shared/features/verify-email/verify-email.component.ts b/client/src/app/shared/features/verify-email/verify-email.component.ts new file mode 100644 index 000000000..f7369237e --- /dev/null +++ b/client/src/app/shared/features/verify-email/verify-email.component.ts @@ -0,0 +1,144 @@ +import { Component, inject, signal, ChangeDetectionStrategy, ViewChildren, QueryList, ElementRef } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators, FormGroup, FormControl } from '@angular/forms'; +import { Router, ActivatedRoute, RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { ButtonComponent } from '../../components/button/button.component'; +import { AUTH_ERRORS } from '../../constants/auth-errors.constant'; +import { Subscription, interval, takeWhile } from 'rxjs'; + +@Component({ + selector: 'app-verify-email', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent], + templateUrl: './verify-email.component.html', + styleUrl: './verify-email.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VerifyEmailComponent { + private readonly fb = inject(FormBuilder); + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + private readonly authService = inject(AuthService); + + readonly email = signal(''); + readonly portal = signal<'user' | 'vendor' | 'admin'>('user'); + readonly loading = signal(false); + readonly error = signal(''); + readonly countdown = signal(0); + readonly resendLoading = signal(false); + + private countdownSubscription: Subscription | null = null; + + @ViewChildren('otpInput') otpInputs!: QueryList>; + + readonly form = this.fb.group({ + otp1: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(1)]], + otp2: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(1)]], + otp3: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(1)]], + otp4: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(1)]], + otp5: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(1)]], + otp6: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(1)]], + }); + + constructor() { + const emailParam = this.route.snapshot.queryParamMap.get('email'); + const portalParam = this.route.snapshot.queryParamMap.get('portal') as 'user' | 'vendor' | 'admin' | null; + + if (!emailParam) { + this.router.navigate(['/signup']); + return; + } + + this.email.set(emailParam); + this.portal.set(portalParam || 'user'); + this.startCountdown(); + } + + onInput(event: Event, index: number): void { + const input = event.target as HTMLInputElement; + const value = input.value; + + if (value.length === 1 && index < 5) { + const inputs = this.otpInputs.toArray(); + inputs[index + 1].nativeElement.focus(); + } + } + + onPaste(event: ClipboardEvent): void { + event.preventDefault(); + const pastedData = event.clipboardData?.getData('text'); + if (pastedData && pastedData.length >= 6) { + const otpArray = pastedData.slice(0, 6).split(''); + otpArray.forEach((char, index) => { + const controlName = `otp${index + 1}` as keyof typeof this.form.controls; + this.form.get(controlName)?.setValue(char); + }); + const inputs = this.otpInputs.toArray(); + inputs[5].nativeElement.focus(); + } + } + + onKeyDown(event: KeyboardEvent, index: number): void { + if (event.key === 'Backspace') { + const control = this.form.get(`otp${index + 1}`); + if (control?.value === '' && index > 0) { + const inputs = this.otpInputs.toArray(); + inputs[index - 1].nativeElement.focus(); + } + } + } + + onSubmit(): void { + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } + + const otp = Object.values(this.form.value).join(''); + this.loading.set(true); + this.error.set(''); + + this.authService.verifyEmail({ email: this.email(), otp }).subscribe({ + next: () => { + this.loading.set(false); + this.router.navigate(['/login'], { queryParams: { portal: this.portal() } }); + }, + error: (err) => { + this.loading.set(false); + if (err.error?.code === AUTH_ERRORS.INVALID_OTP || err.error?.code === AUTH_ERRORS.OTP_EXPIRED) { + this.error.set(err.error?.message || 'Invalid or expired OTP'); + } else { + this.error.set('Verification failed. Please try again.'); + } + }, + }); + } + + onResend(): void { + if (this.countdown() > 0 || this.resendLoading()) return; + + this.resendLoading.set(true); + this.error.set(''); + + this.authService.resendOtp({ email: this.email() }).subscribe({ + next: () => { + this.resendLoading.set(false); + this.startCountdown(); + }, + error: () => { + this.resendLoading.set(false); + this.error.set('Failed to resend OTP. Please try again.'); + }, + }); + } + + private startCountdown(): void { + this.countdown.set(30); + this.countdownSubscription?.unsubscribe(); + this.countdownSubscription = interval(1000) + .pipe(takeWhile(() => this.countdown() > 0)) + .subscribe(() => { + this.countdown.update((v) => v - 1); + }); + } +} \ No newline at end of file diff --git a/client/src/app/shared/models/auth-response.model.ts b/client/src/app/shared/models/auth-response.model.ts index ee04c4537..7b0be010d 100644 --- a/client/src/app/shared/models/auth-response.model.ts +++ b/client/src/app/shared/models/auth-response.model.ts @@ -5,6 +5,8 @@ export interface AuthResponse { role: string; accessToken?: string; refreshToken?: string; + emailVerified?: boolean; + requiresVerification?: boolean; } export interface AuthUser { @@ -45,4 +47,22 @@ export interface RefreshTokenApiResponse { role: string; accessToken: string; refreshToken: string; + emailVerified?: boolean; + requiresVerification?: boolean; +} + +// Phase 2: OTP Email Verification +export interface VerifyEmailRequest { + email: string; + otp: string; +} + +export interface ResendOtpRequest { + email: string; +} + +export interface OtpResponse { + success: boolean; + message: string; + expiresIn?: number; } diff --git a/client/src/app/shared/services/auth.repository.ts b/client/src/app/shared/services/auth.repository.ts index 3a8214bf8..e5d9b7949 100644 --- a/client/src/app/shared/services/auth.repository.ts +++ b/client/src/app/shared/services/auth.repository.ts @@ -10,6 +10,9 @@ import { ForgotPasswordRequest, ResetPasswordRequest, RefreshTokenApiResponse, + VerifyEmailRequest, + ResendOtpRequest, + OtpResponse, } from '../models/auth-response.model'; import { ApiResponse } from '../models/api-response.model'; @@ -74,4 +77,18 @@ signup(payload: SignupRequest): Observable { payload ); } + + verifyEmail(payload: VerifyEmailRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.AUTH.VERIFY_EMAIL}`, + payload + ); + } + + resendOtp(payload: ResendOtpRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.AUTH.RESEND_VERIFICATION}`, + payload + ); + } } diff --git a/client/src/app/shared/services/auth.service.ts b/client/src/app/shared/services/auth.service.ts index 94ae19fe7..98bc0a477 100644 --- a/client/src/app/shared/services/auth.service.ts +++ b/client/src/app/shared/services/auth.service.ts @@ -9,10 +9,23 @@ import { LoginRequest, SignupRequest, ForgotPasswordRequest, + VerifyEmailRequest, + ResendOtpRequest, + OtpResponse, } from '../models/auth-response.model'; import { UserRole } from '../enums/user-role.enum'; +import { AUTH_ERRORS } from '../constants/auth-errors.constant'; import { map, Observable } from 'rxjs'; +export class EmailNotVerifiedError extends Error { + readonly email: string; + constructor(email: string) { + super('Email not verified'); + this.name = 'EmailNotVerifiedError'; + this.email = email; + } +} + @Injectable({ providedIn: 'root' }) export class AuthService { private readonly authRepository = inject(AuthRepository); @@ -23,6 +36,7 @@ export class AuthService { readonly userSession = signal(null); readonly vendorSession = signal(null); readonly adminSession = signal(null); + readonly pendingVerificationEmail = signal(null); readonly currentUser = computed(() => { const portal = this.detectPortal(); @@ -139,6 +153,12 @@ export class AuthService { this.loading.set(true); this.authRepository.login(payload).subscribe({ next: (response) => { + if (response.requiresVerification && !response.emailVerified) { + this.loading.set(false); + this.pendingVerificationEmail.set(payload.email); + this.notification.error('Please verify your email first'); + return; + } const user = this.toAuthUser(response); const portal = response.role === UserRole.Vendor ? 'vendor' : 'user'; this.savePortalSession(portal, response.accessToken || '', response.refreshToken || '', user); @@ -146,7 +166,13 @@ export class AuthService { this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); this.loading.set(false); }, - error: () => { + error: (err) => { + if (err.status === 403 && err.error?.code === AUTH_ERRORS.EMAIL_NOT_VERIFIED) { + this.loading.set(false); + this.pendingVerificationEmail.set(payload.email); + this.notification.error('Please verify your email first'); + return; + } this.notification.error('Invalid credentials'); this.loading.set(false); }, @@ -157,35 +183,39 @@ export class AuthService { this.loading.set(true); this.authRepository.adminLogin(payload).subscribe({ next: (response) => { + if (response.requiresVerification && !response.emailVerified) { + this.loading.set(false); + this.pendingVerificationEmail.set(payload.email); + this.notification.error('Please verify your email first'); + return; + } const user = this.toAuthUser(response); this.savePortalSession('admin', response.accessToken || '', response.refreshToken || '', user); this.notification.success('Login successful'); this.router.navigate(['/admin/dashboard']); this.loading.set(false); }, - error: () => { + error: (err) => { + if (err.status === 403 && err.error?.code === AUTH_ERRORS.EMAIL_NOT_VERIFIED) { + this.loading.set(false); + this.pendingVerificationEmail.set(payload.email); + this.notification.error('Please verify your email first'); + return; + } this.notification.error('Invalid credentials'); this.loading.set(false); }, }); } - signup(payload: SignupRequest): void { + signup(payload: SignupRequest): Observable { this.loading.set(true); - this.authRepository.signup(payload).subscribe({ - next: (response) => { - const user = this.toAuthUser(response); - const portal = response.role === UserRole.Vendor ? 'vendor' : 'user'; - this.savePortalSession(portal, response.accessToken || '', response.refreshToken || '', user); - this.notification.success(portal === 'vendor' ? 'Vendor account created successfully' : 'Account created successfully'); - this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); + return this.authRepository.signup(payload).pipe( + map((response) => { this.loading.set(false); - }, - error: () => { - this.notification.error('Signup failed. Please try again.'); - this.loading.set(false); - }, - }); + return response; + }) + ); } forgotPassword(payload: ForgotPasswordRequest): void { @@ -229,4 +259,16 @@ handleLogout(): void { this.router.navigate(['/login']); } +verifyEmail(payload: VerifyEmailRequest): Observable { + return this.authRepository.verifyEmail(payload); +} + +resendOtp(payload: ResendOtpRequest): Observable { + return this.authRepository.resendOtp(payload); +} + +clearPendingVerification(): void { + this.pendingVerificationEmail.set(null); +} + } diff --git a/client/src/app/user/features/login/login.component.html b/client/src/app/user/features/login/login.component.html index 2d2fb10af..0ab6b0c41 100644 --- a/client/src/app/user/features/login/login.component.html +++ b/client/src/app/user/features/login/login.component.html @@ -122,6 +122,18 @@

Welcome back

[fullWidth]="true" size="lg" /> + + @if (showVerifyButton()) { + + }

diff --git a/client/src/app/user/features/login/login.component.ts b/client/src/app/user/features/login/login.component.ts index 6d7206366..2ace0e81b 100644 --- a/client/src/app/user/features/login/login.component.ts +++ b/client/src/app/user/features/login/login.component.ts @@ -1,6 +1,6 @@ -import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, ChangeDetectionStrategy, computed } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../../shared/services/auth.service'; import { ButtonComponent } from '../../../shared/components/button/button.component'; import { InputComponent } from '../../../shared/components/input/input.component'; @@ -15,9 +15,13 @@ import { InputComponent } from '../../../shared/components/input/input.component }) export class LoginComponent { private readonly fb = inject(FormBuilder); + private readonly router = inject(Router); private readonly authService = inject(AuthService); readonly loading = this.authService.loading; + readonly pendingVerificationEmail = this.authService.pendingVerificationEmail; + + readonly showVerifyButton = computed(() => this.pendingVerificationEmail() !== null); readonly panelFeatures = [ 'Browse 500+ verified venues across India', @@ -37,4 +41,18 @@ export class LoginComponent { this.form.markAllAsTouched(); } } + + onVerify(): void { + const email = this.pendingVerificationEmail(); + if (email) { + this.authService.clearPendingVerification(); + this.authService.resendOtp({ email }).subscribe({ + next: () => { + this.router.navigate(['/verify-email'], { + queryParams: { email, portal: 'user' }, + }); + }, + }); + } + } } diff --git a/client/src/app/user/features/signup/signup.component.ts b/client/src/app/user/features/signup/signup.component.ts index 7a44f211b..30a3f3e3e 100644 --- a/client/src/app/user/features/signup/signup.component.ts +++ b/client/src/app/user/features/signup/signup.component.ts @@ -1,6 +1,6 @@ import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../../shared/services/auth.service'; import { ButtonComponent } from '../../../shared/components/button/button.component'; import { InputComponent } from '../../../shared/components/input/input.component'; @@ -16,6 +16,7 @@ import { AppValidators } from '../../../shared/utils/validators'; }) export class SignupComponent { private readonly fb = inject(FormBuilder); + private readonly router = inject(Router); private readonly authService = inject(AuthService); readonly loading = this.authService.loading; @@ -35,7 +36,14 @@ export class SignupComponent { onSubmit(): void { if (this.form.valid) { const { confirmPassword, ...payload } = this.form.getRawValue(); - this.authService.signup(payload); + this.authService.signup(payload).subscribe({ + next: () => { + const portal = payload.isVendor ? 'vendor' : 'user'; + this.router.navigate(['/verify-email'], { + queryParams: { email: payload.email, portal }, + }); + }, + }); } else { this.form.markAllAsTouched(); } From 80e94d0b718271a00e30229cd9ac3b20e94ac9e2 Mon Sep 17 00:00:00 2001 From: Anzyll Date: Fri, 3 Jul 2026 15:01:28 +0530 Subject: [PATCH 53/59] fix(flyway): resolve duplicate migration versions --- ...tage_to_venue.sql => V10__add_advance_percentage_to_venue.sql} | 0 ...7__seed_venue_categories.sql => V8__seed_venue_categories.sql} | 0 .../{V8__add_payment_type.sql => V9__add_payment_type.sql} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename server/src/main/resources/db/migration/{V9__add_advance_percentage_to_venue.sql => V10__add_advance_percentage_to_venue.sql} (100%) rename server/src/main/resources/db/migration/{V7__seed_venue_categories.sql => V8__seed_venue_categories.sql} (100%) rename server/src/main/resources/db/migration/{V8__add_payment_type.sql => V9__add_payment_type.sql} (100%) diff --git a/server/src/main/resources/db/migration/V9__add_advance_percentage_to_venue.sql b/server/src/main/resources/db/migration/V10__add_advance_percentage_to_venue.sql similarity index 100% rename from server/src/main/resources/db/migration/V9__add_advance_percentage_to_venue.sql rename to server/src/main/resources/db/migration/V10__add_advance_percentage_to_venue.sql diff --git a/server/src/main/resources/db/migration/V7__seed_venue_categories.sql b/server/src/main/resources/db/migration/V8__seed_venue_categories.sql similarity index 100% rename from server/src/main/resources/db/migration/V7__seed_venue_categories.sql rename to server/src/main/resources/db/migration/V8__seed_venue_categories.sql diff --git a/server/src/main/resources/db/migration/V8__add_payment_type.sql b/server/src/main/resources/db/migration/V9__add_payment_type.sql similarity index 100% rename from server/src/main/resources/db/migration/V8__add_payment_type.sql rename to server/src/main/resources/db/migration/V9__add_payment_type.sql From f236bc036a71bb6bd70784378cba37d20e10fb18 Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Sat, 4 Jul 2026 02:22:44 +0530 Subject: [PATCH 54/59] add forgot password --- .../auth/controller/AuthController.java | 35 +++++++++++ .../dto/request/ForgotPasswordRequest.java | 18 ++++++ .../auth/dto/request/RegisterRequest.java | 4 +- .../dto/request/ResetPasswordRequest.java | 31 ++++++++++ .../server/auth/service/AuthService.java | 5 ++ .../server/auth/service/AuthServiceImpl.java | 61 +++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 26 ++++++++ .../mail/service/MailService.java | 2 +- .../mail/service/MailServiceImpl.java | 17 ++++++ .../server/verification/util/RedisKeys.java | 3 + .../migration/V8__seed_venue_categories.sql | 3 +- 11 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/request/ForgotPasswordRequest.java create mode 100644 server/src/main/java/com/bookmyvenue/server/auth/dto/request/ResetPasswordRequest.java diff --git a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java index 61cebab4c..40dd0173b 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/controller/AuthController.java @@ -1,7 +1,9 @@ package com.bookmyvenue.server.auth.controller; +import com.bookmyvenue.server.auth.dto.request.ForgotPasswordRequest; import com.bookmyvenue.server.auth.dto.request.LoginRequest; import com.bookmyvenue.server.auth.dto.request.RegisterRequest; +import com.bookmyvenue.server.auth.dto.request.ResetPasswordRequest; import com.bookmyvenue.server.auth.dto.response.AuthResponse; import com.bookmyvenue.server.auth.dto.response.AuthResult; import com.bookmyvenue.server.auth.security.CookieUtils; @@ -106,4 +108,37 @@ public ResponseEntity logout() { .build(); } + @Operation(summary = "Forgot password", description = "Sends a password reset OTP to the registered email address.") + @ApiResponses({@ApiResponse(responseCode = "200", description = "Password reset OTP sent successfully"), + @ApiResponse(responseCode = "404", description = "User not found", + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))}) + @PostMapping("/forgot-password") + public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { + + MessageResponse response = authService.forgotPassword(request.getEmail()); + return ResponseEntity.ok(response); + } + + @Operation(summary = "Reset password", description = "Resets the user's password using a valid OTP.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Password reset successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired OTP", + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "User not found", + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))) + }) + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + + MessageResponse response = authService.resetPassword( + request.getEmail(), + request.getOtp(), + request.getNewPassword() + ); + + return ResponseEntity.ok(response); + } + + + } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/ForgotPasswordRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/ForgotPasswordRequest.java new file mode 100644 index 000000000..d57e668e4 --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/ForgotPasswordRequest.java @@ -0,0 +1,18 @@ +package com.bookmyvenue.server.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "Forgot password request") +public class ForgotPasswordRequest { + + @Schema(description = "Registered email address", example = "ajay@gmail.com") + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + private String email; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java index 9d289286e..810118b7e 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/RegisterRequest.java @@ -30,8 +30,10 @@ public class RegisterRequest { @NotBlank(message = "Phone number is required") private String phone; - @Schema(description = "Account password", example = "password123") + @Schema(description = "Account password", example = "Password@123") @NotBlank(message = "Password is required") + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,100}$", + message = "Password must contain at least 8 characters, one uppercase letter, one lowercase letter, one number and one special character") private String password; @Schema(description = "Account role", example = "USER", allowableValues = {"USER", "VENDOR"}) diff --git a/server/src/main/java/com/bookmyvenue/server/auth/dto/request/ResetPasswordRequest.java b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/ResetPasswordRequest.java new file mode 100644 index 000000000..bb1e54f4d --- /dev/null +++ b/server/src/main/java/com/bookmyvenue/server/auth/dto/request/ResetPasswordRequest.java @@ -0,0 +1,31 @@ +package com.bookmyvenue.server.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "Reset password request") +public class ResetPasswordRequest { + + @Schema(description = "Registered email address", example = "ajay@gmail.com") + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + private String email; + + @Schema(description = "OTP sent to the registered email", example = "123456") + @NotBlank(message = "OTP is required") + private String otp; + + @Schema(description = "New account password", example = "Password@123") + @NotBlank(message = "Password is required") + @Pattern( + regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,100}$", + message = "Password must contain at least 8 characters, one uppercase letter, one lowercase letter, one number and one special character" + ) + private String newPassword; +} \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java index 056f6cecf..f75c57684 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthService.java @@ -23,4 +23,9 @@ public interface AuthService { * Generates a new access token using a valid refresh token. */ AuthResult refreshToken(String refreshToken); + + + MessageResponse forgotPassword(String email); + + MessageResponse resetPassword(String email,String otp, String newPassword); } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java index dbcba31d6..8742e696b 100644 --- a/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/auth/service/AuthServiceImpl.java @@ -10,7 +10,11 @@ import com.bookmyvenue.server.user.entity.User; import com.bookmyvenue.server.user.enums.Role; import com.bookmyvenue.server.user.repository.UserRepository; +import com.bookmyvenue.server.verification.mail.service.MailService; +import com.bookmyvenue.server.verification.service.OtpRedisService; import com.bookmyvenue.server.verification.service.VerificationService; +import com.bookmyvenue.server.verification.util.OtpGenerator; +import com.bookmyvenue.server.verification.util.RedisKeys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; @@ -26,6 +30,9 @@ public class AuthServiceImpl implements AuthService { private final PasswordEncoder passwordEncoder; private final JwtService jwtService; private final VerificationService verificationService; + private final OtpGenerator otpGenerator; + private final OtpRedisService otpRedisService; + private final MailService mailService; @Override public MessageResponse register(RegisterRequest request) { @@ -137,4 +144,58 @@ public AuthResult refreshToken(String refreshToken) { newRefreshToken ); } + + @Override + public MessageResponse forgotPassword(String email) { + + log.info("Forgot password requested for {}", email); + User user = userRepository.findByEmail(email) + .orElseThrow(() -> + new BusinessException(ErrorCode.USER_NOT_FOUND)); + + String otp = otpGenerator.generateOtp(); + String key = RedisKeys.passwordReset(email); + otpRedisService.saveOtp(key, otp); + mailService.sendPasswordResetOtp(email, otp); + + log.info("Password reset OTP sent to {}", email); + return MessageResponse.builder() + .message("Password reset OTP sent successfully.") + .build(); + } + + @Override + public MessageResponse resetPassword(String email, String otp, String newPassword) + { + + log.info("Password reset requested for {}", email); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> + new BusinessException(ErrorCode.USER_NOT_FOUND)); + + + String key = RedisKeys.passwordReset(email); + String savedOtp = otpRedisService.getOtp(key); + + if (savedOtp == null) { + throw new BusinessException(ErrorCode.OTP_EXPIRED); + } + + if (!savedOtp.equals(otp)) { + throw new BusinessException(ErrorCode.INVALID_OTP); + } + + user.setPassword(passwordEncoder.encode(newPassword)); + userRepository.save(user); + otpRedisService.deleteOtp(key); + log.info("Password reset successfully for {}", email); + return MessageResponse.builder() + .message("Password reset successful.") + .build(); + } + + + + } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java index 295327163..9cf5a083f 100644 --- a/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java +++ b/server/src/main/java/com/bookmyvenue/server/common/exception/GlobalExceptionHandler.java @@ -7,6 +7,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -108,4 +110,28 @@ private ApiErrorResponse buildErrorResponse( .timestamp(LocalDateTime.now()) .build(); } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException( + MethodArgumentNotValidException ex, + HttpServletRequest request + ) { + String message = ex.getBindingResult() + .getFieldErrors() + .stream() + .findFirst() + .map(FieldError::getDefaultMessage) + .orElse("Validation failed"); + + ApiErrorResponse response = ApiErrorResponse.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .code(ErrorCode.BAD_REQUEST.getCode()) + .error(HttpStatus.BAD_REQUEST.getReasonPhrase()) + .message(message) + .path(request.getRequestURI()) + .timestamp(LocalDateTime.now()) + .build(); + + return ResponseEntity.badRequest().body(response); + } } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailService.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailService.java index 4cf02d97b..68f566149 100644 --- a/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailService.java +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailService.java @@ -6,6 +6,6 @@ public interface MailService { void sendVerificationOtp(String email, String otp); - + void sendPasswordResetOtp(String email, String otp); } diff --git a/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailServiceImpl.java b/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailServiceImpl.java index d14974af4..06ac33f74 100644 --- a/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailServiceImpl.java +++ b/server/src/main/java/com/bookmyvenue/server/verification/mail/service/MailServiceImpl.java @@ -24,4 +24,21 @@ public void sendVerificationOtp(String email, String otp) { brevoClient.sendEmail(email, subject, html); } + + + @Override + public void sendPasswordResetOtp(String email, String otp) { + + String subject = "Reset your BookMyVenue password"; + + String html = """ +

Password Reset

+

Your password reset code is:

+

%s

+

This code will expire in 5 minutes.

+

If you did not request a password reset, you can safely ignore this email.

+ """.formatted(otp); + + brevoClient.sendEmail(email, subject, html); + } } \ No newline at end of file diff --git a/server/src/main/java/com/bookmyvenue/server/verification/util/RedisKeys.java b/server/src/main/java/com/bookmyvenue/server/verification/util/RedisKeys.java index ae79ad5aa..08e6cf6cf 100644 --- a/server/src/main/java/com/bookmyvenue/server/verification/util/RedisKeys.java +++ b/server/src/main/java/com/bookmyvenue/server/verification/util/RedisKeys.java @@ -13,4 +13,7 @@ public static String verifiedEmail(String email) { return "verified:email:" + email; } + public static String passwordReset(String email) { + return "reset:password:" + email; + } } \ No newline at end of file diff --git a/server/src/main/resources/db/migration/V8__seed_venue_categories.sql b/server/src/main/resources/db/migration/V8__seed_venue_categories.sql index e8e5abb2c..9aec01431 100644 --- a/server/src/main/resources/db/migration/V8__seed_venue_categories.sql +++ b/server/src/main/resources/db/migration/V8__seed_venue_categories.sql @@ -11,4 +11,5 @@ VALUES ('Farm House', 'Farm houses for private events'), ('Beach Venue', 'Beachside venues for weddings and parties'), ('Rooftop Venue', 'Open rooftop event spaces'), - ('Community Hall', 'Community-owned multipurpose halls'); \ No newline at end of file + ('Community Hall', 'Community-owned multipurpose halls') + ON CONFLICT (name) DO NOTHING; \ No newline at end of file From a5802b339366082d59eb01dbb9573d62042e027b Mon Sep 17 00:00:00 2001 From: Ajay Velanchery Date: Sun, 5 Jul 2026 14:53:54 +0530 Subject: [PATCH 55/59] fixed redis issue --- server/docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/docker-compose.yml b/server/docker-compose.yml index a8d5e3089..58743d0ed 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -34,6 +34,9 @@ services: SPRING_DATASOURCE_USERNAME: ${DB_USERNAME} SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} + SPRING_DATA_REDIS_HOST: redis + SPRING_DATA_REDIS_PORT: 6379 + JWT_SECRET: ${JWT_SECRET} JWT_ACCESS_TOKEN_EXPIRATION: ${JWT_ACCESS_TOKEN_EXPIRATION} JWT_REFRESH_TOKEN_EXPIRATION: ${JWT_REFRESH_TOKEN_EXPIRATION} From 1558c366126e0dc6b88632b78c2c431009cb578b Mon Sep 17 00:00:00 2001 From: syamjith45 Date: Mon, 6 Jul 2026 14:16:30 +0530 Subject: [PATCH 56/59] feat: implement core auth infrastructure including storage service, route guards, interceptors, and notification system --- client/src/app/app.html | 1 + client/src/app/app.ts | 3 +- client/src/app/core/app.routes.ts | 5 + .../notification/notification.component.css | 139 ++++++++++++++++++ .../notification/notification.component.html | 47 ++++++ .../notification/notification.component.ts | 20 +++ .../shared/constants/route-paths.constant.ts | 1 + .../shared/interceptors/error.interceptor.ts | 19 ++- .../app/shared/models/auth-response.model.ts | 6 +- .../app/shared/services/auth.repository.ts | 24 ++- .../src/app/shared/services/auth.service.ts | 44 +++++- .../app/shared/services/storage.service.ts | 24 ++- .../forgot-password.component.ts | 7 +- .../reset-password.component.css | 1 + .../reset-password.component.html | 65 ++++++++ .../reset-password.component.ts | 44 ++++++ .../interceptors/user-auth.interceptor.ts | 20 ++- .../forgot-password.component.ts | 8 +- 18 files changed, 447 insertions(+), 31 deletions(-) create mode 100644 client/src/app/shared/components/notification/notification.component.css create mode 100644 client/src/app/shared/components/notification/notification.component.html create mode 100644 client/src/app/shared/components/notification/notification.component.ts create mode 100644 client/src/app/user/features/reset-password/reset-password.component.css create mode 100644 client/src/app/user/features/reset-password/reset-password.component.html create mode 100644 client/src/app/user/features/reset-password/reset-password.component.ts diff --git a/client/src/app/app.html b/client/src/app/app.html index 67e7bd4cd..cb44d97ec 100644 --- a/client/src/app/app.html +++ b/client/src/app/app.html @@ -1 +1,2 @@ + diff --git a/client/src/app/app.ts b/client/src/app/app.ts index 547fb8c54..81874a460 100644 --- a/client/src/app/app.ts +++ b/client/src/app/app.ts @@ -1,9 +1,10 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { NotificationComponent } from './shared/components/notification/notification.component'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, NotificationComponent], templateUrl: './app.html', styleUrl: './app.css', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/client/src/app/core/app.routes.ts b/client/src/app/core/app.routes.ts index 2b7760a9a..77aa35590 100644 --- a/client/src/app/core/app.routes.ts +++ b/client/src/app/core/app.routes.ts @@ -23,6 +23,11 @@ export const routes: Routes = [ canActivate: [guestGuard], loadComponent: () => import('../user/features/forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent), }, + { + path: 'reset-password', + canActivate: [guestGuard], + loadComponent: () => import('../user/features/reset-password/reset-password.component').then(m => m.ResetPasswordComponent), + }, { path: 'verify-email', canActivate: [guestGuard], diff --git a/client/src/app/shared/components/notification/notification.component.css b/client/src/app/shared/components/notification/notification.component.css new file mode 100644 index 000000000..9215f3560 --- /dev/null +++ b/client/src/app/shared/components/notification/notification.component.css @@ -0,0 +1,139 @@ +.notification-container { + position: fixed; + top: 1.5rem; + right: 1.5rem; + z-index: 100; + display: flex; + flex-direction: column; + gap: 0.75rem; + max-width: 24rem; + width: calc(100% - 3rem); + pointer-events: none; +} + +.notification-toast { + pointer-events: auto; + display: flex; + align-items: flex-start; + gap: 0.875rem; + padding: 1rem; + border-radius: 1rem; + border-width: 1px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +.toast-icon { + flex-shrink: 0; + width: 1.25rem; + height: 1.25rem; +} + +.toast-icon svg { + width: 100%; + height: 100%; +} + +.toast-content { + flex-grow: 1; +} + +.toast-message { + margin: 0; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.25rem; + color: #1f2937; +} + +.toast-close { + flex-shrink: 0; + width: 1.25rem; + height: 1.25rem; + padding: 0; + background: transparent; + border: none; + color: #9ca3af; + cursor: pointer; + border-radius: 0.375rem; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.toast-close:hover { + color: #4b5563; + background-color: rgba(0, 0, 0, 0.05); +} + +.toast-close svg { + width: 1rem; + height: 1rem; +} + +/* Success Toast */ +.notification-toast.success { + background-color: rgba(236, 253, 245, 0.9); + border-color: rgba(16, 185, 129, 0.2); +} +.notification-toast.success .toast-icon { + color: #059669; +} +.notification-toast.success .toast-message { + color: #065f46; +} + +/* Error Toast */ +.notification-toast.error { + background-color: rgba(254, 242, 242, 0.9); + border-color: rgba(239, 68, 68, 0.2); +} +.notification-toast.error .toast-icon { + color: #dc2626; +} +.notification-toast.error .toast-message { + color: #991b1b; +} + +/* Warning Toast */ +.notification-toast.warning { + background-color: rgba(255, 251, 235, 0.9); + border-color: rgba(245, 158, 11, 0.2); +} +.notification-toast.warning .toast-icon { + color: #d97706; +} +.notification-toast.warning .toast-message { + color: #92400e; +} + +/* Info Toast */ +.notification-toast.info { + background-color: rgba(240, 249, 255, 0.9); + border-color: rgba(14, 165, 233, 0.2); +} +.notification-toast.info .toast-icon { + color: #0284c7; +} +.notification-toast.info .toast-message { + color: #075985; +} + +/* Animations */ +@keyframes slide-in-right { + from { + opacity: 0; + transform: translateX(1.5rem); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.animate-slide-in { + animation: slide-in-right 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} diff --git a/client/src/app/shared/components/notification/notification.component.html b/client/src/app/shared/components/notification/notification.component.html new file mode 100644 index 000000000..2f4cd9e83 --- /dev/null +++ b/client/src/app/shared/components/notification/notification.component.html @@ -0,0 +1,47 @@ +
+ @for (notification of notifications(); track notification.id) { + + } +
diff --git a/client/src/app/shared/components/notification/notification.component.ts b/client/src/app/shared/components/notification/notification.component.ts new file mode 100644 index 000000000..d55e61318 --- /dev/null +++ b/client/src/app/shared/components/notification/notification.component.ts @@ -0,0 +1,20 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { NgClass } from '@angular/common'; +import { NotificationService } from '../../services/notification.service'; + +@Component({ + selector: 'app-notification', + standalone: true, + imports: [NgClass], + templateUrl: './notification.component.html', + styleUrl: './notification.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NotificationComponent { + readonly notificationService = inject(NotificationService); + readonly notifications = this.notificationService.notifications; + + dismiss(id: string): void { + this.notificationService.dismiss(id); + } +} diff --git a/client/src/app/shared/constants/route-paths.constant.ts b/client/src/app/shared/constants/route-paths.constant.ts index 28413d787..d8f2f585f 100644 --- a/client/src/app/shared/constants/route-paths.constant.ts +++ b/client/src/app/shared/constants/route-paths.constant.ts @@ -4,6 +4,7 @@ export const ROUTE_PATHS = { LOGIN: 'login', SIGNUP: 'register', FORGOT_PASSWORD: 'forgot-password', + RESET_PASSWORD: 'reset-password', DASHBOARD: 'dashboard', VENUES: 'venues', VENUE_DETAILS: 'venues/:id', diff --git a/client/src/app/shared/interceptors/error.interceptor.ts b/client/src/app/shared/interceptors/error.interceptor.ts index f7174f735..e014ed470 100644 --- a/client/src/app/shared/interceptors/error.interceptor.ts +++ b/client/src/app/shared/interceptors/error.interceptor.ts @@ -12,7 +12,10 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { catchError((error: HttpErrorResponse) => { // 401 = access token expired — try refresh then retry // 403 is NOT a token error; it means the user lacks permission — do not refresh + // Exclude auth endpoints from auto-refresh if (error.status === 401 && + !req.url.includes('/auth/login') && + !req.url.includes('/auth/register') && !req.url.includes('/auth/refresh-token') && !req.url.includes('/auth/logout')) { @@ -27,16 +30,22 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { ); } - let message = 'An unexpected error occurred'; + let message = error.error?.message || 'An unexpected error occurred'; switch (error.status) { case 0: message = 'Unable to connect to server'; break; - case 403: message = 'You do not have permission to perform this action'; break; - case 404: message = 'Resource not found'; break; - case 422: message = error.error?.message || 'Validation error'; break; + case 403: message = error.error?.message; break; + case 404: message = error.error?.message || 'Resource not found'; break; case 500: message = 'Server error. Please try again later.'; break; } - notification.error(message); + // Do not display global notifications for auth endpoints - they handle their own errors + const isAuthEndpoint = req.url.includes('/auth/login') || + req.url.includes('/auth/register') || + req.url.includes('/auth/forgot-password') || + req.url.includes('/auth/reset-password'); + if (!isAuthEndpoint && message) { + notification.error(message); + } return throwError(() => error); }) ); diff --git a/client/src/app/shared/models/auth-response.model.ts b/client/src/app/shared/models/auth-response.model.ts index 7b0be010d..f47f5da76 100644 --- a/client/src/app/shared/models/auth-response.model.ts +++ b/client/src/app/shared/models/auth-response.model.ts @@ -35,9 +35,9 @@ export interface ForgotPasswordRequest { } export interface ResetPasswordRequest { - token: string; - password: string; - confirmPassword: string; + email: string; + otp: string; + newPassword: string; } export interface RefreshTokenApiResponse { diff --git a/client/src/app/shared/services/auth.repository.ts b/client/src/app/shared/services/auth.repository.ts index e5d9b7949..e6803b140 100644 --- a/client/src/app/shared/services/auth.repository.ts +++ b/client/src/app/shared/services/auth.repository.ts @@ -24,14 +24,16 @@ export class AuthRepository { login(payload: LoginRequest): Observable { return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.LOGIN}`, - payload + payload, + { withCredentials: true } ); } adminLogin(payload: LoginRequest): Observable { return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.ADMIN_LOGIN}`, - payload + payload, + { withCredentials: true } ); } @@ -53,42 +55,48 @@ signup(payload: SignupRequest): Observable { logout(): Observable { return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.LOGOUT}`, - {} + {}, + { withCredentials: true } ); } refreshToken(): Observable { return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.REFRESH_TOKEN}`, - {} + {}, + { withCredentials: true } ); } forgotPassword(payload: ForgotPasswordRequest): Observable> { return this.http.post>( `${this.apiUrl}${API_ENDPOINTS.AUTH.FORGOT_PASSWORD}`, - payload + payload, + { withCredentials: true } ); } resetPassword(payload: ResetPasswordRequest): Observable> { return this.http.post>( `${this.apiUrl}${API_ENDPOINTS.AUTH.RESET_PASSWORD}`, - payload + payload, + { withCredentials: true } ); } verifyEmail(payload: VerifyEmailRequest): Observable { return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.VERIFY_EMAIL}`, - payload + payload, + { withCredentials: true } ); } resendOtp(payload: ResendOtpRequest): Observable { return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.RESEND_VERIFICATION}`, - payload + payload, + { withCredentials: true } ); } } diff --git a/client/src/app/shared/services/auth.service.ts b/client/src/app/shared/services/auth.service.ts index 98bc0a477..00ab87a7b 100644 --- a/client/src/app/shared/services/auth.service.ts +++ b/client/src/app/shared/services/auth.service.ts @@ -61,9 +61,10 @@ export class AuthService { } private detectPortal(): string { - const url = this.router.url || window.location.pathname; - if (url.startsWith('/admin')) return 'admin'; - if (url.startsWith('/vendor')) return 'vendor'; + const url = window.location.pathname || ''; + const routerUrl = this.router.url || ''; + if (url.startsWith('/admin') || routerUrl.startsWith('/admin')) return 'admin'; + if (url.startsWith('/vendor') || routerUrl.startsWith('/vendor')) return 'vendor'; return 'user'; } @@ -103,6 +104,7 @@ export class AuthService { this.storage.remove(keys.token); this.storage.remove(keys.refresh); this.storage.remove(keys.user); + this.storage.remove('accessToken'); if (portal === 'admin') this.adminSession.set(null); else if (portal === 'vendor') this.vendorSession.set(null); @@ -161,7 +163,10 @@ export class AuthService { } const user = this.toAuthUser(response); const portal = response.role === UserRole.Vendor ? 'vendor' : 'user'; - this.savePortalSession(portal, response.accessToken || '', response.refreshToken || '', user); + // Tokens are in cookies - read from storage (cookies are set by browser via Set-Cookie header) + const token = this.storage.get('accessToken') || ''; + const refreshToken = this.storage.get('refreshToken') || ''; + this.savePortalSession(portal, token, refreshToken, user); this.notification.success('Login successful'); this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); this.loading.set(false); @@ -190,7 +195,10 @@ export class AuthService { return; } const user = this.toAuthUser(response); - this.savePortalSession('admin', response.accessToken || '', response.refreshToken || '', user); + // Tokens are in cookies - read from storage (cookies are set by browser via Set-Cookie header) + const token = this.storage.get('accessToken') || ''; + const refreshToken = this.storage.get('refreshToken') || ''; + this.savePortalSession('admin', token, refreshToken, user); this.notification.success('Login successful'); this.router.navigate(['/admin/dashboard']); this.loading.set(false); @@ -218,15 +226,35 @@ export class AuthService { ); } - forgotPassword(payload: ForgotPasswordRequest): void { + forgotPassword(payload: ForgotPasswordRequest, onSuccess?: () => void): void { this.loading.set(true); this.authRepository.forgotPassword(payload).subscribe({ next: () => { - this.notification.success('Password reset email sent'); + this.notification.success('OTP sent to your email'); this.loading.set(false); + if (onSuccess) { + onSuccess(); + } + }, + error: () => { + this.notification.error('Failed to send OTP'); + this.loading.set(false); + }, + }); + } + + resetPassword(payload: { email: string; otp: string; newPassword: string }, onSuccess?: () => void): void { + this.loading.set(true); + this.authRepository.resetPassword(payload).subscribe({ + next: () => { + this.notification.success('Password reset successful'); + this.loading.set(false); + if (onSuccess) { + onSuccess(); + } }, error: () => { - this.notification.error('Failed to send reset email'); + this.notification.error('Failed to reset password'); this.loading.set(false); }, }); diff --git a/client/src/app/shared/services/storage.service.ts b/client/src/app/shared/services/storage.service.ts index ab824e2c8..8fa518cfa 100644 --- a/client/src/app/shared/services/storage.service.ts +++ b/client/src/app/shared/services/storage.service.ts @@ -2,19 +2,41 @@ import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class StorageService { + private readonly cookieOptions = ';path=/;SameSite=Lax'; + get(key: string): string | null { + // Try cookie first, then localStorage + const cookieValue = this.getCookie(key); + if (cookieValue) return cookieValue; return localStorage.getItem(key); } - set(key: string, value: string): void { + set(key: string, value: string, useCookie = false): void { + if (useCookie) { + this.setCookie(key, value); + } localStorage.setItem(key, value); } remove(key: string): void { + this.deleteCookie(key); localStorage.removeItem(key); } clear(): void { localStorage.clear(); } + + private getCookie(name: string): string | null { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? decodeURIComponent(match[2]) : null; + } + + private setCookie(name: string, value: string): void { + document.cookie = `${name}=${encodeURIComponent(value)}${this.cookieOptions}`; + } + + private deleteCookie(name: string): void { + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC${this.cookieOptions}`; + } } diff --git a/client/src/app/user/features/forgot-password/forgot-password.component.ts b/client/src/app/user/features/forgot-password/forgot-password.component.ts index efcfed4ce..39c7b00b5 100644 --- a/client/src/app/user/features/forgot-password/forgot-password.component.ts +++ b/client/src/app/user/features/forgot-password/forgot-password.component.ts @@ -1,6 +1,6 @@ import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../../shared/services/auth.service'; import { ButtonComponent } from '../../../shared/components/button/button.component'; import { InputComponent } from '../../../shared/components/input/input.component'; @@ -16,6 +16,7 @@ import { InputComponent } from '../../../shared/components/input/input.component export class ForgotPasswordComponent { private readonly fb = inject(FormBuilder); private readonly authService = inject(AuthService); + private readonly router = inject(Router); readonly loading = this.authService.loading; @@ -25,7 +26,9 @@ export class ForgotPasswordComponent { onSubmit(): void { if (this.form.valid) { - this.authService.forgotPassword(this.form.getRawValue()); + this.authService.forgotPassword(this.form.getRawValue(), () => { + this.router.navigate(['/reset-password']); + }); } else { this.form.markAllAsTouched(); } diff --git a/client/src/app/user/features/reset-password/reset-password.component.css b/client/src/app/user/features/reset-password/reset-password.component.css new file mode 100644 index 000000000..50ce81d13 --- /dev/null +++ b/client/src/app/user/features/reset-password/reset-password.component.css @@ -0,0 +1 @@ +/* Reset Password styles - uses utility classes in template */ \ No newline at end of file diff --git a/client/src/app/user/features/reset-password/reset-password.component.html b/client/src/app/user/features/reset-password/reset-password.component.html new file mode 100644 index 000000000..06bc5186b --- /dev/null +++ b/client/src/app/user/features/reset-password/reset-password.component.html @@ -0,0 +1,65 @@ +
+
+
+

Reset Password

+

Enter the OTP sent to your email and create a new password

+
+ +
+
+
+ + + + + + + +
+ +
+ +
+
+ +

+ Remember your password? + Sign in +

+
+
+
\ No newline at end of file diff --git a/client/src/app/user/features/reset-password/reset-password.component.ts b/client/src/app/user/features/reset-password/reset-password.component.ts new file mode 100644 index 000000000..f39fceff6 --- /dev/null +++ b/client/src/app/user/features/reset-password/reset-password.component.ts @@ -0,0 +1,44 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; + +@Component({ + selector: 'app-user-reset-password', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './reset-password.component.html', + styleUrl: './reset-password.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ResetPasswordComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + private readonly router = inject(Router); + + readonly loading = this.authService.loading; + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + otp: ['', [Validators.required, Validators.minLength(6), Validators.maxLength(6)]], + newPassword: ['', [Validators.required, Validators.minLength(6)]], + confirmPassword: ['', [Validators.required]], + }); + + onSubmit(): void { + if (this.form.valid) { + const { email, otp, newPassword, confirmPassword } = this.form.getRawValue(); + if (newPassword !== confirmPassword) { + this.form.controls.confirmPassword.setErrors({ mismatch: true }); + return; + } + this.authService.resetPassword({ email, otp, newPassword }, () => { + this.router.navigate(['/login']); + }); + } else { + this.form.markAllAsTouched(); + } + } +} \ No newline at end of file diff --git a/client/src/app/user/interceptors/user-auth.interceptor.ts b/client/src/app/user/interceptors/user-auth.interceptor.ts index aa03949d3..aeb39e670 100644 --- a/client/src/app/user/interceptors/user-auth.interceptor.ts +++ b/client/src/app/user/interceptors/user-auth.interceptor.ts @@ -1,5 +1,23 @@ import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { AuthService } from '../../shared/services/auth.service'; export const userAuthInterceptor: HttpInterceptorFn = (req, next) => { - return next(req.clone({ withCredentials: true })); + const authService = inject(AuthService); + const token = authService.getToken(); + + if (token) { + req = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + }, + withCredentials: true + }); + } else { + req = req.clone({ + withCredentials: true + }); + } + + return next(req); }; diff --git a/client/src/app/vendor/features/forgot-password/forgot-password.component.ts b/client/src/app/vendor/features/forgot-password/forgot-password.component.ts index e35fd3ae6..ae716708f 100644 --- a/client/src/app/vendor/features/forgot-password/forgot-password.component.ts +++ b/client/src/app/vendor/features/forgot-password/forgot-password.component.ts @@ -1,6 +1,6 @@ import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../../shared/services/auth.service'; import { ButtonComponent } from '../../../shared/components/button/button.component'; import { InputComponent } from '../../../shared/components/input/input.component'; @@ -16,6 +16,8 @@ import { InputComponent } from '../../../shared/components/input/input.component export class ForgotPasswordComponent { private readonly fb = inject(FormBuilder); private readonly authService = inject(AuthService); + private readonly router = inject(Router); + readonly loading = this.authService.loading; readonly form = this.fb.nonNullable.group({ @@ -24,7 +26,9 @@ export class ForgotPasswordComponent { onSubmit(): void { if (this.form.valid) { - this.authService.forgotPassword(this.form.getRawValue()); + this.authService.forgotPassword(this.form.getRawValue(), () => { + this.router.navigate(['/reset-password']); + }); } else { this.form.markAllAsTouched(); } From fcd760a5c419bc758eaa4b28cacb6eeb21a0e250 Mon Sep 17 00:00:00 2001 From: syamjith45 Date: Tue, 7 Jul 2026 10:00:33 +0530 Subject: [PATCH 57/59] feat: implement venue creation interface and data models for vendors --- client/src/app/shared/models/venue.model.ts | 3 + .../create-venue/create-venue.component.html | 2 +- .../create-venue/create-venue.component.ts | 72 ++++++++++--------- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/client/src/app/shared/models/venue.model.ts b/client/src/app/shared/models/venue.model.ts index 4a6906bac..132c33518 100644 --- a/client/src/app/shared/models/venue.model.ts +++ b/client/src/app/shared/models/venue.model.ts @@ -8,6 +8,7 @@ export interface Venue { district: string; capacity: number; pricePerSlot: number; + advancePercentage: number; category: string; status: VenueStatus; imageUrls?: string[]; @@ -20,6 +21,7 @@ export interface CreateVenueRequest { district: string; capacity: number; pricePerSlot: number; + advancePercentage: number; categoryId: number; imageUrls?: string[]; } @@ -31,6 +33,7 @@ export interface UpdateVenueRequest { district?: string; capacity?: number; pricePerSlot?: number; + advancePercentage?: number; categoryId?: number; imageUrls?: string[]; } diff --git a/client/src/app/vendor/features/create-venue/create-venue.component.html b/client/src/app/vendor/features/create-venue/create-venue.component.html index b79bcf5e2..b89e5388e 100644 --- a/client/src/app/vendor/features/create-venue/create-venue.component.html +++ b/client/src/app/vendor/features/create-venue/create-venue.component.html @@ -8,7 +8,7 @@

Create New Venue

- +
diff --git a/client/src/app/vendor/features/create-venue/create-venue.component.ts b/client/src/app/vendor/features/create-venue/create-venue.component.ts index 31244501c..97cbb9f2e 100644 --- a/client/src/app/vendor/features/create-venue/create-venue.component.ts +++ b/client/src/app/vendor/features/create-venue/create-venue.component.ts @@ -9,7 +9,7 @@ import { NotificationService } from '../../../shared/services/notification.servi import { forkJoin } from 'rxjs'; export interface VenueCategory { id: number; name: string; } -export interface VenueAmenity { id: string; name: string; desc: string; icon: string; } +export interface VenueAmenity { id: string; name: string; desc: string; icon: string; } @Component({ selector: 'app-create-venue', @@ -19,17 +19,17 @@ export interface VenueAmenity { id: string; name: string; desc: string; icon: s changeDetection: ChangeDetectionStrategy.OnPush, }) export class CreateVenueComponent { - private readonly fb = inject(FormBuilder); + private readonly fb = inject(FormBuilder); private readonly venueService = inject(VenueService); - private readonly cloudinary = inject(CloudinaryService); + private readonly cloudinary = inject(CloudinaryService); private readonly notification = inject(NotificationService); - readonly router = inject(Router); + readonly router = inject(Router); - readonly saving = signal(false); + readonly saving = signal(false); readonly uploading = signal(false); readonly imageUrls = signal([]); readonly selectedAmenities = signal>(new Set()); - readonly dragOver = signal(false); + readonly dragOver = signal(false); readonly categories: VenueCategory[] = [ { id: 1, name: 'Banquet Hall' }, @@ -43,28 +43,29 @@ export class CreateVenueComponent { ]; readonly amenitiesList: VenueAmenity[] = [ - { id: 'catering', name: 'Catering Service', desc: 'In-house food & beverage', icon: 'M3 3h2l.4 2M7 13h10l4-4H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z' }, - { id: 'av', name: 'AV & Sound System', desc: 'PA system, projector & lighting', icon: 'M15.536 8.464a5 5 0 010 7.072M12 6a7 7 0 010 12M8.464 8.464a5 5 0 000 7.072M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, - { id: 'parking', name: 'Parking', desc: 'On-site vehicle parking', icon: 'M8 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM15 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM3 4h13l4 4v4H3V4z' }, - { id: 'decoration', name: 'Decoration', desc: 'Floral & thematic decor setup', icon: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z' }, - { id: 'security', name: 'Security', desc: 'Trained security personnel', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' }, - { id: 'photography', name: 'Photography', desc: 'Photo booths & candid setup', icon: 'M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z M15 13a3 3 0 11-6 0 3 3 0 016 0z' }, - { id: 'wifi', name: 'High-Speed WiFi', desc: 'Fibre broadband for all guests', icon: 'M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0' }, - { id: 'ac', name: 'Air Conditioning', desc: 'Central AC throughout venue', icon: 'M9.59 4.59A2 2 0 1111 8H2m10.59 11.41A2 2 0 1010 16H2m15.73-8.27A2 2 0 1119 12H2' }, - { id: 'generator', name: 'Generator Backup', desc: '24/7 power backup available', icon: 'M13 10V3L4 14h7v7l9-11h-7z' }, - { id: 'bridal_room', name: 'Bridal / VIP Room', desc: 'Private suite for the main guests', icon: 'M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z' }, - { id: 'kitchen', name: 'Catering Kitchen', desc: 'Commercial kitchen for external caterers', icon: 'M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z' }, - { id: 'stage', name: 'Stage & Podium', desc: 'Elevated stage with podium mic', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' }, + { id: 'catering', name: 'Catering Service', desc: 'In-house food & beverage', icon: 'M3 3h2l.4 2M7 13h10l4-4H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z' }, + { id: 'av', name: 'AV & Sound System', desc: 'PA system, projector & lighting', icon: 'M15.536 8.464a5 5 0 010 7.072M12 6a7 7 0 010 12M8.464 8.464a5 5 0 000 7.072M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, + { id: 'parking', name: 'Parking', desc: 'On-site vehicle parking', icon: 'M8 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM15 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM3 4h13l4 4v4H3V4z' }, + { id: 'decoration', name: 'Decoration', desc: 'Floral & thematic decor setup', icon: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z' }, + { id: 'security', name: 'Security', desc: 'Trained security personnel', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' }, + { id: 'photography', name: 'Photography', desc: 'Photo booths & candid setup', icon: 'M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z M15 13a3 3 0 11-6 0 3 3 0 016 0z' }, + { id: 'wifi', name: 'High-Speed WiFi', desc: 'Fibre broadband for all guests', icon: 'M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0' }, + { id: 'ac', name: 'Air Conditioning', desc: 'Central AC throughout venue', icon: 'M9.59 4.59A2 2 0 1111 8H2m10.59 11.41A2 2 0 1010 16H2m15.73-8.27A2 2 0 1119 12H2' }, + { id: 'generator', name: 'Generator Backup', desc: '24/7 power backup available', icon: 'M13 10V3L4 14h7v7l9-11h-7z' }, + { id: 'bridal_room', name: 'Bridal / VIP Room', desc: 'Private suite for the main guests', icon: 'M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z' }, + { id: 'kitchen', name: 'Catering Kitchen', desc: 'Commercial kitchen for external caterers', icon: 'M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z' }, + { id: 'stage', name: 'Stage & Podium', desc: 'Elevated stage with podium mic', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' }, ]; readonly form = this.fb.nonNullable.group({ - name: ['', [Validators.required]], + name: ['', [Validators.required]], description: ['', [Validators.required, Validators.minLength(20)]], - address: ['', [Validators.required]], - district: ['', [Validators.required]], - capacity: ['', [Validators.required, Validators.min(1)]], - pricePerSlot:['', [Validators.required, Validators.min(1)]], - categoryId: ['', [Validators.required, Validators.min(1)]], + address: ['', [Validators.required]], + district: ['', [Validators.required]], + capacity: ['', [Validators.required, Validators.min(1)]], + pricePerSlot: ['', [Validators.required, Validators.min(1)]], + advancePercentage:['', [Validators.min(0), Validators.max(100)]], + categoryId: ['', [Validators.required, Validators.min(1)]], }); toggleAmenity(id: string): void { @@ -79,8 +80,8 @@ export class CreateVenueComponent { return this.selectedAmenities().has(id); } - onDragOver(e: DragEvent): void { e.preventDefault(); this.dragOver.set(true); } - onDragLeave(): void { this.dragOver.set(false); } + onDragOver(e: DragEvent): void { e.preventDefault(); this.dragOver.set(true); } + onDragLeave(): void { this.dragOver.set(false); } onDrop(e: DragEvent): void { e.preventDefault(); this.dragOver.set(false); @@ -99,7 +100,7 @@ export class CreateVenueComponent { this.uploading.set(true); forkJoin(files.map(f => this.cloudinary.upload(f))).subscribe({ next: urls => { this.imageUrls.update(existing => [...existing, ...urls]); this.uploading.set(false); }, - error: () => { this.uploading.set(false); this.notification.error('Image upload failed. Please try again.'); }, + error: () => { this.uploading.set(false); this.notification.error('Image upload failed. Please try again.'); }, }); } @@ -112,18 +113,19 @@ export class CreateVenueComponent { this.saving.set(true); const raw = this.form.getRawValue(); this.venueService.createVenue({ - name: raw.name, - description: raw.description, - address: raw.address, - district: raw.district, - capacity: Number(raw.capacity), + name: raw.name, + description: raw.description, + address: raw.address, + district: raw.district, + capacity: Number(raw.capacity), pricePerSlot: Number(raw.pricePerSlot), - categoryId: Number(raw.categoryId), - imageUrls: this.imageUrls(), + advancePercentage: Number(raw.advancePercentage), + categoryId: Number(raw.categoryId), + imageUrls: this.imageUrls(), // amenities: Array.from(this.selectedAmenities()), // add when backend supports it }).subscribe({ next: () => { this.saving.set(false); this.router.navigate(['/vendor/venues']); }, - error: () => { this.saving.set(false); this.notification.error('Failed to create venue. Please check your inputs.'); }, + error: () => { this.saving.set(false); this.notification.error('Failed to create venue. Please check your inputs.'); }, }); } else { this.form.markAllAsTouched(); From bdb61ed7a736706437e35e0d2224a35a6dd60371 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Wed, 8 Jul 2026 23:56:10 +0530 Subject: [PATCH 58/59] otp update --- .../forgot-password.component.html | 2 +- .../forgot-password.component.ts | 2 +- client/src/app/core/app.routes.ts | 10 +++ .../constants/api-endpoints.constant.ts | 19 ++++-- .../shared/interceptors/error.interceptor.ts | 16 ++--- .../app/shared/models/auth-response.model.ts | 19 +++++- client/src/app/shared/models/venue.model.ts | 9 +++ .../app/shared/services/auth.repository.ts | 30 +++++++-- .../src/app/shared/services/auth.service.ts | 66 +++++++++++++++---- .../app/shared/services/category.service.ts | 16 +++++ client/src/app/shared/utils/validators.ts | 11 ++++ .../forgot-password.component.html | 4 +- .../reset-password.component.html | 56 ++++++++++++++++ .../reset-password.component.ts | 45 +++++++++++++ .../features/signup/signup.component.html | 4 +- .../user/features/signup/signup.component.ts | 2 +- .../verify-email/verify-email.component.html | 50 ++++++++++++++ .../verify-email/verify-email.component.ts | 63 ++++++++++++++++++ .../create-venue/create-venue.component.html | 13 +++- .../create-venue/create-venue.component.ts | 38 ++++++----- .../edit-venue/edit-venue.component.html | 13 +++- .../edit-venue/edit-venue.component.ts | 47 ++++++++----- 22 files changed, 452 insertions(+), 83 deletions(-) create mode 100644 client/src/app/shared/services/category.service.ts create mode 100644 client/src/app/user/features/reset-password/reset-password.component.html create mode 100644 client/src/app/user/features/reset-password/reset-password.component.ts create mode 100644 client/src/app/user/features/verify-email/verify-email.component.html create mode 100644 client/src/app/user/features/verify-email/verify-email.component.ts diff --git a/client/src/app/admin/features/forgot-password/forgot-password.component.html b/client/src/app/admin/features/forgot-password/forgot-password.component.html index 31d8f5185..b92816cc2 100644 --- a/client/src/app/admin/features/forgot-password/forgot-password.component.html +++ b/client/src/app/admin/features/forgot-password/forgot-password.component.html @@ -8,7 +8,7 @@

Forgot Password

-
+

Back to login diff --git a/client/src/app/admin/features/forgot-password/forgot-password.component.ts b/client/src/app/admin/features/forgot-password/forgot-password.component.ts index 995e82989..ae5d5ad84 100644 --- a/client/src/app/admin/features/forgot-password/forgot-password.component.ts +++ b/client/src/app/admin/features/forgot-password/forgot-password.component.ts @@ -18,7 +18,7 @@ export class ForgotPasswordComponent { readonly form = this.fb.nonNullable.group({ email: ['', [Validators.required, Validators.email]] }); onSubmit(): void { - if (this.form.valid) { this.authService.forgotPassword(this.form.getRawValue()); } + if (this.form.valid) { this.authService.forgotPassword(this.form.getRawValue(), 'admin'); } else { this.form.markAllAsTouched(); } } } diff --git a/client/src/app/core/app.routes.ts b/client/src/app/core/app.routes.ts index 943c1b2fa..391537bb6 100644 --- a/client/src/app/core/app.routes.ts +++ b/client/src/app/core/app.routes.ts @@ -23,6 +23,16 @@ export const routes: Routes = [ canActivate: [guestGuard], loadComponent: () => import('../user/features/forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent), }, + { + path: 'verify-email', + canActivate: [guestGuard], + loadComponent: () => import('../user/features/verify-email/verify-email.component').then(m => m.VerifyEmailComponent), + }, + { + path: 'reset-password', + canActivate: [guestGuard], + loadComponent: () => import('../user/features/reset-password/reset-password.component').then(m => m.ResetPasswordComponent), + }, { path: ROUTE_PATHS.USER.BASE, loadChildren: () => import('../user/routes/user.routes').then(m => m.USER_ROUTES), diff --git a/client/src/app/shared/constants/api-endpoints.constant.ts b/client/src/app/shared/constants/api-endpoints.constant.ts index 3ac75a144..c4258ae79 100644 --- a/client/src/app/shared/constants/api-endpoints.constant.ts +++ b/client/src/app/shared/constants/api-endpoints.constant.ts @@ -1,12 +1,17 @@ export const API_ENDPOINTS = { AUTH: { - LOGIN: '/auth/login', - SIGNUP: '/auth/register', - LOGOUT: '/auth/logout', - REFRESH_TOKEN: '/auth/refresh-token', - FORGOT_PASSWORD: '/auth/forgot-password', - RESET_PASSWORD: '/auth/reset-password', - ADMIN_LOGIN: '/auth/login', + LOGIN: '/v1/auth/login', + SIGNUP: '/v1/auth/register', + LOGOUT: '/v1/auth/logout', + REFRESH_TOKEN: '/v1/auth/refresh-token', + FORGOT_PASSWORD: '/v1/auth/forgot-password', + RESET_PASSWORD: '/v1/auth/reset-password', + VERIFY_EMAIL: '/v1/auth/verify-email', + RESEND_VERIFICATION: '/v1/auth/resend-verification', + ADMIN_LOGIN: '/v1/auth/login', + }, + CATEGORIES: { + BASE: '/v1/categories', }, USERS: { BASE: '/v1/users', diff --git a/client/src/app/shared/interceptors/error.interceptor.ts b/client/src/app/shared/interceptors/error.interceptor.ts index f7174f735..cb7aa49cc 100644 --- a/client/src/app/shared/interceptors/error.interceptor.ts +++ b/client/src/app/shared/interceptors/error.interceptor.ts @@ -10,11 +10,10 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( catchError((error: HttpErrorResponse) => { - // 401 = access token expired — try refresh then retry - // 403 is NOT a token error; it means the user lacks permission — do not refresh - if (error.status === 401 && - !req.url.includes('/auth/refresh-token') && - !req.url.includes('/auth/logout')) { + // 401 = access token expired — try refresh then retry. + // Auth endpoints (login, register, etc.) return 401/403 for business reasons + // (invalid credentials, unverified email) — never treat those as an expired session. + if (error.status === 401 && !req.url.includes('/auth/')) { return authService.refreshToken().pipe( switchMap(() => next(req)), // retry original request with new cookie @@ -27,12 +26,11 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { ); } - let message = 'An unexpected error occurred'; + let message = error.error?.message || 'An unexpected error occurred'; switch (error.status) { case 0: message = 'Unable to connect to server'; break; - case 403: message = 'You do not have permission to perform this action'; break; - case 404: message = 'Resource not found'; break; - case 422: message = error.error?.message || 'Validation error'; break; + case 403: message = error.error?.message || 'You do not have permission to perform this action'; break; + case 404: message = error.error?.message || 'Resource not found'; break; case 500: message = 'Server error. Please try again later.'; break; } diff --git a/client/src/app/shared/models/auth-response.model.ts b/client/src/app/shared/models/auth-response.model.ts index ee04c4537..bfeb16510 100644 --- a/client/src/app/shared/models/auth-response.model.ts +++ b/client/src/app/shared/models/auth-response.model.ts @@ -33,9 +33,22 @@ export interface ForgotPasswordRequest { } export interface ResetPasswordRequest { - token: string; - password: string; - confirmPassword: string; + email: string; + otp: string; + newPassword: string; +} + +export interface VerifyEmailRequest { + email: string; + otp: string; +} + +export interface ResendVerificationRequest { + email: string; +} + +export interface MessageResponse { + message: string; } export interface RefreshTokenApiResponse { diff --git a/client/src/app/shared/models/venue.model.ts b/client/src/app/shared/models/venue.model.ts index 4a6906bac..7a8df5bfb 100644 --- a/client/src/app/shared/models/venue.model.ts +++ b/client/src/app/shared/models/venue.model.ts @@ -8,11 +8,18 @@ export interface Venue { district: string; capacity: number; pricePerSlot: number; + advancePercentage: number; category: string; status: VenueStatus; imageUrls?: string[]; } +export interface VenueCategory { + id: number; + name: string; + description?: string; +} + export interface CreateVenueRequest { name: string; description: string; @@ -20,6 +27,7 @@ export interface CreateVenueRequest { district: string; capacity: number; pricePerSlot: number; + advancePercentage: number; categoryId: number; imageUrls?: string[]; } @@ -31,6 +39,7 @@ export interface UpdateVenueRequest { district?: string; capacity?: number; pricePerSlot?: number; + advancePercentage?: number; categoryId?: number; imageUrls?: string[]; } diff --git a/client/src/app/shared/services/auth.repository.ts b/client/src/app/shared/services/auth.repository.ts index 3a8214bf8..e1e440587 100644 --- a/client/src/app/shared/services/auth.repository.ts +++ b/client/src/app/shared/services/auth.repository.ts @@ -9,9 +9,11 @@ import { SignupRequest, ForgotPasswordRequest, ResetPasswordRequest, + VerifyEmailRequest, + ResendVerificationRequest, + MessageResponse, RefreshTokenApiResponse, } from '../models/auth-response.model'; -import { ApiResponse } from '../models/api-response.model'; @Injectable({ providedIn: 'root' }) export class AuthRepository { @@ -32,7 +34,7 @@ export class AuthRepository { ); } -signup(payload: SignupRequest): Observable { +signup(payload: SignupRequest): Observable { const body = { name: `${payload.firstName} ${payload.lastName}`.trim(), email: payload.email, @@ -40,7 +42,7 @@ signup(payload: SignupRequest): Observable { password: payload.password, role: payload.isVendor ? 'VENDOR' : 'USER', }; - return this.http.post( + return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.SIGNUP}`, body ); @@ -61,17 +63,31 @@ signup(payload: SignupRequest): Observable { ); } - forgotPassword(payload: ForgotPasswordRequest): Observable> { - return this.http.post>( + forgotPassword(payload: ForgotPasswordRequest): Observable { + return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.FORGOT_PASSWORD}`, payload ); } - resetPassword(payload: ResetPasswordRequest): Observable> { - return this.http.post>( + resetPassword(payload: ResetPasswordRequest): Observable { + return this.http.post( `${this.apiUrl}${API_ENDPOINTS.AUTH.RESET_PASSWORD}`, payload ); } + + verifyEmail(payload: VerifyEmailRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.AUTH.VERIFY_EMAIL}`, + payload + ); + } + + resendVerification(payload: ResendVerificationRequest): Observable { + return this.http.post( + `${this.apiUrl}${API_ENDPOINTS.AUTH.RESEND_VERIFICATION}`, + payload + ); + } } diff --git a/client/src/app/shared/services/auth.service.ts b/client/src/app/shared/services/auth.service.ts index 94ae19fe7..b1e63d0ef 100644 --- a/client/src/app/shared/services/auth.service.ts +++ b/client/src/app/shared/services/auth.service.ts @@ -1,4 +1,5 @@ import { Injectable, inject, signal, computed } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; import { Router } from '@angular/router'; import { AuthRepository } from './auth.repository'; import { StorageService } from './storage.service'; @@ -9,6 +10,9 @@ import { LoginRequest, SignupRequest, ForgotPasswordRequest, + ResetPasswordRequest, + VerifyEmailRequest, + ResendVerificationRequest, } from '../models/auth-response.model'; import { UserRole } from '../enums/user-role.enum'; import { map, Observable } from 'rxjs'; @@ -146,9 +150,11 @@ export class AuthService { this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); this.loading.set(false); }, - error: () => { - this.notification.error('Invalid credentials'); + error: (error: HttpErrorResponse) => { this.loading.set(false); + if (error.error?.code === 'EMAIL_NOT_VERIFIED') { + this.router.navigate(['/verify-email'], { queryParams: { email: payload.email } }); + } }, }); } @@ -164,7 +170,6 @@ export class AuthService { this.loading.set(false); }, error: () => { - this.notification.error('Invalid credentials'); this.loading.set(false); }, }); @@ -174,29 +179,66 @@ export class AuthService { this.loading.set(true); this.authRepository.signup(payload).subscribe({ next: (response) => { - const user = this.toAuthUser(response); - const portal = response.role === UserRole.Vendor ? 'vendor' : 'user'; - this.savePortalSession(portal, response.accessToken || '', response.refreshToken || '', user); - this.notification.success(portal === 'vendor' ? 'Vendor account created successfully' : 'Account created successfully'); - this.router.navigate(portal === 'vendor' ? ['/vendor/dashboard'] : ['/user/venues']); + this.notification.success(response.message || 'Registration successful. Please verify your email.'); + this.router.navigate(['/verify-email'], { queryParams: { email: payload.email } }); this.loading.set(false); }, error: () => { - this.notification.error('Signup failed. Please try again.'); this.loading.set(false); }, }); } - forgotPassword(payload: ForgotPasswordRequest): void { + forgotPassword(payload: ForgotPasswordRequest, portal: 'user' | 'admin' = 'user'): void { this.loading.set(true); this.authRepository.forgotPassword(payload).subscribe({ + next: (response) => { + this.notification.success(response.message || 'Password reset OTP sent'); + this.router.navigate(['/reset-password'], { queryParams: { email: payload.email, portal } }); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + }, + }); + } + + resetPassword(payload: ResetPasswordRequest, portal: 'user' | 'admin' = 'user'): void { + this.loading.set(true); + this.authRepository.resetPassword(payload).subscribe({ + next: (response) => { + this.notification.success(response.message || 'Password reset successful'); + this.router.navigate([portal === 'admin' ? '/admin/login' : '/login']); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + }, + }); + } + + verifyEmail(payload: VerifyEmailRequest): void { + this.loading.set(true); + this.authRepository.verifyEmail(payload).subscribe({ + next: () => { + this.notification.success('Email verified successfully. Please sign in.'); + this.router.navigate(['/login']); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + }, + }); + } + + resendVerification(payload: ResendVerificationRequest): void { + this.loading.set(true); + this.authRepository.resendVerification(payload).subscribe({ next: () => { - this.notification.success('Password reset email sent'); + this.notification.success('Verification email sent'); this.loading.set(false); }, error: () => { - this.notification.error('Failed to send reset email'); this.loading.set(false); }, }); diff --git a/client/src/app/shared/services/category.service.ts b/client/src/app/shared/services/category.service.ts new file mode 100644 index 000000000..2032dfec4 --- /dev/null +++ b/client/src/app/shared/services/category.service.ts @@ -0,0 +1,16 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../core/config/environment'; +import { API_ENDPOINTS } from '../constants/api-endpoints.constant'; +import { VenueCategory } from '../models/venue.model'; + +@Injectable({ providedIn: 'root' }) +export class CategoryService { + private readonly http = inject(HttpClient); + private readonly apiUrl = environment.apiUrl; + + getCategories(): Observable { + return this.http.get(`${this.apiUrl}${API_ENDPOINTS.CATEGORIES.BASE}`); + } +} diff --git a/client/src/app/shared/utils/validators.ts b/client/src/app/shared/utils/validators.ts index 955ceb498..f0ec0e8c6 100644 --- a/client/src/app/shared/utils/validators.ts +++ b/client/src/app/shared/utils/validators.ts @@ -35,4 +35,15 @@ export class AppValidators { today.setHours(0, 0, 0, 0); return selectedDate >= today ? null : { futureDate: true }; } + + static strongPassword(control: AbstractControl): ValidationErrors | null { + if (!control.value) return null; + const pattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,100}$/; + return pattern.test(control.value) ? null : { strongPassword: true }; + } + + static otp(control: AbstractControl): ValidationErrors | null { + if (!control.value) return null; + return /^\d{6}$/.test(control.value) ? null : { otp: true }; + } } diff --git a/client/src/app/user/features/forgot-password/forgot-password.component.html b/client/src/app/user/features/forgot-password/forgot-password.component.html index 025246149..000948d9f 100644 --- a/client/src/app/user/features/forgot-password/forgot-password.component.html +++ b/client/src/app/user/features/forgot-password/forgot-password.component.html @@ -2,7 +2,7 @@

Forgot Password

-

Enter your email to receive a reset link

+

Enter your email to receive a reset code

@@ -20,7 +20,7 @@

Forgot Password

+
+
+

Reset Password

+

+ Enter the code sent to + {{ email() || 'your email address' }} + and choose a new password +

+
+ +
+
+
+ + + +
+ +
+ +
+
+ +

+ Remember your password? + Sign in +

+
+
+
diff --git a/client/src/app/user/features/reset-password/reset-password.component.ts b/client/src/app/user/features/reset-password/reset-password.component.ts new file mode 100644 index 000000000..1e83039db --- /dev/null +++ b/client/src/app/user/features/reset-password/reset-password.component.ts @@ -0,0 +1,45 @@ +import { Component, inject, signal, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; +import { AppValidators } from '../../../shared/utils/validators'; + +@Component({ + selector: 'app-reset-password', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './reset-password.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ResetPasswordComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly route = inject(ActivatedRoute); + private readonly authService = inject(AuthService); + + readonly loading = this.authService.loading; + readonly email = signal(''); + private portal: 'user' | 'admin' = 'user'; + + readonly form = this.fb.nonNullable.group({ + otp: ['', [Validators.required, AppValidators.otp]], + newPassword: ['', [Validators.required, AppValidators.strongPassword]], + confirmPassword: ['', [Validators.required, AppValidators.matchField('newPassword')]], + }); + + ngOnInit(): void { + const params = this.route.snapshot.queryParamMap; + this.email.set(params.get('email') ?? ''); + this.portal = params.get('portal') === 'admin' ? 'admin' : 'user'; + } + + onSubmit(): void { + if (this.form.valid) { + const { confirmPassword, ...rest } = this.form.getRawValue(); + this.authService.resetPassword({ email: this.email(), ...rest }, this.portal); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/client/src/app/user/features/signup/signup.component.html b/client/src/app/user/features/signup/signup.component.html index e65275cfd..99b089915 100644 --- a/client/src/app/user/features/signup/signup.component.html +++ b/client/src/app/user/features/signup/signup.component.html @@ -130,10 +130,10 @@

Create account +
+
+

Verify your email

+

+ Enter the 6-digit code we sent to + {{ email() || 'your email address' }} +

+
+ +
+
+
+ +
+ +
+ +
+
+ +

+ Didn't get the code? + +

+ +

+ Back to sign in +

+
+
+

diff --git a/client/src/app/user/features/verify-email/verify-email.component.ts b/client/src/app/user/features/verify-email/verify-email.component.ts new file mode 100644 index 000000000..37d67d1a4 --- /dev/null +++ b/client/src/app/user/features/verify-email/verify-email.component.ts @@ -0,0 +1,63 @@ +import { Component, inject, signal, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; +import { ButtonComponent } from '../../../shared/components/button/button.component'; +import { InputComponent } from '../../../shared/components/input/input.component'; +import { AppValidators } from '../../../shared/utils/validators'; + +@Component({ + selector: 'app-verify-email', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ButtonComponent, InputComponent], + templateUrl: './verify-email.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VerifyEmailComponent implements OnInit, OnDestroy { + private readonly fb = inject(FormBuilder); + private readonly route = inject(ActivatedRoute); + private readonly authService = inject(AuthService); + + readonly loading = this.authService.loading; + readonly email = signal(''); + readonly resendCooldown = signal(0); + private cooldownHandle?: ReturnType; + + readonly form = this.fb.nonNullable.group({ + otp: ['', [Validators.required, AppValidators.otp]], + }); + + ngOnInit(): void { + this.email.set(this.route.snapshot.queryParamMap.get('email') ?? ''); + this.startCooldown(); + } + + ngOnDestroy(): void { + if (this.cooldownHandle) clearInterval(this.cooldownHandle); + } + + onSubmit(): void { + if (this.form.valid) { + this.authService.verifyEmail({ email: this.email(), otp: this.form.getRawValue().otp }); + } else { + this.form.markAllAsTouched(); + } + } + + resend(): void { + if (this.resendCooldown() > 0 || !this.email()) return; + this.authService.resendVerification({ email: this.email() }); + this.startCooldown(); + } + + private startCooldown(): void { + this.resendCooldown.set(30); + if (this.cooldownHandle) clearInterval(this.cooldownHandle); + this.cooldownHandle = setInterval(() => { + this.resendCooldown.update(v => { + if (v <= 1 && this.cooldownHandle) clearInterval(this.cooldownHandle); + return Math.max(0, v - 1); + }); + }, 1000); + } +} diff --git a/client/src/app/vendor/features/create-venue/create-venue.component.html b/client/src/app/vendor/features/create-venue/create-venue.component.html index b79bcf5e2..c894b9727 100644 --- a/client/src/app/vendor/features/create-venue/create-venue.component.html +++ b/client/src/app/vendor/features/create-venue/create-venue.component.html @@ -56,7 +56,7 @@

Basic Information

class="w-full px-3.5 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-accent-500/40 focus:border-accent-500 bg-white transition-colors" > - @for (cat of categories; track cat.id) { + @for (cat of categories(); track cat.id) { } @@ -120,6 +120,17 @@

Pricing & Capacity

[error]="form.controls.pricePerSlot.touched && form.controls.pricePerSlot.errors ? 'Enter a valid price' : ''" />
+
+ +
diff --git a/client/src/app/vendor/features/create-venue/create-venue.component.ts b/client/src/app/vendor/features/create-venue/create-venue.component.ts index 31244501c..c3754c148 100644 --- a/client/src/app/vendor/features/create-venue/create-venue.component.ts +++ b/client/src/app/vendor/features/create-venue/create-venue.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, signal, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { VenueService } from '../../services/venue.service'; @@ -6,9 +6,10 @@ import { ButtonComponent } from '../../../shared/components/button/button.compon import { InputComponent } from '../../../shared/components/input/input.component'; import { CloudinaryService } from '../../../shared/services/cloudinary.service'; import { NotificationService } from '../../../shared/services/notification.service'; +import { CategoryService } from '../../../shared/services/category.service'; +import { VenueCategory } from '../../../shared/models/venue.model'; import { forkJoin } from 'rxjs'; -export interface VenueCategory { id: number; name: string; } export interface VenueAmenity { id: string; name: string; desc: string; icon: string; } @Component({ @@ -18,12 +19,13 @@ export interface VenueAmenity { id: string; name: string; desc: string; icon: s templateUrl: './create-venue.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CreateVenueComponent { - private readonly fb = inject(FormBuilder); - private readonly venueService = inject(VenueService); - private readonly cloudinary = inject(CloudinaryService); - private readonly notification = inject(NotificationService); - readonly router = inject(Router); +export class CreateVenueComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly venueService = inject(VenueService); + private readonly cloudinary = inject(CloudinaryService); + private readonly notification = inject(NotificationService); + private readonly categoryService = inject(CategoryService); + readonly router = inject(Router); readonly saving = signal(false); readonly uploading = signal(false); @@ -31,16 +33,14 @@ export class CreateVenueComponent { readonly selectedAmenities = signal>(new Set()); readonly dragOver = signal(false); - readonly categories: VenueCategory[] = [ - { id: 1, name: 'Banquet Hall' }, - { id: 2, name: 'Rooftop' }, - { id: 3, name: 'Outdoor / Garden' }, - { id: 4, name: 'Heritage' }, - { id: 5, name: 'Conference Centre' }, - { id: 6, name: 'Resort' }, - { id: 7, name: 'Club House' }, - { id: 8, name: 'Hotel Ballroom' }, - ]; + readonly categories = signal([]); + + ngOnInit(): void { + this.categoryService.getCategories().subscribe({ + next: (categories) => this.categories.set(categories), + error: () => this.notification.error('Failed to load venue categories'), + }); + } readonly amenitiesList: VenueAmenity[] = [ { id: 'catering', name: 'Catering Service', desc: 'In-house food & beverage', icon: 'M3 3h2l.4 2M7 13h10l4-4H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z' }, @@ -64,6 +64,7 @@ export class CreateVenueComponent { district: ['', [Validators.required]], capacity: ['', [Validators.required, Validators.min(1)]], pricePerSlot:['', [Validators.required, Validators.min(1)]], + advancePercentage: ['', [Validators.required, Validators.min(0), Validators.max(100)]], categoryId: ['', [Validators.required, Validators.min(1)]], }); @@ -118,6 +119,7 @@ export class CreateVenueComponent { district: raw.district, capacity: Number(raw.capacity), pricePerSlot: Number(raw.pricePerSlot), + advancePercentage: Number(raw.advancePercentage), categoryId: Number(raw.categoryId), imageUrls: this.imageUrls(), // amenities: Array.from(this.selectedAmenities()), // add when backend supports it diff --git a/client/src/app/vendor/features/edit-venue/edit-venue.component.html b/client/src/app/vendor/features/edit-venue/edit-venue.component.html index 4443f326c..4db9b7b36 100644 --- a/client/src/app/vendor/features/edit-venue/edit-venue.component.html +++ b/client/src/app/vendor/features/edit-venue/edit-venue.component.html @@ -79,7 +79,7 @@

Basic Information

class="w-full px-3.5 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-accent-500/40 focus:border-accent-500 bg-white transition-colors" > - @for (cat of categories; track cat.id) { + @for (cat of categories(); track cat.id) { } @@ -143,6 +143,17 @@

Pricing & Capacity

[error]="form.controls.pricePerSlot.touched && form.controls.pricePerSlot.errors ? 'Enter a valid price' : ''" /> +
+ +
diff --git a/client/src/app/vendor/features/edit-venue/edit-venue.component.ts b/client/src/app/vendor/features/edit-venue/edit-venue.component.ts index 7748a5f13..fa9f7333c 100644 --- a/client/src/app/vendor/features/edit-venue/edit-venue.component.ts +++ b/client/src/app/vendor/features/edit-venue/edit-venue.component.ts @@ -6,8 +6,10 @@ import { ButtonComponent } from '../../../shared/components/button/button.compon import { InputComponent } from '../../../shared/components/input/input.component'; import { CloudinaryService } from '../../../shared/services/cloudinary.service'; import { NotificationService } from '../../../shared/services/notification.service'; +import { CategoryService } from '../../../shared/services/category.service'; +import { VenueCategory } from '../../../shared/models/venue.model'; import { forkJoin } from 'rxjs'; -import { VenueCategory, VenueAmenity } from '../create-venue/create-venue.component'; +import { VenueAmenity } from '../create-venue/create-venue.component'; @Component({ selector: 'app-edit-venue', @@ -17,12 +19,13 @@ import { VenueCategory, VenueAmenity } from '../create-venue/create-venue.compon changeDetection: ChangeDetectionStrategy.OnPush, }) export class EditVenueComponent implements OnInit { - private readonly route = inject(ActivatedRoute); - readonly router = inject(Router); - private readonly fb = inject(FormBuilder); - private readonly venueService = inject(VenueService); - private readonly cloudinary = inject(CloudinaryService); - private readonly notification = inject(NotificationService); + private readonly route = inject(ActivatedRoute); + readonly router = inject(Router); + private readonly fb = inject(FormBuilder); + private readonly venueService = inject(VenueService); + private readonly cloudinary = inject(CloudinaryService); + private readonly notification = inject(NotificationService); + private readonly categoryService = inject(CategoryService); readonly loading = signal(true); readonly saving = signal(false); @@ -33,16 +36,7 @@ export class EditVenueComponent implements OnInit { private venueId = ''; - readonly categories: VenueCategory[] = [ - { id: 1, name: 'Banquet Hall' }, - { id: 2, name: 'Rooftop' }, - { id: 3, name: 'Outdoor / Garden' }, - { id: 4, name: 'Heritage' }, - { id: 5, name: 'Conference Centre' }, - { id: 6, name: 'Resort' }, - { id: 7, name: 'Club House' }, - { id: 8, name: 'Hotel Ballroom' }, - ]; + readonly categories = signal([]); readonly amenitiesList: VenueAmenity[] = [ { id: 'catering', name: 'Catering Service', desc: 'In-house food & beverage', icon: 'M3 3h2l.4 2M7 13h10l4-4H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z' }, @@ -66,14 +60,27 @@ export class EditVenueComponent implements OnInit { district: ['', [Validators.required]], capacity: ['', [Validators.required, Validators.min(1)]], pricePerSlot: ['', [Validators.required, Validators.min(1)]], + advancePercentage: ['', [Validators.required, Validators.min(0), Validators.max(100)]], categoryId: ['', [Validators.required, Validators.min(1)]], }); ngOnInit(): void { this.venueId = this.route.snapshot.paramMap.get('id') || ''; + + this.categoryService.getCategories().subscribe({ + next: (categories) => { + this.categories.set(categories); + const match = categories.find(c => c.name === this.pendingCategoryName); + if (match) this.form.patchValue({ categoryId: String(match.id) }); + }, + error: () => this.notification.error('Failed to load venue categories'), + }); + if (this.venueId) { this.venueService.loadVenueById(this.venueId).subscribe({ next: (venue) => { + this.pendingCategoryName = venue.category; + const match = this.categories().find(c => c.name === venue.category); this.form.patchValue({ name: venue.name, description: venue.description, @@ -81,7 +88,8 @@ export class EditVenueComponent implements OnInit { district: venue.district, capacity: String(venue.capacity), pricePerSlot: String(venue.pricePerSlot), - categoryId: String((venue as any).categoryId ?? ''), + advancePercentage: String(venue.advancePercentage ?? ''), + categoryId: match ? String(match.id) : '', }); this.imageUrls.set(venue.imageUrls ?? []); // this.selectedAmenities.set(new Set(venue.amenities ?? [])); // enable when backend ready @@ -92,6 +100,8 @@ export class EditVenueComponent implements OnInit { } } + private pendingCategoryName = ''; + toggleAmenity(id: string): void { this.selectedAmenities.update(set => { const next = new Set(set); @@ -143,6 +153,7 @@ export class EditVenueComponent implements OnInit { district: raw.district, capacity: Number(raw.capacity), pricePerSlot: Number(raw.pricePerSlot), + advancePercentage: raw.advancePercentage ? Number(raw.advancePercentage) : undefined, categoryId: raw.categoryId ? Number(raw.categoryId) : undefined, imageUrls: this.imageUrls(), // amenities: Array.from(this.selectedAmenities()), // enable when backend ready From 24aa1fb94dcf9cbba2f31dab7b2cadec2aa064c5 Mon Sep 17 00:00:00 2001 From: Sachin Kizhakkepurath Date: Thu, 9 Jul 2026 22:16:15 +0530 Subject: [PATCH 59/59] otp update --- .../app/user/features/my-bookings/my-bookings.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/user/features/my-bookings/my-bookings.component.html b/client/src/app/user/features/my-bookings/my-bookings.component.html index 25b8ead24..1371e7e08 100644 --- a/client/src/app/user/features/my-bookings/my-bookings.component.html +++ b/client/src/app/user/features/my-bookings/my-bookings.component.html @@ -24,7 +24,7 @@

My Bookings

@for (booking of bookings(); track booking.id) { - {{ booking.venueId }} + {{ booking.venueId }} {{ booking.bookingDate | dateFormat }}