From ea19c49ad134f00ea0479505c470a0d717848dd0 Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Tue, 4 Nov 2025 09:32:11 +0100 Subject: [PATCH 01/13] Integrate google drive --- .env.example | 11 + nuxt.config.ts | 19 +- package.json | 3 + pnpm-lock.yaml | 418 +++++++++++++- server/api/auth/google/callback.get.ts | 85 +++ server/api/auth/google/login.get.ts | 30 + server/api/auth/google/logout.post.ts | 41 ++ server/api/auth/google/status.get.ts | 30 + server/api/google-drive/oauth-status.get.ts | 32 ++ server/api/google-drive/token.get.ts | 75 +++ src/assets/icons/wallets/google-drive.svg | 8 + src/components/navigation/AccountSwitcher.vue | 12 +- src/components/navigation/AppSidebar.vue | 11 +- src/components/onboarding/SelectWallet.vue | 7 + .../onboarding/WalletOnboarding.vue | 19 +- .../google-drive/GoogleDriveConnect.vue | 534 ++++++++++++++++++ src/components/settings/GoogleDriveSync.vue | 203 +++++++ src/composables/useGoogleDriveWallet.ts | 73 +++ src/layouts/default.vue | 90 ++- src/pages/index.vue | 4 +- src/pages/settings.vue | 18 + src/stores/settings.store.ts | 151 ++++- src/stores/wallet.store.ts | 47 +- src/utils/wallet/google-drive/provider.ts | 394 +++++++++++++ 24 files changed, 2287 insertions(+), 28 deletions(-) create mode 100644 server/api/auth/google/callback.get.ts create mode 100644 server/api/auth/google/login.get.ts create mode 100644 server/api/auth/google/logout.post.ts create mode 100644 server/api/auth/google/status.get.ts create mode 100644 server/api/google-drive/oauth-status.get.ts create mode 100644 server/api/google-drive/token.get.ts create mode 100644 src/assets/icons/wallets/google-drive.svg create mode 100644 src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue create mode 100644 src/components/settings/GoogleDriveSync.vue create mode 100644 src/composables/useGoogleDriveWallet.ts create mode 100644 src/pages/settings.vue create mode 100644 src/utils/wallet/google-drive/provider.ts diff --git a/.env.example b/.env.example index f119fc5..76458dc 100644 --- a/.env.example +++ b/.env.example @@ -17,5 +17,16 @@ NUXT_PUBLIC_HIVE_CHAIN_ID= NUXT_GOOGLE_APPLICATION_CREDENTIALS_JSON= # Your Google Wallet Issuer ID. You can find it on Google Pay and Wallet Console -> Google Wallet API, as IssuerID NUXT_GOOGLE_WALLET_ISSUER_ID= + +# Google Drive Integration +# Get these from https://console.cloud.google.com/ +GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=your-client-secret +GOOGLE_API_KEY=your-api-key +GOOGLE_DRIVE_STORAGE_FILE=profile_data.json + +# App URL (for OAuth callbacks) +NUXT_PUBLIC_APP_URL=http://localhost:3000 + # For development, you can set a mock server URL # NUXT_PUBLIC_CTOKENS_API_URL=http://localhost:8080/mock-ctokens-api diff --git a/nuxt.config.ts b/nuxt.config.ts index ea17f4d..b48a58b 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -40,12 +40,10 @@ export default defineNuxtConfig({ middleware: 'src/middleware', plugins: 'src/plugins' }, - modules: [ - '@nuxtjs/tailwindcss', - '@nuxtjs/color-mode', - '@nuxt/eslint', - '@pinia/nuxt' - ], + imports: { + dirs: ['src/composables'] + }, + modules: ['@nuxtjs/tailwindcss', '@nuxtjs/color-mode', '@nuxt/eslint', '@pinia/nuxt'], alias: { '@': path.resolve(import.meta.dirname, './src') }, @@ -60,9 +58,14 @@ export default defineNuxtConfig({ hiveNodeEndpoint: 'https://api.hive.blog', hiveChainId: '', snapOrigin: '', - snapVersion: '' + snapVersion: '', + appUrl: process.env.NUXT_PUBLIC_APP_URL || 'http://localhost:3000' }, googleApplicationCredentialsJson: {}, - googleWalletIssuerId: '' + googleWalletIssuerId: process.env.GOOGLE_WALLET_ISSUER_ID || '', + googleClientId: process.env.GOOGLE_CLIENT_ID || '', + googleClientSecret: process.env.GOOGLE_CLIENT_SECRET || '', + googleApiKey: process.env.GOOGLE_API_KEY || '', + googleDriveStorageFile: process.env.GOOGLE_DRIVE_STORAGE_FILE || 'profile_data.json' } }); diff --git a/package.json b/package.json index 01b79f7..15588e4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@hiveio/wax-signers-keychain": "1.28.4-rc0-stable.251125135529", "@hiveio/wax-signers-metamask": "1.28.4-rc0-stable.251125135529", "@hiveio/wax-signers-peakvault": "1.28.4-rc0-stable.251125135529", + "@hiveio/wax-signers-external": "1.28.4-rc0-251117123659", "@mdi/js": "^7.4.47", "@metamask/providers": "^16.0.0", "@mtyszczak-cargo/htm": "^1.3.0", @@ -51,6 +52,7 @@ "@pinia/nuxt": "0.11.2", "@stylistic/eslint-plugin": "^5.2.2", "@tanstack/vue-table": "^8.21.2", + "@types/google.accounts": "^0.0.14", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.13.9", "@types/qrcode": "^1.5.6", @@ -89,6 +91,7 @@ }, "dependencies": { "google-auth-library": "^10.4.2", + "googleapis": "^140.0.0", "jsonwebtoken": "^9.0.2", "qrcode": "^1.5.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4969069..c8240eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: google-auth-library: specifier: ^10.4.2 version: 10.4.2 + googleapis: + specifier: ^140.0.0 + version: 140.0.1 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -37,6 +40,9 @@ importers: '@hiveio/wax': specifier: 1.28.4-rc0-stable.251125135529 version: 1.28.4-rc0-stable.251125135529 + '@hiveio/wax-signers-external': + specifier: 1.28.4-rc0-251117123659 + version: 1.28.4-rc0-251117123659 '@hiveio/wax-signers-keychain': specifier: 1.28.4-rc0-stable.251125135529 version: 1.28.4-rc0-stable.251125135529 @@ -76,6 +82,9 @@ importers: '@tanstack/vue-table': specifier: ^8.21.2 version: 8.21.2(vue@3.5.22(typescript@5.7.3)) + '@types/google.accounts': + specifier: ^0.0.14 + version: 0.0.14 '@types/jsonwebtoken': specifier: ^9.0.10 version: 9.0.10 @@ -376,6 +385,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.0': resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} engines: {node: '>=18'} @@ -388,6 +403,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.0': resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} engines: {node: '>=18'} @@ -400,6 +421,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.0': resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} engines: {node: '>=18'} @@ -412,6 +439,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.0': resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} engines: {node: '>=18'} @@ -424,6 +457,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.0': resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} engines: {node: '>=18'} @@ -436,6 +475,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.0': resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} engines: {node: '>=18'} @@ -448,6 +493,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.0': resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} engines: {node: '>=18'} @@ -460,6 +511,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.0': resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} engines: {node: '>=18'} @@ -472,6 +529,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.0': resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} engines: {node: '>=18'} @@ -484,6 +547,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.0': resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} engines: {node: '>=18'} @@ -496,6 +565,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.0': resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} engines: {node: '>=18'} @@ -508,6 +583,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.0': resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} engines: {node: '>=18'} @@ -520,6 +601,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.0': resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} engines: {node: '>=18'} @@ -532,6 +619,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.0': resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} engines: {node: '>=18'} @@ -544,6 +637,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.0': resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} engines: {node: '>=18'} @@ -556,6 +655,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.0': resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} engines: {node: '>=18'} @@ -568,6 +673,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.0': resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} engines: {node: '>=18'} @@ -580,6 +691,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.0': resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} engines: {node: '>=18'} @@ -592,6 +709,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.0': resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} engines: {node: '>=18'} @@ -604,6 +727,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.0': resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} engines: {node: '>=18'} @@ -616,12 +745,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.11': resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.0': resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} engines: {node: '>=18'} @@ -634,6 +775,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.0': resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} engines: {node: '>=18'} @@ -646,6 +793,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.0': resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} engines: {node: '>=18'} @@ -658,6 +811,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.0': resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} engines: {node: '>=18'} @@ -670,6 +829,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -761,10 +926,17 @@ packages: '@floating-ui/vue@1.1.6': resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==} + '@hiveio/beekeeper@1.28.3-251105100532': + resolution: {integrity: sha1-dRqsIX64FvudhMk0yn8RV9C3tIQ=, tarball: https://gitlab.syncad.com/api/v4/projects/198/packages/npm/@hiveio/beekeeper/-/@hiveio/beekeeper-1.28.3-251105100532.tgz} + engines: {node: ^20.11 || >= 21.2} + '@hiveio/beekeeper@1.28.4-rc0': resolution: {integrity: sha1-qLgaAY/l+gXAdleuIeyaMAo6wxA=, tarball: https://gitlab.syncad.com/api/v4/projects/198/packages/npm/@hiveio/beekeeper/-/@hiveio/beekeeper-1.28.4-rc0.tgz} engines: {node: ^20.11 || >= 21.2} + '@hiveio/wax-signers-external@1.28.4-rc0-251117123659': + resolution: {integrity: sha1-+nVGYNk+4V4uoxo7kXccU4xJxJ8=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax-signers-external/-/@hiveio/wax-signers-external-1.28.4-rc0-251117123659.tgz} + '@hiveio/wax-signers-keychain@1.28.4-rc0-stable.251125135529': resolution: {integrity: sha1-0hRtPo0ZtQqLB0o2dOr9KUkTGeU=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax-signers-keychain/-/@hiveio/wax-signers-keychain-1.28.4-rc0-stable.251125135529.tgz} @@ -774,6 +946,10 @@ packages: '@hiveio/wax-signers-peakvault@1.28.4-rc0-stable.251125135529': resolution: {integrity: sha1-+tq4uF74VA9UOIHFNIQFKzbV+CY=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax-signers-peakvault/-/@hiveio/wax-signers-peakvault-1.28.4-rc0-stable.251125135529.tgz} + '@hiveio/wax@1.28.4-rc0-251117123659': + resolution: {integrity: sha1-S0BuAs6PiyommK8UDUGMvgh3S1U=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax/-/@hiveio/wax-1.28.4-rc0-251117123659.tgz} + engines: {node: ^20.11 || >= 21.2} + '@hiveio/wax@1.28.4-rc0-stable.251125135529': resolution: {integrity: sha1-OE6212Rtqq77FgAGyR3BU2uqIcQ=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax/-/@hiveio/wax-1.28.4-rc0-stable.251125135529.tgz} engines: {node: ^20.11 || >= 21.2} @@ -1892,8 +2068,8 @@ packages: peerDependencies: vue: 3.5.22 - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} @@ -1919,6 +2095,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/google.accounts@0.0.14': + resolution: {integrity: sha512-HqIVkVzpiLWhlajhQQd4rIV7czanFvXblJI2J1fSrL+VKQuQwwZ63m35D/mI0flsqKE6p/hNrAG0Yn4FD6JvNA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3111,6 +3290,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3482,10 +3666,18 @@ packages: resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} engines: {node: '>=10'} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + gaxios@7.1.2: resolution: {integrity: sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA==} engines: {node: '>=18'} + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + gcp-metadata@8.1.1: resolution: {integrity: sha512-dTCcAe9fRQf06ELwel6lWWFrEbstwjUBYEhr5VRGoC+iPDZQucHppCowaIp8b8v92tU1G4X4H3b/Y6zXZxkMsQ==} engines: {node: '>=18'} @@ -3578,10 +3770,26 @@ packages: resolution: {integrity: sha512-EKiQasw6aEdxSovPEf1oBxCEvxjFamZ6MPaVOSPXZMnqKFLo+rrYjHyjKlFfZcXiKi9qAH6cutr5WRqqa1jKhg==} engines: {node: '>=18'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + google-logging-utils@1.1.1: resolution: {integrity: sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==} engines: {node: '>=14'} + googleapis-common@7.2.0: + resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} + engines: {node: '>=14.0.0'} + + googleapis@140.0.1: + resolution: {integrity: sha512-ZGvBX4mQcFXO9ACnVNg6Aqy3KtBPB5zTuue43YVLxwn8HSv8jB7w+uDKoIPSoWuxGROgnj2kbng6acXncOQRNA==} + engines: {node: '>=14.0.0'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3592,6 +3800,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + gtoken@8.0.0: resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} engines: {node: '>=18'} @@ -4918,6 +5130,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -5678,6 +5894,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -6331,153 +6550,231 @@ snapshots: '@esbuild/aix-ppc64@0.25.11': optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/android-arm64@0.25.0': optional: true '@esbuild/android-arm64@0.25.11': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm@0.25.0': optional: true '@esbuild/android-arm@0.25.11': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-x64@0.25.0': optional: true '@esbuild/android-x64@0.25.11': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.25.0': optional: true '@esbuild/darwin-arm64@0.25.11': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.25.0': optional: true '@esbuild/darwin-x64@0.25.11': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.25.0': optional: true '@esbuild/freebsd-arm64@0.25.11': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.25.0': optional: true '@esbuild/freebsd-x64@0.25.11': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.25.0': optional: true '@esbuild/linux-arm64@0.25.11': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm@0.25.0': optional: true '@esbuild/linux-arm@0.25.11': optional: true + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-ia32@0.25.0': optional: true '@esbuild/linux-ia32@0.25.11': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-loong64@0.25.0': optional: true '@esbuild/linux-loong64@0.25.11': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.25.0': optional: true '@esbuild/linux-mips64el@0.25.11': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.25.0': optional: true '@esbuild/linux-ppc64@0.25.11': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.25.0': optional: true '@esbuild/linux-riscv64@0.25.11': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-s390x@0.25.0': optional: true '@esbuild/linux-s390x@0.25.11': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-x64@0.25.0': optional: true '@esbuild/linux-x64@0.25.11': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.25.0': optional: true '@esbuild/netbsd-arm64@0.25.11': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.25.0': optional: true '@esbuild/netbsd-x64@0.25.11': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.25.0': optional: true '@esbuild/openbsd-arm64@0.25.11': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.25.0': optional: true '@esbuild/openbsd-x64@0.25.11': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.25.11': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.25.0': optional: true '@esbuild/sunos-x64@0.25.11': optional: true + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.25.0': optional: true '@esbuild/win32-arm64@0.25.11': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-ia32@0.25.0': optional: true '@esbuild/win32-ia32@0.25.11': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-x64@0.25.0': optional: true '@esbuild/win32-x64@0.25.11': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@eslint-community/eslint-utils@4.7.0(eslint@9.32.0(jiti@2.6.1))': dependencies: eslint: 9.32.0(jiti@2.6.1) @@ -6596,8 +6893,15 @@ snapshots: - '@vue/composition-api' - vue + '@hiveio/beekeeper@1.28.3-251105100532': {} + '@hiveio/beekeeper@1.28.4-rc0': {} + '@hiveio/wax-signers-external@1.28.4-rc0-251117123659': + dependencies: + '@hiveio/beekeeper': 1.28.3-251105100532 + '@hiveio/wax': 1.28.4-rc0-251117123659 + '@hiveio/wax-signers-keychain@1.28.4-rc0-stable.251125135529': dependencies: '@hiveio/wax': 1.28.4-rc0-stable.251125135529 @@ -6614,6 +6918,10 @@ snapshots: dependencies: '@hiveio/wax': 1.28.4-rc0-stable.251125135529 + '@hiveio/wax@1.28.4-rc0-251117123659': + dependencies: + events: 3.3.0 + '@hiveio/wax@1.28.4-rc0-stable.251125135529': dependencies: events: 3.3.0 @@ -7872,7 +8180,7 @@ snapshots: '@tanstack/virtual-core': 3.13.2 vue: 3.5.22(typescript@5.7.3) - '@tsconfig/node10@1.0.11': + '@tsconfig/node10@1.0.12': optional: true '@tsconfig/node12@1.0.11': @@ -7902,6 +8210,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/google.accounts@0.0.14': {} + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -9275,6 +9585,36 @@ snapshots: '@esbuild/win32-ia32': 0.25.11 '@esbuild/win32-x64': 0.25.11 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + optional: true + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -9722,6 +10062,17 @@ snapshots: fuse.js@7.1.0: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + gaxios@7.1.2: dependencies: extend: 3.0.2 @@ -9730,6 +10081,15 @@ snapshots: transitivePeerDependencies: - supports-color + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gcp-metadata@8.1.1: dependencies: gaxios: 7.1.2 @@ -9867,14 +10227,56 @@ snapshots: transitivePeerDependencies: - supports-color + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + google-logging-utils@1.1.1: {} + googleapis-common@7.2.0: + dependencies: + extend: 3.0.2 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + qs: 6.14.0 + url-template: 2.0.8 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + googleapis@140.0.1: + dependencies: + google-auth-library: 9.15.1 + googleapis-common: 7.2.0 + transitivePeerDependencies: + - encoding + - supports-color + gopd@1.2.0: {} graceful-fs@4.2.11: {} graphemer@1.4.0: {} + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gtoken@8.0.0: dependencies: gaxios: 7.1.2 @@ -11404,6 +11806,10 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -12059,7 +12465,7 @@ snapshots: ts-node@10.9.2(@swc/core@1.3.78(@swc/helpers@0.5.15))(@types/node@22.13.9)(typescript@5.7.3): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 + '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 @@ -12090,7 +12496,7 @@ snapshots: tsx@4.19.3: dependencies: - esbuild: 0.25.11 + esbuild: 0.25.12 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -12342,6 +12748,8 @@ snapshots: dependencies: punycode: 2.3.1 + url-template@2.0.8: {} + util-deprecate@1.0.2: {} uuid@9.0.1: {} diff --git a/server/api/auth/google/callback.get.ts b/server/api/auth/google/callback.get.ts new file mode 100644 index 0000000..362c195 --- /dev/null +++ b/server/api/auth/google/callback.get.ts @@ -0,0 +1,85 @@ +import { google } from 'googleapis'; + +/** + * GET /api/auth/google/callback + * Handle Google OAuth callback + */ + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const query = getQuery(event); + + const code = query.code as string; + const error = query.error as string; + + if (error) + return sendRedirect(event, `${config.public.appUrl}/?error=${encodeURIComponent(error)}`); + + + if (!code) + return sendRedirect(event, `${config.public.appUrl}/?error=no_code`); + + + try { + const oauth2Client = new google.auth.OAuth2( + config.googleClientId, + config.googleClientSecret, + `${config.public.appUrl}/api/auth/google/callback` + ); + + // Exchange code for tokens + const { tokens } = await oauth2Client.getToken(code); + + if (!tokens.access_token) + throw new Error('No access token received'); + + + // Set credentials + oauth2Client.setCredentials(tokens); + + // Get user info + const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client }); + const userInfo = await oauth2.userinfo.get(); + + // Store tokens in httpOnly cookies + setCookie(event, 'google_access_token', tokens.access_token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7 // 7 days + }); + + if (tokens.refresh_token) { + setCookie(event, 'google_refresh_token', tokens.refresh_token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 30 // 30 days + }); + } else { + // If no refresh token in response, check if we have an old one + // This can happen when user re-authenticates without revoking access + const existingRefreshToken = getCookie(event, 'google_refresh_token'); + if (!existingRefreshToken) + console.warn('No refresh token received and no existing refresh token found'); + } + + // Store user info in a readable cookie (not sensitive) + setCookie(event, 'google_user', JSON.stringify({ + name: userInfo.data.name, + email: userInfo.data.email, + picture: userInfo.data.picture + }), { + httpOnly: false, // Readable by client + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7 + }); + + // Redirect back to app + return sendRedirect(event, `${config.public.appUrl}/?auth=success`); + } catch (err) { + console.error('OAuth callback error:', err); + return sendRedirect(event, `${config.public.appUrl}/?error=auth_failed`); + } +}); diff --git a/server/api/auth/google/login.get.ts b/server/api/auth/google/login.get.ts new file mode 100644 index 0000000..9acf894 --- /dev/null +++ b/server/api/auth/google/login.get.ts @@ -0,0 +1,30 @@ +import { google } from 'googleapis'; + +/** + * GET /api/auth/google/login + * Initiate Google OAuth flow + */ + +export default defineEventHandler((event) => { + const config = useRuntimeConfig(); + + const oauth2Client = new google.auth.OAuth2( + config.googleClientId, + config.googleClientSecret, + `${config.public.appUrl}/api/auth/google/callback` + ); + + // Generate the URL to redirect to for Google OAuth + const authUrl = oauth2Client.generateAuthUrl({ + access_type: 'offline', // Get refresh token + scope: [ + 'https://www.googleapis.com/auth/drive.appdata', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email' + ], + prompt: 'consent', // Force consent screen to always get refresh token + include_granted_scopes: true + }); + + return sendRedirect(event, authUrl); +}); diff --git a/server/api/auth/google/logout.post.ts b/server/api/auth/google/logout.post.ts new file mode 100644 index 0000000..2db012a --- /dev/null +++ b/server/api/auth/google/logout.post.ts @@ -0,0 +1,41 @@ +import { google } from 'googleapis'; + +/** + * POST /api/auth/google/logout + * Logout from Google and clear session + */ + +export default defineEventHandler(async (event) => { + try { + const accessToken = getCookie(event, 'google_access_token'); + + // Revoke Google token if exists + if (accessToken) { + try { + const oauth2Client = new google.auth.OAuth2(); + oauth2Client.setCredentials({ access_token: accessToken }); + await oauth2Client.revokeCredentials(); + } catch (err) { + console.error('Error revoking Google token:', err); + // Continue with logout even if revoke fails + } + } + + // Clear all cookies + deleteCookie(event, 'google_access_token'); + deleteCookie(event, 'google_refresh_token'); + deleteCookie(event, 'google_user'); + + return { + success: true, + message: 'Logged out successfully' + }; + } catch (error) { + console.error('Logout error:', error); + + throw createError({ + statusCode: 500, + message: 'Failed to logout' + }); + } +}); diff --git a/server/api/auth/google/status.get.ts b/server/api/auth/google/status.get.ts new file mode 100644 index 0000000..cecc691 --- /dev/null +++ b/server/api/auth/google/status.get.ts @@ -0,0 +1,30 @@ +/** + * GET /api/auth/google/status + * Check Google authentication status + */ + +export default defineEventHandler((event) => { + const accessToken = getCookie(event, 'google_access_token'); + const userCookie = getCookie(event, 'google_user'); + + if (!accessToken) { + return { + authenticated: false, + user: null + }; + } + + let user = null; + if (userCookie) { + try { + user = JSON.parse(userCookie); + } catch (err) { + console.error('Error parsing user cookie:', err); + } + } + + return { + authenticated: true, + user + }; +}); diff --git a/server/api/google-drive/oauth-status.get.ts b/server/api/google-drive/oauth-status.get.ts new file mode 100644 index 0000000..0bec5dc --- /dev/null +++ b/server/api/google-drive/oauth-status.get.ts @@ -0,0 +1,32 @@ +/** + * GET /api/google-drive/oauth-status + * Check Google Drive OAuth authentication status + * This is an alias for /api/auth/google/status + */ + +export default defineEventHandler((event) => { + const accessToken = getCookie(event, 'google_access_token'); + const userCookie = getCookie(event, 'google_user'); + + if (!accessToken) { + return { + authenticated: false, + user: null + }; + } + + let user = null; + if (userCookie) { + try { + user = JSON.parse(userCookie); + } + catch (err) { + console.error('Error parsing user cookie:', err); + } + } + + return { + authenticated: true, + user + }; +}); diff --git a/server/api/google-drive/token.get.ts b/server/api/google-drive/token.get.ts new file mode 100644 index 0000000..8764750 --- /dev/null +++ b/server/api/google-drive/token.get.ts @@ -0,0 +1,75 @@ +import { google } from 'googleapis'; + +/** + * GET /api/google-drive/token + * Get fresh Google access token for Google Drive operations + * This endpoint handles token refresh automatically + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + + const accessToken = getCookie(event, 'google_access_token'); + const refreshToken = getCookie(event, 'google_refresh_token'); + + if (!accessToken) { + throw createError({ + statusCode: 401, + message: 'Not authenticated with Google' + }); + } + + try { + // Setup OAuth2 client with token refresh capability + const oauth2Client = new google.auth.OAuth2( + config.googleClientId, + config.googleClientSecret, + `${config.public.appUrl}/api/auth/google/callback` + ); + + oauth2Client.setCredentials({ + access_token: accessToken, + refresh_token: refreshToken + }); + + // Handle token refresh - update cookies with new tokens + oauth2Client.on('tokens', (tokens) => { + if (tokens.access_token) { + setCookie(event, 'google_access_token', tokens.access_token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7 + }); + } + if (tokens.refresh_token) { + setCookie(event, 'google_refresh_token', tokens.refresh_token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 30 + }); + } + }); + + // This will trigger token refresh if needed + const currentToken = oauth2Client.credentials.access_token; + + if (!currentToken) { + throw createError({ + statusCode: 401, + message: 'Failed to get valid access token' + }); + } + + return { + success: true, + token: currentToken + }; + } catch (error) { + const err = error as { statusCode?: number; message?: string }; + throw createError({ + statusCode: err.statusCode || 500, + message: err.message || 'Failed to get access token' + }); + } +}); diff --git a/src/assets/icons/wallets/google-drive.svg b/src/assets/icons/wallets/google-drive.svg new file mode 100644 index 0000000..34c92d4 --- /dev/null +++ b/src/assets/icons/wallets/google-drive.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/navigation/AccountSwitcher.vue b/src/components/navigation/AccountSwitcher.vue index 0461051..49828a1 100644 --- a/src/components/navigation/AccountSwitcher.vue +++ b/src/components/navigation/AccountSwitcher.vue @@ -8,12 +8,13 @@ import cTokensLogoUrl from '@/assets/icons/wallets/ctokens.svg'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { useSettingsStore } from '@/stores/settings.store'; +import { useSettingsStore, UsedWallet } from '@/stores/settings.store'; import { useTokensStore } from '@/stores/tokens.store'; import { useUserStore } from '@/stores/user.store'; import { useWalletStore } from '@/stores/wallet.store'; import { toastError } from '@/utils/parse-error'; import CTokensProvider from '@/utils/wallet/ctokens/signer'; +import GoogleDriveWalletProvider from '@/utils/wallet/google-drive/provider'; const settingsStore = useSettingsStore(); const walletStore = useWalletStore(); @@ -34,6 +35,11 @@ const hasHiveAccount = computed(() => // HTM account data const hasHTMAccount = computed(() => tokensStore.wallet !== undefined); +// Check if current Hive account uses Google Drive wallet +const isGoogleDriveWallet = computed(() => + hasHiveAccount.value && settingsStore.settings.wallet === UsedWallet.GOOGLE_DRIVE +); + const htmUserMetadata = ref<{ displayName: string; about?: string; @@ -104,6 +110,10 @@ const connectToHTM = async () => { // Disconnect from Hive const disconnectFromHive = async () => { try { + // If Google Drive wallet, logout from Google + if (isGoogleDriveWallet.value) + await GoogleDriveWalletProvider.logout(); + settingsStore.resetSettings(); walletStore.resetWallet(); userStore.resetSettings(); diff --git a/src/components/navigation/AppSidebar.vue b/src/components/navigation/AppSidebar.vue index dc01cfc..241a941 100644 --- a/src/components/navigation/AppSidebar.vue +++ b/src/components/navigation/AppSidebar.vue @@ -1,5 +1,5 @@ diff --git a/src/stores/account-name-prompt.store.ts b/src/stores/account-name-prompt.store.ts new file mode 100644 index 0000000..fdf5b65 --- /dev/null +++ b/src/stores/account-name-prompt.store.ts @@ -0,0 +1,46 @@ +import { defineStore } from 'pinia'; + +export class AccountNameEntryCancelledError extends Error { + public constructor () { + super('User cancelled account name entry'); + this.name = 'AccountNameEntryCancelledError'; + } +} + +type AccountNameResolver = (accountName: string) => void; +type AccountNameRejecter = (error: Error) => void; + +export const useAccountNamePromptStore = defineStore('accountNamePrompt', { + state: () => ({ + isOpen: false, + _resolve: null as AccountNameResolver | null, + _reject: null as AccountNameRejecter | null + }), + actions: { + requestAccountName (): Promise { + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + this.isOpen = true; + }); + }, + + submitAccountName (accountName: string) { + if (this._resolve) { + this._resolve(accountName); + this._resolve = null; + this._reject = null; + } + this.isOpen = false; + }, + + cancel () { + if (this._reject) { + this._reject(new AccountNameEntryCancelledError()); + this._resolve = null; + this._reject = null; + } + this.isOpen = false; + } + } +}); diff --git a/src/stores/recovery-password.store.ts b/src/stores/recovery-password.store.ts new file mode 100644 index 0000000..9fc412c --- /dev/null +++ b/src/stores/recovery-password.store.ts @@ -0,0 +1,46 @@ +import { defineStore } from 'pinia'; + +export class PasswordEntryCancelledError extends Error { + public constructor () { + super('User cancelled password entry'); + this.name = 'PasswordEntryCancelledError'; + } +} + +type PasswordResolver = (password: string) => void; +type PasswordRejecter = (error: Error) => void; + +export const useRecoveryPasswordStore = defineStore('recoveryPassword', { + state: () => ({ + isOpen: false, + _resolve: null as PasswordResolver | null, + _reject: null as PasswordRejecter | null + }), + actions: { + requestPassword (): Promise { + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + this.isOpen = true; + }); + }, + + submitPassword (password: string) { + if (this._resolve) { + this._resolve(password); + this._resolve = null; + this._reject = null; + } + this.isOpen = false; + }, + + cancel () { + if (this._reject) { + this._reject(new PasswordEntryCancelledError()); + this._resolve = null; + this._reject = null; + } + this.isOpen = false; + } + } +}); diff --git a/src/stores/wallet.store.ts b/src/stores/wallet.store.ts index 60d9fd4..4c8fe46 100644 --- a/src/stores/wallet.store.ts +++ b/src/stores/wallet.store.ts @@ -139,7 +139,7 @@ export const useWalletStore = defineStore('wallet', { break; } case UsedWallet.GOOGLE_DRIVE: { - currentWallet.value = await GoogleDriveWalletProvider.for(role); + currentWallet.value = await GoogleDriveWalletProvider.for(settings.account!, role); this.isL2Wallet = false; break; diff --git a/src/utils/wallet/google-drive/provider.ts b/src/utils/wallet/google-drive/provider.ts index af55be7..d9a9b2d 100644 --- a/src/utils/wallet/google-drive/provider.ts +++ b/src/utils/wallet/google-drive/provider.ts @@ -1,40 +1,98 @@ -import type { TPublicKey, TRole } from '@hiveio/wax'; -import { ExternalSignatureProvider, type ExternalWalletSigner } from '@hiveio/wax-signers-external'; +import type { TPublicKey, TRole, TAccountName, AEncryptionProvider } from '@hiveio/wax'; +import { createExternalWallet, type IExternalWalletContent, type IExternalWallet, type TStorageEncryptionCredentials } from '@hiveio/wax-signers-external'; + +import { useAccountNamePromptStore } from '@/stores/account-name-prompt.store'; +import { useRecoveryPasswordStore, PasswordEntryCancelledError } from '@/stores/recovery-password.store'; const WALLET_FILE_NAME = 'hivebridge_wallet.json'; +const LOCAL_ENCRYPTION_KEY_STORAGE = 'hivebridge_encryption_key_wif'; + +// Track if we're currently creating a new wallet +let pendingWalletCreationPassword: string | undefined; + +export class RecoveryPasswordRequiredError extends Error { + public constructor () { + super('RECOVERY_PASSWORD_REQUIRED: Please provide your recovery password to access the wallet.'); + this.name = 'RecoveryPasswordRequiredError'; + } +} + +export { AccountNameEntryCancelledError } from '@/stores/account-name-prompt.store'; -/** - * Token provider callback - fetches fresh OAuth token from server - */ const tokenProvider = async (): Promise => { const response = await $fetch<{ success: boolean; token: string }>('/api/google-drive/token'); return response.token; }; -let providerInstance: ExternalSignatureProvider | null = null; +let walletInstance: IExternalWallet | null = null; -/** - * Get or create the ExternalSignatureProvider instance - */ -async function getProvider (): Promise { - if (!providerInstance) { - const { getWax } = await import('@/stores/wax.store'); +const getStoredEncryptionKey = (): string | undefined => { + if (typeof window === 'undefined') + return undefined; + return localStorage.getItem(LOCAL_ENCRYPTION_KEY_STORAGE) ?? undefined; +}; - const chain = await getWax(); +const setStoredEncryptionKey = (keyWif: string): void => { + if (typeof window === 'undefined') + return; + localStorage.setItem(LOCAL_ENCRYPTION_KEY_STORAGE, keyWif); +}; - providerInstance = new ExternalSignatureProvider(chain, WALLET_FILE_NAME, tokenProvider); +const clearStoredEncryptionKey = (): void => { + if (typeof window === 'undefined') + return; + localStorage.removeItem(LOCAL_ENCRYPTION_KEY_STORAGE); +}; + +const storagePasswordProvider = async (missingStorageFile: boolean): Promise => { + // If we're creating a new wallet, use the pending password + if (missingStorageFile && pendingWalletCreationPassword) { + const password = pendingWalletCreationPassword; + + return { password }; + } + + // Check localStorage for cached encryption key + const storedKey = getStoredEncryptionKey(); + + if (storedKey) { + // We have the encryption key stored - return it directly + return { encryptionKey: storedKey }; } - return providerInstance; + // No key cached - prompt user via dialog + if (!missingStorageFile) { + // Wallet file exists but we don't have the encryption key cached + const recoveryPasswordStore = useRecoveryPasswordStore(); + const password = await recoveryPasswordStore.requestPassword(); + + // Don't store yet - we'll derive and store the key after wallet is loaded + return { password }; + } + + // Missing file and no pending password - this shouldn't happen + throw new Error('Recovery password must be set before creating a new wallet'); +}; + +async function getWallet (): Promise { + if (!walletInstance) { + const { getWax } = await import('@/stores/wax.store'); + const wax = await getWax(); + walletInstance = await createExternalWallet( + wax, + tokenProvider, + storagePasswordProvider, + WALLET_FILE_NAME + ); + } + + return walletInstance; } // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class GoogleDriveWalletProvider { private constructor () {} - /** - * Check if user is authenticated with Google - */ public static async isAuthenticated (): Promise { try { const response = await $fetch<{ authenticated: boolean }>('/api/google-drive/oauth-status'); @@ -44,168 +102,283 @@ export class GoogleDriveWalletProvider { } } - /** - * Get OAuth login URL - */ public static getLoginUrl (): string { return '/api/auth/google/login'; } /** * Create a new wallet and save to Google Drive + * @param accountName - The Hive account name + * @param key - The private key for the role + * @param role - The role for this key (posting, active, owner, memo) + * @param recoveryPassword - User's recovery password for wallet encryption */ public static async createWallet ( accountName: string, - keys: { posting?: string; active?: string; owner?: string; memo?: string } - ): Promise<{ [K in TRole]?: TPublicKey }> { + key: string, + role: TRole, + recoveryPassword: string + ): Promise { if (!await GoogleDriveWalletProvider.isAuthenticated()) throw new Error('Not authenticated with Google'); - const provider = await getProvider(); - const publicKeys: { [K in TRole]?: TPublicKey } = {}; - const roles: TRole[] = ['posting', 'active', 'owner', 'memo']; + if (!recoveryPassword) + throw new Error('Recovery password is required to create wallet'); - for (const role of roles) { - const privateKey = keys[role]; - if (!privateKey) continue; + // Set pending password so callback can use it when creating new wallet + pendingWalletCreationPassword = recoveryPassword; - const signer = await provider.createWalletFor(role, accountName, privateKey); - publicKeys[role] = signer.publicKey; + // Reset wallet instance to ensure fresh creation with new password + if (walletInstance) { + await walletInstance.close(); + walletInstance = null; } - return publicKeys; + try { + const wallet = await getWallet(); + const content = await wallet.createForHiveKey(role, accountName, key); + + // Extract and store the encryption key WIF for future automatic decryption + const encryptionKeyWif = wallet.getEncryptionKeyWif(); + setStoredEncryptionKey(encryptionKeyWif); + + // Clear pending password after successfully storing the key + pendingWalletCreationPassword = undefined; + + return content; + } catch (error) { + // Clear pending password on error + pendingWalletCreationPassword = undefined; + throw error; + } } /** * Load wallet from Google Drive + * @param accountName - The Hive account name to load wallet for */ - public static async loadWallet (): Promise<{ accountName: string; roles: TRole[] }> { + public static async loadWallet (accountName: TAccountName, role: TRole): Promise<{ accountName: string; role?: TRole }> { if (!await GoogleDriveWalletProvider.isAuthenticated()) throw new Error('Not authenticated with Google'); - const provider = await getProvider(); - return await provider.loadWallet(); + const wallet = await getWallet(); + const content = await wallet.loadForHiveKey(accountName, role); + + // Extract and store the encryption key WIF if not already stored + if (!getStoredEncryptionKey()) { + const encryptionKeyWif = wallet.getEncryptionKeyWif(); + setStoredEncryptionKey(encryptionKeyWif); + } + + // Get all roles from enumerated keys + const r = [...content.enumStoredHiveKeys(accountName, role)][0]?.role; + + return { accountName, role: r }; } /** * Get wallet info without loading keys into memory + * @param accountName - The Hive account name to check */ - public static async getWalletInfo (): Promise<{ + public static async getWalletInfo (accountName: TAccountName, role: TRole): Promise<{ exists: boolean; accountName?: string; - roles?: TRole[]; + role?: TRole; }> { - try { - if (!await GoogleDriveWalletProvider.isAuthenticated()) - return { exists: false }; + if (!await GoogleDriveWalletProvider.isAuthenticated()) + return { exists: false }; - const provider = await getProvider(); + // Verify Google authentication with a test API call and check if wallet file exists + try { + const [_, walletFileCheck] = await Promise.all([ + $fetch('/api/google-drive/verify-auth'), + $fetch<{ exists: boolean; fileId?: string | null }>('/api/google-drive/check-wallet-file') + ]); - const exists = await provider.hasWallet(); - if (!exists) + // If wallet file doesn't exist, return early without trying to load it + if (!walletFileCheck.exists) return { exists: false }; + } catch (error) { + const err = error as { statusCode?: number; statusMessage?: string; data?: { statusMessage?: string } }; + + // Check if it's an auth expired error + if (err.statusCode === 401 || err.statusMessage === 'GOOGLE_AUTH_EXPIRED' || err.data?.statusMessage === 'GOOGLE_AUTH_EXPIRED') { + // Throw error with specific code so UI can handle re-authentication + const authError = new Error('Google authentication has expired. Please log in again.'); + (authError as Error & { code: string }).code = 'GOOGLE_AUTH_EXPIRED'; + throw authError; + } + + // Re-throw other errors + throw error; + } - const accountName = await provider.getAccountName(); - const loadResult = await provider.loadWallet(); + // Wallet file exists, now try to load it + const wallet = await getWallet(); + + try { + const content = await wallet.loadForHiveKey(accountName, role); + const r = [...content.enumStoredHiveKeys(accountName, role)][0]?.role; return { exists: true, accountName, - roles: loadResult.roles + role: r }; - } catch { + } catch (error) { + // Re-throw PasswordEntryCancelledError so UI can handle it + if (error instanceof PasswordEntryCancelledError) + throw error; + + // Wallet file doesn't exist or account not found return { exists: false }; } } /** - * Delete wallet from Google Drive + * Get all configured roles for an account by probing each role individually + * @param accountName - The Hive account name to check + * @returns Array of configured roles */ - public static async deleteWallet (): Promise { + public static async getAllConfiguredRoles (accountName: TAccountName): Promise { if (!await GoogleDriveWalletProvider.isAuthenticated()) - throw new Error('Not authenticated with Google'); + return []; + + const allRoles: TRole[] = ['posting', 'active', 'owner', 'memo']; + const configuredRoles: TRole[] = []; + const wallet = await getWallet(); + + for (const role of allRoles) { + try { + // Try to load this specific role + await wallet.loadForHiveKey(accountName, role); + configuredRoles.push(role); + } catch (error) { + // Re-throw PasswordEntryCancelledError so UI can handle it + if (error instanceof PasswordEntryCancelledError) + throw error; + + // Role doesn't exist in wallet - continue checking others + } + } - const provider = await getProvider(); - await provider.deleteWallet(); - providerInstance = null; + return configuredRoles; } /** - * Logout and clear session + * Delete wallet from Google Drive + * @param accountName - The Hive account name */ + // public static async deleteWallet (): Promise { + // if (!await GoogleDriveWalletProvider.isAuthenticated()) + // throw new Error('Not authenticated with Google'); + + // const wallet = await getWallet(); + // await wallet.deleteStorageFile(); + // // const content = await wallet.loadForHiveKey(accountName, role); + // // await content.clearContents(true); + + // // Reset wallet instance after deletion + // await wallet.close(); + // walletInstance = null; + // } + + public static setEncryptionKey (keyWif: string): void { + setStoredEncryptionKey(keyWif); + } + + public static getEncryptionKey (): string | undefined { + return getStoredEncryptionKey(); + } + + public static clearEncryptionKey (): void { + clearStoredEncryptionKey(); + } + + public static hasEncryptionKey (): boolean { + return !!getStoredEncryptionKey(); + } + public static async logout (): Promise { await $fetch('/api/auth/google/logout', { method: 'POST' }); - if (providerInstance) { - await providerInstance.destroy(); - providerInstance = null; + if (walletInstance) { + await walletInstance.close(); + walletInstance = null; } + } /** - * Get provider for a specific role (switches active role) - * This is called by wallet.store.ts createWalletFor() - * Returns the ExternalWalletSigner with the specified role active + * Get wallet content for a specific role + * Returns the IExternalWalletContent with the specified role active * * If the requested role is not available, falls back to the first available role. * Priority order: posting > active > owner > memo + * + * @param accountName - The Hive account name + * @param role - The role to load (posting, active, owner, or memo) */ - public static async for (role: TRole): Promise { - const provider = await getProvider(); - - // Check if the requested role exists - const hasRequestedRole = await provider.hasRole(role); + public static async for (accountName: TAccountName, role: TRole): Promise { + const wallet = await getWallet(); - if (hasRequestedRole) - return await provider.for(role); - - // Fall back to first available role - const loadResult = await provider.loadWallet(); - - if (!loadResult.roles || loadResult.roles.length === 0) - throw new Error('No wallet found or wallet has no keys'); + try { + // Try to load the requested role + return await wallet.loadForHiveKey(accountName, role) as unknown as AEncryptionProvider; + } catch { + // Fall back to loading any available key for this account + const content = await wallet.loadForHiveKey(accountName, role); + const roles = [...content.enumStoredHiveKeys(accountName)]; - const firstRole = loadResult.roles[0]; - if (!firstRole) - throw new Error('No roles available in wallet'); + if (roles.length === 0) + throw new Error('No wallet found or wallet has no keys'); - return await provider.for(firstRole); + return content as unknown as AEncryptionProvider; + } } - - /** * Add a new key for a specific role to the wallet * If the role already has a key, it will be overwritten * + * @param accountName - The Hive account name * @param role - The role to add the key for (posting, active, owner, or memo) * @param privateKey - The private key to add - * @returns The public key corresponding to the added private key + * @returns Object with public key */ - public static async addKey (role: TRole, privateKey: string): Promise { + public static async addKey (accountName: TAccountName, role: TRole, privateKey: string): Promise<{ publicKey: TPublicKey }> { if (!await GoogleDriveWalletProvider.isAuthenticated()) throw new Error('Not authenticated with Google'); - const provider = await getProvider(); + const wallet = await getWallet(); + const content = await wallet.createForHiveKey(role, accountName, privateKey); + + // Extract and store the encryption key WIF if not already stored + if (!getStoredEncryptionKey()) { + const encryptionKeyWif = wallet.getEncryptionKeyWif(); + setStoredEncryptionKey(encryptionKeyWif); + } - // Get current wallet account name - const accountName = await provider.getAccountName(); + // Get public key from enumerated keys + const keyInfo = [...content.enumStoredHiveKeys(accountName, role)][0]; + if (!keyInfo) + throw new Error('Failed to add key'); - const signer = await provider.createWalletFor(role, accountName, privateKey); - return signer.publicKey; + return { publicKey: keyInfo.publicKey }; } + // TODO: Add Key removal once it is supported by the wax-signers-external library + /** - * Remove a key for a specific role from the wallet + * Request account name from user via dialog + * Used when checking wallet existence but no account name is stored in settings * - * @param role - The role to remove (posting, active, owner, or memo) + * @returns Promise - The account name entered by user + * @throws AccountNameEntryCancelledError if user cancels */ - public static async removeKey (role: TRole): Promise { - if (!await GoogleDriveWalletProvider.isAuthenticated()) - throw new Error('Not authenticated with Google'); - - const provider = await getProvider(); - await provider.removeKey(role); + public static async requestAccountName (): Promise { + const accountNamePromptStore = useAccountNamePromptStore(); + return await accountNamePromptStore.requestAccountName(); } } -- GitLab From 490e14f331c26c08edcf975bb63bffa3ae893a02 Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Wed, 3 Dec 2025 13:45:54 +0100 Subject: [PATCH 05/13] Update .env.example file --- .env.example | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 76458dc..d3d79b4 100644 --- a/.env.example +++ b/.env.example @@ -20,10 +20,14 @@ NUXT_GOOGLE_WALLET_ISSUER_ID= # Google Drive Integration # Get these from https://console.cloud.google.com/ -GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=your-client-secret -GOOGLE_API_KEY=your-api-key -GOOGLE_DRIVE_STORAGE_FILE=profile_data.json +# 1. Enable Google Drive API in APIs & Services -> Library +# 2. Go to APIs & Services -> Credentials -> Create Credentials -> OAuth client ID +NUXT_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com +NUXT_GOOGLE_CLIENT_SECRET=your-client-secret +# 3. Go to APIs & Services -> Credentials -> Create Credentials -> API key +NUXT_GOOGLE_API_KEY=your-api-key +# This is the filename used to store data in the user's Google Drive App Data folder +NUXT_GOOGLE_DRIVE_STORAGE_FILE=profile_data.json # App URL (for OAuth callbacks) NUXT_PUBLIC_APP_URL=http://localhost:3000 -- GitLab From 4cf0b4e7b2e3d6aafcd31aa92ced1fb5a9907eec Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Thu, 4 Dec 2025 13:00:49 +0100 Subject: [PATCH 06/13] Improve managing google wallet keys view --- server/api/auth/google/callback.get.ts | 11 +- server/api/auth/google/login.get.ts | 5 +- src/components/navigation/AppSidebar.vue | 2 +- .../google-drive/GoogleDriveConnect.vue | 5 +- .../settings/GoogleDriveKeyManager.vue | 223 +++++++++++------- src/composables/useGoogleDriveWallet.ts | 9 +- src/pages/settings.vue | 102 +++++++- src/stores/settings.store.ts | 5 +- src/utils/wallet/google-drive/provider.ts | 78 +++++- 9 files changed, 338 insertions(+), 102 deletions(-) diff --git a/server/api/auth/google/callback.get.ts b/server/api/auth/google/callback.get.ts index 362c195..9239cf8 100644 --- a/server/api/auth/google/callback.get.ts +++ b/server/api/auth/google/callback.get.ts @@ -11,13 +11,14 @@ export default defineEventHandler(async (event) => { const code = query.code as string; const error = query.error as string; + const returnUrl = (query.state as string) || '/'; if (error) - return sendRedirect(event, `${config.public.appUrl}/?error=${encodeURIComponent(error)}`); + return sendRedirect(event, `${config.public.appUrl}${returnUrl}?error=${encodeURIComponent(error)}`); if (!code) - return sendRedirect(event, `${config.public.appUrl}/?error=no_code`); + return sendRedirect(event, `${config.public.appUrl}${returnUrl}?error=no_code`); try { @@ -76,10 +77,10 @@ export default defineEventHandler(async (event) => { maxAge: 60 * 60 * 24 * 7 }); - // Redirect back to app - return sendRedirect(event, `${config.public.appUrl}/?auth=success`); + // Redirect back to the page user was on + return sendRedirect(event, `${config.public.appUrl}${returnUrl}?auth=success`); } catch (err) { console.error('OAuth callback error:', err); - return sendRedirect(event, `${config.public.appUrl}/?error=auth_failed`); + return sendRedirect(event, `${config.public.appUrl}${returnUrl}?error=auth_failed`); } }); diff --git a/server/api/auth/google/login.get.ts b/server/api/auth/google/login.get.ts index 9acf894..a0e97b8 100644 --- a/server/api/auth/google/login.get.ts +++ b/server/api/auth/google/login.get.ts @@ -7,6 +7,8 @@ import { google } from 'googleapis'; export default defineEventHandler((event) => { const config = useRuntimeConfig(); + const query = getQuery(event); + const returnUrl = (query.returnUrl as string) || '/'; const oauth2Client = new google.auth.OAuth2( config.googleClientId, @@ -23,7 +25,8 @@ export default defineEventHandler((event) => { 'https://www.googleapis.com/auth/userinfo.email' ], prompt: 'consent', // Force consent screen to always get refresh token - include_granted_scopes: true + include_granted_scopes: true, + state: returnUrl // Pass returnUrl through OAuth flow }); return sendRedirect(event, authUrl); diff --git a/src/components/navigation/AppSidebar.vue b/src/components/navigation/AppSidebar.vue index 1f9e213..3a5ef08 100644 --- a/src/components/navigation/AppSidebar.vue +++ b/src/components/navigation/AppSidebar.vue @@ -122,7 +122,7 @@ const mainGroups: { title: string; items: Array<{ title: string; url: string; ic title: 'Settings', items: [ { - title: 'Settings', + title: 'Google Drive Wallet', url: '/settings', icon: mdiCog } diff --git a/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue b/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue index 8532d3a..5b7005a 100644 --- a/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue +++ b/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue @@ -125,8 +125,9 @@ const close = () => { }; const openGoogleLogin = () => { - // Redirect to Google OAuth login endpoint - window.location.href = GoogleDriveProvider.getLoginUrl(); + // Redirect to Google OAuth login endpoint with current path + const currentPath = window.location.pathname; + window.location.href = GoogleDriveProvider.getLoginUrl(currentPath); }; const logoutFromGoogle = async () => { diff --git a/src/components/settings/GoogleDriveKeyManager.vue b/src/components/settings/GoogleDriveKeyManager.vue index 03f36f9..1407ad6 100644 --- a/src/components/settings/GoogleDriveKeyManager.vue +++ b/src/components/settings/GoogleDriveKeyManager.vue @@ -1,6 +1,6 @@ diff --git a/src/stores/settings.store.ts b/src/stores/settings.store.ts index f5d735e..63ab3e8 100644 --- a/src/stores/settings.store.ts +++ b/src/stores/settings.store.ts @@ -118,8 +118,9 @@ export const useSettingsStore = defineStore('settings', { } }, - loginWithGoogle () { - window.location.href = '/api/auth/google/login'; + loginWithGoogle (returnUrl?: string) { + const currentUrl = returnUrl || window.location.pathname; + window.location.href = `/api/auth/google/login?returnUrl=${encodeURIComponent(currentUrl)}`; }, async logoutFromGoogle () { diff --git a/src/utils/wallet/google-drive/provider.ts b/src/utils/wallet/google-drive/provider.ts index d9a9b2d..63573ba 100644 --- a/src/utils/wallet/google-drive/provider.ts +++ b/src/utils/wallet/google-drive/provider.ts @@ -102,8 +102,11 @@ export class GoogleDriveWalletProvider { } } - public static getLoginUrl (): string { - return '/api/auth/google/login'; + public static getLoginUrl (returnUrl?: string): string { + const url = '/api/auth/google/login'; + if (returnUrl) + return `${url}?returnUrl=${encodeURIComponent(returnUrl)}`; + return url; } /** @@ -220,6 +223,12 @@ export class GoogleDriveWalletProvider { const content = await wallet.loadForHiveKey(accountName, role); const r = [...content.enumStoredHiveKeys(accountName, role)][0]?.role; + // Extract and store the encryption key WIF if not already stored + if (!getStoredEncryptionKey()) { + const encryptionKeyWif = wallet.getEncryptionKeyWif(); + setStoredEncryptionKey(encryptionKeyWif); + } + return { exists: true, accountName, @@ -247,12 +256,20 @@ export class GoogleDriveWalletProvider { const allRoles: TRole[] = ['posting', 'active', 'owner', 'memo']; const configuredRoles: TRole[] = []; const wallet = await getWallet(); + let keyStored = false; for (const role of allRoles) { try { // Try to load this specific role await wallet.loadForHiveKey(accountName, role); configuredRoles.push(role); + + // Extract and store the encryption key WIF if not already stored (only once) + if (!keyStored && !getStoredEncryptionKey()) { + const encryptionKeyWif = wallet.getEncryptionKeyWif(); + setStoredEncryptionKey(encryptionKeyWif); + keyStored = true; + } } catch (error) { // Re-throw PasswordEntryCancelledError so UI can handle it if (error instanceof PasswordEntryCancelledError) @@ -265,6 +282,40 @@ export class GoogleDriveWalletProvider { return configuredRoles; } + /** + * Get public key for a specific role + * @param accountName - The Hive account name + * @param role - The role to get public key for + * @returns Object with public key or null if not found + */ + public static async getPublicKeyForRole (accountName: TAccountName, role: TRole): Promise<{ publicKey: TPublicKey } | null> { + if (!await GoogleDriveWalletProvider.isAuthenticated()) + return null; + + try { + const wallet = await getWallet(); + const content = await wallet.loadForHiveKey(accountName, role); + const keyInfo = [...content.enumStoredHiveKeys(accountName, role)][0]; + + // Extract and store the encryption key WIF if not already stored + if (!getStoredEncryptionKey()) { + const encryptionKeyWif = wallet.getEncryptionKeyWif(); + setStoredEncryptionKey(encryptionKeyWif); + } + + if (!keyInfo) + return null; + + return { publicKey: keyInfo.publicKey }; + } catch (error) { + // Re-throw PasswordEntryCancelledError so UI can handle it + if (error instanceof PasswordEntryCancelledError) + throw error; + + return null; + } + } + /** * Delete wallet from Google Drive * @param accountName - The Hive account name @@ -307,6 +358,13 @@ export class GoogleDriveWalletProvider { walletInstance = null; } + // Update settings store to reflect logged out state + const { useSettingsStore } = await import('@/stores/settings.store'); + const settingsStore = useSettingsStore(); + settingsStore.isGoogleAuthenticated = false; + settingsStore.googleUser = null; + settingsStore.settings.googleDriveSync = false; + settingsStore.saveSettings(); } /** @@ -324,7 +382,15 @@ export class GoogleDriveWalletProvider { try { // Try to load the requested role - return await wallet.loadForHiveKey(accountName, role) as unknown as AEncryptionProvider; + const content = await wallet.loadForHiveKey(accountName, role) as unknown as AEncryptionProvider; + + // Extract and store the encryption key WIF if not already stored + if (!getStoredEncryptionKey()) { + const encryptionKeyWif = wallet.getEncryptionKeyWif(); + setStoredEncryptionKey(encryptionKeyWif); + } + + return content; } catch { // Fall back to loading any available key for this account const content = await wallet.loadForHiveKey(accountName, role); @@ -333,6 +399,12 @@ export class GoogleDriveWalletProvider { if (roles.length === 0) throw new Error('No wallet found or wallet has no keys'); + // Extract and store the encryption key WIF if not already stored + if (!getStoredEncryptionKey()) { + const encryptionKeyWif = wallet.getEncryptionKeyWif(); + setStoredEncryptionKey(encryptionKeyWif); + } + return content as unknown as AEncryptionProvider; } } -- GitLab From 1042f516a578f2795efdee3d16315117f5917036 Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Fri, 5 Dec 2025 12:02:50 +0100 Subject: [PATCH 07/13] Add remove key functionality --- .../settings/GoogleDriveKeyManager.vue | 97 +++++++++++++++++++ src/composables/useGoogleDriveWallet.ts | 5 +- src/utils/wallet/google-drive/provider.ts | 44 ++++++++- 3 files changed, 144 insertions(+), 2 deletions(-) diff --git a/src/components/settings/GoogleDriveKeyManager.vue b/src/components/settings/GoogleDriveKeyManager.vue index 1407ad6..07bea3e 100644 --- a/src/components/settings/GoogleDriveKeyManager.vue +++ b/src/components/settings/GoogleDriveKeyManager.vue @@ -34,10 +34,13 @@ const rolePublicKeys = ref>({} as Record); // Dialog state const showAddDialog = ref(false); const showDeleteWalletDialog = ref(false); +const showDeleteKeyDialog = ref(false); const roleToAdd = ref(null); +const roleToDelete = ref(null); const newPrivateKey = ref(''); const showPrivateKey = ref(false); const isSavingKey = ref(false); +const isDeletingKey = ref(false); // Available roles const allRoles: TRole[] = ['posting', 'active', 'owner', 'memo']; @@ -178,6 +181,35 @@ const handleDeleteWallet = async () => { } }; +// Delete key dialog +const openDeleteKeyDialog = (role: TRole) => { + roleToDelete.value = role; + showDeleteKeyDialog.value = true; +}; + +const closeDeleteKeyDialog = () => { + showDeleteKeyDialog.value = false; + roleToDelete.value = null; +}; + +const handleDeleteKey = async () => { + if (!roleToDelete.value || !hiveAccountName.value) return; + + isDeletingKey.value = true; + try { + const publicKey = rolePublicKeys.value[roleToDelete.value]; + await googleDrive.removeKey(hiveAccountName.value, publicKey, roleToDelete.value); + toast.success(`${roleToDelete.value} key removed successfully`); + closeDeleteKeyDialog(); + await loadWalletInfo(); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to remove key'; + toast.error(message); + } finally { + isDeletingKey.value = false; + } +}; + onMounted(() => { loadWalletInfo(); }); @@ -281,6 +313,14 @@ onMounted(() => { Configured +
@@ -493,5 +533,62 @@ onMounted(() => { + + + + + + + + Remove {{ roleToDelete }} Key? + + + This will remove the {{ roleToDelete }} key from your wallet. + + + + + + Make sure you have a backup of this key before removing it. Once removed, you'll need to add it again to use {{ roleToDelete }} permissions. + + + +
+
+ + Public Key to Remove +
+
+ + {{ rolePublicKeys[roleToDelete] }} + +
+
+ + + + + +
+
diff --git a/src/composables/useGoogleDriveWallet.ts b/src/composables/useGoogleDriveWallet.ts index 89a730e..44397be 100644 --- a/src/composables/useGoogleDriveWallet.ts +++ b/src/composables/useGoogleDriveWallet.ts @@ -50,7 +50,9 @@ export function useGoogleDriveWallet () { return await GoogleDriveWalletProvider.addKey(accountName, role, privateKey); }; - // TODO: Add key removal once it is supported by the wax-signers-external library + const removeKey = async (accountName: TAccountName, publicKey?: TPublicKey, role?: TRole): Promise => { + return await GoogleDriveWalletProvider.removeKey(accountName, publicKey, role); + }; const setEncryptionKey = (keyWif: string): void => { GoogleDriveWalletProvider.setEncryptionKey(keyWif); @@ -88,6 +90,7 @@ export function useGoogleDriveWallet () { getPublicKeyForRole, logout, addKey, + removeKey, setEncryptionKey, getEncryptionKey, clearEncryptionKey, diff --git a/src/utils/wallet/google-drive/provider.ts b/src/utils/wallet/google-drive/provider.ts index 63573ba..0692e4c 100644 --- a/src/utils/wallet/google-drive/provider.ts +++ b/src/utils/wallet/google-drive/provider.ts @@ -439,7 +439,49 @@ export class GoogleDriveWalletProvider { return { publicKey: keyInfo.publicKey }; } - // TODO: Add Key removal once it is supported by the wax-signers-external library + /** + * Remove a key from the wallet + * Can remove by role or by public key + * + * @param accountName - The Hive account name + * @param roleOrPublicKey - Either a role (posting, active, owner, memo) or a public key string + * @throws Error if not authenticated or key not found + */ + public static async removeKey (accountName: TAccountName, publicKey?: TPublicKey, role?: TRole): Promise { + if (!await GoogleDriveWalletProvider.isAuthenticated()) + throw new Error('Not authenticated with Google'); + + const wallet = await getWallet(); + + if (role !== undefined) { + const content = await wallet.loadForHiveKey(accountName, role); + const keyInfo = [...content.enumStoredHiveKeys(accountName, role)][0]; + + if (!keyInfo) + throw new Error(`No key found for ${accountName}@${role}`); + + if (publicKey !== undefined && keyInfo.publicKey !== publicKey) + throw new Error(`Public key mismatch for ${accountName}@${role}`); + + await content.removeKey(keyInfo); + return; + } + + if (publicKey !== undefined) { + // Remove by public key - try loading with any available role + const rolesToTry: TRole[] = ['posting', 'active', 'owner', 'memo']; + + for (const role of rolesToTry) { + try { + const content = await wallet.loadForHiveKey(accountName, role); + await content.removeKey(publicKey); + return; + } catch {} + } + + throw new Error(`Could not load wallet for account ${accountName} to remove key`); + } + } /** * Request account name from user via dialog -- GitLab From 6495df745f8f77db02ff8f6169b81095f00324bd Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Fri, 5 Dec 2025 12:54:55 +0100 Subject: [PATCH 08/13] Separate checking google auth status function from default layout --- src/layouts/default.vue | 130 +++--------------- .../checkGoogleAuthAndLoadWallet.ts | 126 +++++++++++++++++ 2 files changed, 144 insertions(+), 112 deletions(-) create mode 100644 src/utils/wallet/google-drive/checkGoogleAuthAndLoadWallet.ts diff --git a/src/layouts/default.vue b/src/layouts/default.vue index 8513219..079719d 100644 --- a/src/layouts/default.vue +++ b/src/layouts/default.vue @@ -8,15 +8,12 @@ import AppSidebar from '@/components/navigation'; import AppHeader from '@/components/navigation/AppHeader.vue'; import RecoveryPasswordDialog from '@/components/RecoveryPasswordDialog.vue'; import { SidebarProvider } from '@/components/ui/sidebar'; -import { AccountNameEntryCancelledError } from '@/stores/account-name-prompt.store'; -import { PasswordEntryCancelledError } from '@/stores/recovery-password.store'; -import type { UsedWallet } from '@/stores/settings.store'; -import { useSettingsStore, UsedWallet as UsedWalletEnum } from '@/stores/settings.store'; +import type { UsedWallet , UsedWallet as UsedWalletEnum } from '@/stores/settings.store'; +import { useSettingsStore } from '@/stores/settings.store'; import { useUserStore } from '@/stores/user.store'; import { useWalletStore } from '@/stores/wallet.store'; import { toastError } from '@/utils/parse-error'; -import GoogleDriveWalletProvider from '@/utils/wallet/google-drive/provider'; - +import checkGoogleAuthAndLoadWallet from '@/utils/wallet/google-drive/checkGoogleAuthAndLoadWallet'; const route = useRoute(); @@ -34,118 +31,27 @@ const settingsStore = useSettingsStore(); const walletStore = useWalletStore(); const userStore = useUserStore(); -const checkGoogleAuthAndLoadWallet = async () => { - // Check if we just returned from Google OAuth - const urlParams = new URLSearchParams(window.location.search); - const authStatus = urlParams.get('auth'); - - if (authStatus === 'success') { - // Remove the auth parameter from URL - const newUrl = window.location.pathname; - window.history.replaceState({}, document.title, newUrl); - - try { - // Reset user store to show loading state - userStore.resetSettings(); - - // Check if authenticated with Google - const isAuthenticated = await GoogleDriveWalletProvider.isAuthenticated(); - - if (isAuthenticated) { - // Check if we have a saved account name from previous session - let savedAccount = settingsStore.settings.account; - - // Check sessionStorage for account name from OAuth flow - if (!savedAccount) { - const sessionAccountName = sessionStorage.getItem('google_drive_account_name'); - if (sessionAccountName) { - savedAccount = sessionAccountName; - sessionStorage.removeItem('google_drive_account_name'); - } - } - - // If still no saved account, ask user for account name ONCE - if (!savedAccount) { - try { - savedAccount = await GoogleDriveWalletProvider.requestAccountName(); - // Store in sessionStorage in case of errors during wallet loading - sessionStorage.setItem('google_drive_account_name', savedAccount); - } catch (error) { - // User cancelled account name entry - show onboarding with prefilled account - if (error instanceof AccountNameEntryCancelledError) { - toast.info('Google Drive connected. Please create a wallet.'); - preselectedWallet.value = UsedWalletEnum.GOOGLE_DRIVE; - prefilledAccountName.value = undefined; - walletStore.openWalletSelectModal(); - return; - } - throw error; - } - } - - if (savedAccount) { - // Try to load wallet for saved account - const walletInfo = await GoogleDriveWalletProvider.getWalletInfo(savedAccount, 'posting'); - - if (walletInfo.exists && walletInfo.accountName) { - toast.success('Google Drive connected successfully'); - - // Load the wallet - const result = await GoogleDriveWalletProvider.loadWallet(savedAccount, 'posting'); - - // Save settings - const settings = { - account: result.accountName, - wallet: UsedWalletEnum.GOOGLE_DRIVE, - googleDriveSync: settingsStore.settings.googleDriveSync || false, - lastGoogleSyncTime: settingsStore.settings.lastGoogleSyncTime - }; - - settingsStore.setSettings(settings); - hasUser.value = true; - - // Create wallet and load user data - await walletStore.createWalletFor(settings, 'posting'); - await userStore.parseUserData(result.accountName); - - // Clear sessionStorage after successful load - sessionStorage.removeItem('google_drive_account_name'); - toast.success(`Wallet loaded: ${result.accountName}`); - } else { - // No wallet found - open wallet creation modal with Google Drive preselected and account name - toast.info('Google Drive connected. Please create a wallet.'); - preselectedWallet.value = UsedWalletEnum.GOOGLE_DRIVE; - prefilledAccountName.value = savedAccount; - // Keep in sessionStorage for error recovery - sessionStorage.setItem('google_drive_account_name', savedAccount); - walletStore.openWalletSelectModal(); - } - } - } - } catch (error) { - // Ignore if user cancelled password entry - if (error instanceof PasswordEntryCancelledError) - return; - - // Ignore if user cancelled account name entry - if (error instanceof AccountNameEntryCancelledError) - return; - - // eslint-disable-next-line no-console - console.error('Failed to load Google Drive wallet:', error); - toast.error('Failed to load wallet from Google Drive'); - } - } -}; - onMounted(async () => { isLoading.value = true; settingsStore.loadSettings(); // Check for Google OAuth callback first - await checkGoogleAuthAndLoadWallet(); + const googleAuthResult = await checkGoogleAuthAndLoadWallet(); + + // Update state from Google auth result + if (googleAuthResult.hasUser) + hasUser.value = googleAuthResult.hasUser; + else + hasUser.value = settingsStore.settings.account !== undefined; + + + if (googleAuthResult.preselectedWallet) + preselectedWallet.value = googleAuthResult.preselectedWallet; + + + if (googleAuthResult.prefilledAccountName) + prefilledAccountName.value = googleAuthResult.prefilledAccountName; - hasUser.value = settingsStore.settings.account !== undefined; if (hasUser.value) { walletStore.createWalletFor(settingsStore.settings, 'posting').then(() => { userStore.parseUserData(settingsStore.settings.account!).catch(error => { diff --git a/src/utils/wallet/google-drive/checkGoogleAuthAndLoadWallet.ts b/src/utils/wallet/google-drive/checkGoogleAuthAndLoadWallet.ts new file mode 100644 index 0000000..66c0e64 --- /dev/null +++ b/src/utils/wallet/google-drive/checkGoogleAuthAndLoadWallet.ts @@ -0,0 +1,126 @@ +import { toast } from 'vue-sonner'; + +import { PasswordEntryCancelledError } from '@/stores/recovery-password.store'; +import { useSettingsStore, UsedWallet as UsedWalletEnum } from '@/stores/settings.store'; +import { useUserStore } from '@/stores/user.store'; +import { useWalletStore } from '@/stores/wallet.store'; +import { toastError } from '@/utils/parse-error'; +import GoogleDriveWalletProvider, { AccountNameEntryCancelledError } from '@/utils/wallet/google-drive/provider'; + +const userStore = useUserStore(); +const settingsStore = useSettingsStore(); +const walletStore = useWalletStore(); + +interface GoogleAuthResult { + hasUser: boolean; + preselectedWallet?: UsedWalletEnum; + prefilledAccountName?: string; +} + +export default async function (): Promise { + // Default return values + const result: GoogleAuthResult = { + hasUser: false, + preselectedWallet: undefined, + prefilledAccountName: undefined + }; + + // Check if we just returned from Google OAuth + const urlParams = new URLSearchParams(window.location.search); + const authStatus = urlParams.get('auth'); + + if (authStatus === 'success') { + // Remove the auth parameter from URL + const newUrl = window.location.pathname; + window.history.replaceState({}, document.title, newUrl); + + try { + // Reset user store to show loading state + userStore.resetSettings(); + + // Check if authenticated with Google + const isAuthenticated = await GoogleDriveWalletProvider.isAuthenticated(); + + if (isAuthenticated) { + // Check if we have a saved account name from previous session + let savedAccount = settingsStore.settings.account; + + // Check sessionStorage for account name from OAuth flow + if (!savedAccount) { + const sessionAccountName = sessionStorage.getItem('google_drive_account_name'); + if (sessionAccountName) { + savedAccount = sessionAccountName; + sessionStorage.removeItem('google_drive_account_name'); + } + } + + // If still no saved account, ask user for account name ONCE + if (!savedAccount) { + try { + savedAccount = await GoogleDriveWalletProvider.requestAccountName(); + // Store in sessionStorage in case of errors during wallet loading + sessionStorage.setItem('google_drive_account_name', savedAccount); + } catch (error) { + // User cancelled account name entry - show onboarding with prefilled account + if (error instanceof AccountNameEntryCancelledError) { + result.preselectedWallet = UsedWalletEnum.GOOGLE_DRIVE; + result.prefilledAccountName = undefined; + walletStore.openWalletSelectModal(); + return result; + } + throw error; + } + } + + if (savedAccount) { + // Try to load wallet for saved account + const walletInfo = await GoogleDriveWalletProvider.getWalletInfo(savedAccount, 'posting'); + + if (walletInfo.exists && walletInfo.accountName) { + toast.success('Google Drive connected successfully'); + + // Load the wallet + const loadResult = await GoogleDriveWalletProvider.loadWallet(savedAccount, 'posting'); + + // Save settings + const settings = { + account: loadResult.accountName, + wallet: UsedWalletEnum.GOOGLE_DRIVE, + googleDriveSync: settingsStore.settings.googleDriveSync || false, + lastGoogleSyncTime: settingsStore.settings.lastGoogleSyncTime + }; + + settingsStore.setSettings(settings); + result.hasUser = true; + + // Create wallet and load user data + await walletStore.createWalletFor(settings, 'posting'); + await userStore.parseUserData(loadResult.accountName); + + // Clear sessionStorage after successful load + sessionStorage.removeItem('google_drive_account_name'); + toast.success(`Wallet loaded: ${loadResult.accountName}`); + } else { + result.preselectedWallet = UsedWalletEnum.GOOGLE_DRIVE; + result.prefilledAccountName = savedAccount; + // Keep in sessionStorage for error recovery + sessionStorage.setItem('google_drive_account_name', savedAccount); + walletStore.openWalletSelectModal(); + } + } + } + } catch (error) { + // Ignore if user cancelled password entry + if (error instanceof PasswordEntryCancelledError) + return result; + + // Ignore if user cancelled account name entry + if (error instanceof AccountNameEntryCancelledError) + return result; + + toastError('Failed to load wallet from Google Drive', error); + } + } + + return result; +}; -- GitLab From 7aa0ecacc4fb198070502cf7186721e8c99ac49b Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Fri, 5 Dec 2025 13:00:39 +0100 Subject: [PATCH 09/13] Move checking google auth status in cookies to the client side --- server/api/auth/google/status.get.ts | 30 ---------------- server/api/google-drive/oauth-status.get.ts | 32 ----------------- .../google-drive/GoogleDriveConnect.vue | 10 +++--- src/composables/useGoogleAuth.ts | 35 +++++++++++++++++++ src/stores/settings.store.ts | 10 +++--- src/stores/wallet.store.ts | 8 ++--- src/utils/wallet/google-drive/provider.ts | 6 ++-- 7 files changed, 53 insertions(+), 78 deletions(-) delete mode 100644 server/api/auth/google/status.get.ts delete mode 100644 server/api/google-drive/oauth-status.get.ts create mode 100644 src/composables/useGoogleAuth.ts diff --git a/server/api/auth/google/status.get.ts b/server/api/auth/google/status.get.ts deleted file mode 100644 index cecc691..0000000 --- a/server/api/auth/google/status.get.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * GET /api/auth/google/status - * Check Google authentication status - */ - -export default defineEventHandler((event) => { - const accessToken = getCookie(event, 'google_access_token'); - const userCookie = getCookie(event, 'google_user'); - - if (!accessToken) { - return { - authenticated: false, - user: null - }; - } - - let user = null; - if (userCookie) { - try { - user = JSON.parse(userCookie); - } catch (err) { - console.error('Error parsing user cookie:', err); - } - } - - return { - authenticated: true, - user - }; -}); diff --git a/server/api/google-drive/oauth-status.get.ts b/server/api/google-drive/oauth-status.get.ts deleted file mode 100644 index 0bec5dc..0000000 --- a/server/api/google-drive/oauth-status.get.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * GET /api/google-drive/oauth-status - * Check Google Drive OAuth authentication status - * This is an alias for /api/auth/google/status - */ - -export default defineEventHandler((event) => { - const accessToken = getCookie(event, 'google_access_token'); - const userCookie = getCookie(event, 'google_user'); - - if (!accessToken) { - return { - authenticated: false, - user: null - }; - } - - let user = null; - if (userCookie) { - try { - user = JSON.parse(userCookie); - } - catch (err) { - console.error('Error parsing user cookie:', err); - } - } - - return { - authenticated: true, - user - }; -}); diff --git a/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue b/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue index 5b7005a..597e013 100644 --- a/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue +++ b/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue @@ -164,12 +164,10 @@ async function checkWalletStatus () { // Fetch Google user info try { - const statusResponse = await fetch('/api/auth/google/status'); - if (statusResponse.ok) { - const statusData = await statusResponse.json(); - if (statusData.authenticated && statusData.user) - googleUser.value = statusData.user; - } + const { getAuthStatus } = useGoogleAuth(); + const statusData = getAuthStatus(); + if (statusData.authenticated && statusData.user) + googleUser.value = statusData.user; } catch (_e) {} // Get saved account name from multiple sources diff --git a/src/composables/useGoogleAuth.ts b/src/composables/useGoogleAuth.ts new file mode 100644 index 0000000..4de3ad9 --- /dev/null +++ b/src/composables/useGoogleAuth.ts @@ -0,0 +1,35 @@ +import { toastError } from '@/utils/parse-error'; + +export const useGoogleAuth = () => { + const getAuthStatus = () => { + const accessToken = useCookie('google_access_token'); + const userCookie = useCookie('google_user'); + + if (!accessToken.value) { + return { + authenticated: false, + user: null + }; + } + + let user = null; + if (userCookie.value) { + try { + user = typeof userCookie.value === 'string' + ? JSON.parse(userCookie.value) + : userCookie.value; + } catch (err) { + toastError('Error parsing user cookie', err); + } + } + + return { + authenticated: true, + user + }; + }; + + return { + getAuthStatus + }; +}; diff --git a/src/stores/settings.store.ts b/src/stores/settings.store.ts index 63ab3e8..7c577a5 100644 --- a/src/stores/settings.store.ts +++ b/src/stores/settings.store.ts @@ -5,6 +5,7 @@ import googleDriveLogoUrl from '@/assets/icons/wallets/google-drive.svg'; import keychainLogoUrl from '@/assets/icons/wallets/keychain.svg'; import metamaskLogoUrl from '@/assets/icons/wallets/metamask.svg'; import peakVaultLogoUrl from '@/assets/icons/wallets/peakvault.svg'; +import { toastError } from '@/utils/parse-error'; export enum UsedWallet { METAMASK, @@ -109,7 +110,8 @@ export const useSettingsStore = defineStore('settings', { // Google Drive integration async checkGoogleAuth () { try { - const response = await $fetch<{ authenticated: boolean; user: GoogleUser | null }>('/api/auth/google/status'); + const { getAuthStatus } = useGoogleAuth(); + const response = getAuthStatus(); this.isGoogleAuthenticated = response.authenticated; this.googleUser = response.user; } catch (_error) { @@ -131,7 +133,7 @@ export const useSettingsStore = defineStore('settings', { this.settings.googleDriveSync = false; this.saveSettings(); } catch (error) { - console.error('Failed to logout from Google:', error); + toastError('Failed to logout from Google', error); throw error; } }, @@ -159,7 +161,7 @@ export const useSettingsStore = defineStore('settings', { ? ((error as { data?: { message?: string } }).data?.message || 'Failed to sync to Google Drive') : 'Failed to sync to Google Drive'; this.lastSyncError = errorMessage; - console.error('Sync to Google Drive failed:', error); + toastError('Sync to Google Drive failed', error); throw error; } finally { this.isSyncing = false; @@ -200,7 +202,7 @@ export const useSettingsStore = defineStore('settings', { ? ((error as { data?: { message?: string } }).data?.message || 'Failed to sync from Google Drive') : 'Failed to sync from Google Drive'; this.lastSyncError = errorMessage; - console.error('Sync from Google Drive failed:', error); + toastError('Sync from Google Drive failed', error); throw error; } finally { this.isSyncing = false; diff --git a/src/stores/wallet.store.ts b/src/stores/wallet.store.ts index 4c8fe46..8082337 100644 --- a/src/stores/wallet.store.ts +++ b/src/stores/wallet.store.ts @@ -58,8 +58,8 @@ export const useWalletStore = defineStore('wallet', { if (shouldCheckGoogleDrive) { state._lastGoogleDriveCheck = now; try { - const response = await fetch('/api/google-drive/oauth-status'); - const data = await response.json(); + const { getAuthStatus } = useGoogleAuth(); + const data = getAuthStatus(); state._walletsStatus.googleDrive = data.authenticated || false; } catch { @@ -82,8 +82,8 @@ export const useWalletStore = defineStore('wallet', { // Force recheck by resetting the timer this._lastGoogleDriveCheck = 0; try { - const response = await fetch('/api/google-drive/oauth-status'); - const data = await response.json(); + const { getAuthStatus } = useGoogleAuth(); + const data = getAuthStatus(); this._walletsStatus.googleDrive = data.authenticated || false; } catch { diff --git a/src/utils/wallet/google-drive/provider.ts b/src/utils/wallet/google-drive/provider.ts index 0692e4c..1b235fa 100644 --- a/src/utils/wallet/google-drive/provider.ts +++ b/src/utils/wallet/google-drive/provider.ts @@ -95,8 +95,10 @@ export class GoogleDriveWalletProvider { public static async isAuthenticated (): Promise { try { - const response = await $fetch<{ authenticated: boolean }>('/api/google-drive/oauth-status'); - return response.authenticated; + const { getAuthStatus } = useGoogleAuth(); + const { authenticated } = getAuthStatus(); + + return authenticated; } catch { return false; } -- GitLab From 90138593b2e269ab33fab203148940d21046f320 Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Fri, 5 Dec 2025 13:14:13 +0100 Subject: [PATCH 10/13] Use $fetch instead of fetch in AddToGoogleWallet component --- .../wallets/google-drive/GoogleDriveConnect.vue | 3 +-- src/components/wallet/AddToGoogleWallet.vue | 16 ++++++---------- src/stores/wallet.store.ts | 6 ++---- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue b/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue index 597e013..385a48c 100644 --- a/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue +++ b/src/components/onboarding/wallets/google-drive/GoogleDriveConnect.vue @@ -164,8 +164,7 @@ async function checkWalletStatus () { // Fetch Google user info try { - const { getAuthStatus } = useGoogleAuth(); - const statusData = getAuthStatus(); + const statusData = await $fetch<{ authenticated: boolean; user?: GoogleUser | null }>('/api/auth/google/status'); if (statusData.authenticated && statusData.user) googleUser.value = statusData.user; } catch (_e) {} diff --git a/src/components/wallet/AddToGoogleWallet.vue b/src/components/wallet/AddToGoogleWallet.vue index 44355b5..2d294e2 100644 --- a/src/components/wallet/AddToGoogleWallet.vue +++ b/src/components/wallet/AddToGoogleWallet.vue @@ -16,22 +16,18 @@ const addToGoogleWallet = async () => { try { const { operationalKey, name } = await tokensStore.getCurrentUserMetadata(); const baseUrl = window.location.origin; - const res = await fetch('/api/google-wallet', { + const data = await $fetch<{ url?: string; error?: boolean; message?: string }>('/api/google-wallet', { method: 'POST', - - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ + body: { operationalPublicKey: operationalKey, displayName: name || 'User', baseUrl - }) + } }); - const data = await res.text(); - const parsed = JSON.parse(data); - if (parsed.url && !parsed.error) - window.open(parsed.url, '_blank'); + if (data.url && !data.error) + window.open(data.url, '_blank'); else - toastError('Failed to generate Google Wallet pass', new Error(parsed.message || data, { cause: parsed })); + toastError('Failed to generate Google Wallet pass', new Error(data.message || 'Unknown error', { cause: data })); } catch (error) { toastError('Error generating Google Wallet pass', error); } finally { diff --git a/src/stores/wallet.store.ts b/src/stores/wallet.store.ts index 8082337..89bf0d1 100644 --- a/src/stores/wallet.store.ts +++ b/src/stores/wallet.store.ts @@ -58,8 +58,7 @@ export const useWalletStore = defineStore('wallet', { if (shouldCheckGoogleDrive) { state._lastGoogleDriveCheck = now; try { - const { getAuthStatus } = useGoogleAuth(); - const data = getAuthStatus(); + const data = await $fetch('/api/auth/google/status'); state._walletsStatus.googleDrive = data.authenticated || false; } catch { @@ -82,8 +81,7 @@ export const useWalletStore = defineStore('wallet', { // Force recheck by resetting the timer this._lastGoogleDriveCheck = 0; try { - const { getAuthStatus } = useGoogleAuth(); - const data = getAuthStatus(); + const data = await $fetch('/api/auth/google/status'); this._walletsStatus.googleDrive = data.authenticated || false; } catch { -- GitLab From 028d3524fec27ed674419b031a7b01675f6e0b25 Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Fri, 5 Dec 2025 13:51:41 +0100 Subject: [PATCH 11/13] Separate google store from settings store --- server/api/auth/google/status.get.ts | 30 +++++ src/composables/useGoogleAuth.ts | 35 ------ src/stores/google.store.ts | 128 ++++++++++++++++++++++ src/stores/settings.store.ts | 3 +- src/utils/wallet/google-drive/provider.ts | 6 +- 5 files changed, 161 insertions(+), 41 deletions(-) create mode 100644 server/api/auth/google/status.get.ts delete mode 100644 src/composables/useGoogleAuth.ts create mode 100644 src/stores/google.store.ts diff --git a/server/api/auth/google/status.get.ts b/server/api/auth/google/status.get.ts new file mode 100644 index 0000000..cecc691 --- /dev/null +++ b/server/api/auth/google/status.get.ts @@ -0,0 +1,30 @@ +/** + * GET /api/auth/google/status + * Check Google authentication status + */ + +export default defineEventHandler((event) => { + const accessToken = getCookie(event, 'google_access_token'); + const userCookie = getCookie(event, 'google_user'); + + if (!accessToken) { + return { + authenticated: false, + user: null + }; + } + + let user = null; + if (userCookie) { + try { + user = JSON.parse(userCookie); + } catch (err) { + console.error('Error parsing user cookie:', err); + } + } + + return { + authenticated: true, + user + }; +}); diff --git a/src/composables/useGoogleAuth.ts b/src/composables/useGoogleAuth.ts deleted file mode 100644 index 4de3ad9..0000000 --- a/src/composables/useGoogleAuth.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { toastError } from '@/utils/parse-error'; - -export const useGoogleAuth = () => { - const getAuthStatus = () => { - const accessToken = useCookie('google_access_token'); - const userCookie = useCookie('google_user'); - - if (!accessToken.value) { - return { - authenticated: false, - user: null - }; - } - - let user = null; - if (userCookie.value) { - try { - user = typeof userCookie.value === 'string' - ? JSON.parse(userCookie.value) - : userCookie.value; - } catch (err) { - toastError('Error parsing user cookie', err); - } - } - - return { - authenticated: true, - user - }; - }; - - return { - getAuthStatus - }; -}; diff --git a/src/stores/google.store.ts b/src/stores/google.store.ts new file mode 100644 index 0000000..c152e66 --- /dev/null +++ b/src/stores/google.store.ts @@ -0,0 +1,128 @@ +import { defineStore } from 'pinia'; + +import { toastError } from '@/utils/parse-error'; + +import type { Settings } from './settings.store'; + +// Import function to avoid circular dependency +const getSettingsStore = () => import('./settings.store').then(m => m.useSettingsStore); + +export interface GoogleUser { + name: string; + email: string; + picture?: string; +} + +export const useGoogleStore = defineStore('google', { + state: () => ({ + googleUser: null as GoogleUser | null, + isGoogleAuthenticated: false, + isSyncing: false, + lastSyncError: null as string | null + }), + actions: { + async checkGoogleAuth () { + try { + const response = await $fetch<{ + authenticated: boolean; + user: GoogleUser | null; + }>('/api/auth/google/status'); + this.isGoogleAuthenticated = response.authenticated; + this.googleUser = response.user; + } catch (_error) { + this.isGoogleAuthenticated = false; + this.googleUser = null; + } + }, + + loginWithGoogle (returnUrl?: string) { + const currentUrl = returnUrl || window.location.pathname; + window.location.href = `/api/auth/google/login?returnUrl=${encodeURIComponent(currentUrl)}`; + }, + + async logoutFromGoogle () { + try { + await $fetch('/api/auth/google/logout', { method: 'POST' }); + this.isGoogleAuthenticated = false; + this.googleUser = null; + + // Disable Google Drive sync in settings + const useSettingsStore = await getSettingsStore(); + const settingsStore = useSettingsStore(); + settingsStore.settings.googleDriveSync = false; + settingsStore.saveSettings(); + } catch (error) { + toastError('Failed to logout from Google', error); + throw error; + } + }, + + async syncToGoogleDrive (settings: Settings) { + if (!this.isGoogleAuthenticated) + return; + + try { + this.isSyncing = true; + this.lastSyncError = null; + + await $fetch('/api/google-drive/settings', { + method: 'POST', + body: { + settings, + timestamp: Date.now() + } + }); + + return Date.now(); + } catch (error: unknown) { + const errorMessage = error && typeof error === 'object' && 'data' in error + ? ((error as { data?: { message?: string } }).data?.message || 'Failed to sync to Google Drive') + : 'Failed to sync to Google Drive'; + this.lastSyncError = errorMessage; + toastError('Sync to Google Drive failed', error); + throw error; + } finally { + this.isSyncing = false; + } + }, + + async syncFromGoogleDrive (): Promise<{ settings?: Settings; timestamp?: number } | null> { + if (!this.isGoogleAuthenticated) + return null; + + try { + this.isSyncing = true; + this.lastSyncError = null; + + const response = await $fetch<{ + success: boolean; + data?: { settings?: Settings; timestamp?: number } | null; + exists: boolean; + }>('/api/google-drive/settings'); + + if (response.exists && response.data?.settings) + return response.data; + + return null; + } catch (error: unknown) { + const errorMessage = error && typeof error === 'object' && 'data' in error + ? ((error as { data?: { message?: string } }).data?.message || 'Failed to sync from Google Drive') + : 'Failed to sync from Google Drive'; + this.lastSyncError = errorMessage; + toastError('Sync from Google Drive failed', error); + throw error; + } finally { + this.isSyncing = false; + } + }, + + clearSyncError () { + this.lastSyncError = null; + }, + + resetGoogleAuth () { + this.googleUser = null; + this.isGoogleAuthenticated = false; + } + } +}); diff --git a/src/stores/settings.store.ts b/src/stores/settings.store.ts index 7c577a5..ee98e35 100644 --- a/src/stores/settings.store.ts +++ b/src/stores/settings.store.ts @@ -110,8 +110,7 @@ export const useSettingsStore = defineStore('settings', { // Google Drive integration async checkGoogleAuth () { try { - const { getAuthStatus } = useGoogleAuth(); - const response = getAuthStatus(); + const response = await $fetch<{ authenticated: boolean; user: GoogleUser | null }>('/api/auth/google/status'); this.isGoogleAuthenticated = response.authenticated; this.googleUser = response.user; } catch (_error) { diff --git a/src/utils/wallet/google-drive/provider.ts b/src/utils/wallet/google-drive/provider.ts index 1b235fa..9768d20 100644 --- a/src/utils/wallet/google-drive/provider.ts +++ b/src/utils/wallet/google-drive/provider.ts @@ -95,10 +95,8 @@ export class GoogleDriveWalletProvider { public static async isAuthenticated (): Promise { try { - const { getAuthStatus } = useGoogleAuth(); - const { authenticated } = getAuthStatus(); - - return authenticated; + const response = await $fetch<{ authenticated: boolean }>('/api/auth/google/status'); + return response.authenticated; } catch { return false; } -- GitLab From 62dbf82aab3c05a266620034d4c884f27b53fcf4 Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Fri, 5 Dec 2025 14:28:49 +0100 Subject: [PATCH 12/13] Use global toasError function --- src/components/settings/GoogleDriveKeyManager.vue | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/settings/GoogleDriveKeyManager.vue b/src/components/settings/GoogleDriveKeyManager.vue index 07bea3e..71eb939 100644 --- a/src/components/settings/GoogleDriveKeyManager.vue +++ b/src/components/settings/GoogleDriveKeyManager.vue @@ -19,6 +19,7 @@ import { Label } from '@/components/ui/label'; import { Skeleton } from '@/components/ui/skeleton'; import { useGoogleDriveWallet } from '@/composables/useGoogleDriveWallet'; import { useSettingsStore } from '@/stores/settings.store'; +import { toastError } from '@/utils/parse-error'; const googleDrive = useGoogleDriveWallet(); const settingsStore = useSettingsStore(); @@ -101,8 +102,8 @@ const loadWalletInfo = async () => { rolePublicKeys.value = {} as Record; } } - } catch (_error) { - toast.error('Failed to load wallet information. Please try refreshing the page.'); + } catch (error) { + toastError('Failed to load wallet information. Please try refreshing the page.', error); isGoogleDriveConnected.value = false; hasGoogleDriveWallet.value = false; } finally { @@ -135,7 +136,7 @@ const handleAddKey = async () => { await loadWalletInfo(); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to add key'; - toast.error(message); + toastError(message, error); } finally { isSavingKey.value = false; } @@ -175,7 +176,7 @@ const handleDeleteWallet = async () => { availableKeyRoles.value = []; } catch (error) { const message = error instanceof Error ? error.message : 'Failed to delete wallet'; - toast.error(message); + toastError(message, error); } finally { isSavingKey.value = false; } @@ -204,7 +205,7 @@ const handleDeleteKey = async () => { await loadWalletInfo(); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to remove key'; - toast.error(message); + toastError(message, error); } finally { isDeletingKey.value = false; } -- GitLab From 9cd851b1a7c59d997731458119f38f6f98da4577 Mon Sep 17 00:00:00 2001 From: fwaszkiewicz Date: Fri, 5 Dec 2025 14:43:44 +0100 Subject: [PATCH 13/13] Use common composable for recovery password and account name dialogs --- src/components/AccountNamePromptDialog.vue | 10 +-- src/components/RecoveryPasswordDialog.vue | 10 +-- src/composables/useGoogleDriveWallet.ts | 2 +- src/composables/usePromptDialog.ts | 89 +++++++++++++++++++ src/stores/account-name-prompt.store.ts | 46 ---------- src/stores/recovery-password.store.ts | 46 ---------- .../checkGoogleAuthAndLoadWallet.ts | 4 +- src/utils/wallet/google-drive/provider.ts | 13 ++- 8 files changed, 107 insertions(+), 113 deletions(-) create mode 100644 src/composables/usePromptDialog.ts delete mode 100644 src/stores/account-name-prompt.store.ts delete mode 100644 src/stores/recovery-password.store.ts diff --git a/src/components/AccountNamePromptDialog.vue b/src/components/AccountNamePromptDialog.vue index 3b42434..7d1c966 100644 --- a/src/components/AccountNamePromptDialog.vue +++ b/src/components/AccountNamePromptDialog.vue @@ -13,9 +13,9 @@ import { } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { useAccountNamePromptStore } from '@/stores/account-name-prompt.store'; +import { useAccountNamePromptDialog } from '@/composables/usePromptDialog'; -const accountNamePromptStore = useAccountNamePromptStore(); +const accountNamePromptDialog = useAccountNamePromptDialog(); const accountName = ref(''); const isSubmitting = ref(false); @@ -24,14 +24,14 @@ const handleSubmit = () => { if (!accountName.value.trim()) return; isSubmitting.value = true; - accountNamePromptStore.submitAccountName(accountName.value.trim().toLowerCase()); + accountNamePromptDialog.submit(accountName.value.trim().toLowerCase()); accountName.value = ''; isSubmitting.value = false; }; const handleCancel = () => { accountName.value = ''; - accountNamePromptStore.cancel(); + accountNamePromptDialog.cancel(); }; const handleOpenChange = (open: boolean) => { @@ -47,7 +47,7 @@ const handleKeydown = (event: KeyboardEvent) => {