got something working thats only a little hacky
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
}
|
||||
`
|
||||
|
||||
45
app/api/src/lib/cloudinary.ts
Normal file
45
app/api/src/lib/cloudinary.ts
Normal 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}`
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user