mirror of
https://github.com/unraid/api.git
synced 2026-01-03 15:09:48 -06:00
refactor: callbacks and progress on actions
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
NUXT_PUBLIC_CALLBACK_KEY=aNotSoSecretKeyUsedToObfuscateQueryParams
|
||||
NUXT_PUBLIC_UNRAID_NET=https://unraid.ddev.site
|
||||
NUXT_PUBLIC_UNRAID_NET=https://unraid.ddev.site
|
||||
NUXT_PUBLIC_ACCOUNT=https://localhost:8008
|
||||
|
||||
@@ -46,8 +46,8 @@ const serverState = {
|
||||
expireTime,
|
||||
lanIp: '192.168.0.1',
|
||||
locale: 'en',
|
||||
pluginInstalled: false,
|
||||
registered: false,
|
||||
pluginInstalled: true,
|
||||
registered: true,
|
||||
site: 'http://localhost:4321',
|
||||
state,
|
||||
uptime,
|
||||
|
||||
@@ -3,13 +3,11 @@ import { storeToRefs } from 'pinia';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
import { useCallbackStore } from '~/store/callback';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
|
||||
const callbackStore = useCallbackStore();
|
||||
const promoStore = usePromoStore();
|
||||
const { callbackFeedbackVisible } = storeToRefs(callbackStore);
|
||||
const { promoVisible } = storeToRefs(promoStore);
|
||||
const { callbackFeedbackVisible } = storeToRefs(useCallbackActionsStore());
|
||||
const { promoVisible } = storeToRefs(usePromoStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { OnClickOutside } from '@vueuse/components'
|
||||
|
||||
import { useCallbackStore } from '~/store/callback';
|
||||
import { useCallbackStore } from '~/store/callbackActions';
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { Server } from '~/types/server';
|
||||
@@ -19,7 +19,7 @@ const dropdownStore = useDropdownStore()
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const { dropdownVisible } = storeToRefs(dropdownStore);
|
||||
const { name, description, lanIp, theme } = storeToRefs(serverStore);
|
||||
const { name, description, lanIp, theme, stateData } = storeToRefs(serverStore);
|
||||
|
||||
/**
|
||||
* Close dropdown when clicking outside
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useCallbackStore } from '~/store/callback';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useInstallKeyStore } from '~/store/installKey';
|
||||
|
||||
export interface Props {
|
||||
@@ -15,17 +16,27 @@ withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const callbackStore = useCallbackStore();
|
||||
const callbackActionsStore = useCallbackActionsStore();
|
||||
const installKeyStore = useInstallKeyStore();
|
||||
|
||||
const { updating, updateSuccess } = storeToRefs(accountStore);
|
||||
const { callbackLoading, decryptedData } = storeToRefs(callbackStore);
|
||||
const { installing, success } = storeToRefs(installKeyStore);
|
||||
const { callbackLoading } = storeToRefs(callbackActionsStore);
|
||||
const { keyUrl, installing, success } = storeToRefs(installKeyStore);
|
||||
|
||||
const heading = computed(() => {
|
||||
callbackLoading.value ? 'Performing actions' : 'Finished performing actions';
|
||||
});
|
||||
|
||||
const subheading = computed(() => {
|
||||
callbackLoading.value ? 'Please keep this window open' : '';
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (callbackLoading.value) return console.debug('[close] not allowed');
|
||||
callbackStore.hide();
|
||||
callbackActionsStore.closeCallbackFeedback();
|
||||
};
|
||||
|
||||
const { text, copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,22 +48,31 @@ const close = () => {
|
||||
>
|
||||
<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>
|
||||
<h1 class="text-24px font-semibold">{{ heading }}</h1>
|
||||
<p v-if="subheading" class="text-16px opacity-80">{{ subheading }}</p>
|
||||
</header>
|
||||
|
||||
<BrandLoading v-if="callbackLoading" 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>
|
||||
|
||||
<p v-if="installing">Installing License Key</p>
|
||||
<template v-if="(typeof success !== undefined)">
|
||||
<p v-if="success">Installed License Key</p>
|
||||
<p v-else>License Key Install Failed</p>
|
||||
<template v-if="installing !== undefined">
|
||||
<p v-if="installing">Installing License Key</p>
|
||||
<template v-else>
|
||||
<p v-if="success === true">Installed License Key</p>
|
||||
<template v-else-if="success === false">
|
||||
<p class="text-red italic">License Key Install Failed</p>
|
||||
<button v-if="isSupported" @click="copy(keyUrl)">{{ copied ? 'Copied' : 'Copy Key URL' }}</button>
|
||||
<p v-else>Copy your Key URL: {{ keyUrl }}</p>
|
||||
<p>Then go to <a href="/Tools/Registration">Tools > Registration</a> to manually install it</p>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<p v-if="updating">Account Connect</p>
|
||||
<template v-if="(typeof success !== undefined)">
|
||||
<p v-if="success">Connect config updated with your account</p>
|
||||
<p v-else>Connect config failed to update</p>
|
||||
<template v-if="updating !== undefined">
|
||||
<p v-if="updating">Updating Connect account config</p>
|
||||
<template v-else>
|
||||
<p v-if="updateSuccess === true">Connect config updated with your account</p>
|
||||
<p v-else-if="updateSuccess === false" class="text-red italic">Connect config failed to update</p>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div v-if="!callbackLoading" class="w-full max-w-xs flex flex-col gap-y-16px mx-auto">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useDropdownStore } from '~/store/dropdown';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
import type { ServerStateDataAction } from '~/types/server';
|
||||
|
||||
const myServersEnv = ref<string>('Staging');
|
||||
const devEnv = ref<string>('development');
|
||||
@@ -15,6 +16,9 @@ const dropdownStore = useDropdownStore();
|
||||
const promoStore = usePromoStore();
|
||||
const { keyActions, pluginInstalled, registered, stateData } = storeToRefs(useServerStore());
|
||||
|
||||
const signInAction = computed(() => stateData.value.actions?.filter((act: { name: string; }) => act.name === 'signIn') ?? []);
|
||||
const signOutAction = computed(() => stateData.value.actions?.filter((act: { name: string; }) => act.name === 'signOut') ?? []);
|
||||
|
||||
const links = computed(():UserProfileLink[] => {
|
||||
return [
|
||||
...(registered.value && pluginInstalled.value
|
||||
@@ -40,25 +44,13 @@ const links = computed(():UserProfileLink[] => {
|
||||
text: 'Settings',
|
||||
title: 'Go to Connect plugin settings',
|
||||
},
|
||||
{
|
||||
click: () => { console.debug('signOut') },
|
||||
external: true,
|
||||
icon: ArrowRightOnRectangleIcon,
|
||||
text: 'Sign Out',
|
||||
title: 'Sign Out to Unregister your server with Connect',
|
||||
},
|
||||
...(signOutAction.value),
|
||||
]
|
||||
: []
|
||||
),
|
||||
...(!registered.value && pluginInstalled.value
|
||||
? [
|
||||
{
|
||||
click: () => { console.debug('signIn') },
|
||||
external: true,
|
||||
icon: UserIcon,
|
||||
text: 'Sign In with Unraid.net Account',
|
||||
title: 'Sign In with Unraid.net Account',
|
||||
},
|
||||
...(signInAction.value),
|
||||
]
|
||||
: []
|
||||
),
|
||||
@@ -77,7 +69,7 @@ const links = computed(():UserProfileLink[] => {
|
||||
: []
|
||||
),
|
||||
];
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @todo setup .env
|
||||
*/
|
||||
const ACCOUNT = 'https://account.unraid.net';
|
||||
const ACCOUNT = 'https://localhost:8008/connect';
|
||||
const CONNECT_DOCS = 'https://docs.unraid.net/category/unraid-connect';
|
||||
const CONNECT_DASHBOARD = 'https://connect.myunraid.net';
|
||||
const DEV_GRAPH_URL = '';
|
||||
|
||||
@@ -15,11 +15,11 @@ export default defineNuxtConfig({
|
||||
{ path: '~/components/UserProfile', prefix: 'Upc' },
|
||||
'~/components',
|
||||
],
|
||||
// runtimeConfig: {
|
||||
// public: { // will be exposed to the client-side
|
||||
// callbackKey: '', // set in .env – https://nuxt.com/docs/guide/going-further/runtime-config#environment-variables
|
||||
// }
|
||||
// },
|
||||
runtimeConfig: {
|
||||
public: { // will be exposed to the client-side
|
||||
callbackKey: 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB', // set in .env – https://nuxt.com/docs/guide/going-further/runtime-config#environment-variables
|
||||
}
|
||||
},
|
||||
customElements: {
|
||||
entries: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { useCallbackStore } from './callback';
|
||||
import { useCallbackStore } from './callbackActions';
|
||||
import { useServerStore } from './server';
|
||||
import { WebguiUpdate } from '~/composables/services/webgui';
|
||||
import type { CallbackAction } from '~/types/callback';
|
||||
@@ -12,56 +12,62 @@ setActivePinia(createPinia());
|
||||
export const useAccountStore = defineStore('account', () => {
|
||||
const callbackStore = useCallbackStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
// State
|
||||
const updating = ref(false);
|
||||
const updateSuccess = ref<boolean|undefined>(undefined);
|
||||
const updating = ref<boolean | undefined>(undefined);
|
||||
const updateSuccess = ref<boolean | undefined>(undefined);
|
||||
|
||||
// Actions
|
||||
const recover = () => {
|
||||
console.debug('[recover]');
|
||||
callbackStore.send('https://account.unraid.net', {
|
||||
...serverStore.serverAccountPayload,
|
||||
console.debug('[accountStore.recover]');
|
||||
callbackStore.send('https://localhost:8008/connect', [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'recover',
|
||||
});
|
||||
}]);
|
||||
};
|
||||
const replace = () => {
|
||||
console.debug('[replace]');
|
||||
callbackStore.send('https://account.unraid.net', {
|
||||
...serverStore.serverAccountPayload,
|
||||
console.debug('[accountStore.replace]');
|
||||
callbackStore.send('https://localhost:8008/connect', [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'replace',
|
||||
});
|
||||
}]);
|
||||
};
|
||||
const signIn = () => {
|
||||
console.debug('[signIn]');
|
||||
callbackStore.send('https://account.unraid.net', {
|
||||
...serverStore.serverAccountPayload,
|
||||
console.debug('[accountStore.signIn]');
|
||||
callbackStore.send('https://localhost:8008/connect', [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'signIn',
|
||||
});
|
||||
}]);
|
||||
};
|
||||
const signOut = () => {
|
||||
console.debug('[signOut]');
|
||||
callbackStore.send('https://account.unraid.net', {
|
||||
...serverStore.serverAccountPayload,
|
||||
console.debug('[accountStore.accountStore.signOut]');
|
||||
callbackStore.send('https://localhost:8008/connect', [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'signOut',
|
||||
});
|
||||
}]);
|
||||
};
|
||||
/**
|
||||
* @description Update myservers.cfg for both Sign In & Sign Out
|
||||
* @note unraid-api requires apikey & token realted keys to be lowercase
|
||||
*/
|
||||
const updatePluginConfig = async (action: CallbackAction) => {
|
||||
console.debug('[updatePluginConfig]', action);
|
||||
console.debug('[accountStore.updatePluginConfig]', action);
|
||||
updating.value = true;
|
||||
const userPayload = {
|
||||
...(action.user
|
||||
? {
|
||||
accesstoken: action.user.signInUserSession.accessToken.jwtToken,
|
||||
apikey: serverStore.apiKey,
|
||||
// avatar: action.user?.attributes.avatar,
|
||||
email: action.user?.attributes.email,
|
||||
idtoken: action.user.signInUserSession.idToken.jwtToken,
|
||||
refreshtoken: action.user.signInUserSession.refreshToken.token,
|
||||
email: action.user?.email,
|
||||
regWizTime: `${Date.now()}_${serverStore.guid}`, // set when signing in the first time and never unset for the sake of displaying Sign In/Up in the UPC without needing to validate guid every time
|
||||
username: action.user?.attributes.preferred_username,
|
||||
username: action.user?.preferred_username,
|
||||
}
|
||||
: {
|
||||
accesstoken: '',
|
||||
@@ -81,17 +87,25 @@ export const useAccountStore = defineStore('account', () => {
|
||||
'#section': 'remote',
|
||||
...userPayload,
|
||||
})
|
||||
.post();
|
||||
console.debug('[updatePluginConfig] WebguiUpdate response', response);
|
||||
updateSuccess.value = true;
|
||||
} catch (error) {
|
||||
console.debug('[updatePluginConfig] WebguiUpdate error', error);
|
||||
updateSuccess.value = false;
|
||||
.post()
|
||||
.res(res => {
|
||||
console.debug('[accountStore.updatePluginConfig] WebguiUpdate res', res);
|
||||
updateSuccess.value = true;
|
||||
})
|
||||
.catch(err => {
|
||||
console.debug('[accountStore.updatePluginConfig] WebguiUpdate err', err);
|
||||
updateSuccess.value = false;
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(updating, (newV, oldV) => {
|
||||
console.debug('[updating.watch]', newV, oldV);
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
updating,
|
||||
|
||||
@@ -1,116 +1,153 @@
|
||||
import AES from 'crypto-js/aes';
|
||||
import Utf8 from 'crypto-js/enc-utf8';
|
||||
import { ref } from 'vue';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { useAccountStore } from './account';
|
||||
import { useInstallKeyStore } from './installKey';
|
||||
import type { CallbackSendPayload, CallbackReceivePayload } from '~/types/callback';
|
||||
import type {
|
||||
ServerAccountCallbackSendPayload,
|
||||
ServerPurchaseCallbackSendPayload,
|
||||
} from '~/types/server';
|
||||
|
||||
export interface ServerAccountCallbackServerData {
|
||||
description?: string;
|
||||
deviceCount?: number;
|
||||
expireTime?: number;
|
||||
flashProduct?: string;
|
||||
flashVendor?: string;
|
||||
guid?: string;
|
||||
keyfile?: string;
|
||||
locale?: string;
|
||||
name?: string;
|
||||
registered: boolean;
|
||||
regGen?: number;
|
||||
regGuid?: string;
|
||||
state: string;
|
||||
wanFQDN?: string;
|
||||
}
|
||||
export type ServerStateDataActionType =
|
||||
| 'signIn'
|
||||
| 'signOut'
|
||||
| 'purchase'
|
||||
| 'redeem'
|
||||
| 'upgrade'
|
||||
| 'recover'
|
||||
| 'replace'
|
||||
| 'trialExtend'
|
||||
| 'trialStart';
|
||||
|
||||
export interface ServerPayload {
|
||||
server: ServerAccountCallbackServerData;
|
||||
type: ServerStateDataActionType;
|
||||
}
|
||||
|
||||
export interface ExternalSignIn {
|
||||
type: 'signIn';
|
||||
apiKey: string;
|
||||
user: UserInfo;
|
||||
}
|
||||
export interface ExternalSignOut {
|
||||
type: 'signOut';
|
||||
}
|
||||
export interface ExternalKeyActions {
|
||||
type: 'recover' | 'replace' | 'trialExtend' | 'trialStart';
|
||||
keyUrl: string;
|
||||
}
|
||||
|
||||
export interface ExternalPurchaseActions {
|
||||
type: 'purchase' | 'redeem' | 'upgrade';
|
||||
}
|
||||
|
||||
export type ExternalActions =
|
||||
| ExternalSignIn
|
||||
| ExternalSignOut
|
||||
| ExternalKeyActions
|
||||
| ExternalPurchaseActions;
|
||||
|
||||
export type UpcActions = ServerPayload;
|
||||
|
||||
export interface ExternalPayload {
|
||||
actions: ExternalActions[];
|
||||
sender: string;
|
||||
type: 'forUpc';
|
||||
}
|
||||
export interface UpcPayload {
|
||||
actions: UpcActions[];
|
||||
sender: string;
|
||||
type: 'fromUpc';
|
||||
}
|
||||
|
||||
export type SendPayloads = ExternalActions[] | UpcActions[];
|
||||
|
||||
export type QueryPayloads = ExternalPayload | UpcPayload;
|
||||
|
||||
export interface UserInfo {
|
||||
'custom:ips_id'?: string;
|
||||
email?: string;
|
||||
email_verifed?: 'true' | 'false';
|
||||
preferred_username?: string;
|
||||
sub?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
interface CallbackActionsStore {
|
||||
redirectToCallbackType: (decryptedData: QueryPayloads) => void;
|
||||
encryptionKey: string;
|
||||
sendType: 'fromUpc' | 'forUpc';
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 useCallbackStore = defineStore('callback', () => {
|
||||
// store helpers
|
||||
const accountStore = useAccountStore();
|
||||
const installKeyStore = useInstallKeyStore();
|
||||
// const config = useRuntimeConfig(); // results in a nuxt error after web components are built
|
||||
// const encryptKey = config.public.callbackKey;
|
||||
const encryptKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
|
||||
// state
|
||||
const callbackFeedbackVisible = ref<boolean>(false);
|
||||
const callbackLoading = ref(false);
|
||||
const decryptedData = ref<CallbackReceivePayload|null>(null);
|
||||
const encryptedMessage = ref<string|null>(null);
|
||||
// actions
|
||||
const send = (url: string = 'https://unraid.ddev.site/init-purchase', payload: CallbackSendPayload) => {
|
||||
console.debug('[send]');
|
||||
const stringifiedData = JSON.stringify({
|
||||
...payload,
|
||||
sender: window.location.href,
|
||||
});
|
||||
encryptedMessage.value = AES.encrypt(stringifiedData, encryptKey).toString();
|
||||
// build and go to url
|
||||
const destinationUrl = new URL(url);
|
||||
console.debug('[send]', encryptedMessage.value, url);
|
||||
destinationUrl.searchParams.set('data', encryptedMessage.value);
|
||||
window.location.href = destinationUrl.toString();
|
||||
};
|
||||
|
||||
const watcher = () => {
|
||||
console.debug('[watcher]');
|
||||
const currentUrl = new URL(window.location.toString());
|
||||
const callbackValue = currentUrl.searchParams.get('data');
|
||||
console.debug('[watcher]', { callbackValue });
|
||||
if (!callbackValue) {
|
||||
return console.debug('[watcher] no callback to handle');
|
||||
}
|
||||
callbackLoading.value = true;
|
||||
const decryptedMessage = AES.decrypt(callbackValue, encryptKey);
|
||||
decryptedData.value = JSON.parse(decryptedMessage.toString(Utf8));
|
||||
console.debug('[watcher]', decryptedMessage, decryptedData.value);
|
||||
if (!decryptedData.value) {
|
||||
callbackLoading.value = false;
|
||||
return console.error('Callback Watcher: Data not present');
|
||||
}
|
||||
if (!decryptedData.value.actions) {
|
||||
callbackLoading.value = false;
|
||||
return console.error('Callback Watcher: Required "action" not present');
|
||||
}
|
||||
// Display the feedback modal
|
||||
show();
|
||||
// Parse the data and perform actions
|
||||
decryptedData.value.actions.forEach(async (action, index, array) => {
|
||||
console.debug('[action]', action);
|
||||
if (action.keyUrl) {
|
||||
const response = await installKeyStore.install(action);
|
||||
console.debug('[action] installKeyStore.install response', response);
|
||||
}
|
||||
if (action.user) {
|
||||
const response = await accountStore.updatePluginConfig(action);
|
||||
console.debug('[action] accountStore.updatePluginConfig', response);
|
||||
}
|
||||
// all actions have run
|
||||
if (array.length === (index + 1)) {
|
||||
console.debug('[actions] DONE');
|
||||
callbackLoading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
console.debug('[hide]');
|
||||
callbackFeedbackVisible.value = false;
|
||||
};
|
||||
const show = () => {
|
||||
console.debug('[show]');
|
||||
callbackFeedbackVisible.value = true;
|
||||
}
|
||||
const toggle = useToggle(callbackFeedbackVisible);
|
||||
|
||||
watch(callbackFeedbackVisible, (newVal, _oldVal) => {
|
||||
console.debug('[callbackFeedbackVisible]', newVal);
|
||||
// removing query string once actions are done so users can't refresh the page and go through the same actions
|
||||
if (newVal === false) {
|
||||
console.debug('[callbackFeedbackVisible] push history w/o query');
|
||||
window.history.pushState(null, '', window.location.pathname);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
export const useCallbackStoreGeneric = (
|
||||
useCallbackActions: () => CallbackActionsStore,
|
||||
) =>
|
||||
defineStore('callback', () => {
|
||||
const callbackActions = useCallbackActions();
|
||||
const encryptionKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
|
||||
const sendType = 'fromUpc';
|
||||
// state
|
||||
callbackFeedbackVisible,
|
||||
callbackLoading,
|
||||
decryptedData,
|
||||
const callbackLoading = ref(false);
|
||||
const encryptedMessage = ref<string | null>(null);
|
||||
|
||||
// actions
|
||||
send,
|
||||
watcher,
|
||||
hide,
|
||||
show,
|
||||
toggle,
|
||||
};
|
||||
});
|
||||
const send = (url: string, payload: SendPayloads) => {
|
||||
console.debug('[callback.send]');
|
||||
const stringifiedData = JSON.stringify({
|
||||
actions: [
|
||||
...payload,
|
||||
],
|
||||
sender: window.location.href,
|
||||
type: sendType,
|
||||
});
|
||||
encryptedMessage.value = AES.encrypt(stringifiedData, encryptionKey).toString();
|
||||
// build and go to url
|
||||
const destinationUrl = new URL(url);
|
||||
destinationUrl.searchParams.set('data', encodeURI(encryptedMessage.value));
|
||||
console.debug('[callback.send]', encryptedMessage.value, destinationUrl);
|
||||
window.location.href = destinationUrl.toString();
|
||||
return;
|
||||
};
|
||||
|
||||
const watcher = () => {
|
||||
console.debug('[callback.watcher]');
|
||||
const currentUrl = new URL(window.location.toString());
|
||||
const callbackValue = decodeURI(currentUrl.searchParams.get('data') ?? '');
|
||||
console.debug('[callback.watcher]', { callbackValue });
|
||||
if (!callbackValue) {
|
||||
return console.debug('[callback.watcher] no callback to handle');
|
||||
}
|
||||
|
||||
callbackLoading.value = true;
|
||||
const decryptedMessage = AES.decrypt(callbackValue, encryptionKey);
|
||||
const decryptedData: QueryPayloads = JSON.parse(decryptedMessage.toString(Utf8));
|
||||
console.debug('[callback.watcher]', decryptedMessage, decryptedData);
|
||||
// Parse the data and perform actions
|
||||
callbackActions.redirectToCallbackType(decryptedData);
|
||||
};
|
||||
|
||||
return {
|
||||
// state
|
||||
callbackLoading,
|
||||
// actions
|
||||
send,
|
||||
watcher,
|
||||
};
|
||||
});
|
||||
|
||||
68
store/callbackActions.ts
Normal file
68
store/callbackActions.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { useAccountStore } from './account';
|
||||
import { useInstallKeyStore } from './installKey';
|
||||
import { useCallbackStoreGeneric, type UpcActions, type QueryPayloads } from './callback';
|
||||
import { useServerStore } from './server';
|
||||
|
||||
export const useCallbackActionsStore = defineStore(
|
||||
'callbackActions',
|
||||
() => {
|
||||
const accountStore = useAccountStore();
|
||||
const installKeyStore = useInstallKeyStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const callbackError = ref();
|
||||
const callbackLoading = ref(false);
|
||||
const callbackFeedbackVisible = ref<boolean>(false);
|
||||
|
||||
const redirectToCallbackType = (decryptedData: QueryPayloads) => {
|
||||
console.debug('[redirectToCallbackType]', { decryptedData });
|
||||
|
||||
if (!decryptedData.type || decryptedData.type === 'fromUpc' || !decryptedData.actions?.length) {
|
||||
callbackError.value = 'Callback redirect type not present or incorrect';
|
||||
return console.error('[redirectToCallbackType]', callbackError.value);
|
||||
}
|
||||
|
||||
// Display the feedback modal
|
||||
callbackFeedbackVisible.value = true;
|
||||
callbackLoading.value = true;
|
||||
// Parse the data and perform actions
|
||||
decryptedData.actions.forEach(async (action, index, array) => {
|
||||
console.debug('[action]', action);
|
||||
if (action?.keyUrl) {
|
||||
await installKeyStore.install(action);
|
||||
}
|
||||
if (action?.user || action.type === 'signOut') {
|
||||
await accountStore.updatePluginConfig(action);
|
||||
}
|
||||
// all actions have run
|
||||
if (array.length === (index + 1)) {
|
||||
console.debug('[actions] DONE');
|
||||
setTimeout(() => {
|
||||
callbackLoading.value = false;
|
||||
}, 2500);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeCallbackFeedback = () => callbackFeedbackVisible.value = false;
|
||||
|
||||
watch(callbackLoading, (newVal, _oldVal) => {
|
||||
console.debug('[callbackLoading]', newVal);
|
||||
// removing query string once actions are done so users can't refresh the page and go through the same actions
|
||||
if (newVal === false) {
|
||||
console.debug('[callbackLoading] push history w/o query');
|
||||
window.history.pushState(null, '', window.location.pathname);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
redirectToCallbackType,
|
||||
callbackFeedbackVisible,
|
||||
callbackLoading,
|
||||
closeCallbackFeedback,
|
||||
}
|
||||
});
|
||||
|
||||
export const useCallbackStore = useCallbackStoreGeneric(useCallbackActionsStore);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
// import { useAccountStore } from './account';
|
||||
// import { useCallbackStore } from './callback';
|
||||
// import { useCallbackStore } from './callbackActions';
|
||||
// import { useInstallKeyStore } from './installKey';
|
||||
// import { useServerStore } from './server';
|
||||
|
||||
|
||||
@@ -12,12 +12,15 @@ setActivePinia(createPinia());
|
||||
export const useInstallKeyStore = defineStore('installKey', () => {
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const installing = ref(false);
|
||||
const success = ref<boolean|undefined>();
|
||||
const keyUrl = ref<string>('');
|
||||
const installing = ref<boolean | undefined>();
|
||||
const success = ref<boolean | undefined>();
|
||||
|
||||
const install = async (action: CallbackAction) => {
|
||||
console.debug('[install]');
|
||||
installing.value = true;
|
||||
keyUrl.value = action.keyUrl ?? '';
|
||||
|
||||
try {
|
||||
const response = await WebguiInstallKey
|
||||
.query({ url: action.keyUrl })
|
||||
@@ -43,8 +46,13 @@ export const useInstallKeyStore = defineStore('installKey', () => {
|
||||
}
|
||||
};
|
||||
|
||||
watch(installing, (newV, oldV) => {
|
||||
console.debug('[installing.watch]', newV, oldV);
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
keyUrl,
|
||||
installing,
|
||||
success,
|
||||
// Actions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { useCallbackStore } from './callback';
|
||||
import { useCallbackStore } from './callbackActions';
|
||||
import { useServerStore } from './server';
|
||||
|
||||
/**
|
||||
@@ -18,21 +18,27 @@ export const usePurchaseStore = defineStore('purchase', () => {
|
||||
const redeem = () => {
|
||||
console.debug('[redeem]');
|
||||
callbackStore.send('https://unraid.ddev.site/init-purchase', {
|
||||
...serverStore.serverPurchasePayload,
|
||||
server: {
|
||||
...serverStore.serverPurchasePayload,
|
||||
},
|
||||
type: 'redeem',
|
||||
});
|
||||
};
|
||||
const purchase = () => {
|
||||
console.debug('[purchase]');
|
||||
callbackStore.send('https://unraid.ddev.site/init-purchase', {
|
||||
...serverStore.serverPurchasePayload,
|
||||
server: {
|
||||
...serverStore.serverPurchasePayload,
|
||||
},
|
||||
type: 'purchase',
|
||||
});
|
||||
};
|
||||
const upgrade = () => {
|
||||
console.debug('[upgrade]');
|
||||
callbackStore.send('https://unraid.ddev.site/init-purchase', {
|
||||
...serverStore.serverPurchasePayload,
|
||||
server: {
|
||||
...serverStore.serverPurchasePayload,
|
||||
},
|
||||
type: 'upgrade',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -111,6 +111,7 @@ export const useServerStore = defineStore('server', () => {
|
||||
guid: guid.value,
|
||||
keyfile: keyfile.value,
|
||||
name: name.value,
|
||||
registered: registered.value ?? false,
|
||||
state: state.value,
|
||||
wanFQDN: wanFQDN.value,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { useCallbackStore } from './callback';
|
||||
import { useCallbackStore } from './callbackActions';
|
||||
import { useServerStore } from './server';
|
||||
|
||||
/**
|
||||
@@ -14,15 +14,19 @@ export const useTrialStore = defineStore('trial', () => {
|
||||
|
||||
const extend = () => {
|
||||
console.debug('[extend]');
|
||||
callbackStore.send('https://account.unraid.net', {
|
||||
...serverStore.serverAccountPayload,
|
||||
callbackStore.send('https://localhost:8008/connect', {
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'trialExtend',
|
||||
});
|
||||
};
|
||||
const start = () => {
|
||||
console.debug('[start]');
|
||||
callbackStore.send('https://account.unraid.net', {
|
||||
...serverStore.serverAccountPayload,
|
||||
callbackStore.send('https://localhost:8008/connect', {
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'trialStart',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16,32 +16,16 @@ export interface UserInfo {
|
||||
sub?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface AuthUser extends CognitoUser {
|
||||
attributes: UserInfo;
|
||||
username?: string;
|
||||
preferredMFA: ChallengeName;
|
||||
signInUserSession: {
|
||||
accessToken: {
|
||||
jwtToken: string;
|
||||
};
|
||||
idToken: {
|
||||
jwtToken: string;
|
||||
};
|
||||
refreshToken: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface CallbackSendPayload extends ServerAccountCallbackSendPayload, ServerPurchaseCallbackSendPayload {
|
||||
export interface CallbackSendPayload {
|
||||
server: ServerAccountCallbackSendPayload|ServerPurchaseCallbackSendPayload;
|
||||
type: ServerStateDataActionType;
|
||||
}
|
||||
|
||||
export interface CallbackAction {
|
||||
apiKey?: string;
|
||||
keyUrl?: string;
|
||||
type: ServerStateDataActionType;
|
||||
user?: AuthUser;
|
||||
user?: UserInfo;
|
||||
}
|
||||
|
||||
export interface CallbackReceivePayload {
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface ServerAccountCallbackSendPayload {
|
||||
keyfile?: string;
|
||||
locale?: string;
|
||||
name?: string;
|
||||
registered: boolean;
|
||||
regGen?: number;
|
||||
regGuid?: string;
|
||||
state: string;
|
||||
|
||||
Reference in New Issue
Block a user