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 notifications from './notifications'
|
||||||
import invite from './invite'
|
import invite from './invite'
|
||||||
import sub from './sub'
|
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 notifications from './notifications'
|
||||||
import invite from './invite'
|
import invite from './invite'
|
||||||
import sub from './sub'
|
import sub from './sub'
|
||||||
|
import upload from './upload'
|
||||||
|
|
||||||
const link = gql`
|
const link = gql`
|
||||||
type Query {
|
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);
|
border: 1px solid var(--theme-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navLink:last-child {
|
|
||||||
padding-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbarNav {
|
.navbarNav {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-wrap: wrap;
|
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 NOFOLLOW_LIMIT = 100
|
||||||
export const BOOST_MIN = 1000
|
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": {
|
"has": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"formik": "^2.2.6",
|
"formik": "^2.2.6",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.0",
|
||||||
|
"graphql-type-json": "^0.3.2",
|
||||||
"ln-service": "^52.8.0",
|
"ln-service": "^52.8.0",
|
||||||
"mdast-util-find-and-replace": "^1.1.1",
|
"mdast-util-find-and-replace": "^1.1.1",
|
||||||
"next": "^11.1.2",
|
"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;
|
|
@ -54,11 +54,30 @@ model User {
|
||||||
noteJobIndicator Boolean @default(true)
|
noteJobIndicator Boolean @default(true)
|
||||||
|
|
||||||
Earn Earn[]
|
Earn Earn[]
|
||||||
|
Upload Upload[]
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([inviteId])
|
@@index([inviteId])
|
||||||
@@map(name: "users")
|
@@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 {
|
model Earn {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
|
@ -154,6 +173,7 @@ model Item {
|
||||||
remote Boolean?
|
remote Boolean?
|
||||||
|
|
||||||
User User[]
|
User User[]
|
||||||
|
Upload Upload[]
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([parentId])
|
@@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