add pdf attachment upload

This commit is contained in:
Hongbo Wu 2022-02-15 22:22:13 +08:00
parent 3f34813e97
commit 5d95e6c734
4 changed files with 228 additions and 1 deletions

View file

@ -0,0 +1,120 @@
import express from 'express'
import { env } from '../../env'
import * as jwt from 'jsonwebtoken'
import { PageType, UploadFileStatus } from '../../generated/graphql'
import {
generateUploadFilePathName,
generateUploadSignedUrl,
getStorageFileDetails,
} from '../../utils/uploads'
import { initModels } from '../../server'
import { kx } from '../../datalayer/knex_config'
import { analytics } from '../../utils/analytics'
export function pdfAttachmentsRouter() {
const router = express.Router()
// eslint-disable-next-line @typescript-eslint/no-misused-promises
router.post('/upload', async (req, res) => {
console.log('pdf-attachments/upload')
const { email, fileName } = req.body as {
email: string
fileName: string
}
const token = req?.headers?.authorization
if (!token || !jwt.verify(token, env.server.jwtSecret)) {
return res.status(401).send('UNAUTHORIZED')
}
const models = initModels(kx, false)
const user = await models.user.getWhere({ email })
if (!user) {
return res.status(401).send('UNAUTHORIZED')
}
analytics.track({
userId: user.id,
event: 'pdf-attachment-upload',
properties: {
env: env.server.apiEnv,
},
})
const contentType = 'application/pdf'
const uploadFileData = await models.uploadFile.create({
url: '',
userId: user.id,
fileName: fileName,
status: UploadFileStatus.Initialized,
contentType: contentType,
})
if (uploadFileData.id) {
const uploadFilePathName = generateUploadFilePathName(
uploadFileData.id,
fileName
)
const uploadSignedUrl = await generateUploadSignedUrl(
uploadFilePathName,
contentType
)
res.send({
id: uploadFileData.id,
url: uploadSignedUrl,
})
} else {
res.status(400).send('BAD REQUEST')
}
})
// eslint-disable-next-line @typescript-eslint/no-misused-promises
router.post('create-article', async (req, res) => {
console.log('pdf-attachments/create-article')
const { email, uploadFileId } = req.body as {
email: string
uploadFileId: string
}
const token = req?.headers?.authorization
if (!token || !jwt.verify(token, env.server.jwtSecret)) {
return res.status(401).send('UNAUTHORIZED')
}
const models = initModels(kx, false)
const user = await models.user.getWhere({ email })
if (!user) {
return res.status(401).send('UNAUTHORIZED')
}
analytics.track({
userId: user.id,
event: 'create-article',
properties: {
env: env.server.apiEnv,
},
})
const uploadFile = await models.uploadFile.getWhere({
id: uploadFileId,
userId: user.id,
})
if (!uploadFile) {
return res.status(400).send('BAD REQUEST')
}
const uploadFileDetails = await getStorageFileDetails(
uploadFileId,
uploadFile.fileName
)
const uploadFileHash = uploadFileDetails.md5Hash
const userArticleUrl = uploadFileDetails.fileUrl
const canonicalUrl = uploadFile.url
const pageType = PageType.File
})
return router
}

View file

@ -37,6 +37,7 @@ import { emailsServiceRouter } from './routers/svc/emails'
import ReminderModel from './datalayer/reminders'
import { remindersServiceRouter } from './routers/svc/reminders'
import { ApolloServer } from 'apollo-server-express'
import { pdfAttachmentsRouter } from './routers/svc/pdf_attachments'
const PORT = process.env.PORT || 4000
@ -97,6 +98,7 @@ export const createApp = (): {
app.use('/svc/pubsub/newsletters', newsletterServiceRouter())
app.use('/svc/pubsub/emails', emailsServiceRouter())
app.use('/svc/reminders', remindersServiceRouter())
app.use('/svc/pdf-attachments', pdfAttachmentsRouter())
if (env.dev.isLocal) {
app.use('/local/debug', localDebugRouter())

View file

@ -13,6 +13,7 @@ import {
isNewsletter,
} from './newsletter'
import { PubSub } from '@google-cloud/pubsub'
import { handlePdfAttachment } from './pdf'
const NON_NEWSLETTER_EMAIL_TOPIC = 'nonNewsletterEmailReceived'
const pubsub = new PubSub()
@ -23,9 +24,14 @@ export const inboundEmailHandler = Sentry.GCPFunction.wrapHttpFunction(
const parsed: Record<string, string> = {}
for (const part of parts) {
const { name, data } = part
const { name, data, type, filename } = part
if (name && data) {
parsed[name] = data.toString()
} else if (type === 'application/pdf' && data) {
parsed['pdf-attachment-data'] = data.toString()
parsed['pdf-attachment-filename'] = filename
? filename
: 'attachment.pdf'
} else {
console.log('no data or name for ', part)
}
@ -76,7 +82,15 @@ export const inboundEmailHandler = Sentry.GCPFunction.wrapHttpFunction(
if (isConfirmationEmail(from)) {
console.log('handleConfirmation', from, recipientAddress)
await handleConfirmation(recipientAddress, subject)
} else if (parsed['pdf-attachment']) {
console.log('handle PDF attachment', from, recipientAddress)
await handlePdfAttachment(
recipientAddress,
parsed['pdf-attachment-filename'],
parsed['pdf-attachment-data']
)
}
// queue non-newsletter emails
await pubsub.topic(NON_NEWSLETTER_EMAIL_TOPIC).publishMessage({
json: {

View file

@ -0,0 +1,91 @@
import axios, { AxiosResponse } from 'axios'
import { promisify } from 'util'
import * as jwt from 'jsonwebtoken'
const signToken = promisify(jwt.sign)
type UploadResponse = {
id: string
url: string
}
export const handlePdfAttachment = async (
email: string,
fileName: string,
data: string
): Promise<void> => {
const uploadResult = await getUploadIdAndSignedUrl(email, fileName)
await uploadToSignedUrl(uploadResult.url, data)
await createArticle(email, uploadResult.id)
}
const getUploadIdAndSignedUrl = async (
email: string,
fileName: string
): Promise<UploadResponse> => {
if (process.env.JWT_SECRET === undefined) {
throw new Error('JWT_SECRET is not defined')
}
const auth = await signToken(email, process.env.JWT_SECRET)
const data = {
fileName,
email,
}
if (process.env.REST_BACKEND_ENDPOINT === undefined) {
throw new Error('REST_BACKEND_ENDPOINT is not defined')
}
const response = await axios.post(
`${process.env.REST_BACKEND_ENDPOINT}/svc/pdf-attachments/upload`,
data,
{
headers: {
Authorization: `${auth as string}`,
'Content-Type': 'application/json',
},
}
)
return response.data as UploadResponse
}
const uploadToSignedUrl = (
uploadUrl: string,
data: string
): Promise<AxiosResponse> => {
return axios.put(uploadUrl, data, {
headers: {
'Content-Type': 'application/pdf',
},
maxBodyLength: 1000000000,
maxContentLength: 100000000,
})
}
const createArticle = async (
email: string,
uploadFileId: string
): Promise<AxiosResponse> => {
const data = {
email,
uploadFileId,
}
if (process.env.JWT_SECRET === undefined) {
throw new Error('JWT_SECRET is not defined')
}
const auth = await signToken(email, process.env.JWT_SECRET)
if (process.env.REST_BACKEND_ENDPOINT === undefined) {
throw new Error('REST_BACKEND_ENDPOINT is not defined')
}
return axios.post(
`${process.env.REST_BACKEND_ENDPOINT}/svc/pdf-attachments/create-article`,
data,
{
headers: {
Authorization: `${auth as string}`,
'Content-Type': 'application/json',
},
}
)
}