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 @@
+
+
+
Hello {{ me.user?.username }}
TODO: account stub page
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"
+ }
+ ]
}