day len server theo loi Khoi noi, toi xin tuyen bo mien tru trach nhiem

This commit is contained in:
2024-11-05 15:02:53 +07:00
parent 0f68b51d75
commit 56ba2808c8
22 changed files with 482 additions and 63 deletions

225
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@graphql-codegen/typescript": "^4.0.9", "@graphql-codegen/typescript": "^4.0.9",
"@graphql-codegen/typescript-operations": "^4.2.3", "@graphql-codegen/typescript-operations": "^4.2.3",
"@graphql-codegen/typescript-resolvers": "^4.2.1", "@graphql-codegen/typescript-resolvers": "^4.2.1",
"@livekit/rtc-node": "^0.11.0",
"@nestjs-modules/mailer": "^2.0.2", "@nestjs-modules/mailer": "^2.0.2",
"@nestjs/apollo": "^12.2.0", "@nestjs/apollo": "^12.2.0",
"@nestjs/axios": "^3.1.1", "@nestjs/axios": "^3.1.1",
@@ -53,6 +54,7 @@
"graphql-ws": "^5.16.0", "graphql-ws": "^5.16.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"livekit-server-sdk": "^2.7.3",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"minio": "^8.0.1", "minio": "^8.0.1",
"nestjs-minio": "^2.6.2", "nestjs-minio": "^2.6.2",
@@ -88,6 +90,7 @@
"@types/nodemailer": "^6.4.16", "@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
@@ -1913,6 +1916,12 @@
"node": ">=14.21.3" "node": ">=14.21.3"
} }
}, },
"node_modules/@bufbuild/protobuf": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.2.tgz",
"integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@clerk/backend": { "node_modules/@clerk/backend": {
"version": "1.15.2", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.15.2.tgz", "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.15.2.tgz",
@@ -4139,6 +4148,134 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@livekit/mutex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.0.0.tgz",
"integrity": "sha512-aiUhoThBNF9UyGTxEURFzJLhhPLIVTnQiEVMjRhPnfHNKLfo2JY9xovHKIus7B78UD5hsP6DlgpmAsjrz4U0Iw==",
"license": "Apache-2.0"
},
"node_modules/@livekit/protocol": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.27.1.tgz",
"integrity": "sha512-ISEp7uWdV82mtCR1eyHFTzdRZTVbe2+ZztjmjiMPzR/KPrI1Ma/u5kLh87NNuY3Rn8wv1VlEvGHHsFjQ+dKVUw==",
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protobuf": "^1.10.0"
}
},
"node_modules/@livekit/protocol/node_modules/@bufbuild/protobuf": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@livekit/rtc-node": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@livekit/rtc-node/-/rtc-node-0.11.0.tgz",
"integrity": "sha512-HRnoGsNJC+okhzV9IlljaWskOYIGi1xLD9NzwJwRkzYRdPFwEMf5QU5bmELNCfwDw8bMOpMQB4M8wIkVhCrcxg==",
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protobuf": "^2.2.0",
"@livekit/mutex": "^1.0.0",
"@livekit/typed-emitter": "^3.0.0"
},
"engines": {
"node": ">= 18"
},
"optionalDependencies": {
"@livekit/rtc-node-darwin-arm64": "0.11.0",
"@livekit/rtc-node-darwin-x64": "0.11.0",
"@livekit/rtc-node-linux-arm64-gnu": "0.11.0",
"@livekit/rtc-node-linux-x64-gnu": "0.11.0",
"@livekit/rtc-node-win32-x64-msvc": "0.11.0"
}
},
"node_modules/@livekit/rtc-node-darwin-arm64": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@livekit/rtc-node-darwin-arm64/-/rtc-node-darwin-arm64-0.11.0.tgz",
"integrity": "sha512-ZepFYFO984NnkM6h29KvGagGpQ7/Q/7WdtVr68rhque8YyP/SLPfW1AA73jSn7+FwIVQiaUUERtrOlSlEWe8hg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@livekit/rtc-node-darwin-x64": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@livekit/rtc-node-darwin-x64/-/rtc-node-darwin-x64-0.11.0.tgz",
"integrity": "sha512-JsqwV6XVDFYFUV67QZ3CFxD3OeK6+eZZzWuDjR7ELam5cDyqYjqi2bWEVDPnv+4zA+w6Xx9g7IPRDzkMdA60kg==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@livekit/rtc-node-linux-arm64-gnu": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@livekit/rtc-node-linux-arm64-gnu/-/rtc-node-linux-arm64-gnu-0.11.0.tgz",
"integrity": "sha512-3d1WtyMKhQwLbiZQuw6MnpazPq2nOSChFiePoIuZhcrDyZrN5Tte/QBkIL8G+dCXwyD1jfknTL4D0FeI+GRMww==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@livekit/rtc-node-linux-x64-gnu": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@livekit/rtc-node-linux-x64-gnu/-/rtc-node-linux-x64-gnu-0.11.0.tgz",
"integrity": "sha512-iwi1FpnJgI0JNq1pNe6jgqIXa16bujBQPhEF0ul47uyp7bOnSpVzVAnbAdkrACnaalB106YWZ6cc1L37gACYPg==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@livekit/rtc-node-win32-x64-msvc": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@livekit/rtc-node-win32-x64-msvc/-/rtc-node-win32-x64-msvc-0.11.0.tgz",
"integrity": "sha512-tLrdolxU+0PBxssIrSFNJie5XaCi1X/GN6GIbp/ZnSOZ+nwaNxFBmYolGjxRuW7ks1HGR7z/zIIpJey5CnFkWg==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@livekit/typed-emitter": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@livekit/typed-emitter/-/typed-emitter-3.0.0.tgz",
"integrity": "sha512-9bl0k4MgBPZu3Qu3R3xy12rmbW17e3bE9yf4YY85gJIQ3ezLEj/uzpKHWBsLaDoL5Mozz8QCgggwIBudYQWeQg==",
"license": "MIT"
},
"node_modules/@ljharb/through": { "node_modules/@ljharb/through": {
"version": "2.3.13", "version": "2.3.13",
"resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz",
@@ -5759,6 +5896,13 @@
"@types/superagent": "^8.1.0" "@types/superagent": "^8.1.0"
} }
}, },
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/validator": { "node_modules/@types/validator": {
"version": "13.12.2", "version": "13.12.2",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
@@ -7686,6 +7830,60 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/camelcase-keys": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz",
"integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==",
"license": "MIT",
"dependencies": {
"camelcase": "^8.0.0",
"map-obj": "5.0.0",
"quick-lru": "^6.1.1",
"type-fest": "^4.3.2"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase-keys/node_modules/camelcase": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
"integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase-keys/node_modules/map-obj": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz",
"integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase-keys/node_modules/type-fest": {
"version": "4.26.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz",
"integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001676", "version": "1.0.30001676",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz",
@@ -12237,7 +12435,6 @@
"version": "5.9.6", "version": "5.9.6",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz",
"integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
@@ -12759,6 +12956,20 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/livekit-server-sdk": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-2.7.3.tgz",
"integrity": "sha512-dBiyMJ2o3Adw7aBVuFxVOlYHmiZtGGS9zVksMuv/wiEVHY+6XSDzo0X67pZVkyGlq1moF4YZAReVY2Dbxve8NQ==",
"license": "Apache-2.0",
"dependencies": {
"@livekit/protocol": "^1.27.0",
"camelcase-keys": "^9.0.0",
"jose": "^5.1.2"
},
"engines": {
"node": ">=19"
}
},
"node_modules/loader-runner": { "node_modules/loader-runner": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -15241,6 +15452,18 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/quick-lru": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
"integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/randombytes": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",

View File

@@ -37,6 +37,7 @@
"@graphql-codegen/typescript": "^4.0.9", "@graphql-codegen/typescript": "^4.0.9",
"@graphql-codegen/typescript-operations": "^4.2.3", "@graphql-codegen/typescript-operations": "^4.2.3",
"@graphql-codegen/typescript-resolvers": "^4.2.1", "@graphql-codegen/typescript-resolvers": "^4.2.1",
"@livekit/rtc-node": "^0.11.0",
"@nestjs-modules/mailer": "^2.0.2", "@nestjs-modules/mailer": "^2.0.2",
"@nestjs/apollo": "^12.2.0", "@nestjs/apollo": "^12.2.0",
"@nestjs/axios": "^3.1.1", "@nestjs/axios": "^3.1.1",
@@ -75,6 +76,7 @@
"graphql-ws": "^5.16.0", "graphql-ws": "^5.16.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"livekit-server-sdk": "^2.7.3",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"minio": "^8.0.1", "minio": "^8.0.1",
"nestjs-minio": "^2.6.2", "nestjs-minio": "^2.6.2",
@@ -110,6 +112,7 @@
"@types/nodemailer": "^6.4.16", "@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",

View File

@@ -142,7 +142,7 @@ export class CenterMentorSchema extends PothosSchema {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
// get centerId by user id from context // get centerId by user id from context
const userId = ctx.http.me.id const userId = ctx.http.me?.id
if (!userId) { if (!userId) {
throw new Error('User ID is required') throw new Error('User ID is required')
} }
@@ -266,7 +266,7 @@ export class CenterMentorSchema extends PothosSchema {
data: { data: {
content: args.adminNote ?? '', content: args.adminNote ?? '',
mentorId: mentor.id, mentorId: mentor.id,
notedByUserId: ctx.http.me.id, notedByUserId: ctx.http.me?.id ?? '',
}, },
}) })
// update user role // update user role
@@ -312,7 +312,7 @@ export class CenterMentorSchema extends PothosSchema {
adminNote: { adminNote: {
create: { create: {
content: args.adminNote ?? '', content: args.adminNote ?? '',
notedByUserId: ctx.http.me.id, notedByUserId: ctx.http.me?.id ?? '',
updatedAt: new Date(), updatedAt: new Date(),
}, },
}, },

View File

@@ -43,7 +43,7 @@ export type SchemaContext =
http: { http: {
req: Request req: Request
res: Response res: Response
me: User me: User | null
pubSub: PubSub pubSub: PubSub
invalidateCache: () => Promise<void> invalidateCache: () => Promise<void>
generator: PrismaCrudGenerator<BuilderTypes> generator: PrismaCrudGenerator<BuilderTypes>

View File

@@ -30,6 +30,10 @@ export class GraphqlService {
if (disableAuth) { if (disableAuth) {
return null return null
} }
// check if the sessionId is valid
if (!sessionId) {
return null
}
// redis context cache // redis context cache
const cachedUser = await this.redis.getUser(sessionId) const cachedUser = await this.redis.getUser(sessionId)
if (cachedUser) { if (cachedUser) {

View File

@@ -0,0 +1,7 @@
// import { Module } from '@nestjs/common'
// import { LiveKitService } from './livekit.service'
// @Module({
// providers: [LiveKitService],
// exports: [LiveKitService],
// })
// export class LiveKitModule {}

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common'
// @ts-ignore
import { AccessToken } from 'livekit-server-sdk'
@Injectable()
export class LiveKitParticipantService {
async createAccessToken(participantId: string) {
return new AccessToken(
process.env.LIVEKIT_API_KEY,
process.env.LIVEKIT_API_SECRET,
{
identity: participantId,
},
)
}
async grantRoomJoinPermission(token: AccessToken, roomName: string) {
token.addGrant({ roomJoin: true, room: roomName })
}
async toJWT(token: AccessToken) {
return token.toJwt()
}
}

View File

@@ -0,0 +1,36 @@
// import {
// Room,
// RoomServiceClient,
// RoomCompositeOptions,
// // @ts-ignore
// } from 'livekit-server-sdk'
// import { v4 as uuidv4 } from 'uuid'
// export class LiveKitRoomService {
// private roomServiceClient: RoomServiceClient
// constructor() {
// this.roomServiceClient = new RoomServiceClient(
// process.env.LIVEKIT_URL as string,
// process.env.LIVEKIT_API_KEY as string,
// process.env.LIVEKIT_API_SECRET as string,
// )
// }
// async createServiceMeetingRoom(chattingRoomId: string) {
// const room = await this.roomServiceClient.createRoom({
// maxParticipants: 3,
// name: chattingRoomId,
// })
// return room
// }
// async createWorkshopMeetingRoom(workshopId: string, maxParticipants: number) {
// const room = await this.roomServiceClient.createRoom({
// maxParticipants: maxParticipants,
// name: workshopId,
// })
// return room
// }
// }

View File

@@ -0,0 +1,11 @@
// import { Injectable, OnModuleInit } from '@nestjs/common'
// import { LiveKitRoomService } from './livekit.room.service'
// @Injectable()
// export class LiveKitService implements OnModuleInit {
// private liveKitRoomService: LiveKitRoomService
// async onModuleInit() {
// // init livekit room service
// this.liveKitRoomService = new LiveKitRoomService()
// }
// }

View File

@@ -0,0 +1,5 @@
// @ts-ignore
import { TrackInfo } from 'livekit-server-sdk'
export class LiveKitTrackService {
}

View File

@@ -57,7 +57,7 @@ export class MessageSchema extends PothosSchema {
type: this.message(), type: this.message(),
description: 'Retrieve a single message by its unique identifier.', description: 'Retrieve a single message by its unique identifier.',
args: this.builder.generator.findUniqueArgs('Message'), args: this.builder.generator.findUniqueArgs('Message'),
resolve: async (query, root, args) => { resolve: async (query, _root, args) => {
return await this.prisma.message.findUnique({ return await this.prisma.message.findUnique({
...query, ...query,
where: args.where, where: args.where,
@@ -69,7 +69,7 @@ export class MessageSchema extends PothosSchema {
description: description:
'Retrieve a list of messages with optional filtering, ordering, and pagination.', 'Retrieve a list of messages with optional filtering, ordering, and pagination.',
args: this.builder.generator.findManyArgs('Message'), args: this.builder.generator.findManyArgs('Message'),
resolve: async (query, root, args) => { resolve: async (query, _root, args) => {
return await this.prisma.message.findMany({ return await this.prisma.message.findMany({
...query, ...query,
skip: args.skip ?? undefined, skip: args.skip ?? undefined,
@@ -83,7 +83,7 @@ export class MessageSchema extends PothosSchema {
type: [this.message()], type: [this.message()],
description: 'Retrieve a list of messages by chat room ID.', description: 'Retrieve a list of messages by chat room ID.',
args: this.builder.generator.findManyArgs('Message'), args: this.builder.generator.findManyArgs('Message'),
resolve: async (query, root, args) => { resolve: async (query, _root, args) => {
return await this.prisma.message.findMany({ return await this.prisma.message.findMany({
...query, ...query,
where: args.filter ?? undefined, where: args.filter ?? undefined,
@@ -94,6 +94,19 @@ export class MessageSchema extends PothosSchema {
// mutations // mutations
this.builder.mutationFields((t) => ({ this.builder.mutationFields((t) => ({
testSendMessage: t.field({
type: 'String',
description: 'Test sending a message.',
resolve: async (_, __, ctx) => {
if (ctx.isSubscription) {
throw new Error('Not allowed')
}
ctx.http.pubSub.publish('MESSAGE_SENT', {
message: 'Hello, world!',
})
return 'Message sent'
},
}),
sendMessage: t.prismaField({ sendMessage: t.prismaField({
type: this.message(), type: this.message(),
description: 'Send a message to a chat room.', description: 'Send a message to a chat room.',
@@ -104,7 +117,7 @@ export class MessageSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, ctx, _info) => {
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
...query, ...query,
data: args.input, data: args.input,

View File

@@ -8,8 +8,8 @@ import {
import { Builder } from '../Graphql/graphql.builder' import { Builder } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service' import { PrismaService } from '../Prisma/prisma.service'
import { OrderStatus } from '@prisma/client' import { OrderStatus } from '@prisma/client'
import { DateTimeUtils } from 'src/common/utils/datetime.utils' import { DateTimeUtils } from '../common/utils/datetime.utils'
import { PayosService } from 'src/Payos/payos.service' import { PayosService } from '../Payos/payos.service'
@Injectable() @Injectable()
export class OrderSchema extends PothosSchema { export class OrderSchema extends PothosSchema {
constructor( constructor(
@@ -45,6 +45,9 @@ export class OrderSchema extends PothosSchema {
scheduleId: t.exposeID('scheduleId', { scheduleId: t.exposeID('scheduleId', {
description: 'The ID of the schedule.', description: 'The ID of the schedule.',
}), }),
schedule: t.relation('schedule', {
description: 'The schedule of the order.',
}),
createdAt: t.expose('createdAt', { createdAt: t.expose('createdAt', {
type: 'DateTime', type: 'DateTime',
description: 'The date and time the order was created.', description: 'The date and time the order was created.',
@@ -140,7 +143,7 @@ export class OrderSchema extends PothosSchema {
} }
// check if input schedule has order id then throw error // check if input schedule has order id then throw error
const schedule = await this.prisma.schedule.findUnique({ const schedule = await this.prisma.schedule.findUnique({
where: { id: args.data.scheduleId }, where: { id: args.data.schedule.connect?.id ?? '' },
}) })
if (schedule?.orderId) { if (schedule?.orderId) {
// check if order status is PAID OR PENDING // check if order status is PAID OR PENDING
@@ -159,9 +162,9 @@ export class OrderSchema extends PothosSchema {
data: { data: {
status: OrderStatus.PENDING, status: OrderStatus.PENDING,
total: service.price, total: service.price,
userId: ctx.http.me.id, userId: ctx.http.me?.id ?? '',
serviceId: service.id, serviceId: service.id,
scheduleId: args.data.scheduleId, scheduleId: args.data.schedule.connect?.id ?? '',
}, },
}) })
// check if service is valid // check if service is valid
@@ -173,7 +176,7 @@ export class OrderSchema extends PothosSchema {
if (order.total === 0) { if (order.total === 0) {
// assign schedule // assign schedule
await this.prisma.schedule.update({ await this.prisma.schedule.update({
where: { id: args.data.scheduleId }, where: { id: args.data.schedule.connect?.id ?? '' },
data: { data: {
orderId: order.id, orderId: order.id,
}, },
@@ -197,8 +200,8 @@ export class OrderSchema extends PothosSchema {
orderCode: paymentCode, orderCode: paymentCode,
amount: service.price, amount: service.price,
description: service.name, description: service.name,
buyerName: ctx.http.me.name, buyerName: ctx.http.me?.name ?? '',
buyerEmail: ctx.http.me.email, buyerEmail: ctx.http.me?.email ?? '',
returnUrl: `${process.env.PAYOS_RETURN_URL}`.replace( returnUrl: `${process.env.PAYOS_RETURN_URL}`.replace(
'<serviceId>', '<serviceId>',
service.id, service.id,

View File

@@ -10,7 +10,12 @@ import type {
CancelPaymentLinkRequestType, CancelPaymentLinkRequestType,
DataType, DataType,
} from '@payos/node/lib/type' } from '@payos/node/lib/type'
import { OrderStatus, PaymentStatus, ScheduleStatus } from '@prisma/client' import {
ChatRoomType,
OrderStatus,
PaymentStatus,
ScheduleStatus,
} from '@prisma/client'
export type CreatePaymentBody = CheckoutRequestType export type CreatePaymentBody = CheckoutRequestType
export type CreatePaymentResponse = CheckoutResponseDataType export type CreatePaymentResponse = CheckoutResponseDataType
@Injectable() @Injectable()
@@ -52,7 +57,7 @@ export class PayosService {
status: orderStatus, status: orderStatus,
}, },
}) })
const order = await this.prisma.order.findUnique({ const order = await this.prisma.order.findUniqueOrThrow({
where: { id: payment.orderId }, where: { id: payment.orderId },
}) })
const schedule = await this.prisma.schedule.findUnique({ const schedule = await this.prisma.schedule.findUnique({
@@ -62,16 +67,36 @@ export class PayosService {
await this.prisma.schedule.update({ await this.prisma.schedule.update({
where: { id: schedule?.id }, where: { id: schedule?.id },
data: { data: {
customerId: order?.userId,
orderId: order?.id, orderId: order?.id,
status: ScheduleStatus.IN_PROGRESS, status: ScheduleStatus.IN_PROGRESS,
}, },
}) })
// get mentor id from managed service
const managedService = await this.prisma.managedService.findUniqueOrThrow({
where: { id: schedule?.managedServiceId },
})
const mentorId = managedService.mentorId
// get center id from order service
const orderService = await this.prisma.service.findUniqueOrThrow({
where: { id: order?.serviceId },
})
const centerId = orderService.centerId
// create chatroom for service meeting room
await this.prisma.chatRoom.create({
data: {
type: ChatRoomType.SUPPORT,
customerId: order.userId,
centerId: centerId,
mentorId: mentorId,
},
})
return { return {
message: 'Payment received', message: 'Payment received',
} }
} }
async createPaymentURL(body: any) { async createPaymentURL(body: CheckoutRequestType) {
return await this.payos.createPaymentLink(body) return await this.payos.createPaymentLink(body)
} }
@@ -90,6 +115,7 @@ export class PayosService {
return await this.payos.cancelPaymentLink(orderId, cancellationReason) return await this.payos.cancelPaymentLink(orderId, cancellationReason)
} }
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
async refundPayment(body: any) { async refundPayment(body: any) {
return body return body
} }

View File

@@ -117,7 +117,7 @@ export class ResumeSchema extends PothosSchema {
const resumes = await this.prisma.resume.findMany({ const resumes = await this.prisma.resume.findMany({
...query, ...query,
where: { where: {
userId: ctx.http.me.id, userId: ctx.http.me?.id ?? '',
status: args.status ?? undefined, status: args.status ?? undefined,
}, },
}) })
@@ -274,7 +274,7 @@ export class ResumeSchema extends PothosSchema {
if (ctx.isSubscription) { if (ctx.isSubscription) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
if (ctx.http.me.role !== Role.MODERATOR) { if (ctx.http.me?.role !== Role.MODERATOR) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
const { resumeId, status, adminNote } = args const { resumeId, status, adminNote } = args
@@ -315,7 +315,7 @@ export class ResumeSchema extends PothosSchema {
_adminNote = await tx.adminNote.create({ _adminNote = await tx.adminNote.create({
data: { data: {
content: adminNote, content: adminNote,
notedByUserId: ctx.http.me.id, notedByUserId: ctx.http.me?.id ?? '',
resumeId, resumeId,
}, },
}) })

View File

@@ -11,6 +11,7 @@ import { ScheduleStatus } from '@prisma/client'
import { ScheduleService } from './schedule.service' import { ScheduleService } from './schedule.service'
import { AppConfigService } from '../AppConfig/appconfig.service' import { AppConfigService } from '../AppConfig/appconfig.service'
import { ScheduleConfigType } from './schedule' import { ScheduleConfigType } from './schedule'
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
@Injectable() @Injectable()
export class ScheduleSchema extends PothosSchema { export class ScheduleSchema extends PothosSchema {
@@ -290,36 +291,40 @@ d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-1
required: true, required: true,
}), }),
}, },
resolve: async (query, _root, args, _ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Cannot create schedule in subscription')
}
Logger.log('args.schedule', args.schedule) Logger.log('args.schedule', args.schedule)
// check if there is any overlapping schedule // generate preview and check if there is any overlapping with other schedules date in same service
const overlappingSchedules = await this.prisma.schedule.findMany({ const previewSchedule =
where: { await this.scheduleService.createSchedulePreviewForCenter({
OR: [ startDate: args.schedule.scheduleStart as string,
{ endDate: args.schedule.scheduleEnd as string,
scheduleStart: { slots: args.schedule.slots as number[],
gte: args.schedule.scheduleStart, days: args.schedule.daysOfWeek as number[],
}, })
scheduleEnd: { const existingScheduleDates = await this.prisma.scheduleDate.findMany(
lte: args.schedule.scheduleEnd, {
}, where: {
}, serviceId: args.schedule.managedService.connect?.id,
], },
}, },
}) )
// check if is same managedServiceId // check if there is any overlapping with existing schedule dates in same service using DateTimeUtils
if ( const isOverlapping = DateTimeUtils.isOverlaps(
overlappingSchedules.some( previewSchedule.slots.map((slot) => ({
(schedule) => start: DateTimeUtils.fromIsoString(slot.start),
schedule.managedServiceId === end: DateTimeUtils.fromIsoString(slot.end),
args.schedule.managedService.connect?.id, })),
) existingScheduleDates.map((date) => ({
) { start: DateTimeUtils.fromDate(date.start),
throw new Error( end: DateTimeUtils.fromDate(date.end),
`Overlapping schedule with ${JSON.stringify( })),
overlappingSchedules.map((schedule) => schedule.id), )
)}`, if (isOverlapping) {
) Logger.error('Overlapping schedule', 'ScheduleSchema')
throw new Error('Overlapping schedule')
} }
const schedule = await this.prisma.schedule.create({ const schedule = await this.prisma.schedule.create({
...query, ...query,

View File

@@ -102,7 +102,6 @@ export class ScheduleService {
} }
} }
} }
const scheduleDatesCreated = const scheduleDatesCreated =
await this.prisma.scheduleDate.createManyAndReturn({ await this.prisma.scheduleDate.createManyAndReturn({
data: scheduleDates, data: scheduleDates,

View File

@@ -159,14 +159,14 @@ export class ServiceSchema extends PothosSchema {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
// check role if user is mentor or center owner // check role if user is mentor or center owner
const role = ctx.http.me.role const role = ctx.http.me?.role
if (role !== Role.CENTER_MENTOR && role !== Role.CENTER_OWNER) { if (role !== Role.CENTER_MENTOR && role !== Role.CENTER_OWNER) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
if (role === Role.CENTER_MENTOR) { if (role === Role.CENTER_MENTOR) {
// load only service belong to center of current user // load only service belong to center of current user
const managedServices = await this.prisma.managedService.findMany({ const managedServices = await this.prisma.managedService.findMany({
where: { mentorId: ctx.http.me.id }, where: { mentorId: ctx.http.me?.id ?? '' },
}) })
if (!managedServices) { if (!managedServices) {
throw new Error('Managed services not found') throw new Error('Managed services not found')
@@ -179,7 +179,7 @@ export class ServiceSchema extends PothosSchema {
// if role is center owner, load all services belong to center of current user // if role is center owner, load all services belong to center of current user
if (role === Role.CENTER_OWNER) { if (role === Role.CENTER_OWNER) {
const center = await this.prisma.center.findUnique({ const center = await this.prisma.center.findUnique({
where: { centerOwnerId: ctx.http.me.id }, where: { centerOwnerId: ctx.http.me?.id ?? '' },
}) })
if (!center) { if (!center) {
throw new Error('Center not found') throw new Error('Center not found')
@@ -234,7 +234,7 @@ export class ServiceSchema extends PothosSchema {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
// replace userId with current user id // replace userId with current user id
args.input.user = { connect: { id: ctx.http.me.id } } args.input.user = { connect: { id: ctx.http.me?.id ?? '' } }
return await this.prisma.service.create({ return await this.prisma.service.create({
...query, ...query,
data: args.input, data: args.input,
@@ -321,7 +321,7 @@ export class ServiceSchema extends PothosSchema {
adminNote: { adminNote: {
create: { create: {
content: args.adminNote ?? '', content: args.adminNote ?? '',
notedByUserId: ctx.http.me.id, notedByUserId: ctx.http.me?.id ?? '',
}, },
}, },
}, },

View File

@@ -7,12 +7,15 @@ import {
} from '@smatch-corp/nestjs-pothos' } from '@smatch-corp/nestjs-pothos'
import { Builder } from '../Graphql/graphql.builder' import { Builder } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service' import { PrismaService } from '../Prisma/prisma.service'
// import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service'
import { v4 as uuidv4 } from 'uuid'
@Injectable() @Injectable()
export class ServiceMeetingRoomSchema extends PothosSchema { export class ServiceMeetingRoomSchema extends PothosSchema {
constructor( constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder, @Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
// private readonly liveKitRoomService: LiveKitRoomService,
) { ) {
super() super()
} }
@@ -66,5 +69,33 @@ export class ServiceMeetingRoomSchema extends PothosSchema {
}, },
}), }),
})) }))
this.builder.mutationFields((t) => ({
createServiceMeetingRoom: t.prismaField({
type: this.serviceMeetingRoom(),
args: {
input: t.arg({
type: this.builder.generator.getCreateInput('ServiceMeetingRoom'),
required: true,
}),
},
description: 'Create a new service meeting room.',
resolve: async (query, _root, args, _ctx, _info) => {
// for test only !!!
if (args.input.chattingRoom.create) {
args.input.chattingRoom.create.id = uuidv4()
}
// call livekit room service to create room
// this.liveKitRoomService.createServiceMeetingRoom(
// args.input.chattingRoom.create?.id ?? '',
// )
return await this.prisma.serviceMeetingRoom.create({
...query,
data: args.input,
})
},
}),
}))
} }
} }

View File

@@ -262,7 +262,10 @@ export class UserSchema extends PothosSchema {
if (ctx.isSubscription) { if (ctx.isSubscription) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
const id = ctx.http.me.id const id = ctx.http.me?.id
if (!id) {
throw new Error('User not found')
}
if (args.imageBlob) { if (args.imageBlob) {
const { mimetype, createReadStream } = await args.imageBlob const { mimetype, createReadStream } = await args.imageBlob
if (mimetype && createReadStream) { if (mimetype && createReadStream) {
@@ -333,7 +336,7 @@ export class UserSchema extends PothosSchema {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
// check context is admin // check context is admin
if (ctx.http.me.role !== 'ADMIN') { if (ctx.http.me?.role !== 'ADMIN') {
throw new UnauthorizedException(`Only admin can invite moderator`) throw new UnauthorizedException(`Only admin can invite moderator`)
} }
let user let user

View File

@@ -4,6 +4,7 @@ import { GraphqlModule } from './Graphql/graphql.module'
import { MailModule } from './Mail/mail.module' import { MailModule } from './Mail/mail.module'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { RestfulModule } from './Restful/restful.module' import { RestfulModule } from './Restful/restful.module'
// import { LiveKitModule } from './LiveKit/livekit.module'
@Module({ @Module({
imports: [ imports: [
@@ -14,6 +15,7 @@ import { RestfulModule } from './Restful/restful.module'
MailModule, MailModule,
GraphqlModule, GraphqlModule,
RestfulModule, RestfulModule,
// LiveKitModule,
], ],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -60,6 +60,20 @@ export class DateTimeUtils {
) )
} }
static isOverlaps(
listA: { start: DateTime; end: DateTime }[],
listB: { start: DateTime; end: DateTime }[],
): boolean {
for (const a of listA) {
for (const b of listB) {
if (this.isOverlap(a.start, a.end, b.start, b.end)) {
return true
}
}
}
return false
}
static fromIsoString(isoString: string): DateTime { static fromIsoString(isoString: string): DateTime {
const dateTime = DateTime.fromISO(isoString) const dateTime = DateTime.fromISO(isoString)
if (!dateTime.isValid) { if (!dateTime.isValid) {

File diff suppressed because one or more lines are too long