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 @@
-
- {{ stateData.heading }}
- {{ stateData.message }}
-
- -
-
-
-
+
+ -
+
+ {{ error.heading }}
+
+
+
+
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' | '';