Enhanced file uploading for production (thanks citrinitas3421!)
This commit is contained in:
@ -20,10 +20,15 @@ NAME=Ahmed Al-Taiar
|
|||||||
GMAIL=example@gmail.com
|
GMAIL=example@gmail.com
|
||||||
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
|
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
|
||||||
|
|
||||||
|
DOMAIN=example.com
|
||||||
|
API_DOMAIN=api.example.com
|
||||||
|
|
||||||
# Must not end with "/"
|
# Must not end with "/"
|
||||||
DOMAIN_PROD=https://example.com
|
ADDRESS_PROD=https://example.com
|
||||||
DOMAIN_DEV=http://localhost:8910
|
ADDRESS_DEV=http://localhost:8910
|
||||||
|
API_ADDRESS_PROD=https://api.example.com
|
||||||
|
API_ADDRESS_DEV=http://localhost:8911
|
||||||
|
|
||||||
|
MAX_HTTP_CONNECTIONS_PER_MINUTE=60
|
||||||
|
|
||||||
DATABASE_URL=postgresql://user:password@localhost:5432/rw_portfolio
|
DATABASE_URL=postgresql://user:password@localhost:5432/rw_portfolio
|
||||||
|
|
||||||
SESSION_SECRET=regenerate_me
|
|
||||||
|
13
.env.example
13
.env.example
@ -8,10 +8,15 @@ NAME=Firstname Lastname
|
|||||||
GMAIL=example@gmail.com
|
GMAIL=example@gmail.com
|
||||||
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
|
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
|
||||||
|
|
||||||
|
DOMAIN=example.com
|
||||||
|
API_DOMAIN=api.example.com
|
||||||
|
|
||||||
# Must not end with "/"
|
# Must not end with "/"
|
||||||
DOMAIN_PROD=https://example.com
|
ADDRESS_PROD=https://example.com
|
||||||
DOMAIN_DEV=http://localhost:8910
|
ADDRESS_DEV=http://localhost:8910
|
||||||
|
API_ADDRESS_PROD=https://api.example.com
|
||||||
|
API_ADDRESS_DEV=http://localhost:8911
|
||||||
|
|
||||||
|
MAX_HTTP_CONNECTIONS_PER_MINUTE=60
|
||||||
|
|
||||||
DATABASE_URL=postgresql://user:password@localhost:5432/rw_portfolio
|
DATABASE_URL=postgresql://user:password@localhost:5432/rw_portfolio
|
||||||
|
|
||||||
SESSION_SECRET=regenerate_me
|
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,4 +22,4 @@ api/src/lib/generateGraphiQLHeader.*
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
files
|
files_*
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cors": "^9.0.1",
|
||||||
|
"@fastify/rate-limit": "^9.1.0",
|
||||||
"@redwoodjs/api": "7.7.4",
|
"@redwoodjs/api": "7.7.4",
|
||||||
"@redwoodjs/api-server": "7.7.4",
|
"@redwoodjs/api-server": "7.7.4",
|
||||||
"@redwoodjs/auth-dbauth-api": "7.7.4",
|
"@redwoodjs/auth-dbauth-api": "7.7.4",
|
||||||
|
7
api/quick-lint-js.config
Normal file
7
api/quick-lint-js.config
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"context": {
|
||||||
|
"writable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
|
import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
|
||||||
|
|
||||||
|
import { isProduction } from '@redwoodjs/api/dist/logger'
|
||||||
import {
|
import {
|
||||||
DbAuthHandler,
|
DbAuthHandler,
|
||||||
PasswordValidationError,
|
PasswordValidationError,
|
||||||
@ -16,11 +17,9 @@ export const handler = async (
|
|||||||
) => {
|
) => {
|
||||||
const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
|
const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
|
||||||
handler: async (user, resetToken) => {
|
handler: async (user, resetToken) => {
|
||||||
const domain =
|
const domain = isProduction
|
||||||
(process.env.NODE_ENV || 'development') == 'production'
|
? process.env.ADDRESS_PROD
|
||||||
? process.env.DOMAIN_PROD
|
: process.env.ADDRESS_DEV
|
||||||
: process.env.DOMAIN_DEV
|
|
||||||
|
|
||||||
const text = `If this wasn't you, please disregard this email.
|
const text = `If this wasn't you, please disregard this email.
|
||||||
|
|
||||||
Hello,
|
Hello,
|
||||||
@ -76,9 +75,7 @@ ${domain}/reset-password?resetToken=${resetToken}
|
|||||||
// didn't validate their email yet), throw an error and it will be returned
|
// didn't validate their email yet), throw an error and it will be returned
|
||||||
// by the `logIn()` function from `useAuth()` in the form of:
|
// by the `logIn()` function from `useAuth()` in the form of:
|
||||||
// `{ message: 'Error message' }`
|
// `{ message: 'Error message' }`
|
||||||
handler: (user) => {
|
handler: (user) => user,
|
||||||
return user
|
|
||||||
},
|
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
usernameOrPasswordMissing: 'Both username and password are required',
|
usernameOrPasswordMissing: 'Both username and password are required',
|
||||||
@ -199,6 +196,14 @@ ${domain}/reset-password?resetToken=${resetToken}
|
|||||||
resetTokenExpiresAt: 'resetTokenExpiresAt',
|
resetTokenExpiresAt: 'resetTokenExpiresAt',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cors: {
|
||||||
|
origin: isProduction
|
||||||
|
? [process.env.ADDRESS_PROD, process.env.API_ADDRESS_PROD]
|
||||||
|
: [process.env.ADDRESS_DEV, process.env.API_ADDRESS_DEV],
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],
|
||||||
|
},
|
||||||
|
|
||||||
// A list of fields on your user object that are safe to return to the
|
// A list of fields on your user object that are safe to return to the
|
||||||
// client when invoking a handler that returns a user (like forgotPassword
|
// client when invoking a handler that returns a user (like forgotPassword
|
||||||
// and signup). This list should be as small as possible to be sure not to
|
// and signup). This list should be as small as possible to be sure not to
|
||||||
@ -211,12 +216,10 @@ ${domain}/reset-password?resetToken=${resetToken}
|
|||||||
attributes: {
|
attributes: {
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Path: '/',
|
Path: '/',
|
||||||
SameSite: 'Strict',
|
SameSite: isProduction ? 'None' : 'Strict',
|
||||||
Secure: process.env.NODE_ENV !== 'development',
|
Secure: isProduction,
|
||||||
|
Domain: isProduction ? 'localhost' : 'localhost',
|
||||||
// If you need to allow other domains (besides the api side) access to
|
// Domain: isProduction ? process.env.DOMAIN : 'localhost',
|
||||||
// the dbAuth session cookie:
|
|
||||||
// Domain: 'example.com',
|
|
||||||
},
|
},
|
||||||
name: cookieName,
|
name: cookieName,
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import type { Decoded } from '@redwoodjs/api'
|
import type { Decoded } from '@redwoodjs/api'
|
||||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
|
import { decryptSession, getSession } from '@redwoodjs/auth-dbauth-api'
|
||||||
|
import {
|
||||||
|
AuthenticationError,
|
||||||
|
ForbiddenError,
|
||||||
|
ValidationError,
|
||||||
|
} from '@redwoodjs/graphql-server'
|
||||||
|
|
||||||
import { db } from './db'
|
import { db } from './db'
|
||||||
|
|
||||||
@ -12,6 +17,10 @@ import { db } from './db'
|
|||||||
*/
|
*/
|
||||||
export const cookieName = 'session_%port%'
|
export const cookieName = 'session_%port%'
|
||||||
|
|
||||||
|
const cookieRegex = /([a-zA-Z0-9+/|=]{110})\w+/
|
||||||
|
const tokenRegex =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The session object sent in as the first argument to getCurrentUser() will
|
* The session object sent in as the first argument to getCurrentUser() will
|
||||||
* have a single key `id` containing the unique ID of the logged in user
|
* have a single key `id` containing the unique ID of the logged in user
|
||||||
@ -30,9 +39,8 @@ export const cookieName = 'session_%port%'
|
|||||||
* seen if someone were to open the Web Inspector in their browser.
|
* seen if someone were to open the Web Inspector in their browser.
|
||||||
*/
|
*/
|
||||||
export const getCurrentUser = async (session: Decoded) => {
|
export const getCurrentUser = async (session: Decoded) => {
|
||||||
if (!session || typeof session.id !== 'number') {
|
if (!session || typeof session.id !== 'number')
|
||||||
throw new Error('Invalid session')
|
throw new Error('Invalid session')
|
||||||
}
|
|
||||||
|
|
||||||
return await db.user.findUnique({
|
return await db.user.findUnique({
|
||||||
where: { id: session.id },
|
where: { id: session.id },
|
||||||
@ -107,11 +115,29 @@ export const hasRole = (roles: AllowedRoles): boolean => {
|
|||||||
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
|
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
|
||||||
*/
|
*/
|
||||||
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
|
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
|
||||||
if (!isAuthenticated()) {
|
if (!isAuthenticated())
|
||||||
throw new AuthenticationError("You don't have permission to do that.")
|
throw new AuthenticationError("You don't have permission to do that.")
|
||||||
}
|
|
||||||
|
|
||||||
if (roles && !hasRole(roles)) {
|
if (roles && !hasRole(roles))
|
||||||
throw new ForbiddenError("You don't have access to do that.")
|
throw new ForbiddenError("You don't have access to do that.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validateSessionCookie = (sessionCookie: string) => {
|
||||||
|
const sessionCookieContent = sessionCookie.substring(
|
||||||
|
sessionCookie.indexOf('=') + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!cookieRegex.test(sessionCookieContent))
|
||||||
|
throw new ValidationError('Invalid token format')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decryptAndValidateSession = (sessionCookie: string) => {
|
||||||
|
const cookie = cookieName.replace('%port%', '8911')
|
||||||
|
const [session, csrfToken] = decryptSession(getSession(sessionCookie, cookie))
|
||||||
|
if (!session.id) throw new ValidationError('Invalid session')
|
||||||
|
|
||||||
|
if (!tokenRegex.test(csrfToken))
|
||||||
|
throw new ValidationError('Invalid session format')
|
||||||
|
|
||||||
|
return session.id
|
||||||
}
|
}
|
||||||
|
19
api/src/lib/cors.ts
Normal file
19
api/src/lib/cors.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { FastifyReply } from 'fastify'
|
||||||
|
|
||||||
|
import { isProduction } from '@redwoodjs/api/dist/logger'
|
||||||
|
|
||||||
|
export const setCorsHeaders = (res: FastifyReply) => {
|
||||||
|
res.raw.setHeader(
|
||||||
|
'Access-Control-Allow-Origin',
|
||||||
|
isProduction ? process.env.ADDRESS_PROD : process.env.ADDRESS_DEV
|
||||||
|
)
|
||||||
|
res.raw.setHeader(
|
||||||
|
'Access-Control-Allow-Methods',
|
||||||
|
'GET, POST, OPTIONS, PATCH, HEAD'
|
||||||
|
)
|
||||||
|
res.raw.setHeader(
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset'
|
||||||
|
)
|
||||||
|
res.raw.setHeader('Access-Control-Allow-Credentials', 'true')
|
||||||
|
}
|
@ -15,7 +15,7 @@ const transporter = nodemailer.createTransport({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function sendEmail({ to, subject, text, html }: Options) {
|
export const sendEmail = async ({ to, subject, text, html }: Options) => {
|
||||||
return await transporter.sendMail({
|
return await transporter.sendMail({
|
||||||
from: `"${process.env.NAME} (noreply)" <${process.env.GMAIL}>`,
|
from: `"${process.env.NAME} (noreply)" <${process.env.GMAIL}>`,
|
||||||
to: Array.isArray(to) ? to : [to],
|
to: Array.isArray(to) ? to : [to],
|
||||||
@ -25,7 +25,7 @@ export async function sendEmail({ to, subject, text, html }: Options) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function censorEmail(email: string): string {
|
export const censorEmail = (email: string): string => {
|
||||||
const [localPart, domain] = email.split('@')
|
const [localPart, domain] = email.split('@')
|
||||||
|
|
||||||
if (localPart.length <= 2) return `${localPart}@${domain}`
|
if (localPart.length <= 2) return `${localPart}@${domain}`
|
||||||
|
106
api/src/lib/tus.ts
Normal file
106
api/src/lib/tus.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import type { Server } from '@tus/server'
|
||||||
|
import type { FastifyReply, FastifyRequest } from 'fastify'
|
||||||
|
|
||||||
|
import { isProduction } from '@redwoodjs/api/dist/logger'
|
||||||
|
import { ValidationError } from '@redwoodjs/graphql-server'
|
||||||
|
|
||||||
|
import { decryptAndValidateSession, validateSessionCookie } from 'src/lib/auth'
|
||||||
|
import { setCorsHeaders } from 'src/lib/cors'
|
||||||
|
import { db } from 'src/lib/db'
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
hashedPassword: string
|
||||||
|
salt: string
|
||||||
|
resetToken: string | null
|
||||||
|
resetTokenExpiresAt: Date | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleTusUpload = (
|
||||||
|
req: FastifyRequest,
|
||||||
|
res: FastifyReply,
|
||||||
|
tusHandler: Server,
|
||||||
|
isPublicEndpoint: boolean
|
||||||
|
) => {
|
||||||
|
if (isProduction) {
|
||||||
|
if (req.method === 'OPTIONS') handleOptionsRequest(res)
|
||||||
|
else if (isPublicEndpoint && req.method === 'GET')
|
||||||
|
tusHandler.handle(req.raw, res.raw)
|
||||||
|
else if (['GET', 'POST', 'HEAD', 'PATCH'].includes(req.method)) {
|
||||||
|
if (req.headers.cookie) handleAuthenticatedRequest(req, res, tusHandler)
|
||||||
|
else {
|
||||||
|
res.raw.statusCode = 401
|
||||||
|
res.raw.end('Unauthenticated')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.raw.statusCode = 405
|
||||||
|
res.raw.end('Method not allowed')
|
||||||
|
}
|
||||||
|
} else tusHandler.handle(req.raw, res.raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAuthenticatedRequest = async (
|
||||||
|
req: FastifyRequest,
|
||||||
|
res: FastifyReply,
|
||||||
|
tusHandler: Server
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const sessionCookie = extractSessionCookie(req.headers.cookie)
|
||||||
|
validateSessionCookie(sessionCookie)
|
||||||
|
const userId = decryptAndValidateSession(sessionCookie)
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
res.raw.setHeader('Access-Control-Allow-Credentials', 'true')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await db.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
addUserMetadataToRequest(req, user)
|
||||||
|
|
||||||
|
if ((req.raw as any).userId) tusHandler.handle(req.raw, res.raw)
|
||||||
|
else {
|
||||||
|
res.raw.statusCode = 500
|
||||||
|
res.raw.end('Server error')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.raw.statusCode = 500
|
||||||
|
res.raw.end('Server error')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.raw.statusCode = 403
|
||||||
|
res.raw.end('Forbidden')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.raw.statusCode = 401
|
||||||
|
res.raw.end('Unauthenticated')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addUserMetadataToRequest = (req: FastifyRequest, user: User) => {
|
||||||
|
;(req.raw as any).userId = user.id
|
||||||
|
;(req.raw as any).userEmail = user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOptionsRequest = (res: FastifyReply) => {
|
||||||
|
setCorsHeaders(res)
|
||||||
|
res.raw.statusCode = 204
|
||||||
|
res.raw.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractSessionCookie = (cookie: string) => {
|
||||||
|
const sessionCookie = cookie
|
||||||
|
.split(';')
|
||||||
|
.find((item) => item.trim().startsWith('session_8911'))
|
||||||
|
?.trim()
|
||||||
|
|
||||||
|
if (!sessionCookie) throw new ValidationError('Invalid token')
|
||||||
|
|
||||||
|
return sessionCookie
|
||||||
|
}
|
@ -1,17 +1,40 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import Cors from '@fastify/cors'
|
||||||
|
import RateLimit from '@fastify/rate-limit'
|
||||||
import { FileStore } from '@tus/file-store'
|
import { FileStore } from '@tus/file-store'
|
||||||
import { Server } from '@tus/server'
|
import { Server } from '@tus/server'
|
||||||
|
|
||||||
|
import { isProduction } from '@redwoodjs/api/dist/logger'
|
||||||
import { createServer } from '@redwoodjs/api-server'
|
import { createServer } from '@redwoodjs/api-server'
|
||||||
|
|
||||||
import { logger } from 'src/lib/logger'
|
import { logger } from 'src/lib/logger'
|
||||||
|
import { handleTusUpload } from 'src/lib/tus'
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const server = await createServer({
|
const server = await createServer({
|
||||||
logger,
|
logger,
|
||||||
|
configureApiServer: async (server) => {
|
||||||
|
await server.register(Cors, {
|
||||||
|
origin: isProduction
|
||||||
|
? [process.env.ADDRESS_PROD, process.env.API_ADDRESS_PROD]
|
||||||
|
: [process.env.ADDRESS_DEV, process.env.API_ADDRESS_DEV],
|
||||||
|
methods: ['GET', 'POST', 'OPTIONS', 'PATCH', 'HEAD'],
|
||||||
|
credentials: isProduction ? true : false,
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.register(RateLimit, {
|
||||||
|
max: Number(process.env.MAX_HTTP_CONNECTIONS_PER_MINUTE),
|
||||||
|
timeWindow: `1 minute`,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const tusServer = new Server({
|
const tusServer = new Server({
|
||||||
path: '/files',
|
path: '/files',
|
||||||
datastore: new FileStore({ directory: './files' }),
|
respectForwardedHeaders: true,
|
||||||
|
datastore: new FileStore({
|
||||||
|
directory: `./files_${isProduction ? 'prod' : 'dev'}`,
|
||||||
|
}),
|
||||||
|
onResponseError: (_req, res, _err) => logger.error(res),
|
||||||
})
|
})
|
||||||
|
|
||||||
server.addContentTypeParser(
|
server.addContentTypeParser(
|
||||||
@ -19,15 +42,12 @@ import { logger } from 'src/lib/logger'
|
|||||||
(_request, _payload, done) => done(null)
|
(_request, _payload, done) => done(null)
|
||||||
)
|
)
|
||||||
|
|
||||||
server.all('/files', (req, res) => {
|
server.all('/files', (req, res) =>
|
||||||
tusServer.handle(req.raw, res.raw)
|
handleTusUpload(req, res, tusServer, false)
|
||||||
return res
|
)
|
||||||
})
|
server.all('/files/*', (req, res) =>
|
||||||
|
handleTusUpload(req, res, tusServer, true)
|
||||||
server.all('/files/*', (req, res) => {
|
)
|
||||||
tusServer.handle(req.raw, res.raw)
|
|
||||||
return res
|
|
||||||
})
|
|
||||||
|
|
||||||
await server.start()
|
await server.start()
|
||||||
})()
|
})()
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
title = "${NAME}"
|
title = "${NAME}"
|
||||||
port = 8910
|
port = 8910
|
||||||
apiUrl = "/api"
|
apiUrl = "/api"
|
||||||
includeEnvironmentVariables = ["NAME"]
|
includeEnvironmentVariables = ["NAME", "API_ADDRESS_PROD", "API_ADDRESS_DEV"]
|
||||||
[generate]
|
[generate]
|
||||||
tests = false
|
tests = false
|
||||||
stories = false
|
stories = false
|
||||||
|
@ -2,26 +2,62 @@ import { useState } from 'react'
|
|||||||
|
|
||||||
import Compressor from '@uppy/compressor'
|
import Compressor from '@uppy/compressor'
|
||||||
import Uppy from '@uppy/core'
|
import Uppy from '@uppy/core'
|
||||||
|
import type { UploadResult, Meta } from '@uppy/core'
|
||||||
import ImageEditor from '@uppy/image-editor'
|
import ImageEditor from '@uppy/image-editor'
|
||||||
import { Dashboard } from '@uppy/react'
|
import { Dashboard } from '@uppy/react'
|
||||||
import Tus from '@uppy/tus'
|
import Tus from '@uppy/tus'
|
||||||
|
|
||||||
|
import { isProduction } from '@redwoodjs/api/dist/logger'
|
||||||
|
|
||||||
import '@uppy/image-editor/dist/style.min.css'
|
import '@uppy/image-editor/dist/style.min.css'
|
||||||
import '@uppy/core/dist/style.min.css'
|
import '@uppy/core/dist/style.min.css'
|
||||||
import '@uppy/dashboard/dist/style.min.css'
|
import '@uppy/dashboard/dist/style.min.css'
|
||||||
|
|
||||||
const Uploader = () => {
|
interface Props {
|
||||||
const [uppy] = useState(() =>
|
onComplete?(result: UploadResult<Meta, Record<string, never>>): void
|
||||||
new Uppy({
|
}
|
||||||
|
|
||||||
|
const apiDomain = isProduction
|
||||||
|
? process.env.API_ADDRESS_PROD
|
||||||
|
: process.env.API_ADDRESS_DEV
|
||||||
|
|
||||||
|
const Uploader = ({ onComplete }: Props) => {
|
||||||
|
const [uppy] = useState(() => {
|
||||||
|
const instance = new Uppy({
|
||||||
restrictions: {
|
restrictions: {
|
||||||
allowedFileTypes: ['image/*'],
|
allowedFileTypes: [
|
||||||
|
'image/webp',
|
||||||
|
'image/png',
|
||||||
|
'image/jpg',
|
||||||
|
'image/jpeg',
|
||||||
|
],
|
||||||
maxNumberOfFiles: 10,
|
maxNumberOfFiles: 10,
|
||||||
|
maxFileSize: 25 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
onBeforeUpload: (files) => {
|
||||||
|
for (const [key, file] of Object.entries(files)) {
|
||||||
|
instance.setFileMeta(key, {
|
||||||
|
name: new Date().getTime().toString(),
|
||||||
|
type: file.type,
|
||||||
|
contentType: file.type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.use(Tus, { endpoint: 'http://localhost:8911/files' }) // TODO: check if env is production and change endpoint accordingly
|
.use(Tus, {
|
||||||
|
endpoint: `${apiDomain}/files`,
|
||||||
|
withCredentials: true,
|
||||||
|
removeFingerprintOnSuccess: true,
|
||||||
|
})
|
||||||
.use(ImageEditor)
|
.use(ImageEditor)
|
||||||
.use(Compressor)
|
.use(Compressor, {
|
||||||
)
|
mimeType: 'image/webp',
|
||||||
|
})
|
||||||
|
|
||||||
|
return instance.on('complete', onComplete)
|
||||||
|
})
|
||||||
|
|
||||||
return <Dashboard uppy={uppy} proudlyDisplayPoweredByUppy={false} />
|
return <Dashboard uppy={uppy} proudlyDisplayPoweredByUppy={false} />
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import Uploader from 'src/components/Uploader/Uploader'
|
|||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const { isAuthenticated } = useAuth()
|
const { isAuthenticated } = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metadata title="Home" description="Home page" />
|
<Metadata title="Home" description="Home page" />
|
||||||
|
41
yarn.lock
41
yarn.lock
@ -2419,6 +2419,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@fastify/cors@npm:^9.0.1":
|
||||||
|
version: 9.0.1
|
||||||
|
resolution: "@fastify/cors@npm:9.0.1"
|
||||||
|
dependencies:
|
||||||
|
fastify-plugin: "npm:^4.0.0"
|
||||||
|
mnemonist: "npm:0.39.6"
|
||||||
|
checksum: 10c0/4db9d3d02edbca741c8ed053819bf3b235ecd70e07c640ed91ba0fc1ee2dc8abedbbffeb79ae1a38ccbf59832e414cad90a554ee44227d0811d5a2d062940611
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@fastify/error@npm:^3.0.0, @fastify/error@npm:^3.3.0, @fastify/error@npm:^3.4.0":
|
"@fastify/error@npm:^3.0.0, @fastify/error@npm:^3.3.0, @fastify/error@npm:^3.4.0":
|
||||||
version: 3.4.1
|
version: 3.4.1
|
||||||
resolution: "@fastify/error@npm:3.4.1"
|
resolution: "@fastify/error@npm:3.4.1"
|
||||||
@ -2456,6 +2466,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@fastify/rate-limit@npm:^9.1.0":
|
||||||
|
version: 9.1.0
|
||||||
|
resolution: "@fastify/rate-limit@npm:9.1.0"
|
||||||
|
dependencies:
|
||||||
|
"@lukeed/ms": "npm:^2.0.1"
|
||||||
|
fastify-plugin: "npm:^4.0.0"
|
||||||
|
toad-cache: "npm:^3.3.1"
|
||||||
|
checksum: 10c0/55914e4af5d93fff29e94ad4e3f369a371a3e7b97e501e796167dd49115c5d8dfa94aec8ec78ffcdb058268465b072a0892880523774b7b90a27a8ff304fea82
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@fastify/reply-from@npm:^9.0.0":
|
"@fastify/reply-from@npm:^9.0.0":
|
||||||
version: 9.8.0
|
version: 9.8.0
|
||||||
resolution: "@fastify/reply-from@npm:9.8.0"
|
resolution: "@fastify/reply-from@npm:9.8.0"
|
||||||
@ -7254,6 +7275,8 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "api@workspace:api"
|
resolution: "api@workspace:api"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@fastify/cors": "npm:^9.0.1"
|
||||||
|
"@fastify/rate-limit": "npm:^9.1.0"
|
||||||
"@redwoodjs/api": "npm:7.7.4"
|
"@redwoodjs/api": "npm:7.7.4"
|
||||||
"@redwoodjs/api-server": "npm:7.7.4"
|
"@redwoodjs/api-server": "npm:7.7.4"
|
||||||
"@redwoodjs/auth-dbauth-api": "npm:7.7.4"
|
"@redwoodjs/auth-dbauth-api": "npm:7.7.4"
|
||||||
@ -16052,6 +16075,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"mnemonist@npm:0.39.6":
|
||||||
|
version: 0.39.6
|
||||||
|
resolution: "mnemonist@npm:0.39.6"
|
||||||
|
dependencies:
|
||||||
|
obliterator: "npm:^2.0.1"
|
||||||
|
checksum: 10c0/a538945ea547976136ee6e16f224c0a50983143619941f6c4d2c82159e36eb6f8ee93d69d3a1267038fc5b16f88e2d43390023de10dfb145fa15c5e2befa1cdf
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"module-not-found-error@npm:^1.0.1":
|
"module-not-found-error@npm:^1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "module-not-found-error@npm:1.0.1"
|
resolution: "module-not-found-error@npm:1.0.1"
|
||||||
@ -16620,6 +16652,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"obliterator@npm:^2.0.1":
|
||||||
|
version: 2.0.4
|
||||||
|
resolution: "obliterator@npm:2.0.4"
|
||||||
|
checksum: 10c0/ff2c10d4de7d62cd1d588b4d18dfc42f246c9e3a259f60d5716f7f88e5b3a3f79856b3207db96ec9a836a01d0958a21c15afa62a3f4e73a1e0b75f2c2f6bab40
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"obuf@npm:^1.0.0, obuf@npm:^1.1.2":
|
"obuf@npm:^1.0.0, obuf@npm:^1.1.2":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "obuf@npm:1.1.2"
|
resolution: "obuf@npm:1.1.2"
|
||||||
@ -20632,7 +20671,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"toad-cache@npm:^3.3.0, toad-cache@npm:^3.7.0":
|
"toad-cache@npm:^3.3.0, toad-cache@npm:^3.3.1, toad-cache@npm:^3.7.0":
|
||||||
version: 3.7.0
|
version: 3.7.0
|
||||||
resolution: "toad-cache@npm:3.7.0"
|
resolution: "toad-cache@npm:3.7.0"
|
||||||
checksum: 10c0/7dae2782ee20b22c9798bb8b71dec7ec6a0091021d2ea9dd6e8afccab6b65b358fdba49a02209fac574499702e2c000660721516c87c2538d1b2c0ba03e8c0c3
|
checksum: 10c0/7dae2782ee20b22c9798bb8b71dec7ec6a0091021d2ea9dd6e8afccab6b65b358fdba49a02209fac574499702e2c000660721516c87c2538d1b2c0ba03e8c0c3
|
||||||
|
Reference in New Issue
Block a user