From a46f5a3cb4e4217a2cfc882aa3a36710d3c6962e Mon Sep 17 00:00:00 2001 From: Zack Spear Date: Tue, 11 Jul 2023 18:40:30 -0700 Subject: [PATCH] feat: WIP error store progress with server data --- _data/serverState.ts | 7 +- components/Brand/Button.vue | 4 +- components/UserProfile/CallbackFeedback.vue | 2 +- components/UserProfile/DropdownContent.vue | 4 +- components/UserProfile/DropdownError.vue | 63 +++++---- components/UserProfile/DropdownTrigger.vue | 10 +- helpers/urls.ts | 7 +- store/errors.ts | 26 +++- store/server.ts | 134 ++++++++++++++++++-- types/server.ts | 11 +- 10 files changed, 208 insertions(+), 60 deletions(-) diff --git a/_data/serverState.ts b/_data/serverState.ts index 811fd655a..fb8016543 100644 --- a/_data/serverState.ts +++ b/_data/serverState.ts @@ -37,7 +37,7 @@ if (state === 'TRIAL') expireTime = Date.now() + 60 * 60 * 1000; // in 1 hour if (state === 'EEXPIRED') expireTime = uptime; // 1 hour ago const serverState = { - "apiKey": "unupc_12312313123", + "apiKey": "XXXunupc_12312313123", "avatar": "https://source.unsplash.com/300x300/?portrait", "description": "DevServer9000", "deviceCount": "3", @@ -51,9 +51,8 @@ const serverState = { "license": "", "locale": "en_US", "name": "fuji", - // "pluginInstalled": "dynamix.unraid.net.staging.plg", - "pluginInstalled": true, - "registered": true, + "pluginInstalled": "dynamix.unraid.net.staging.plg", + "registered": false, "regGen": 0, // "regGuid": "0781-5583-8355-81071A2B0211", "site": "http://localhost:4321", diff --git a/components/Brand/Button.vue b/components/Brand/Button.vue index 86d02dbde..f35ec83ad 100644 --- a/components/Brand/Button.vue +++ b/components/Brand/Button.vue @@ -1,6 +1,6 @@ diff --git a/components/UserProfile/DropdownTrigger.vue b/components/UserProfile/DropdownTrigger.vue index 817ece42c..5134ba27d 100644 --- a/components/UserProfile/DropdownTrigger.vue +++ b/components/UserProfile/DropdownTrigger.vue @@ -2,10 +2,12 @@ import { storeToRefs } from 'pinia'; import { InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/vue/24/solid'; import { useDropdownStore } from '~/store/dropdown'; +import { useErrorsStore } from '~/store/errors'; import { useServerStore } from '~/store/server'; const dropdownStore = useDropdownStore(); const { dropdownVisible } = storeToRefs(dropdownStore); +const { errors } = storeToRefs(useErrorsStore()); const { pluginInstalled, pluginOutdated, @@ -16,7 +18,7 @@ const { } = storeToRefs(useServerStore()); const registeredAndPluginInstalled = computed(() => pluginInstalled.value && registered.value); -const showErrorIcon = computed(() => stateData.value.error); +const showErrorIcon = computed(() => errors.value.length || stateData.value.error); const text = computed((): string | undefined => { if ((stateData.value.error) && state.value !== 'EEXPIRED') return 'Fix Error'; @@ -27,11 +29,7 @@ const text = computed((): string | undefined => { const title = computed((): string => { if (state.value === 'ENOKEYFILE') return 'Get Started'; if (state.value === 'EEXPIRED') return 'Trial Expired, see options below'; - if (stateData.value.error) return 'Learn More'; - // if (cloud.value && cloud.value.error) return 'Unraid API Error'; - // if (myServersError.value && registeredAndPluginInstalled.value return 'Unraid API Error'; - // if (errorTooManyDisks.value) return 'Too many devices'; - // if (isLaunchpadOpen.value) return 'Close and continue to webGUI'; + if (showErrorIcon.value) return 'Learn more about the error'; return dropdownVisible.value ? 'Close Dropdown' : 'Open Dropdown'; }); diff --git a/helpers/urls.ts b/helpers/urls.ts index 0e350f078..eaf08fb45 100644 --- a/helpers/urls.ts +++ b/helpers/urls.ts @@ -6,8 +6,10 @@ const CONNECT_DOCS = 'https://docs.unraid.net/category/unraid-connect'; const CONNECT_DASHBOARD = 'https://connect.myunraid.net'; const CONNECT_FORUMS = 'https://forums.unraid.net/forum/94-connect-plugin-support/'; const DEV_GRAPH_URL = ''; -const PURCHASE = 'https://unraid.net/preflight'; -const PLUGIN_SETTINGS = '/Settings/ManagementAccess#UnraidNetSettings'; +const PURCHASE = 'https://unraid.ddev.site/callback'; + +const SETTINGS_MANAGMENT_ACCESS = '/Settings/ManagementAccess'; +const PLUGIN_SETTINGS = `${SETTINGS_MANAGMENT_ACCESS}#UnraidNetSettings`; export { ACCOUNT, @@ -17,4 +19,5 @@ export { DEV_GRAPH_URL, PURCHASE, PLUGIN_SETTINGS, + SETTINGS_MANAGMENT_ACCESS, }; \ No newline at end of file diff --git a/store/errors.ts b/store/errors.ts index a5a77d7cc..d5879084c 100644 --- a/store/errors.ts +++ b/store/errors.ts @@ -1,8 +1,11 @@ +import { XCircleIcon } from '@heroicons/vue/24/solid'; import { defineStore, createPinia, setActivePinia } from 'pinia'; + // import { useAccountStore } from '~/store/account'; // import { useCallbackStore, useCallbackActionsStore } from '~/store/callbackActions'; // import { useInstallKeyStore } from '~/store/installKey'; // import { useServerStore } from '~/store/server'; +import type { ButtonProps } from '~/components/Brand/Button.vue'; /** * @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components @@ -10,6 +13,17 @@ import { defineStore, createPinia, setActivePinia } from 'pinia'; */ setActivePinia(createPinia()); +export type ErrorType = 'account' | 'callback' | 'installKey' | 'server' | 'serverState'; +export interface Error { + actions?: ButtonProps[]; + heading: string; + level: 'error' | 'info' | 'warning'; + message: string; + ref?: string; + supportLink?: boolean; + type: ErrorType; +} + export const useErrorsStore = defineStore('errors', () => { // const accountStore = useAccountStore(); // const callbackStore = useCallbackStore(); @@ -17,13 +31,16 @@ export const useErrorsStore = defineStore('errors', () => { // const installKeyStore = useInstallKeyStore(); // const serverStore = useServerStore(); - /** @todo type the errors */ - const errors = ref([]); + const errors = ref([]); - const removeError = (index: number) => { + const removeErrorByIndex = (index: number) => { errors.value = errors.value.filter((_error, i) => i !== index); }; + const removeErrorByRef = (ref: ErrorType) => { + errors.value = errors.value.filter(error => error?.ref !== ref); + }; + const resetErrors = () => { errors.value = []; }; @@ -34,7 +51,8 @@ export const useErrorsStore = defineStore('errors', () => { return { errors, - removeError, + removeErrorByIndex, + removeErrorByRef, resetErrors, setError, }; diff --git a/store/server.ts b/store/server.ts index a6eb8111b..80e914eca 100644 --- a/store/server.ts +++ b/store/server.ts @@ -1,18 +1,27 @@ import { defineStore, createPinia, setActivePinia } from 'pinia'; -import { ArrowRightOnRectangleIcon, GlobeAltIcon, KeyIcon } from '@heroicons/vue/24/solid'; +import { + ArrowRightOnRectangleIcon, + CogIcon, + GlobeAltIcon, + KeyIcon, +} from '@heroicons/vue/24/solid'; -import { useAccountStore } from './account'; -import { usePurchaseStore } from "./purchase"; -import { useTrialStore } from './trial'; -import { useThemeStore, type Theme } from './theme'; +import { SETTINGS_MANAGMENT_ACCESS } from '~/helpers/urls'; +import { useAccountStore } from '~/store/account'; +import { useErrorsStore, type Error } from '~/store/errors'; +import { usePurchaseStore } from "~/store/purchase"; +import { useTrialStore } from '~/store/trial'; +import { useThemeStore, type Theme } from '~/store/theme'; import type { Server, ServerAccountCallbackSendPayload, ServerKeyTypeForPurchase, ServerPurchaseCallbackSendPayload, ServerState, + ServerStateConfigStatus, ServerStateData, ServerStateDataAction, + ServerPluginInstalled, } from '~/types/server'; /** * @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components @@ -22,6 +31,7 @@ setActivePinia(createPinia()); export const useServerStore = defineStore('server', () => { const accountStore = useAccountStore(); + const errorsStore = useErrorsStore(); const purchaseStore = usePurchaseStore(); const themeStore = useThemeStore(); const trialStore = useTrialStore(); @@ -30,6 +40,7 @@ export const useServerStore = defineStore('server', () => { */ const avatar = ref(''); // @todo potentially move to a user store const apiKey = ref(''); // @todo potentially move to a user store + const config = ref(); const csrf = ref(''); // required to make requests to Unraid webgui const description = ref(''); const deviceCount = ref(0); @@ -47,7 +58,7 @@ export const useServerStore = defineStore('server', () => { const license = ref(''); const locale = ref(''); const name = ref(''); - const pluginInstalled = ref(false); + const pluginInstalled = ref(''); const registered = ref(); const regGen = ref(0); const regGuid = ref(''); @@ -57,7 +68,6 @@ export const useServerStore = defineStore('server', () => { const uptime = ref(0); const username = ref(''); // @todo potentially move to a user store const wanFQDN = ref(''); - /** * Getters */ @@ -428,6 +438,81 @@ export const useServerStore = defineStore('server', () => { }); const trialExtensionEligible = computed(() => !regGen.value || regGen.value < 2); + const invalidApiKey = computed((): Error | undefined => { + // must be registered with plugin installed + if (!registered.value) { + return undefined; + } + + // Keeping separate from validApiKeyLength because we may want to add more checks. Cloud also help with debugging user error submissions. + if (apiKey.value.length !== 64) { + console.debug('[invalidApiKey] invalid length'); + return { + heading: 'Invalid API Key', + level: 'error', + message: 'Please sign out then sign back in to refresh your API key.', + ref: 'invalidApiKeyLength', + type: 'server', + }; + } + if (!apiKey.value.startsWith('unupc_')) { + console.debug('[invalidApiKey] invalid for upc'); + return { + heading: 'Invalid API Key Format', + level: 'error', + message: 'Please sign out then sign back in to refresh your API key.', + ref: 'invalidApiKeyFormat', + type: 'server', + }; + } + return undefined; + }); + + const tooManyDevices = computed((): Error | undefined => { + if (!config.value?.valid && config.value?.error === 'INVALID') { + return { + heading: 'Too Many Devices', + level: 'error', + message: 'You have exceeded the number of devices allowed for your license. Please remove a device before adding another.', + ref: 'tooManyDevices', + type: 'server', + }; + } + }); + + const pluginInstallFailed = computed((): Error | undefined => { + if (pluginInstalled.value && pluginInstalled.value.includes('_installFailed')) { + return { + heading: 'Unraid Connect Install Failed', + level: 'error', + message: 'Rebooting will likely solve this.', + ref: 'pluginInstallFailed', + type: 'server', + }; + } + }); + + /** + * Deprecation warning for [hash].unraid.net SSL certs. Deprecation started 2023-01-01 + */ + const deprecatedUnraidSSL = computed((): Error | undefined => { + if (window.location.hostname.includes('.unraid.net')) { + return { + actions: [ + { + href: SETTINGS_MANAGMENT_ACCESS, + icon: CogIcon, + text: 'Go to Management Access Now', + }, + ], + heading: 'Unraid.net SSL Certificate Deprecation', + level: 'warning', + message: 'Unraid.net SSL certificates will be deprecated on January 1st, 2023. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.', + ref: 'deprecatedUnraidSSL', + type: 'server', + }; + } + }); /** * Actions */ @@ -461,14 +546,45 @@ export const useServerStore = defineStore('server', () => { console.debug('[setServer] server.value', server.value); }; - watch(theme, () => { - if (theme.value) themeStore.setTheme(theme.value); + watch(theme, (newVal) => { + if (newVal) themeStore.setTheme(newVal); + }); + + watch(stateData, (newVal, oldVal) => { + if (oldVal.error) { + errorsStore.removeErrorByRef('serverState'); + } + if (newVal.error && !oldVal.error) { + const stateDataError = { + heading: newVal.heading, + message: newVal.message, + type: 'serverState', + }; + errorsStore.setError(stateDataError); + } + }); + watch(invalidApiKey, (newVal, oldVal) => { + if (oldVal && oldVal.ref) errorsStore.removeErrorByRef(oldVal.ref); + if (newVal) errorsStore.setError(newVal); + }); + watch(tooManyDevices, (newVal, oldVal) => { + if (oldVal && oldVal.ref) errorsStore.removeErrorByRef(oldVal.ref); + if (newVal) errorsStore.setError(newVal); + }); + watch(pluginInstallFailed, (newVal, oldVal) => { + if (oldVal && oldVal.ref) errorsStore.removeErrorByRef(oldVal.ref); + if (newVal) errorsStore.setError(newVal); + }); + watch(deprecatedUnraidSSL, (newVal, oldVal) => { + if (oldVal && oldVal.ref) errorsStore.removeErrorByRef(oldVal.ref); + if (newVal) errorsStore.setError(newVal); }); return { // state apiKey, avatar, + config, csrf, description, deviceCount, diff --git a/types/server.ts b/types/server.ts index bab6c55c0..46704fd49 100644 --- a/types/server.ts +++ b/types/server.ts @@ -2,6 +2,10 @@ import { KeyIcon } from '@heroicons/vue/24/solid'; import { Theme } from '~/store/theme'; import { UserProfileLink } from '~/types/userProfile'; +export interface ServerStateConfigStatus { + error: 'INVALID' | 'NO_KEY_SERVER' | 'UNKNOWN_ERROR' | 'WITHDRAWN'; + valid: boolean; +} export type ServerState = 'BASIC' | 'PLUS' | 'PRO' @@ -29,6 +33,7 @@ export type ServerState = 'BASIC' export interface Server { apiKey?: string; avatar?: string; + config?: ServerStateConfigStatus | undefined; csrf?: string; description?: string; deviceCount?: number; @@ -43,7 +48,7 @@ export interface Server { license?: string; locale?: string; name?: string; - pluginInstalled?: boolean; + pluginInstalled?: ServerPluginInstalled; registered?: boolean; regGen?: number; regGuid?: string; @@ -113,4 +118,6 @@ export interface ServerStateData { message?: string; error?: ServerStateDataError | boolean; withKey?: boolean; // @todo potentially remove -} \ No newline at end of file +} + +export type ServerPluginInstalled = 'dynamix.unraid.net.plg' | 'dynamix.unraid.net.staging.plg' | 'dynamix.unraid.net.plg_installFailed' | 'dynamix.unraid.net.staging.plg_installFailed' | '';