diff --git a/bun.lock b/bun.lock index 2fbb6b4..2e9703e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,11 +5,14 @@ "name": "web", "dependencies": { "@base-ui/react": "^1.0.0", + "@workos-inc/node": "^7.77.0", + "@xboxreplay/xboxlive-api": "^3.4.3", "@yudiel/react-qr-scanner": "^2.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "input-otp": "^1.4.2", "lucide-react": "^0.561.0", + "motion": "^12.23.26", "next": "16.0.10", "next-themes": "^0.4.6", "qrcode": "^1.5.4", @@ -17,6 +20,7 @@ "react": "19.2.1", "react-dom": "19.2.1", "shadcn": "^3.6.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", }, @@ -266,6 +270,12 @@ "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="], + + "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], + + "@peculiar/webcrypto": ["@peculiar/webcrypto@1.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.8", "@peculiar/json-schema": "^1.1.12", "pvtsutils": "^1.3.5", "tslib": "^2.6.2", "webcrypto-core": "^1.8.0" } }, "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -428,22 +438,58 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/accepts": ["@types/accepts@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/content-disposition": ["@types/content-disposition@0.5.9", "", {}, "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ=="], + + "@types/cookie": ["@types/cookie@0.5.4", "", {}, "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA=="], + + "@types/cookies": ["@types/cookies@0.9.2", "", { "dependencies": { "@types/connect": "*", "@types/express": "*", "@types/keygrip": "*", "@types/node": "*" } }, "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A=="], + "@types/emscripten": ["@types/emscripten@1.41.5", "", {}, "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="], + + "@types/http-assert": ["@types/http-assert@1.5.6", "", {}, "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/keygrip": ["@types/keygrip@1.0.6", "", {}, "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ=="], + + "@types/koa": ["@types/koa@2.15.0", "", { "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", "@types/cookies": "*", "@types/http-assert": "*", "@types/http-errors": "*", "@types/keygrip": "*", "@types/koa-compose": "*", "@types/node": "*" } }, "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g=="], + + "@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + + "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="], @@ -504,6 +550,12 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@workos-inc/node": ["@workos-inc/node@7.77.0", "", { "dependencies": { "iron-session": "~6.3.1", "jose": "~5.6.3", "leb": "^1.0.0", "qs": "6.14.0" } }, "sha512-6LGBAqih8kkzhHqmxueT9/xX93AJQxQhKekyNs0mqgWsrnqOPDiag1WIFzgxbQTvr564MqtEW502Tatfp1+x0Q=="], + + "@xboxreplay/errors": ["@xboxreplay/errors@0.1.0", "", {}, "sha512-Tgz1d/OIPDWPeyOvuL5+aai5VCcqObhPnlI3skQuf80GVF3k1I0lPCnGC+8Cm5PV9aLBT5m8qPcJoIUQ2U4y9g=="], + + "@xboxreplay/xboxlive-api": ["@xboxreplay/xboxlive-api@3.4.3", "", { "dependencies": { "@xboxreplay/errors": "^0.1.0", "axios": "^0.21.1" } }, "sha512-+iSmNCN4RHXZXBc48e0tJB+tvUs6XEwEWcXTcECrIttgDVN9fsFxroo+6ET+K38FobEM+5xUOfCVNqQoWQthxQ=="], + "@yudiel/react-qr-scanner": ["@yudiel/react-qr-scanner@2.5.0", "", { "dependencies": { "barcode-detector": "3.0.8", "webrtc-adapter": "9.0.3" }, "peerDependencies": { "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" } }, "sha512-wVWvm0z5kGGc3tiMcxyr2aji3C6aU5K3Q7R08MOdyFDFkvMbFMF1Hz5P5WFYW+zuuqxDPBcRZPCvmVs6jBheTQ=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -546,6 +598,8 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="], + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], @@ -556,12 +610,16 @@ "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], + "axios": ["axios@0.21.4", "", { "dependencies": { "follow-redirects": "^1.14.0" } }, "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg=="], + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "barcode-detector": ["barcode-detector@3.0.8", "", { "dependencies": { "zxing-wasm": "2.2.4" } }, "sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg=="], "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], @@ -572,6 +630,8 @@ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -620,7 +680,7 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -794,12 +854,16 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "framer-motion": ["framer-motion@12.23.26", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], @@ -878,6 +942,8 @@ "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -892,6 +958,10 @@ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "iron-session": ["iron-session@6.3.1", "", { "dependencies": { "@peculiar/webcrypto": "^1.4.0", "@types/cookie": "^0.5.1", "@types/express": "^4.17.13", "@types/koa": "^2.13.5", "@types/node": "^17.0.41", "cookie": "^0.5.0", "iron-webcrypto": "^0.2.5" }, "peerDependencies": { "express": ">=4", "koa": ">=2", "next": ">=10" }, "optionalPeers": ["express", "koa", "next"] }, "sha512-3UJ7y2vk/WomAtEySmPgM6qtYF1cZ3tXuWX5GsVX4PJXAcs5y/sV9HuSfpjKS6HkTL/OhZcTDWJNLZ7w+Erx3A=="], + + "iron-webcrypto": ["iron-webcrypto@0.2.8", "", { "dependencies": { "buffer": "^6" } }, "sha512-YPdCvjFMOBjXaYuDj5tiHst5CEk6Xw84Jo8Y2+jzhMceclAnb3+vNPP/CTtb5fO2ZEuXEaO4N+w62Vfko757KA=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], @@ -980,7 +1050,7 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "jose": ["jose@5.6.3", "", {}, "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1012,6 +1082,8 @@ "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + "leb": ["leb@1.0.0", "", {}, "sha512-Y3c3QZfvKWHX60BVOQPhLCvVGmDYWyJEiINE3drOog6KCyN2AOwvuQQzlS3uJg1J85kzpILXIUwRXULWavir+w=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], @@ -1078,6 +1150,12 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "motion": ["motion@12.23.26", "", { "dependencies": { "framer-motion": "^12.23.26", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ=="], + + "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], + + "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msw": ["msw@2.12.4", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg=="], @@ -1192,6 +1270,10 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], @@ -1296,6 +1378,8 @@ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1424,6 +1508,8 @@ "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "webcrypto-core": ["webcrypto-core@1.8.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/json-schema": "^1.1.12", "asn1js": "^3.0.5", "pvtsutils": "^1.3.5", "tslib": "^2.7.0" } }, "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A=="], + "webrtc-adapter": ["webrtc-adapter@9.0.3", "", { "dependencies": { "sdp": "^3.2.0" } }, "sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1480,6 +1566,8 @@ "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "@modelcontextprotocol/sdk/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], @@ -1524,6 +1612,8 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "iron-session/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], + "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -1532,6 +1622,8 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "msw/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "msw/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], diff --git a/package.json b/package.json index dc1bf47..cb09aaa 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,14 @@ }, "dependencies": { "@base-ui/react": "^1.0.0", + "@workos-inc/node": "^7.77.0", + "@xboxreplay/xboxlive-api": "^3.4.3", "@yudiel/react-qr-scanner": "^2.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "input-otp": "^1.4.2", "lucide-react": "^0.561.0", + "motion": "^12.23.26", "next": "16.0.10", "next-themes": "^0.4.6", "qrcode": "^1.5.4", @@ -22,6 +25,7 @@ "react": "19.2.1", "react-dom": "19.2.1", "shadcn": "^3.6.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0" }, diff --git a/src/app/access/[method]/[step]/page.tsx b/src/app/access/[method]/[step]/page.tsx new file mode 100644 index 0000000..d09d537 --- /dev/null +++ b/src/app/access/[method]/[step]/page.tsx @@ -0,0 +1,695 @@ +"use client"; + +import { useState, useTransition, useEffect, use } from "react"; +import { Asterisk, CheckCircle2, Camera, CameraOff, PartyPopper, ExternalLink, ArrowLeft, Copy } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Scanner } from "@yudiel/react-qr-scanner"; +import { toast } from "sonner"; +import { motion } from "motion/react"; +import { Item, ItemContent, ItemTitle, ItemMedia, ItemActions } from "@/components/ui/item"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { cfg } from "@/lib/cfg"; +import { getAvatarProfile } from "@/lib/avatar"; + +interface PageProps { + params: Promise<{ + method: string; + step: string; + }>; +} + +export default function AccessPage({ params }: PageProps) { + const router = useRouter(); + const resolvedParams = use(params); + const method = resolvedParams.method; // 'code' or 'qr' + const step = resolvedParams.step; // 'verify', 'details', 'success' + + // Access Code State + const [length, setLength] = useState(6); + const [code, setCode] = useState(""); + const [verified, setVerified] = useState(false); + const [whitelistSuccess, setWhitelistSuccess] = useState(false); + const [status, setStatus] = useState<{ ok: boolean; message?: string }>({ ok: false }); + const [isPending, startTransition] = useTransition(); + + // Details State + const [preference, setPreference] = useState<"java" | "bedrock" | "both">("java"); + const [javaName, setJavaName] = useState(""); + const [bedrockName, setBedrockName] = useState(""); + + // QR State + const [showScanner, setShowScanner] = useState(false); + const [hasPermission, setHasPermission] = useState(null); + const [scannerError, setScannerError] = useState(null); + + // Success State + const [copied, setCopied] = useState(false); + const [profileData, setProfileData] = useState<{ avatarUrl: string; gamertag: string } | null>(null); + + // Loading state to prevent premature redirects + const [isLoading, setIsLoading] = useState(true); + + // Handle redirects based on verification state + useEffect(() => { + if (!isLoading) { + if (step === 'details' && !verified) { + router.push(`/access/${method}/verify`); + } else if (step === 'success' && !whitelistSuccess) { + router.push(`/access/${method}/verify`); + } + } + }, [step, verified, whitelistSuccess, method, router, isLoading]); + useEffect(() => { + if (method === 'qr' && step === 'verify' && hasPermission === null) { + const requestCameraPermission = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + stream.getTracks().forEach(track => track.stop()); // Stop immediately after getting permission + setHasPermission(true); + } catch (error) { + console.error('Camera permission denied:', error); + setHasPermission(false); + setScannerError("Camera permission denied. Please enable camera access to scan QR codes."); + } + }; + + requestCameraPermission(); + } + }, [method, step, hasPermission]); + useEffect(() => { + const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; + + const savedVerified = localStorage.getItem(`${prefix}verified`); + const savedWhitelistSuccess = localStorage.getItem(`${prefix}whitelist-success`); + const savedCode = localStorage.getItem(`${prefix}auth-code`); + const savedJavaName = localStorage.getItem(`${prefix}java-name`); + const savedBedrockName = localStorage.getItem(`${prefix}bedrock-name`); + const savedPreference = localStorage.getItem(`${prefix}preference`); + + if (savedVerified === 'true') { + setVerified(true); + } + if (savedWhitelistSuccess === 'true') { + setWhitelistSuccess(true); + } + if (savedCode) { + setCode(savedCode); + } + if (savedJavaName) { + setJavaName(savedJavaName); + } + if (savedBedrockName) { + setBedrockName(savedBedrockName); + } + if (savedPreference) { + setPreference(savedPreference as "java" | "bedrock" | "both"); + } + + // Fetch profile data for success step + if (step === 'success') { + fetchProfileData(); + } + + // Set loading to false after state is loaded + setIsLoading(false); + }, [method, step]); + + // Save state to localStorage + useEffect(() => { + const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; + localStorage.setItem(`${prefix}verified`, verified.toString()); + }, [verified, method]); + + useEffect(() => { + const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; + localStorage.setItem(`${prefix}whitelist-success`, whitelistSuccess.toString()); + }, [whitelistSuccess, method]); + + useEffect(() => { + const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; + localStorage.setItem(`${prefix}auth-code`, code); + }, [code, method]); + + useEffect(() => { + const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; + localStorage.setItem(`${prefix}java-name`, javaName); + }, [javaName, method]); + + useEffect(() => { + const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; + localStorage.setItem(`${prefix}bedrock-name`, bedrockName); + }, [bedrockName, method]); + + useEffect(() => { + const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; + localStorage.setItem(`${prefix}preference`, preference); + }, [preference, method]); + + useEffect(() => { + const fetchConfig = async () => { + try { + const response = await fetch("/api/access-code-config"); + const config = await response.json(); + setLength(config.length || 6); + } catch (error) { + console.error("Failed to fetch access code config:", error); + } + }; + + fetchConfig(); + }, []); + + const fetchProfileData = async () => { + try { + const res = await fetch( + `/api/xbox/profile/get?gamertag=${encodeURIComponent(cfg.server.bedrockFriend)}` + ); + const data = await res.json(); + setProfileData(data); + } catch (e) { + console.error("Failed to fetch profile", e); + } + }; + + const needsJavaName = preference === "java" || preference === "both"; + const needsBedrockName = preference === "bedrock" || preference === "both"; + + const handleVerifyCode = async () => { + startTransition(async () => { + try { + const response = await fetch("/api/access-code", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ code }), + }); + const result = await response.json(); + + if (!result.success) { + setVerified(false); + setStatus({ ok: false, message: result.message }); + return; + } + + setVerified(true); + setStatus({ + ok: true, + message: "Access code verified!", + }); + + // Navigate to details step + router.push(`/access/${method}/details`); + } catch { + setVerified(false); + setStatus({ + ok: false, + message: "We couldn't reach the server. Double-check your connection and try again.", + }); + } + }); + }; + + const handleQRScan = (detectedCodes: any[]) => { + if (detectedCodes.length > 0) { + const result = detectedCodes[0].rawValue; + const sanitizedResult = result.replace(/[^0-9A-Za-z]/g, ""); + + startTransition(async () => { + try { + const response = await fetch("/api/qr-verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + code: sanitizedResult, + }), + }); + const verifyResult = await response.json(); + + if (verifyResult.success) { + setVerified(true); + setStatus({ + ok: true, + message: verifyResult.message || "QR code verified successfully!", + }); + + // Navigate to details step + router.push(`/access/${method}/details`); + } else { + setVerified(false); + setStatus({ + ok: false, + message: verifyResult.message || "QR code verification failed", + }); + } + } catch { + setVerified(false); + setStatus({ + ok: false, + message: "We couldn't reach the server. Try again soon.", + }); + } + }); + } + }; + + const handleWhitelistSubmit = async () => { + if ( + (needsJavaName && !javaName.trim()) || + (needsBedrockName && !bedrockName.trim()) + ) { + setStatus({ + ok: false, + message: "Please fill out every username we need to whitelist.", + }); + return; + } + + startTransition(async () => { + try { + const response = await fetch("/api/whitelist", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + javaName: needsJavaName ? javaName.trim() : undefined, + bedrockName: needsBedrockName ? bedrockName.trim() : undefined, + preference, + accessCode: method === 'qr' ? "qr_verified" : code, + }), + }); + + const result = await response.json(); + + if (result.success) { + setStatus({ + ok: true, + message: result.results.join(" ") || "Successfully added to whitelist!", + }); + setWhitelistSuccess(true); + + // Navigate to success step + router.push(`/access/${method}/success`); + } else { + setStatus({ + ok: false, + message: result.errors.join(" ") || "Failed to add to whitelist.", + }); + } + } catch { + setStatus({ + ok: false, + message: "We couldn't reach the server. Double-check your connection and try again.", + }); + } + }); + }; + + const handleScannerError = (error: any) => { + console.error("QR Scanner error:", error); + setScannerError("Unable to access camera. Please check permissions."); + setHasPermission(false); + }; + + const handleCopyIp = async () => { + const serverIp = cfg.server.javaIp; + if (serverIp) { + await navigator.clipboard.writeText(serverIp); + toast.success("Copied to clipboard"); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + // Get server information based on user's preference + const getServerInfo = () => { + switch (preference) { + case 'java': + return { + address: cfg.server.javaIp, + label: 'Java Server Address', + showProfile: false + }; + case 'bedrock': + return { + address: cfg.server.bedrockIp + (cfg.server.bedrockPort ? `:${cfg.server.bedrockPort}` : ''), + label: 'Bedrock Server Address', + showProfile: true + }; + case 'both': + return { + javaAddress: cfg.server.javaIp, + bedrockAddress: cfg.server.bedrockIp + (cfg.server.bedrockPort ? `:${cfg.server.bedrockPort}` : ''), + label: 'Server Addresses', + showProfile: true + }; + default: + return { + address: cfg.server.javaIp, + label: 'Server Address', + showProfile: false + }; + } + }; + + const serverInfo = getServerInfo(); + + const handleReset = () => { + const prefix = method === 'qr' ? 'frg-qr-' : 'frg-'; + localStorage.removeItem(`${prefix}verified`); + localStorage.removeItem(`${prefix}whitelist-success`); + localStorage.removeItem(`${prefix}auth-code`); + localStorage.removeItem(`${prefix}java-name`); + localStorage.removeItem(`${prefix}bedrock-name`); + localStorage.removeItem(`${prefix}preference`); + + setVerified(false); + setWhitelistSuccess(false); + setCode(""); + setJavaName(""); + setBedrockName(""); + setStatus({ ok: false }); + + router.push(`/access/${method}/verify`); + }; + + // Render different steps + if (step === 'verify') { + return ( +
+
+ {/* Header */} +
+ + + Back to home + +

+ {method === 'code' ? 'Access Code Verification' : 'QR Code Verification'} +

+

+ {method === 'code' + ? `Use the ${length}-character invite your friend gave you so we can safely whitelist you.` + : 'Scan a QR code to verify your access.' + } +

+
+ + {/* Verification Content */} +
+ {method === 'code' ? ( +
+ + + + {Array.from({ length }).map((_, i) => ( + + ))} + + +
+ ) : ( +
+ {scannerError ? ( +
+ +

{scannerError}

+ +
+ ) : ( + <> +
+ + {hasPermission === null && ( +
+
+ +

+ Requesting camera permission... +

+
+
+ )} +
+ + )} +
+ )} + + {/* Status */} +
+ {status.message && ( +

+ {status.ok && } + {status.message} +

+ )} +
+ + {/* Actions */} + {method === 'code' && ( + + )} +
+
+
+ ); + } + + if (step === 'details') { + if (!verified) { + return null; // Redirect handled by useEffect + } + + return ( +
+
+ {/* Header */} +
+ + + Back to verification + +

Server Details

+

+ Tell us which Minecraft edition you play and your username(s). +

+
+ + {/* Form */} +
+
+ + + value && setPreference(value as typeof preference) + } + className="w-full" + > + + Java + + + Bedrock + + + Both + + +
+ + {needsJavaName && ( +
+ + setJavaName(event.target.value)} + /> +
+ )} + + {needsBedrockName && ( +
+ + setBedrockName(event.target.value)} + /> +
+ )} + + {/* Status */} +
+ {status.message && ( +

+ {status.ok && } + {status.message} +

+ )} +
+ + {/* Actions */} + +
+
+
+ ); + } + + if (step === 'success') { + if (!whitelistSuccess) { + return null; // Redirect handled by useEffect + } + + const serverIp = cfg.server.javaIp; + + return ( +
+
+ {/* Header */} +
+ +

You're whitelisted!

+

+ Welcome to FRG Network! You can now join the server. + As soon as you join, come back and click "Claim my reward" to get a gift on behalf of the team here. +

+
+ + {/* Server IP */} +
+

Server Address

+
+ + +
+
+ + {/* Profile */} +
+

Add on Bedrock

+ + + + + ? + + + + {cfg.server.bedrockFriend} + + + + + +
+ + {/* Actions */} +
+ + + +
+
+
+ ); + } + + // Invalid step - redirect handled by useEffect + return null; +} diff --git a/src/app/api/auth/callback/route.ts b/src/app/api/auth/callback/route.ts new file mode 100644 index 0000000..e20eb4c --- /dev/null +++ b/src/app/api/auth/callback/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server' +import { WorkOS } from '@workos-inc/node' + +const workos = new WorkOS(process.env.WORKOS_API_KEY) +const clientId = process.env.WORKOS_CLIENT_ID +const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback` + +export async function POST(request: NextRequest) { + try { + const { code } = await request.json() + + if (!code) { + return NextResponse.json( + { error: 'Authorization code is required' }, + { status: 400 } + ) + } + + if (!clientId) { + return NextResponse.json( + { error: 'WorkOS client ID not configured' }, + { status: 500 } + ) + } + + // Exchange the authorization code for an access token + const { user } = await workos.userManagement.authenticateWithCode({ + code, + clientId, + }) + + // Here you would typically: + // 1. Create or update the user in your database + // 2. Create a session for the user + // 3. Set secure HTTP-only cookies with the tokens + + // For now, we'll return the user info and a redirect URL + // In a real implementation, you'd want to handle sessions properly + + return NextResponse.json({ + success: true, + user: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + profilePictureUrl: user.profilePictureUrl, + }, + redirect: '/', // or to a dashboard page + }) + } catch (error) { + console.error('Auth callback error:', error) + + return NextResponse.json( + { + error: error instanceof Error ? error.message : 'Authentication failed' + }, + { status: 500 } + ) + } +} diff --git a/src/app/api/whitelist/route.ts b/src/app/api/whitelist/route.ts index 5f89883..8c9b0b4 100644 --- a/src/app/api/whitelist/route.ts +++ b/src/app/api/whitelist/route.ts @@ -31,7 +31,7 @@ export async function POST(request: Request) { // Handle Java whitelist if ((preference === "java" || preference === "both") && javaName?.trim()) { - const result = await servertap.addToWhitelist(javaName.trim(), undefined, false); + const result = await servertap.addToWhitelist(javaName.trim()); if (result.success) { results.push(`Successfully whitelisted Java player: ${javaName}`); } else { @@ -42,7 +42,7 @@ export async function POST(request: Request) { // Handle Bedrock whitelist (for now, we'll use the same endpoint but could be different) if ((preference === "bedrock" || preference === "both") && bedrockName?.trim()) { const bedrockNameWithDot = `.${bedrockName.trim()}`; - const result = await servertap.addToWhitelist(bedrockNameWithDot, undefined, true); + const result = await servertap.addToWhitelist(bedrockNameWithDot); if (result.success) { results.push(`Successfully whitelisted Bedrock player: ${bedrockNameWithDot}`); } else { diff --git a/src/app/api/xbox/profile/get/route.ts b/src/app/api/xbox/profile/get/route.ts new file mode 100644 index 0000000..fbce05c --- /dev/null +++ b/src/app/api/xbox/profile/get/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getAvatarProfile } from "@/lib/avatar"; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const gamertag = searchParams.get("gamertag"); + + if (!gamertag) { + return NextResponse.json({ error: "no gamertag" }, { status: 400 }); + } + + // Use the simple avatar service + const result = await getAvatarProfile(gamertag); + + return NextResponse.json(result); + + } catch (error) { + console.error("Failed to fetch Xbox profile:", error); + + // Return fallback data on error + const { searchParams } = new URL(req.url); + const gamertag = searchParams.get("gamertag") || "Unknown"; + + return NextResponse.json( + { + gamertag, + avatarUrl: "", + error: "Failed to fetch profile data" + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/xbox/profile/route.ts b/src/app/api/xbox/profile/route.ts new file mode 100644 index 0000000..3d29b84 --- /dev/null +++ b/src/app/api/xbox/profile/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getXboxProfileWithAvatar } from "@/lib/xbox"; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const gamertag = searchParams.get("gamertag"); + + if (!gamertag) { + return NextResponse.json( + { error: "Gamertag parameter is required" }, + { status: 400 } + ); + } + + // Use the modified function that reads from environment variables + const result = await getXboxProfileWithAvatar(gamertag); + + return NextResponse.json(result); + + } catch (error) { + console.error("Failed to fetch Xbox profile:", error); + + // Return fallback data on error + const { searchParams } = new URL(req.url); + const gamertag = searchParams.get("gamertag") || "Unknown"; + + return NextResponse.json( + { + gamertag, + avatarUrl: "", + error: "Failed to fetch profile data" + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/xbox/tokens/route.ts b/src/app/api/xbox/tokens/route.ts new file mode 100644 index 0000000..23e4e9a --- /dev/null +++ b/src/app/api/xbox/tokens/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import XboxTokenManager from "@/lib/xbox-auth"; + +export async function GET() { + try { + const tokenManager = XboxTokenManager.getInstance(); + const status = tokenManager.getTokenStatus(); + + return NextResponse.json({ + status, + message: status.valid ? "Tokens are valid" : "Tokens need refresh" + }); + } catch (error) { + console.error("Failed to get token status:", error); + return NextResponse.json( + { error: "Failed to get token status" }, + { status: 500 } + ); + } +} + +export async function POST() { + try { + const tokenManager = XboxTokenManager.getInstance(); + const tokens = await tokenManager.forceRefresh(); + + return NextResponse.json({ + success: true, + message: "Tokens refreshed successfully", + expiresAt: tokens.expiresAt + }); + } catch (error) { + console.error("Failed to refresh tokens:", error); + return NextResponse.json( + { + error: "Failed to refresh tokens", + message: error instanceof Error ? error.message : "Unknown error" + }, + { status: 500 } + ); + } +} diff --git a/src/app/auth/callback/page.tsx b/src/app/auth/callback/page.tsx new file mode 100644 index 0000000..10b8ed0 --- /dev/null +++ b/src/app/auth/callback/page.tsx @@ -0,0 +1,139 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { Loader2 } from 'lucide-react' + +export default function AuthCallback() { + const router = useRouter() + const searchParams = useSearchParams() + const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading') + const [error, setError] = useState('') + + useEffect(() => { + const handleCallback = async () => { + try { + const code = searchParams.get('code') + const error = searchParams.get('error') + const errorDescription = searchParams.get('error_description') + + if (error) { + setError(errorDescription || error) + setStatus('error') + return + } + + if (!code) { + setError('No authorization code received') + setStatus('error') + return + } + + // Exchange the authorization code for tokens + const response = await fetch('/api/auth/callback', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ code }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Authentication failed') + } + + const data = await response.json() + + // Store the session/token as needed + if (data.redirect) { + window.location.href = data.redirect + } else { + // Default redirect to dashboard or home + router.push('/') + } + + setStatus('success') + } catch (err) { + setError(err instanceof Error ? err.message : 'Authentication failed') + setStatus('error') + } + } + + handleCallback() + }, [searchParams, router]) + + if (status === 'loading') { + return ( +
+
+ +

Completing authentication...

+
+
+ ) + } + + if (status === 'error') { + return ( +
+
+
+ + + +
+

Authentication Failed

+

{error}

+
+ + +
+
+
+ ) + } + + return ( +
+
+
+ + + +
+

Authentication successful! Redirecting...

+
+
+ ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fe9dfb1..1701385 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import "./globals.css"; +import { Toaster } from "@/components/ui/sonner" import { ThemeProvider } from "@/components/theme-provider"; import { Google_Sans_Code, Mona_Sans } from "next/font/google"; @@ -36,7 +37,7 @@ export default function RootLayout({ > {children} - + ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 901463b..4bb785a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,9 @@ import { ComponentExample } from "@/components/component-example"; -import { AccessCodeDialog, QRDialog } from "@/components/dialogs"; import { Button } from "@/components/ui/button"; import { Asterisk, QrCode } from "lucide-react"; import { DotGothic16 } from "next/font/google"; import { cfg } from "@/lib/cfg"; +import Link from "next/link"; const dots = DotGothic16({ weight: "400", @@ -21,14 +21,18 @@ export default function Page() {

A private Minecraft server for me and my friends.

{cfg.accessCode.enabled && ( - + )} {cfg.qr.enabled && ( - - - + + )} diff --git a/src/app/survey/page.tsx b/src/app/survey/page.tsx new file mode 100644 index 0000000..c59f559 --- /dev/null +++ b/src/app/survey/page.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { ArrowLeft, ExternalLink, Gift } from "lucide-react"; +import Link from "next/link"; + +export default function SurveyPage() { + const [minecraftUsername, setMinecraftUsername] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [rewardQueued, setRewardQueued] = useState(false); + + const handleRewardOnly = async (e: React.FormEvent) => { + e.preventDefault(); + if (!minecraftUsername.trim()) return; + + setIsSubmitting(true); + + try { + // Call the reward endpoint to give diamonds + const response = await fetch("/reward", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + player: minecraftUsername, + }), + }); + + if (response.ok) { + setRewardQueued(true); + } else { + console.error("Failed to queue reward"); + } + } catch (error) { + console.error("Error queuing reward:", error); + } finally { + setIsSubmitting(false); + } + }; + + const TALLY_FORM_URL = "https://tally.so/r/nP5KdL"; // Replace with your actual Tally form URL + + return ( +
+ + +
+ +
+ FRG Network Survey + + Help us improve your server experience! Complete this survey to receive 2 diamonds as a thank you. + +
+ + {/* Quick reward section */} +
+
+ +

Get Your Diamonds

+
+

+ Enter your Minecraft username to queue 2 diamonds for when you join the server: +

+ + {!rewardQueued ? ( +
+ setMinecraftUsername(e.target.value)} + className="flex-1 font-mono" + required + /> + +
+ ) : ( +
+ + 2 diamonds queued for {minecraftUsername}! +
+ )} +
+ + {/* Survey section */} +
+
+

Take the Full Survey

+

+ Help us improve the server by sharing your thoughts and preferences. +

+
+ + + +

+ Opens in a new window • Takes about 2-3 minutes +

+
+ + {/* Navigation */} +
+ +
+
+
+
+ ); +} diff --git a/src/components/dialogs.tsx b/src/components/dialogs.tsx deleted file mode 100644 index e476b2c..0000000 --- a/src/components/dialogs.tsx +++ /dev/null @@ -1,422 +0,0 @@ -"use client"; - -import { useState, useTransition, useEffect } from "react"; -import { Asterisk, CheckCircle2, Camera, CameraOff } from "lucide-react"; -import { Button } from "./ui/button"; -import { - Dialog, - DialogDescription, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "./ui/dialog"; -import { Google_Sans_Code } from "next/font/google"; -import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp"; -import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group"; -import { Input } from "./ui/input"; -import { Label } from "./ui/label"; -import { Scanner } from "@yudiel/react-qr-scanner"; -import { googleSansCode } from "@/app/layout"; - - -export function AccessCodeDialog() { - const [length, setLength] = useState(6); - const [enabled, setEnabled] = useState(false); - const [code, setCode] = useState(""); - const [preference, setPreference] = useState<"java" | "bedrock" | "both">( - "java" - ); - const [javaName, setJavaName] = useState(""); - const [bedrockName, setBedrockName] = useState(""); - const [status, setStatus] = useState<{ ok: boolean; message?: string }>({ - ok: false, - }); - const [isPending, startTransition] = useTransition(); - const [verified, setVerified] = useState(false); - - useEffect(() => { - const fetchConfig = async () => { - try { - const response = await fetch("/api/access-code-config"); - const config = await response.json(); - setLength(config.length || 6); - setEnabled(config.enabled); - } catch (error) { - console.error("Failed to fetch access code config:", error); - } - }; - - fetchConfig(); - }, []); - - const needsJavaName = preference === "java" || preference === "both"; - const needsBedrockName = preference === "bedrock" || preference === "both"; - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setStatus({ ok: false, message: undefined }); - - if (verified) { - // Handle whitelist submission - if ( - (needsJavaName && !javaName.trim()) || - (needsBedrockName && !bedrockName.trim()) - ) { - setStatus({ - ok: false, - message: "Please fill out every username we need to whitelist.", - }); - return; - } - - startTransition(async () => { - try { - const response = await fetch("/api/whitelist", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - javaName: needsJavaName ? javaName.trim() : undefined, - bedrockName: needsBedrockName ? bedrockName.trim() : undefined, - preference, - accessCode: code, - }), - }); - - const result = await response.json(); - - if (result.success) { - setStatus({ - ok: true, - message: result.results.join(" ") || "Successfully added to whitelist!", - }); - } else { - setStatus({ - ok: false, - message: result.errors.join(" ") || "Failed to add to whitelist.", - }); - } - } catch { - setStatus({ - ok: false, - message: "We couldn't reach the server. Double-check your connection and try again.", - }); - } - }); - return; - } - - // Handle access code verification - startTransition(async () => { - try { - const response = await fetch("/api/access-code", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - code, - }), - }); - const result = await response.json(); - - if (!result.success) { - setVerified(false); - setStatus({ ok: false, message: result.message }); - return; - } - - setVerified(true); - setStatus({ - ok: true, - message: - "Access code verified!", - }); - } catch { - setVerified(false); - setStatus({ - ok: false, - message: - "We couldn't reach the server. Double-check your connection and try again.", - }); - } - }); - }; - - return ( - - - - - - - - Authenticate via access code - - Use the {length}-character invite your friend gave you so we can - safely whitelist you. - - - -
-
- - - - {Array.from({ length }).map((_, i) => ( - - ))} - - -
- - {verified && ( - <> -
- - - value && setPreference(value as typeof preference) - } - className="w-full" - > - - Java - - - Bedrock - - - Both - - -
- - {needsJavaName && ( -
- - setJavaName(event.target.value)} - /> -
- )} - - {needsBedrockName && ( -
- - setBedrockName(event.target.value)} - /> -
- )} - - )} - - -
- {status.message && ( -

- {status.ok && } - {status.message} -

- )} -
- -
-
-
-
- ); -} - -export function QRDialog({ children }: { children: React.ReactNode }) { - const [status, setStatus] = useState<{ ok: boolean; message?: string }>({ - ok: false, - }); - const [isPending, startTransition] = useTransition(); - const [showScanner, setShowScanner] = useState(false); - const [hasPermission, setHasPermission] = useState(null); - const [scannerError, setScannerError] = useState(null); - - const handleQRScan = (detectedCodes: any[]) => { - if (detectedCodes.length > 0) { - const result = detectedCodes[0].rawValue; - const sanitizedResult = result.replace(/[^0-9A-Za-z]/g, ""); - - startTransition(async () => { - try { - const response = await fetch("/api/qr-verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - code: sanitizedResult, - }), - }); - const verifyResult = await response.json(); - - if (verifyResult.success) { - setStatus({ - ok: true, - message: verifyResult.message || "QR code verified successfully!", - }); - } else { - setStatus({ - ok: false, - message: verifyResult.message || "QR code verification failed", - }); - } - } catch { - setStatus({ - ok: false, - message: "We couldn't reach the server. Try again soon.", - }); - } - }); - } - }; - - const handleScannerError = (error: any) => { - console.error("QR Scanner error:", error); - setScannerError("Unable to access camera. Please check permissions."); - setHasPermission(false); - }; - - const handlePermission = (permission: boolean) => { - setHasPermission(permission); - if (!permission) { - setScannerError("Camera permission denied. Please enable camera access to scan QR codes."); - } else { - setScannerError(null); - } - }; - - return ( - - - {children} - - - - - Scan QR Code - - Scan a QR code to verify your access. - - - -
- {scannerError ? ( -
- -

{scannerError}

- -
- ) : ( - <> -
- {hasPermission === false ? ( -
- -

- Camera permission is required to scan QR codes. -

- -
- ) : ( -
- - {hasPermission === null && ( -
-
- -

- Requesting camera permission... -

-
-
- )} -
- )} -
- -
-

- Position the QR code within the frame to scan -

- {status.message && ( -

- {status.ok && } - {status.message} -

- )} - -
- - )} -
-
-
- ); -} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..07235b7 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,109 @@ +"use client" + +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className)} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 6a0e305..d26a4e3 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { Slot } from "radix-ui" import { cn } from "@/lib/utils" const buttonVariants = cva( - "focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", + "focus-visible:border-ring cursor-pointer focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", { variants: { variant: { diff --git a/src/components/ui/item.tsx b/src/components/ui/item.tsx new file mode 100644 index 0000000..9c30241 --- /dev/null +++ b/src/components/ui/item.tsx @@ -0,0 +1,195 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" + +function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const itemVariants = cva( + "[a]:hover:bg-muted rounded-md border text-xs/relaxed w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors", + { + variants: { + variant: { + default: "border-transparent", + outline: "border-border", + muted: "bg-muted/50 border-transparent", + }, + size: { + default: "gap-2.5 px-3 py-2.5", + sm: "gap-2.5 px-3 py-2.5", + xs: "gap-2.5 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Item({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"div"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "div" + return ( + + ) +} + +const itemMediaVariants = cva( + "gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none", + { + variants: { + variant: { + default: "bg-transparent", + icon: "[&_svg:not([class*='size-'])]:size-4", + image: "size-8 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function ItemMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function ItemContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +

a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...props} + /> + ) +} + +function ItemActions({ className, ...props }: React.ComponentProps<"div">) { + return ( +

+ ) +} + +function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Item, + ItemMedia, + ItemContent, + ItemActions, + ItemGroup, + ItemSeparator, + ItemTitle, + ItemDescription, + ItemHeader, + ItemFooter, +} diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..9280ee5 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,49 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner, type ToasterProps } from "sonner" +import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ), + info: ( + + ), + warning: ( + + ), + error: ( + + ), + loading: ( + + ), + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + toastOptions={{ + classNames: { + toast: "cn-toast", + }, + }} + {...props} + /> + ) +} + +export { Toaster } diff --git a/src/lib/avatar.ts b/src/lib/avatar.ts new file mode 100644 index 0000000..62562e5 --- /dev/null +++ b/src/lib/avatar.ts @@ -0,0 +1,64 @@ +// Simple avatar service using environment variables +// No Xbox API complexity required! + +export interface AvatarProfile { + gamertag: string; + avatarUrl: string; +} + +export async function getAvatarProfile(gamertag: string): Promise { + // Check if there's a custom avatar URL in environment variables + const customAvatarUrl = process.env.AVATAR_URL; + + if (customAvatarUrl) { + console.log('Using custom avatar URL from environment'); + return { + gamertag, + avatarUrl: customAvatarUrl + }; + } + + // Fallback to playerdb.co (no authentication required) + try { + const gamertagFormats = [ + gamertag.replace(/\s+/g, ''), // No spaces: "FRGServers" + gamertag.replace(/\s+/g, '_'), // Underscore: "FRG_Servers" + gamertag.replace(/\s+/g, '%20'), // URL encoded: "FRG%20Servers" + gamertag // Original: "FRG Servers" + ]; + + for (const format of gamertagFormats) { + try { + console.log(`Trying playerdb.co with gamertag: "${format}"`); + + const res = await fetch( + `https://playerdb.co/api/player/xbox/${encodeURIComponent(format)}` + ); + + if (res.ok) { + const data = await res.json(); + console.log('playerdb.co success with format:', format); + + const avatarUrl = data.data?.player?.avatar || + data.data?.player?.head?.avatar || + ""; + + return { + gamertag: data.data?.player?.username || gamertag, + avatarUrl + }; + } + } catch (e) { + console.log(`playerdb.co error with format "${format}":`, (e as Error).message); + } + } + } catch (error) { + console.error("Avatar lookup failed:", error); + } + + // Final fallback - no avatar + return { + gamertag, + avatarUrl: "" + }; +} diff --git a/src/lib/cfg.ts b/src/lib/cfg.ts index 378b7b0..7455dec 100644 --- a/src/lib/cfg.ts +++ b/src/lib/cfg.ts @@ -2,7 +2,6 @@ const sanitizeCode = (value?: string | null) => value?.replace(/[^0-9A-Za-z]/g, "") ?? ""; const accessCodeValue = process.env.SERVER_ACCESS_CODE ?? ""; - export const cfg = { qr: { enabled: process.env.SERVER_QR_CODE_ENABLED === "true", @@ -13,14 +12,18 @@ export const cfg = { value: accessCodeValue, length: sanitizeCode(accessCodeValue).length || 6, }, + minecraftApi: { + url: process.env.MINECRAFT_API_URL ?? "http://localhost:6767", + key: process.env.MINECRAFT_API_KEY ?? "", + }, serverTap: { url: process.env.SERVERTAP_BASE_URL, key: process.env.SERVERTAP_API_KEY, }, server: { - javaIp: process.env.SERVER_JAVA_IP ?? "", - bedrockIp: process.env.SERVER_BEDROCK_IP ?? "", - bedrockPort: process.env.SERVER_BEDROCK_PORT ?? "", - bedrockFriend: process.env.SERVER_BEDROCK_FRIEND ?? "", + javaIp: process.env.NEXT_PUBLIC_SERVER_JAVA_IP ?? "", + bedrockIp: process.env.NEXT_PUBLIC_SERVER_BEDROCK_IP ?? "", + bedrockPort: process.env.NEXT_PUBLIC_SERVER_BEDROCK_PORT ?? "", + bedrockFriend: process.env.NEXT_PUBLIC_SERVER_BEDROCK_FRIEND ?? "", }, }; \ No newline at end of file diff --git a/src/lib/minecraft-api.ts b/src/lib/minecraft-api.ts new file mode 100644 index 0000000..3da823d --- /dev/null +++ b/src/lib/minecraft-api.ts @@ -0,0 +1,93 @@ +const API_BASE_URL = process.env.NEXT_PUBLIC_MINECRAFT_API_URL || 'http://localhost:6767'; +const API_KEY = process.env.NEXT_PUBLIC_MINECRAFT_API_KEY; + +interface ApiResponse { + success: boolean; + message?: string; + error?: string; + details?: string; + data?: T; +} + +class MinecraftApiClient { + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise> { + const url = `${API_BASE_URL}${endpoint}`; + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record || {}), + }; + + if (API_KEY) { + headers.Authorization = `Bearer ${API_KEY}`; + } + + try { + const response = await fetch(url, { + ...options, + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + return { + success: false, + error: data.error || 'Request failed', + details: data.details, + }; + } + + return { + success: true, + data, + message: data.message, + }; + } catch (error) { + return { + success: false, + error: 'Network error', + details: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + async rewardPlayer(playerName: string): Promise { + return this.request('/reward', { + method: 'POST', + body: JSON.stringify({ player: playerName }), + }); + } + + async addToWhitelist(playerName: string): Promise { + return this.request('/api/whitelist/add', { + method: 'POST', + body: JSON.stringify({ player: playerName }), + }); + } + + async removeFromWhitelist(playerName: string): Promise { + return this.request('/api/whitelist/remove', { + method: 'POST', + body: JSON.stringify({ player: playerName }), + }); + } + + async getWhitelist(): Promise> { + return this.request('/api/whitelist/list', { + method: 'GET', + }); + } + + async executeCommand(command: string): Promise> { + return this.request('/api/command', { + method: 'POST', + body: JSON.stringify({ command }), + }); + } +} + +export const minecraftApi = new MinecraftApiClient(); +export type { ApiResponse }; diff --git a/src/lib/servertap.ts b/src/lib/servertap.ts index 25d1125..e236758 100644 --- a/src/lib/servertap.ts +++ b/src/lib/servertap.ts @@ -1,71 +1,57 @@ import { cfg } from "./cfg"; -type ServerTapPlayer = { - uuid: string; - displayName: string; - address: string; - port: number; - exhaustion: number; - exp: number; - whitelisted: boolean; - banned: boolean; - op: boolean; +type MinecraftPlayer = { + name: string; }; -type ServerTapResponse = { +type MinecraftResponse = { success: boolean; data?: T; error?: string; + message?: string; + details?: string; }; -class ServerTapClient { +class MinecraftClient { private baseUrl: string; private apiKey: string; constructor() { - this.baseUrl = cfg.serverTap.url || ""; - this.apiKey = cfg.serverTap.key || ""; + this.baseUrl = cfg.minecraftApi.url || "http://localhost:6767"; + this.apiKey = cfg.minecraftApi.key || ""; } private async makeRequest( endpoint: string, options: RequestInit = {} - ): Promise> { - if (!this.baseUrl || !this.apiKey) { - return { - success: false, - error: "ServerTap is not configured", - }; - } - + ): Promise> { try { const url = `${this.baseUrl}${endpoint}`; - console.log(`ServerTap API Request: ${options.method || 'GET'} ${url}`); - console.log(`Headers:`, { + const headers: Record = { "Content-Type": "application/json", - "Accept": "application/json", - key: this.apiKey ? "***" : "MISSING", - ...options.headers, - }); + ...(options.headers as Record || {}), + }; + + if (this.apiKey) { + headers.Authorization = `Bearer ${this.apiKey}`; + } + + console.log(`Minecraft API Request: ${options.method || 'GET'} ${url}`); const response = await fetch(url, { - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - key: this.apiKey, - ...options.headers, - }, + headers, ...options, }); - console.log(`ServerTap API Response: ${response.status} ${response.statusText}`); + console.log(`Minecraft API Response: ${response.status} ${response.statusText}`); if (!response.ok) { - const errorText = await response.text(); - console.error(`ServerTap API Error Response: ${errorText}`); + const errorData = await response.json().catch(() => ({})); + console.error(`Minecraft API Error Response:`, errorData); return { success: false, - error: `HTTP ${response.status}: ${response.statusText} - ${errorText}`, + error: errorData.error || `HTTP ${response.status}: ${response.statusText}`, + details: errorData.details, }; } @@ -73,9 +59,10 @@ class ServerTapClient { return { success: true, data, + message: data.message, }; } catch (error) { - console.error(`ServerTap API Error:`, error); + console.error(`Minecraft API Error:`, error); console.error(`Base URL: ${this.baseUrl}`); console.error(`API Key configured: ${!!this.apiKey}`); return { @@ -85,66 +72,43 @@ class ServerTapClient { } } - async addToWhitelist(playerName: string, playerUuid?: string, isBedrock?: boolean): Promise> { - let uuid = playerUuid; - - // For Bedrock players, convert XUID to Java UUID format if no UUID provided - if (isBedrock && !uuid) { - // Remove dot prefix and convert XUID to Java UUID format - const xuid = playerName.replace('.', ''); - uuid = `00000000-0000-0000-0000-${xuid.padStart(12, '0')}`; - } else if (!uuid) { - // For Java players without UUID, leave empty as shown in curl example - uuid = ''; - } - - const body = new URLSearchParams({ - uuid: uuid, - name: playerName, - }); - - return this.makeRequest(`/v1/server/whitelist`, { + async addToWhitelist(playerName: string): Promise> { + return this.makeRequest("/api/whitelist/add", { method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: body.toString(), + body: JSON.stringify({ player: playerName }), }); } - async removeFromWhitelist(playerName: string): Promise> { - return this.makeRequest(`/v1/whitelist/remove/${encodeURIComponent(playerName)}`, { + async removeFromWhitelist(playerName: string): Promise> { + return this.makeRequest("/api/whitelist/remove", { method: "POST", + body: JSON.stringify({ player: playerName }), }); } - async getWhitelist(): Promise> { - return this.makeRequest("/v1/whitelist"); + async getWhitelist(): Promise> { + return this.makeRequest("/api/whitelist/list", { + method: "GET", + }); } - async addOp(playerName: string): Promise> { - return this.makeRequest(`/v1/ops/add/${encodeURIComponent(playerName)}`, { + async rewardPlayer(playerName: string): Promise> { + return this.makeRequest("/reward", { method: "POST", + body: JSON.stringify({ player: playerName }), }); } - async removeOp(playerName: string): Promise> { - return this.makeRequest(`/v1/ops/remove/${encodeURIComponent(playerName)}`, { + async executeCommand(command: string): Promise> { + return this.makeRequest("/api/command", { method: "POST", + body: JSON.stringify({ command }), }); } - - async getOps(): Promise> { - return this.makeRequest("/v1/ops"); - } - - async getPlayers(): Promise> { - return this.makeRequest("/v1/players"); - } - - async getServerInfo(): Promise> { - return this.makeRequest("/v1/server"); - } } -export const servertap = new ServerTapClient(); +export const minecraft = new MinecraftClient(); + +// Legacy exports for backward compatibility +export const servertap = minecraft; +export type { MinecraftPlayer as ServerTapPlayer, MinecraftResponse as ServerTapResponse }; diff --git a/src/lib/xbox-server.ts b/src/lib/xbox-server.ts new file mode 100644 index 0000000..d25b039 --- /dev/null +++ b/src/lib/xbox-server.ts @@ -0,0 +1,28 @@ +import { getXboxProfileWithAvatar } from "./xbox"; + +/** + * Server-side helper to fetch Xbox profile data using environment variables + * This function can only be called on the server side + */ +export async function getXboxProfileServer(gamertag: string): Promise<{ + gamertag: string; + avatarUrl: string; + error?: string; +}> { + try { + // Use the modified function that reads from environment variables + const result = await getXboxProfileWithAvatar(gamertag); + + return result; + + } catch (error) { + console.error("Failed to fetch Xbox profile:", error); + + // Return fallback data on error + return { + gamertag, + avatarUrl: "", + error: "Failed to fetch profile data" + }; + } +}