diff --git a/api/dev/states/var.ini b/api/dev/states/var.ini index 662903a4c0..c8df905cfb 100644 --- a/api/dev/states/var.ini +++ b/api/dev/states/var.ini @@ -91,6 +91,7 @@ configValid="ineligible" joinStatus="Not joined" deviceCount="4" flashGUID="0000-0000-0000-000000000000" +tpmGUID="03-V35H8S0L1QHK1SBG1XHXJNH7" flashProduct="DataTraveler_3.0" flashVendor="KINGSTON" regCheck="" @@ -141,4 +142,4 @@ shareSMBCount="1" shareNFSCount="0" shareMoverActive="no" reservedNames="parity,parity2,parity3,diskP,diskQ,diskR,disk,disks,flash,boot,user,user0,disk0,disk1,disk2,disk3,disk4,disk5,disk6,disk7,disk8,disk9,disk10,disk11,disk12,disk13,disk14,disk15,disk16,disk17,disk18,disk19,disk20,disk21,disk22,disk23,disk24,disk25,disk26,disk27,disk28,disk29,disk30,disk31" -csrf_token="0000000000000000" \ No newline at end of file +csrf_token="0000000000000000" diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 2f51f85b5b..6f35a56d8e 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -552,6 +552,7 @@ type Vars implements Node { flashGuid: String flashProduct: String flashVendor: String + tpmGuid: String regCheck: String regFile: String regGuid: String diff --git a/api/src/__test__/store/modules/emhttp.test.ts b/api/src/__test__/store/modules/emhttp.test.ts index e9e987e859..696f8d35c6 100644 --- a/api/src/__test__/store/modules/emhttp.test.ts +++ b/api/src/__test__/store/modules/emhttp.test.ts @@ -1144,6 +1144,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000 "sysFlashSlots": 1, "sysModel": "Dell R710", "timeZone": "Australia/Adelaide", + "tpmGuid": "03-V35H8S0L1QHK1SBG1XHXJNH7", "useNetbios": "yes", "useNtp": true, "useSsh": true, diff --git a/api/src/__test__/store/state-parsers/var.test.ts b/api/src/__test__/store/state-parsers/var.test.ts index 9b70869387..26042cc934 100644 --- a/api/src/__test__/store/state-parsers/var.test.ts +++ b/api/src/__test__/store/state-parsers/var.test.ts @@ -161,6 +161,7 @@ test('Returns parsed state file', async () => { "sysFlashSlots": 1, "sysModel": "Dell R710", "timeZone": "Australia/Adelaide", + "tpmGuid": "03-V35H8S0L1QHK1SBG1XHXJNH7", "useNetbios": "yes", "useNtp": true, "useSsh": true, diff --git a/api/src/core/types/states/var.ts b/api/src/core/types/states/var.ts index 2245f78c34..7b7dc7eaaf 100644 --- a/api/src/core/types/states/var.ts +++ b/api/src/core/types/states/var.ts @@ -32,6 +32,7 @@ export type Var = { flashGuid: string; flashProduct: string; flashVendor: string; + tpmGuid?: string; /** Current progress of the {@link ?content=mover | mover}. */ fsCopyPrcnt: number; fsNumMounted: number; diff --git a/api/src/store/services/__test__/state-file-loader.test.ts b/api/src/store/services/__test__/state-file-loader.test.ts index d699cdcab3..6738014c33 100644 --- a/api/src/store/services/__test__/state-file-loader.test.ts +++ b/api/src/store/services/__test__/state-file-loader.test.ts @@ -11,7 +11,10 @@ import { StateFileKey } from '@app/store/types.js'; const VAR_FIXTURE = readFileSync(new URL('../../../../dev/states/var.ini', import.meta.url), 'utf-8'); const writeVarFixture = (dir: string, safeMode: 'yes' | 'no') => { - const content = VAR_FIXTURE.replace(/safeMode="(yes|no)"/, `safeMode="${safeMode}"`); + const content = VAR_FIXTURE.replace(/safeMode="(yes|no)"/, `safeMode="${safeMode}"`).replace( + /flashGUID="([^"]+)"/, + 'flashGUID="$1"\ntpmGUID="03-V35H8S0L1QHK1SBG1XHXJNH7"' + ); writeFileSync(join(dir, `${StateFileKey.var}.ini`), content); }; @@ -43,12 +46,16 @@ describe('loadStateFileSync', () => { const result = loadStateFileSync(StateFileKey.var); expect(result?.safeMode).toBe(true); + expect(result?.tpmGuid).toBe('03-V35H8S0L1QHK1SBG1XHXJNH7'); expect(dispatchSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'emhttp/updateEmhttpState', payload: { field: StateFileKey.var, - state: expect.objectContaining({ safeMode: true }), + state: expect.objectContaining({ + safeMode: true, + tpmGuid: '03-V35H8S0L1QHK1SBG1XHXJNH7', + }), }, }) ); diff --git a/api/src/store/state-parsers/var.ts b/api/src/store/state-parsers/var.ts index 140569d71e..e0446a1220 100644 --- a/api/src/store/state-parsers/var.ts +++ b/api/src/store/state-parsers/var.ts @@ -39,6 +39,7 @@ export type VarIni = { flashGuid: string; flashProduct: string; flashVendor: string; + tpmGuid?: string; fsCopyPrcnt: string; fsNumMounted: string; fsNumUnmountable: string; diff --git a/api/src/unraid-api/cli/generated/graphql.ts b/api/src/unraid-api/cli/generated/graphql.ts index e95c361499..1f03d6b58f 100644 --- a/api/src/unraid-api/cli/generated/graphql.ts +++ b/api/src/unraid-api/cli/generated/graphql.ts @@ -414,6 +414,8 @@ export type BrandingConfig = { background?: Maybe; /** Banner image source. Supports local path, remote URL, or data URI/base64. */ bannerImage?: Maybe; + /** Built-in case model value written to case-model.cfg when no custom override is supplied. */ + caseModel?: Maybe; /** Case model image source. Supports local path, remote URL, or data URI/base64. */ caseModelImage?: Maybe; /** Indicates if a partner logo exists */ @@ -451,6 +453,7 @@ export type BrandingConfig = { export type BrandingConfigInput = { background?: InputMaybe; bannerImage?: InputMaybe; + caseModel?: InputMaybe; caseModelImage?: InputMaybe; hasPartnerLogo?: InputMaybe; header?: InputMaybe; @@ -3548,4 +3551,4 @@ export const GetSsoUsersDocument = {"kind":"Document","definitions":[{"kind":"Op export const SystemReportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SystemReport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"machineId"}},{"kind":"Field","name":{"kind":"Name","value":"system"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"manufacturer"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"serial"}},{"kind":"Field","name":{"kind":"Name","value":"uuid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"core"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraid"}},{"kind":"Field","name":{"kind":"Name","value":"kernel"}}]}},{"kind":"Field","name":{"kind":"Name","value":"packages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"openssl"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"server"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const ConnectStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dynamicRemoteAccess"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabledType"}},{"kind":"Field","name":{"kind":"Name","value":"runningType"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode; export const ServicesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"online"}},{"kind":"Field","name":{"kind":"Name","value":"uptime"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]} as unknown as DocumentNode; -export const ValidateOidcSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidateOidcSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateOidcSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode; +export const ValidateOidcSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidateOidcSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateOidcSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts index 1097db4f15..15ec2b727d 100644 --- a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts +++ b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts @@ -311,6 +311,9 @@ export class Vars extends Node { @Field({ nullable: true }) flashVendor?: string; + @Field({ nullable: true }) + tpmGuid?: string; + @Field({ nullable: true }) regCheck?: string; diff --git a/web/__test__/components/CallbackFeedback.test.ts b/web/__test__/components/CallbackFeedback.test.ts index 79d2028b0e..abdb0e8a64 100644 --- a/web/__test__/components/CallbackFeedback.test.ts +++ b/web/__test__/components/CallbackFeedback.test.ts @@ -190,6 +190,8 @@ vi.mock('~/store/updateOsActions', () => ({ })); describe('CallbackFeedback.vue', () => { + const originalLocation = window.location; + beforeEach(() => { accountAction.value = undefined; accountActionHide.value = false; @@ -220,6 +222,11 @@ describe('CallbackFeedback.vue', () => { mockSetCallbackStatus.mockClear(); mockInstallOsUpdate.mockClear(); mockSetUpdateOsStatus.mockClear(); + + Object.defineProperty(window, 'location', { + configurable: true, + value: originalLocation, + }); }); afterEach(() => { @@ -302,4 +309,29 @@ describe('CallbackFeedback.vue', () => { expect(wrapper.find('.modal').attributes('data-error')).toBe('true'); expect(wrapper.find('.modal').attributes('data-success')).toBe('false'); }); + + it('reloads the page when the modal is dismissed after a callback action', async () => { + const mockReload = vi.fn(); + + Object.defineProperty(window, 'location', { + configurable: true, + value: { + ...originalLocation, + reload: mockReload, + }, + }); + + callbackStatus.value = 'success'; + keyActionType.value = 'purchase'; + keyInstallStatus.value = 'success'; + keyType.value = 'Pro'; + + const wrapper = mountComponent(); + + wrapper.findComponent({ name: 'Modal' }).vm.$emit('close'); + await wrapper.vm.$nextTick(); + + expect(mockSetCallbackStatus).toHaveBeenCalledWith('ready'); + expect(mockReload).toHaveBeenCalledTimes(1); + }); }); diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts index d6d363d925..a2ff2bde2e 100644 --- a/web/__test__/components/Registration.test.ts +++ b/web/__test__/components/Registration.test.ts @@ -121,8 +121,10 @@ vi.mock('~/components/UserProfile/UptimeExpire.vue', () => ({ const initialServerState = { dateTimeFormat: { date: 'MMM D, YYYY', time: 'h:mm A' }, deviceCount: 0, + flashGuid: '', guid: '', keyfile: '', + mdState: '', regGuid: '', regTm: '', regTo: '', @@ -133,6 +135,7 @@ const initialServerState = { state: 'ENOKEYFILE', stateData: { heading: 'Default Heading', message: 'Default Message' }, stateDataError: false, + tpmGuid: '', tooManyDevices: false, }; @@ -223,6 +226,18 @@ describe('Registration.standalone.vue', () => { expect(wrapper.find('[data-testid="key-linked-status"]').exists()).toBe(false); }); + it('does not show a connect sign-in action on the registration page', async () => { + serverStore.state = 'ENOKEYFILE'; + serverStore.registered = false; + serverStore.connectPluginInstalled = 'INSTALLED' as ServerconnectPluginInstalled; + + await wrapper.vm.$nextTick(); + + expect(serverStore.authAction?.name).toBe('signIn'); + expect(wrapper.text()).not.toContain('Sign In'); + expect(serverStore.stateData.actions?.some((action) => action.name === 'signIn')).toBe(true); + }); + it('triggers expected action when key action is clicked', async () => { serverStore.state = 'TRIAL'; @@ -258,6 +273,9 @@ describe('Registration.standalone.vue', () => { await wrapper.vm.$nextTick(); + expect(wrapper.text()).toContain('License Device'); + expect(wrapper.text()).toContain('License device type'); + const keyTypeItem = findItemByLabel(t('License key type')); expect(keyTypeItem).toBeDefined(); @@ -288,6 +306,106 @@ describe('Registration.standalone.vue', () => { expect(attachedStorageDevicesItem?.props('text')).toBe('8 out of unlimited devices'); }); + it('shows TPM transfer guidance when TPM licensing is available', async () => { + serverStore.state = 'PRO'; + serverStore.guid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.mdState = 'STOPPED'; + serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.keyfile = 'keyfile-present'; + + await wrapper.vm.$nextTick(); + + const transferNotice = wrapper.find('[data-testid="tpm-transfer-available"]'); + + expect(transferNotice.exists()).toBe(true); + expect(transferNotice.text()).toContain('TPM licensing is available on this server.'); + expect(transferNotice.text()).toContain('Stop the array.'); + expect(transferNotice.text()).toContain('Remove the USB flash boot device.'); + expect(transferNotice.text()).toContain('Refresh this page.'); + expect(transferNotice.text()).toContain('Press Replace Key.'); + expect(transferNotice.text()).toContain('Start the array.'); + expect(transferNotice.text()).not.toContain('Tools > Registration'); + }); + + it('only checks the stop-array step when the array is stopped', async () => { + serverStore.state = 'PRO'; + serverStore.guid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.mdState = 'STARTED'; + serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.keyfile = 'keyfile-present'; + + await wrapper.vm.$nextTick(); + + const transferNotice = wrapper.find('[data-testid="tpm-transfer-available"]'); + + expect(transferNotice.exists()).toBe(true); + expect(transferNotice.text()).not.toContain('[x] Stop the array.'); + expect(transferNotice.text()).toMatch(/\[\s\]Stop the array\./); + }); + + it('shows TPM purchase guidance instead of TPM transfer steps for trial states', async () => { + serverStore.state = 'TRIAL'; + serverStore.guid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.keyfile = 'keyfile-present'; + + await wrapper.vm.$nextTick(); + + const trialNotice = wrapper.find('[data-testid="tpm-transfer-trial"]'); + + expect(trialNotice.exists()).toBe(true); + expect(trialNotice.text()).toContain( + 'TPM licensing will be available after you purchase a license.' + ); + expect(trialNotice.text()).toContain( + 'Trial licenses cannot be moved to TPM. Once you purchase a license for this server, you will be able to transfer it from your USB flash device to TPM.' + ); + expect(wrapper.find('[data-testid="tpm-transfer-available"]').exists()).toBe(false); + }); + + it('shows checked TPM transfer steps after switching to TPM boot', async () => { + serverStore.state = 'EGUID'; + serverStore.guid = '03-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.mdState = 'STOPPED'; + serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.regGuid = '058F-6387-0000-0000F1F1E1C6'; + + await wrapper.vm.$nextTick(); + + const transferNotice = wrapper.find('[data-testid="tpm-transfer-ready"]'); + + expect(transferNotice.exists()).toBe(true); + expect(transferNotice.text()).toContain('Continue your TPM license transfer.'); + expect(transferNotice.text()).toContain('The first two steps are already complete.'); + expect(transferNotice.text()).toContain('[x]'); + expect(transferNotice.text()).toContain('Stop the array.'); + expect(transferNotice.text()).toContain('Remove the USB flash boot device.'); + expect(transferNotice.text()).toContain('Press Replace Key.'); + expect(transferNotice.text()).toContain('Start the array.'); + }); + + it('shows the stop-array step as incomplete in TPM-ready state while the array is running', async () => { + serverStore.state = 'EGUID'; + serverStore.guid = '03-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.mdState = 'STARTED'; + serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.regGuid = '058F-6387-0000-0000F1F1E1C6'; + + await wrapper.vm.$nextTick(); + + const transferNotice = wrapper.find('[data-testid="tpm-transfer-ready"]'); + + expect(transferNotice.exists()).toBe(true); + expect(transferNotice.text()).toContain( + 'The USB flash boot device is already removed. Stop the array, then press Replace Key to transfer this license to TPM.' + ); + expect(transferNotice.text()).toMatch(/\[\s\]Stop the array\./); + expect(transferNotice.text()).toMatch(/\[x\]Remove the USB flash boot device\./); + }); + it('adds Activate Trial fallback for ENOKEYFILE partner activation', async () => { activationCodeStateHolder.current!.value = { code: 'PARTNER-CODE-123', diff --git a/web/_webGui/testWebComponents.page b/web/_webGui/testWebComponents.page index 58b07af40c..3aad585c30 100644 --- a/web/_webGui/testWebComponents.page +++ b/web/_webGui/testWebComponents.page @@ -90,6 +90,7 @@ $serverData = [ "flashProduct" => $var['flashProduct'], "flashVendor" => $var['flashVendor'], "flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true', + "flashGuid" => $var['flashGUID'], "guid" => $var['flashGUID'], "hasRemoteApikey" => !empty($myservers['remote']['apikey']), "lanIp" => ipaddr(), @@ -113,6 +114,7 @@ $serverData = [ "registeredTime" => $myservers['remote']['regWizTime'] ?? '', "site" => $_SERVER['REQUEST_SCHEME']."://".$_SERVER['HTTP_HOST'], "state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']), + "tpmGuid" => $var['tpmGUID'] ?? '', "textColor" => ($header) ? '#'.$header : '', "theme" => $display['theme'], "ts" => time(), diff --git a/web/src/_data/serverState.ts b/web/src/_data/serverState.ts index 92f2b20adc..2a9293ace3 100644 --- a/web/src/_data/serverState.ts +++ b/web/src/_data/serverState.ts @@ -49,6 +49,7 @@ import type { const state: ServerState = 'BASIC' as ServerState; const currentFlashGuid = '1111-1111-YIJD-ZACK1234TEST'; // this is the flash drive that's been booted from +const currentTpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7'; const regGuid = '1111-1111-YIJD-ZACK1234TEST'; // this guid is registered in key server const keyfileBase64 = ''; @@ -147,6 +148,7 @@ const baseServerState: Server = { deviceCount: 3, expireTime, flashBackupActivated: !!connectPluginInstalled, + flashGuid: currentFlashGuid, flashProduct: 'SanDisk_3.2Gen1', flashVendor: 'USB', guid: currentFlashGuid, @@ -181,6 +183,7 @@ const baseServerState: Server = { name: 'white', textColor: '', }, + tpmGuid: currentTpmGuid, // updateOsResponse: { // version: '6.12.6', // name: 'Unraid 6.12.6', diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue index 5c54af1156..b8e3bfb273 100644 --- a/web/src/components/Registration.standalone.vue +++ b/web/src/components/Registration.standalone.vue @@ -20,7 +20,7 @@ import { useI18n } from 'vue-i18n'; import { storeToRefs } from 'pinia'; import { ShieldCheckIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid'; -import { BrandButton, CardWrapper, PageContainer, SettingsGrid } from '@unraid/ui'; +import { CardWrapper, PageContainer, SettingsGrid } from '@unraid/ui'; import type { RegistrationItemProps } from '~/types/registration'; import type { ServerStateDataAction } from '~/types/server'; @@ -43,16 +43,17 @@ const serverStore = useServerStore(); const { activationCode } = storeToRefs(useActivationCodeDataStore()); const { - authAction, bootDeviceType, dateTimeFormat, deviceCount, + flashGuid, flashProduct, flashVendor, guid, keyActions, keyfile, computedRegDevs, + mdState, regGuid, regTm, regTo, @@ -63,6 +64,7 @@ const { state, stateData, stateDataError, + tpmGuid, tooManyDevices, } = storeToRefs(serverStore); @@ -132,6 +134,83 @@ const showPartnerActivationCode = computed(() => { (currentState === 'ENOKEYFILE' || currentState === 'TRIAL' || currentState === 'EEXPIRED') ); }); +const showTpmTransferInfo = computed((): boolean => + Boolean( + keyInstalled.value && + !showTrialExpiration.value && + bootDeviceType.value === 'flash' && + // On TPM systems, flashGuid may fall back to the TPM GUID, so only show + // this while we're still booted from flash and the GUIDs differ. + flashGuid.value && + tpmGuid.value && + flashGuid.value !== tpmGuid.value + ) +); +const showTpmTransferTrialInfo = computed((): boolean => + Boolean( + showTrialExpiration.value && + bootDeviceType.value === 'flash' && + flashGuid.value && + tpmGuid.value && + flashGuid.value !== tpmGuid.value + ) +); +const showTpmTransferReadyInfo = computed((): boolean => + Boolean( + !showTrialExpiration.value && + state.value === 'EGUID' && + bootDeviceType.value === 'tpm' && + guid.value && + tpmGuid.value && + guid.value === tpmGuid.value + ) +); +const isArrayStopped = computed((): boolean => mdState.value === 'STOPPED'); +const tpmTransferReadyDescription = computed(() => + isArrayStopped.value + ? t('registration.tpmTransferReadyDescription') + : t('registration.tpmTransferReadyDescriptionArrayRunning') +); +const tpmTransferPreparationSteps = computed(() => [ + { + completed: isArrayStopped.value, + label: t('registration.tpmTransferAvailableSteps.stopArray'), + }, + { + completed: false, + label: t('registration.tpmTransferAvailableSteps.removeFlash'), + }, + { + completed: false, + label: t('registration.tpmTransferAvailableSteps.refreshPage'), + }, + { + completed: false, + label: t('registration.tpmTransferAvailableSteps.replaceKey'), + }, + { + completed: false, + label: t('registration.tpmTransferAvailableSteps.startArray'), + }, +]); +const tpmTransferReadySteps = computed(() => [ + { + completed: isArrayStopped.value, + label: t('registration.tpmTransferAvailableSteps.stopArray'), + }, + { + completed: true, + label: t('registration.tpmTransferAvailableSteps.removeFlash'), + }, + { + completed: false, + label: t('registration.tpmTransferAvailableSteps.replaceKey'), + }, + { + completed: false, + label: t('registration.tpmTransferAvailableSteps.startArray'), + }, +]); // Organize items into three sections const bootDeviceItems = computed((): RegistrationItemProps[] => { @@ -316,15 +395,6 @@ const actionItems = computed((): RegistrationItemProps[] => { class="prose text-base leading-relaxed whitespace-normal opacity-75" v-html="subheading" /> - - - @@ -384,6 +454,64 @@ const actionItems = computed((): RegistrationItemProps[] => { class="rounded-lg border border-gray-200 p-4 dark:border-gray-700" >

{{ t('registration.actions') }}

+
+

+ {{ t('registration.tpmTransferTrial') }} +

+

+ {{ t('registration.tpmTransferTrialDescription') }} +

+
+
+

+ {{ t('registration.tpmTransferAvailable') }} +

+

+ {{ t('registration.tpmTransferAvailableDescription') }} +

+
    +
  • + {{ step.completed ? '[x]' : '[ ]' }} + {{ step.label }} +
  • +
+
+
+

+ {{ t('registration.tpmTransferReady') }} +

+

+ {{ tpmTransferReadyDescription }} +

+
    +
  • + + {{ step.completed ? '[x]' : '[ ]' }} + + {{ step.label }} +
  • +
+
{ }); const closeText = computed(() => t('common.close')); // !connectPluginInstalled.value ? t('userProfile.callbackFeedback.noThanks') : -const close = () => { +const close = (options?: { reload?: boolean }) => { if (callbackStatus.value === 'loading') { return; } - return callbackActionsStore.setCallbackStatus('ready'); + callbackActionsStore.setCallbackStatus('ready'); + + if (options?.reload) { + window.location.reload(); + } }; const confirmUpdateOs = () => { @@ -254,7 +258,7 @@ const showPostInstallKeyError = computed(() => :error="callbackStatus === 'error'" :success="callbackStatus === 'success' && updateOsStatus !== 'confirming'" :show-close-x="callbackStatus !== 'loading'" - @close="close" + @close="close({ reload: true })" >