From 6c436e45931fd0a1456a0801e4b1fc813d94c9d7 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Sun, 12 May 2024 21:10:32 +0200 Subject: [PATCH 01/29] Init websocket --- .env | 3 +- .prettierrc.js | 1 - Dockerfile | 4 +- api/index.ts | 161 +++++++++++++----- api/package.json | 8 +- api/pnpm-lock.yaml | 110 ++++++++++++ api/routes/_express-call.ts | 17 -- api/routes/fs/text.ts | 119 ------------- api/{routes => services}/_auth.ts | 22 --- api/{routes => services}/_upload_utils.ts | 0 api/{routes => services}/fs/base64.ts | 2 +- .../browse/:imageType/:country/:magazine.ts | 2 +- api/{routes => services}/fs/browseEdges.ts | 2 +- .../fs/generateDefaultEdge.ts | 2 +- api/{routes => services}/fs/save.ts | 2 +- api/{routes => services}/fs/upload-base64.ts | 4 +- api/{routes => services}/fs/upload.ts | 6 +- api/services/text/index.ts | 123 +++++++++++++ api/services/text/types.ts | 30 ++++ api/tsconfig.json | 9 +- api/tsconfig.prod.json | 48 ------ auto-imports.d.ts | 14 +- components.d.ts | 56 +----- package.json | 3 +- pnpm-lock.yaml | 3 + src/App.vue | 24 ++- src/components/renders/StapleRender.vue | 13 +- src/components/renders/TextRender.vue | 76 ++++----- src/composables/useEdgecreatorSocket.ts | 23 +++ src/main.ts | 17 +- src/stores/api.ts | 1 - src/stores/collection.ts | 2 +- tsconfig.json | 1 + types/ModelContributor.ts | 2 +- types/SimpleUser.ts | 2 +- vite.config.ts | 20 +-- 36 files changed, 522 insertions(+), 410 deletions(-) delete mode 100644 .prettierrc.js delete mode 100644 api/routes/_express-call.ts delete mode 100644 api/routes/fs/text.ts rename api/{routes => services}/_auth.ts (80%) rename api/{routes => services}/_upload_utils.ts (100%) rename api/{routes => services}/fs/base64.ts (94%) rename api/{routes => services}/fs/browse/:imageType/:country/:magazine.ts (92%) rename api/{routes => services}/fs/browseEdges.ts (95%) rename api/{routes => services}/fs/generateDefaultEdge.ts (96%) rename api/{routes => services}/fs/save.ts (98%) rename api/{routes => services}/fs/upload-base64.ts (91%) rename api/{routes => services}/fs/upload.ts (97%) create mode 100644 api/services/text/index.ts create mode 100644 api/services/text/types.ts delete mode 100644 api/tsconfig.prod.json create mode 100644 src/composables/useEdgecreatorSocket.ts diff --git a/.env b/.env index 1077d067..60ca3ed5 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ -VITE_DM_API_URL=http://localhost:3000 +VITE_DM_SOCKET_URL=http://localhost:3000 +VITE_EDGECREATOR_SOCKET_URL=http://localhost:3003 VITE_DM_URL=http://localhost:8001 VITE_EDGECREATOR_API_URL=http://localhost:3001 VITE_EDGES_URL=http://localhost:8001/edges diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index f053ebf7..00000000 --- a/.prettierrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/Dockerfile b/Dockerfile index b1291406..fd558543 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,8 +21,8 @@ COPY types /app/types COPY api . RUN mv tsconfig.prod.json tsconfig.json RUN mkdir dm_types && cp -r node_modules/ducksmanager/types/* dm_types/ -RUN mkdir prisma_clients && cp -r node_modules/ducksmanager/api/dist/prisma/* prisma_clients/ -RUN sed -i 's#../api/dist/prisma#~prisma_clients#g' dm_types/*.ts +RUN mkdir prisma-clients && cp -r node_modules/ducksmanager/api/dist/prisma/* prisma-clients/ +RUN sed -i 's#../api/dist/prisma#~prisma-clients#g' dm_types/*.ts RUN pnpm run generate-route-types RUN pnpm run build diff --git a/api/index.ts b/api/index.ts index 8f829b5a..2915ad77 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,19 +1,14 @@ import * as Sentry from "@sentry/node"; -import bodyParser from "body-parser"; -import cookieParser from "cookie-parser"; -import cors from "cors"; + import dotenv from "dotenv"; -import express from "express"; -import { router } from "express-file-routing"; +import { OptionalAuthMiddleware } from "~api/services/auth/util"; + +import { createServer } from "http"; +import { Server } from "socket.io"; -const parseForm = bodyParser.json(); -import { - authenticateToken, - checkUserIsAdminForExportOrIsEditorForSaveOrIsFirstFileForModel, - checkUserIsAdminOrEditor, - injectTokenIfValid, -} from "~routes/_auth"; +import type { SessionUser } from "~dm-types/SessionUser"; +import text from "./services/text"; dotenv.config({ path: "../.env", }); @@ -24,36 +19,112 @@ Sentry.init({ dsn: process.env.SENTRY_DSN, }); -const app = express(); - -app.use( - Sentry.Handlers.requestHandler({ - user: ["id", "username"], - }) as express.RequestHandler, -); -app.use( - cors({ - optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 - }), -); -app.use(cookieParser()); - -app.all(/^.+$/, injectTokenIfValid); -app.all(/^\/fs\/save$/, [ - authenticateToken, - parseForm, - checkUserIsAdminForExportOrIsEditorForSaveOrIsFirstFileForModel, -]); -app.all(/^\/fs\/(text|upload|upload-base64)$/, [ - authenticateToken, - checkUserIsAdminOrEditor, -]); - -app.use(Sentry.Handlers.errorHandler() as express.ErrorRequestHandler); - -(async () => { - app.use("/", await router({ directory: `${process.cwd()}/routes` })); - app.listen(port, () => - console.log(`EdgeCreator API listening on port ${port}`), - ); -})(); +// const app = express(); + +// app.use( +// Sentry.Handlers.requestHandler({ +// user: ["id", "username"], +// }) as express.RequestHandler +// ); +// app.use( +// cors({ +// optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 +// }) +// ); +// app.use(cookieParser()); + +// app.all(/^.+$/, injectTokenIfValid); +// app.all(/^\/fs\/save$/, [ +// authenticateToken, +// parseForm, +// checkUserIsAdminForExportOrIsEditorForSaveOrIsFirstFileForModel, +// ]); +// app.all(/^\/fs\/(text|upload|upload-base64)$/, [ +// authenticateToken, +// checkUserIsAdminOrEditor, +// ]); + +// app.use(Sentry.Handlers.errorHandler() as express.ErrorRequestHandler); + +// (async () => { +// app.use("/", await router({ directory: `${process.cwd()}/routes` })); +// app.listen(port, () => +// console.log(`EdgeCreator API listening on port ${port}`) +// ); +// })(); + +class ServerWithUser extends Server< + Record, + Record, + Record, + { user?: SessionUser } +> {} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(BigInt.prototype as any).toJSON = function () { + const int = Number.parseInt(this.toString()); + return int ?? this.toString(); +}; + +dotenv.config({ + path: "./.env", +}); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, +}); + +const httpServer = createServer(async (req, res) => { + let data: { error: string } | object; + switch (req.url) { + // case "/status/db": + // data = await getDbStatus(); + // break; + // case "/status/pastecsearch": + // data = await getPastecSearchStatus(); + // break; + // case "/status/pastec": + // data = await getPastecStatus(); + default: + res.writeHead(404); + res.end(); + return; + } + + res.writeHead("error" in data ? 500 : 200, { "Content-Type": "text/json" }); + res.write(JSON.stringify(data)); + res.end(); +}); +const io = new ServerWithUser(httpServer, { + cors: { + origin: "*", + }, +}); + +httpServer.listen(port); +console.log(`WebSocket open on port ${port}`); + +io.use(OptionalAuthMiddleware); +io.use((_socket, next) => { + next(); + + // app.all( + // /^\/(edgecreator\/(publish|edgesprites)|notifications)|(edges\/(published))|(\/demo\/reset)|(bookstores\/(approve|refuse))|(presentation-text\/(approve|refuse))/, + // [checkUserIsAdmin] + // ); + + // app.all(/^\/edgecreator\/(.+)/, [ + // authenticateToken, + // checkUserIsEdgeCreatorEditor, + // ]); + + // app.all(/^\/global-stats\/user\/list$/, [ + // authenticateToken, + // checkUserIsEdgeCreatorEditor, + // ]); + + // app.all(/^\/collection\/(.+)/, authenticateToken); + // app.all("/global-stats/user/collection/rarity", authenticateToken); +}); + +text(io); diff --git a/api/package.json b/api/package.json index 98035a41..439bda1e 100644 --- a/api/package.json +++ b/api/package.json @@ -1,5 +1,5 @@ { - "name": "dm-api", + "name": "~edgecreator-api", "version": "1.0.0", "scripts": { "clean": "rm -rf dist", @@ -28,7 +28,11 @@ "node-base64-image": "^2.0.6", "nodemon": "^2.0.22", "sharp": "^0.33.3", - "universal-cookie": "^4.0.4" + "socket.io": "^4.7.5", + "universal-cookie": "^4.0.4", + "~api": "workspace:*", + "~dm-types": "workspace:*", + "~socket.io-services": "workspace:*" }, "devDependencies": { "@types/body-parser": "^1.19.5", diff --git a/api/pnpm-lock.yaml b/api/pnpm-lock.yaml index 11ec40ca..8e610612 100644 --- a/api/pnpm-lock.yaml +++ b/api/pnpm-lock.yaml @@ -62,9 +62,21 @@ importers: sharp: specifier: ^0.33.3 version: 0.33.3 + socket.io: + specifier: ^4.7.5 + version: 4.7.5 universal-cookie: specifier: ^4.0.4 version: 4.0.4 + ~api: + specifier: workspace:* + version: link:../../../packages/api + ~dm-types: + specifier: workspace:* + version: link:../../../packages/types + ~socket.io-services: + specifier: workspace:* + version: link:../../../packages/socket.io-services devDependencies: '@types/body-parser': specifier: ^1.19.5 @@ -430,6 +442,9 @@ packages: resolution: {integrity: sha512-319N90McVpupQ6vws4+tfCy/03AdtsU0MurIE4+W5cubHME08HtiEWlfacvAxX+yuKFhvdsO4K4BB/dj54ideg==} engines: {node: '>=8'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} @@ -445,6 +460,9 @@ packages: '@types/cookie@0.3.3': resolution: {integrity: sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==} + '@types/cookie@0.4.1': + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} @@ -608,6 +626,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -787,6 +809,14 @@ packages: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} + engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} + engines: {node: '>=10.0.0'} + + engine.io@6.5.4: + resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==} + engines: {node: '>=10.2.0'} + esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -1256,6 +1286,17 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + socket.io-adapter@2.5.4: + resolution: {integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.7.5: + resolution: {integrity: sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==} + engines: {node: '>=10.2.0'} + source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -1353,6 +1394,18 @@ packages: vue@3.3.4: resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} + ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -1592,6 +1645,8 @@ snapshots: dependencies: '@sentry/types': 7.114.0 + '@socket.io/component-emitter@3.1.2': {} + '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.35 @@ -1611,6 +1666,8 @@ snapshots: '@types/cookie@0.3.3': {} + '@types/cookie@0.4.1': {} + '@types/cors@2.8.17': dependencies: '@types/node': 16.18.38 @@ -1798,6 +1855,8 @@ snapshots: balanced-match@1.0.2: {} + base64id@2.0.0: {} + binary-extensions@2.2.0: {} body-parser@1.20.2: @@ -1965,6 +2024,25 @@ snapshots: encodeurl@1.0.2: {} + engine.io-parser@5.2.2: {} + + engine.io@6.5.4: + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 18.19.33 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.2.2 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + esbuild@0.20.2: optionalDependencies: '@esbuild/aix-ppc64': 0.20.2 @@ -2493,6 +2571,36 @@ snapshots: slash@3.0.0: {} + socket.io-adapter@2.5.4: + dependencies: + debug: 4.3.4 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + socket.io@4.7.5: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.5.4 + socket.io-adapter: 2.5.4 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + source-map-js@1.0.2: {} statuses@2.0.1: {} @@ -2576,6 +2684,8 @@ snapshots: '@vue/server-renderer': 3.3.4(vue@3.3.4) '@vue/shared': 3.3.4 + ws@8.11.0: {} + xtend@4.0.2: {} yallist@4.0.0: {} diff --git a/api/routes/_express-call.ts b/api/routes/_express-call.ts deleted file mode 100644 index 696698ef..00000000 --- a/api/routes/_express-call.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Busboy } from "busboy"; -import { Request, Response } from "express"; - -export type ExpressCall< - T extends { - resBody?: object; - params?: object; - reqBody?: object; - query?: object; - }, -> = [ - Request & { - busboy: Busboy; - }, - - Response, -]; diff --git a/api/routes/fs/text.ts b/api/routes/fs/text.ts deleted file mode 100644 index 93a537b4..00000000 --- a/api/routes/fs/text.ts +++ /dev/null @@ -1,119 +0,0 @@ -import axios from "axios"; -import { v2 as cloudinary } from "cloudinary"; - -import { ExpressCall } from "~routes/_express-call"; - -const sessionHashes: Record = {}; - -export const get = async ( - ...[req, res]: ExpressCall<{ - resBody: - | { - width: number; - height: number; - url: string; - } - | { error: string }; - query: { - color: string; - colorBackground: string; - width: number; - font: string; - text: string; - }; - }> -) => { - const { color, colorBackground, width, font, text } = req.query; - const context: Record = { - color, - colorBackground, - width, - text, - }; - await cloudinary.search - .expression( - `tags=${font} AND ${Object.keys(context) - .reduce( - (acc, key) => [...acc, `context.${key}="${String(context[key])}"`], - [], - ) - .join(" AND ")}`, - ) - .execute() - .then( - ({ - resources, - }: { - resources: { width: number; height: number; secure_url: string }[]; - }) => { - if (resources.length) { - console.log(`Found an existing text`); - const { width, height, secure_url: url } = resources[0]; - return res.json({ width, height, url }); - } else { - console.log(`Found no existing text, generating text image...`); - generateImage(req.query) - .then(({ width, height, secure_url: url }) => { - console.log(`Text image generated: url=${url}`); - return res.json({ width, height, url }); - }) - .catch((response: Error) => { - res.status(500).send({ error: response.message }); - }); - } - }, - ) - .catch((e) => { - console.error(e); - }); -}; - -const generateImage = (parameters: { - color: string; - colorBackground: string; - width: number; - font: string; - text: string; -}) => - axios - .get( - parameters.font.includes("/") - ? process.env.FONT_BASE_URL! - : `${process.env.FONT_PRODUCT_BASE_URL!}${parameters.font}`, - ) - .then(({ data }: { data: string }) => { - const sessionHashMatch = data.match(/(?<=font_rend.php\?id=)[a-z\d]+/); - if (sessionHashMatch) { - sessionHashes[parameters.font] = sessionHashMatch[0]; - } else { - throw new Error( - `No session ID found in URL ${process.env.FONT_BASE_URL!}${ - parameters.font - }`, - ); - } - }) - .then(() => - cloudinary.uploader.upload( - `${process.env.FONT_IMAGE_GEN_URL!}?${new URLSearchParams({ - id: sessionHashes[parameters.font], - rbe: "fixed", - rt: parameters.text, - fg: parameters.color, - bg: parameters.colorBackground, - }).toString()}`, - { - folder: "texts", - async: false, - tags: [parameters.font], - context: parameters, - }, - (error, result) => { - if (error) { - console.error(error); - } - const { width, height, secure_url: url } = result!; - return { width, height, url }; - }, - ), - ); diff --git a/api/routes/_auth.ts b/api/services/_auth.ts similarity index 80% rename from api/routes/_auth.ts rename to api/services/_auth.ts index 5b44f7ad..f354de7d 100644 --- a/api/routes/_auth.ts +++ b/api/services/_auth.ts @@ -10,28 +10,6 @@ export const getUserCredentials = (user: User) => ({ "x-dm-pass": user.hashedPassword, }); -export const authenticateToken = ( - req: Request, - res: Response, - next: CallableFunction, -) => { - const authHeader = req.headers.authorization; - const token = authHeader?.split(" ")?.[1]; - - if (token == null) return res.sendStatus(401); - - jwt.verify( - token, - process.env.TOKEN_SECRET!, - (err: unknown, user: unknown) => { - if (err) { - return res.sendStatus(401); - } - req.user = user as User; - next(); - }, - ); -}; export const checkUserIsAdminForExportOrIsEditorForSaveOrIsFirstFileForModel = ( req: Request, diff --git a/api/routes/_upload_utils.ts b/api/services/_upload_utils.ts similarity index 100% rename from api/routes/_upload_utils.ts rename to api/services/_upload_utils.ts diff --git a/api/routes/fs/base64.ts b/api/services/fs/base64.ts similarity index 94% rename from api/routes/fs/base64.ts rename to api/services/fs/base64.ts index d2f396fe..56dddd65 100644 --- a/api/routes/fs/base64.ts +++ b/api/services/fs/base64.ts @@ -1,7 +1,7 @@ import axios from "axios"; import sizeOf from "image-size"; -import { ExpressCall } from "~routes/_express-call"; +import { ExpressCall } from "~/services/_express-call"; export const get = async ( ...[req, res]: ExpressCall<{ diff --git a/api/routes/fs/browse/:imageType/:country/:magazine.ts b/api/services/fs/browse/:imageType/:country/:magazine.ts similarity index 92% rename from api/routes/fs/browse/:imageType/:country/:magazine.ts rename to api/services/fs/browse/:imageType/:country/:magazine.ts index a3dc54ef..533a9784 100644 --- a/api/routes/fs/browse/:imageType/:country/:magazine.ts +++ b/api/services/fs/browse/:imageType/:country/:magazine.ts @@ -1,6 +1,6 @@ import fs from "fs"; -import { ExpressCall } from "~routes/_express-call"; +import { ExpressCall } from "~/services/_express-call"; export const get = ( ...[req, res]: ExpressCall<{ diff --git a/api/routes/fs/browseEdges.ts b/api/services/fs/browseEdges.ts similarity index 95% rename from api/routes/fs/browseEdges.ts rename to api/services/fs/browseEdges.ts index 7e88ac98..dd0cde4e 100644 --- a/api/routes/fs/browseEdges.ts +++ b/api/services/fs/browseEdges.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; -import { ExpressCall } from "~routes/_express-call"; +import { ExpressCall } from "~/services/_express-call"; const edgesPath = `${process.env.PWD!}/../${process.env.EDGES_PATH!}`; const REGEX_IS_BROWSABLE_FILE = /^[-+(). _A-Za-z\d]+$/; diff --git a/api/routes/fs/generateDefaultEdge.ts b/api/services/fs/generateDefaultEdge.ts similarity index 96% rename from api/routes/fs/generateDefaultEdge.ts rename to api/services/fs/generateDefaultEdge.ts index a8b68197..f2d4a292 100644 --- a/api/routes/fs/generateDefaultEdge.ts +++ b/api/services/fs/generateDefaultEdge.ts @@ -1,7 +1,7 @@ import fs from "fs"; import sharp from "sharp"; -import { ExpressCall } from "~routes/_express-call"; +import { ExpressCall } from "~/services/_express-call"; // eslint-disable-next-line max-len const REGEX_EDGE_URL = /^edges\/(?[^/]+)\/gen\/_?(?[^.]+)\.(?[^.]+)\.(?[^?]+)?(?:\?.+)?$/; diff --git a/api/routes/fs/save.ts b/api/services/fs/save.ts similarity index 98% rename from api/routes/fs/save.ts rename to api/services/fs/save.ts index 006ff7c0..c5eda1cc 100644 --- a/api/routes/fs/save.ts +++ b/api/services/fs/save.ts @@ -7,7 +7,7 @@ import { PUT__edgecreator__publish__$country__$magazine__$issuenumber, PUT__edgecreator__submit, } from "~dm_types/routes"; -import { ExpressCall } from "~routes/_express-call"; +import { ExpressCall } from "~/services/_express-call"; import { ExportPaths } from "~types/ExportPaths"; import { ModelContributor } from "~types/ModelContributor"; diff --git a/api/routes/fs/upload-base64.ts b/api/services/fs/upload-base64.ts similarity index 91% rename from api/routes/fs/upload-base64.ts rename to api/services/fs/upload-base64.ts index 295de977..63835f5b 100644 --- a/api/routes/fs/upload-base64.ts +++ b/api/services/fs/upload-base64.ts @@ -1,8 +1,8 @@ import { decode } from "node-base64-image"; import { PUT__edgecreator__multiple_edge_photo__v2 } from "~dm_types/routes"; -import { ExpressCall } from "~routes/_express-call"; -import { getNextAvailableFile } from "~routes/_upload_utils"; +import { ExpressCall } from "~/services/_express-call"; +import { getNextAvailableFile } from "~/services/_upload_utils"; import { call, createAxios } from "../../axios-helper"; const edgesPath: string = process.env.EDGES_PATH!; diff --git a/api/routes/fs/upload.ts b/api/services/fs/upload.ts similarity index 97% rename from api/routes/fs/upload.ts rename to api/services/fs/upload.ts index 36feec45..757f8649 100644 --- a/api/routes/fs/upload.ts +++ b/api/services/fs/upload.ts @@ -3,9 +3,9 @@ import fs from "fs"; import multer from "multer"; import { dirname } from "path"; -import { getUserCredentials } from "~routes/_auth"; -import { ExpressCall } from "~routes/_express-call"; -import { getNextAvailableFile } from "~routes/_upload_utils"; +import { getUserCredentials } from "~/services/_auth"; +import { ExpressCall } from "~/services/_express-call"; +import { getNextAvailableFile } from "~/services/_upload_utils"; import { call, createAxios } from "../../axios-helper"; const dmApi = createAxios(process.env.VITE_DM_API_URL!); diff --git a/api/services/text/index.ts b/api/services/text/index.ts new file mode 100644 index 00000000..b3ab9a85 --- /dev/null +++ b/api/services/text/index.ts @@ -0,0 +1,123 @@ +import type { Namespace, Server } from "socket.io"; +import { v2 as cloudinary } from "cloudinary"; +import axios from "axios"; + +import type Events from "./types"; +import { namespaceEndpoint } from "./types"; + +const sessionHashes: Record = {}; + +const generateImage = (parameters: { + color: string; + colorBackground: string; + width: number; + font: string; + text: string; +}) => + axios + .get( + parameters.font.includes("/") + ? process.env.FONT_BASE_URL! + : `${process.env.FONT_PRODUCT_BASE_URL!}${parameters.font}` + ) + .then(({ data }: { data: string }) => { + const sessionHashMatch = data.match(/(?<=font_rend.php\?id=)[a-z\d]+/); + if (sessionHashMatch) { + sessionHashes[parameters.font] = sessionHashMatch[0]; + } else { + throw new Error( + `No session ID found in URL ${process.env.FONT_BASE_URL!}${ + parameters.font + }` + ); + } + }) + .catch((response: Error) => { + return Promise.reject(response); + }) + .then(() => + cloudinary.uploader.upload( + `${process.env.FONT_IMAGE_GEN_URL!}?${new URLSearchParams({ + id: sessionHashes[parameters.font], + rbe: "fixed", + rt: parameters.text, + fg: parameters.color, + bg: parameters.colorBackground, + }).toString()}`, + { + folder: "texts", + async: false, + tags: [parameters.font], + context: parameters, + }, + (error, result) => { + if (error) { + console.error(error); + } + const { width, height, secure_url: url } = result!; + Promise.resolve({ width, height, url }); + } + ) + ); + +export default (io: Server) => { + (io.of(namespaceEndpoint) as Namespace).on("connection", (socket) => { + console.log("connected to text"); + + socket.on("getText", async (parameters, callback) => { + const { color, colorBackground, width, font, text } = parameters; + const context: Record = { + color, + colorBackground, + width, + text, + }; + await cloudinary.search + .expression( + `tags=${font} AND ${Object.keys(context) + .reduce( + (acc, key) => [ + ...acc, + `context.${key}="${String(context[key])}"`, + ], + [] + ) + .join(" AND ")}` + ) + .execute() + .then( + ({ + resources, + }: { + resources: { + width: number; + height: number; + secure_url: string; + }[]; + }) => { + if (resources.length) { + console.log(`Found an existing text`); + const { width, height, secure_url: url } = resources[0]; + callback({ results: { width, height, url } }); + } else { + console.log(`Found no existing text, generating text image...`); + generateImage(parameters) + .then(({ width, height, secure_url: url }) => { + console.log(`Text image generated: url=${url}`); + callback({ results: { width, height, url } }); + }) + .catch((response: Error) => { + callback({ + error: "Image generation error", + errorDetails: response.message, + }); + }); + } + } + ) + .catch((e) => { + console.error(e); + }); + }); + }); +}; diff --git a/api/services/text/types.ts b/api/services/text/types.ts new file mode 100644 index 00000000..daea9897 --- /dev/null +++ b/api/services/text/types.ts @@ -0,0 +1,30 @@ +import type { WantedEdge } from "~dm-types/WantedEdge"; +import type { edge } from "~prisma-clients/client_dm"; +import { Errorable } from "~socket.io-services/types"; + +export const namespaceEndpoint = "/text"; +export default abstract class { + static namespaceEndpoint = namespaceEndpoint; + + abstract getText: ( + parameters: { + color: string; + colorBackground: string; + width: number; + font: string; + text: string; + }, + callback: ( + value: Errorable< + { + results: { + width: number; + height: number; + url: string; + }; + }, + "Image generation error" + > + ) => void + ) => void; +} diff --git a/api/tsconfig.json b/api/tsconfig.json index fc48b799..63ae54b2 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -18,10 +18,11 @@ "paths": { "~/*": ["./*"], "~types/*": ["../types/*"], - "~dm_types/*": ["./node_modules/ducksmanager/types/*"], - "~prisma/*": ["./node_modules/ducksmanager/types/*"], - "~prisma_clients/*": ["node_modules/ducksmanager/api/dist/prisma/*"], - "~routes/*": ["./routes/*"] + "~dm-services/*": ["../../../packages/api/services/*"], + "~dm-types/*": ["../../../packages/types/*"], + "~prisma-clients/*": ["../../../packages/prisma-types/*"], + "~routes/*": ["services/*"], + "~socket.io-services/*": ["../../../packages/socket.io-services/*"] } }, "exclude": ["node_modules"], diff --git a/api/tsconfig.prod.json b/api/tsconfig.prod.json deleted file mode 100644 index 71c9fe02..00000000 --- a/api/tsconfig.prod.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "module": "commonjs", - "target": "es2018", - "lib": [ - "DOM", - "ESNext" - ], - "strict": true, - "jsx": "preserve", - "esModuleInterop": true, - "skipLibCheck": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "noUnusedLocals": true, - "strictNullChecks": true, - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "outDir": "./dist", - "paths": { - "~/*": [ - "*" - ], - "~types/*": [ - "../types/*" - ], - "~dm_types/*": [ - "./dm_types/*" - ], - "~prisma/*": [ - "./dm_types/*" - ], - "~prisma_clients/*": [ - "./prisma_clients/*" - ], - "~routes/*": [ - "./routes/*" - ] - } - }, - "include": [ - "*.ts", - "routes/**/*.ts", - "dm_types/**/*.ts", - "prisma_clients/*" - ] -} \ No newline at end of file diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 5a368798..06139677 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -54,6 +54,7 @@ declare global { const h: typeof import('vue')['h'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const inject: typeof import('vue')['inject'] + const injectLocal: typeof import('@vueuse/core')['injectLocal'] const isDefined: typeof import('@vueuse/core')['isDefined'] const isProxy: typeof import('vue')['isProxy'] const isReactive: typeof import('vue')['isReactive'] @@ -83,6 +84,7 @@ declare global { const onUpdated: typeof import('vue')['onUpdated'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const provide: typeof import('vue')['provide'] + const provideLocal: typeof import('@vueuse/core')['provideLocal'] const reactify: typeof import('@vueuse/core')['reactify'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactive: typeof import('vue')['reactive'] @@ -147,6 +149,7 @@ declare global { const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] const useCached: typeof import('@vueuse/core')['useCached'] const useClipboard: typeof import('@vueuse/core')['useClipboard'] + const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] const useCloned: typeof import('@vueuse/core')['useCloned'] const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useCondition: typeof import('./src/composables/useCondition')['default'] @@ -312,11 +315,13 @@ declare global { // for type re-export declare global { // @ts-ignore - export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue' + export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' + import('vue') } // for vue template auto import import { UnwrapRef } from 'vue' declare module 'vue' { + interface GlobalComponents {} interface ComponentCustomProperties { readonly $$: UnwrapRef readonly $: UnwrapRef @@ -367,6 +372,7 @@ declare module 'vue' { readonly h: UnwrapRef readonly ignorableWatch: UnwrapRef readonly inject: UnwrapRef + readonly injectLocal: UnwrapRef readonly isDefined: UnwrapRef readonly isProxy: UnwrapRef readonly isReactive: UnwrapRef @@ -396,6 +402,7 @@ declare module 'vue' { readonly onUpdated: UnwrapRef readonly pausableWatch: UnwrapRef readonly provide: UnwrapRef + readonly provideLocal: UnwrapRef readonly reactify: UnwrapRef readonly reactifyObject: UnwrapRef readonly reactive: UnwrapRef @@ -460,6 +467,7 @@ declare module 'vue' { readonly useBrowserLocation: UnwrapRef readonly useCached: UnwrapRef readonly useClipboard: UnwrapRef + readonly useClipboardItems: UnwrapRef readonly useCloned: UnwrapRef readonly useColorMode: UnwrapRef readonly useCondition: UnwrapRef @@ -624,6 +632,7 @@ declare module 'vue' { } } declare module '@vue/runtime-core' { + interface GlobalComponents {} interface ComponentCustomProperties { readonly $$: UnwrapRef readonly $: UnwrapRef @@ -674,6 +683,7 @@ declare module '@vue/runtime-core' { readonly h: UnwrapRef readonly ignorableWatch: UnwrapRef readonly inject: UnwrapRef + readonly injectLocal: UnwrapRef readonly isDefined: UnwrapRef readonly isProxy: UnwrapRef readonly isReactive: UnwrapRef @@ -703,6 +713,7 @@ declare module '@vue/runtime-core' { readonly onUpdated: UnwrapRef readonly pausableWatch: UnwrapRef readonly provide: UnwrapRef + readonly provideLocal: UnwrapRef readonly reactify: UnwrapRef readonly reactifyObject: UnwrapRef readonly reactive: UnwrapRef @@ -767,6 +778,7 @@ declare module '@vue/runtime-core' { readonly useBrowserLocation: UnwrapRef readonly useCached: UnwrapRef readonly useClipboard: UnwrapRef + readonly useClipboardItems: UnwrapRef readonly useCloned: UnwrapRef readonly useColorMode: UnwrapRef readonly useCondition: UnwrapRef diff --git a/components.d.ts b/components.d.ts index 49bad629..b9575d1c 100644 --- a/components.d.ts +++ b/components.d.ts @@ -1,46 +1,13 @@ /* eslint-disable */ -/* prettier-ignore */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 export {} +/* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { ArcCircleRender: typeof import('./src/components/renders/ArcCircleRender.vue')['default'] - BAlert: typeof import('bootstrap-vue-next')['BAlert'] - BBadge: typeof import('bootstrap-vue-next')['BBadge'] - BButton: typeof import('bootstrap-vue-next')['BButton'] - BButtonGroup: typeof import('bootstrap-vue-next')['BButtonGroup'] - BButtonToolbar: typeof import('bootstrap-vue-next')['BButtonToolbar'] - BCard: typeof import('bootstrap-vue-next')['BCard'] - BCardGroup: typeof import('bootstrap-vue-next')['BCardGroup'] - BCardText: typeof import('bootstrap-vue-next')['BCardText'] - BCarousel: typeof import('bootstrap-vue-next')['BCarousel'] - BCarouselSlide: typeof import('bootstrap-vue-next')['BCarouselSlide'] - BCol: typeof import('bootstrap-vue-next')['BCol'] - BCollapse: typeof import('bootstrap-vue-next')['BCollapse'] - BContainer: typeof import('bootstrap-vue-next')['BContainer'] - BDropdown: typeof import('bootstrap-vue-next')['BDropdown'] - BDropdownItem: typeof import('bootstrap-vue-next')['BDropdownItem'] - BFormCheckbox: typeof import('bootstrap-vue-next')['BFormCheckbox'] - BFormGroup: typeof import('bootstrap-vue-next')['BFormGroup'] - BFormInput: typeof import('bootstrap-vue-next')['BFormInput'] - BFormRadio: typeof import('bootstrap-vue-next')['BFormRadio'] - BFormSelect: typeof import('bootstrap-vue-next')['BFormSelect'] - BFormSelectOption: typeof import('bootstrap-vue-next')['BFormSelectOption'] - BImg: typeof import('bootstrap-vue-next')['BImg'] - BLink: typeof import('bootstrap-vue-next')['BLink'] - BModal: typeof import('bootstrap-vue-next')['BModal'] - BNavbar: typeof import('bootstrap-vue-next')['BNavbar'] - BNavbarBrand: typeof import('bootstrap-vue-next')['BNavbarBrand'] - BNavbarToggle: typeof import('bootstrap-vue-next')['BNavbarToggle'] - BPopover: typeof import('bootstrap-vue-next')['BPopover'] - BProgress: typeof import('bootstrap-vue-next')['BProgress'] - BProgressBar: typeof import('bootstrap-vue-next')['BProgressBar'] - BRow: typeof import('bootstrap-vue-next')['BRow'] - BTab: typeof import('bootstrap-vue-next')['BTab'] - BTabs: typeof import('bootstrap-vue-next')['BTabs'] ConfirmEditMultipleValues: typeof import('./src/components/ConfirmEditMultipleValues.vue')['default'] Default: typeof import('./src/layouts/default.vue')['default'] Dimensions: typeof import('./src/components/Dimensions.vue')['default'] @@ -52,24 +19,6 @@ declare module 'vue' { FormInputRow: typeof import('./src/components/FormInputRow.vue')['default'] Gallery: typeof import('./src/components/Gallery.vue')['default'] GradientRender: typeof import('./src/components/renders/GradientRender.vue')['default'] - IBiArchive: typeof import('~icons/bi/archive')['default'] - IBiArrowDownSquareFill: typeof import('~icons/bi/arrow-down-square-fill')['default'] - IBiArrowsAngleExpand: typeof import('~icons/bi/arrows-angle-expand')['default'] - IBiArrowUpSquareFill: typeof import('~icons/bi/arrow-up-square-fill')['default'] - IBiCamera: typeof import('~icons/bi/camera')['default'] - IBiCheck: typeof import('~icons/bi/check')['default'] - IBiCloudArrowUpFill: typeof import('~icons/bi/cloud-arrow-up-fill')['default'] - IBiEmojiFrownFill: typeof import('~icons/bi/emoji-frown-fill')['default'] - IBiEmojiNeutralFill: typeof import('~icons/bi/emoji-neutral-fill')['default'] - IBiEmojiSmileFill: typeof import('~icons/bi/emoji-smile-fill')['default'] - IBiEyeFill: typeof import('~icons/bi/eye-fill')['default'] - IBiEyeSlashFill: typeof import('~icons/bi/eye-slash-fill')['default'] - IBiFront: typeof import('~icons/bi/front')['default'] - IBiHouse: typeof import('~icons/bi/house')['default'] - IBiInfoCircleFill: typeof import('~icons/bi/info-circle-fill')['default'] - IBiPencil: typeof import('~icons/bi/pencil')['default'] - IBiX: typeof import('~icons/bi/x')['default'] - IBiXSquareFill: typeof import('~icons/bi/x-square-fill')['default'] ImageRender: typeof import('./src/components/renders/ImageRender.vue')['default'] Issue: typeof import('./src/components/from-dm/Issue.vue')['default'] IssueSelect: typeof import('./src/components/IssueSelect.vue')['default'] @@ -93,7 +42,4 @@ declare module 'vue' { Upload: typeof import('./src/components/Upload.vue')['default'] UploadableEdgesCarousel: typeof import('./src/components/from-dm/UploadableEdgesCarousel.vue')['default'] } - export interface ComponentCustomProperties { - vBTooltip: typeof import('bootstrap-vue-next')['vBTooltip'] - } } diff --git a/package.json b/package.json index d92f561b..42f6f313 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "vue-router": "^4.3.2", "vue3-simple-typeahead": "^1.0.11", "xmldom": "^0.6.0", - "~prisma-clients": "workspace:*" + "~prisma-clients": "workspace:*", + "~socket.io-client-services": "workspace:*" }, "devDependencies": { "@iconify-json/bi": "^1.1.23", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4cc94524..676e40d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: ~prisma-clients: specifier: workspace:* version: link:../../packages/prisma-clients + ~socket.io-client-services: + specifier: workspace:* + version: link:../../packages/socket.io-client-services devDependencies: '@iconify-json/bi': specifier: ^1.1.23 diff --git a/src/App.vue b/src/App.vue index 7274244b..37851759 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,6 +4,24 @@ diff --git a/src/components/renders/StapleRender.vue b/src/components/renders/StapleRender.vue index 065ec563..d79874a8 100644 --- a/src/components/renders/StapleRender.vue +++ b/src/components/renders/StapleRender.vue @@ -8,7 +8,8 @@ width: 0.5, stroke: 'black', x: dimensions.width / 2 - 0.25, - y: dimensions.height / 2 - options.yDistanceFromCenter! - options.height, + y: + dimensions.height / 2 - options.yDistanceFromCenter! - options.height, }" > @@ -27,11 +28,9 @@ diff --git a/src/components/renders/TextRender.vue b/src/components/renders/TextRender.vue index 37d60fd6..d269a4b2 100644 --- a/src/components/renders/TextRender.vue +++ b/src/components/renders/TextRender.vue @@ -23,12 +23,14 @@ diff --git a/src/components/ConfirmEditMultipleValues.vue b/src/components/ConfirmEditMultipleValues.vue index 7f5e9573..53cc4003 100644 --- a/src/components/ConfirmEditMultipleValues.vue +++ b/src/components/ConfirmEditMultipleValues.vue @@ -21,7 +21,7 @@ diff --git a/src/components/renders/TextRender.vue b/src/components/renders/TextRender.vue index d269a4b2..98a07020 100644 --- a/src/components/renders/TextRender.vue +++ b/src/components/renders/TextRender.vue @@ -22,12 +22,11 @@ diff --git a/src/components/EdgeCanvas.vue b/src/components/EdgeCanvas.vue index ab58d759..0e6f63c4 100644 --- a/src/components/EdgeCanvas.vue +++ b/src/components/EdgeCanvas.vue @@ -147,9 +147,9 @@ const toKeyValue = (arr: OptionNameAndValue[]) => { ); return Object.keys(val).length ? val : undefined; }; -const borderWidth = ref(1 as number); +const borderWidth = ref(1); -const canvas = ref(null as HTMLElement | null); +const canvas = ref(null); const hoveredStepStore = hoveredStep(); const editingStepStore = editingStep(); diff --git a/src/components/EdgeGallery.vue b/src/components/EdgeGallery.vue index b5baf756..4b9c3b66 100644 --- a/src/components/EdgeGallery.vue +++ b/src/components/EdgeGallery.vue @@ -29,10 +29,10 @@ diff --git a/src/components/FormColorInputRow.vue b/src/components/FormColorInputRow.vue index 05268bbd..da1c629e 100644 --- a/src/components/FormColorInputRow.vue +++ b/src/components/FormColorInputRow.vue @@ -111,12 +111,12 @@ const props = withDefaults( }, ); -const originalColor = ref(null as string | null); +const originalColor = ref(null); const stepStore = step(); const mainStore = main(); -const isTransparent = ref(false as boolean); +const isTransparent = ref(false); const photoUrls = computed(() => main().photoUrls); const hasPhotoUrl = computed(() => Object.keys(photoUrls.value).length); const colorPickerOption = computed(() => ui().colorPickerOption); diff --git a/src/components/Gallery.vue b/src/components/Gallery.vue index 3d63586e..dec2a489 100644 --- a/src/components/Gallery.vue +++ b/src/components/Gallery.vue @@ -104,9 +104,9 @@ withDefaults( const emit = defineEmits<(e: "change", value: string) => void>(); -const clickedImage = ref(null as GalleryItem | null); -const showUploadModal = ref(false as boolean); -const showChooseImageModal = ref(false as boolean); +const clickedImage = ref(null); +const showUploadModal = ref(false); +const showChooseImageModal = ref(false); const country = computed(() => main().country); const magazine = computed(() => main().magazine); diff --git a/src/components/IssueSelect.vue b/src/components/IssueSelect.vue index 2ec7e7c0..0130dbd5 100644 --- a/src/components/IssueSelect.vue +++ b/src/components/IssueSelect.vue @@ -77,14 +77,14 @@ diff --git a/src/components/Popover.vue b/src/components/Popover.vue index a989491f..f7d637b1 100644 --- a/src/components/Popover.vue +++ b/src/components/Popover.vue @@ -37,8 +37,8 @@ const closePopupSoon = () => { isOverPopupText.value = false; }, closeDelay); }; -const isOverPopup = ref(false as boolean); -const isOverPopupText = ref(false as boolean); +const isOverPopup = ref(false); +const isOverPopupText = ref(false); const onOpen = () => { for (const element of document.getElementsByClassName("popper")) diff --git a/src/components/PublishedEdge.vue b/src/components/PublishedEdge.vue index 76a5f075..5b64bf5d 100644 --- a/src/components/PublishedEdge.vue +++ b/src/components/PublishedEdge.vue @@ -20,7 +20,7 @@ const props = defineProps<{ defineEmits<(event: "load" | "error") => void>(); -const naturalHeight = ref(0 as number); +const naturalHeight = ref(0); const zoom = computed(() => ui().zoom); const getEdgeUrl = () => `${import.meta.env.VITE_EDGES_URL as string}/${main() diff --git a/src/components/SaveModelButton.vue b/src/components/SaveModelButton.vue index b4642154..5edbfa79 100644 --- a/src/components/SaveModelButton.vue +++ b/src/components/SaveModelButton.vue @@ -62,7 +62,7 @@ :ref="`${userContributionEnL10n[contributionType]}Typeahead`" :items=" getUsersWithoutContributors( - contributionType as userContributionType, + contributionType as userContributionType ) " :item-projection="({ username }: SimpleUser) => username" @@ -72,14 +72,14 @@ (user: SimpleUser) => onUserSelect( user.username, - contributionType as userContributionType, + contributionType as userContributionType ) " />
  • @@ -112,12 +112,11 @@ import { useI18n } from "vue-i18n"; import Vue3SimpleTypeahead from "vue3-simple-typeahead"; import saveEdge from "~/composables/useSaveEdge"; -import { collection } from "~/stores/collection"; import { main } from "~/stores/main"; import { ui } from "~/stores/ui"; -import { users } from "~/stores/users"; import type { userContributionType } from "~prisma-clients/client_dm"; import type { SimpleUser } from "~types/SimpleUser"; +import { stores as webStores } from "~web"; const userContributionEnL10n: Record = { photographes: "photographers", @@ -127,8 +126,8 @@ const userContributionEnL10n: Record = { const { saveEdgeSvg } = saveEdge(); const { t: $t } = useI18n(); -const userStore = users(); -const collectionStore = collection(); +const userStore = webStores.users(); +const collectionStore = webStores.collection(); const mainStore = main(); const props = withDefaults( @@ -142,10 +141,10 @@ const props = withDefaults( }, ); -const showModal = ref(false as boolean); -const progress = ref(0 as number); -const issueIndexToSave = ref(null as number | null); -const result = ref(null as string | null); +const showModal = ref(false); +const progress = ref(0); +const issueIndexToSave = ref(null); +const result = ref(null); const designersTypeahead = ref(); const photographersTypeahead = ref(); @@ -205,7 +204,7 @@ watch( props.withExport, props.withSubmit, ).then((response) => { - const isSuccess = response.paths.svgPath; + const isSuccess = response!.paths.svgPath; if (isSuccess) { progress.value += 100 / mainStore.issuenumbers.length; issueIndexToSave.value!++; diff --git a/src/components/SessionInfo.vue b/src/components/SessionInfo.vue index c23b3fd8..a120f913 100644 --- a/src/components/SessionInfo.vue +++ b/src/components/SessionInfo.vue @@ -20,17 +20,16 @@ import { useI18n } from "vue-i18n"; import { availableLocales } from "~/composables/useLocales"; -import { coa } from "~/stores/coa"; -import { collection } from "~/stores/collection"; +import { stores as webStores } from "~web"; const { locale } = useI18n(); const locales = computed(() => availableLocales); -const collectionStore = collection(); +const collectionStore = webStores.collection(); const username = computed(() => collectionStore.user?.username as string); const reloadWithLocale = async (key: string) => { locale.value = key; - await coa().fetchCountryNames(locale.value); + await webStores.coa().fetchCountryNames(); }; diff --git a/types/SessionUser.ts b/types/SessionUser.ts deleted file mode 100644 index 7ecf8ff3..00000000 --- a/types/SessionUser.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface User { - id: number; - username: string; - hashedPassword: string; - privileges: Record; -} - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Express { - interface Request { - user: User | null; - } - } -} From d3d5ceb8af4d452750de25873de56995590884cf Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Sun, 19 May 2024 23:22:01 +0200 Subject: [PATCH 13/29] WIP rollup --- .eslintrc-auto-import.json | 3 +- api/Dockerfile | 10 +++---- api/rollup.config.js | 50 +++++++++++++++++++++++-------- package.json | 5 ++-- src/components/Popover.vue | 4 +-- src/composables/useStepOptions.ts | 29 +++++++++--------- src/i18n.ts | 3 +- vite.config.ts | 7 ++--- 8 files changed, 67 insertions(+), 44 deletions(-) diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index b5676bb8..055835b3 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -317,6 +317,7 @@ "watchThrottled": true, "watchTriggerable": true, "watchWithFilter": true, - "whenever": true + "whenever": true, + "useI18n": true } } diff --git a/api/Dockerfile b/api/Dockerfile index 1e30c81a..c01b8db3 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -2,12 +2,10 @@ FROM pnpm LABEL org.opencontainers.image.authors="Bruno Perel" WORKDIR /app -COPY dist /app -COPY package.json /app -COPY pnpm-lock.yaml /app -RUN pnpm i --production --frozen-lockfile +RUN apt update && apt install -y imagemagick && apt clean && rm -rf /var/lib/apt/lists/* + +COPY dist/bundle.js /app/index.mjs EXPOSE 3001 -WORKDIR /app/apps/edgecreator/api -CMD ["node", "index.js"] +CMD ["node", "index.mjs"] diff --git a/api/rollup.config.js b/api/rollup.config.js index e1df8629..5eb62971 100644 --- a/api/rollup.config.js +++ b/api/rollup.config.js @@ -1,14 +1,40 @@ -import resolve from '@rollup/plugin-node-resolve'; -import typescript from '@rollup/plugin-typescript'; -import tscAlias from 'rollup-plugin-tsc-alias'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import esmShim from '@rollup/plugin-esm-shim'; - -export default commandLineArgs => ({ - input: 'dist/apps/edgecreator/api/index.js', +import resolve from "@rollup/plugin-node-resolve"; +import typescript from "@rollup/plugin-typescript"; +import tscAlias from "rollup-plugin-tsc-alias"; +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; +import esmShim from "@rollup/plugin-esm-shim"; +import { readFileSync, writeFileSync } from "fs"; + +const esmShimFixPlugin = () => ({ + name: "plugin-esm-shim", + writeBundle: async (options) => { + const file = options.file; + const lines = readFileSync(file, "utf-8").split("\n"); + + const X = lines.findIndex((line) => /^\/\/ -- Shims --/.test(line)) + 1; + const Y = lines.findIndex((line) => /^const require =/.test(line)) + 1; + const Z = lines.findIndex((line) => line === "") + 1; + + const movingLines = lines.slice(X - 1, Y); + lines.splice(X - 1, Y - X + 1); + lines.splice(Z - 1, 0, ...movingLines); + writeFileSync(file, lines.join("\n")); + }, +}); + +export default () => ({ + input: "dist/apps/edgecreator/api/index.js", output: { - file: 'dist/bundle.js', + file: "dist/bundle.js", }, - plugins: [resolve(), typescript(),tscAlias(), commonjs(), json(),esmShim()], -}); \ No newline at end of file + plugins: [ + resolve(), + typescript(), + tscAlias(), + commonjs(), + json(), + esmShim(), + esmShimFixPlugin(), + ], +}); diff --git a/package.json b/package.json index a1b9ca81..0ab9896b 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,10 @@ }, "scripts": { "build": "vite build", - "dev": "concurrently -n front,back \"pnpm run start:front\" \"pnpm run start:back\"", + "dev": "concurrently -n vite,vue-tsc \"vite --port 8002\" \"vue-tsc --noEmit --watch\"", "lint": "eslint --fix --cache .", "prepare": "husky install", "preview": "vite preview", - "start:back": "cd api && pnpm run dev", - "start:front": "concurrently -n vite,vue-tsc \"vite --port 8002\" \"vue-tsc --noEmit --watch\"", "typecheck": "vue-tsc --noEmit" }, "dependencies": { @@ -46,6 +44,7 @@ "vue-router": "^4.3.2", "vue3-simple-typeahead": "^1.0.11", "xmldom": "^0.6.0", + "~edgecreator-api": "workspace:*", "~web": "workspace:*", "~prisma-clients": "workspace:*", "~socket.io-client-services": "workspace:*" diff --git a/src/components/Popover.vue b/src/components/Popover.vue index f7d637b1..972e6b97 100644 --- a/src/components/Popover.vue +++ b/src/components/Popover.vue @@ -23,10 +23,10 @@ diff --git a/src/components/IssueSelect.vue b/src/components/IssueSelect.vue index 0130dbd5..0d62b42a 100644 --- a/src/components/IssueSelect.vue +++ b/src/components/IssueSelect.vue @@ -104,7 +104,7 @@ const props = withDefaults( canBeMultiple: false, edgeGallery: false, baseIssueNumbers: () => [], - }, + } ); const currentCountryCode = ref(undefined); @@ -118,13 +118,20 @@ const surroundingIssuesToLoad = ref({ before: 10, after: 10 } as Record< number >); -const countryNames = computed(() => coaStore.countryNames); +const countryNames = computed( + () => + coaStore.countryNames && + Object.entries(coaStore.countryNames).map(([countryCode, countryName]) => ({ + text: countryName, + value: countryCode, + })) +); const publications = computed( () => coaStore.publicationNames && Object.keys(coaStore.publicationNames) .filter((publicationCode) => - publicationCode.startsWith(`${currentCountryCode.value!}/`), + publicationCode.startsWith(`${currentCountryCode.value!}/`) ) .map((publicationCode) => ({ text: coaStore.publicationNames[publicationCode], @@ -132,12 +139,12 @@ const publications = computed( })) .filter(({ text }) => text !== null) .sort(({ text: text1 }, { text: text2 }) => - text1! < text2! ? -1 : text2! < text1! ? 1 : 0, - ), + text1! < text2! ? -1 : text2! < text1! ? 1 : 0 + ) ); const publicationIssues = computed( - () => coaStore.issueNumbers[currentPublicationCode.value!], + () => coaStore.issueNumbers[currentPublicationCode.value!] ); const issues = computed( @@ -157,7 +164,7 @@ const issues = computed( (props.disableOngoingOrPublished && status !== "none") || (props.disableNotOngoingNorPublished && status === "none"), }; - }), + }) ); watch( @@ -172,7 +179,7 @@ watch( }, { immediate: true, - }, + } ); watch( @@ -183,12 +190,12 @@ watch( await coaStore.fetchIssueNumbers([newValue]); await loadEdges(); } - }, + } ); watch( () => surroundingIssuesToLoad.value, - async () => await loadEdges(), + async () => await loadEdges() ); if (props.countryCode) { @@ -199,10 +206,10 @@ const loadEdges = async () => { let issueNumbersFilter = ""; if (props.withEdgeGallery) { const minBaseIssueNumberIndex = publicationIssues.value.indexOf( - props.baseIssueNumbers[0], + props.baseIssueNumbers[0] ); const maxBaseIssueNumberIndex = publicationIssues.value.indexOf( - props.baseIssueNumbers[props.baseIssueNumbers.length - 1], + props.baseIssueNumbers[props.baseIssueNumbers.length - 1] ); issueNumbersFilter = `/${publicationIssues.value .filter( @@ -211,7 +218,7 @@ const loadEdges = async () => { surroundingIssuesToLoad.value.before && index - maxBaseIssueNumberIndex < surroundingIssuesToLoad.value.after && - !props.baseIssueNumbers.includes(issueNumber), + !props.baseIssueNumbers.includes(issueNumber) ) .join(",")}`; hasMoreIssuesToLoad.value = { @@ -226,7 +233,7 @@ const loadEdges = async () => { }; const onChange = ( - data: { width: number; height: number } | Record, + data: { width: number; height: number } | Record ) => emit("change", { width: data.width, diff --git a/src/composables/useEdgecreatorSocket.ts b/src/composables/useEdgecreatorSocket.ts index f3199d4f..c3297f23 100644 --- a/src/composables/useEdgecreatorSocket.ts +++ b/src/composables/useEdgecreatorSocket.ts @@ -5,24 +5,44 @@ import TextServices from "~edgecreator-services/text/types"; import UploadServices from "~edgecreator-services/upload/types"; import type { useSocket } from "~socket.io-client-services"; -const defaultExport = (options: { - session: { - getToken: () => Promise; - clearSession: () => void; - sessionExists: () => Promise; - }; -}) => { - const { addNamespace } = inject("socket") as ReturnType; +const defaultExport = ( + socket: ReturnType, + options: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onConnectError: (e: any, namespace: string) => Promise | void; + session: { + getToken: () => Promise; + clearSession: () => void; + sessionExists: () => Promise; + }; + } +) => { + const { addNamespace } = socket; + + const { session, onConnectError } = options; return { options, imageInfo: addNamespace( ImageInfoServices.namespaceEndpoint, + { session, onConnectError } ), - browse: addNamespace(BrowseServices.namespaceEndpoint), - save: addNamespace(SaveServices.namespaceEndpoint), - text: addNamespace(TextServices.namespaceEndpoint), - upload: addNamespace(UploadServices.namespaceEndpoint), + browse: addNamespace(BrowseServices.namespaceEndpoint, { + session, + onConnectError, + }), + save: addNamespace(SaveServices.namespaceEndpoint, { + session, + onConnectError, + }), + text: addNamespace(TextServices.namespaceEndpoint, { + session, + onConnectError, + }), + upload: addNamespace(UploadServices.namespaceEndpoint, { + session, + onConnectError, + }), }; }; diff --git a/src/main.ts b/src/main.ts index 19c83ad6..8f09ed38 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,5 +34,9 @@ createApp(App) .use(store) .use(head) .use(router) - .provide("socket", useSocket(import.meta.env.VITE_EDGECREATOR_SOCKET_URL)) + .provide("dmSocket", useSocket(import.meta.env.VITE_DM_SOCKET_URL)) + .provide( + "edgecreatorSocket", + useSocket(import.meta.env.VITE_EDGECREATOR_SOCKET_URL) + ) .mount("#app"); diff --git a/src/pages/[...all].vue b/src/pages/[...all].vue index 316a846a..bb083684 100644 --- a/src/pages/[...all].vue +++ b/src/pages/[...all].vue @@ -26,7 +26,7 @@ meta: {{ $t( "Send us photos of magazine edges that you own and earn up to {0} Edge photographer points per edge!", - [mostPopularIssuesInCollectionWithoutEdge[0].popularity], + [mostPopularIssuesInCollectionWithoutEdge[0].popularity] ) }} @@ -48,7 +48,7 @@ meta: {{ $t( "Send us photos of magazine edges that you find on the Internet and earn up to {0} Edge photographer points per edge!", - [mostWantedEdges[0].popularity], + [mostWantedEdges[0].popularity] ) }} @@ -77,7 +77,7 @@ meta: