image uploading backend
This commit is contained in:
parent
e2409efbaf
commit
9abc41b7b2
|
@ -6,5 +6,8 @@ import lnurl from './lnurl'
|
|||
import notifications from './notifications'
|
||||
import invite from './invite'
|
||||
import sub from './sub'
|
||||
import upload from './upload'
|
||||
import { GraphQLJSONObject } from 'graphql-type-json'
|
||||
|
||||
export default [user, item, message, wallet, lnurl, notifications, invite, sub]
|
||||
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
|
||||
upload, { JSONObject: GraphQLJSONObject }]
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { AuthenticationError, UserInputError } from 'apollo-server-micro'
|
||||
import AWS from 'aws-sdk'
|
||||
import { IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_TYPES_ALLOW } from '../../lib/constants'
|
||||
|
||||
const bucketRegion = 'us-east-1'
|
||||
|
||||
AWS.config.update({
|
||||
region: bucketRegion
|
||||
})
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
getSignedPOST: async (parent, { type, size, width, height }, { models, me }) => {
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in to get a signed url')
|
||||
}
|
||||
|
||||
if (UPLOAD_TYPES_ALLOW.indexOf(type) === -1) {
|
||||
throw new UserInputError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
|
||||
}
|
||||
|
||||
if (size > UPLOAD_SIZE_MAX) {
|
||||
throw new UserInputError(`image must be less than ${UPLOAD_SIZE_MAX} bytes`)
|
||||
}
|
||||
|
||||
if (width * height > IMAGE_PIXELS_MAX) {
|
||||
throw new UserInputError(`image must be less than ${IMAGE_PIXELS_MAX} pixels`)
|
||||
}
|
||||
|
||||
// create upload record
|
||||
const upload = await models.upload.create({
|
||||
data: {
|
||||
type,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
userId: me.id
|
||||
}
|
||||
})
|
||||
|
||||
// get presigned POST ur
|
||||
const s3 = new AWS.S3({ apiVersion: '2006-03-01' })
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
s3.createPresignedPost({
|
||||
Bucket: process.env.AWS_UPLOAD_BUCKET,
|
||||
Fields: {
|
||||
key: String(upload.id)
|
||||
},
|
||||
Expires: 300,
|
||||
Conditions: [
|
||||
{ 'Content-Type': type },
|
||||
{ acl: 'public-read' },
|
||||
['content-length-range', size, size]
|
||||
]
|
||||
}, (err, preSigned) => { if (err) { reject(err) } else { resolve(preSigned) } })
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import lnurl from './lnurl'
|
|||
import notifications from './notifications'
|
||||
import invite from './invite'
|
||||
import sub from './sub'
|
||||
import upload from './upload'
|
||||
|
||||
const link = gql`
|
||||
type Query {
|
||||
|
@ -23,4 +24,5 @@ const link = gql`
|
|||
}
|
||||
`
|
||||
|
||||
export default [link, user, item, message, wallet, lnurl, notifications, invite, sub]
|
||||
export default [link, user, item, message, wallet, lnurl, notifications, invite,
|
||||
sub, upload]
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { gql } from 'apollo-server-micro'
|
||||
|
||||
export default gql`
|
||||
scalar JSONObject
|
||||
|
||||
extend type Mutation {
|
||||
getSignedPOST(type: String!, size: Int!, width: Int!, height: Int!): SignedPost!
|
||||
}
|
||||
|
||||
type SignedPost {
|
||||
url: String!
|
||||
fields: JSONObject!
|
||||
}
|
||||
`
|
|
@ -45,10 +45,6 @@
|
|||
border: 1px solid var(--theme-body);
|
||||
}
|
||||
|
||||
.navLink:last-child {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.navbarNav {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import { useRef, useState } from 'react'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { UPLOAD_TYPES_ALLOW } from '../lib/constants'
|
||||
import AddImage from '../svgs/image-add-fill.svg'
|
||||
import Moon from '../svgs/moon-fill.svg'
|
||||
import { Image as BImage } from 'react-bootstrap'
|
||||
|
||||
// what we want is that they supply a component that we turn into an upload button
|
||||
// we need to return an error if the upload fails
|
||||
// we need to report that the upload started
|
||||
// we return the image id on success
|
||||
|
||||
export default function Upload ({ as: Component, onStarted, onError, onSuccess }) {
|
||||
const [getSignedPOST] = useMutation(
|
||||
gql`
|
||||
mutation getSignedPOST($type: String!, $size: Int!, $width: Int!, $height: Int!) {
|
||||
getSignedPOST(type: $type, size: $size, width: $width, height: $height) {
|
||||
url
|
||||
fields
|
||||
}
|
||||
}`)
|
||||
const ref = useRef()
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
ref={ref}
|
||||
type='file'
|
||||
className='d-none'
|
||||
onChange={(e) => {
|
||||
if (e.target.files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
onStarted && onStarted()
|
||||
|
||||
const file = e.target.files[0]
|
||||
if (UPLOAD_TYPES_ALLOW.indexOf(file.type) === -1) {
|
||||
onError && onError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
|
||||
return
|
||||
}
|
||||
const img = new Image()
|
||||
img.src = window.URL.createObjectURL(file)
|
||||
img.onload = async () => {
|
||||
let data
|
||||
try {
|
||||
({ data } = await getSignedPOST({
|
||||
variables: {
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
width: img.width,
|
||||
height: img.height
|
||||
}
|
||||
}))
|
||||
} catch (e) {
|
||||
onError && onError(e.toString())
|
||||
return
|
||||
}
|
||||
|
||||
const form = new FormData()
|
||||
Object.keys(data.getSignedPOST.fields).forEach(key =>
|
||||
form.append(key, data.getSignedPOST.fields[key]))
|
||||
form.append('Content-Type', file.type)
|
||||
form.append('acl', 'public-read')
|
||||
form.append('file', file)
|
||||
|
||||
const res = await fetch(data.getSignedPOST.url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
onError && onError(res.statusText)
|
||||
return
|
||||
}
|
||||
|
||||
onSuccess && onSuccess(data.getSignedPOST.fields.key)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Component onClick={() => ref.current?.click()} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function UploadExample () {
|
||||
const [error, setError] = useState()
|
||||
const [key, setKey] = useState()
|
||||
const [uploading, setUploading] = useState()
|
||||
|
||||
const Component = uploading
|
||||
? ({ onClick }) => <Moon className='fill-grey spin' onClick={onClick} />
|
||||
: ({ onClick }) => <AddImage className='fill-grey' onClick={onClick} />
|
||||
|
||||
return (
|
||||
<>
|
||||
<Upload
|
||||
onError={e => {
|
||||
setUploading(false)
|
||||
setError(e)
|
||||
}}
|
||||
onSuccess={key => {
|
||||
setUploading(false)
|
||||
setKey(key)
|
||||
}}
|
||||
onStarted={() => {
|
||||
setError(false)
|
||||
setUploading(true)
|
||||
}}
|
||||
as={Component}
|
||||
/>
|
||||
<div>
|
||||
{key && <BImage src={`https://sn-mtest.s3.amazonaws.com/${key}`} width='100%' />}
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,2 +1,11 @@
|
|||
export const NOFOLLOW_LIMIT = 100
|
||||
export const BOOST_MIN = 1000
|
||||
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
|
||||
export const IMAGE_PIXELS_MAX = 35000000
|
||||
export const UPLOAD_TYPES_ALLOW = [
|
||||
'image/gif',
|
||||
'image/heic',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp'
|
||||
]
|
||||
|
|
|
@ -3764,6 +3764,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"graphql-type-json": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz",
|
||||
"integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg=="
|
||||
},
|
||||
"has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"domino": "^2.1.6",
|
||||
"formik": "^2.2.6",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"ln-service": "^52.8.0",
|
||||
"mdast-util-find-and-replace": "^1.1.1",
|
||||
"next": "^11.1.2",
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Upload" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"type" TEXT NOT NULL,
|
||||
"size" INTEGER NOT NULL,
|
||||
"width" INTEGER,
|
||||
"height" INTEGER,
|
||||
"itemId" INTEGER,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Upload.created_at_index" ON "Upload"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Upload.itemId_index" ON "Upload"("itemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Upload.userId_index" ON "Upload"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Upload" ADD FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Upload" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -53,12 +53,31 @@ model User {
|
|||
noteInvites Boolean @default(true)
|
||||
noteJobIndicator Boolean @default(true)
|
||||
|
||||
Earn Earn[]
|
||||
Earn Earn[]
|
||||
Upload Upload[]
|
||||
@@index([createdAt])
|
||||
@@index([inviteId])
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model Upload {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||
type String
|
||||
size Int
|
||||
width Int?
|
||||
height Int?
|
||||
item Item? @relation(fields: [itemId], references: [id])
|
||||
itemId Int?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([itemId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Earn {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
|
@ -153,7 +172,8 @@ model Item {
|
|||
longitude Float?
|
||||
remote Boolean?
|
||||
|
||||
User User[]
|
||||
User User[]
|
||||
Upload Upload[]
|
||||
@@index([createdAt])
|
||||
@@index([userId])
|
||||
@@index([parentId])
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M21 15v3h3v2h-3v3h-2v-3h-3v-2h3v-3h2zm.008-12c.548 0 .992.445.992.993v9.349A5.99 5.99 0 0 0 20 13V5H4l.001 14 9.292-9.293a.999.999 0 0 1 1.32-.084l.093.085 3.546 3.55a6.003 6.003 0 0 0-3.91 7.743L2.992 21A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016zM8 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></svg>
|
After Width: | Height: | Size: 431 B |
Loading…
Reference in New Issue