implement redis cache for context

This commit is contained in:
2024-10-29 00:56:23 +07:00
parent ae1aa64b41
commit 34cea2ccd3
18 changed files with 477 additions and 55 deletions

88
package-lock.json generated
View File

@@ -48,6 +48,7 @@
"graphql-tools": "^9.0.1", "graphql-tools": "^9.0.1",
"graphql-upload": "15.0.2", "graphql-upload": "15.0.2",
"graphql-ws": "^5.16.0", "graphql-ws": "^5.16.0",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"minio": "^8.0.1", "minio": "^8.0.1",
"nestjs-minio": "^2.6.2", "nestjs-minio": "^2.6.2",
@@ -3390,6 +3391,12 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -7836,6 +7843,15 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": { "node_modules/co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -8358,6 +8374,15 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -10779,6 +10804,30 @@
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
}, },
"node_modules/ioredis": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz",
"integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -12463,6 +12512,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.get": { "node_modules/lodash.get": {
"version": "4.4.2", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@@ -12475,6 +12530,12 @@
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lodash.isboolean": { "node_modules/lodash.isboolean": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
@@ -14993,6 +15054,27 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
@@ -15816,6 +15898,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",

View File

@@ -68,6 +68,7 @@
"graphql-tools": "^9.0.1", "graphql-tools": "^9.0.1",
"graphql-upload": "15.0.2", "graphql-upload": "15.0.2",
"graphql-ws": "^5.16.0", "graphql-ws": "^5.16.0",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"minio": "^8.0.1", "minio": "^8.0.1",
"nestjs-minio": "^2.6.2", "nestjs-minio": "^2.6.2",

View File

@@ -1,11 +1,11 @@
import { Global, Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { AppConfigSchema } from './appconfig.schema'; import { AppConfigSchema } from './appconfig.schema';
import { AppConfigService } from './appconfig.service';
@Global() @Global()
@Module({ @Module({
imports: [AppConfigSchema], providers: [AppConfigSchema, AppConfigService],
providers: [AppConfigSchema], exports: [AppConfigSchema, AppConfigService],
exports: [AppConfigSchema],
}) })
export class AppConfigModule {} export class AppConfigModule {}

View File

@@ -26,8 +26,12 @@ export class AppConfigSchema extends PothosSchema {
id: t.exposeID('id', { id: t.exposeID('id', {
description: 'The unique identifier for the config', description: 'The unique identifier for the config',
}), }),
name: t.exposeString('name', { description: 'The name of the config' }), name: t.exposeString('name', {
key: t.exposeString('key', { description: 'The key of the config' }), description: 'The name of the config',
}),
key: t.exposeString('key', {
description: 'The key of the config',
}),
value: t.exposeString('value', { value: t.exposeString('value', {
description: 'The value of the config', description: 'The value of the config',
}), }),
@@ -45,7 +49,7 @@ export class AppConfigSchema extends PothosSchema {
type: [this.appConfig()], type: [this.appConfig()],
description: 'Get all app configs', description: 'Get all app configs',
args: this.builder.generator.findManyArgs('Config'), args: this.builder.generator.findManyArgs('Config'),
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args) => {
return await this.prisma.config.findMany({ return await this.prisma.config.findMany({
...query, ...query,
where: args.filter ?? undefined, where: args.filter ?? undefined,
@@ -56,6 +60,53 @@ export class AppConfigSchema extends PothosSchema {
}); });
}, },
}), }),
appConfig: t.prismaField({
type: this.appConfig(),
description: 'Get an app config by key',
args: this.builder.generator.findUniqueArgs('Config'),
resolve: async (query, root, args) => {
return await this.prisma.config.findUnique({
...query,
where: args.where ?? undefined,
});
},
}),
}));
// Mutations
this.builder.mutationFields((t) => ({
createAppConfig: t.prismaField({
type: this.appConfig(),
description: 'Create an app config',
args: {
input: t.arg({
type: this.builder.generator.getCreateInput('Config'),
required: true,
}),
},
resolve: async (query, root, args) => {
return await this.prisma.config.create({
...query,
data: args.input,
});
},
}),
createAppConfigs: t.prismaField({
type: [this.appConfig()],
description: 'Create multiple app configs',
args: {
input: t.arg({
type: this.builder.generator.getCreateManyInput('Config'),
required: true,
}),
},
resolve: async (query, root, args) => {
return await this.prisma.config.createManyAndReturn({
...query,
data: args.input,
});
},
}),
})); }));
} }
} }

View File

@@ -262,7 +262,7 @@ export class CenterSchema extends PothosSchema {
if (args.approve) { if (args.approve) {
try { try {
await this.mailService.sendTemplateEmail( await this.mailService.sendTemplateEmail(
centerOwner.email, [centerOwner.email],
'Thông báo phê duyệt đăng ký trung tâm', 'Thông báo phê duyệt đăng ký trung tâm',
'CenterApproved', 'CenterApproved',
{ {
@@ -277,7 +277,7 @@ export class CenterSchema extends PothosSchema {
// mail to center owner if rejected // mail to center owner if rejected
try { try {
await this.mailService.sendTemplateEmail( await this.mailService.sendTemplateEmail(
centerOwner.email, [centerOwner.email],
'Thông báo từ chối đăng ký trung tâm', 'Thông báo từ chối đăng ký trung tâm',
'CenterRejected', 'CenterRejected',
{ {

View File

@@ -56,7 +56,7 @@ export class CenterMentorSchema extends PothosSchema {
@Pothos() @Pothos()
init(): void { init(): void {
this.builder.queryFields((t) => ({ this.builder.queryFields((t) => ({
centerMentor: t.prismaField({ centerMentors: t.prismaField({
description: description:
'Retrieve a list of center mentors with optional filtering, ordering, and pagination.', 'Retrieve a list of center mentors with optional filtering, ordering, and pagination.',
type: [this.centerMentor()], type: [this.centerMentor()],
@@ -159,7 +159,7 @@ export class CenterMentorSchema extends PothosSchema {
const inviteUrl = `${process.env.CENTER_BASE_URL}/invite?token=${token}`; const inviteUrl = `${process.env.CENTER_BASE_URL}/invite?token=${token}`;
// mail to user with params centerId, email // mail to user with params centerId, email
await this.mailService.sendTemplateEmail( await this.mailService.sendTemplateEmail(
args.email, [args.email],
'Invite to center', 'Invite to center',
'MentorInvitation', 'MentorInvitation',
{ {
@@ -189,7 +189,7 @@ export class CenterMentorSchema extends PothosSchema {
const inviteUrl = `${process.env.CENTER_BASE_URL}/invite?token=${token}`; const inviteUrl = `${process.env.CENTER_BASE_URL}/invite?token=${token}`;
// mail to user with params centerId, email // mail to user with params centerId, email
await this.mailService.sendTemplateEmail( await this.mailService.sendTemplateEmail(
args.email, [args.email],
'Invite to center', 'Invite to center',
'MentorInvitation', 'MentorInvitation',
{ {
@@ -251,7 +251,7 @@ export class CenterMentorSchema extends PothosSchema {
if (args.approved) { if (args.approved) {
// send mail to user // send mail to user
await this.mailService.sendTemplateEmail( await this.mailService.sendTemplateEmail(
email.email, [email.email],
'Thông báo về việc được chấp nhận làm mentor', 'Thông báo về việc được chấp nhận làm mentor',
'MentorApproved', 'MentorApproved',
{ {
@@ -291,7 +291,7 @@ export class CenterMentorSchema extends PothosSchema {
} }
// if rejected, update adminNote // if rejected, update adminNote
await this.mailService.sendTemplateEmail( await this.mailService.sendTemplateEmail(
email.email, [email.email],
'Thông báo về việc không được chấp nhận làm mentor', 'Thông báo về việc không được chấp nhận làm mentor',
'MentorRejected', 'MentorRejected',
{ {

View File

@@ -2,6 +2,7 @@ import { Global, MiddlewareConsumer, Module } from '@nestjs/common';
import { AdminNoteModule } from '../AdminNote/adminnote.module'; import { AdminNoteModule } from '../AdminNote/adminnote.module';
import { ApolloDriverConfig } from '@nestjs/apollo'; import { ApolloDriverConfig } from '@nestjs/apollo';
import { AppConfigModule } from '../AppConfig/appconfig.module';
import { Builder } from './graphql.builder'; import { Builder } from './graphql.builder';
import { CategoryModule } from '../Category/category.module'; import { CategoryModule } from '../Category/category.module';
import { CenterMentorModule } from '../CenterMentor/centermentor.module'; import { CenterMentorModule } from '../CenterMentor/centermentor.module';
@@ -21,6 +22,8 @@ import { PothosModule } from '@smatch-corp/nestjs-pothos';
import { PrismaCrudGenerator } from './graphql.generator'; import { PrismaCrudGenerator } from './graphql.generator';
import { PrismaModule } from '../Prisma/prisma.module'; import { PrismaModule } from '../Prisma/prisma.module';
import { PrismaService } from '../Prisma/prisma.service'; import { PrismaService } from '../Prisma/prisma.service';
import { RedisModule } from 'src/Redis/redis.module';
import { RedisService } from 'src/Redis/redis.service';
import { RefundTicketModule } from '../RefundTicket/refundticket.module'; import { RefundTicketModule } from '../RefundTicket/refundticket.module';
import { Request } from 'express'; import { Request } from 'express';
import { ResumeModule } from '../Resume/resume.module'; import { ResumeModule } from '../Resume/resume.module';
@@ -42,6 +45,8 @@ import { initContextCache } from '@pothos/core';
imports: [ imports: [
CommonModule, CommonModule,
PrismaModule, PrismaModule,
RedisModule,
AppConfigModule,
UserModule, UserModule,
CenterModule, CenterModule,
ServiceModule, ServiceModule,
@@ -93,8 +98,9 @@ import { initContextCache } from '@pothos/core';
providers: [ providers: [
{ {
provide: GraphqlService, provide: GraphqlService,
useFactory: (prisma: PrismaService) => new GraphqlService(prisma), useFactory: (prisma: PrismaService, redis: RedisService) =>
inject: [PrismaService], new GraphqlService(prisma, redis),
inject: [PrismaService, 'REDIS_CLIENT'],
}, },
{ {
provide: Builder, provide: Builder,

View File

@@ -1,12 +1,22 @@
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import {
Inject,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from '../Prisma/prisma.service'; import { PrismaService } from '../Prisma/prisma.service';
import { Request } from 'express'; import { Request } from 'express';
import { clerkClient } from '@clerk/express'; import { clerkClient } from '@clerk/express';
import { RedisService } from '../Redis/redis.service';
@Injectable() @Injectable()
export class GraphqlService { export class GraphqlService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
@Inject('REDIS_CLIENT') private readonly redis: RedisService,
) {}
async acquireContext(req: Request) { async acquireContext(req: Request) {
// get x-session-id from headers // get x-session-id from headers
@@ -24,6 +34,11 @@ export class GraphqlService {
if (disableAuth) { if (disableAuth) {
return null; return null;
} }
// redis context cache
const cachedUser = await this.redis.getUser(sessionId);
if (cachedUser) {
return cachedUser;
}
// check if the token is valid // check if the token is valid
const session = await clerkClient.sessions.getSession(sessionId as string); const session = await clerkClient.sessions.getSession(sessionId as string);
if (!session) { if (!session) {
@@ -35,7 +50,7 @@ export class GraphqlService {
if (!user) { if (!user) {
throw new UnauthorizedException('User not found'); throw new UnauthorizedException('User not found');
} }
Logger.log(`User ${user.name} with id ${user.id} acquired context`); await this.redis.setUser(sessionId, user, session.expireAt);
return user; return user;
} }
} }

View File

@@ -30,7 +30,7 @@ export class MailService {
} }
async sendTemplateEmail( async sendTemplateEmail(
to: string, to: string[],
subject: string, subject: string,
template: string, template: string,
context: any, context: any,

View File

@@ -0,0 +1,72 @@
doctype html
html
head
meta(charset="UTF-8")
title Thông báo phê duyệt Dịch vụ #{SERVICE_NAME} của Trung tâm #{CENTER_NAME}
style.
body {
font-family: Arial, sans-serif;
background-color: #f0f8ff;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
background-color: #457D84; /* Medium teal */
color: #ffffff;
padding: 15px;
border-radius: 8px 8px 0 0;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.content {
padding: 20px;
color: #333;
}
.content p {
font-size: 16px;
line-height: 1.5;
}
.button {
display: inline-block;
padding: 12px 20px;
background-color: #2BD4E2; /* Bright aqua */
color: #ffffff;
text-decoration: none;
font-size: 16px;
border-radius: 5px;
text-align: center;
margin: 20px 0;
}
.footer {
text-align: center;
font-size: 14px;
color: #555;
padding: 10px;
border-top: 1px solid #e0e0e0;
}
body
.container
.header
h1 Chúc mừng Dịch vụ #{SERVICE_NAME} đã được phê duyệt
.content
p Kính gửi Quý Trung tâm #{CENTER_NAME},
p Chúng tôi vui mừng thông báo rằng dịch vụ #{SERVICE_NAME} của bạn đã được phê duyệt trên nền tảng của chúng tôi.
p Vui lòng nhấn vào nút dưới đây để truy cập vào dịch vụ của bạn:
a.button(href="https://center.epess.org") Truy cập Dịch vụ
p Nếu bạn có bất kỳ thắc mắc nào, đừng ngần ngại liên hệ với chúng tôi.
.footer
p Trân trọng,
p EPESS
p Nền tảng hỗ trợ viết luận

View File

@@ -0,0 +1,84 @@
doctype html
html
head
meta(charset="UTF-8")
title Thông báo từ chối Dịch vụ #{SERVICE_NAME} của Trung tâm #{CENTER_NAME}
style.
body {
font-family: Arial, sans-serif;
background-color: #f0f8ff;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
background-color: #d9534f; /* Red color for rejection */
color: #ffffff;
padding: 15px;
border-radius: 8px 8px 0 0;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.content {
padding: 20px;
color: #333;
}
.content p {
font-size: 16px;
line-height: 1.5;
}
.note {
background-color: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
border: 1px solid #f5c6cb;
}
.button {
display: inline-block;
padding: 12px 20px;
background-color: #2BD4E2; /* Bright aqua */
color: #ffffff;
text-decoration: none;
font-size: 16px;
border-radius: 5px;
text-align: center;
margin: 20px 0;
}
.footer {
text-align: center;
font-size: 14px;
color: #555;
padding: 10px;
border-top: 1px solid #e0e0e0;
}
body
.container
.header
h1 Thông báo từ chối Dịch vụ #{SERVICE_NAME}
.content
p Kính gửi Quý Trung tâm #{CENTER_NAME},
p Chúng tôi rất tiếc thông báo rằng dịch vụ #{SERVICE_NAME} của bạn chưa được phê duyệt trên nền tảng của chúng tôi.
.note
p Lý do từ chối:
p #{ADMIN_NOTE}
p Chúng tôi khuyến khích bạn xem xét lại thông tin và nộp đơn đăng ký lại trong tương lai.
p Bạn có thể truy cập trang web của chúng tôi để biết thêm thông tin:
a.button(href="https://center.epess.org") Truy cập Trung tâm
p Nếu bạn có bất kỳ thắc mắc nào, đừng ngần ngại liên hệ với chúng tôi.
.footer
p Trân trọng,
p EPESS
p Nền tảng hỗ trợ viết luận

File diff suppressed because one or more lines are too long

View File

@@ -14,7 +14,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit {
super({ super({
log: [ log: [
{ {
emit: 'event', emit: 'stdout',
level: 'query', level: 'query',
}, },
{ {

15
src/Redis/redis.module.ts Normal file
View File

@@ -0,0 +1,15 @@
import { Global, Module } from '@nestjs/common';
import { RedisService } from './redis.service';
@Global()
@Module({
providers: [
{
provide: 'REDIS_CLIENT',
useClass: RedisService,
},
],
exports: ['REDIS_CLIENT'],
})
export class RedisModule {}

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { Redis } from 'ioredis';
import { User } from '@prisma/client';
@Injectable()
export class RedisService {
private readonly redis: Redis;
constructor() {
this.redis = new Redis(process.env.REDIS_URL as string);
}
async get(key: string) {
return await this.redis.get(key);
}
async set(key: string, value: string, expireAt: number) {
return await this.redis.set(key, value, 'EXAT', expireAt);
}
async del(key: string) {
return await this.redis.del(key);
}
async close() {
return await this.redis.quit();
}
async getUser(sessionId: string) {
const userData = await this.get(sessionId);
if (!userData) {
return null;
}
const retrievedUser: User = JSON.parse(userData);
return retrievedUser;
}
async setUser(sessionId: string, user: User, expireAt: number) {
return await this.set(sessionId, JSON.stringify(user), expireAt);
}
}

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { import {
Pothos, Pothos,
PothosRef, PothosRef,
@@ -237,6 +237,10 @@ export class ServiceSchema extends PothosSchema {
type: 'Boolean', type: 'Boolean',
required: true, required: true,
}), }),
adminNote: t.arg({
type: 'String',
required: false,
}),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args, ctx, info) => {
return await this.prisma.$transaction(async (prisma) => { return await this.prisma.$transaction(async (prisma) => {
@@ -258,9 +262,15 @@ export class ServiceSchema extends PothosSchema {
status: args.approve status: args.approve
? ServiceStatus.APPROVED ? ServiceStatus.APPROVED
: ServiceStatus.REJECTED, : ServiceStatus.REJECTED,
adminNote: {
create: {
content: args.adminNote ?? '',
notedByUserId: ctx.me.id,
},
},
}, },
}); });
// mail to center owner and mentor who requested the service // mail to all mentor or center owner for the center
const center = await prisma.center.findUnique({ const center = await prisma.center.findUnique({
where: { id: service.centerId }, where: { id: service.centerId },
}); });
@@ -276,15 +286,36 @@ export class ServiceSchema extends PothosSchema {
const centerMentor = await prisma.centerMentor.findMany({ const centerMentor = await prisma.centerMentor.findMany({
where: { centerId: service.centerId }, where: { centerId: service.centerId },
}); });
const mentorEmails = centerMentor.map((mentor) => mentor.mentorId); const mentorIds = centerMentor.map((mentor) => mentor.mentorId);
const emails = [centerOwner.email, ...mentorEmails]; // get mentor emails
for (const email of emails) { const mentorEmails = await prisma.user.findMany({
await this.mailService.sendEmail( where: { id: { in: mentorIds } },
email, });
args.approve Logger.log(mentorEmails, 'ServiceSchema');
? 'Your service has been approved' const emails = [
: 'Your service has been rejected', centerOwner.email,
args.approve ? 'service-approved' : 'service-rejected', ...mentorEmails.map((mentor) => mentor.email),
];
if (args.approve) {
await this.mailService.sendTemplateEmail(
emails,
'Thông báo về trạng thái dịch vụ',
'ServiceApproved',
{
SERVICE_NAME: service.name,
CENTER_NAME: center.name,
},
);
} else {
await this.mailService.sendTemplateEmail(
emails,
'Thông báo về trạng thái dịch vụ',
'ServiceRejected',
{
SERVICE_NAME: service.name,
CENTER_NAME: center.name,
ADMIN_NOTE: args.adminNote ?? 'Không có lý do',
},
); );
} }
return updatedService; return updatedService;

View File

@@ -129,7 +129,7 @@ export class UserSchema extends PothosSchema {
me: t.prismaField({ me: t.prismaField({
description: 'Retrieve the current user by token.', description: 'Retrieve the current user by token.',
type: this.user(), type: this.user(),
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args, ctx) => {
// get session id from X-Session-Id // get session id from X-Session-Id
const sessionId = ctx.req.headers['x-session-id']; const sessionId = ctx.req.headers['x-session-id'];
if (!sessionId) if (!sessionId)
@@ -155,7 +155,7 @@ export class UserSchema extends PothosSchema {
'Retrieve a list of users with optional filtering, ordering, and pagination.', 'Retrieve a list of users with optional filtering, ordering, and pagination.',
type: [this.user()], type: [this.user()],
args: this.builder.generator.findManyArgs('User'), args: this.builder.generator.findManyArgs('User'),
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args) => {
return await this.prisma.user.findMany({ return await this.prisma.user.findMany({
...query, ...query,
take: args.take ?? undefined, take: args.take ?? undefined,
@@ -170,7 +170,7 @@ export class UserSchema extends PothosSchema {
description: 'Retrieve a single user by their unique identifier.', description: 'Retrieve a single user by their unique identifier.',
type: this.user(), type: this.user(),
args: this.builder.generator.findUniqueArgs('User'), args: this.builder.generator.findUniqueArgs('User'),
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args) => {
return await this.prisma.user.findUniqueOrThrow({ return await this.prisma.user.findUniqueOrThrow({
...query, ...query,
where: args.where, where: args.where,
@@ -183,7 +183,7 @@ export class UserSchema extends PothosSchema {
args: { args: {
sessionId: t.arg({ type: 'String', required: true }), sessionId: t.arg({ type: 'String', required: true }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args) => {
// check if the token is valid // check if the token is valid
const session = await clerkClient.sessions.getSession(args.sessionId); const session = await clerkClient.sessions.getSession(args.sessionId);
Logger.log(session, 'Session'); Logger.log(session, 'Session');
@@ -212,7 +212,7 @@ export class UserSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args) => {
return await this.prisma.user.update({ return await this.prisma.user.update({
...query, ...query,
where: args.where, where: args.where,
@@ -221,29 +221,14 @@ export class UserSchema extends PothosSchema {
}, },
}), }),
// banUser: t.prismaField({
// description: 'Ban a user.',
// type: this.user(),
// args: {
// userId: t.arg({ type: 'String', required: true }),
// },
// resolve: async (query, root, args, ctx, info) => {
// return await this.prisma.user.update({
// ...query,
// where: { id: args.userId },
// data: { banned: true },
// });
// },
// }),
sendEmailTest: t.field({ sendEmailTest: t.field({
type: 'String', type: 'String',
args: { args: {
to: t.arg({ type: 'String', required: true }), to: t.arg({ type: 'String', required: true }),
}, },
resolve: async (_parent, args, _context, _info) => { resolve: async (_parent, args) => {
await this.mailService.sendTemplateEmail( await this.mailService.sendTemplateEmail(
args.to, [args.to],
'Bạn đã được mời làm việc tại Trung tâm băng đĩa lậu hải ngoại', 'Bạn đã được mời làm việc tại Trung tâm băng đĩa lậu hải ngoại',
'MentorInvitation', 'MentorInvitation',
{ {

View File

@@ -50,7 +50,7 @@ export class WorkshopSubscriptionSchema extends PothosSchema {
args: this.builder.generator.findUniqueArgs('WorkshopSubscription'), args: this.builder.generator.findUniqueArgs('WorkshopSubscription'),
description: description:
'Retrieve a single workshop subscription by its unique identifier.', 'Retrieve a single workshop subscription by its unique identifier.',
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args) => {
return await this.prisma.workshopSubscription.findUnique({ return await this.prisma.workshopSubscription.findUnique({
...query, ...query,
where: args.where, where: args.where,
@@ -62,7 +62,7 @@ export class WorkshopSubscriptionSchema extends PothosSchema {
args: this.builder.generator.findManyArgs('WorkshopSubscription'), args: this.builder.generator.findManyArgs('WorkshopSubscription'),
description: description:
'Retrieve a list of workshop subscriptions with optional filtering, ordering, and pagination.', 'Retrieve a list of workshop subscriptions with optional filtering, ordering, and pagination.',
resolve: async (query, root, args, ctx, info) => { resolve: async (query, root, args) => {
return await this.prisma.workshopSubscription.findMany({ return await this.prisma.workshopSubscription.findMany({
...query, ...query,
skip: args.skip ?? undefined, skip: args.skip ?? undefined,