mirror of
https://github.com/unraid/api.git
synced 2026-01-01 14:10:10 -06:00
refactor(web): update os use sha256 key server lookup + callback handle multiple actions with update os
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user