From 7f0a7b1d913ef1b93eeda9c8f0ea3dd69235f6d3 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Mon, 1 Jun 2026 16:37:23 -0500 Subject: [PATCH] add 2MFA --- Overlord-Server/bun.lock | 53 +++++++ Overlord-Server/package.json | 1 + Overlord-Server/public/assets/login.js | 21 ++- Overlord-Server/public/assets/settings.js | 128 +++++++++++++++ Overlord-Server/public/login.html | 16 ++ Overlord-Server/public/settings.html | 90 +++++++++++ Overlord-Server/src/auth.test.ts | 60 +++++++ Overlord-Server/src/config.ts | 14 ++ Overlord-Server/src/db/user-schema.ts | 8 + Overlord-Server/src/mfa.test.ts | 30 ++++ Overlord-Server/src/mfa.ts | 121 +++++++++++++++ Overlord-Server/src/qr.test.ts | 20 +++ Overlord-Server/src/qr.ts | 22 +++ Overlord-Server/src/security-settings.test.ts | 6 +- .../src/server/routes/auth-routes.ts | 146 +++++++++++++++++- .../src/server/routes/misc-routes.ts | 14 +- Overlord-Server/src/users.ts | 70 ++++++++- 17 files changed, 815 insertions(+), 5 deletions(-) create mode 100644 Overlord-Server/src/mfa.test.ts create mode 100644 Overlord-Server/src/mfa.ts create mode 100644 Overlord-Server/src/qr.test.ts create mode 100644 Overlord-Server/src/qr.ts diff --git a/Overlord-Server/bun.lock b/Overlord-Server/bun.lock index 05f624b..86f2fba 100644 --- a/Overlord-Server/bun.lock +++ b/Overlord-Server/bun.lock @@ -12,6 +12,7 @@ "geoip-lite": "latest", "jose": "^6.1.3", "msgpackr": "1.11.8", + "qrcode": "^1.5.4", "uuid": "^9.0.1", "web-push": "^3.6.7", "winston": "^3.11.0", @@ -197,6 +198,8 @@ "animejs": ["animejs@3.2.2", "", {}, "sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "ansi-to-html": ["ansi-to-html@0.7.2", "", { "dependencies": { "entities": "^2.2.0" }, "bin": { "ansi-to-html": "bin/ansi-to-html" } }, "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g=="], @@ -235,6 +238,8 @@ "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -243,6 +248,8 @@ "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], + "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + "codemirror": ["codemirror@5.65.16", "", {}, "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg=="], "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], @@ -257,10 +264,14 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], @@ -269,6 +280,8 @@ "electron-to-chromium": ["electron-to-chromium@1.5.321", "", {}, "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], @@ -289,6 +302,8 @@ "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "flag-icons": ["flag-icons@7.5.0", "", {}, "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg=="], "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], @@ -297,6 +312,8 @@ "geoip-lite": ["geoip-lite@2.0.1", "", { "dependencies": { "chalk": "4.1 - 4.1.2", "iconv-lite": "0.4.13 - 0.6.3", "ip-address": "5.8.9 - 5.9.4", "lazy": "1.0.11", "yauzl": "^3.2.1" } }, "sha512-cR9E28nu1a6dsvzB1tANhdmCyXWV1L4AiSCT9alHLIUl06599EGu33mqY99ieU0twQob0kfcDQ/sAUBvHb7swA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -321,6 +338,8 @@ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -371,6 +390,8 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], @@ -411,24 +432,40 @@ "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "rpc-websockets": ["rpc-websockets@9.3.7", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^10.0.0", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^11.0.0", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^6.0.0" } }, "sha512-dQal1U0yKH2umW0DgqSecP4G1jNxyPUGY60uUMB8bLoXabC2aWT3Cag9hOhZXsH/52QJEcggxNNWhF+Fp48ykw=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -437,6 +474,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -451,8 +490,12 @@ "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -493,12 +536,22 @@ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="], "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + + "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "yauzl": ["yauzl@3.2.1", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A=="], "@solana/errors/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], diff --git a/Overlord-Server/package.json b/Overlord-Server/package.json index a72060f..2210726 100644 --- a/Overlord-Server/package.json +++ b/Overlord-Server/package.json @@ -23,6 +23,7 @@ "geoip-lite": "latest", "jose": "^6.1.3", "msgpackr": "1.11.8", + "qrcode": "^1.5.4", "uuid": "^9.0.1", "web-push": "^3.6.7", "winston": "^3.11.0" diff --git a/Overlord-Server/public/assets/login.js b/Overlord-Server/public/assets/login.js index 8f8a557..6caff3f 100644 --- a/Overlord-Server/public/assets/login.js +++ b/Overlord-Server/public/assets/login.js @@ -1,6 +1,8 @@ const form = document.getElementById("login-form"); const user = document.getElementById("user"); const pass = document.getElementById("pass"); +const mfaGroup = document.getElementById("mfa-group"); +const mfaCode = document.getElementById("mfa-code"); const errEl = document.getElementById("error"); (async () => { @@ -218,9 +220,26 @@ form?.addEventListener("submit", async (e) => { const res = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ user: user.value, pass: pass.value }), + body: JSON.stringify({ + user: user.value, + pass: pass.value, + mfaCode: mfaCode?.value || "", + }), }); + if (res.status === 202) { + const data = await res.json().catch(() => ({})); + if (data.mfaRequired) { + if (mfaGroup) mfaGroup.style.display = ""; + if (mfaCode) { + mfaCode.required = true; + mfaCode.focus(); + } + errEl.textContent = "Enter your authenticator code to continue."; + return; + } + } + if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Invalid credentials"); diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js index 39dfbc8..33d6bec 100644 --- a/Overlord-Server/public/assets/settings.js +++ b/Overlord-Server/public/assets/settings.js @@ -19,6 +19,19 @@ const currentPasswordInput = document.getElementById("current-password"); const newPasswordInput = document.getElementById("new-password"); const confirmPasswordInput = document.getElementById("confirm-password"); const passwordPolicyHint = document.getElementById("password-policy-hint"); +const mfaStatusText = document.getElementById("mfa-status-text"); +const mfaStartBtn = document.getElementById("mfa-start-btn"); +const mfaSetupPanel = document.getElementById("mfa-setup-panel"); +const mfaQrCode = document.getElementById("mfa-qr-code"); +const mfaSecretText = document.getElementById("mfa-secret-text"); +const mfaEnableCodeInput = document.getElementById("mfa-enable-code"); +const mfaEnableBtn = document.getElementById("mfa-enable-btn"); +const mfaOtpauthLink = document.getElementById("mfa-otpauth-link"); +const mfaDisablePanel = document.getElementById("mfa-disable-panel"); +const mfaDisablePasswordInput = document.getElementById("mfa-disable-password"); +const mfaDisableCodeInput = document.getElementById("mfa-disable-code"); +const mfaDisableBtn = document.getElementById("mfa-disable-btn"); +const mfaMessage = document.getElementById("mfa-message"); const prefsForm = document.getElementById("prefs-form"); const prefNotificationsInput = document.getElementById("pref-notifications"); @@ -45,6 +58,8 @@ const securityRequireUppercaseInput = document.getElementById("security-require- const securityRequireLowercaseInput = document.getElementById("security-require-lowercase"); const securityRequireNumberInput = document.getElementById("security-require-number"); const securityRequireSymbolInput = document.getElementById("security-require-symbol"); +const securityMfaAdminsInput = document.getElementById("security-mfa-admins"); +const securityMfaNonAdminsInput = document.getElementById("security-mfa-non-admins"); const tlsForm = document.getElementById("tls-form"); const tlsPermissionNote = document.getElementById("tls-permission-note"); @@ -184,6 +199,8 @@ function setSecurityFormDisabled(disabled) { securityRequireLowercaseInput, securityRequireNumberInput, securityRequireSymbolInput, + securityMfaAdminsInput, + securityMfaNonAdminsInput, securitySaveBtn, ]; @@ -231,6 +248,8 @@ function applySecurityForm() { securityRequireLowercaseInput.checked = Boolean(securityConfig.passwordRequireLowercase); securityRequireNumberInput.checked = Boolean(securityConfig.passwordRequireNumber); securityRequireSymbolInput.checked = Boolean(securityConfig.passwordRequireSymbol); + if (securityMfaAdminsInput) securityMfaAdminsInput.checked = Boolean(securityConfig.mfaRequiredForAdmins); + if (securityMfaNonAdminsInput) securityMfaNonAdminsInput.checked = Boolean(securityConfig.mfaRequiredForNonAdmins); updatePasswordPolicyUi(); } @@ -246,6 +265,8 @@ async function loadSecurityPolicy() { passwordRequireLowercase: false, passwordRequireNumber: false, passwordRequireSymbol: false, + mfaRequiredForAdmins: false, + mfaRequiredForNonAdmins: false, }; updatePasswordPolicyUi(); return; @@ -393,6 +414,107 @@ async function updatePassword(event) { showMessage("Password updated successfully."); } +function showMfaMessage(text, type = "ok") { + if (!mfaMessage) return; + mfaMessage.textContent = text; + mfaMessage.className = `text-sm rounded-lg px-3 py-2 border ${ + type === "error" + ? "border-rose-800 bg-rose-900/20 text-rose-200" + : "border-emerald-800 bg-emerald-900/20 text-emerald-200" + }`; + mfaMessage.classList.remove("hidden"); + setTimeout(() => mfaMessage.classList.add("hidden"), 5000); +} + +async function loadMfaStatus() { + if (!mfaStatusText) return; + try { + const res = await fetch("/api/mfa/status", { credentials: "include" }); + if (!res.ok) { + mfaStatusText.textContent = "Failed to load MFA status."; + return; + } + const data = await res.json(); + const requiredText = data.required ? " Required by policy." : ""; + mfaStatusText.textContent = data.enabled + ? `MFA is enabled.${requiredText}` + : `MFA is not enabled.${requiredText}`; + if (mfaStartBtn) mfaStartBtn.classList.toggle("hidden", !!data.enabled); + if (mfaDisablePanel) mfaDisablePanel.classList.toggle("hidden", !data.enabled); + if (mfaDisableBtn) mfaDisableBtn.disabled = !!data.required; + if (mfaDisablePanel) { + mfaDisablePanel.classList.toggle("opacity-60", !!data.required); + } + } catch { + mfaStatusText.textContent = "Failed to load MFA status."; + } +} + +async function startMfaSetup() { + try { + const res = await fetch("/api/mfa/setup", { method: "POST", credentials: "include" }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + showMfaMessage(data.error || "Failed to start MFA setup.", "error"); + return; + } + if (mfaQrCode) mfaQrCode.innerHTML = data.qrSvg || ""; + if (mfaSecretText) mfaSecretText.textContent = data.secret || ""; + if (mfaOtpauthLink) mfaOtpauthLink.href = data.otpauthUrl || "#"; + if (mfaSetupPanel) mfaSetupPanel.classList.remove("hidden"); + if (mfaEnableCodeInput) mfaEnableCodeInput.focus(); + } catch { + showMfaMessage("Network error while starting MFA setup.", "error"); + } +} + +async function enableMfa() { + const code = String(mfaEnableCodeInput?.value || "").trim(); + try { + const res = await fetch("/api/mfa/enable", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ code }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + showMfaMessage(data.error || "Failed to enable MFA.", "error"); + return; + } + if (mfaSetupPanel) mfaSetupPanel.classList.add("hidden"); + if (mfaEnableCodeInput) mfaEnableCodeInput.value = ""; + showMfaMessage("MFA enabled."); + await loadMfaStatus(); + } catch { + showMfaMessage("Network error while enabling MFA.", "error"); + } +} + +async function disableMfa() { + const currentPassword = String(mfaDisablePasswordInput?.value || ""); + const code = String(mfaDisableCodeInput?.value || "").trim(); + try { + const res = await fetch("/api/mfa/disable", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ currentPassword, code }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + showMfaMessage(data.error || "Failed to disable MFA.", "error"); + return; + } + if (mfaDisablePasswordInput) mfaDisablePasswordInput.value = ""; + if (mfaDisableCodeInput) mfaDisableCodeInput.value = ""; + showMfaMessage("MFA disabled."); + await loadMfaStatus(); + } catch { + showMfaMessage("Network error while disabling MFA.", "error"); + } +} + function savePrefs(event) { event.preventDefault(); @@ -452,6 +574,8 @@ async function saveSecurityPolicy(event) { passwordRequireLowercase: securityRequireLowercaseInput.checked, passwordRequireNumber: securityRequireNumberInput.checked, passwordRequireSymbol: securityRequireSymbolInput.checked, + mfaRequiredForAdmins: !!securityMfaAdminsInput?.checked, + mfaRequiredForNonAdmins: !!securityMfaNonAdminsInput?.checked, }; const res = await fetch("/api/settings/security", { @@ -1837,6 +1961,7 @@ async function init() { await loadCurrentUser(); applyPermissionVisibility(); loadPrefs(); + await loadMfaStatus(); if (userHas("clients:build")) { if (buildsShowAllWrap && !isAdmin(currentUser?.role)) { @@ -1863,6 +1988,9 @@ async function init() { } passwordForm.addEventListener("submit", updatePassword); + if (mfaStartBtn) mfaStartBtn.addEventListener("click", startMfaSetup); + if (mfaEnableBtn) mfaEnableBtn.addEventListener("click", enableMfa); + if (mfaDisableBtn) mfaDisableBtn.addEventListener("click", disableMfa); prefsForm.addEventListener("submit", savePrefs); const sidebarBtn = document.getElementById("pref-nav-sidebar"); diff --git a/Overlord-Server/public/login.html b/Overlord-Server/public/login.html index c8bc962..d160579 100644 --- a/Overlord-Server/public/login.html +++ b/Overlord-Server/public/login.html @@ -57,6 +57,22 @@ /> + +
+
+
+

Multi-Factor Authentication

+

Loading MFA status...

+
+ +
+ + + + + + +
+ @@ -574,6 +644,26 @@ +
+

MFA Enforcement

+
+ + +
+
+