refactor: callback progress

This commit is contained in:
Zack Spear
2023-06-05 19:11:01 -07:00
parent 4263749486
commit f4b4271c91
18 changed files with 476 additions and 100 deletions

View File

@@ -8,9 +8,9 @@ function makeid(length: number) {
return result;
}
const registeredGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
const regWizTime = `1616711990500_${registeredGuid}`;
const regWizTime = `1616711990500_${randomGuid}`;
const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
// ENOKEYFILE
@@ -41,12 +41,12 @@ const serverState = {
avatar: 'https://source.unsplash.com/300x300/?portrait',
name: 'DevServer9000',
description: 'Fully automated media server',
guid: '9292-1111-BITE-444444444444',
guid: randomGuid,
deviceCount: 8,
expireTime,
lanIp: '192.168.0.1',
locale: 'en',
pluginInstalled: false,
pluginInstalled: true,
registered: true,
site: 'http://localhost:4321',
state,

View File

@@ -16,48 +16,124 @@ $localSource = '/plugins/dynamix.my.servers/webComponents/' . $webComponentFile;
// add the web component source to the DOM
echo '<script id="unraid-webcomponents" defer src="' . $localSource . '"></script>';
/**
* Build vars for user profile prop
*/
// add 'ipaddr' function for 6.9 backwards compatibility
if (!function_exists('ipaddr')) {
function ipaddr($ethX='eth0', $prot=4) {
global $$ethX;
switch ($$ethX['PROTOCOL:0']) {
case 'ipv4':
return $$ethX['IPADDR:0'];
case 'ipv6':
return $$ethX['IPADDR6:0'];
case 'ipv4+ipv6':
switch ($prot) {
case 4: return $$ethX['IPADDR:0'];
case 6: return $$ethX['IPADDR6:0'];
default:return [$$ethX['IPADDR:0'],$$ethX['IPADDR6:0']];}
default:
return $$ethX['IPADDR:0'];
}
}
}
$configErrorEnum = [ // used to map $var['configValid'] value to mimic unraid-api's `configError` ENUM
"error" => 'UNKNOWN_ERROR',
"invalid" => 'INVALID',
"nokeyserver" => 'NO_KEY_SERVER',
"withdrawn" => 'WITHDRAWN',
];
// read flashbackup ini file
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
$pluginInstalled = '';
if (!file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net') && !file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) {
$pluginInstalled = ''; // base OS only, plugin not installed • show ad for plugin
} else {
// plugin is installed but if the unraid-api file doesn't fully install it's a failed install
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) $pluginInstalled = 'dynamix.unraid.net.plg';
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) $pluginInstalled = 'dynamix.unraid.net.staging.plg';
// plugin install failed • append failure detected so we can show warning about failed install via UPC
if (!file_exists('/usr/local/sbin/unraid-api')) $pluginInstalled = $pluginInstalled . '_installFailed';
}
$serverData = [
"avatar" => (!empty($myservers['remote']['avatar']) && $plgInstalled) ? $myservers['remote']['avatar'] : '',
"apiKey" => $myservers['upc']['apikey'] ?? '',
"apiVersion" => $myservers['api']['version'] ?? '',
"avatar" => (!empty($myservers['remote']['avatar']) && $pluginInstalled) ? $myservers['remote']['avatar'] : '',
"banner" => $display['banner'] ?? '',
"bannerGradient" => $display['showBannerGradient'] ?? 'yes',
"bgColor" => ($backgnd) ? '#'.$backgnd : '',
"config" => [
'valid' => $var['configValid'] === 'yes',
'error' => $var['configValid'] !== 'yes'
? (array_key_exists($var['configValid'], $configErrorEnum) ? $configErrorEnum[$var['configValid']] : 'UNKNOWN_ERROR')
: null,
],
"csrf" => $var['csrf_token'],
"description" => $var['COMMENT'],
"descriptionShow" => ($display['headerdescription']??''!='no') ? 'true' : '',
"deviceCount" => $var['deviceCount'],
"email" => $myservers['remote']['email'] ?? '',
"expireTime" => 1000*($var['regTy']=='Trial'||strstr($var['regTy'],'expired')?$var['regTm2']:0),
"extraOrigins" => explode(',', $myservers['api']['extraOrigins']??''),
"flashproduct" => $var['flashProduct'],
"flashvendor" => $var['flashVendor'],
"flashProduct" => $var['flashProduct'],
"flashVendor" => $var['flashVendor'],
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
"guid" => $var['flashGUID'],
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
"internalip" => ipaddr(),
"internalport" => $_SERVER['SERVER_PORT'],
"internalIp" => ipaddr(),
"internalPort" => $_SERVER['SERVER_PORT'],
"keyfile" => empty($var['regFILE'])? "" : str_replace(['+','/','='], ['-','_',''], trim(base64_encode(@file_get_contents($var['regFILE'])))),
"locale" => 'en',
"locale" => ($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US',
"metaColor" => ($display['headermetacolor']??'') ? '#'.$display['headermetacolor'] : '',
"model" => $var['SYS_MODEL'],
"name" => $var['NAME'],
"osVersion" => $var['version'],
"plgVersion" => $plgversion = file_exists('/var/log/plugins/dynamix.unraid.net.plg')
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
: ( file_exists('/var/log/plugins/dynamix.unraid.net.staging.plg')
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
: 'base-'.$var['version'] ),
"plgInstalled" => $plgInstalled,
"pluginInstalled" => $pluginInstalled,
"protocol" => $_SERVER['REQUEST_SCHEME'],
"reggen" => (int)$var['regGen'],
"regGen" => (int)$var['regGen'],
"regGuid" => $var['regGUID'],
"registered" => (!empty($myservers['remote']['username']) && $plgInstalled),
"name" => $var['NAME'],
"registered" => (!empty($myservers['remote']['username']) && $pluginInstalled),
"registeredTime" => $myservers['remote']['regWizTime'] ?? '',
"site" => $_SERVER['REQUEST_SCHEME']."://".$_SERVER['HTTP_HOST'],
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
"textColor" => ($header) ? '#'.$header : '',
"theme" => $display['theme'],
"ts" => time(),
"username" => (!empty($myservers['remote']['username']) && $plgInstalled) ? $myservers['remote']['username'] : '',
"uptime" => 1000*(time() - round(strtok(exec("cat /proc/uptime"),' '))),
"username" => (!empty($myservers['remote']['username']) && $pluginInstalled) ? $myservers['remote']['username'] : '',
"wanFQDN" => $nginx['NGINX_WANFQDN'] ?? '',
];
echo "<connect-user-profile server='" . json_encode($serverData) . "'></connect-user-profile>";
$themeBg = '#111';
if ($display['theme'] === 'black' || $display['theme'] === 'azure') {
$themeBg = '#fff';
}
?>
<connect-callback-handler></connect-callback-handler>
<connect-auth></connect-auth>
<connect-key-actions></connect-key-actions>
<connect-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
<style>
.ComponentWrapper {
padding: 16px;
}
</style>
<div class="ComponentWrapper" style="background-color: <?=$themeBg?>;">
<?="<connect-user-profile server='" . json_encode($serverData) . "'></connect-user-profile>"?>
</div>
<div class="ComponentWrapper">
<connect-auth></connect-auth>
</div>
<div class="ComponentWrapper">
<connect-key-actions></connect-key-actions>
</div>
<div class="ComponentWrapper">
<connect-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
</div>

View File

@@ -26,9 +26,6 @@ onBeforeMount(() => {
<hr />
<h3>WanIpCheckCe</h3>
<WanIpCheckCe />
<hr />
<h3>CallbackHandlerCe</h3>
<CallbackHandlerCe />
</div>
<div class="flex flex-col gap-6 p-6">
<h2>Web Components</h2>
@@ -46,9 +43,6 @@ onBeforeMount(() => {
<hr />
<h3>WanIpCheckCe</h3>
<connect-wan-ip-check></connect-wan-ip-check>
<hr />
<h3>CallbackHandlerCe</h3>
<connect-callback-handler></connect-callback-handler>
</div>
</client-only>
</div>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { useCallbackStore } from '~/store/callback';
import { storeToRefs } from 'pinia';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { storeToRefs } from 'pinia';
import { useCallbackStore } from '~/store/callback';
const callbackStore = useCallbackStore();

View File

@@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia';
import { OnClickOutside } from '@vueuse/components'
import { useCallbackStore } from '~/store/callback';
import { useDropdownStore } from '~/store/dropdown';
import { useServerStore } from '~/store/server';
import type { Server } from '~/types/server';
@@ -16,6 +17,7 @@ const props = withDefaults(defineProps<Props>(), {
showDescription: true,
});
const callbackStore = useCallbackStore();
const dropdownStore = useDropdownStore()
const serverStore = useServerStore();
@@ -57,10 +59,12 @@ watch(showCopyNotSupported, async (newVal, oldVal) => {
onBeforeMount(() => {
console.debug('[onBeforeMount]', { props }, typeof props.server);
if (!props.server) return console.error('Server data not present');
// set props from web component in store so the data is available throughout other components
if (typeof props.server === 'object') { // handle the testing dev Vue component
/**
* Set props from web component in store so the data is available throughout other components
*/
if (typeof props.server === 'object') { // Handles the testing dev Vue component
serverStore.setServer(props.server);
} else if (typeof props.server === 'string') { // handle web component
} else if (typeof props.server === 'string') { // Handle web component
try {
const parsedServerProp = JSON.parse(props.server);
serverStore.setServer(parsedServerProp);
@@ -68,10 +72,16 @@ onBeforeMount(() => {
console.error(e);
}
}
/**
* Listen for callbacks, if we receive one that needs to be acted upon the store will display
* the feedback modal to show the user something is happening behind the scenes.
*/
callbackStore.watcher();
});
</script>
<template>
<UpcCallbackFeedback />
<UpcPromo />
<div id="UserProfile" class="text-alpha relative z-20 flex flex-col h-full gap-y-4px pl-40px rounded">
@@ -82,7 +92,7 @@ onBeforeMount(() => {
</div>
<div class="relative z-0 flex flex-row items-center justify-end gap-x-16px h-full">
<h1 class="relative text-18px border-t-0 border-r-0 border-l-0 border-b-2 border-transparent">
<h1 class="text-alpha relative text-18px border-t-0 border-r-0 border-l-0 border-b-2 border-transparent">
<template v-if="showDescription">
<span>{{ description }}</span>
<span class="text-grey-mid px-8px">&bull;</span>
@@ -99,12 +109,10 @@ onBeforeMount(() => {
<div class="block w-2px h-24px bg-grey-mid"></div>
<div class="flex items-center justify-end h-full">
<OnClickOutside @trigger="outsideDropdown" :options="{ ignore: [clickOutsideIgnoreTarget] }">
<UpcDropdownTrigger ref="clickOutsideIgnoreTarget" />
<UpcDropdown ref="clickOutsideTarget" />
</OnClickOutside>
</div>
<OnClickOutside class="flex items-center justify-end h-full" @trigger="outsideDropdown" :options="{ ignore: [clickOutsideIgnoreTarget] }">
<UpcDropdownTrigger ref="clickOutsideIgnoreTarget" />
<UpcDropdown ref="clickOutsideTarget" />
</OnClickOutside>
</div>
</div>
</template>

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useCallbackStore } from '~/store/callback';
const callbackStore = useCallbackStore();
const { callbackFeedbackVisible, decryptedData } = storeToRefs(callbackStore);
onBeforeMount(() => {
callbackStore.watcher();
});
</script>
<template>
<Modal
:open="callbackFeedbackVisible"
@close="callbackStore.hide()"
max-width="max-w-800px"
>
<div class="text-center relative w-full flex flex-col gap-y-16px">
<header>
<h1 class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">Callback Feedback</h1>
</header>
<BrandLoading class="w-90px mx-auto" />
<pre class="text-left text-black p-8px w-full overflow-scroll bg-gray-400">{{ JSON.stringify(decryptedData, null, 2) }}</pre>
<div class="w-full max-w-xs flex flex-col gap-y-16px mx-auto">
<button
@click="callbackStore.hide()"
class="text-12px tracking-wide inline-block mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
:title="'Close Promo'"
>
{{ 'Close' }}
</button>
</div>
</div>
</Modal>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -22,10 +22,10 @@ const links = ref<UserProfileLink[]>([
</script>
<template>
<ul v-if="stateData.error" class="list-reset flex flex-col gap-y-4px p-12px -mx-4px bg-red/20">
<h3>{{ stateData.error.heading }}</h3>
<p>{{ stateData.error.message }}</p>
<li v-for="(link, index) in links" :key="`link_${index}`" class="-mx-8px">
<ul v-if="stateData.error" class="list-reset flex flex-col gap-y-4px -mx-8px p-12px bg-red/40 rounded">
<h3 class="text-18px">{{ stateData.heading }}</h3>
<p class="text-14px opacity-85">{{ stateData.message }}</p>
<li v-for="(link, index) in links" :key="`link_${index}`" class="-mx-">
<UpcDropdownItem :item="link" />
</li>
</ul>

View File

@@ -44,12 +44,8 @@ const title = computed((): string => {
>
<InformationCircleIcon v-if="pluginOutdated" class="text-red fill-current relative w-24px h-24px" />
<ExclamationTriangleIcon v-else-if="showErrorIcon" class="text-red fill-current relative w-24px h-24px" />
<span class="flex flex-row items-center gap-x-8px">
<span class="leading-none">{{ text }}</span>
<UpcDropdownTriggerMenuIcon :open="dropdownVisible" />
</span>
<span v-if="text" class="leading-none">{{ text }}</span>
<UpcDropdownTriggerMenuIcon :open="dropdownVisible" />
<BrandAvatar />
</button>
</template>

View File

@@ -88,7 +88,7 @@ const installButtonClasses = 'text-white text-14px text-center w-full flex flex-
<Switch v-model="installStaging" :class="[installStaging ? 'bg-indigo-600' : 'bg-gray-200', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2']">
<span aria-hidden="true" :class="[installStaging ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out']" />
</Switch>
<SwitchLabel as="span" class="ml-3 text-sm">
<SwitchLabel as="span" class="ml-3 text-12px">
<span class="font-semibold">Install Staging</span>
</SwitchLabel>
</SwitchGroup>

View File

@@ -1,6 +1,6 @@
<template>
<button class="text-sm font-semibold transition-colors duration-150 ease-in-out border-t-0 border-l-0 border-r-0 border-b-2 border-transparent hover:border-orange-dark focus:border-orange-dark focus:outline-none">
<button class="text-12px font-semibold transition-colors duration-150 ease-in-out border-t-0 border-l-0 border-r-0 border-b-2 border-transparent hover:border-orange-dark focus:border-orange-dark focus:outline-none">
<slot />
</button>
</template>

View File

@@ -29,10 +29,6 @@ export default defineNuxtConfig({
name: 'ConnectAuth',
path: '@/components/Auth.ce',
},
{
name: 'ConnectCallbackHandler',
path: '@/components/CallbackHandler.ce',
},
{
name: 'ConnectDownloadApiLogs',
path: '@/components/DownloadApiLogs.ce',

66
store/account.ts Normal file
View File

@@ -0,0 +1,66 @@
import { useToggle } from '@vueuse/core';
import { defineStore, createPinia, setActivePinia } from "pinia";
import { useCallbackStore } from './callback';
import { useServerStore } from './server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useAccountStore = defineStore('account', () => {
const callbackStore = useCallbackStore();
const serverStore = useServerStore();
// State
const accountVisible = ref<boolean>(false);
// Actions
const recover = () => {
console.debug('[recover]');
callbackStore.send('https://account.unraid.net', {
...serverStore.serverAccountPayload,
type: 'recover',
});
};
const replace = () => {
console.debug('[replace]');
callbackStore.send('https://account.unraid.net', {
...serverStore.serverAccountPayload,
type: 'replace',
});
};
const signIn = () => {
console.debug('[signIn]');
callbackStore.send('https://account.unraid.net', {
...serverStore.serverAccountPayload,
type: 'signIn',
});
};
const signOut = () => {
console.debug('[signOut]');
callbackStore.send('https://account.unraid.net', {
...serverStore.serverAccountPayload,
type: 'signOut',
});
};
const accountHide = () => accountVisible.value = false;
const accountShow = () => accountVisible.value = true;
const accountToggle = useToggle(accountVisible);
watch(accountVisible, (newVal, _oldVal) => {
console.debug('[accountVisible]', newVal, _oldVal);
});
return {
// State
accountVisible,
accountHide,
accountShow,
// Actions
recover,
replace,
signIn,
signOut,
accountToggle,
};
});

View File

@@ -1,7 +1,11 @@
import AES from 'crypto-js/aes';
import Utf8 from 'crypto-js/enc-utf8';
import { defineStore, createPinia, setActivePinia } from "pinia";
import { useServerStore } from './server';
import type { CallbackSendPayload } from '~/types/callback';
import type {
ServerAccountCallbackSendPayload,
ServerPurchaseCallbackSendPayload,
} from '~/types/server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
@@ -9,22 +13,19 @@ import { useServerStore } from './server';
setActivePinia(createPinia());
export const useCallbackStore = defineStore('callback', () => {
const serverStore = useServerStore();
// store helpers
// const config = useRuntimeConfig(); // results in a nuxt error after web components are built
// const encryptKey = config.public.callbackKey;
const encryptKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
console.debug('[useCallbackStore]', { encryptKey });
// state
const encryptedMessage = ref('');
const callbackFeedbackVisible = ref<boolean>(false);
const decryptedData = ref();
// getters
const encryptedMessage = ref('');
// actions
const send = (url: string = 'https://unraid.ddev.site/init-purchase') => {
const send = (url: string = 'https://unraid.ddev.site/init-purchase', payload: CallbackSendPayload) => {
console.debug('[send]');
const stringifiedData = JSON.stringify({
...serverStore.server,
...payload,
sender: window.location.href,
});
// @todo don't save to store
@@ -42,14 +43,16 @@ export const useCallbackStore = defineStore('callback', () => {
console.debug('[watcher]', currentUrl);
const callbackValue = currentUrl.searchParams.get('data');
if (!callbackValue) {
console.debug('[watcher] no callback to handle');
return;
return console.debug('[watcher] no callback to handle');
}
const decryptedMessage = AES.decrypt(callbackValue, encryptKey);
decryptedData.value = JSON.parse(decryptedMessage.toString(Utf8));
console.debug('[watcher]', decryptedMessage, decryptedData.value);
if (!decryptedData.value) return console.error('Callback Watcher: Data not present');
if (!decryptedData.value.action) return console.error('Callback Watcher: Required "action" not present');
// Display the feedback modal
show();
// Parse the data and perform actions
switch (decryptedData.value.action) {
case 'install':
console.debug(`Installing key ${decryptedData.value.keyUrl}\n\nOEM: ${decryptedData.value.oem}\n\nSender: ${decryptedData.value.sender}`);
@@ -63,5 +66,26 @@ export const useCallbackStore = defineStore('callback', () => {
}
};
return { send, watcher, decryptedData };
const hide = () => callbackFeedbackVisible.value = false;
const show = () => callbackFeedbackVisible.value = true;
const toggle = useToggle(callbackFeedbackVisible);
/**
* @todo consider removing query string once actions are done
*/
watch(callbackFeedbackVisible, (newVal, _oldVal) => {
console.debug('[callbackFeedbackVisible]', newVal, _oldVal);
});
return {
// state
decryptedData,
callbackFeedbackVisible,
// actions
send,
watcher,
hide,
show,
toggle,
};
});

58
store/purchase.ts Normal file
View File

@@ -0,0 +1,58 @@
import { useToggle } from '@vueuse/core';
import { defineStore, createPinia, setActivePinia } from "pinia";
import { useCallbackStore } from './callback';
import { useServerStore } from './server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const usePurchaseStore = defineStore('purchase', () => {
const callbackStore = useCallbackStore();
const serverStore = useServerStore();
// State
const purchaseVisible = ref<boolean>(false);
// Actions
const redeem = () => {
console.debug('[redeem]');
callbackStore.send('https://unraid.ddev.site/init-purchase', {
...serverStore.serverPurchasePayload,
type: 'redeem',
});
};
const purchase = () => {
console.debug('[purchase]');
callbackStore.send('https://unraid.ddev.site/init-purchase', {
...serverStore.serverPurchasePayload,
type: 'purchase',
});
};
const upgrade = () => {
console.debug('[upgrade]');
callbackStore.send('https://unraid.ddev.site/init-purchase', {
...serverStore.serverPurchasePayload,
type: 'upgrade',
});
};
const purchaseHide = () => purchaseVisible.value = false;
const purchaseShow = () => purchaseVisible.value = true;
const purchaseToggle = useToggle(purchaseVisible);
watch(purchaseVisible, (newVal, _oldVal) => {
console.debug('[purchaseVisible]', newVal, _oldVal);
});
return {
// State
purchaseVisible,
purchaseHide,
purchaseShow,
// Actions
purchaseToggle,
redeem,
purchase,
upgrade,
};
});

View File

@@ -1,7 +1,13 @@
import { defineStore, createPinia, setActivePinia } from "pinia";
import { ArrowRightOnRectangleIcon, GlobeAltIcon, KeyIcon } from '@heroicons/vue/24/solid';
import { useAccountStore } from './account';
import { usePurchaseStore } from "./purchase";
import { useTrialStore } from './trial';
import type {
Server,
ServerAccountCallbackSendPayload,
ServerPurchaseCallbackSendPayload,
ServerState,
ServerStateData,
ServerStateDataAction,
@@ -13,6 +19,9 @@ import type {
setActivePinia(createPinia());
export const useServerStore = defineStore('server', () => {
const accountStore = useAccountStore();
const purchaseStore = usePurchaseStore();
const trialStore = useTrialStore();
/**
* State
*/
@@ -80,64 +89,85 @@ export const useServerStore = defineStore('server', () => {
}
});
const serverPurchasePayload = computed((): ServerPurchaseCallbackSendPayload => {
return {
deviceCount: deviceCount.value,
guid: guid.value,
}
});
const serverAccountPayload = computed((): ServerAccountCallbackSendPayload => {
return {
description: description.value,
expireTime: expireTime.value,
flashProduct: flashProduct.value,
flashVendor: flashVendor.value,
guid: guid.value,
keyfile: keyfile.value,
name: name.value,
state: state.value,
wanFQDN: wanFQDN.value,
}
});
const purchaseAction: ServerStateDataAction = {
click: () => { console.debug('purchase') },
click: () => { purchaseStore.purchase() },
external: true,
icon: KeyIcon,
name: 'purchase',
text: 'Purchase Key',
};
const upgradeAction: ServerStateDataAction = {
click: () => { console.debug('upgrade') },
click: () => { purchaseStore.upgrade() },
external: true,
icon: KeyIcon,
name: 'upgrade',
text: 'Upgrade Key',
};
const recoverAction: ServerStateDataAction = {
click: () => { console.debug('recover') },
click: () => { accountStore.recover() },
external: true,
icon: KeyIcon,
name: 'recover',
text: 'Recover Key',
};
const redeemAction: ServerStateDataAction = {
click: () => { console.debug('redeem') },
click: () => { purchaseStore.redeem() },
external: true,
icon: KeyIcon,
name: 'redeem',
text: 'Redeem Activation Code',
};
const replaceAction: ServerStateDataAction = {
click: () => { console.debug('replace') },
click: () => { accountStore.replace() },
external: true,
icon: KeyIcon,
name: 'replace',
text: 'Replace Key',
};
const signInAction: ServerStateDataAction = {
click: () => { console.debug('signIn') },
click: () => { accountStore.signIn() },
external: true,
icon: GlobeAltIcon,
name: 'signIn',
text: 'Sign In with Unraid.net Account',
};
const signOutAction: ServerStateDataAction = {
click: () => { console.debug('signOut') },
click: () => { accountStore.signOut() },
external: true,
icon: ArrowRightOnRectangleIcon,
name: 'signOut',
text: 'Sign Out of Unraid.net',
};
const trialExtendAction: ServerStateDataAction = {
click: () => { console.debug('trialExtend') },
click: () => { trialStore.extend() },
external: true,
icon: ArrowRightOnRectangleIcon,
name: 'trialExtend',
text: 'Extend Trial',
};
const trialStartAction: ServerStateDataAction = {
click: () => { console.debug('trialStart') },
click: () => { trialStore.start() },
external: true,
icon: ArrowRightOnRectangleIcon,
name: 'trialStart',
@@ -176,6 +206,7 @@ export const useServerStore = defineStore('server', () => {
...(trialExtensionEligible.value ? [trialExtendAction] : []),
...(registered.value ? [signOutAction] : []),
],
error: true,
humanReadable: 'Trial Expired',
heading: 'Your Trial has expired',
message: trialExtensionEligible.value
@@ -236,6 +267,7 @@ export const useServerStore = defineStore('server', () => {
...([purchaseAction, redeemAction, replaceAction]),
...(registered.value ? [signOutAction] : []),
],
error: true,
humanReadable: 'Flash GUID Error',
heading: 'Registration key / USB Flash GUID mismatch',
message: messageEGUID,
@@ -247,10 +279,11 @@ export const useServerStore = defineStore('server', () => {
...(registered.value ? [purchaseAction, redeemAction] : []),
...(registered.value ? [signOutAction] : []),
],
error: true,
humanReadable: 'Multiple License Keys Present',
heading: 'Multiple License Keys Present',
message: 'There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device. Alternately you may purchase a license key for this USB flash device. If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.',
// signInToFix: true, // @todo
// signInToFix: true, // @todo is this needed?
};
case 'ENOKEYFILE2':
return {
@@ -259,6 +292,7 @@ export const useServerStore = defineStore('server', () => {
...([purchaseAction, redeemAction]),
...(registered.value ? [recoverAction, signOutAction] : []),
],
error: true,
humanReadable: 'Missing key file',
heading: 'Missing key file',
message: 'It appears that your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device. If you do not have a backup copy of your license key file you may install the Connect (beta) plugin to attempt to recover your key. If this was an expired Trial installation, you may purchase a license key.',
@@ -270,6 +304,7 @@ export const useServerStore = defineStore('server', () => {
...([purchaseAction, redeemAction]),
...(registered.value ? [signOutAction] : []),
],
error: true,
humanReadable: 'Invalid installation',
heading: 'Invalid installation',
message: 'It is not possible to use a Trial key with an existing Unraid OS installation. You may purchase a license key corresponding to this USB Flash device to continue using this installation.',
@@ -281,6 +316,7 @@ export const useServerStore = defineStore('server', () => {
...([purchaseAction, redeemAction]),
...(registered.value ? [signOutAction] : []),
],
error: true,
humanReadable: 'No Keyfile',
heading: 'No USB flash configuration data',
message: 'There is a problem with your USB Flash device',
@@ -294,36 +330,42 @@ export const useServerStore = defineStore('server', () => {
case 'ENOFLASH6':
case 'ENOFLASH7':
return {
error: true,
humanReadable: 'No Flash',
heading: 'Cannot access your USB Flash boot device',
message: 'There is a physical problem accessing your USB Flash boot device',
};
case 'EBLACKLISTED':
return {
error: true,
humanReadable: 'BLACKLISTED',
heading: 'Blacklisted USB Flash GUID',
message: 'This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device. A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers.',
};
case 'EBLACKLISTED1':
return {
error: true,
humanReadable: 'BLACKLISTED',
heading: 'USB Flash device error',
message: 'This USB Flash device has an invalid GUID. Please try a different USB Flash device',
};
case 'EBLACKLISTED2':
return {
error: true,
humanReadable: 'BLACKLISTED',
heading: 'USB Flash has no serial number',
message: 'This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device. A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers.',
};
case 'ENOCONN':
return {
error: true,
humanReadable: 'Trial Requires Internet Connection',
heading: 'Cannot validate Unraid Trial key',
message: 'Your Trial key requires an internet connection. Please check Settings > Network',
};
default:
return {
error: true,
humanReadable: 'Stale',
heading: 'Stale Server',
message: 'Please refresh the page to ensure you load your latest configuration',
@@ -347,29 +389,30 @@ export const useServerStore = defineStore('server', () => {
* Actions
*/
const setServer = (data: Server) => {
console.debug('[setServer]', data);
if (data?.apiKey) apiKey.value = data.apiKey;
if (data?.avatar) avatar.value = data.avatar;
if (data?.description) description.value = data.description;
if (data?.deviceCount) deviceCount.value = data.deviceCount;
if (data?.expireTime) expireTime.value = data.expireTime;
if (data?.flashProduct) flashProduct.value = data.flashProduct;
if (data?.flashVendor) flashVendor.value = data.flashVendor;
if (data?.guid) guid.value = data.guid;
if (data?.keyfile) keyfile.value = data.keyfile;
if (data?.lanIp) lanIp.value = data.lanIp;
if (data?.license) license.value = data.license;
if (data?.locale) locale.value = data.locale;
if (data?.name) name.value = data.name;
if (data?.pluginInstalled) pluginInstalled.value = data.pluginInstalled;
if (data?.registered) registered.value = data.registered;
if (data?.regGen) regGen.value = data.regGen;
if (data?.regGuid) regGuid.value = data.regGuid;
if (data?.site) site.value = data.site;
if (data?.state) state.value = data.state;
if (data?.uptime) uptime.value = data.uptime;
if (data?.username) username.value = data.username;
if (data?.wanFQDN) wanFQDN.value = data.wanFQDN;
console.debug('[setServer] data', data);
if (typeof data?.apiKey !== 'undefined') apiKey.value = data.apiKey;
if (typeof data?.avatar !== 'undefined') avatar.value = data.avatar;
if (typeof data?.description !== 'undefined') description.value = data.description;
if (typeof data?.deviceCount !== 'undefined') deviceCount.value = data.deviceCount;
if (typeof data?.expireTime !== 'undefined') expireTime.value = data.expireTime;
if (typeof data?.flashProduct !== 'undefined') flashProduct.value = data.flashProduct;
if (typeof data?.flashVendor !== 'undefined') flashVendor.value = data.flashVendor;
if (typeof data?.guid !== 'undefined') guid.value = data.guid;
if (typeof data?.keyfile !== 'undefined') keyfile.value = data.keyfile;
if (typeof data?.lanIp !== 'undefined') lanIp.value = data.lanIp;
if (typeof data?.license !== 'undefined') license.value = data.license;
if (typeof data?.locale !== 'undefined') locale.value = data.locale;
if (typeof data?.name !== 'undefined') name.value = data.name;
if (typeof data?.pluginInstalled !== 'undefined') pluginInstalled.value = data.pluginInstalled;
if (typeof data?.registered !== 'undefined') registered.value = data.registered;
if (typeof data?.regGen !== 'undefined') regGen.value = data.regGen;
if (typeof data?.regGuid !== 'undefined') regGuid.value = data.regGuid;
if (typeof data?.site !== 'undefined') site.value = data.site;
if (typeof data?.state !== 'undefined') state.value = data.state;
if (typeof data?.uptime !== 'undefined') uptime.value = data.uptime;
if (typeof data?.username !== 'undefined') username.value = data.username;
if (typeof data?.wanFQDN !== 'undefined') wanFQDN.value = data.wanFQDN;
console.debug('[setServer] server.value', server.value);
};
return {
@@ -397,6 +440,8 @@ export const useServerStore = defineStore('server', () => {
keyActions,
pluginOutdated,
server,
serverAccountPayload,
serverPurchasePayload,
stateData,
// actions
setServer,

37
store/trial.ts Normal file
View File

@@ -0,0 +1,37 @@
import { useToggle } from '@vueuse/core';
import { defineStore, createPinia, setActivePinia } from "pinia";
import { useCallbackStore } from './callback';
import { useServerStore } from './server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useTrialStore = defineStore('trial', () => {
const callbackStore = useCallbackStore();
const serverStore = useServerStore();
const extend = () => {
console.debug('[extend]');
callbackStore.send('https://account.unraid.net', {
...serverStore.serverAccountPayload,
type: 'trialExtend',
});
};
const start = () => {
console.debug('[start]');
callbackStore.send('https://account.unraid.net', {
...serverStore.serverAccountPayload,
type: 'trialStart',
});
};
return {
// State
// Actions
extend,
start,
};
});

9
types/callback.ts Normal file
View File

@@ -0,0 +1,9 @@
import type {
ServerAccountCallbackSendPayload,
ServerPurchaseCallbackSendPayload,
ServerStateDataActionType,
} from '~/types/server';
export interface CallbackSendPayload extends ServerAccountCallbackSendPayload, ServerPurchaseCallbackSendPayload {
type: ServerStateDataActionType;
}

View File

@@ -46,7 +46,27 @@ export interface Server {
wanIp?: string;
}
// @todo convert to object with text and click payload
export interface ServerAccountCallbackSendPayload {
description?: string;
deviceCount?: number;
expireTime?: number;
flashProduct?: string;
flashVendor?: string;
guid?: string;
keyfile?: string;
locale?: string;
name?: string;
regGen?: number;
regGuid?: string;
state: string;
wanFQDN?: string;
}
export interface ServerPurchaseCallbackSendPayload {
deviceCount?: number;
guid?: string;
}
export type ServerStateDataActionType = 'signIn'|'signOut'|'purchase'|'redeem'|'upgrade'|'recover'|'replace'|'trialExtend'|'trialStart';
export interface ServerStateDataAction extends UserProfileLink {
@@ -64,6 +84,6 @@ export interface ServerStateData {
humanReadable: string; // @todo create interface of ENUM to string mapping
heading?: string;
message?: string;
error?: ServerStateDataError;
error?: ServerStateDataError | boolean;
withKey?: boolean; // @todo potentially remove
}