From e89a372152831913b28284c5ba7ec0e2a2edbe47 Mon Sep 17 00:00:00 2001 From: Rokojori Date: Mon, 13 Jul 2026 14:45:06 +0200 Subject: [PATCH] auth: replace local session auth with rokojori-auth JWT integration Removes express-session, bcryptjs, local user system, login/register pages, and email module. Adds JWT verification middleware (cookie + Bearer header), cookie-parser, and redirects login/logout to account.rokojori.com. Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 260 +++++++++++------- package.json | 8 +- source/components/app-nav/app-nav.ts | 50 ++-- source/components/login-form/login-form.css | 58 ---- source/components/login-form/login-form.ts | 31 --- .../register-form/register-form.css | 58 ---- .../components/register-form/register-form.ts | 31 --- source/pages/login.html | 13 - source/pages/register.html | 13 - source/server/db.ts | 23 -- source/server/email/EmailSender.ts | 4 - source/server/email/EmailService.ts | 20 -- source/server/email/SMTPEmailSender.ts | 45 --- source/server/index.ts | 23 +- source/server/middleware/auth.ts | 57 +++- source/server/routes/auth.ts | 47 ---- source/server/routes/layout.ts | 4 +- source/server/routes/projects.ts | 2 +- source/server/sessionStore.ts | 66 ----- 19 files changed, 260 insertions(+), 553 deletions(-) delete mode 100644 source/components/login-form/login-form.css delete mode 100644 source/components/login-form/login-form.ts delete mode 100644 source/components/register-form/register-form.css delete mode 100644 source/components/register-form/register-form.ts delete mode 100644 source/pages/login.html delete mode 100644 source/pages/register.html delete mode 100644 source/server/email/EmailSender.ts delete mode 100644 source/server/email/EmailService.ts delete mode 100644 source/server/email/SMTPEmailSender.ts delete mode 100644 source/server/routes/auth.ts delete mode 100644 source/server/sessionStore.ts diff --git a/package-lock.json b/package-lock.json index 4758230..9ff3cca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,16 @@ "dependencies": { "@langchain/core": "^1.2.2", "@langchain/openai": "^1.5.4", - "bcryptjs": "^2.4.3", + "@types/cookie-parser": "^1.4.10", + "@types/jsonwebtoken": "^9.0.10", + "cookie-parser": "^1.4.7", "express": "^4.18.2", - "express-session": "^1.17.3", + "jsonwebtoken": "^9.0.3", "markdown-it": "^14.3.0", "nodemailer": "^9.0.3" }, "devDependencies": { - "@types/bcryptjs": "^2.4.6", "@types/express": "^4.17.21", - "@types/express-session": "^1.17.10", "@types/node": "^20.11.0", "@types/nodemailer": "^8.0.1", "ts-node": "^10.9.2", @@ -143,18 +143,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -165,18 +157,26 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -188,7 +188,6 @@ "version": "4.19.8", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -197,35 +196,38 @@ "@types/send": "*" } }, - "node_modules/@types/express-session": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.19.0.tgz", - "integrity": "sha512-GbypG0bog68UbOq2tSAp7SclvCUm3ha1uDi58OPRGK1NfRvCIu7Gz0M7fTGtpNG1T9a29GpuurQj9zEcT/lMXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -246,21 +248,18 @@ "version": "6.15.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -270,7 +269,6 @@ "version": "1.15.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -282,7 +280,6 @@ "version": "0.17.6", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -367,12 +364,6 @@ ], "license": "MIT" }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "license": "MIT" - }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -397,6 +388,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -465,6 +462,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", @@ -530,6 +546,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -654,29 +679,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express-session": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", - "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", - "license": "MIT", - "dependencies": { - "cookie": "~0.7.2", - "cookie-signature": "~1.0.7", - "debug": "~2.6.9", - "depd": "~2.0.0", - "on-headers": "~1.1.0", - "parseurl": "~1.3.3", - "safe-buffer": "~5.2.1", - "uid-safe": "~2.1.5" - }, - "engines": { - "node": ">= 0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -851,6 +853,55 @@ "base64-js": "^1.5.1" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/langsmith": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.8.0.tgz", @@ -903,6 +954,48 @@ "uc.micro": "^2.0.0" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1069,15 +1162,6 @@ "node": ">= 0.8" } }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/openai": { "version": "6.45.0", "resolved": "https://registry.npmjs.org/openai/-/openai-6.45.0.tgz", @@ -1199,15 +1283,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1258,6 +1333,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -1477,23 +1564,10 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, - "node_modules/uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "license": "MIT", - "dependencies": { - "random-bytes": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { diff --git a/package.json b/package.json index a9eb6de..9089f21 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,16 @@ "dependencies": { "@langchain/core": "^1.2.2", "@langchain/openai": "^1.5.4", - "bcryptjs": "^2.4.3", + "@types/cookie-parser": "^1.4.10", + "@types/jsonwebtoken": "^9.0.10", + "cookie-parser": "^1.4.7", "express": "^4.18.2", - "express-session": "^1.17.3", + "jsonwebtoken": "^9.0.3", "markdown-it": "^14.3.0", "nodemailer": "^9.0.3" }, "devDependencies": { - "@types/bcryptjs": "^2.4.6", "@types/express": "^4.17.21", - "@types/express-session": "^1.17.10", "@types/node": "^20.11.0", "@types/nodemailer": "^8.0.1", "ts-node": "^10.9.2", diff --git a/source/components/app-nav/app-nav.ts b/source/components/app-nav/app-nav.ts index 5d9990e..3953df9 100644 --- a/source/components/app-nav/app-nav.ts +++ b/source/components/app-nav/app-nav.ts @@ -1,13 +1,33 @@ -interface AuthUser { - id: number; - username: string; +const AUTH_HOST = 'https://account.rokojori.com'; +const APP_URL = 'https://roject.rokojori.com'; + +interface JwtUser { + userId: string; + email: string; + roles: string[]; + products: string[]; } class AppNav extends HTMLElement { async connectedCallback(): Promise { - const res = await fetch('/api/auth/me'); - if (!res.ok) { location.href = '/login.html'; return; } - const user = await res.json() as AuthUser; + const res = await fetch( '/api/auth/me' ); + + if ( !res.ok ) { + const loginHref = `${AUTH_HOST}/login.html?redirect=${encodeURIComponent( APP_URL )}`; + this.innerHTML = ` + + `; + return; + } + + const user = await res.json() as JwtUser; + const logoutHref = `${AUTH_HOST}/api/auth/logout?redirect=${encodeURIComponent( APP_URL )}`; this.innerHTML = ` `; - - this.querySelector('.btn-logout')!.addEventListener('click', async () => { - await fetch('/api/auth/logout', { method: 'POST' }); - location.href = '/login.html'; - }); - - this.querySelector('.btn-delete')!.addEventListener('click', async () => { - if (!confirm('Delete your account? This cannot be undone.')) return; - await fetch('/api/auth/me', { method: 'DELETE' }); - location.href = '/login.html'; - }); } } -customElements.define('app-nav', AppNav); +customElements.define( 'app-nav', AppNav ); diff --git a/source/components/login-form/login-form.css b/source/components/login-form/login-form.css deleted file mode 100644 index ba387aa..0000000 --- a/source/components/login-form/login-form.css +++ /dev/null @@ -1,58 +0,0 @@ -login-form { - display: block; - max-width: 360px; - margin: 80px auto; - padding: 2rem; - border: 1px solid #ddd; - border-radius: 6px; -} - -login-form form { - display: flex; - flex-direction: column; - gap: 1rem; -} - -login-form h2 { - margin: 0; -} - -login-form label { - display: flex; - flex-direction: column; - gap: 4px; - font-size: 0.9rem; -} - -login-form input { - padding: 0.5rem; - border: 1px solid #ccc; - border-radius: 4px; - font-size: 1rem; -} - -login-form button { - padding: 0.6rem; - background: #222; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; -} - -login-form button:hover { - background: #444; -} - -login-form .error { - color: #c00; - font-size: 0.85rem; - min-height: 1em; -} - -login-form a { - text-align: center; - font-size: 0.9rem; - color: #555; -} diff --git a/source/components/login-form/login-form.ts b/source/components/login-form/login-form.ts deleted file mode 100644 index 73eee63..0000000 --- a/source/components/login-form/login-form.ts +++ /dev/null @@ -1,31 +0,0 @@ -class LoginForm extends HTMLElement { - connectedCallback(): void { - this.innerHTML = ` -
-

Login

-

- - - - Create account -
- `; - this.querySelector('form')!.addEventListener('submit', async (e: Event) => { - e.preventDefault(); - const form = e.target as HTMLFormElement; - const data = Object.fromEntries(new FormData(form)); - const res = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }); - if (res.ok) { - location.href = '/dashboard.html'; - } else { - const err = await res.json() as { error: string }; - this.querySelector('.error')!.textContent = err.error; - } - }); - } -} -customElements.define('login-form', LoginForm); diff --git a/source/components/register-form/register-form.css b/source/components/register-form/register-form.css deleted file mode 100644 index b18346b..0000000 --- a/source/components/register-form/register-form.css +++ /dev/null @@ -1,58 +0,0 @@ -register-form { - display: block; - max-width: 360px; - margin: 80px auto; - padding: 2rem; - border: 1px solid #ddd; - border-radius: 6px; -} - -register-form form { - display: flex; - flex-direction: column; - gap: 1rem; -} - -register-form h2 { - margin: 0; -} - -register-form label { - display: flex; - flex-direction: column; - gap: 4px; - font-size: 0.9rem; -} - -register-form input { - padding: 0.5rem; - border: 1px solid #ccc; - border-radius: 4px; - font-size: 1rem; -} - -register-form button { - padding: 0.6rem; - background: #222; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; -} - -register-form button:hover { - background: #444; -} - -register-form .error { - color: #c00; - font-size: 0.85rem; - min-height: 1em; -} - -register-form a { - text-align: center; - font-size: 0.9rem; - color: #555; -} diff --git a/source/components/register-form/register-form.ts b/source/components/register-form/register-form.ts deleted file mode 100644 index 20e63d1..0000000 --- a/source/components/register-form/register-form.ts +++ /dev/null @@ -1,31 +0,0 @@ -class RegisterForm extends HTMLElement { - connectedCallback(): void { - this.innerHTML = ` -
-

Create Account

-

- - - - Already have an account? -
- `; - this.querySelector('form')!.addEventListener('submit', async (e: Event) => { - e.preventDefault(); - const form = e.target as HTMLFormElement; - const data = Object.fromEntries(new FormData(form)); - const res = await fetch('/api/auth/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }); - if (res.ok) { - location.href = '/dashboard.html'; - } else { - const err = await res.json() as { error: string }; - this.querySelector('.error')!.textContent = err.error; - } - }); - } -} -customElements.define('register-form', RegisterForm); diff --git a/source/pages/login.html b/source/pages/login.html deleted file mode 100644 index 6542553..0000000 --- a/source/pages/login.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Login — Roject - - - - - - - diff --git a/source/pages/register.html b/source/pages/register.html deleted file mode 100644 index c316b8c..0000000 --- a/source/pages/register.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Register — Roject - - - - - - - diff --git a/source/server/db.ts b/source/server/db.ts index b8e5e96..a447f7a 100644 --- a/source/server/db.ts +++ b/source/server/db.ts @@ -5,13 +5,6 @@ import { randomUUID } from 'crypto'; const DATA_DIR = path.join(__dirname, '..', '..', 'build', 'data', 'db'); if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); -export interface User { - id: string; - username: string; - password: string; - created_at: string; -} - export interface Group { id: string; name: string; @@ -50,22 +43,6 @@ function write(table: string, rows: T[]): void { fs.writeFileSync(path.join(DATA_DIR, `${table}.json`), JSON.stringify(rows, null, 2)); } -export const users = { - all: (): User[] => read('users'), - findById: (id: string): User | undefined => read('users').find(u => u.id === id), - findByUsername: (name: string): User | undefined => read('users').find(u => u.username === name), - create(data: Omit): User { - const rows = read('users'); - if (rows.find(u => u.username === data.username)) throw new Error('Taken'); - const user: User = { id: randomUUID(), ...data, created_at: new Date().toISOString() }; - write('users', [...rows, user]); - return user; - }, - delete(id: string): void { - write('users', read('users').filter(u => u.id !== id)); - } -}; - export const groups = { all: (): Group[] => read('groups'), findById: (id: string): Group | undefined => read('groups').find(g => g.id === id), diff --git a/source/server/email/EmailSender.ts b/source/server/email/EmailSender.ts deleted file mode 100644 index 01e5f14..0000000 --- a/source/server/email/EmailSender.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface EmailSender -{ - sendEmail( to: string, subject: string, body: string ): Promise; -} diff --git a/source/server/email/EmailService.ts b/source/server/email/EmailService.ts deleted file mode 100644 index d3bfb21..0000000 --- a/source/server/email/EmailService.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { EmailSender } from './EmailSender'; -import { SMTPEmailSender } from './SMTPEmailSender'; - -export class EmailService -{ - private static sender: EmailSender = new SMTPEmailSender( - { - host: process.env.SMTP_HOST ?? '', - port: Number( process.env.SMTP_PORT ?? 587 ), - secure: process.env.SMTP_SECURE === 'true', - user: process.env.SMTP_USER ?? '', - pass: process.env.SMTP_PASS ?? '', - from: process.env.SMTP_FROM ?? '' - } ); - - static async sendEmail( to: string, subject: string, body: string ): Promise - { - return EmailService.sender.sendEmail( to, subject, body ); - } -} diff --git a/source/server/email/SMTPEmailSender.ts b/source/server/email/SMTPEmailSender.ts deleted file mode 100644 index f907a9e..0000000 --- a/source/server/email/SMTPEmailSender.ts +++ /dev/null @@ -1,45 +0,0 @@ -import nodemailer from 'nodemailer'; -import { EmailSender } from './EmailSender'; - -export interface SMTPConfig -{ - host: string; - port: number; - secure: boolean; - user: string; - pass: string; - from: string; -} - -export class SMTPEmailSender implements EmailSender -{ - private transporter: nodemailer.Transporter; - private from: string; - - constructor( config: SMTPConfig ) - { - this.from = config.from; - this.transporter = nodemailer.createTransport( - { - host: config.host, - port: config.port, - secure: config.secure, - auth: - { - user: config.user, - pass: config.pass - } - } ); - } - - async sendEmail( to: string, subject: string, body: string ): Promise - { - await this.transporter.sendMail( - { - from: this.from, - to, - subject, - text: body - } ); - } -} diff --git a/source/server/index.ts b/source/server/index.ts index 662cef6..5499352 100644 --- a/source/server/index.ts +++ b/source/server/index.ts @@ -1,8 +1,7 @@ import express from 'express'; -import session from 'express-session'; +import cookieParser from 'cookie-parser'; import path from 'path'; -import { JsonSessionStore } from './sessionStore'; -import authRouter from './routes/auth'; +import { jwtMiddleware, requireAuth } from './middleware/auth'; import groupsRouter from './routes/groups'; import projectsRouter from './routes/projects'; import filesRouter from './routes/files'; @@ -17,18 +16,11 @@ const app = express(); app.use( express.json() ); app.use( express.text( { type: 'text/plain' } ) ); -app.use( session( -{ - store: new JsonSessionStore( path.join( __dirname, '..', '..', 'build', 'data', 'storage', 'sessions' ) ), - secret: 'roject-secret-key', - resave: false, - saveUninitialized: false, - cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } -} ) ); +app.use( cookieParser() ); +app.use( jwtMiddleware ); app.use( express.static( path.join( __dirname, '..', '..', 'build', 'app' ) ) ); -app.use( '/api/auth', authRouter ); app.use( '/api/groups', groupsRouter ); app.use( '/api/projects', projectsRouter ); app.use( '/api/files', filesRouter ); @@ -36,10 +28,9 @@ app.use( '/api/locales', localesRouter ); app.use( '/api/layout', layoutRouter ); app.use( '/api/rojos', rojosRouter ); -app.get( '/', ( req, res ) => -{ - res.redirect( req.session.userId ? '/dashboard.html' : '/login.html' ); -} ); +app.get( '/api/auth/me', requireAuth, ( req, res ) => res.json( req.user ) ); + +app.get( '/', ( _req, res ) => res.redirect( '/dashboard.html' ) ); const PORT = process.env.PORT ? Number( process.env.PORT ) : 3000; app.listen( PORT, () => console.log( `Roject running on http://localhost:${PORT}` ) ); diff --git a/source/server/middleware/auth.ts b/source/server/middleware/auth.ts index 478ec84..fd7a9c6 100644 --- a/source/server/middleware/auth.ts +++ b/source/server/middleware/auth.ts @@ -1,15 +1,58 @@ import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; -declare module 'express-session' { - interface SessionData { - userId?: string; - username?: string; +const AUTH_HOST = process.env.AUTH_HOST ?? 'https://account.rokojori.com'; +const JWT_SECRET = process.env.JWT_SECRET ?? ''; + +export interface JwtUser { + userId: string; + email: string; + roles: string[]; + products: string[]; + settings: Record; +} + +declare global { + namespace Express { + interface Request { + user?: JwtUser; + } } } -export function requireAuth(req: Request, res: Response, next: NextFunction): void { - if (!req.session.userId) { - res.status(401).json({ error: 'Not authenticated' }); +function extractToken( req: Request ): string | undefined { + const cookie = req.cookies?.accessToken as string | undefined; + if ( cookie ) return cookie; + const header = req.headers.authorization; + if ( header?.startsWith( 'Bearer ' ) ) return header.slice( 7 ); + return undefined; +} + +function isApiRequest( req: Request ): boolean { + return req.path.startsWith( '/api/' ); +} + +export function jwtMiddleware( req: Request, res: Response, next: NextFunction ): void { + const token = extractToken( req ); + + if ( !token ) { next(); return; } + + try { + req.user = jwt.verify( token, JWT_SECRET ) as JwtUser; + next(); + } catch ( err: unknown ) { + if ( err instanceof jwt.TokenExpiredError && !isApiRequest( req ) ) { + const redirect = encodeURIComponent( req.protocol + '://' + req.get( 'host' ) + req.originalUrl ); + res.redirect( `${AUTH_HOST}/api/auth/refresh-session?redirect=${redirect}` ); + } else { + next(); + } + } +} + +export function requireAuth( req: Request, res: Response, next: NextFunction ): void { + if ( !req.user ) { + res.status( 401 ).json( { error: 'Not authenticated' } ); return; } next(); diff --git a/source/server/routes/auth.ts b/source/server/routes/auth.ts deleted file mode 100644 index 1a56350..0000000 --- a/source/server/routes/auth.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Router } from 'express'; -import bcrypt from 'bcryptjs'; -import { users } from '../db'; -import { requireAuth } from '../middleware/auth'; - -const router = Router(); - -router.post('/register', async (req, res) => { - const { username, password } = req.body as { username?: string; password?: string }; - if (!username || !password) { res.status(400).json({ error: 'Username and password required' }); return; } - try { - const hash = await bcrypt.hash(password, 10); - const user = users.create({ username, password: hash }); - req.session.userId = user.id; - req.session.username = user.username; - res.json({ id: user.id, username: user.username }); - } catch { - res.status(409).json({ error: 'Username already taken' }); - } -}); - -router.post('/login', async (req, res) => { - const { username, password } = req.body as { username?: string; password?: string }; - const user = username ? users.findByUsername(username) : undefined; - if (!user || !password || !(await bcrypt.compare(password, user.password))) { - res.status(401).json({ error: 'Invalid credentials' }); - return; - } - req.session.userId = user.id; - req.session.username = user.username; - res.json({ id: user.id, username: user.username }); -}); - -router.post('/logout', (req, res) => { - req.session.destroy(() => res.json({ ok: true })); -}); - -router.delete('/me', requireAuth, (req, res) => { - users.delete(req.session.userId!); - req.session.destroy(() => res.json({ ok: true })); -}); - -router.get('/me', requireAuth, (req, res) => { - res.json({ id: req.session.userId, username: req.session.username }); -}); - -export default router; diff --git a/source/server/routes/layout.ts b/source/server/routes/layout.ts index 3c53b95..7f51a60 100644 --- a/source/server/routes/layout.ts +++ b/source/server/routes/layout.ts @@ -22,7 +22,7 @@ router.get( '/', ( req, res ) => { const deviceId = req.query.deviceId as string; if ( !deviceId ) { res.json( null ); return; } - const fp = layoutFilePath( req.session.userId!, deviceId ); + const fp = layoutFilePath( req.user!.userId, deviceId ); if ( !fs.existsSync( fp ) ) { res.json( null ); return; } try { res.json( JSON.parse( fs.readFileSync( fp, 'utf8' ) ) ); } catch { res.json( null ); } @@ -32,7 +32,7 @@ router.put( '/', ( req, res ) => { const deviceId = req.query.deviceId as string; if ( !deviceId ) { res.status( 400 ).json( { error: 'Missing deviceId' } ); return; } - const fp = layoutFilePath( req.session.userId!, deviceId ); + const fp = layoutFilePath( req.user!.userId, deviceId ); try { fs.writeFileSync( fp, JSON.stringify( req.body ), 'utf8' ); diff --git a/source/server/routes/projects.ts b/source/server/routes/projects.ts index 9343aad..149e718 100644 --- a/source/server/routes/projects.ts +++ b/source/server/routes/projects.ts @@ -12,7 +12,7 @@ router.get('/', (_req, res) => res.json(projects.all())); router.post('/', (req, res) => { const { name } = req.body as { name?: string }; if (!name) { res.status(400).json({ error: 'Name required' }); return; } - const project = projects.create({ name, owner_id: req.session.userId! }); + const project = projects.create({ name, owner_id: req.user!.userId }); createProjectStorage(project.id); res.json(project); }); diff --git a/source/server/sessionStore.ts b/source/server/sessionStore.ts deleted file mode 100644 index 98fcf0e..0000000 --- a/source/server/sessionStore.ts +++ /dev/null @@ -1,66 +0,0 @@ -import session from 'express-session'; -import fs from 'fs'; -import path from 'path'; - -interface SessionFile -{ - session: session.SessionData; - expires: number; -} - -export class JsonSessionStore extends session.Store -{ - _dir: string; - - constructor( dir: string ) - { - super(); - this._dir = dir; - if ( ! fs.existsSync( dir ) ) fs.mkdirSync( dir, { recursive: true } ); - } - - _filePath( sid: string ): string - { - const safe = sid.replace( /[^a-zA-Z0-9_-]/g, '_' ); - return path.join( this._dir, safe + '.json' ); - } - - get( sid: string, callback: ( err: any, session?: session.SessionData ) => void ): void - { - try - { - const fp = this._filePath( sid ); - if ( ! fs.existsSync( fp ) ) { callback( null, null ); return; } - const data: SessionFile = JSON.parse( fs.readFileSync( fp, 'utf8' ) ); - if ( Date.now() > data.expires ) { fs.unlinkSync( fp ); callback( null, null ); return; } - callback( null, data.session ); - } - catch ( e ) { callback( e ); } - } - - set( sid: string, sessionData: session.SessionData, callback?: ( err?: any ) => void ): void - { - try - { - const fp = this._filePath( sid ); - const expires = sessionData.cookie?.expires - ? new Date( sessionData.cookie.expires ).getTime() - : Date.now() + 7 * 24 * 60 * 60 * 1000; - const file: SessionFile = { session: sessionData, expires }; - fs.writeFileSync( fp, JSON.stringify( file ), 'utf8' ); - callback?.(); - } - catch ( e ) { callback?.( e ); } - } - - destroy( sid: string, callback?: ( err?: any ) => void ): void - { - try - { - const fp = this._filePath( sid ); - if ( fs.existsSync( fp ) ) fs.unlinkSync( fp ); - callback?.(); - } - catch ( e ) { callback?.( e ); } - } -}