diff --git a/apps/api/.env.example b/apps/api/.env.example index 885748a..458b00d 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -21,4 +21,4 @@ SMTP_PASSWORD= SANITY_PROJECT_ID= SANITY_WEBHOOK_SECRET=my-hard-to-guess-secret -SANITY_API_TOKEN= \ No newline at end of file +SANITY_API_TOKEN= diff --git a/apps/api/package.json b/apps/api/package.json index ecd7d28..0a96473 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -60,7 +60,9 @@ "@nestjs/cqrs": "^10.2.7", "@nestjs/event-emitter": "^2.0.4", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.3.8", "@nestjs/swagger": "^7.3.0", + "@nestjs/websockets": "^10.3.8", "@sanity/client": "^6.15.3", "@sanity/webhook": "^4.0.2", "auth0": "^4.3.1", @@ -76,6 +78,7 @@ "radash": "^12.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", + "socket.io": "^4.7.5", "zod": "^3.22.4" }, "devDependencies": { diff --git a/apps/api/pnpm-lock.yaml b/apps/api/pnpm-lock.yaml index ebd7e25..da2ad8f 100644 --- a/apps/api/pnpm-lock.yaml +++ b/apps/api/pnpm-lock.yaml @@ -34,7 +34,7 @@ dependencies: version: 3.2.0(@nestjs/common@10.0.0)(rxjs@7.8.1) '@nestjs/core': specifier: ^10.0.0 - version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/cqrs': specifier: ^10.2.7 version: 10.2.7(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) @@ -44,9 +44,15 @@ dependencies: '@nestjs/platform-express': specifier: ^10.0.0 version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + '@nestjs/platform-socket.io': + specifier: ^10.3.8 + version: 10.3.8(@nestjs/common@10.0.0)(@nestjs/websockets@10.3.8)(rxjs@7.8.1) '@nestjs/swagger': specifier: ^7.3.0 version: 7.3.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(reflect-metadata@0.1.13) + '@nestjs/websockets': + specifier: ^10.3.8 + version: 10.3.8(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@sanity/client': specifier: ^6.15.3 version: 6.15.3 @@ -92,6 +98,9 @@ dependencies: rxjs: specifier: ^7.8.1 version: 7.8.1 + socket.io: + specifier: ^4.7.5 + version: 4.7.5 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1007,7 +1016,7 @@ packages: dependencies: '@mikro-orm/core': 6.1.5 '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) dev: false /@mikro-orm/postgresql@6.1.5(@mikro-orm/core@6.1.5): @@ -1117,7 +1126,7 @@ packages: uuid: 9.0.1 dev: false - /@nestjs/core@10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): + /@nestjs/core@10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1): resolution: {integrity: sha512-HFTdj4vsF+2qOaq97ZPRDle6Q/KyL5lmMah0/ZR0ie+e1/tnlvmlqw589xFACTemLJFFOjZMy763v+icO9u72w==} requiresBuild: true peerDependencies: @@ -1137,6 +1146,7 @@ packages: dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + '@nestjs/websockets': 10.3.8(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -1157,7 +1167,7 @@ packages: rxjs: ^7.2.0 dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) reflect-metadata: 0.1.13 rxjs: 7.8.1 uuid: 9.0.1 @@ -1170,7 +1180,7 @@ packages: '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) eventemitter2: 6.4.9 dev: false @@ -1198,7 +1208,7 @@ packages: '@nestjs/core': ^10.0.0 dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 express: 4.18.2 @@ -1207,6 +1217,23 @@ packages: transitivePeerDependencies: - supports-color + /@nestjs/platform-socket.io@10.3.8(@nestjs/common@10.0.0)(@nestjs/websockets@10.3.8)(rxjs@7.8.1): + resolution: {integrity: sha512-CpsWw/AaJMDTan0FoYHkKyptsrFz8JSnHBGcajvj9UMeKCJ8Hi80T9ymKnP7OozbArKimh1oBusN+k4sKRxRTg==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + rxjs: ^7.1.0 + dependencies: + '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/websockets': 10.3.8(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + rxjs: 7.8.1 + socket.io: 4.7.5 + tslib: 2.6.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /@nestjs/schematics@10.0.0(chokidar@3.5.3)(typescript@5.1.3): resolution: {integrity: sha512-gfUy/N1m1paN33BXq4d7HoCM+zM4rFxYjqAb8jkrBfBHiwyEhHHozfX/aRy/kOnAcy/VP8v4Zs4HKKrbRRlHnw==} peerDependencies: @@ -1241,7 +1268,7 @@ packages: dependencies: '@microsoft/tsdoc': 0.14.2 '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.0.0)(reflect-metadata@0.1.13) js-yaml: 4.1.0 lodash: 4.17.21 @@ -1264,11 +1291,32 @@ packages: optional: true dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) tslib: 2.5.3 dev: true + /@nestjs/websockets@10.3.8(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-DTSCK+FYtSTljT6XjVUUZhf1cPxKEJf1AG1y2n+ERnd0vzMpnYpMFgGkDlXqa3uC+LAMcOcx1EyTCcHsSHrOVg==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/platform-socket.io': ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/platform-socket.io': + optional: true + dependencies: + '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/platform-socket.io': 10.3.8(@nestjs/common@10.0.0)(@nestjs/websockets@10.3.8)(rxjs@7.8.1) + iterare: 1.2.1 + object-hash: 3.0.0 + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + tslib: 2.6.2 + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1381,6 +1429,9 @@ packages: '@sinonjs/commons': 3.0.1 dev: true + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -1443,10 +1494,18 @@ packages: '@types/node': 20.3.1 dev: true + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + /@types/cookiejar@2.1.5: resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} dev: true + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + dependencies: + '@types/node': 20.3.1 + /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -2115,6 +2174,10 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -2474,6 +2537,10 @@ packages: /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -2698,6 +2765,29 @@ packages: dependencies: once: 1.4.0 + /engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} + engines: {node: '>=10.0.0'} + + /engine.io@6.5.4: + resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.3.1 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.2.2 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} engines: {node: '>=10.13.0'} @@ -4574,6 +4664,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -5366,6 +5460,41 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + /socket.io-adapter@2.5.4: + resolution: {integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==} + dependencies: + debug: 4.3.4 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + /socket.io@4.7.5: + resolution: {integrity: sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.5.4 + socket.io-adapter: 2.5.4 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /sonic-boom@3.8.0: resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==} dependencies: @@ -6078,6 +6207,18 @@ packages: signal-exit: 3.0.7 dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/apps/api/src/activity/activity.entity.ts b/apps/api/src/activity/activity.entity.ts new file mode 100644 index 0000000..65a3781 --- /dev/null +++ b/apps/api/src/activity/activity.entity.ts @@ -0,0 +1,27 @@ +import { Entity, Enum, Property } from '@mikro-orm/core'; +import { AggregateRoot } from 'shared/database/aggregate-root'; +import { Topic } from 'shared/event/messaging'; + +export const Type = { + RESERVATION_CREATED: Topic.RESERVATION_CREATED, + RESERVATION_DELETED: Topic.RESERVATION_DELETED, +} as const; + +export type Type = (typeof Type)[keyof typeof Type]; + +@Entity({ tableName: 'activity' }) +export class Activity extends AggregateRoot { + @Enum(() => Type) + readonly type: Type; + + @Property() + userId: number; + + constructor(userId: number, createdAt: Date, type: Type) { + super(); + this.type = type; + this.userId = userId; + this.createdAt = createdAt; + this.updatedAt = createdAt; + } +} diff --git a/apps/api/src/activity/activity.module.ts b/apps/api/src/activity/activity.module.ts new file mode 100644 index 0000000..a65ca5b --- /dev/null +++ b/apps/api/src/activity/activity.module.ts @@ -0,0 +1,18 @@ +import { ReservationCreatedHandler } from './api/event-handlers/reservation-created-event.handler'; +import { ReservationDeletedHandler } from './api/event-handlers/reservation-deleted-event.handler'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { ActivityService } from './activity.service'; +import { Activity } from './activity.entity'; +import { Module } from '@nestjs/common'; +import { ActivityController } from './api/http/activity.controller'; +import { GetAllActivitiesHandler } from './get-all-activities.query'; + +const eventHandlers = [ReservationCreatedHandler, ReservationDeletedHandler]; +const queries = [GetAllActivitiesHandler]; + +@Module({ + imports: [MikroOrmModule.forFeature([Activity])], + providers: [ActivityService, ...eventHandlers, ...queries], + controllers: [ActivityController], +}) +export class ActivityModule {} diff --git a/apps/api/src/activity/activity.service.ts b/apps/api/src/activity/activity.service.ts new file mode 100644 index 0000000..095dc8b --- /dev/null +++ b/apps/api/src/activity/activity.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Activity, Type } from './activity.entity'; +import { EntityManager } from '@mikro-orm/core'; + +@Injectable() +export class ActivityService { + constructor(private em: EntityManager) {} + + async createActivity({ + userId, + occurredOn, + type, + }: { + userId: number; + occurredOn: Date; + type: Type; + }) { + const activity = new Activity(userId, occurredOn, type); + this.em.persistAndFlush(activity); + } +} diff --git a/apps/api/src/activity/api/event-handlers/reservation-created-event.handler.ts b/apps/api/src/activity/api/event-handlers/reservation-created-event.handler.ts new file mode 100644 index 0000000..6ba0cef --- /dev/null +++ b/apps/api/src/activity/api/event-handlers/reservation-created-event.handler.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReservationCreated, Topic } from 'shared/event/messaging'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { ActivityService } from 'activity/activity.service'; +import { Type } from 'activity/activity.entity'; + +@Injectable() +export class ReservationCreatedHandler { + constructor( + @InjectPinoLogger(ReservationCreatedHandler.name) + private readonly logger: PinoLogger, + private activityService: ActivityService, + ) {} + + @OnEvent(Topic.RESERVATION_CREATED, { async: true }) + async handle(payload: ReservationCreated) { + this.logger.info({ payload }, 'Executing ReservationCreatedHandler'); + const { userId, occurredOn } = payload; + await this.activityService.createActivity({ + userId, + occurredOn, + type: Type.RESERVATION_CREATED, + }); + } +} diff --git a/apps/api/src/activity/api/event-handlers/reservation-deleted-event.handler.ts b/apps/api/src/activity/api/event-handlers/reservation-deleted-event.handler.ts new file mode 100644 index 0000000..97f7977 --- /dev/null +++ b/apps/api/src/activity/api/event-handlers/reservation-deleted-event.handler.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReservationDeleted, Topic } from 'shared/event/messaging'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { ActivityService } from 'activity/activity.service'; +import { Type } from 'activity/activity.entity'; + +@Injectable() +export class ReservationDeletedHandler { + constructor( + @InjectPinoLogger(ReservationDeletedHandler.name) + private readonly logger: PinoLogger, + private activityService: ActivityService, + ) {} + + @OnEvent(Topic.RESERVATION_DELETED, { async: true }) + async handle(payload: ReservationDeleted) { + this.logger.info({ payload }, 'Executing ReservationDeletedHandler'); + const { userId, occurredOn } = payload; + await this.activityService.createActivity({ + userId, + occurredOn, + type: Type.RESERVATION_DELETED, + }); + } +} diff --git a/apps/api/src/activity/api/http/activity.controller.ts b/apps/api/src/activity/api/http/activity.controller.ts new file mode 100644 index 0000000..137980d --- /dev/null +++ b/apps/api/src/activity/api/http/activity.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Get, UseInterceptors } from '@nestjs/common'; +import { ApiBearerAuth, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + PaginationInterceptor, + PaginationQuery, +} from 'shared/pagination.interceptor'; +import { Permissions } from 'shared/auth/permission.decorator'; +import { QueryBus } from '@nestjs/cqrs'; +import { GetAllActivitiesResult } from 'activity/get-all-activities-result'; +import { GetAllActivitiesQuery } from 'activity/get-all-activities.query'; + +@Controller('activities') +@ApiTags('activities') +@ApiBearerAuth('access-token') +export class ActivityController { + constructor(private readonly queryBus: QueryBus) {} + + @Get() + @ApiResponse({ + status: 200, + type: GetAllActivitiesResult, + }) + @ApiQuery({ type: () => PaginationQuery }) + @UseInterceptors(PaginationInterceptor) + @Permissions('read:activities') + getAll(): Promise { + return this.queryBus.execute(new GetAllActivitiesQuery()); + } +} diff --git a/apps/api/src/activity/get-all-activities-result.ts b/apps/api/src/activity/get-all-activities-result.ts new file mode 100644 index 0000000..a0dd81e --- /dev/null +++ b/apps/api/src/activity/get-all-activities-result.ts @@ -0,0 +1,12 @@ +import { IQueryResult } from '@nestjs/cqrs'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from './activity.entity'; + +export class GetAllActivitiesResult implements IQueryResult { + @ApiProperty({ example: Type.RESERVATION_CREATED }) + type: Type; + @ApiProperty({ example: 1 }) + userId: number; + @ApiProperty({ example: '2024-01-01T12:00:00.000Z' }) + createdAt: Date; +} diff --git a/apps/api/src/activity/get-all-activities.query.ts b/apps/api/src/activity/get-all-activities.query.ts new file mode 100644 index 0000000..c214d4a --- /dev/null +++ b/apps/api/src/activity/get-all-activities.query.ts @@ -0,0 +1,32 @@ +import { IQuery, IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { GetAllActivitiesResult } from './get-all-activities-result'; +import { Activity } from './activity.entity'; + +export class GetAllActivitiesQuery implements IQuery { + constructor() {} +} + +@QueryHandler(GetAllActivitiesQuery) +export class GetAllActivitiesHandler + implements IQueryHandler +{ + constructor( + @InjectPinoLogger(GetAllActivitiesHandler.name) + private readonly logger: PinoLogger, + private em: EntityManager, + ) {} + + async execute( + query: GetAllActivitiesQuery, + ): Promise { + this.logger.info({ query }, 'Executing GetAllActivitiesQuery'); + + const activities = await this.em + .createQueryBuilder(Activity) + .select(['id', 'type', 'userId', 'createdAt']); + + return activities; + } +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 30c2c83..ba9b881 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -16,6 +16,8 @@ import { ReservationModule } from './reservation/reservation.module'; import { NotificationModule } from './notification/notification.module'; import mailConfig from 'config/mail.config'; import cmsConfig from 'config/cms.config'; +import { ActivityModule } from 'activity/activity.module'; +import { WebsocketModule } from 'websocket/websocket.module'; @Module({ imports: [ @@ -39,6 +41,8 @@ import cmsConfig from 'config/cms.config'; AuthModule, ReservationModule, NotificationModule, + ActivityModule, + WebsocketModule, ], controllers: [HealthController], }) diff --git a/apps/api/src/reservation/application/commands/delete-reservation-command.ts b/apps/api/src/reservation/application/commands/delete-reservation-command.ts index d201fe4..0240bc7 100644 --- a/apps/api/src/reservation/application/commands/delete-reservation-command.ts +++ b/apps/api/src/reservation/application/commands/delete-reservation-command.ts @@ -11,6 +11,8 @@ import { InjectRepository } from '@mikro-orm/nestjs'; import { InventoryItem } from 'reservation/core/entities/inventory-item.entity'; import { MikroORM } from '@mikro-orm/core'; import { z } from 'zod'; +import { IDomainEventPublisher } from 'shared/event/domain-event-publisher'; +import { Inject } from '@nestjs/common'; export class DeleteReservationCommand { constructor( @@ -31,6 +33,8 @@ export class DeleteReservationHandler implements ICommandHandler { constructor( + @Inject(IDomainEventPublisher) + private eventPublisher: IDomainEventPublisher, @InjectRepository(InventoryItem) private repository: EntityRepository, private em: EntityManager, @@ -46,13 +50,14 @@ export class DeleteReservationHandler const { userId, inventoryItemId, reservationId } = await deleteReservationSchema.parseAsync(command); - const inventoryItem = await this.repository.findOne({ - id: inventoryItemId, - }); - if (!inventoryItem) throw new EntityNotFoundException('InventoryItem'); + const entity = await this.repository.findOne({ id: inventoryItemId }); + if (!entity) throw new EntityNotFoundException('InventoryItem'); + + const inventoryItem = this.eventPublisher.mergeObjectContext(entity); inventoryItem.deleteReservation(reservationId, userId); await this.em.flush(); + inventoryItem.commit(); } } diff --git a/apps/api/src/reservation/application/event-handlers/send-reservation-deleted-integration-event.handler.ts b/apps/api/src/reservation/application/event-handlers/send-reservation-deleted-integration-event.handler.ts new file mode 100644 index 0000000..0bdc0c8 --- /dev/null +++ b/apps/api/src/reservation/application/event-handlers/send-reservation-deleted-integration-event.handler.ts @@ -0,0 +1,24 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { ReservationDeletedEvent } from 'reservation/core/events/reservation-deleted.event'; +import { ReservationDeleted, Topic } from 'shared/event/messaging'; + +@EventsHandler(ReservationDeletedEvent) +export class SendReservationDeletedIntegrationEventHandler + implements IEventHandler +{ + constructor( + private eventEmitter: EventEmitter2, + @InjectPinoLogger(SendReservationDeletedIntegrationEventHandler.name) + private readonly logger: PinoLogger, + ) {} + + handle(event: ReservationDeletedEvent) { + this.logger.info({ event }, 'Sending ReservationDeleted integration event'); + this.eventEmitter.emit( + Topic.RESERVATION_DELETED, + new ReservationDeleted(event.userId, event.createdAt), + ); + } +} diff --git a/apps/api/src/reservation/core/entities/inventory-item.entity.ts b/apps/api/src/reservation/core/entities/inventory-item.entity.ts index 7505be3..6a076f6 100644 --- a/apps/api/src/reservation/core/entities/inventory-item.entity.ts +++ b/apps/api/src/reservation/core/entities/inventory-item.entity.ts @@ -10,6 +10,7 @@ import { isAfter, isBefore, isEqual } from 'date-fns'; import { AggregateRoot } from 'shared/database/aggregate-root'; import { Reservation } from './reservation.entity'; import { ReservationCreatedEvent } from '../events/reservation-created.event'; +import { ReservationDeletedEvent } from '../events/reservation-deleted.event'; import { ReservationOverlapException } from '../exceptions/reservation-overlap.exception'; import { EntityNotFoundException } from 'shared/exceptions/entity-not-found.exception'; import { ReservationDeleteForbiddenException } from '../exceptions/reservation-delete.exception'; @@ -122,5 +123,6 @@ export class InventoryItem extends AggregateRoot { ); this._reservations.remove(reservation); + this.apply(new ReservationDeletedEvent(userId)); } } diff --git a/apps/api/src/reservation/core/events/reservation-deleted.event.ts b/apps/api/src/reservation/core/events/reservation-deleted.event.ts new file mode 100644 index 0000000..79e2f53 --- /dev/null +++ b/apps/api/src/reservation/core/events/reservation-deleted.event.ts @@ -0,0 +1,7 @@ +import { BaseEvent } from 'shared/event/base.event'; + +export class ReservationDeletedEvent extends BaseEvent { + constructor(public readonly userId: number) { + super(); + } +} diff --git a/apps/api/src/reservation/reservation.module.ts b/apps/api/src/reservation/reservation.module.ts index a8f584e..77d3ac9 100644 --- a/apps/api/src/reservation/reservation.module.ts +++ b/apps/api/src/reservation/reservation.module.ts @@ -12,6 +12,7 @@ import { GetInventoryItemsHandler } from './application/queries/get-inventory-it import { UpsertInventoryItemHandler } from './application/commands/upsert-inventory-items.command'; import { UpsertLocationHandler } from './application/commands/upsert-location.command'; import { DeleteReservationHandler } from './application/commands/delete-reservation-command'; +import { SendReservationDeletedIntegrationEventHandler } from './application/event-handlers/send-reservation-deleted-integration-event.handler'; const commands = [ CreateReservationHandler, @@ -20,7 +21,10 @@ const commands = [ UpsertLocationHandler, ]; const queries = [GetUserReservationsHandler, GetInventoryItemsHandler]; -const eventHandlers = [SendReservationCreatedIntegrationEventHandler]; +const eventHandlers = [ + SendReservationCreatedIntegrationEventHandler, + SendReservationDeletedIntegrationEventHandler, +]; @Module({ imports: [MikroOrmModule.forFeature([Reservation, Location, InventoryItem])], diff --git a/apps/api/src/shared/database/migrations/20240425132148-create-activity-table.ts b/apps/api/src/shared/database/migrations/20240425132148-create-activity-table.ts new file mode 100644 index 0000000..daa88da --- /dev/null +++ b/apps/api/src/shared/database/migrations/20240425132148-create-activity-table.ts @@ -0,0 +1,29 @@ +import { Migration } from '@mikro-orm/migrations'; + +const TABLE_NAME = 'activity'; +const types = ['reservation.created', 'reservation.deleted']; + +export class CreateReservationTable extends Migration { + async up(): Promise { + const knex = this.getKnex(); + const createActivityTable = knex.schema.createTable(TABLE_NAME, (table) => { + table.increments('id').primary(); + table.enum('type', types); + table.integer('user_id').notNullable(); + table.foreign('user_id').references('user.id').onDelete('CASCADE'); + table + .timestamp('created_at', { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + table + .timestamp('updated_at', { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + }); + this.addSql(createActivityTable.toQuery()); + } + + async down(): Promise { + this.addSql(this.getKnex().schema.dropTable(TABLE_NAME).toQuery()); + } +} diff --git a/apps/api/src/shared/event/messaging.ts b/apps/api/src/shared/event/messaging.ts index f0228ec..5054666 100644 --- a/apps/api/src/shared/event/messaging.ts +++ b/apps/api/src/shared/event/messaging.ts @@ -1,6 +1,7 @@ export const Topic = { USER_CREATED: 'user.created', RESERVATION_CREATED: 'reservation.created', + RESERVATION_DELETED: 'reservation.deleted', } as const; interface IIntegrationEvent { @@ -25,3 +26,10 @@ export class ReservationCreated implements IIntegrationEvent { readonly occurredOn: Date, ) {} } + +export class ReservationDeleted implements IIntegrationEvent { + constructor( + readonly userId: number, + readonly occurredOn: Date, + ) {} +} diff --git a/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts new file mode 100644 index 0000000..24f2992 --- /dev/null +++ b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReservationCreated, Topic } from 'shared/event/messaging'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { WebsocketService } from 'websocket/websocket.service'; +import { EventType } from 'websocket/event-type'; + +@Injectable() +export class SendReservationCreatedMessageHandler { + constructor( + @InjectPinoLogger(SendReservationCreatedMessageHandler.name) + private readonly logger: PinoLogger, + private readonly websocketService: WebsocketService, + ) {} + + @OnEvent(Topic.RESERVATION_CREATED, { async: true }) + async handle(payload: ReservationCreated) { + this.logger.info( + { payload }, + 'Executing SendReservationCreatedMessageHandler', + ); + this.websocketService.addEvent(EventType.ACTIVITY, { + type: Topic.RESERVATION_CREATED, + userId: payload.userId, + createdAt: payload.occurredOn, + }); + } +} diff --git a/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts new file mode 100644 index 0000000..6200313 --- /dev/null +++ b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReservationDeleted, Topic } from 'shared/event/messaging'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { EventType } from 'websocket/event-type'; +import { WebsocketService } from 'websocket/websocket.service'; + +@Injectable() +export class SendReservationDeletedMessageHandler { + constructor( + @InjectPinoLogger(SendReservationDeletedMessageHandler.name) + private readonly logger: PinoLogger, + private readonly websocketService: WebsocketService, + ) {} + + @OnEvent(Topic.RESERVATION_DELETED, { async: true }) + async handle(payload: ReservationDeleted) { + this.logger.info( + { payload }, + 'Executing SendReservationDeletedMessageHandler', + ); + this.websocketService.addEvent(EventType.ACTIVITY, { + type: Topic.RESERVATION_DELETED, + userId: payload.userId, + createdAt: payload.occurredOn, + }); + } +} diff --git a/apps/api/src/websocket/event-type.ts b/apps/api/src/websocket/event-type.ts new file mode 100644 index 0000000..74690f5 --- /dev/null +++ b/apps/api/src/websocket/event-type.ts @@ -0,0 +1,3 @@ +export const EventType = { + ACTIVITY: 'ACTIVITY', +}; diff --git a/apps/api/src/websocket/websocket-provider.service.ts b/apps/api/src/websocket/websocket-provider.service.ts new file mode 100644 index 0000000..fecc88c --- /dev/null +++ b/apps/api/src/websocket/websocket-provider.service.ts @@ -0,0 +1,34 @@ +import { + OnGatewayInit, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import { Server } from 'socket.io'; +import { WebsocketService } from './websocket.service'; +import { OnApplicationShutdown } from '@nestjs/common'; +import { Subscription } from 'rxjs'; + +@WebSocketGateway({ cors: true }) +export class WebSocketProviderService + implements OnGatewayInit, OnApplicationShutdown +{ + constructor(private readonly websocketService: WebsocketService) {} + + private eventSubscription: Subscription; + + @WebSocketServer() + server: Server; + + afterInit(): void { + this.eventSubscription = this.websocketService + .getEventSubject$() + .subscribe({ + next: (value: { event: string; data: any }) => + this.server.emit(value.event, value.data), + }); + } + + onApplicationShutdown() { + this.eventSubscription.unsubscribe(); + } +} diff --git a/apps/api/src/websocket/websocket.module.ts b/apps/api/src/websocket/websocket.module.ts new file mode 100644 index 0000000..36a1e9f --- /dev/null +++ b/apps/api/src/websocket/websocket.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { WebSocketProviderService } from './websocket-provider.service'; +import { SendReservationCreatedMessageHandler } from './api/event-handlers/send-message-on-reservation-created-event.handler'; +import { SendReservationDeletedMessageHandler } from './api/event-handlers/send-message-on-reservation-deleted-event.handler'; +import { WebsocketService } from './websocket.service'; + +const eventHandlers = [ + SendReservationCreatedMessageHandler, + SendReservationDeletedMessageHandler, +]; + +@Module({ + providers: [WebSocketProviderService, WebsocketService, ...eventHandlers], +}) +export class WebsocketModule {} diff --git a/apps/api/src/websocket/websocket.service.ts b/apps/api/src/websocket/websocket.service.ts new file mode 100644 index 0000000..2e97f82 --- /dev/null +++ b/apps/api/src/websocket/websocket.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { Observable, Subject } from 'rxjs'; + +@Injectable() +export class WebsocketService { + private subject = new Subject<{ event: string; data: unknown }>(); + + addEvent(eventName: string, eventData: unknown): void { + this.subject.next({ event: eventName, data: eventData }); + } + + getEventSubject$(): Observable<{ event: string; data: unknown }> { + return this.subject.asObservable(); + } +} diff --git a/apps/app/package.json b/apps/app/package.json index 4ee0f38..64b181b 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -18,6 +18,7 @@ "@vueuse/integrations": "^10.9.0", "axios": "^1.6.7", "date-fns": "^3.3.1", + "socket.io-client": "^4.7.5", "vue": "^3.4.15", "vue-router": "^4.2.5", "vuetify": "^3.5.8" diff --git a/apps/app/pnpm-lock.yaml b/apps/app/pnpm-lock.yaml index 3fe6a20..5fbe6c3 100644 --- a/apps/app/pnpm-lock.yaml +++ b/apps/app/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: date-fns: specifier: ^3.3.1 version: 3.3.1 + socket.io-client: + specifier: ^4.7.5 + version: 4.7.5 vue: specifier: ^3.4.15 version: 3.4.15(typescript@5.3.2) @@ -533,6 +536,10 @@ packages: resolution: {integrity: sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==} dev: true + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + dev: false + /@tsconfig/node20@20.1.2: resolution: {integrity: sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==} dev: true @@ -1136,7 +1143,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1161,6 +1167,25 @@ packages: esutils: 2.0.3 dev: true + /engine.io-client@6.5.3: + resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-parser: 5.2.2 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} + engines: {node: '>=10.0.0'} + dev: false + /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1691,7 +1716,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /muggle-string@0.3.1: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} @@ -1943,6 +1967,30 @@ packages: engines: {node: '>=8'} dev: true + /socket.io-client@4.7.5: + resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-client: 6.5.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -2214,11 +2262,29 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} dev: true + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true diff --git a/apps/app/src/api/activities.ts b/apps/app/src/api/activities.ts new file mode 100644 index 0000000..affee74 --- /dev/null +++ b/apps/app/src/api/activities.ts @@ -0,0 +1,5 @@ +import instance from './request'; + +export function getAll() { + return instance.get('/activities').then(response => response.data); +} diff --git a/apps/app/src/components/DefaultLayout.vue b/apps/app/src/components/DefaultLayout.vue index 4024faf..593e04d 100644 --- a/apps/app/src/components/DefaultLayout.vue +++ b/apps/app/src/components/DefaultLayout.vue @@ -32,6 +32,12 @@ prepend-icon="mdi-account-group" title="User Management" value="user-management" /> +