toi bi ngu

This commit is contained in:
2024-11-17 20:27:33 +07:00
parent bb0eed1851
commit 3430971449
14 changed files with 675 additions and 82 deletions

View File

@@ -155,7 +155,7 @@ export class CenterMentorSchema extends PothosSchema {
}
// build signature
const token = this.jwtUtils.signTokenRS256(
JSON.stringify({ centerId: center.id, email: args.email }),
{ centerId: center.id, email: args.email },
'1d',
)
// build invite url
@@ -185,7 +185,7 @@ export class CenterMentorSchema extends PothosSchema {
return this.prisma.$transaction(async () => {
// sign token
const token = this.jwtUtils.signTokenRS256(
JSON.stringify({ centerId: args.centerId, email: args.email }),
{ centerId: args.centerId, email: args.email },
'1d',
)
// build invite url

View File

@@ -11,11 +11,13 @@ import { DocumentEvent } from './document.event'
import { Document } from '@prisma/client'
import { DocumentDelta } from './document.type'
import Delta from 'quill-delta'
import { MinioService } from 'src/Minio/minio.service'
@Injectable()
export class DocumentSchema extends PothosSchema {
constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService,
private readonly minio: MinioService,
) {
super()
}
@@ -46,6 +48,7 @@ export class DocumentSchema extends PothosSchema {
delta: t.field({
type: 'Delta',
}),
senderId: t.string(),
}),
})
}
@@ -88,6 +91,30 @@ export class DocumentSchema extends PothosSchema {
})
},
}),
newDocument: t.field({
type: this.document(),
args: {},
resolve: async (query, _args, ctx, _info) => {
if (ctx.isSubscription) throw new Error('Not allowed')
const userId = ctx.http?.me?.id
if (!userId) throw new Error('User not found')
const fileUrl = await this.minio.getFileUrl(
'document',
'document',
'document',
)
if (!fileUrl) throw new Error('File not found')
const document = await this.prisma.document.create({
...query,
data: {
name: 'Untitled',
fileUrl,
ownerId: userId,
},
})
return document
},
}),
}))
this.builder.mutationFields((t) => ({
@@ -99,10 +126,16 @@ export class DocumentSchema extends PothosSchema {
required: true,
}),
},
resolve: async (query, _root, args) => {
resolve: async (query, _root, args, ctx: SchemaContext) => {
if (ctx.isSubscription) throw new Error('Not allowed')
const userId = ctx.http?.me?.id
if (!userId) throw new Error('User not found')
return await this.prisma.document.create({
...query,
data: args.data,
data: {
...args.data,
// ownerId: userId,
},
})
},
}),
@@ -120,6 +153,7 @@ export class DocumentSchema extends PothosSchema {
documentId: args.documentId,
pageIndex: args.pageIndex,
delta,
senderId: ctx.http?.me?.id,
}
ctx.http.pubSub.publish(
`${DocumentEvent.CHANGED}.${args.documentId}`,
@@ -142,10 +176,12 @@ export class DocumentSchema extends PothosSchema {
const {
http: { pubSub },
} = ctx
pubSub.publish(
`${DocumentEvent.CHANGED}.${args.data.documentId}`,
args.data,
)
const senderId = ctx.http?.me?.id
if (!senderId) throw new Error('User not found')
pubSub.publish(`${DocumentEvent.CHANGED}.${args.data.documentId}`, {
...args.data,
senderId,
})
return args.data
},
}),
@@ -172,7 +208,11 @@ export class DocumentSchema extends PothosSchema {
`${DocumentEvent.SAVED}.${args.documentId}`,
]) as unknown as AsyncIterable<DocumentDelta>
},
resolve: async (payload: DocumentDelta) => payload,
resolve: async (payload: DocumentDelta, _args, ctx: SchemaContext) => {
if (!ctx.isSubscription) throw new Error('Not allowed')
if (payload.senderId === ctx.websocket?.me?.id) return
return payload
},
}),
}))
}

View File

@@ -31,7 +31,7 @@ export class DocumentService {
const { default: Quill } = await import('quill')
this.quill = Quill
}
// TODO: maybe never do :)
async handleOnChange(delta: DocumentDelta) {}
async handleOnSave() {}

View File

@@ -3,4 +3,5 @@ import Delta from 'quill-delta'
export type DocumentDelta = Delta & {
pageIndex: number
documentId: string
senderId?: string
}

View File

@@ -34,6 +34,7 @@ export type SchemaContext =
websocket: {
req: Request
pubSub: PubSub
sessionId: string
me: User
generator: PrismaCrudGenerator<BuilderTypes>
}
@@ -159,8 +160,8 @@ export class Builder extends SchemaBuilder<SchemaBuilderOption> {
},
})
this.scalarType('Delta', {
serialize: (value) => value.toString(),
parseValue: (value: unknown) => value as unknown as Delta,
serialize: (value) => JSON.stringify(value),
parseValue: (value: unknown) => JSON.parse(value as string) as Delta,
parseLiteral: (ast: ValueNode) => ast as unknown as Delta,
})

View File

@@ -41,6 +41,7 @@ import { WorkshopSubscriptionModule } from '../WorkshopSubscription/workshopsubs
import { initContextCache } from '@pothos/core'
import { PubSub } from 'graphql-subscriptions'
import { DocumentModule } from 'src/Document/document.module'
import { Context } from 'graphql-ws'
@Global()
@Module({
@@ -90,25 +91,43 @@ import { DocumentModule } from 'src/Document/document.module'
debug: process.env.NODE_ENV === 'development' || false,
playground: process.env.NODE_ENV === 'development' || false,
introspection: process.env.NODE_ENV === 'development' || false,
installSubscriptionHandlers: true,
subscriptions: {
'graphql-ws': true,
'graphql-ws': {
onConnect: (ctx: Context<Record<string, unknown>>) => {
if (!ctx.connectionParams) {
throw new Error('No connectionParams provided')
}
if (!ctx.extra) {
throw new Error('No extra provided')
}
// @ts-expect-error: TODO
ctx.extra.request.headers['x-session-id'] = ctx.connectionParams['x-session-id']
},
},
},
context: async ({
req,
subscriptions,
extra,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
}: { req?: Request; subscriptions?: any; extra?: any }) => {
}: {
req?: Request
subscriptions?: Record<string, never>
extra?: Record<string, never>
}) => {
initContextCache()
if (subscriptions) {
// @ts-expect-error: TODO
if (!extra?.request?.headers['x-session-id']) {
throw new Error('No sessionId provided')
}
return {
isSubscription: true,
websocket: {
req: extra.request,
req: extra?.request,
pubSub: pubsub,
me: await graphqlService.acquireContext(
extra.request.headers['x-session-id'],
),
// @ts-expect-error: TODO
me: await graphqlService.acquireContextFromSessionId(extra.request.headers['x-session-id']),
},
}
}
@@ -118,10 +137,7 @@ import { DocumentModule } from 'src/Document/document.module'
req,
me: req ? await graphqlService.acquireContext(req) : null,
pubSub: pubsub,
invalidateCache: () =>
graphqlService.invalidateCache(
req?.headers['x-session-id'] as string,
),
invalidateCache: () => graphqlService.invalidateCache(req?.headers['x-session-id'] as string),
},
}
},
@@ -132,8 +148,7 @@ import { DocumentModule } from 'src/Document/document.module'
RedisService,
{
provide: GraphqlService,
useFactory: (prisma: PrismaService, redis: RedisService) =>
new GraphqlService(prisma, redis),
useFactory: (prisma: PrismaService, redis: RedisService) => new GraphqlService(prisma, redis),
inject: [PrismaService, 'REDIS_CLIENT'],
},
{
@@ -151,12 +166,6 @@ import { DocumentModule } from 'src/Document/document.module'
useFactory: () => new PubSub(),
},
],
exports: [
Builder,
PrismaCrudGenerator,
GraphqlService,
RedisService,
'PUB_SUB',
],
exports: [Builder, PrismaCrudGenerator, GraphqlService, RedisService, 'PUB_SUB'],
})
export class GraphqlModule {}

View File

@@ -18,6 +18,12 @@ export class GraphqlService {
@Inject('REDIS_CLIENT') private readonly redis: RedisService,
) {}
async acquireContextFromSessionId(sessionId: string) {
return this.acquireContext({
headers: { 'x-session-id': sessionId },
} as unknown as Request)
}
async acquireContext(req: Request) {
// get x-session-id from headers
let sessionId: string

View File

@@ -433,7 +433,6 @@ export class UserSchema extends PothosSchema {
const {
websocket: { pubSub },
} = ctx
Logger.log(ctx.websocket.me?.id, 'Me ID')
return pubSub.asyncIterator([
`${PubSubEvent.NEW_MESSAGE}.${ctx.websocket.me?.id}`,
]) as unknown as AsyncIterable<Message>

View File

@@ -4,10 +4,10 @@ import { Injectable } from '@nestjs/common'
@Injectable()
export class JwtUtils {
signToken(payload: string, expiresIn: string) {
signToken(payload: object, expiresIn: string) {
return sign(payload, process.env.JWT_SECRET!, { expiresIn })
}
signTokenRS256(payload: string, expiresIn: string) {
signTokenRS256(payload: object, expiresIn: string) {
const privateKey = process.env.JWT_RS256_PRIVATE_KEY
if (!privateKey) {
throw new Error('JWT_RS256_PRIVATE_KEY is not defined')

View File

@@ -7,54 +7,32 @@ import { clerkMiddleware } from '@clerk/express'
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'
import path from 'node:path'
import { readFileSync } from 'node:fs'
import { json } from 'express'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// load private key and public key
const privateKey = readFileSync(
path.join(__dirname, 'KeyStore', 'private_key.pem'),
'utf8',
)
const publicKey = readFileSync(
path.join(__dirname, 'KeyStore', 'public_key.pem'),
'utf8',
)
const privateKey = readFileSync(path.join(__dirname, 'KeyStore', 'private_key.pem'), 'utf8')
const publicKey = readFileSync(path.join(__dirname, 'KeyStore', 'public_key.pem'), 'utf8')
// set private key and public key to env
process.env.JWT_RS256_PRIVATE_KEY = privateKey
process.env.JWT_RS256_PUBLIC_KEY = publicKey
Logger.log(
`Private key: ${privateKey.slice(0, 10).replace(/\n/g, '')}...`,
'Bootstrap',
)
Logger.log(
`Public key: ${publicKey.slice(0, 10).replace(/\n/g, '')}...`,
'Bootstrap',
)
Logger.log(`Private key: ${privateKey.slice(0, 10).replace(/\n/g, '')}...`, 'Bootstrap')
Logger.log(`Public key: ${publicKey.slice(0, 10).replace(/\n/g, '')}...`, 'Bootstrap')
const corsOrigin = (process.env.CORS_ORIGIN ?? '').split(',') // split by comma to array
app.enableCors({
origin: corsOrigin,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'*',
'x-apollo-operation-name',
'x-session-id',
],
allowedHeaders: ['Content-Type', '*', 'x-apollo-operation-name', 'x-session-id'],
credentials: true,
})
// set base path for api
app.setGlobalPrefix(process.env.API_PATH ?? '/v1')
const config = new DocumentBuilder()
.setTitle('EPESS API')
.setDescription('API documentation for EPESS application')
.setVersion('0.0.1')
.addBearerAuth()
.build()
const config = new DocumentBuilder().setTitle('EPESS API').setDescription('API documentation for EPESS application').setVersion('0.0.1').addBearerAuth().build()
const document = SwaggerModule.createDocument(app, config)
SwaggerModule.setup(process.env.SWAGGER_PATH ?? 'v1', app, document)
@@ -63,8 +41,7 @@ async function bootstrap() {
get: {
tags: ['GraphQL'],
summary: 'GraphQL Playground',
description:
'Access the GraphQL Playground to interact with the GraphQL API.',
description: 'Access the GraphQL Playground to interact with the GraphQL API.',
responses: {
'200': {
description: 'GraphQL Playground',
@@ -74,6 +51,9 @@ async function bootstrap() {
}
try {
// body parser
app.use(json({ limit: '50mb' }))
// clerk middleware
app.use(clerkMiddleware({}))
@@ -87,10 +67,7 @@ async function bootstrap() {
)
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
} catch (error: any) {
Logger.error(
`Error in file upload middleware: ${error.message}`,
'Bootstrap',
)
Logger.error(`Error in file upload middleware: ${error.message}`, 'Bootstrap')
// Optionally, you can handle the error further or rethrow it
}