refactor(web): update os use sha256 key server lookup + callback handle multiple actions with update os

This commit is contained in:
Zack Spear
2023-10-10 17:16:28 -05:00
committed by Zack Spear
parent c0319d56b0
commit 22ebb06980
7 changed files with 132 additions and 82 deletions

View File

@@ -2,11 +2,16 @@
// @todo ensure key installs and updateOs can be handled at the same time
// @todo with multiple actions of key install and update after successful key install, rather than showing default success message, show a message to have them confirm the update
import { useClipboard } from '@vueuse/core';
import { ChevronDoubleDownIcon, ClipboardIcon, CogIcon } from '@heroicons/vue/24/solid';
import {
ChevronDoubleDownIcon,
ClipboardIcon,
CogIcon,
InformationCircleIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { WEBGUI_CONNECT_SETTINGS } from '~/helpers/urls';
import { WEBGUI_CONNECT_SETTINGS, WEBGUI_TOOLS_REGISTRATION } from '~/helpers/urls';
import { useAccountStore } from '~/store/account';
import { useCallbackActionsStore } from '~/store/callbackActions';
import { useInstallKeyStore } from '~/store/installKey';
@@ -50,6 +55,8 @@ const {
refreshServerStateStatus,
username,
osVersion,
stateData,
stateDataError,
} = storeToRefs(serverStore);
const {
status: updateOsStatus,
@@ -137,6 +144,7 @@ const keyInstallStatusCopy = computed((): { text: string; } => {
case 'installing':
if (keyActionType.value === 'trialExtend') { txt1 = props.t('Installing Extended Trial'); }
if (keyActionType.value === 'recover') { txt1 = props.t('Installing Recovered'); }
if (keyActionType.value === 'renew') { txt1 = props.t('Installing Extended'); }
if (keyActionType.value === 'replace') { txt1 = props.t('Installing Replaced'); }
return {
text: props.t('{0} {1} Key…', [txt1, keyType.value]),
@@ -229,6 +237,15 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
{{ t('Calculating trial expiration') }}
</p>
</div>
<div v-if="keyType === 'Starter' || keyType === 'Unleashed'" class="opacity-75 italic mt-4px">
<RegistrationUpdateExpiration
v-if="refreshServerStateStatus === 'done'"
:t="t"
/>
<p v-else>
{{ t('Calculating OS Update Eligibility') }}
</p>
</div>
<template v-if="keyInstallStatus === 'failed'">
<div v-if="isSupported" class="flex justify-center">
@@ -249,6 +266,15 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
</template>
</UpcCallbackFeedbackStatus>
<UpcCallbackFeedbackStatus
v-if="stateDataError && callbackStatus !== 'loading' && (keyInstallStatus === 'success' || keyInstallStatus === 'failed')"
:error="true"
:text="t('Post Install License Key Error')"
>
<h4 class="text-18px text-left font-semibold">{{ t(stateData.heading) }}</h4>
<div class="text-left text-16px" v-html="t(stateData.message)" />
</UpcCallbackFeedbackStatus>
<UpcCallbackFeedbackStatus
v-if="accountActionStatus !== 'ready' && !accountActionHide"
:success="accountActionStatus === 'success'"
@@ -263,16 +289,19 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
/> -->
</div>
<template v-if="updateOsStatus === 'confirming'">
<template v-if="updateOsStatus === 'confirming' && !stateDataError">
<div class="text-center flex flex-col gap-y-8px my-16px">
<div class="flex flex-col gap-y-4px">
<p class="text-18px">
{{ t('Current Version: Unraid {0}', [osVersion]) }}
</p>
<ChevronDoubleDownIcon class="animate-pulse w-32px h-32px mx-auto fill-current opacity-50" />
<p class="text-18px">
{{ t('New Version: {0}', [callbackUpdateRelease?.name]) }}
</p>
<p class="text-14px italic opacity-75">
{{ t('This update will require a reboot') }}
</p>
@@ -314,7 +343,7 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
/> -->
</template>
<template v-if="updateOsStatus === 'confirming'">
<template v-if="updateOsStatus === 'confirming' && !stateDataError">
<BrandButton
btn-style="underline"
:text="t('Cancel')"
@@ -325,6 +354,13 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
@click="confirmUpdateOs"
/>
</template>
<template v-if="stateDataError">
<BrandButton
:href="WEBGUI_TOOLS_REGISTRATION.toString()"
:text="t('Fix Error')"
/>
</template>
</div>
</template>
</Modal>

View File

@@ -17,7 +17,7 @@ withDefaults(defineProps<Props>(), {
</script>
<template>
<div class="mx-auto max-w-[45ch]">
<div class="mx-auto max-w-[45ch] flex flex-col gap-8px">
<div class="flex items-start justify-center gap-x-8px">
<CheckCircleIcon v-if="success" class="fill-green-600 w-28px shrink-0" />
<XCircleIcon v-if="error" class="fill-unraid-red w-28px shrink-0" />

View File

@@ -1,5 +1,7 @@
import { request } from '~/composables/services/request';
import type { Release } from '~/store/updateOs';
const KeyServer = request.url('https://keys.lime-technology.com');
export interface StartTrialPayload {
@@ -10,7 +12,7 @@ export interface StartTrialResponse {
license?: string;
trial?: string
}
export const startTrial = (payload: StartTrialPayload) => KeyServer
export const startTrial = async (payload: StartTrialPayload) => await KeyServer
.url('/account/trial')
.formUrl(payload)
.post();
@@ -28,10 +30,11 @@ export interface ValidateGuidPayload {
guid: string;
keyfile?: string;
}
export const validateGuid = (payload: ValidateGuidPayload) => KeyServer
export const validateGuid = async (payload: ValidateGuidPayload) => await KeyServer
.url('/validate/guid')
.formUrl(payload)
.post();
.post()
.json();
export interface KeyLatestPayload {
keyfile: string;
@@ -39,7 +42,12 @@ export interface KeyLatestPayload {
export interface KeyLatestResponse {
license: string;
}
export const keyLatest = (payload: KeyLatestPayload) => KeyServer
export const keyLatest = async (payload: KeyLatestPayload) => await KeyServer
.url('/key/latest')
.formUrl(payload)
.post();
.post();
export const getOsReleaseBySha256 = async (sha256: string): Release => await KeyServer
.url(`/versions/sha256/${sha256}`)
.get()
.json();

View File

@@ -34,7 +34,8 @@ export const useCallbackActionsStore = defineStore('callbackActions', () => {
redirectToCallbackType?.();
};
const redirectToCallbackType = () => {
const redirectToCallbackType = async () => {
console.debug('[redirectToCallbackType]');
if (!callbackData.value || !callbackData.value.type || callbackData.value.type !== 'forUpc' || !callbackData.value.actions?.length) {
callbackError.value = 'Callback redirect type not present or incorrect';
callbackStatus.value = 'ready'; // default status
@@ -45,6 +46,8 @@ export const useCallbackActionsStore = defineStore('callbackActions', () => {
// Parse the data and perform actions
callbackData.value.actions.forEach(async (action, index, array) => {
console.debug('[redirectToCallbackType]', { action, index, array });
if (action?.keyUrl) {
await installKeyStore.install(action as ExternalKeyActions);
}
@@ -61,13 +64,16 @@ export const useCallbackActionsStore = defineStore('callbackActions', () => {
accountStore.setQueueConnectSignOut(true);
}
if (action.type === 'updateOs' && action?.releaseHash) {
const foundRelease = updateOsStore.findReleaseByMd5(action.releaseHash);
if (action.type === 'updateOs' && action?.sha256) {
console.debug('[redirectToCallbackType] updateOs', action);
const foundRelease = await updateOsActionsStore.getReleaseFromKeyServer(action.sha256);
console.debug('[redirectToCallbackType] updateOs foundRelease', foundRelease);
if (!foundRelease) {
throw new Error('Release not found');
}
updateOsActionsStore.confirmUpdateOs(foundRelease);
if (array.length === 1) { // only 1 action, skip refresh server state
console.debug('[redirectToCallbackType] updateOs done');
// removing query string relase is set so users can't refresh the page and go through the same actions
window.history.replaceState(null, '', window.location.pathname);
return

View File

@@ -13,6 +13,7 @@ import {
type ValidateGuidResponse,
} from '~/composables/services/keyServer';
import { WebguiNotify } from '~/composables/services/webgui';
import { useCallbackStore } from '~/store/callbackActions';
import { useInstallKeyStore } from '~/store/installKey';
import { useServerStore } from '~/store/server';
import type { UiBadgeProps } from '~/types/ui/badge';
@@ -36,6 +37,7 @@ interface CachedValidationResponse extends ValidateGuidResponse {
export const REPLACE_CHECK_LOCAL_STORAGE_KEY = 'unraidReplaceCheck';
export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
const callbackStore = useCallbackStore();
const installKeyStore = useInstallKeyStore();
const serverStore = useServerStore();
@@ -124,7 +126,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
response = await validateGuid({
guid: guid.value,
keyfile: keyfile.value,
}).json();
});
}
setReplaceStatus(response?.replaceable ? 'eligible' : 'ineligible');
@@ -146,25 +148,34 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
}).json();
if (keyLatestResponse?.license) {
setRenewStatus('installing');
callbackStore.send(
window.location.origin,
[{
keyUrl: keyLatestResponse.license,
type: 'renew',
}],
false,
'forUpc',
);
// setRenewStatus('installing');
await installKeyStore.install({
keyUrl: keyLatestResponse.license,
type: 'renew',
}).then(() => {
setRenewStatus('installed');
// reset the validation response so we can check again on the subsequent page load. Will also prevent the keyfile from being installed again on page refresh.
purgeValidationResponse();
/** @todo this doesn't work */
WebguiNotify({
cmd: 'add',
csrf_token: serverStore.csrf,
e: 'Keyfile Renewed and Installed (event)',
s: 'Keyfile Renewed and Installed (subject)',
d: 'While license keys are perpetual, certain keyfiles are not. Your keyfile has automatically been renewed and installed in the background. Thanks for your support!',
m: 'Your keyfile has automatically been renewed and installed in the background. Thanks for your support!',
})
});
// await installKeyStore.install({
// keyUrl: keyLatestResponse.license,
// type: 'renew',
// }).then(() => {
// setRenewStatus('installed');
// // reset the validation response so we can check again on the subsequent page load. Will also prevent the keyfile from being installed again on page refresh.
// purgeValidationResponse();
// /** @todo this doesn't work */
// WebguiNotify({
// cmd: 'add',
// csrf_token: serverStore.csrf,
// e: 'Keyfile Renewed and Installed (event)',
// s: 'Keyfile Renewed and Installed (subject)',
// d: 'While license keys are perpetual, certain keyfiles are not. Your keyfile has automatically been renewed and installed in the background. Thanks for your support!',
// m: 'Your keyfile has automatically been renewed and installed in the background. Thanks for your support!',
// })
// });
}
}
} catch (err) {

View File

@@ -25,17 +25,20 @@ export interface RequestReleasesPayload {
}
export interface Release {
version: string; // 6.12.4
name: string; // Unraid Server 6.12.4
basefile: string; // unRAIDServer-6.12.4-x86_64.zip
date: string; // 2023-08-31
url: string; // https://dl.stable.unraid.net/unRAIDServer-6.12.4-x86_64.zip
changelog: string; // https://unraid.net/blog/unraid-os-6.12.4-release-notes
md5: string; // 9050bddcf415f2d0518804e551c1be98
size: number; // 12345122
sha256: string; // fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4
plugin_url: string; // https://dl.stable.unraid.net/unRAIDServer-6.12.4.plg
plugin_sha256: string; // 83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9"
version: string; // "6.12.4"
name: string; // "Unraid 6.12.4"
basefile: string; // "unRAIDServer-6.12.4-x86_64.zip"
date: string; // "2023-08-31"
url: string; // "https://stable.dl.unraid.net/unRAIDServer-6.12.4-x86_64.zip"
changelog: string; // "https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/6.12.4.md"
changelog_pretty: string; // "https://docs.unraid.net/unraid-os/release-notes/6.12.4/"
md5: string; // "df6e5859d28c14617efde36d59458206"
size: string; // "439999418"
sha256: string; // "5ad2d22e8c124e3b925c3bd05f1d782d8521965aabcbedd7dd782db76afd9ace"
plugin_url: string; // "https://stable.dl.unraid.net/unRAIDServer-6.12.4.plg"
plugin_sha256: string; // "57d2ab6036e663208b3f72298ceb478b937b17e333986e68dcae2696c88ed152"
announce_url: string; // "https://unraid.net/blog/6-12-4"
branch: 'stable' | 'next' | 'preview' | 'test'; // "stable"
}
export interface ReleasesResponse {
stable: Release[];
@@ -294,46 +297,19 @@ export const useUpdateOsStoreGeneric = (
});
};
const findReleaseByMd5 = (releaseMd5: string): Release | null => {
let releaseForReturn: Release | null = null;
Object.keys(releases.value?.response ?? {}).forEach(key => {
const branchReleases = releases.value?.response[key as keyof ReleasesResponse];
if (releaseForReturn || !branchReleases || branchReleases.length == 0) {
return;
}
branchReleases.find(release => {
if (release.md5 === releaseMd5) {
releaseForReturn = release;
return releaseForReturn;
}
});
});
return releaseForReturn;
};
const findRelease = (searchKey: keyof Release, searchValue: string): Release | null => {
let releaseForReturn: Release | null = null;
const response = releases?.value?.response;
if (!response) return null;
Object.keys(releases.value?.response ?? {}).forEach(key => {
const branchReleases = releases.value?.response[key as keyof ReleasesResponse];
for (const key of Object.keys(response)) {
const branchReleases = response[key as keyof ReleasesResponse];
if (!branchReleases || branchReleases.length === 0) continue;
if (releaseForReturn || !branchReleases || branchReleases.length == 0) {
return;
}
const foundRelease = branchReleases.find(release => release[searchKey] === searchValue);
if (foundRelease) return foundRelease;
}
branchReleases.find(release => {
if (release[searchKey] === searchValue) {
releaseForReturn = release;
return release;
}
});
});
return releaseForReturn;
return null;
};
const isVersionStable = (version: SemVer | string): boolean => prerelease(version) === null;
@@ -363,7 +339,6 @@ export const useUpdateOsStoreGeneric = (
allFilteredReleases,
// actions
checkForUpdate,
findReleaseByMd5,
findRelease,
requestReleases,
isVersionStable,

View File

@@ -2,6 +2,7 @@ import { BellAlertIcon } from '@heroicons/vue/24/solid';
import { defineStore, createPinia, setActivePinia } from 'pinia';
import useInstallPlugin from '~/composables/installPlugin';
import { getOsReleaseBySha256 } from '~/composables/services/keyServer';
import { ACCOUNT_CALLBACK, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
@@ -114,8 +115,20 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
};
/**
* @description When receiving the callback the Account update page we'll use the provided releaseMd5 to find the release in the releases cache.
* @description When receiving the callback the Account update page we'll use the provided sha256 of the release to get the release from the keyserver
*/
const getReleaseFromKeyServer = async (sha256: string): Release => {
console.debug('[getReleaseFromKeyServer]', sha256)
try {
const response = await getOsReleaseBySha256(sha256);
console.debug('[getReleaseFromKeyServer]', response);
return response;
} catch (error) {
console.error(error);
throw new Error('Unable to get release from keyserver');
}
};
const confirmUpdateOs = async (release: Release) => {
callbackUpdateRelease.value = release;
setStatus('confirming');
@@ -130,7 +143,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
installPlugin({
modalTitle: `${callbackUpdateRelease.value.name} Update`,
pluginUrl: callbackUpdateRelease.value.plugin_url,
update: true,
update: false,
});
};
@@ -186,6 +199,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
setStatus,
setRebootType,
viewCurrentReleaseNotes,
getReleaseFromKeyServer,
};
});