toi bi ngu
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -3,4 +3,5 @@ import Delta from 'quill-delta'
|
||||
export type DocumentDelta = Delta & {
|
||||
pageIndex: number
|
||||
documentId: string
|
||||
senderId?: string
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
47
src/main.ts
47
src/main.ts
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user