From fb0efeb4edbeedd31e0eea084ebc29ee2981c821 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Fri, 7 Aug 2026 13:16:43 +0200 Subject: [PATCH] feat: implement auth stores --- nuxt.config.ts | 18 +- package-lock.json | 384 ++++++++++++++++++++++- package.json | 3 + src/components/Navbar/Navbar.vue | 8 +- src/middleware/1.auth.global.ts | 27 ++ src/middleware/2.enforce.global.ts | 13 + src/pages/account/index.vue | 10 + src/plugins/types.d.ts | 7 + src/server/api/account/login/index.ts | 2 +- src/server/api/account/register/index.ts | 2 +- src/server/api/auth/me.get.ts | 18 ++ src/server/middleware/auth.ts | 28 ++ src/server/utils/enforceAuth.ts | 22 ++ src/server/utils/useGrpc.ts | 31 ++ src/stores/auth.ts | 74 +++++ src/stores/me.ts | 25 ++ tsconfig.json | 16 +- 17 files changed, 667 insertions(+), 21 deletions(-) create mode 100644 src/middleware/1.auth.global.ts create mode 100644 src/middleware/2.enforce.global.ts create mode 100644 src/plugins/types.d.ts create mode 100644 src/server/api/auth/me.get.ts create mode 100644 src/server/middleware/auth.ts create mode 100644 src/server/utils/enforceAuth.ts create mode 100644 src/server/utils/useGrpc.ts create mode 100644 src/stores/auth.ts create mode 100644 src/stores/me.ts diff --git a/nuxt.config.ts b/nuxt.config.ts index 1c8f723..537a09c 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -12,6 +12,7 @@ export default defineNuxtConfig({ }, modules: [ + "@pinia/nuxt", '@nuxt/eslint', '@nuxt/fonts', '@nuxt/icon', @@ -26,9 +27,17 @@ export default defineNuxtConfig({ }, runtimeConfig: { + nitro: { + envPrefix: "PN_WEBSITE_", + }, + grpcHost: '', + grpcApiKey: '', + public: { apiBase: 'https://api.pretendo.cc', - hCaptchaSitekey: '' + hCaptchaSitekey: '', + homepageUrl: "#", + cookieSecure: false, } }, @@ -41,6 +50,13 @@ export default defineNuxtConfig({ } }, + components: [ + { + path: "~/components", + pathPrefix: false, + }, + ], + content: { build: { markdown: { diff --git a/package-lock.json b/package-lock.json index e688271..6104780 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,12 @@ "@nuxt/fonts": "^0.14.0", "@nuxt/icon": "^2.4.1", "@nuxtjs/i18n": "^10.6.0", + "@pinia/nuxt": "^1.0.1", + "@pretendonetwork/grpc": "^2.5.4", "better-sqlite3": "^12.11.1", "eslint": "^9.39.5", "feed": "^6.0.0", + "nice-grpc": "^2.1.17", "nuxt": "^4.5.2", "vue": "^3.5.13", "vue-router": "^5.2.0" @@ -470,7 +473,6 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.13.0.tgz", "integrity": "sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==", - "devOptional": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@capsizecss/unpack": { @@ -1319,6 +1321,130 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/@hcaptcha/vue3-hcaptcha": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz", @@ -2209,6 +2335,12 @@ "url": "https://github.com/sponsors/kazupon" } }, + "node_modules/@intlify/vue-i18n-extensions/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/@intlify/vue-i18n-extensions/node_modules/vue-i18n": { "version": "10.0.8", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.8.tgz", @@ -2372,6 +2504,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -4298,6 +4440,21 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pinia/nuxt": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-1.0.1.tgz", + "integrity": "sha512-le5bc7KnlAg7W04XKhIQEGTrIgNPV2eNS5wj62OKfvUGGBAvdQLEoEbxKxuli4q0PPS888waxphNzCuz7ilvDQ==", + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^4.4.8" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "pinia": "^4.0.2" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4383,6 +4540,87 @@ "@typescript-eslint/parser": "^8.20.0" } }, + "node_modules/@pretendonetwork/grpc": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.5.4.tgz", + "integrity": "sha512-spjg6sOSP8z+9T9vqR9RM5guJb7UHYQlFBfb6p6WxpaYHsXlmVIRPeQlR7NTkgpNzARV/4QVpqerBbtLDvN4oA==", + "license": "AGPL-3.0-only", + "dependencies": { + "@bufbuild/protobuf": "^2.2.2", + "nice-grpc-common": "^2.0.2", + "typescript": "^5.7.2" + } + }, + "node_modules/@pretendonetwork/grpc/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", @@ -5475,6 +5713,15 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/parse-path": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", @@ -6404,10 +6651,14 @@ } }, "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } }, "node_modules/@vue/devtools-core": { "version": "8.2.1", @@ -6539,6 +6790,12 @@ "node": ">=6.5" } }, + "node_modules/abort-controller-x": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.5.0.tgz", + "integrity": "sha512-yTt9CI0x+nRfX6BFMenEGP8ooPvErGH6AbFz20C2IeOLIlDsrw/VHpgne3GsCEuTA410IiFiaLVFKmgM4bKEPQ==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -13234,12 +13491,24 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -14460,6 +14729,26 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "license": "MIT" }, + "node_modules/nice-grpc": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.17.tgz", + "integrity": "sha512-pu9xYPlWSeqoYQOCqb2ftQqZi/P2DaI70PSuwnxvoyIRiilaOVtktyPe6LmuXegWB4Ic1sPuDGCnCCASbwzK0w==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.14.0", + "abort-controller-x": "^0.5.0", + "nice-grpc-common": "^2.0.4" + } + }, + "node_modules/nice-grpc-common": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.4.tgz", + "integrity": "sha512-gOEXlD6ShXZMZ8k+49wm/bA2j1+3IKbEFV9WYREJcvZV4kM5L1KkILYTX9VYBzZF2OuYCe1wp8c82bQkvR0fHw==", + "license": "MIT", + "dependencies": { + "ts-error": "^1.0.6" + } + }, "node_modules/nitropack": { "version": "2.13.4", "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.13.4.tgz", @@ -16100,6 +16389,32 @@ "node": ">=6" } }, + "node_modules/pinia": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.2.tgz", + "integrity": "sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "nostics": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/devtools-api": "^8.1.5", + "typescript": ">=5.6.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "@vue/devtools-api": { + "optional": false + }, + "typescript": { + "optional": true + } + } + }, "node_modules/pkg-types": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", @@ -16704,6 +17019,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/protocols": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", @@ -17261,6 +17599,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reserved-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", @@ -19286,6 +19633,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz", + "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==", + "license": "MIT" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -19552,6 +19905,12 @@ "node": ">=22.19.0" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, "node_modules/unenv": { "version": "2.0.0-rc.24", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", @@ -20583,6 +20942,12 @@ "vue": "^3.0.0" } }, + "node_modules/vue-i18n/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/vue-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz", @@ -20696,15 +21061,6 @@ "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", - "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^8.2.1" - } - }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index 81a77a8..cd1592e 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,12 @@ "@nuxt/fonts": "^0.14.0", "@nuxt/icon": "^2.4.1", "@nuxtjs/i18n": "^10.6.0", + "@pinia/nuxt": "^1.0.1", + "@pretendonetwork/grpc": "^2.5.4", "better-sqlite3": "^12.11.1", "eslint": "^9.39.5", "feed": "^6.0.0", + "nice-grpc": "^2.1.17", "nuxt": "^4.5.2", "vue": "^3.5.13", "vue-router": "^5.2.0" diff --git a/src/components/Navbar/Navbar.vue b/src/components/Navbar/Navbar.vue index 596e608..003fe7b 100644 --- a/src/components/Navbar/Navbar.vue +++ b/src/components/Navbar/Navbar.vue @@ -1,8 +1,10 @@ + + diff --git a/src/plugins/types.d.ts b/src/plugins/types.d.ts new file mode 100644 index 0000000..11ed680 --- /dev/null +++ b/src/plugins/types.d.ts @@ -0,0 +1,7 @@ +declare module "#app" { + interface PageMeta { + needsAuth?: boolean; + } +} + +export { }; diff --git a/src/server/api/account/login/index.ts b/src/server/api/account/login/index.ts index 25749ca..27787e9 100644 --- a/src/server/api/account/login/index.ts +++ b/src/server/api/account/login/index.ts @@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => { try { const apiResponse = await $fetch(`/v1/login`, { method: 'POST', - baseURL: useRuntimeConfig(event).apiBase, + baseURL: useRuntimeConfig(event).public.apiBase, body: { ...body, grant_type: 'password' } }); diff --git a/src/server/api/account/register/index.ts b/src/server/api/account/register/index.ts index f27c9bd..1127e0c 100644 --- a/src/server/api/account/register/index.ts +++ b/src/server/api/account/register/index.ts @@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => { try { const apiResponse = await $fetch(`/v1/register`, { method: 'POST', - baseURL: useRuntimeConfig(event).apiBase, + baseURL: useRuntimeConfig(event).public.apiBase, body: body }); diff --git a/src/server/api/auth/me.get.ts b/src/server/api/auth/me.get.ts new file mode 100644 index 0000000..43c5d7d --- /dev/null +++ b/src/server/api/auth/me.get.ts @@ -0,0 +1,18 @@ +import { enforceLoggedIn } from '~/server/utils/enforceAuth'; +import { useGrpc } from '~/server/utils/useGrpc'; + +export interface GetApiAuthMe { + pid: number; + username: string; +} + +export default defineEventHandler(async (event): Promise => { + const auth = enforceLoggedIn(event); + + const grpc = useGrpc(event); + const data = await grpc.getUserData({ pid: auth.pid }); + return { + pid: data.pid, + username: data.username, + } +}); diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts new file mode 100644 index 0000000..22d7a9a --- /dev/null +++ b/src/server/middleware/auth.ts @@ -0,0 +1,28 @@ +import { Metadata } from "nice-grpc"; +import { useGrpc } from "../utils/useGrpc"; + +export default defineEventHandler(async (event) => { + const authHeader = getRequestHeader(event, "authorization"); + event.context.auth = null; + if (authHeader) { + const grpc = useGrpc(event); + try { + const [type, token] = authHeader.split(" ", 2); + if (type !== "Bearer") throw new Error("Invalid token type"); + if (!token) throw new Error("Invalid token"); + const userData = await grpc.getUserData({}, { + metadata: new Metadata({ + 'X-Token': token + }) + }); + + event.context.auth = { + pid: userData.pid, + username: userData.username, + } + } catch (err) { + console.error("Failed to request user data: ", err); + return; // Continue like nothing happened, further steps will validate if authed + } + } +}) diff --git a/src/server/utils/enforceAuth.ts b/src/server/utils/enforceAuth.ts new file mode 100644 index 0000000..63a1230 --- /dev/null +++ b/src/server/utils/enforceAuth.ts @@ -0,0 +1,22 @@ +export type AuthContext = { + pid: number; +} + +export function setAuthContext(event: H3Event, context: AuthContext) { + event.context.auth = context; +} + + +export function getAuthContext(event: H3Event): AuthContext | null { + return event.context.auth ?? null; +} + + +export function enforceLoggedIn(event: H3Event): AuthContext { + const context = getAuthContext(event); + if (!context) throw createError({ + status: 401, + statusText: "This action requires authentication", + }) + return context; +} diff --git a/src/server/utils/useGrpc.ts b/src/server/utils/useGrpc.ts new file mode 100644 index 0000000..ef72165 --- /dev/null +++ b/src/server/utils/useGrpc.ts @@ -0,0 +1,31 @@ +import { APIDefinition } from "@pretendonetwork/grpc/api/api_service"; +import { createChannel, createClient, Channel, type Client, Metadata } from "nice-grpc"; + +let grpc: { channel: Channel, client: Client } | null = null; + +function getGrpcClient(event: H3Event): Client { + if (!grpc) { + const config = useRuntimeConfig(); + if (!config.grpcHost || !config.grpcApiKey) { + throw new Error("GRPC not configured"); + } + + const channel = createChannel(config.grpcHost); + const metadata = new Metadata(); + metadata.append("X-API-Key", config.grpcApiKey); + grpc = { + channel, + client: createClient(APIDefinition, channel, { + "*": { + metadata + } + }) + } + } + + return grpc.client; +} + +export function useGrpc(event: H3Event) { + return getGrpcClient(event); +} diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 0000000..718fcf4 --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,74 @@ +import type { CookieOptions } from "#app"; + +type AuthState = { + accessToken: string; + refreshToken: string; +}; + +// Cookies on pretendo are weird: +// `pretendo::auth` has the real authentication state that the website uses. +// This has been made as secure as possible and must only be used in the website codebase. +// +// Then we have the rest of the cookies: +// - `access_token` +// - `refresh_token` +// - `token_type` +// These are for backwards compatibility. They must be configured exactly as implemented below +// because other services use them (and overwrite them) +// These are not used by the website codebase for security purposes. + +const oldCookieTokenType = "Bearer" +const cookieExpirySec = 7 * 24 * 60 * 60 // 7 days +const oldCookieOptions = { + domain: '.pretendo.network', + secure: false, + httpOnly: false, + maxAge: cookieExpirySec, + refresh: true, + readonly: false, +} satisfies CookieOptions & { readonly: false } + +// Not actually a store, but may as well be +export function useAuthStore() { + const authState = useCookie("pretendo::auth", { + sameSite: "strict", + secure: useRuntimeConfig().public.cookieSecure, + refresh: true, + default: () => null, + }); + const accessTokenCookie = useCookie("access_token", oldCookieOptions); + const refreshTokenCookie = useCookie("refresh_token", oldCookieOptions); + const tokenTypeCookie = useCookie("token_type", oldCookieOptions); + + function getToken() { + return authState.value?.accessToken ?? null; + } + + function refresh() { + authState.value = authState.value; + accessTokenCookie.value = accessTokenCookie.value; + refreshTokenCookie.value = refreshTokenCookie.value; + tokenTypeCookie.value = tokenTypeCookie.value; + } + + function set(val: AuthState | null) { + if (val) { + authState.value = val + accessTokenCookie.value = val.accessToken; + refreshTokenCookie.value = val.refreshToken; + tokenTypeCookie.value = oldCookieTokenType; + } + else { + authState.value = null; + accessTokenCookie.value = null; + refreshTokenCookie.value = null; + tokenTypeCookie.value = null; + } + } + + return { + getToken, + refresh, + set, + }; +} diff --git a/src/stores/me.ts b/src/stores/me.ts new file mode 100644 index 0000000..ef16a23 --- /dev/null +++ b/src/stores/me.ts @@ -0,0 +1,25 @@ +export type Me = { + pid: number; + username: string; +}; + +export const useMeStore = defineStore("me", () => { + const data = ref(null); + const loaded = ref(false); + + function setMe(input: Me | null) { + data.value = input; + loaded.value = true; + } + + function clear() { + data.value = null; + } + + return { + user: computed(() => data.value), + loaded: computed(() => loaded.value), + setMe, + clear, + }; +}); diff --git a/tsconfig.json b/tsconfig.json index 184570e..6ae5970 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,17 @@ { - "extends": "./.nuxt/tsconfig.json" + "files": [], + "references": [ + { + "path": "./.nuxt/tsconfig.app.json" + }, + { + "path": "./.nuxt/tsconfig.server.json" + }, + { + "path": "./.nuxt/tsconfig.shared.json" + }, + { + "path": "./.nuxt/tsconfig.node.json" + } + ] }