mirror of
https://github.com/unraid/api.git
synced 2026-01-01 06:01:18 -06:00
feat(web): Registration key linked to account status
This commit is contained in:
@@ -46,10 +46,10 @@ import type {
|
||||
|
||||
// '1111-1111-5GDB-123412341234' Starter.key = TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg
|
||||
|
||||
const state: ServerState = 'TRIAL';
|
||||
const currentFlashGuid = '1111-1111-NUIK-TEST1234ZACK'; // this is the flash drive that's been booted from
|
||||
const regGuid = '1111-1111-NUIK-TEST1234ZACK'; // this guid is registered in key server
|
||||
const keyfileBase64 = ''; // @todo raycast download key to base64
|
||||
const state: ServerState = 'BASIC';
|
||||
const currentFlashGuid = '1111-1111-CFXF-TEST1234ZACK'; // this is the flash drive that's been booted from
|
||||
const regGuid = '1111-1111-CFXF-TEST1234ZACK'; // this guid is registered in key server
|
||||
const keyfileBase64 = 'asdf'; // @todo raycast download key to base64
|
||||
|
||||
// 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
|
||||
|
||||
@@ -32,6 +32,7 @@ import type { RegistrationItemProps } from '~/types/registration';
|
||||
|
||||
import KeyActions from '~/components/KeyActions.vue';
|
||||
import RegistrationReplaceCheck from '~/components/Registration/ReplaceCheck.vue';
|
||||
import RegistrationKeyLinkedStatus from '~/components/Registration/KeyLinkedStatus.vue';
|
||||
import RegistrationUpdateExpirationAction from '~/components/Registration/UpdateExpirationAction.vue';
|
||||
import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
|
||||
|
||||
@@ -39,6 +40,8 @@ const { t } = useI18n();
|
||||
|
||||
const replaceRenewCheckStore = useReplaceRenewStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const { keyLinkedStatus } = storeToRefs(replaceRenewCheckStore);
|
||||
const {
|
||||
computedArray,
|
||||
arrayWarning,
|
||||
@@ -103,7 +106,7 @@ const subheading = computed(() => {
|
||||
const showTrialExpiration = computed((): boolean => state.value === 'TRIAL' || state.value === 'EEXPIRED');
|
||||
const showUpdateEligibility = computed((): boolean => !!(regExp.value));
|
||||
const keyInstalled = computed((): boolean => !!(!stateDataError.value && state.value !== 'ENOKEYFILE'));
|
||||
const showTransferStatus = computed((): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value));
|
||||
const showLinkedAndTransferStatus = computed((): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value));
|
||||
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
|
||||
const showFilteredKeyActions = computed((): boolean => !!(keyActions.value && keyActions.value?.filter(action => !['renew'].includes(action.name)).length > 0));
|
||||
|
||||
@@ -189,13 +192,20 @@ const items = computed((): RegistrationItemProps[] => {
|
||||
: t('{0} out of {1} devices', [deviceCount.value, computedRegDevs.value === -1 ? t('unlimited') : computedRegDevs.value]),
|
||||
}]
|
||||
: []),
|
||||
...(showTransferStatus.value
|
||||
...(showLinkedAndTransferStatus.value
|
||||
? [{
|
||||
label: t('Transfer License to New Flash'),
|
||||
component: RegistrationReplaceCheck,
|
||||
componentProps: { t },
|
||||
}]
|
||||
: []),
|
||||
...(regTo.value && showLinkedAndTransferStatus.value
|
||||
? [{
|
||||
label: t('Linked to Unraid.net account'),
|
||||
component: RegistrationKeyLinkedStatus,
|
||||
componentProps: { t },
|
||||
}]
|
||||
: []),
|
||||
|
||||
...(showFilteredKeyActions.value
|
||||
? [{
|
||||
|
||||
@@ -35,7 +35,13 @@ const evenBgColor = computed(() => {
|
||||
class="leading-normal sm:col-span-3"
|
||||
:class="!label && 'sm:col-start-2'"
|
||||
>
|
||||
<span v-if="text" class="select-all" :class="!error ? 'opacity-75' : ''">
|
||||
<span
|
||||
v-if="text"
|
||||
class="select-all"
|
||||
:class="{
|
||||
'opacity-75': !error,
|
||||
}"
|
||||
>
|
||||
{{ text }}
|
||||
</span>
|
||||
<template v-if="$slots['right']">
|
||||
|
||||
73
web/components/Registration/KeyLinkedStatus.vue
Normal file
73
web/components/Registration/KeyLinkedStatus.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowPathIcon,
|
||||
LinkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const replaceRenewStore = useReplaceRenewStore();
|
||||
const { keyLinkedStatus, keyLinkedOutput } = storeToRefs(replaceRenewStore);
|
||||
|
||||
defineProps<{
|
||||
t: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center justify-between gap-8px">
|
||||
<BrandButton
|
||||
v-if="keyLinkedStatus !== 'linked' && keyLinkedStatus !== 'checking'"
|
||||
btn-style="none"
|
||||
:no-padding="true"
|
||||
:title="t('Refresh')"
|
||||
class="group"
|
||||
@click="replaceRenewStore.check(true)"
|
||||
>
|
||||
<UiBadge
|
||||
v-if="keyLinkedOutput"
|
||||
:color="keyLinkedOutput.color"
|
||||
:icon="keyLinkedOutput.icon"
|
||||
:icon-right="ArrowPathIcon"
|
||||
size="16px"
|
||||
>
|
||||
{{ t(keyLinkedOutput.text) }}
|
||||
</UiBadge>
|
||||
</BrandButton>
|
||||
<UiBadge
|
||||
v-else
|
||||
:color="keyLinkedOutput.color"
|
||||
:icon="keyLinkedOutput.icon"
|
||||
size="16px"
|
||||
>
|
||||
{{ t(keyLinkedOutput.text) }}
|
||||
</UiBadge>
|
||||
|
||||
<span class="inline-flex flex-wrap-items-start gap-8px">
|
||||
<BrandButton
|
||||
v-if="keyLinkedStatus === 'notLinked'"
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:icon="LinkIcon"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
:text="t('Link Key')"
|
||||
:title="t('Learn more and link your key to your account')"
|
||||
class="text-14px"
|
||||
@click="accountStore.linkKey"
|
||||
/>
|
||||
<BrandButton
|
||||
v-else
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
:text="t('Learn More')"
|
||||
class="text-14px"
|
||||
@click="accountStore.manage"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,7 +17,7 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-start justify-between gap-8px">
|
||||
<div class="flex flex-wrap items-center justify-between gap-8px">
|
||||
<BrandButton
|
||||
v-if="!replaceStatusOutput"
|
||||
:icon="KeyIcon"
|
||||
|
||||
@@ -18,6 +18,7 @@ export const startTrial = async (payload: StartTrialPayload): Promise<StartTrial
|
||||
|
||||
export interface ValidateGuidResponse {
|
||||
hasNewerKeyfile : boolean;
|
||||
linked: boolean;
|
||||
purchaseable: true;
|
||||
registered: false;
|
||||
replaceable: false;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { CONNECT_SIGN_IN, CONNECT_SIGN_OUT } from './account.fragment';
|
||||
import { useCallbackStore } from '~/store/callbackActions';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUnraidApiStore } from '~/store/unraidApi';
|
||||
import { ACCOUNT_CALLBACK } from '~/helpers/urls';
|
||||
@@ -24,6 +25,7 @@ export interface ConnectSignInMutationPayload {
|
||||
export const useAccountStore = defineStore('account', () => {
|
||||
const callbackStore = useCallbackStore();
|
||||
const errorsStore = useErrorsStore();
|
||||
const replaceRenewStore = useReplaceRenewStore();
|
||||
const serverStore = useServerStore();
|
||||
const unraidApiStore = useUnraidApiStore();
|
||||
|
||||
@@ -84,6 +86,40 @@ export const useAccountStore = defineStore('account', () => {
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
);
|
||||
};
|
||||
const myKeys = async () => {
|
||||
/**
|
||||
* Purge the validation response so we can start fresh after the user has linked their key
|
||||
*/
|
||||
await replaceRenewStore.purgeValidationResponse();
|
||||
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'myKeys',
|
||||
}],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
);
|
||||
};
|
||||
const linkKey = async () => {
|
||||
/**
|
||||
* Purge the validation response so we can start fresh after the user has linked their key
|
||||
*/
|
||||
await replaceRenewStore.purgeValidationResponse();
|
||||
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'linkKey',
|
||||
}],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
);
|
||||
};
|
||||
const recover = () => {
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
@@ -267,6 +303,7 @@ export const useAccountStore = defineStore('account', () => {
|
||||
// Getters
|
||||
accountActionType,
|
||||
// Actions
|
||||
linkKey,
|
||||
manage,
|
||||
recover,
|
||||
replace,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
XCircleIcon,
|
||||
ShieldExclamationIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
@@ -54,6 +55,47 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
|
||||
cause?: unknown;
|
||||
} | null>(null);
|
||||
|
||||
const keyLinkedStatus = ref<'checking' | 'linked' | 'notLinked' | 'error' | 'ready'>('ready');
|
||||
const setKeyLinked = (value: typeof keyLinkedStatus.value) => {
|
||||
keyLinkedStatus.value = value;
|
||||
};
|
||||
const keyLinkedOutput = computed((): UiBadgePropsExtended => {
|
||||
// text values are translated in the component
|
||||
switch (keyLinkedStatus.value) {
|
||||
case 'checking':
|
||||
return {
|
||||
color: 'gamma',
|
||||
icon: BrandLoadingWhite,
|
||||
text: 'Checking...',
|
||||
};
|
||||
case 'linked':
|
||||
return {
|
||||
color: 'green',
|
||||
icon: CheckCircleIcon,
|
||||
text: 'Linked',
|
||||
};
|
||||
case 'notLinked':
|
||||
return {
|
||||
color: 'yellow',
|
||||
icon: ExclamationCircleIcon,
|
||||
text: 'Not Linked',
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
color: 'red',
|
||||
icon: ShieldExclamationIcon,
|
||||
text: error.value?.message || 'Unknown error',
|
||||
};
|
||||
case 'ready':
|
||||
default:
|
||||
return {
|
||||
color: 'gray',
|
||||
icon: ExclamationCircleIcon,
|
||||
text: 'Unknown',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const renewStatus = ref<'checking' | 'error' | 'installing' | 'installed' | 'ready'>('ready');
|
||||
const setRenewStatus = (status: typeof renewStatus.value) => {
|
||||
renewStatus.value = status;
|
||||
@@ -128,7 +170,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const check = async () => {
|
||||
const check = async (skipCache: boolean = false) => {
|
||||
if (!guid.value) {
|
||||
setReplaceStatus('error');
|
||||
error.value = { name: 'Error', message: 'Flash GUID required to check replacement status' };
|
||||
@@ -139,9 +181,14 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// validate the cache first - will purge if it's too old
|
||||
await validateCache();
|
||||
if (skipCache) {
|
||||
await purgeValidationResponse();
|
||||
} else {
|
||||
// validate the cache first - will purge if it's too old
|
||||
await validateCache();
|
||||
}
|
||||
|
||||
setKeyLinked('checking');
|
||||
setReplaceStatus('checking');
|
||||
error.value = null;
|
||||
/**
|
||||
@@ -158,6 +205,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
|
||||
}
|
||||
|
||||
setReplaceStatus(response?.replaceable ? 'eligible' : 'ineligible');
|
||||
setKeyLinked(response?.linked ? 'linked' : 'notLinked');
|
||||
|
||||
/** cache the response to prevent repeated POSTs in the session */
|
||||
if ((replaceStatus.value === 'eligible' || replaceStatus.value === 'ineligible') && !validationResponse.value) {
|
||||
@@ -197,11 +245,14 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
|
||||
|
||||
return {
|
||||
// state
|
||||
keyLinkedStatus,
|
||||
keyLinkedOutput,
|
||||
renewStatus,
|
||||
replaceStatus,
|
||||
replaceStatusOutput,
|
||||
// actions
|
||||
check,
|
||||
purgeValidationResponse,
|
||||
setReplaceStatus,
|
||||
setRenewStatus,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user