got something working thats only a little hacky

This commit is contained in:
Kurt Hutten
2021-08-16 18:43:52 +10:00
parent 180cbb9503
commit 9fa22a0469
12 changed files with 257 additions and 202 deletions

View File

@@ -1,72 +0,0 @@
import type { APIGatewayEvent } from 'aws-lambda'
import { logger } from 'src/lib/logger'
import { v2 as cloudinary, UploadApiResponse } from 'cloudinary'
// import { requireOwnership } from 'src/lib/owner'
// import { requireAuth } from 'src/lib/auth'
cloudinary.config({
cloud_name: 'irevdev',
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})
/**
* The handler function is your code that processes http request events.
* You can use return and throw to send a response or error, respectively.
*
* Important: When deployed, a custom serverless function is an open API endpoint and
* is your responsibility to secure appropriately.
*
* @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations}
* in the RedwoodJS documentation for more information.
*
* @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent
* @typedef { import('aws-lambda').Context } Context
* @param { APIGatewayEvent } event - an object which contains information from the invoker.
* @param { Context } context - contains information about the invocation,
* function, and execution environment.
*/
export const handler = async (event: APIGatewayEvent, _context) => {
logger.info('Invoked image-upload function')
// requireAuth()
const {
image64,
upload_preset,
public_id,
invalidate,
projectId,
}: {
image64: string
upload_preset: string
public_id: string
invalidate: boolean
projectId: string
} = JSON.parse(event.body)
// await requireOwnership({projectId})
const uploadResult: UploadApiResponse = await new Promise(
(resolve, reject) => {
cloudinary.uploader.upload(
image64,
{ upload_preset, public_id, invalidate },
(error, result) => {
if (error) {
reject(error)
return
}
resolve(result)
}
)
}
)
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicId: uploadResult.public_id,
}),
}
}

View File

@@ -48,6 +48,11 @@ export const schema = gql`
createProject(input: CreateProjectInput!): Project!
forkProject(input: CreateProjectInput!): Project!
updateProject(id: String!, input: UpdateProjectInput!): Project!
updateProjectImages(
id: String!
mainImage64: String
socialCard64: String
): Project!
deleteProject(id: String!): Project!
}
`

View File

@@ -0,0 +1,45 @@
import {
v2 as cloudinary,
UploadApiResponse,
UpdateApiOptions,
} from 'cloudinary'
cloudinary.config({
cloud_name: 'irevdev',
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})
interface UploadImageArgs {
image64: string
uploadPreset?: string
publicId?: string
invalidate: boolean
}
export const uploadImage = async ({
image64,
uploadPreset = 'CadHub_project_images',
publicId,
invalidate = true,
}: UploadImageArgs): Promise<UploadApiResponse> => {
const options: UpdateApiOptions = { upload_preset: uploadPreset, invalidate }
if (publicId) {
options.public_id = publicId
}
return new Promise((resolve, reject) => {
cloudinary.uploader.upload(image64, options, (error, result) => {
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
export const makeSocialPublicIdServer = (
userName: string,
projectTitle: string
): string => `u-${userName}-slash-p-${projectTitle}`

View File

@@ -1,4 +1,5 @@
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
import type { Project } from '@prisma/client'
import { db } from 'src/lib/db'
export const requireOwnership = async ({
@@ -55,3 +56,39 @@ export const requireOwnership = async ({
}
}
}
export const requireProjectOwnership = async ({
projectId,
}: {
userId?: string
userName?: string
projectId?: string
sub?: string
} = {}): Promise<Project> => {
// IMPORTANT, don't forget to await this function, as it will only block
// unwanted db actions if it has time to look up resources in the db.
if (!context?.currentUser) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (!projectId) {
throw new ForbiddenError("You don't have access to do that.")
}
const netlifyUserId = context?.currentUser?.sub
if (projectId || context.currentUser.roles?.includes('admin')) {
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
throw new ForbiddenError("That's a local admin ONLY.")
}
const project = await db.project.findUnique({
where: { id: projectId },
})
const hasPermission =
(project && project?.userId === netlifyUserId) ||
context.currentUser.roles?.includes('admin')
if (!hasPermission) {
throw new ForbiddenError("You don't own this resource.")
}
return project
}
}

View File

@@ -1,5 +1,6 @@
import type { Prisma } from '@prisma/client'
import type { Prisma, Project as ProjectType } from '@prisma/client'
import type { ResolverArgs } from '@redwoodjs/api'
import { uploadImage, makeSocialPublicIdServer } from 'src/lib/cloudinary'
import { db } from 'src/lib/db'
import {
@@ -10,7 +11,7 @@ import {
destroyImage,
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
export const projects = ({ userName }) => {
if (!userName) {
@@ -116,6 +117,107 @@ export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
return update
}
export const updateProjectImages = async ({
id,
mainImage64,
socialCard64,
}: {
id: string
mainImage64?: string
socialCard64?: string
}): Promise<ProjectType> => {
requireAuth()
const project = await requireProjectOwnership({ projectId: id })
const replaceSocialCard = async () => {
if (!socialCard64) {
return
}
let publicId = ''
let socialCardId = ''
try {
;({ id: socialCardId, url: publicId } = await db.socialCard.findUnique({
where: { projectId: id },
}))
} catch (e) {
const { userName } = await db.user.findUnique({
where: { id: project.userId },
})
publicId = makeSocialPublicIdServer(userName, project.title)
}
const imagePromise = uploadImage({
image64: socialCard64,
uploadPreset: 'CadHub_project_images',
publicId,
invalidate: true,
})
const saveOrUpdateSocialCard = () => {
const data = {
outOfDate: false,
url: publicId,
}
if (socialCardId) {
return db.socialCard.update({
data,
where: { projectId: id },
})
}
return db.socialCard.create({
data: {
...data,
project: {
connect: {
id: id,
},
},
},
})
}
const socialCardUpdatePromise = saveOrUpdateSocialCard()
const [socialCard] = await Promise.all([
socialCardUpdatePromise,
imagePromise,
])
return socialCard
}
const updateMainImage = async (): Promise<ProjectType> => {
if (!mainImage64) {
return project
}
const { public_id: mainImage } = await uploadImage({
image64: mainImage64,
uploadPreset: 'CadHub_project_images',
invalidate: true,
})
const projectPromise = db.project.update({
data: {
mainImage,
},
where: { id },
})
let imageDestroyPromise = new Promise((r) => r(null))
if (project.mainImage) {
console.log(
`image destroyed, publicId: ${project.mainImage}, projectId: ${id}, replacing image is ${mainImage}`
)
// destroy after the db has been updated
imageDestroyPromise = destroyImage({ publicId: project.mainImage })
}
const [updatedProject] = await Promise.all([
projectPromise,
imageDestroyPromise,
])
return updatedProject
}
const [updatedProject] = await Promise.all([
updateMainImage(),
replaceSocialCard(),
])
return updatedProject
}
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
requireAuth()
await requireOwnership({ projectId: id })