feat: implement auth stores

This commit is contained in:
mrjvs
2026-08-07 13:16:43 +02:00
parent 1c10ac1df0
commit fb0efeb4ed
17 changed files with 667 additions and 21 deletions

View File

@@ -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: {

384
package-lock.json generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -1,8 +1,10 @@
<script lang="ts" setup>
const { locales, setLocale } = useI18n();
const me = useMeStore();
const user = computed(() => me.user);
const openDropdown = ref<boolean | string>(false);
const loggedIn = ref(false);
function handleDropdownButton(dropdown: boolean | string) {
if (!openDropdown.value) {
openDropdown.value = dropdown;
@@ -451,7 +453,7 @@ onMounted(() => {
</div>
<div
v-if="loggedIn"
v-if="user"
class="user-widget-wrapper logged-in"
>
<div class="user-widget-toggle">
@@ -491,7 +493,7 @@ onMounted(() => {
</div>
<div
v-if="!loggedIn"
v-if="!user"
class="user-widget-wrapper"
>
<a

View File

@@ -0,0 +1,27 @@
import type { GetApiAuthMe } from "~/server/api/auth/me.get";
export default defineNuxtRouteMiddleware(async () => {
const meStore = useMeStore();
if (meStore.loaded) return; // Already loaded
const authStore = useAuthStore();
const token = authStore.getToken();
if (!token) {
meStore.setMe(null);
return; // No token
}
try {
const res = await $fetch<GetApiAuthMe>('/api/auth/me', {
headers: {
'Authorization': `Bearer ${token}`
}
})
meStore.setMe({
pid: res.pid,
username: res.username,
});
} catch {
meStore.setMe(null);
}
});

View File

@@ -0,0 +1,13 @@
function notAllowed() {
return navigateTo("/");
}
export default defineNuxtRouteMiddleware(async (to) => {
const meStore = useMeStore();
if (!meStore.loaded) throw new Error("Mestore must be loaded before reaching this middleware");
// NeedsAuth
if (to.meta.needsAuth) {
if (!meStore.user) return notAllowed();
}
});

View File

@@ -1,5 +1,15 @@
<script setup lang="ts">
definePageMeta({
needsAuth: true,
})
const me = useMeStore();
</script>
<template>
<div>
<h1>Hello {{ me.user?.username }}</h1>
<p>TODO: account stub page</p>
</div>
</template>

7
src/plugins/types.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare module "#app" {
interface PageMeta {
needsAuth?: boolean;
}
}
export { };

View File

@@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => {
try {
const apiResponse = await $fetch<LoginCCResponse>(`/v1/login`, {
method: 'POST',
baseURL: useRuntimeConfig(event).apiBase,
baseURL: useRuntimeConfig(event).public.apiBase,
body: { ...body, grant_type: 'password' }
});

View File

@@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => {
try {
const apiResponse = await $fetch<RegisterCCResponse>(`/v1/register`, {
method: 'POST',
baseURL: useRuntimeConfig(event).apiBase,
baseURL: useRuntimeConfig(event).public.apiBase,
body: body
});

View File

@@ -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<GetApiAuthMe> => {
const auth = enforceLoggedIn(event);
const grpc = useGrpc(event);
const data = await grpc.getUserData({ pid: auth.pid });
return {
pid: data.pid,
username: data.username,
}
});

View File

@@ -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
}
}
})

View File

@@ -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;
}

View File

@@ -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<APIDefinition> } | null = null;
function getGrpcClient(event: H3Event): Client<APIDefinition> {
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);
}

74
src/stores/auth.ts Normal file
View File

@@ -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<string> & { readonly: false }
// Not actually a store, but may as well be
export function useAuthStore() {
const authState = useCookie<AuthState | null>("pretendo::auth", {
sameSite: "strict",
secure: useRuntimeConfig().public.cookieSecure,
refresh: true,
default: () => null,
});
const accessTokenCookie = useCookie<string | null>("access_token", oldCookieOptions);
const refreshTokenCookie = useCookie<string | null>("refresh_token", oldCookieOptions);
const tokenTypeCookie = useCookie<string | null>("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,
};
}

25
src/stores/me.ts Normal file
View File

@@ -0,0 +1,25 @@
export type Me = {
pid: number;
username: string;
};
export const useMeStore = defineStore("me", () => {
const data = ref<Me | null>(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,
};
});

View File

@@ -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"
}
]
}