Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(image): simplify image uploading #4189

Draft
wants to merge 3 commits into
base: staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@serlo/katex-styles": "1.0.1"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.670.0",
"@aws-sdk/s3-request-presigner": "^3.670.0",
"@cortex-js/compute-engine": "^0.22.0",
"@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-brands-svg-icons": "6.5.2",
Expand Down
77 changes: 77 additions & 0 deletions apps/web/src/pages/api/media/presigned-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
PutObjectCommand,
PutObjectCommandInput,
S3Client,
} from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { NextApiRequest, NextApiResponse } from 'next/types'
import { v1 as uuidv1 } from 'uuid'

const supportedMimeTypes = [
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
] as const
type SupportedMimeType = (typeof supportedMimeTypes)[number]

// minIO test credentials as fallback
const bucket = process.env.BUCKET_NAME ?? 'serlo-test-bucket'

const s3Client = new S3Client({
region: process.env.BUCKET_REGION ?? 'us-east-1',
credentials: {
accessKeyId: process.env.BUCKET_ACCESS_KEY_ID ?? 'Q3AM3UQ867SPQQA43P2F',
secretAccessKey:
process.env.BUCKET_SECRET_ACCESS_KEY ??
'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
},
endpoint: process.env.BUCKET_ENDPOINT ?? 'https://play.min.io:9000',
forcePathStyle: true, // test, maybe only set on dev
})

const srcPrefix =
process.env.BUCKET_PUBLIC_SRC_PREFIX ?? `https://play.min.io:9000/${bucket}/`

export const getPresignedUrl = async (mimeType: SupportedMimeType) => {
const fileHash = uuidv1()
const fileExtension =
mimeType === 'image/svg+xml' ? 'svg' : mimeType.replace('image/', '')
const fileName = `${fileHash}.${fileExtension}`

const params: PutObjectCommandInput = {
Key: `${fileHash}.${fileExtension}`,
Bucket: bucket,
ContentType: mimeType,
Metadata: { 'Content-Type': mimeType },
}

const command = new PutObjectCommand(params)
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 })
const imgSrc = `${srcPrefix}${fileName}`
return { signedUrl, imgSrc }
}

export function isValidMimeType(
mimeType: string
): mimeType is SupportedMimeType {
if (typeof mimeType !== 'string') return false
return supportedMimeTypes.includes(mimeType as SupportedMimeType)
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const mimeType = decodeURIComponent(String(req.query.mimeType))

if (!isValidMimeType(mimeType)) {
return res.send(
'Missing or invalid query parameter mimeType. Please provide one of: gif, jpeg, png, svg+xml, webp'
)
}

const response = await getPresignedUrl(mimeType)
res.send(response)
}
4 changes: 2 additions & 2 deletions apps/web/src/serlo-editor-integration/create-plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { exercisePlugin } from '@editor/plugins/exercise'
import { exerciseGroupPlugin } from '@editor/plugins/exercise-group'
import { geoGebraPlugin } from '@editor/plugins/geogebra'
import { createHighlightPlugin } from '@editor/plugins/highlight'
import { createImagePlugin } from '@editor/plugins/image'
import { createImageGalleryPlugin } from '@editor/plugins/image-gallery'
import { injectionPlugin } from '@editor/plugins/injection'
import { createInputExercisePlugin } from '@editor/plugins/input-exercise'
Expand Down Expand Up @@ -44,7 +45,6 @@ import { TemplatePluginType } from '@editor/types/template-plugin-type'
import { Instance } from '@/fetcher/graphql-types/operations'
import { isProduction } from '@/helper/is-production'
import { H5pPlugin } from '@/serlo-editor-integration/h5p'
import { imagePlugin } from '@/serlo-editor-integration/image-with-serlo-config'

export function createPlugins({ lang }: { lang: Instance }): PluginsWithData {
const plugins = [
Expand Down Expand Up @@ -98,7 +98,7 @@ export function createPlugins({ lang }: { lang: Instance }): PluginsWithData {

const allPlugins = [
{ type: EditorPluginType.Text, plugin: createTextPlugin({}) },
{ type: EditorPluginType.Image, plugin: imagePlugin },
{ type: EditorPluginType.Image, plugin: createImagePlugin({}) },
{ type: EditorPluginType.ImageGallery, plugin: createImageGalleryPlugin() },
{
type: EditorPluginType.Multimedia,
Expand Down
169 changes: 0 additions & 169 deletions apps/web/src/serlo-editor-integration/image-with-serlo-config.ts

This file was deleted.

12 changes: 0 additions & 12 deletions packages/editor-web-component/src/editor-web-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ export class EditorWebComponent extends HTMLElement {
private _initialState: InitialState = exampleInitialState
private _currentState: unknown

private _testingSecret: string | null = null

private _editorVariant: EditorVariant = 'unknown'

// By default, we are NOT attaching it to the shadow DOM
Expand Down Expand Up @@ -113,14 +111,6 @@ export class EditorWebComponent extends HTMLElement {
return this._history
}

get testingSecret(): string | null {
return this._testingSecret
}

set testingSecret(newTestingSecret) {
if (newTestingSecret) this.setAttribute('testing-secret', newTestingSecret)
}

get editorVariant(): EditorVariant {
return this._editorVariant
}
Expand Down Expand Up @@ -167,7 +157,6 @@ export class EditorWebComponent extends HTMLElement {

mountReactComponent() {
const initialStateAttr = this.getAttribute('initial-state')
const testingSecretAttr = this.getAttribute('testing-secret')

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const initialState: InitialState = initialStateAttr
Expand All @@ -193,7 +182,6 @@ export class EditorWebComponent extends HTMLElement {
<LazySerloEditor
editorVariant={this.editorVariant}
initialState={this.initialState}
_testingSecret={testingSecretAttr}
onChange={(newState) => {
this._currentState = newState
this.broadcastNewState(newState)
Expand Down
34 changes: 10 additions & 24 deletions packages/editor/src/editor-integration/create-basic-plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { equationsPlugin } from '@editor/plugins/equations'
import { exercisePlugin } from '@editor/plugins/exercise'
import { geoGebraPlugin } from '@editor/plugins/geogebra'
import { createHighlightPlugin } from '@editor/plugins/highlight'
import { createImagePlugin } from '@editor/plugins/image'
import { createImageGalleryPlugin } from '@editor/plugins/image-gallery'
import { createInputExercisePlugin } from '@editor/plugins/input-exercise'
import { createMultimediaPlugin } from '@editor/plugins/multimedia'
Expand All @@ -23,37 +24,22 @@ import { unsupportedPlugin } from '@editor/plugins/unsupported'
import { EditorPluginType } from '@editor/types/editor-plugin-type'
import { TemplatePluginType } from '@editor/types/template-plugin-type'

import { createTestingImagePlugin } from './image-with-testing-config'

export function createBasicPlugins(
plugins: (EditorPluginType | TemplatePluginType)[],
testingSecret?: string | null
plugins: (EditorPluginType | TemplatePluginType)[]
) {
if (plugins.includes(EditorPluginType.Image) && !testingSecret) {
/* eslint-disable no-console */
console.log(
'The image plugin needs the `testingSecret` but it is missing. Image plugin was disabled. Either provide it or deactivate the image plugin in the editor API.'
)
plugins = plugins.filter((plugin) => plugin !== EditorPluginType.Image)
}

const allPlugins = [
{
type: EditorPluginType.Text,
plugin: createTextPlugin({}),
},
...(testingSecret
? [
{
type: EditorPluginType.Image,
plugin: createTestingImagePlugin(testingSecret),
},
{
type: EditorPluginType.ImageGallery,
plugin: createImageGalleryPlugin(),
},
]
: []),
{
type: EditorPluginType.Image,
plugin: createImagePlugin({}),
},
{
type: EditorPluginType.ImageGallery,
plugin: createImageGalleryPlugin(),
},
{
type: EditorPluginType.Multimedia,
plugin: createMultimediaPlugin(plugins),
Expand Down
Loading