From a68046ecd6ab21d1e9c90efb986a7ca8b903fef9 Mon Sep 17 00:00:00 2001 From: C4illin Date: Sun, 19 May 2024 00:07:56 +0200 Subject: [PATCH] jsx working --- .eslintrc.cjs | 55 ++++ .gitignore | 4 +- biome.json | 62 +++++ bun.lockb | Bin 18952 -> 99577 bytes db/.gitkeep | 0 package.json | 23 +- prettier.config.cjs | 14 + reset.d.ts | 1 + src/components/base.tsx | 13 + src/components/header.tsx | 49 ++++ src/converters/main.ts | 39 +++ src/converters/sharp.ts | 37 +++ src/helpers/normalizeFiletype.ts | 12 + src/index.ts | 247 ----------------- src/index.tsx | 460 +++++++++++++++++++++++++++++++ src/pages/index.html | 59 ---- src/pages/login.html | 40 --- src/public/script.js | 27 +- tsconfig.json | 130 ++------- 19 files changed, 814 insertions(+), 458 deletions(-) create mode 100644 .eslintrc.cjs create mode 100644 biome.json create mode 100644 db/.gitkeep create mode 100644 prettier.config.cjs create mode 100644 reset.d.ts create mode 100644 src/components/base.tsx create mode 100644 src/components/header.tsx create mode 100644 src/converters/main.ts create mode 100644 src/converters/sharp.ts create mode 100644 src/helpers/normalizeFiletype.ts delete mode 100644 src/index.ts create mode 100644 src/index.tsx delete mode 100644 src/pages/index.html delete mode 100644 src/pages/login.html diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..4e89d35 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,55 @@ +/** @type {import("eslint").Linter.Config} */ +const config = { + root: true, + parser: "@typescript-eslint/parser", + plugins: ["isaacscript", "import"], + extends: [ + "plugin:@typescript-eslint/recommended-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", + "plugin:prettier/recommended", + ], + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + tsconfigRootDir: __dirname, + project: [ + "./tsconfig.json", + "./cli/tsconfig.eslint.json", // separate eslint config for the CLI since we want to lint and typecheck differently due to template files + "./upgrade/tsconfig.json", + "./www/tsconfig.json", + ], + }, + overrides: [ + // Template files don't have reliable type information + { + files: ["./cli/template/**/*.{ts,tsx}"], + extends: ["plugin:@typescript-eslint/disable-type-checked"], + }, + ], + rules: { + // These off/not-configured-the-way-we-want lint rules we like & opt into + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_" }, + ], + "@typescript-eslint/consistent-type-imports": [ + "error", + { prefer: "type-imports", fixStyle: "inline-type-imports" }, + ], + "import/consistent-type-specifier-style": ["error", "prefer-inline"], + + // For educational purposes we format our comments/jsdoc nicely + "isaacscript/complete-sentences-jsdoc": "warn", + "isaacscript/format-jsdoc-comments": "warn", + + // These lint rules don't make sense for us but are enabled in the preset configs + "@typescript-eslint/no-confusing-void-expression": "off", + "@typescript-eslint/restrict-template-expressions": "off", + + // This rule doesn't seem to be working properly + "@typescript-eslint/prefer-nullish-coalescing": "off", + }, +}; + +module.exports = config; \ No newline at end of file diff --git a/.gitignore b/.gitignore index bc34871..ed390cb 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,6 @@ package-lock.json **/*.bun /src/uploads /uploads -/mydb.sqlite \ No newline at end of file +/mydb.sqlite +/output +/db/mydb.sqlite \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..168c992 --- /dev/null +++ b/biome.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", + "formatter": { + "enabled": true, + "formatWithErrors": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80, + "attributePosition": "auto" + }, + "organizeImports": { "enabled": true }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noBannedTypes": "error", + "noUselessThisAlias": "error", + "noUselessTypeConstraint": "error", + "useArrowFunction": "off", + "useLiteralKeys": "error", + "useOptionalChain": "error" + }, + "correctness": { "noPrecisionLoss": "error", "noUnusedVariables": "off" }, + "style": { + "noInferrableTypes": "error", + "noNamespace": "error", + "useAsConstAssertion": "error", + "useBlockStatements": "off", + "useConsistentArrayType": "error", + "useForOf": "error", + "useImportType": "error", + "useShorthandFunctionType": "error" + }, + "suspicious": { + "noEmptyBlockStatements": "error", + "noEmptyInterface": "error", + "noExplicitAny": "error", + "noExtraNonNullAssertion": "error", + "noMisleadingInstantiator": "error", + "noUnsafeDeclarationMerging": "error", + "useAwait": "error", + "useNamespaceKeyword": "error" + } + } + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingComma": "all", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSpacing": true, + "bracketSameLine": false, + "quoteStyle": "double", + "attributePosition": "auto" + } + }, + "overrides": [{ "include": ["./cli/template/**/*.{ts,tsx}"] }] +} diff --git a/bun.lockb b/bun.lockb index 268fe520b511a23411899a3f0c9f6178c3430199..ec4e08752a4f10fd2a1c71a4acbf3c23a9d48c04 100644 GIT binary patch literal 99577 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 delta 3724 zcmcgv4Qx}_6~6byHhwnFe}Y5)A^a8M#7;sSz>iQUO=8wAL&Yg#6cbW#aP1HWNIKS} z9c&N{n&?m-X^R-3YP4!4HncHlC&4#lU}@ptbz=boQ?@4LRa%eHf`9P->X{PdmJ`t4T&Q{R8ydj00>+g|(D_6u#xo*P~@ zd$reAJ7cJ~Ss2YLhTBT|uT>{evdjv*KDMet1UQSburld19cczk1| zLx`imtk<5BGxaYiLO6Il8jnFttVro~#uE%h6DYkO0s)H<`QW!OzzSaAiL97N^z?Og zh`w<5`UK|AS(D>^UVjWwoJrvqZArZkQ~W9LNzfkzcY+^-96NXjJOjKV(H@S5*LAc< zBb)mpFf?ixLdArNjupNJ&XFGiXNP(c(a2iJwQmSVH-ZN-n2zxc;Y8)b(eQdd#yCUu zP-bpsrVN-JslYtjU52PQup9ukUnNAbv2Z#NI{+_@1t0qTAO7;~9gn_pq*s;Hl#yv) zdho&vhbO)KF_mSwX~^Msf8*-TklCap1hu#| z#U^PGtXa~STT_Rn5G%2Uq%3#6dIjiiAS)xKQlY^DO$jSBR-jprpl}-P&YrJaQK)6I zrk14%vA_@>hw3UIACN*o);&OeLwGceS_(Dm-!U-H7>HVEtWZ;aZXrjJrV3;qi5f^_ zHPsI^*FeKqKW3#dh`fWrMHtMZ@vM4fvW;4bHERp1*huH{=c_-)FLuLbL~tvGj0jZ! z1Y?82`fy3TvS$JfmT2l(47NZ4p2A+fogAf_x(dbqryqNPI2gNO^9`VRKoblrxhgrz zH1#1=hEt_bR=_V+8k?jk?_(-kQwy*WSlhxeDGxem5b_5w$O~8uBX2p#QLZWXI;jQh zNhb}KYt~n>0q&=B?)gggL~=~gl(vb~0yZ*{2El%X!3Is4I5*}zU&H{7IjSj2K`4#DKE`=X{xAfm z8%PM|Y_KwgKh4=dHHaPbrsn?}&ib_==JjHE(h#3GHrN2-_G!)$%m?v2wu{l2a|DZ$ z@H1S}j@HtC*18A8T1!(L%*gfsA>YZoenG<72TuH#obTlNMu9$?@Gl|X2P+ z+hQ7Nb!L682K3D}T@=_DlnU+I7?Npp3B83Hq9JLe;b@5VuXWL1qd_@=7CjP@b{avi z(x1^gs5usrPCA5sBHcvqqSkmwrqlE2Ge~xYWF|d`K8t>UKAY4{A(=yA^tp5reI8|X zhh#oQ&`+W>-68tL1{Y0B1Z4qjN`$C4;-a^J3aPv&L^C^Gw4)~|i)kF_ZJ@f%L0L+V zZw}GUjV^i*sGQ#E43R(TqTQWAIh97%w$kNj4h6OZ8qd zxaE~)F5MMRyDc=Zu&ET>YT|9~Q-I}Dw{XD@ISdcBJgV-DP z&I}o!6XS}ofb$XH&B)1N%n`6*PR+lQ;|MuAwwDFsOPO_WcKeMGb5awW7QTU5pajI} z;6^(tdu8U3c~uH? zMox1c(f&41=+CHfnsFC!2nc(?d|24!YOlxZL1qz<@tPT~TDg1rj{*&L$(|!re$N=( ze4N;oXt<{%(UYtW;vSfO@ZB|+?_2)1B3D;?{2tV$2|6gp!2Q$DMhctDKERCX^;Gjh zD}=tyDcwrD(DymzEGK(_iE6w!_0t*BS9B7(Ncq6`cU|k49-etGbn762{W>c2^h~H; z6#ASj=`OmSlFYdH%x}GW)@+5O*N1Rw#4Vw(frMgeTvGI*OzBhp_iH>{WIk+fxZk{a zb?LRk@S-LeYQUmj&XNP4lgMNVjL!_vIik^KJXP3$3q3t`)C_ zNsbE&XRLZ{j`YnnU(JE_=htofPG<#6a3#IMd>>z3xAK7t3r;V=3`)X9dCI0g3+u{@ zHhrJ~{6m|5o>wzp+_$6hSl5&#zlKJw$Hy9`kTh(6 zp$!z)w2{|M(4n8o zMW%9``kxq=Idyv;aJ^Gs04^)^c%CdMG#}$?e8K1ktxvxULvO9_R2(X1||GM23$)4~<)X47-wA1s2p5~JavzOx*;az?r|GAeccXWEg7Ja2p zmMGwl`lMF)6U_5JgYEvhv$8k+lN%`mUClyYUL)&O{v(vATDv)>%UYSM;zy-1r0=Vh zEA-q(8FLu8y*tv?qmS3h!V?D?Wl@^`dQ9e@IJsEfvAEjf(Rg=ddpx$KqdO_1k8F`8 z`i+HBwI?TIVf{A)vRHq2i!9QgsF!7WonQL&uWy#=CoVO~mlS<@hb*->Cf(Vpk1vMs VS7Xwq@9mH^`U5e1JHw~;{{ws+yqW+2 diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 76c60fc..25c0a4c 100644 --- a/package.json +++ b/package.json @@ -3,22 +3,35 @@ "version": "1.0.50", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "bun run --hot src/index.ts" + "dev": "bun run --hot src/index.tsx" }, "dependencies": { "@elysiajs/cookie": "^0.8.0", "@elysiajs/html": "^1.0.2", "@elysiajs/jwt": "^1.0.2", "@elysiajs/static": "^1.0.2", - "elysia": "latest" + "elysia": "latest", + "sharp": "^0.33.4" }, - "module": "src/index.js", + "module": "src/index.tsx", "bun-create": { - "start": "bun run src/index.ts" + "start": "bun run src/index.tsx" }, "devDependencies": { + "@biomejs/biome": "1.7.3", + "@ianvs/prettier-plugin-sort-imports": "^4.2.1", + "@kitajs/ts-html-plugin": "^4.0.1", + "@total-typescript/ts-reset": "^0.5.1", "@types/bun": "^1.1.2", + "@types/eslint": "^8.56.10", "@types/node": "^20.12.12", - "bun-types": "latest" + "@types/ws": "^8.5.10", + "@typescript-eslint/eslint-plugin": "^7.9.0", + "@typescript-eslint/parser": "^7.9.0", + "bun-types": "^1.1.8", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "typescript": "^5.4.5" } } diff --git a/prettier.config.cjs b/prettier.config.cjs new file mode 100644 index 0000000..86b8f0d --- /dev/null +++ b/prettier.config.cjs @@ -0,0 +1,14 @@ +/** + * @type {import('prettier').Config & import("@ianvs/prettier-plugin-sort-imports").PluginConfig} + */ +const config = { + arrowParens: "always", + printWidth: 80, + singleQuote: false, + semi: true, + trailingComma: "all", + tabWidth: 2, + plugins: ["@ianvs/prettier-plugin-sort-imports"], +}; + +export default config; \ No newline at end of file diff --git a/reset.d.ts b/reset.d.ts new file mode 100644 index 0000000..e186b1f --- /dev/null +++ b/reset.d.ts @@ -0,0 +1 @@ +import "@total-typescript/ts-reset"; \ No newline at end of file diff --git a/src/components/base.tsx b/src/components/base.tsx new file mode 100644 index 0000000..ee0d090 --- /dev/null +++ b/src/components/base.tsx @@ -0,0 +1,13 @@ +export const BaseHtml = ({ children, title = "ConvertX" }) => ( + + + + + {title} + + + + + {children} + +); diff --git a/src/components/header.tsx b/src/components/header.tsx new file mode 100644 index 0000000..11918c2 --- /dev/null +++ b/src/components/header.tsx @@ -0,0 +1,49 @@ +export const Header = ({ loggedIn }: { loggedIn?: boolean }) => { + let rightNav: JSX.Element; + if (loggedIn) { + rightNav = ( + + ); + } else { + rightNav = ( + + ); + } + + return ( +
+ +
+ ); +}; diff --git a/src/converters/main.ts b/src/converters/main.ts new file mode 100644 index 0000000..04f3604 --- /dev/null +++ b/src/converters/main.ts @@ -0,0 +1,39 @@ +import { properties, convert } from "./sharp"; + +export async function mainConverter( + inputFilePath: string, + fileType: string, + convertTo: string, + targetPath: string, + // biome-ignore lint/suspicious/noExplicitAny: + options?: any, +) { + // Check if the fileType and convertTo are supported by the sharp converter + if (properties.from.includes(fileType) && properties.to.includes(convertTo)) { + // Use the sharp converter + try { + await convert(inputFilePath, fileType, convertTo, targetPath, options); + console.log( + `Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully.`, + ); + } catch (error) { + console.error( + `Failed to convert ${inputFilePath} from ${fileType} to ${convertTo}.`, + error, + ); + } + } else { + console.log( + `The sharp converter does not support converting from ${fileType} to ${convertTo}.`, + ); + } +} + +export function possibleConversions(fileType: string) { + // Check if the fileType is supported by the sharp converter + if (properties.from.includes(fileType)) { + return properties.to; + } + + return []; +} diff --git a/src/converters/sharp.ts b/src/converters/sharp.ts new file mode 100644 index 0000000..8f1b214 --- /dev/null +++ b/src/converters/sharp.ts @@ -0,0 +1,37 @@ +import sharp from "sharp"; + +// declare possible conversions +export const properties = { + from: ["jpeg", "png", "webp", "gif", "avif", "tiff", "svg"], + to: ["jpeg", "png", "webp", "gif", "avif", "tiff"], + options: { + svg: { + scale: { + description: "Scale the image up or down", + type: "number", + default: 1, + }, + } + } +} + +export async function convert(filePath: string, fileType: string, convertTo: string, targetPath: string, options?: any) { + if (fileType === "svg") { + const scale = options.scale || 1; + const metadata = await sharp(filePath).metadata(); + + if (!metadata || !metadata.width || !metadata.height) { + throw new Error("Could not get metadata from image"); + } + + const newWidth = Math.round(metadata.width * scale) + const newHeight = Math.round(metadata.height * scale) + + return await sharp(filePath) + .resize(newWidth, newHeight) + .toFormat(convertTo) + .toFile(targetPath); + } + + return await sharp(filePath).toFormat(convertTo).toFile(targetPath); +} \ No newline at end of file diff --git a/src/helpers/normalizeFiletype.ts b/src/helpers/normalizeFiletype.ts new file mode 100644 index 0000000..f77b996 --- /dev/null +++ b/src/helpers/normalizeFiletype.ts @@ -0,0 +1,12 @@ +export const normalizeFiletype = (filetype: string): string => { + const lowercaseFiletype = filetype.toLowerCase(); + + switch (lowercaseFiletype) { + case "jpg": + return "jpeg"; + case "htm": + return "html"; + default: + return lowercaseFiletype; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index dff0d8a..0000000 --- a/src/index.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { Elysia, t } from "elysia"; -import { staticPlugin } from "@elysiajs/static"; -import { html } from "@elysiajs/html"; -import { Database } from "bun:sqlite"; -import cookie from "@elysiajs/cookie"; -import { unlink } from "node:fs/promises"; -import { randomUUID } from "node:crypto"; -import { jwt } from "@elysiajs/jwt"; - -const db = new Database("./mydb.sqlite"); -const uploadsDir = "./uploads/"; - -// init db -db.exec(` -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - email TEXT NOT NULL, - password TEXT NOT NULL -); -CREATE TABLE IF NOT EXISTS jobs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - job_id TEXT NOT NULL, - date_created TEXT NOT NULL -);`); - -const app = new Elysia() - .use(cookie()) - .use( - jwt({ - name: "jwt", - schema: t.Object({ - id: t.String(), - }), - secret: "secret", - exp: "7d", - }), - ) - .use(html()) - .use( - staticPlugin({ - assets: "src/public/", - prefix: "/", - }), - ) - .get("/register", async () => { - return Bun.file("src/pages/register.html"); - }) - .post( - "/register", - async function handler({ body, set, jwt, cookie: { auth } }) { - const existingUser = await db - .query("SELECT * FROM users WHERE email = ?") - .get(body.email); - if (existingUser) { - set.status = 400; - return { - message: "Email already in use.", - }; - } - const savedPassword = await Bun.password.hash(body.password); - - db.run( - "INSERT INTO users (email, password) VALUES (?, ?)", - body.email, - savedPassword, - ); - - const user = await db - .query("SELECT * FROM users WHERE email = ?") - .get(body.email); - - const accessToken = await jwt.sign({ - id: String(user.id), - }); - - // set cookie - auth.set({ - value: accessToken, - httpOnly: true, - secure: true, - maxAge: 60 * 60 * 24 * 7, - sameSite: "strict", - }); - - // redirect to home - set.status = 302; - set.headers = { - Location: "/", - }; - }, - ) - .get("/login", async () => { - return Bun.file("src/pages/login.html"); - }) - .post("/login", async function handler({ body, set, jwt, cookie: { auth } }) { - const existingUser = await db - .query("SELECT * FROM users WHERE email = ?") - .get(body.email); - - if (!existingUser) { - set.status = 403; - return { - message: "Invalid credentials.", - }; - } - - const validPassword = await Bun.password.verify( - body.password, - existingUser.password, - ); - - if (!validPassword) { - set.status = 403; - return { - message: "Invalid credentials.", - }; - } - - const accessToken = await jwt.sign({ - id: String(existingUser.id), - }); - - // set cookie - // set cookie - auth.set({ - value: accessToken, - httpOnly: true, - secure: true, - maxAge: 60 * 60 * 24 * 7, - sameSite: "strict", - }); - - // redirect to home - set.status = 302; - set.headers = { - Location: "/", - }; - }) - .post("/logout", async ({ set, cookie: { auth } }) => { - auth.remove(); - set.status = 302; - set.headers = { - Location: "/login", - }; - }) - .get("/", async ({ jwt, set, cookie: { auth, jobId } }) => { - // validate jwt - const user = await jwt.verify(auth.value); - if (!user) { - // redirect to login - set.status = 302; - set.headers = { - Location: "/login", - }; - return; - } - - // make sure user exists in db - const existingUser = await db - .query("SELECT * FROM users WHERE id = ?") - .get(user.id); - - if (!existingUser) { - // redirect to login and clear cookie - auth.remove(); - set.status = 302; - set.headers = { - Location: "/login", - }; - return; - } - - // create a unique job id - jobId.set({ - value: randomUUID(), - httpOnly: true, - secure: true, - maxAge: 24 * 60 * 60, - sameSite: "strict", - }); - - // insert job id into db - db.run( - "INSERT INTO jobs (user_id, job_id, date_created) VALUES (?, ?, ?)", - user.id, - jobId.value, - new Date().toISOString(), - ); - - return Bun.file("src/pages/index.html"); - }) - .post("/upload", async ({ body, set, jwt, cookie: { auth, jobId } }) => { - // validate jwt - const user = await jwt.verify(auth.value); - if (!user) { - // redirect to login - set.status = 302; - set.headers = { - Location: "/login", - }; - return; - } - - // let filesUploaded = []; - - const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; - - if (body?.file) { - await Bun.write(`${userUploadsDir}${body.file.name}`, body.file); - // filesUploaded.push(body.file.name); - } else if (body?.files) { - if (Array.isArray(body.files)) { - for (const file of body.files) { - console.log(file); - await Bun.write(`${userUploadsDir}${file.name}`, file); - // filesUploaded.push(file.name); - } - } else { - await Bun.write(`${userUploadsDir}${body.files.name}`, body.files); - // filesUploaded.push(body.files.name); - } - } - }) - .post("/delete", async ({ body, set, jwt, cookie: { auth, jobId } }) => { - const user = await jwt.verify(auth.value); - if (!user) { - // redirect to login - set.status = 302; - set.headers = { - Location: "/login", - }; - return; - } - - const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; - - await unlink(`${userUploadsDir}${body.filename}`); - }) - .post("/convert", async (ctx) => { - console.log(ctx.body); - }) - .listen(3000); - -console.log( - `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`, -); diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..8b0bda3 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,460 @@ +import { randomUUID } from "node:crypto"; +import { mkdir, unlink } from "node:fs/promises"; +import cookie from "@elysiajs/cookie"; +import { html } from "@elysiajs/html"; +import { jwt } from "@elysiajs/jwt"; +import { staticPlugin } from "@elysiajs/static"; +import { Database } from "bun:sqlite"; +import { Elysia, t } from "elysia"; +import { BaseHtml } from "./components/base"; +import { Header } from "./components/header"; +import { mainConverter, possibleConversions } from "./converters/main"; +import { normalizeFiletype } from "./helpers/normalizeFiletype"; + +const db = new Database("./db/mydb.sqlite"); +const uploadsDir = "./uploads/"; +const outputDir = "./output/"; + +const jobs = {}; + +// init db +db.exec(` +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + password TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS jobs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + job_id TEXT NOT NULL, + date_created TEXT NOT NULL, + status TEXT DEFAULT 'pending' +);`); + +const app = new Elysia() + .use(cookie()) + .use(html()) + .use( + jwt({ + name: "jwt", + schema: t.Object({ + id: t.String(), + }), + secret: "secret", + exp: "7d", + }), + ) + .use( + staticPlugin({ + assets: "src/public/", + prefix: "/", + }), + ) + .get("/register", () => { + return ( + +
+
+
+ + + +
+
+ + ); + }) + .post( + "/register", + async function handler({ body, set, jwt, cookie: { auth } }) { + const existingUser = await db + .query("SELECT * FROM users WHERE email = ?") + .get(body.email); + if (existingUser) { + set.status = 400; + return { + message: "Email already in use.", + }; + } + const savedPassword = await Bun.password.hash(body.password); + + db.run( + "INSERT INTO users (email, password) VALUES (?, ?)", + body.email, + savedPassword, + ); + + const user = await db + .query("SELECT * FROM users WHERE email = ?") + .get(body.email); + + const accessToken = await jwt.sign({ + id: String(user.id), + }); + + // set cookie + auth.set({ + value: accessToken, + httpOnly: true, + secure: true, + maxAge: 60 * 60 * 24 * 7, + sameSite: "strict", + }); + + // redirect to home + set.status = 302; + set.headers = { + Location: "/", + }; + }, + ) + .get("/login", () => { + return ( + +
+
+
+ + + +
+
+ + ); + }) + .post("/login", async function handler({ body, set, jwt, cookie: { auth } }) { + const existingUser = await db + .query("SELECT * FROM users WHERE email = ?") + .get(body.email); + + if (!existingUser) { + set.status = 403; + return { + message: "Invalid credentials.", + }; + } + + const validPassword = await Bun.password.verify( + body.password, + existingUser.password, + ); + + if (!validPassword) { + set.status = 403; + return { + message: "Invalid credentials.", + }; + } + + const accessToken = await jwt.sign({ + id: String(existingUser.id), + }); + + // set cookie + // set cookie + auth.set({ + value: accessToken, + httpOnly: true, + secure: true, + maxAge: 60 * 60 * 24 * 7, + sameSite: "strict", + }); + + // redirect to home + set.status = 302; + set.headers = { + Location: "/", + }; + }) + .get("/logout", ({ redirect, cookie: { auth } }) => { + if (auth?.value) { + auth.remove(); + } + return redirect("/login"); + }) + .post("/logout", ({ redirect, cookie: { auth } }) => { + if (auth?.value) { + auth.remove(); + } + + return redirect("/login"); + }) + .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { + // validate jwt + const user = await jwt.verify(auth.value); + if (!user) { + return redirect("/login"); + } + + // make sure user exists in db + const existingUser = await db + .query("SELECT * FROM users WHERE id = ?") + .get(user.id); + + if (!existingUser) { + if (auth?.value) { + auth.remove(); + } + return redirect("/login"); + } + + // create a unique job id + jobId.set({ + value: randomUUID(), + httpOnly: true, + secure: true, + maxAge: 24 * 60 * 60, + sameSite: "strict", + }); + + // insert job id into db + db.run( + "INSERT INTO jobs (user_id, job_id, date_created) VALUES (?, ?, ?)", + user.id, + jobId.value, + new Date().toISOString(), + ); + + return ( + +
+
+
+ + + + + +
+ +
+ + + + - - - -
- -
- -
- - - - -
-
-
- - -
- -
-
- -
- - - - -
-
- - - \ No newline at end of file diff --git a/src/pages/login.html b/src/pages/login.html deleted file mode 100644 index 78584b3..0000000 --- a/src/pages/login.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - ConvertX | Login - - - - - -
- -
- -
-
- - - -
- -
- - - - \ No newline at end of file diff --git a/src/public/script.js b/src/public/script.js index 9c9eca0..f330dbb 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -1,7 +1,7 @@ // Select the file input element const fileInput = document.querySelector('input[type="file"]'); +const fileNames = []; -const filesToUpload = []; // Add a 'change' event listener to the file input element fileInput.addEventListener("change", (e) => { @@ -13,17 +13,20 @@ fileInput.addEventListener("change", (e) => { const fileList = document.querySelector("#file-list"); // Loop through the selected files - for (let i = 0; i < files.length; i++) { + for (const file of files) { // Create a new table row for each file const row = document.createElement("tr"); row.innerHTML = ` - ${files[i].name} - ${(files[i].size / 1024 / 1024).toFixed(2)} MB + ${file.name} + ${(file.size / 1024 / 1024).toFixed(2)} MB `; // Append the row to the file-list table fileList.appendChild(row); + + // Append the file to the hidden input + fileNames.push(file.name); } uploadFiles(files); @@ -35,6 +38,10 @@ const deleteRow = (target) => { const row = target.parentElement.parentElement; row.remove(); + // remove from fileNames + const index = fileNames.indexOf(filename); + fileNames.splice(index, 1); + fetch("/delete", { method: "POST", body: JSON.stringify({ filename: filename }), @@ -52,8 +59,8 @@ const deleteRow = (target) => { const uploadFiles = (files) => { const formData = new FormData(); - for (let i = 0; i < files.length; i++) { - formData.append("files", files[i], files[i].name); + for (const file of files) { + formData.append("file", file, file.name); } fetch("/upload", { @@ -66,3 +73,11 @@ const uploadFiles = (files) => { }) .catch((err) => console.log(err)); }; + + +const formConvert = document.querySelector("form[action='/convert']"); + +formConvert.addEventListener("submit", (e) => { + const hiddenInput = document.querySelector("input[name='file_names']"); + hiddenInput.value = JSON.stringify(fileNames); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1ca2350..98d81d6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,103 +1,33 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "ES2022", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react", + "jsxFactory": "Html.createElement", + "jsxFragmentFactory": "Html.Fragment", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ], + // non bun init + // "plugins": [{ "name": "@kitajs/ts-html-plugin" }], + "noUncheckedIndexedAccess": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true + // "noImplicitReturns": true } -} +} \ No newline at end of file