mirror of
https://github.com/unraid/api.git
synced 2026-01-01 06:01:18 -06:00
fix: update @unraid/shared-callbacks to version 3.0.0 (#1831)
…on and pnpm-lock.yaml
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a standalone redirect page that shows "Redirecting..." and
navigates automatically.
* **Improvements**
* Redirect preserves hash callback data, validates targets, and logs the
computed redirect.
* Purchase callback origin changed to a different account host.
* Date/time formatting now tolerates missing or empty server formats
with safe fallbacks.
* Redirect page included in backup/restore.
* **Tests**
* Added tests covering date/time formatting fallbacks.
* **Chores**
* Dependency @unraid/shared-callbacks upgraded.
* Removed multiple demo/debug pages and related test UIs.
<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -206,6 +206,7 @@ FILES_TO_BACKUP=(
|
|||||||
"/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php"
|
"/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php"
|
||||||
"/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php"
|
"/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php"
|
||||||
"/usr/local/emhttp/update.htm"
|
"/usr/local/emhttp/update.htm"
|
||||||
|
"/usr/local/emhttp/redirect.htm"
|
||||||
"/usr/local/emhttp/logging.htm"
|
"/usr/local/emhttp/logging.htm"
|
||||||
"/etc/nginx/nginx.conf"
|
"/etc/nginx/nginx.conf"
|
||||||
"/etc/rc.d/rc.nginx"
|
"/etc/rc.d/rc.nginx"
|
||||||
@@ -349,6 +350,7 @@ exit 0
|
|||||||
"/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php"
|
"/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php"
|
||||||
"/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php"
|
"/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php"
|
||||||
"/usr/local/emhttp/update.htm"
|
"/usr/local/emhttp/update.htm"
|
||||||
|
"/usr/local/emhttp/redirect.htm"
|
||||||
"/usr/local/emhttp/logging.htm"
|
"/usr/local/emhttp/logging.htm"
|
||||||
"/etc/nginx/nginx.conf"
|
"/etc/nginx/nginx.conf"
|
||||||
"/etc/rc.d/rc.nginx"
|
"/etc/rc.d/rc.nginx"
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Redirect Page</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="text" style="text-align: center; margin-top: calc(100vh - 75%); display: none; font-family: sans-serif;">
|
||||||
|
<h1>Redirecting...</h1>
|
||||||
|
<h2><a id="redirectButton" href="/Main">Click here if you are not redirected automatically</a></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function parseRedirectTarget(target) {
|
||||||
|
if (target && target !== '/') {
|
||||||
|
// Parse target and ensure it is a bare path with no query parameters.
|
||||||
|
// This keeps us on the same origin and avoids arbitrary redirects.
|
||||||
|
try {
|
||||||
|
const url = new URL(target, window.location.origin);
|
||||||
|
return url.pathname || '/Main';
|
||||||
|
} catch (_e) {
|
||||||
|
// If the target is malformed, fall back safely.
|
||||||
|
return '/Main';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '/Main';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRedirectUrl() {
|
||||||
|
const search = new URLSearchParams(window.location.search);
|
||||||
|
const rawHash = window.location.hash || '';
|
||||||
|
const hashString = rawHash.charAt(0) === '#' ? rawHash.substring(1) : rawHash;
|
||||||
|
|
||||||
|
let hashData = '';
|
||||||
|
if (hashString.startsWith('data=')) {
|
||||||
|
hashData = hashString.slice('data='.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRoute = parseRedirectTarget(search.get('target'));
|
||||||
|
const baseUrl = `${window.location.origin}${targetRoute}`;
|
||||||
|
|
||||||
|
// If the incoming URL already has a hash-based data payload, preserve it exactly.
|
||||||
|
if (hashData) {
|
||||||
|
return `${baseUrl}#data=${hashData}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: accept legacy ?data= input and convert it to hash-based data.
|
||||||
|
const queryData = search.get('data');
|
||||||
|
if (queryData) {
|
||||||
|
const encoded = encodeURIComponent(queryData);
|
||||||
|
return `${baseUrl}#data=${encoded}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showText() {
|
||||||
|
const textEl = document.getElementById('text');
|
||||||
|
if (textEl) {
|
||||||
|
textEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRedirect() {
|
||||||
|
setTimeout(showText, 750);
|
||||||
|
|
||||||
|
const redirectUrl = getRedirectUrl();
|
||||||
|
console.log('[redirect.htm] redirecting to:', redirectUrl);
|
||||||
|
|
||||||
|
const link = document.getElementById('redirectButton');
|
||||||
|
if (link) {
|
||||||
|
link.setAttribute('href', redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', startRedirect);
|
||||||
|
} else {
|
||||||
|
startRedirect();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -1086,8 +1086,8 @@ importers:
|
|||||||
specifier: 4.0.0-alpha.0
|
specifier: 4.0.0-alpha.0
|
||||||
version: 4.0.0-alpha.0(@babel/parser@7.28.4)(@netlify/blobs@9.1.2)(change-case@5.4.4)(db0@0.3.2)(embla-carousel@8.6.0)(focus-trap@7.6.5)(ioredis@5.7.0)(jwt-decode@4.0.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.20(typescript@5.9.2)))(vue@3.5.20(typescript@5.9.2))(zod@3.25.76)
|
version: 4.0.0-alpha.0(@babel/parser@7.28.4)(@netlify/blobs@9.1.2)(change-case@5.4.4)(db0@0.3.2)(embla-carousel@8.6.0)(focus-trap@7.6.5)(ioredis@5.7.0)(jwt-decode@4.0.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.20(typescript@5.9.2)))(vue@3.5.20(typescript@5.9.2))(zod@3.25.76)
|
||||||
'@unraid/shared-callbacks':
|
'@unraid/shared-callbacks':
|
||||||
specifier: 1.1.1
|
specifier: 3.0.0
|
||||||
version: 1.1.1(@vueuse/core@13.8.0(vue@3.5.20(typescript@5.9.2)))
|
version: 3.0.0
|
||||||
'@unraid/ui':
|
'@unraid/ui':
|
||||||
specifier: link:../unraid-ui
|
specifier: link:../unraid-ui
|
||||||
version: link:../unraid-ui
|
version: link:../unraid-ui
|
||||||
@@ -5096,10 +5096,8 @@ packages:
|
|||||||
cpu: [x64, arm64]
|
cpu: [x64, arm64]
|
||||||
os: [linux, darwin]
|
os: [linux, darwin]
|
||||||
|
|
||||||
'@unraid/shared-callbacks@1.1.1':
|
'@unraid/shared-callbacks@3.0.0':
|
||||||
resolution: {integrity: sha512-14x5HFBOIVfUpQFAAhcRqIvj3AIsOyx90BdShXtddW55kiVtg+dDfsnlzExSYWhb35C6gYKZ0Sm9ZhF/YamGzg==}
|
resolution: {integrity: sha512-O4AN5nsmnwUQ1utYhG2wS9L2NAFn3eOg5YHKq9h9EUa3n8xQeUOzeM6UV2xBg9YJGuF3wQsaEpfj1GyX/MIAGw==}
|
||||||
peerDependencies:
|
|
||||||
'@vueuse/core': ^10.9.0 || ^13.0.0
|
|
||||||
|
|
||||||
'@unraid/tailwind-rem-to-rem@2.0.0':
|
'@unraid/tailwind-rem-to-rem@2.0.0':
|
||||||
resolution: {integrity: sha512-zccpQx5fvEBkAB0JkRwwtyRrT9l26LsjkozLy44LGv0NdZGaxgscniIqJRM+OQj5pSpsWDzExebAtUKdE98Flg==}
|
resolution: {integrity: sha512-zccpQx5fvEBkAB0JkRwwtyRrT9l26LsjkozLy44LGv0NdZGaxgscniIqJRM+OQj5pSpsWDzExebAtUKdE98Flg==}
|
||||||
@@ -17140,9 +17138,8 @@ snapshots:
|
|||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@unraid/shared-callbacks@1.1.1(@vueuse/core@13.8.0(vue@3.5.20(typescript@5.9.2)))':
|
'@unraid/shared-callbacks@3.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vueuse/core': 13.8.0(vue@3.5.20(typescript@5.9.2))
|
|
||||||
crypto-js: 4.2.0
|
crypto-js: 4.2.0
|
||||||
|
|
||||||
'@unraid/tailwind-rem-to-rem@2.0.0(tailwindcss@4.1.12)':
|
'@unraid/tailwind-rem-to-rem@2.0.0(tailwindcss@4.1.12)':
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { BrandButton } from '@unraid/ui';
|
|||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { ServerStateDataAction, ServerStateDataActionType } from '~/types/server';
|
import type { ServerStateDataAction } from '~/types/server';
|
||||||
|
|
||||||
import KeyActions from '~/components/KeyActions.vue';
|
import KeyActions from '~/components/KeyActions.vue';
|
||||||
import { createTestI18n } from '../utils/i18n';
|
import { createTestI18n } from '../utils/i18n';
|
||||||
@@ -34,7 +34,7 @@ describe('KeyActions', () => {
|
|||||||
|
|
||||||
it('renders buttons from props when actions prop is provided', () => {
|
it('renders buttons from props when actions prop is provided', () => {
|
||||||
const actions: ServerStateDataAction[] = [
|
const actions: ServerStateDataAction[] = [
|
||||||
{ name: 'purchase' as ServerStateDataActionType, text: 'Custom Action 1', click: vi.fn() },
|
{ name: 'purchase', text: 'Custom Action 1', click: vi.fn() },
|
||||||
];
|
];
|
||||||
|
|
||||||
const wrapper = mount(KeyActions, {
|
const wrapper = mount(KeyActions, {
|
||||||
@@ -68,9 +68,7 @@ describe('KeyActions', () => {
|
|||||||
|
|
||||||
it('calls action click handler when button is clicked', async () => {
|
it('calls action click handler when button is clicked', async () => {
|
||||||
const click = vi.fn();
|
const click = vi.fn();
|
||||||
const actions: ServerStateDataAction[] = [
|
const actions: ServerStateDataAction[] = [{ name: 'purchase', text: 'Clickable Action', click }];
|
||||||
{ name: 'purchase' as ServerStateDataActionType, text: 'Clickable Action', click },
|
|
||||||
];
|
|
||||||
|
|
||||||
const wrapper = mount(KeyActions, {
|
const wrapper = mount(KeyActions, {
|
||||||
props: {
|
props: {
|
||||||
@@ -89,7 +87,7 @@ describe('KeyActions', () => {
|
|||||||
const click = vi.fn();
|
const click = vi.fn();
|
||||||
const actions: ServerStateDataAction[] = [
|
const actions: ServerStateDataAction[] = [
|
||||||
{
|
{
|
||||||
name: 'purchase' as ServerStateDataActionType,
|
name: 'purchase',
|
||||||
text: 'Disabled Action',
|
text: 'Disabled Action',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
click,
|
click,
|
||||||
@@ -111,9 +109,9 @@ describe('KeyActions', () => {
|
|||||||
|
|
||||||
it('filters actions using filterBy prop', () => {
|
it('filters actions using filterBy prop', () => {
|
||||||
const actions: ServerStateDataAction[] = [
|
const actions: ServerStateDataAction[] = [
|
||||||
{ name: 'purchase' as ServerStateDataActionType, text: 'Action 1', click: vi.fn() },
|
{ name: 'purchase', text: 'Action 1', click: vi.fn() },
|
||||||
{ name: 'redeem' as ServerStateDataActionType, text: 'Action 2', click: vi.fn() },
|
{ name: 'redeem', text: 'Action 2', click: vi.fn() },
|
||||||
{ name: 'upgrade' as ServerStateDataActionType, text: 'Action 3', click: vi.fn() },
|
{ name: 'upgrade', text: 'Action 3', click: vi.fn() },
|
||||||
];
|
];
|
||||||
|
|
||||||
const wrapper = mount(KeyActions, {
|
const wrapper = mount(KeyActions, {
|
||||||
@@ -135,9 +133,9 @@ describe('KeyActions', () => {
|
|||||||
|
|
||||||
it('filters out actions using filterOut prop', () => {
|
it('filters out actions using filterOut prop', () => {
|
||||||
const actions: ServerStateDataAction[] = [
|
const actions: ServerStateDataAction[] = [
|
||||||
{ name: 'purchase' as ServerStateDataActionType, text: 'Action 1', click: vi.fn() },
|
{ name: 'purchase', text: 'Action 1', click: vi.fn() },
|
||||||
{ name: 'redeem' as ServerStateDataActionType, text: 'Action 2', click: vi.fn() },
|
{ name: 'redeem', text: 'Action 2', click: vi.fn() },
|
||||||
{ name: 'upgrade' as ServerStateDataActionType, text: 'Action 3', click: vi.fn() },
|
{ name: 'upgrade', text: 'Action 3', click: vi.fn() },
|
||||||
];
|
];
|
||||||
|
|
||||||
const wrapper = mount(KeyActions, {
|
const wrapper = mount(KeyActions, {
|
||||||
@@ -158,9 +156,7 @@ describe('KeyActions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies maxWidth styling when maxWidth prop is true', () => {
|
it('applies maxWidth styling when maxWidth prop is true', () => {
|
||||||
const actions: ServerStateDataAction[] = [
|
const actions: ServerStateDataAction[] = [{ name: 'purchase', text: 'Action 1', click: vi.fn() }];
|
||||||
{ name: 'purchase' as ServerStateDataActionType, text: 'Action 1', click: vi.fn() },
|
|
||||||
];
|
|
||||||
|
|
||||||
const wrapper = mount(KeyActions, {
|
const wrapper = mount(KeyActions, {
|
||||||
props: {
|
props: {
|
||||||
@@ -180,7 +176,7 @@ describe('KeyActions', () => {
|
|||||||
it('passes all required props to BrandButton component', () => {
|
it('passes all required props to BrandButton component', () => {
|
||||||
const actions: ServerStateDataAction[] = [
|
const actions: ServerStateDataAction[] = [
|
||||||
{
|
{
|
||||||
name: 'purchase' as ServerStateDataActionType,
|
name: 'purchase',
|
||||||
text: 'Test Action',
|
text: 'Test Action',
|
||||||
title: 'Action Title',
|
title: 'Action Title',
|
||||||
href: '/test-link',
|
href: '/test-link',
|
||||||
|
|||||||
58
web/__test__/composables/dateTime.test.ts
Normal file
58
web/__test__/composables/dateTime.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import type { ServerDateTimeFormat } from '~/types/server';
|
||||||
|
|
||||||
|
import useDateTimeHelper from '~/composables/dateTime';
|
||||||
|
import { testTranslate } from '../utils/i18n';
|
||||||
|
|
||||||
|
const formatDateWithComponent = (
|
||||||
|
dateTimeFormat: ServerDateTimeFormat | undefined,
|
||||||
|
hideMinutesSeconds: boolean,
|
||||||
|
providedDateTime: number
|
||||||
|
) => {
|
||||||
|
const wrapper = mount(
|
||||||
|
defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { outputDateTimeFormatted } = useDateTimeHelper(
|
||||||
|
dateTimeFormat,
|
||||||
|
testTranslate,
|
||||||
|
hideMinutesSeconds,
|
||||||
|
providedDateTime
|
||||||
|
);
|
||||||
|
return { outputDateTimeFormatted };
|
||||||
|
},
|
||||||
|
template: '<div />',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = (wrapper.vm as unknown as { outputDateTimeFormatted: string | { value: string } })
|
||||||
|
.outputDateTimeFormatted;
|
||||||
|
return typeof output === 'string' ? output : output.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useDateTimeHelper', () => {
|
||||||
|
it('falls back to default date format when server format is empty', () => {
|
||||||
|
const timestamp = new Date(2025, 0, 2, 3, 4, 5).getTime();
|
||||||
|
const formatted = formatDateWithComponent({ date: '', time: '' }, true, timestamp);
|
||||||
|
|
||||||
|
expect(formatted).toBe(dayjs(timestamp).format('dddd, MMMM D, YYYY'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to default date format when server format is unknown', () => {
|
||||||
|
const timestamp = new Date(2025, 0, 2, 3, 4, 5).getTime();
|
||||||
|
const formatted = formatDateWithComponent({ date: '%Q', time: '%Q' }, true, timestamp);
|
||||||
|
|
||||||
|
expect(formatted).toBe(dayjs(timestamp).format('dddd, MMMM D, YYYY'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to default time format when server time format is unknown', () => {
|
||||||
|
const timestamp = new Date(2025, 0, 2, 3, 4, 5).getTime();
|
||||||
|
const formatted = formatDateWithComponent({ date: '%c', time: '%Q' }, false, timestamp);
|
||||||
|
|
||||||
|
expect(formatted).toBe(dayjs(timestamp).format('ddd, D MMMM YYYY hh:mma'));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -161,37 +161,45 @@ const getStore = () => {
|
|||||||
},
|
},
|
||||||
serverPurchasePayload: {
|
serverPurchasePayload: {
|
||||||
get: () => ({
|
get: () => ({
|
||||||
apiVersion: store.apiVersion,
|
|
||||||
connectPluginVersion: store.connectPluginVersion,
|
|
||||||
deviceCount: store.deviceCount,
|
|
||||||
email: store.email,
|
|
||||||
guid: store.guid,
|
|
||||||
keyTypeForPurchase: store.state === 'PLUS' ? 'Plus' : store.state === 'PRO' ? 'Pro' : 'Trial',
|
|
||||||
locale: store.locale,
|
|
||||||
osVersion: store.osVersion,
|
|
||||||
osVersionBranch: store.osVersionBranch,
|
|
||||||
registered: store.registered ?? false,
|
|
||||||
regExp: store.regExp,
|
|
||||||
regTy: store.regTy,
|
|
||||||
regUpdatesExpired: store.regUpdatesExpired,
|
|
||||||
state: store.state,
|
|
||||||
site: store.site,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
serverAccountPayload: {
|
|
||||||
get: () => ({
|
|
||||||
apiVersion: store.apiVersion,
|
|
||||||
caseModel: store.caseModel,
|
|
||||||
connectPluginVersion: store.connectPluginVersion,
|
|
||||||
deviceCount: store.deviceCount,
|
|
||||||
description: store.description,
|
description: store.description,
|
||||||
|
deviceCount: store.deviceCount,
|
||||||
|
expireTime: store.expireTime,
|
||||||
flashProduct: store.flashProduct,
|
flashProduct: store.flashProduct,
|
||||||
|
flashVendor: store.flashVendor,
|
||||||
guid: store.guid,
|
guid: store.guid,
|
||||||
|
locale: store.locale,
|
||||||
name: store.name,
|
name: store.name,
|
||||||
osVersion: store.osVersion,
|
osVersion: store.osVersion,
|
||||||
osVersionBranch: store.osVersionBranch,
|
osVersionBranch: store.osVersionBranch,
|
||||||
registered: store.registered ?? false,
|
registered: store.registered ?? false,
|
||||||
|
regExp: store.regExp,
|
||||||
|
regGen: store.regGen,
|
||||||
|
regGuid: store.regGuid,
|
||||||
regTy: store.regTy,
|
regTy: store.regTy,
|
||||||
|
regUpdatesExpired: store.regUpdatesExpired,
|
||||||
|
state: store.state,
|
||||||
|
wanFQDN: store.wanFQDN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
serverAccountPayload: {
|
||||||
|
get: () => ({
|
||||||
|
deviceCount: store.deviceCount,
|
||||||
|
description: store.description,
|
||||||
|
expireTime: store.expireTime,
|
||||||
|
flashProduct: store.flashProduct,
|
||||||
|
flashVendor: store.flashVendor,
|
||||||
|
guid: store.guid,
|
||||||
|
keyfile: store.keyfile,
|
||||||
|
locale: store.locale,
|
||||||
|
name: store.name,
|
||||||
|
osVersion: store.osVersion,
|
||||||
|
osVersionBranch: store.osVersionBranch,
|
||||||
|
registered: store.registered ?? false,
|
||||||
|
regExp: store.regExp,
|
||||||
|
regGen: store.regGen,
|
||||||
|
regGuid: store.regGuid,
|
||||||
|
regTy: store.regTy,
|
||||||
|
regUpdatesExpired: store.regUpdatesExpired,
|
||||||
state: store.state,
|
state: store.state,
|
||||||
wanFQDN: store.wanFQDN,
|
wanFQDN: store.wanFQDN,
|
||||||
}),
|
}),
|
||||||
@@ -549,49 +557,65 @@ describe('useServerStore', () => {
|
|||||||
const store = getStore();
|
const store = getStore();
|
||||||
|
|
||||||
store.setServer({
|
store.setServer({
|
||||||
apiVersion: '1.0.0',
|
|
||||||
connectPluginVersion: '2.0.0',
|
|
||||||
deviceCount: 6,
|
deviceCount: 6,
|
||||||
email: 'test@example.com',
|
description: 'Test Server',
|
||||||
|
expireTime: 123,
|
||||||
|
flashProduct: 'TestFlash',
|
||||||
|
flashVendor: 'TestVendor',
|
||||||
guid: '123456',
|
guid: '123456',
|
||||||
inIframe: false,
|
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
|
name: 'TestServer',
|
||||||
osVersion: '6.10.3',
|
osVersion: '6.10.3',
|
||||||
osVersionBranch: 'stable',
|
osVersionBranch: 'stable',
|
||||||
registered: true,
|
registered: true,
|
||||||
|
regGen: 7,
|
||||||
|
regGuid: 'reg-guid-1',
|
||||||
regExp: 1234567890,
|
regExp: 1234567890,
|
||||||
regTy: 'Plus',
|
regTy: 'Plus',
|
||||||
state: 'PLUS' as ServerState,
|
state: 'PLUS' as ServerState,
|
||||||
site: 'local',
|
wanFQDN: 'test.myunraid.net',
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = store.serverPurchasePayload;
|
const payload = store.serverPurchasePayload;
|
||||||
|
|
||||||
expect(payload.apiVersion).toBe('1.0.0');
|
expect(payload.description).toBe('Test Server');
|
||||||
expect(payload.connectPluginVersion).toBe('2.0.0');
|
|
||||||
expect(payload.deviceCount).toBe(6);
|
expect(payload.deviceCount).toBe(6);
|
||||||
expect(payload.email).toBe('test@example.com');
|
expect(payload.expireTime).toBe(123);
|
||||||
|
expect(payload.flashProduct).toBe('TestFlash');
|
||||||
|
expect(payload.flashVendor).toBe('TestVendor');
|
||||||
expect(payload.guid).toBe('123456');
|
expect(payload.guid).toBe('123456');
|
||||||
expect(payload.keyTypeForPurchase).toBe('Plus');
|
|
||||||
expect(payload.locale).toBe('en-US');
|
expect(payload.locale).toBe('en-US');
|
||||||
|
expect(payload.name).toBe('TestServer');
|
||||||
expect(payload.osVersion).toBe('6.10.3');
|
expect(payload.osVersion).toBe('6.10.3');
|
||||||
|
expect(payload.osVersionBranch).toBe('stable');
|
||||||
expect(payload.registered).toBe(true);
|
expect(payload.registered).toBe(true);
|
||||||
|
expect(payload.regExp).toBe(1234567890);
|
||||||
|
expect(payload.regGen).toBe(7);
|
||||||
|
expect(payload.regGuid).toBe('reg-guid-1');
|
||||||
|
expect(payload.regTy).toBe('Plus');
|
||||||
|
expect(payload.state).toBe('PLUS');
|
||||||
|
expect(payload.wanFQDN).toBe('test.myunraid.net');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create serverAccountPayload correctly', () => {
|
it('should create serverAccountPayload correctly', () => {
|
||||||
const store = getStore();
|
const store = getStore();
|
||||||
|
|
||||||
store.setServer({
|
store.setServer({
|
||||||
apiVersion: '1.0.0',
|
|
||||||
caseModel: 'TestCase',
|
|
||||||
connectPluginVersion: '2.0.0',
|
|
||||||
deviceCount: 6,
|
deviceCount: 6,
|
||||||
description: 'Test Server',
|
description: 'Test Server',
|
||||||
|
expireTime: 123,
|
||||||
flashProduct: 'TestFlash',
|
flashProduct: 'TestFlash',
|
||||||
|
flashVendor: 'TestVendor',
|
||||||
guid: '123456',
|
guid: '123456',
|
||||||
|
keyfile: '/boot/config/Plus.key',
|
||||||
|
locale: 'en-US',
|
||||||
name: 'TestServer',
|
name: 'TestServer',
|
||||||
osVersion: '6.10.3',
|
osVersion: '6.10.3',
|
||||||
|
osVersionBranch: 'stable',
|
||||||
registered: true,
|
registered: true,
|
||||||
|
regExp: 1234567890,
|
||||||
|
regGen: 7,
|
||||||
|
regGuid: 'reg-guid-1',
|
||||||
regTy: 'Plus',
|
regTy: 'Plus',
|
||||||
state: 'PLUS' as ServerState,
|
state: 'PLUS' as ServerState,
|
||||||
wanFQDN: 'test.myunraid.net',
|
wanFQDN: 'test.myunraid.net',
|
||||||
@@ -599,16 +623,23 @@ describe('useServerStore', () => {
|
|||||||
|
|
||||||
const payload = store.serverAccountPayload;
|
const payload = store.serverAccountPayload;
|
||||||
|
|
||||||
expect(payload.apiVersion).toBe('1.0.0');
|
expect(payload.deviceCount).toBe(6);
|
||||||
expect(payload.caseModel).toBe('TestCase');
|
|
||||||
expect(payload.connectPluginVersion).toBe('2.0.0');
|
|
||||||
expect(payload.description).toBe('Test Server');
|
expect(payload.description).toBe('Test Server');
|
||||||
|
expect(payload.expireTime).toBe(123);
|
||||||
expect(payload.flashProduct).toBe('TestFlash');
|
expect(payload.flashProduct).toBe('TestFlash');
|
||||||
|
expect(payload.flashVendor).toBe('TestVendor');
|
||||||
expect(payload.guid).toBe('123456');
|
expect(payload.guid).toBe('123456');
|
||||||
|
expect(payload.keyfile).toBe('/boot/config/Plus.key');
|
||||||
|
expect(payload.locale).toBe('en-US');
|
||||||
expect(payload.name).toBe('TestServer');
|
expect(payload.name).toBe('TestServer');
|
||||||
expect(payload.osVersion).toBe('6.10.3');
|
expect(payload.osVersion).toBe('6.10.3');
|
||||||
|
expect(payload.osVersionBranch).toBe('stable');
|
||||||
expect(payload.registered).toBe(true);
|
expect(payload.registered).toBe(true);
|
||||||
|
expect(payload.regExp).toBe(1234567890);
|
||||||
|
expect(payload.regGen).toBe(7);
|
||||||
|
expect(payload.regGuid).toBe('reg-guid-1');
|
||||||
expect(payload.regTy).toBe('Plus');
|
expect(payload.regTy).toBe('Plus');
|
||||||
|
expect(payload.regUpdatesExpired).toBe(true);
|
||||||
expect(payload.state).toBe('PLUS');
|
expect(payload.state).toBe('PLUS');
|
||||||
expect(payload.wanFQDN).toBe('test.myunraid.net');
|
expect(payload.wanFQDN).toBe('test.myunraid.net');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"@jsonforms/vue-vanilla": "3.6.0",
|
"@jsonforms/vue-vanilla": "3.6.0",
|
||||||
"@jsonforms/vue-vuetify": "3.6.0",
|
"@jsonforms/vue-vuetify": "3.6.0",
|
||||||
"@nuxt/ui": "4.0.0-alpha.0",
|
"@nuxt/ui": "4.0.0-alpha.0",
|
||||||
"@unraid/shared-callbacks": "1.1.1",
|
"@unraid/shared-callbacks": "3.0.0",
|
||||||
"@unraid/ui": "link:../unraid-ui",
|
"@unraid/ui": "link:../unraid-ui",
|
||||||
"@vue/apollo-composable": "4.2.2",
|
"@vue/apollo-composable": "4.2.2",
|
||||||
"@vueuse/components": "13.8.0",
|
"@vueuse/components": "13.8.0",
|
||||||
|
|||||||
@@ -101,15 +101,23 @@ const useDateTimeHelper = (
|
|||||||
): DateFormatOption | TimeFormatOption | undefined =>
|
): DateFormatOption | TimeFormatOption | undefined =>
|
||||||
formats.find((formatOption) => formatOption.format === selectedFormat);
|
formats.find((formatOption) => formatOption.format === selectedFormat);
|
||||||
|
|
||||||
const dateFormat = findMatchingFormat(format?.date ?? dateFormatOptions[0].format, dateFormatOptions);
|
const defaultTimeFormat = timeFormatOptions[0];
|
||||||
|
const fallbackDateDisplayFormat = 'dddd, MMMM D, YYYY';
|
||||||
|
|
||||||
let displayFormat = `${dateFormat?.display}`;
|
const dateFormatFromServer = (format?.date ?? '').trim();
|
||||||
|
const dateFormat = dateFormatFromServer
|
||||||
|
? (findMatchingFormat(dateFormatFromServer, dateFormatOptions) as DateFormatOption | undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let displayFormat = dateFormat?.display ?? fallbackDateDisplayFormat;
|
||||||
if (!hideMinutesSeconds) {
|
if (!hideMinutesSeconds) {
|
||||||
const timeFormat = findMatchingFormat(
|
const timeFormatFromServer = (format?.time ?? '').trim();
|
||||||
format?.time ?? timeFormatOptions[0].format,
|
const timeFormat = timeFormatFromServer
|
||||||
timeFormatOptions
|
? (findMatchingFormat(timeFormatFromServer, timeFormatOptions) as TimeFormatOption | undefined)
|
||||||
);
|
: undefined;
|
||||||
displayFormat = `${displayFormat} ${timeFormat?.display}`;
|
|
||||||
|
const timeDisplay = timeFormat?.display ?? defaultTimeFormat.display;
|
||||||
|
displayFormat = `${displayFormat} ${timeDisplay}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date: number): string => dayjs(date).format(displayFormat);
|
const formatDate = (date: number): string => dayjs(date).format(displayFormat);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const CONNECT_DASHBOARD = new URL(import.meta.env.VITE_CONNECT ?? 'https://conne
|
|||||||
const CONNECT_FORUMS = new URL('/forum/94-connect-plugin-support/', FORUMS);
|
const CONNECT_FORUMS = new URL('/forum/94-connect-plugin-support/', FORUMS);
|
||||||
const CONTACT = new URL('/contact', UNRAID_NET);
|
const CONTACT = new URL('/contact', UNRAID_NET);
|
||||||
const DISCORD = new URL('https://discord.unraid.net');
|
const DISCORD = new URL('https://discord.unraid.net');
|
||||||
const PURCHASE_CALLBACK = new URL('/c', UNRAID_NET);
|
const PURCHASE_CALLBACK = new URL('c', ACCOUNT);
|
||||||
const UNRAID_NET_SUPPORT = new URL('/support', UNRAID_NET);
|
const UNRAID_NET_SUPPORT = new URL('/support', UNRAID_NET);
|
||||||
|
|
||||||
const WEBGUI_GRAPHQL = '/graphql';
|
const WEBGUI_GRAPHQL = '/graphql';
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import ApiKeyManager from '~/components/ApiKey/ApiKeyManager.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ApiKeyManager />
|
|
||||||
</template>
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onBeforeMount } from 'vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
|
|
||||||
import UpdateOsChangelogModal from '~/components/UpdateOs/ChangelogModal.vue';
|
|
||||||
import { useUpdateOsStore } from '~/store/updateOs';
|
|
||||||
|
|
||||||
const updateOsStore = useUpdateOsStore();
|
|
||||||
const { changelogModalVisible } = storeToRefs(updateOsStore);
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
// Register custom elements if needed for ColorSwitcherCe
|
|
||||||
});
|
|
||||||
|
|
||||||
async function showChangelogModalFromReleasesEndpoint() {
|
|
||||||
const response = await fetch('https://releases.unraid.net/os?branch=stable¤t_version=6.12.3');
|
|
||||||
const data = await response.json();
|
|
||||||
updateOsStore.setReleaseForUpdate(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showChangelogModalWithTestData() {
|
|
||||||
updateOsStore.setReleaseForUpdate({
|
|
||||||
version: '6.12.3',
|
|
||||||
date: '2023-07-15',
|
|
||||||
changelog:
|
|
||||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/6.12.3.md',
|
|
||||||
changelogPretty: 'https://docs.unraid.net/go/release-notes/6.12.3',
|
|
||||||
name: '6.12.3',
|
|
||||||
isEligible: true,
|
|
||||||
isNewer: true,
|
|
||||||
sha256: '1234567890',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showChangelogWithoutPretty() {
|
|
||||||
updateOsStore.setReleaseForUpdate({
|
|
||||||
version: '6.12.3',
|
|
||||||
date: '2023-07-15',
|
|
||||||
changelog:
|
|
||||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/6.12.3.md',
|
|
||||||
changelogPretty: '',
|
|
||||||
name: '6.12.3',
|
|
||||||
isEligible: true,
|
|
||||||
isNewer: true,
|
|
||||||
sha256: '1234567890',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showChangelogBrokenParse() {
|
|
||||||
updateOsStore.setReleaseForUpdate({
|
|
||||||
version: '6.12.3',
|
|
||||||
date: '2023-07-15',
|
|
||||||
changelog: null,
|
|
||||||
changelogPretty: undefined, // intentionally broken
|
|
||||||
name: '6.12.3',
|
|
||||||
isEligible: true,
|
|
||||||
isNewer: true,
|
|
||||||
sha256: '1234567890',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showChangelogFromLocalhost() {
|
|
||||||
updateOsStore.setReleaseForUpdate({
|
|
||||||
version: '6.12.3',
|
|
||||||
date: '2023-07-15',
|
|
||||||
changelog:
|
|
||||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/6.12.3.md',
|
|
||||||
changelogPretty: 'http://localhost:3000/unraid-os/release-notes/6.12.3',
|
|
||||||
name: '6.12.3',
|
|
||||||
isEligible: true,
|
|
||||||
isNewer: true,
|
|
||||||
sha256: '1234567890',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="container mx-auto p-6">
|
|
||||||
<h1 class="mb-6 text-2xl font-bold">Changelog</h1>
|
|
||||||
<UpdateOsChangelogModal :open="changelogModalVisible" />
|
|
||||||
<div class="mb-6 flex flex-col gap-4">
|
|
||||||
<div class="flex max-w-md flex-col gap-4">
|
|
||||||
<button
|
|
||||||
class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
|
||||||
@click="showChangelogModalFromReleasesEndpoint"
|
|
||||||
>
|
|
||||||
Test Changelog Modal (from releases endpoint)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded bg-purple-500 px-4 py-2 text-white hover:bg-purple-600"
|
|
||||||
@click="showChangelogFromLocalhost"
|
|
||||||
>
|
|
||||||
Test Local Pretty Changelog (:3000)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600"
|
|
||||||
@click="showChangelogModalWithTestData"
|
|
||||||
>
|
|
||||||
Test Changelog Modal (with test data)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
|
|
||||||
@click="showChangelogWithoutPretty"
|
|
||||||
>
|
|
||||||
Test Without Pretty Changelog
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded bg-yellow-500 px-4 py-2 text-white hover:bg-yellow-600"
|
|
||||||
@click="showChangelogBrokenParse"
|
|
||||||
>
|
|
||||||
Test Broken Parse Changelog
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
|
|
||||||
import RCloneConfig from '~/components/RClone/RCloneConfig.vue';
|
|
||||||
import RCloneOverview from '~/components/RClone/RCloneOverview.vue';
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.cookie = 'unraid_session_cookie=mockusersession';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<RCloneOverview />
|
|
||||||
<RCloneConfig />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, ref, watch } from 'vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import UButton from '@nuxt/ui/components/Button.vue';
|
|
||||||
|
|
||||||
import { ExclamationTriangleIcon } from '@heroicons/vue/24/solid';
|
|
||||||
import { BrandButton, Toaster } from '@unraid/ui';
|
|
||||||
import { useDummyServerStore } from '~/_data/serverState';
|
|
||||||
import AES from 'crypto-js/aes';
|
|
||||||
|
|
||||||
import type { SendPayloads } from '@unraid/shared-callbacks';
|
|
||||||
|
|
||||||
import WelcomeModalCe from '~/components/Activation/WelcomeModal.standalone.vue';
|
|
||||||
import ConnectSettingsCe from '~/components/ConnectSettings/ConnectSettings.standalone.vue';
|
|
||||||
import DowngradeOsCe from '~/components/DowngradeOs.standalone.vue';
|
|
||||||
import HeaderOsVersionCe from '~/components/HeaderOsVersion.standalone.vue';
|
|
||||||
import LogViewerCe from '~/components/Logs/LogViewer.standalone.vue';
|
|
||||||
import ModalsCe from '~/components/Modals.standalone.vue';
|
|
||||||
import RegistrationCe from '~/components/Registration.standalone.vue';
|
|
||||||
import SsoButtonCe from '~/components/SsoButton.standalone.vue';
|
|
||||||
import UpdateOsCe from '~/components/UpdateOs.standalone.vue';
|
|
||||||
import UserProfileCe from '~/components/UserProfile.standalone.vue';
|
|
||||||
import { useThemeStore } from '~/store/theme';
|
|
||||||
|
|
||||||
const serverStore = useDummyServerStore();
|
|
||||||
const { serverState } = storeToRefs(serverStore);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.cookie = 'unraid_session_cookie=mockusersession';
|
|
||||||
});
|
|
||||||
|
|
||||||
const valueToMakeCallback = ref<SendPayloads | undefined>();
|
|
||||||
const callbackDestination = ref<string | undefined>('');
|
|
||||||
|
|
||||||
const createCallbackUrl = (payload: SendPayloads, sendType: string) => {
|
|
||||||
// params differs from callbackActions.send
|
|
||||||
console.debug('[callback.send]');
|
|
||||||
|
|
||||||
valueToMakeCallback.value = payload; // differs from callbackActions.send
|
|
||||||
|
|
||||||
const stringifiedData = JSON.stringify({
|
|
||||||
actions: [...payload],
|
|
||||||
sender: window.location.href,
|
|
||||||
type: sendType,
|
|
||||||
});
|
|
||||||
const encryptedMessage = AES.encrypt(stringifiedData, import.meta.env.VITE_CALLBACK_KEY).toString();
|
|
||||||
// build and go to url
|
|
||||||
const destinationUrl = new URL(window.location.href); // differs from callbackActions.send
|
|
||||||
destinationUrl.searchParams.set('data', encodeURI(encryptedMessage));
|
|
||||||
|
|
||||||
callbackDestination.value = destinationUrl.toString(); // differs from callbackActions.send
|
|
||||||
};
|
|
||||||
|
|
||||||
const variants = [
|
|
||||||
'fill',
|
|
||||||
'black',
|
|
||||||
'gray',
|
|
||||||
'outline',
|
|
||||||
'outline-black',
|
|
||||||
'outline-white',
|
|
||||||
'underline',
|
|
||||||
'underline-hover-red',
|
|
||||||
'white',
|
|
||||||
'none',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
createCallbackUrl(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
// keyUrl: 'https://keys.lime-technology.com/unraid/d26a033e3097c65ab0b4f742a7c02ce808c6e963/Starter.key', // assigned to guid 1111-1111-5GDB-123412341234, use to test EGUID after key install
|
|
||||||
keyUrl:
|
|
||||||
'https://keys.lime-technology.com/unraid/7f7c2ddff1c38f21ed174f5c5d9f97b7b4577344/Starter.key',
|
|
||||||
type: 'renew',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sha256: 'a7d1a42fc661f55ee45d36bbc49aac71aef045cc1d287b1e7f16be0ba485c9b6',
|
|
||||||
type: 'updateOs',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'forUpc'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const bannerImage = ref<string>('none');
|
|
||||||
|
|
||||||
const { theme } = storeToRefs(useThemeStore());
|
|
||||||
watch(
|
|
||||||
theme,
|
|
||||||
(newTheme) => {
|
|
||||||
if (newTheme.banner) {
|
|
||||||
bannerImage.value = `url(https://picsum.photos/1920/200?${Math.round(Math.random() * 100)})`;
|
|
||||||
} else {
|
|
||||||
bannerImage.value = 'none';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bg-white text-black dark:bg-black dark:text-white">
|
|
||||||
<div class="mx-auto pb-12">
|
|
||||||
<div class="flex flex-col gap-6 p-6">
|
|
||||||
<h2 class="font-mono text-xl font-semibold">Vue Components</h2>
|
|
||||||
<h3 class="font-mono text-lg font-semibold">UserProfileCe</h3>
|
|
||||||
<header
|
|
||||||
class="bg-header-background-color flex items-center justify-between"
|
|
||||||
:style="{
|
|
||||||
backgroundImage: bannerImage,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="inline-flex flex-col items-start gap-4">
|
|
||||||
<HeaderOsVersionCe />
|
|
||||||
</div>
|
|
||||||
<UserProfileCe :server="serverState" />
|
|
||||||
</header>
|
|
||||||
<!-- <hr class="border-black dark:border-white"> -->
|
|
||||||
|
|
||||||
<h3 class="font-mono text-lg font-semibold">ConnectSettingsCe</h3>
|
|
||||||
<ConnectSettingsCe />
|
|
||||||
<hr class="border-muted" />
|
|
||||||
|
|
||||||
<!-- <h3 class="text-lg font-semibold font-mono">
|
|
||||||
AuthCe
|
|
||||||
</h3>
|
|
||||||
<AuthCe />
|
|
||||||
<hr class="border-black dark:border-white"> -->
|
|
||||||
<!-- <h3 class="text-lg font-semibold font-mono">
|
|
||||||
WanIpCheckCe
|
|
||||||
</h3>
|
|
||||||
<WanIpCheckCe php-wan-ip="47.184.85.45" />
|
|
||||||
<hr class="border-black dark:border-white"> -->
|
|
||||||
<h3 class="font-mono text-lg font-semibold">UpdateOsCe</h3>
|
|
||||||
<UpdateOsCe />
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h3 class="font-mono text-lg font-semibold">DowngraadeOsCe</h3>
|
|
||||||
<DowngradeOsCe :restore-release-date="'2022-10-10'" :restore-version="'6.11.2'" />
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h3 class="font-mono text-lg font-semibold">RegistrationCe</h3>
|
|
||||||
<RegistrationCe />
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h3 class="font-mono text-lg font-semibold">ModalsCe</h3>
|
|
||||||
<ModalsCe />
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h3 class="font-mono text-lg font-semibold">WelcomeModalCe</h3>
|
|
||||||
<WelcomeModalCe />
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h3 class="font-mono text-lg font-semibold">Test Callback Builder</h3>
|
|
||||||
<div class="flex flex-col justify-end gap-2">
|
|
||||||
<p>
|
|
||||||
Modify the <code>createCallbackUrl</code> param in <code>onMounted</code> to test a callback.
|
|
||||||
</p>
|
|
||||||
<code>
|
|
||||||
<pre>{{ valueToMakeCallback }}</pre>
|
|
||||||
</code>
|
|
||||||
<BrandButton v-if="callbackDestination" :href="callbackDestination" :external="true">
|
|
||||||
{{ 'Go to Callback URL' }}
|
|
||||||
</BrandButton>
|
|
||||||
<h4>Full URL Destination</h4>
|
|
||||||
<code>
|
|
||||||
<pre>{{ callbackDestination }}</pre>
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h2 class="font-mono text-xl font-semibold">Nuxt UI Button - Primary Color Test</h2>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<UButton color="primary" variant="solid">Primary Solid</UButton>
|
|
||||||
<UButton color="primary" variant="outline">Primary Outline</UButton>
|
|
||||||
<UButton color="primary" variant="soft">Primary Soft</UButton>
|
|
||||||
<UButton color="primary" variant="ghost">Primary Ghost</UButton>
|
|
||||||
<UButton color="primary" variant="link">Primary Link</UButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-background">
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h2 class="font-mono text-xl font-semibold">Brand Button Component</h2>
|
|
||||||
<template v-for="variant in variants" :key="variant">
|
|
||||||
<BrandButton :variant="variant" type="button" size="14px" :icon="ExclamationTriangleIcon">{{
|
|
||||||
variant
|
|
||||||
}}</BrandButton>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="bg-background">
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h2 class="font-mono text-xl font-semibold">SSO Button Component</h2>
|
|
||||||
<SsoButtonCe />
|
|
||||||
</div>
|
|
||||||
<div class="bg-background">
|
|
||||||
<hr class="border-muted" />
|
|
||||||
<h2 class="font-mono text-xl font-semibold">Log Viewer Component</h2>
|
|
||||||
<LogViewerCe />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Toaster rich-colors close-button />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { gql } from '@apollo/client/core';
|
|
||||||
|
|
||||||
export const SERVER_INFO_QUERY = gql`
|
|
||||||
query serverInfo {
|
|
||||||
info {
|
|
||||||
os {
|
|
||||||
hostname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vars {
|
|
||||||
comment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,494 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useQuery } from '@vue/apollo-composable';
|
|
||||||
|
|
||||||
import { Button, Dialog, Input } from '@unraid/ui';
|
|
||||||
import { SERVER_INFO_QUERY } from '~/pages/login.query';
|
|
||||||
|
|
||||||
import SsoButtonCe from '~/components/SsoButton.standalone.vue';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { result } = useQuery(SERVER_INFO_QUERY);
|
|
||||||
|
|
||||||
const serverName = computed(() => result.value?.info?.os?.hostname || 'UNRAID');
|
|
||||||
const serverComment = computed(() => result.value?.vars?.comment || '');
|
|
||||||
|
|
||||||
const showDebugModal = ref(false);
|
|
||||||
const debugData = ref<{ username: string; password: string; timestamp: string } | null>(null);
|
|
||||||
const cliToken = ref('');
|
|
||||||
const cliOutput = ref('');
|
|
||||||
const isExecutingCli = ref(false);
|
|
||||||
|
|
||||||
// Check for token in URL hash on mount (keeps token out of server logs)
|
|
||||||
const route = useRoute();
|
|
||||||
onMounted(() => {
|
|
||||||
// Check hash first (preferred), then query params (fallback)
|
|
||||||
const hashParams = new URLSearchParams(window.location.hash.slice(1));
|
|
||||||
const tokenFromHash = hashParams.get('token');
|
|
||||||
const tokenFromQuery = route.query.token as string;
|
|
||||||
|
|
||||||
const token = tokenFromHash || tokenFromQuery;
|
|
||||||
if (token) {
|
|
||||||
cliToken.value = token;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFormSubmit = (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const form = event.target as HTMLFormElement;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
|
|
||||||
const password = formData.get('password') as string;
|
|
||||||
|
|
||||||
debugData.value = {
|
|
||||||
username: formData.get('username') as string,
|
|
||||||
password: password,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear the token field - it expects a JWT token, not a password
|
|
||||||
cliToken.value = '';
|
|
||||||
|
|
||||||
showDebugModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const executeCliCommand = async () => {
|
|
||||||
if (!cliToken.value.trim()) {
|
|
||||||
cliOutput.value = JSON.stringify({ error: 'Please enter a token', valid: false }, null, 2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isExecutingCli.value = true;
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/debug/validate-token', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ token: cliToken.value }),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Format the output nicely
|
|
||||||
if (data.success && 'stdout' in data && typeof data.stdout === 'object') {
|
|
||||||
cliOutput.value = JSON.stringify(data.stdout, null, 2);
|
|
||||||
} else if ('stdout' in data && data.stdout) {
|
|
||||||
cliOutput.value =
|
|
||||||
typeof data.stdout === 'string' ? data.stdout : JSON.stringify(data.stdout, null, 2);
|
|
||||||
} else {
|
|
||||||
cliOutput.value = JSON.stringify(data, null, 2);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
cliOutput.value = JSON.stringify(
|
|
||||||
{
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
valid: false,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isExecutingCli.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
|
||||||
<section id="login" class="shadow">
|
|
||||||
<div class="logo angle">
|
|
||||||
<div class="wordmark">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222.4 39" class="Nav__logo--white">
|
|
||||||
<path
|
|
||||||
fill="#ffffff"
|
|
||||||
d="M146.70000000000002 29.5H135l-3 9h-6.5L138.9 0h8l13.4 38.5h-7.1L142.6 6.9l-5.8 16.9h8.2l1.7 5.7zM29.7 0v25.4c0 8.9-5.8 13.6-14.9 13.6C5.8 39 0 34.3 0 25.4V0h6.5v25.4c0 5.2 3.2 7.9 8.2 7.9 5.2 0 8.4-2.7 8.4-7.9V0h6.6zM50.9 12v26.5h-6.5V0h6.1l17 26.5V0H74v38.5h-6.1L50.9 12zM171.3 0h6.5v38.5h-6.5V0zM222.4 24.7c0 9-5.9 13.8-15.2 13.8h-14.5V0h14.6c9.2 0 15.1 4.8 15.1 13.8v10.9zm-6.6-10.9c0-5.3-3.3-8.1-8.5-8.1h-8.1v27.1h8c5.3 0 8.6-2.8 8.6-8.1V13.8zM108.3 23.9c4.3-1.6 6.9-5.3 6.9-11.5 0-8.7-5.1-12.4-12.8-12.4H88.8v38.5h6.5V5.7h6.9c3.8 0 6.2 1.8 6.2 6.7s-2.4 6.8-6.2 6.8h-3.4l9.2 19.4h7.5l-7.2-14.7z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<h1>{{ serverName }}</h1>
|
|
||||||
<h2>{{ serverComment }}</h2>
|
|
||||||
|
|
||||||
<div class="case">
|
|
||||||
<!-- Case icon will be implemented in a future update -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form">
|
|
||||||
<form action="/login" method="POST" @submit="handleFormSubmit">
|
|
||||||
<p>
|
|
||||||
<input
|
|
||||||
name="username"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('auth.login.username')"
|
|
||||||
autocapitalize="none"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
autofocus
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input name="password" type="password" :placeholder="t('auth.login.password')" required />
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<button type="submit" class="button button--small">{{ t('auth.login.login') }}</button>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<!-- SSO Button will show when GraphQL query isSSOEnabled returns true -->
|
|
||||||
<SsoButtonCe />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://docs.unraid.net/go/lost-root-password/"
|
|
||||||
target="_blank"
|
|
||||||
class="password-recovery"
|
|
||||||
>{{ t('auth.login.passwordRecovery') }}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Debug Dialog -->
|
|
||||||
<Dialog
|
|
||||||
v-model="showDebugModal"
|
|
||||||
title="SSO Debug Tool"
|
|
||||||
description="Debug SSO configurations and validate tokens"
|
|
||||||
:show-footer="false"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<div class="space-y-6 p-4">
|
|
||||||
<div>
|
|
||||||
<h3 class="mb-2 text-sm font-semibold">Form Data Submitted:</h3>
|
|
||||||
<pre
|
|
||||||
class="bg-muted max-h-32 overflow-x-auto overflow-y-auto rounded p-3 text-xs break-all whitespace-pre-wrap"
|
|
||||||
>{{ JSON.stringify(debugData, null, 2) }}</pre
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-muted border-t pt-4">
|
|
||||||
<h3 class="mb-2 text-sm font-semibold">JWT/OIDC Token Validation Tool:</h3>
|
|
||||||
<p class="text-muted-foreground mb-3 text-xs">
|
|
||||||
Enter a JWT or OIDC session token to validate it using the CLI command
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<Input
|
|
||||||
v-model="cliToken"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter JWT or OIDC session token"
|
|
||||||
class="w-full overflow-hidden break-all"
|
|
||||||
style="word-break: break-all; overflow-wrap: break-word"
|
|
||||||
/>
|
|
||||||
<Button :disabled="isExecutingCli" class="w-full sm:w-auto" @click="executeCliCommand">
|
|
||||||
{{ isExecutingCli ? 'Validating...' : 'Validate Token' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="cliOutput" class="mt-4">
|
|
||||||
<h4 class="mb-2 text-sm font-semibold">CLI Output:</h4>
|
|
||||||
<pre
|
|
||||||
class="bg-muted max-h-48 overflow-x-auto overflow-y-auto rounded p-3 text-xs break-all whitespace-pre-wrap"
|
|
||||||
>{{ cliOutput }}</pre
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style scoped>
|
|
||||||
/************************
|
|
||||||
/
|
|
||||||
/ Fonts
|
|
||||||
/
|
|
||||||
/************************/
|
|
||||||
/************************
|
|
||||||
/
|
|
||||||
/ General styling
|
|
||||||
/
|
|
||||||
/************************/
|
|
||||||
body {
|
|
||||||
background: #f2f2f2;
|
|
||||||
color: #1c1b1b;
|
|
||||||
font-family: clear-sans, sans-serif;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark body {
|
|
||||||
background: #111827;
|
|
||||||
color: #f3f4f6;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bold;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
color: #ff8c2f;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #f15a2c;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 1.8em;
|
|
||||||
margin: 0;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark h1 {
|
|
||||||
color: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 1.8em;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark h2 {
|
|
||||||
color: #d1d5db;
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
color: #ff8c2f;
|
|
||||||
font-family: clear-sans, sans-serif;
|
|
||||||
background:
|
|
||||||
-webkit-gradient(linear, left top, right top, from(#e03237), to(#fd8c3c)) 0 0 no-repeat,
|
|
||||||
-webkit-gradient(linear, left top, right top, from(#e03237), to(#fd8c3c)) 0 100% no-repeat,
|
|
||||||
-webkit-gradient(linear, left bottom, left top, from(#e03237), to(#e03237)) 0 100% no-repeat,
|
|
||||||
-webkit-gradient(linear, left bottom, left top, from(#fd8c3c), to(#fd8c3c)) 100% 100% no-repeat;
|
|
||||||
background:
|
|
||||||
linear-gradient(90deg, #e03237 0, #fd8c3c) 0 0 no-repeat,
|
|
||||||
linear-gradient(90deg, #e03237 0, #fd8c3c) 0 100% no-repeat,
|
|
||||||
linear-gradient(0deg, #e03237 0, #e03237) 0 100% no-repeat,
|
|
||||||
linear-gradient(0deg, #fd8c3c 0, #fd8c3c) 100% 100% no-repeat;
|
|
||||||
background-size:
|
|
||||||
100% 2px,
|
|
||||||
100% 2px,
|
|
||||||
2px 100%,
|
|
||||||
2px 100%;
|
|
||||||
}
|
|
||||||
.button:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #f15a2c;
|
|
||||||
background: -webkit-gradient(linear, left top, right top, from(#e22828), to(#ff8c2f));
|
|
||||||
background: linear-gradient(90deg, #e22828 0, #ff8c2f);
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.button--small {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: 0.125rem;
|
|
||||||
border: 0;
|
|
||||||
-webkit-transition: none;
|
|
||||||
transition: none;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
}
|
|
||||||
[type='email'],
|
|
||||||
[type='number'],
|
|
||||||
[type='password'],
|
|
||||||
[type='search'],
|
|
||||||
[type='tel'],
|
|
||||||
[type='text'],
|
|
||||||
[type='url'],
|
|
||||||
textarea {
|
|
||||||
font-family: clear-sans, sans-serif;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
color: #111827;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border: 2px solid #d1d5db;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 0;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark [type='email'],
|
|
||||||
:root.dark [type='number'],
|
|
||||||
:root.dark [type='password'],
|
|
||||||
:root.dark [type='search'],
|
|
||||||
:root.dark [type='tel'],
|
|
||||||
:root.dark [type='text'],
|
|
||||||
:root.dark [type='url'],
|
|
||||||
:root.dark textarea {
|
|
||||||
background-color: #1f2937;
|
|
||||||
color: #f3f4f6;
|
|
||||||
border-color: #4b5563;
|
|
||||||
}
|
|
||||||
[type='email']:active,
|
|
||||||
[type='email']:focus,
|
|
||||||
[type='number']:active,
|
|
||||||
[type='number']:focus,
|
|
||||||
[type='password']:active,
|
|
||||||
[type='password']:focus,
|
|
||||||
[type='search']:active,
|
|
||||||
[type='search']:focus,
|
|
||||||
[type='tel']:active,
|
|
||||||
[type='tel']:focus,
|
|
||||||
[type='text']:active,
|
|
||||||
[type='text']:focus,
|
|
||||||
[type='url']:active,
|
|
||||||
[type='url']:focus,
|
|
||||||
textarea:active,
|
|
||||||
textarea:focus {
|
|
||||||
border-color: #ff8c2f;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************
|
|
||||||
/
|
|
||||||
/ Login specific styling
|
|
||||||
/
|
|
||||||
/************************/
|
|
||||||
#login {
|
|
||||||
width: 500px;
|
|
||||||
margin: 6rem auto;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark #login {
|
|
||||||
background: #1f2937;
|
|
||||||
}
|
|
||||||
#login::after {
|
|
||||||
content: '';
|
|
||||||
clear: both;
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
#login .logo {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 120px;
|
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
}
|
|
||||||
#login .wordmark {
|
|
||||||
z-index: 1;
|
|
||||||
position: relative;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
#login .wordmark svg {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
#login .case {
|
|
||||||
float: right;
|
|
||||||
width: 30%;
|
|
||||||
font-size: 6rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#login .case img {
|
|
||||||
max-width: 96px;
|
|
||||||
max-height: 96px;
|
|
||||||
}
|
|
||||||
#login .error {
|
|
||||||
color: red;
|
|
||||||
margin-top: -20px;
|
|
||||||
}
|
|
||||||
#login .content {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
#login .form {
|
|
||||||
width: 65%;
|
|
||||||
}
|
|
||||||
.angle:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 120px;
|
|
||||||
background-color: #f15a2c;
|
|
||||||
background: -webkit-gradient(linear, left top, right top, from(#e22828), to(#ff8c2f));
|
|
||||||
background: linear-gradient(90deg, #e22828 0, #ff8c2f);
|
|
||||||
-webkit-transform-origin: bottom left;
|
|
||||||
transform-origin: bottom left;
|
|
||||||
-webkit-transform: skewY(-6deg);
|
|
||||||
transform: skewY(-6deg);
|
|
||||||
-webkit-transition: -webkit-transform 0.15s linear;
|
|
||||||
transition: -webkit-transform 0.15s linear;
|
|
||||||
transition: transform 0.15s linear;
|
|
||||||
transition:
|
|
||||||
transform 0.15s linear,
|
|
||||||
-webkit-transform 0.15s linear;
|
|
||||||
}
|
|
||||||
.shadow {
|
|
||||||
-webkit-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
|
|
||||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
.password-recovery {
|
|
||||||
display: block;
|
|
||||||
margin-top: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
/************************
|
|
||||||
/
|
|
||||||
/ Cases
|
|
||||||
/
|
|
||||||
/************************/
|
|
||||||
[class^='case-'],
|
|
||||||
[class*=' case-'] {
|
|
||||||
/* use !important to prevent issues with browser extensions that change fonts */
|
|
||||||
font-family: 'cases' !important;
|
|
||||||
speak: none;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
font-variant: normal;
|
|
||||||
text-transform: none;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
/* Better Font Rendering =========== */
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************
|
|
||||||
/
|
|
||||||
/ Media queries for mobile responsive
|
|
||||||
/
|
|
||||||
/************************/
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
body {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark body {
|
|
||||||
background: #111827;
|
|
||||||
}
|
|
||||||
[type='email'],
|
|
||||||
[type='number'],
|
|
||||||
[type='password'],
|
|
||||||
[type='search'],
|
|
||||||
[type='tel'],
|
|
||||||
[type='text'],
|
|
||||||
[type='url'],
|
|
||||||
textarea {
|
|
||||||
font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */
|
|
||||||
}
|
|
||||||
#login {
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#login .logo {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.shadow {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
|
|
||||||
const parseRedirectTarget = (target: string | null) => {
|
|
||||||
if (target && target !== '/') {
|
|
||||||
// parse target and ensure it is a bare path with no query parameters
|
|
||||||
console.log(target);
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
return '/';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRedirectUrl = () => {
|
|
||||||
const search = new URLSearchParams(window.location.search);
|
|
||||||
const targetRoute = parseRedirectTarget(search.get('target'));
|
|
||||||
if (search.has('data') && (search.size === 1 || search.size === 2)) {
|
|
||||||
return `${window.location.origin}${targetRoute}?data=${encodeURIComponent(search.get('data')!)}`;
|
|
||||||
}
|
|
||||||
return `${window.location.origin}${targetRoute}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const textElement = document.getElementById('text');
|
|
||||||
if (textElement) {
|
|
||||||
textElement.style.display = 'block';
|
|
||||||
}
|
|
||||||
}, 750);
|
|
||||||
|
|
||||||
window.location.href = getRedirectUrl();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
id="text"
|
|
||||||
style="text-align: center; margin-top: calc(100vh - 75%); display: none; font-family: sans-serif"
|
|
||||||
>
|
|
||||||
<h1>Redirecting...</h1>
|
|
||||||
<h2><a :href="getRedirectUrl()">Click here if you are not redirected automatically</a></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import ApiKeyAuthorize from '~/components/ApiKeyAuthorize.standalone.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ApiKeyAuthorize />
|
|
||||||
</template>
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
import { Button, Label, Switch } from '@unraid/ui';
|
|
||||||
import { useDummyServerStore } from '~/_data/serverState';
|
|
||||||
|
|
||||||
import type { ServerState, ServerUpdateOsResponse } from '~/types/server';
|
|
||||||
|
|
||||||
import CheckUpdateResponseModal from '~/components/UpdateOs/CheckUpdateResponseModal.vue';
|
|
||||||
import { useServerStore } from '~/store/server';
|
|
||||||
import { useUpdateOsStore } from '~/store/updateOs';
|
|
||||||
|
|
||||||
const updateOsStore = useUpdateOsStore();
|
|
||||||
const serverStore = useServerStore();
|
|
||||||
const dummyServerStore = useDummyServerStore();
|
|
||||||
|
|
||||||
// Test scenarios
|
|
||||||
const testScenarios = [
|
|
||||||
{
|
|
||||||
id: 'expired-ineligible',
|
|
||||||
name: 'Expired key with ineligible update',
|
|
||||||
description: 'License expired, update available but not eligible',
|
|
||||||
serverState: 'EEXPIRED',
|
|
||||||
updateResponse: {
|
|
||||||
version: '7.1.0',
|
|
||||||
name: 'Unraid 7.1.0',
|
|
||||||
date: '2024-12-15',
|
|
||||||
isNewer: true,
|
|
||||||
isEligible: false,
|
|
||||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
|
||||||
changelogPretty:
|
|
||||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
|
||||||
sha256: undefined, // requires auth
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'normal-update',
|
|
||||||
name: 'Normal update available',
|
|
||||||
description: 'Active license with eligible update',
|
|
||||||
serverState: 'BASIC',
|
|
||||||
updateResponse: {
|
|
||||||
version: '7.1.0',
|
|
||||||
name: 'Unraid 7.1.0',
|
|
||||||
date: '2024-12-15',
|
|
||||||
isNewer: true,
|
|
||||||
isEligible: true,
|
|
||||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
|
||||||
changelogPretty:
|
|
||||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
|
||||||
sha256: 'abc123def456789',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'renewal-required',
|
|
||||||
name: 'Update requires renewal',
|
|
||||||
description: 'License expired > 1 year, update requires renewal',
|
|
||||||
serverState: 'STARTER',
|
|
||||||
updateResponse: {
|
|
||||||
version: '7.1.0',
|
|
||||||
name: 'Unraid 7.1.0',
|
|
||||||
date: '2024-12-15',
|
|
||||||
isNewer: true,
|
|
||||||
isEligible: false,
|
|
||||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
|
||||||
changelogPretty:
|
|
||||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
|
||||||
sha256: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'no-update',
|
|
||||||
name: 'No update available',
|
|
||||||
description: 'Already on latest version',
|
|
||||||
serverState: 'BASIC',
|
|
||||||
updateResponse: {
|
|
||||||
version: '7.0.0',
|
|
||||||
name: 'Unraid 7.0.0',
|
|
||||||
date: '2024-01-15',
|
|
||||||
isNewer: false,
|
|
||||||
isEligible: true,
|
|
||||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.0.0/',
|
|
||||||
sha256: 'xyz789abc123',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'trial-update',
|
|
||||||
name: 'Trial with update',
|
|
||||||
description: 'Trial license with update available',
|
|
||||||
serverState: 'TRIAL',
|
|
||||||
updateResponse: {
|
|
||||||
version: '7.1.0',
|
|
||||||
name: 'Unraid 7.1.0',
|
|
||||||
date: '2024-12-15',
|
|
||||||
isNewer: true,
|
|
||||||
isEligible: true,
|
|
||||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
|
||||||
sha256: 'def456ghi789',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'pro-auth-required',
|
|
||||||
name: 'Pro license - auth required',
|
|
||||||
description: 'Pro license but authentication required for download',
|
|
||||||
serverState: 'PRO',
|
|
||||||
updateResponse: {
|
|
||||||
version: '7.1.0',
|
|
||||||
name: 'Unraid 7.1.0',
|
|
||||||
date: '2024-12-15',
|
|
||||||
isNewer: true,
|
|
||||||
isEligible: true,
|
|
||||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
|
||||||
sha256: undefined, // requires auth
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Component state
|
|
||||||
const selectedScenario = ref('normal-update');
|
|
||||||
const modalOpen = ref(false);
|
|
||||||
const ignoreRelease = ref(false);
|
|
||||||
const checkingForUpdates = ref(false);
|
|
||||||
const ignoredReleases = ref<string[]>([]);
|
|
||||||
|
|
||||||
// Apply scenario
|
|
||||||
const applyScenario = () => {
|
|
||||||
const scenario = testScenarios.find((s) => s.id === selectedScenario.value);
|
|
||||||
if (!scenario) return;
|
|
||||||
|
|
||||||
// Apply server state
|
|
||||||
dummyServerStore.selector =
|
|
||||||
scenario.serverState === 'EEXPIRED' || scenario.serverState === 'STARTER' ? 'default' : 'default';
|
|
||||||
|
|
||||||
// Set server state
|
|
||||||
const currentTime = Date.now();
|
|
||||||
const expiredTime = scenario.serverState === 'EEXPIRED' ? currentTime - 24 * 60 * 60 * 1000 : 0;
|
|
||||||
const regExp =
|
|
||||||
scenario.serverState === 'STARTER' ? currentTime - 400 * 24 * 60 * 60 * 1000 : undefined;
|
|
||||||
|
|
||||||
// Apply update response
|
|
||||||
if (scenario.serverState === 'EEXPIRED') {
|
|
||||||
serverStore.$patch({
|
|
||||||
expireTime: expiredTime,
|
|
||||||
state: 'EEXPIRED' as ServerState,
|
|
||||||
regExp: undefined,
|
|
||||||
});
|
|
||||||
} else if (scenario.serverState === 'STARTER') {
|
|
||||||
serverStore.$patch({
|
|
||||||
state: 'STARTER' as ServerState,
|
|
||||||
regExp: regExp,
|
|
||||||
regTy: 'Starter',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
serverStore.$patch({
|
|
||||||
state: scenario.serverState as ServerState,
|
|
||||||
regExp: undefined,
|
|
||||||
expireTime: scenario.serverState === 'TRIAL' ? currentTime + 7 * 24 * 60 * 60 * 1000 : 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
serverStore.setUpdateOsResponse(scenario.updateResponse as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
// Apply ignored releases
|
|
||||||
if (ignoreRelease.value && scenario.updateResponse.isNewer) {
|
|
||||||
if (!ignoredReleases.value.includes(scenario.updateResponse.version)) {
|
|
||||||
ignoredReleases.value.push(scenario.updateResponse.version);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ignoredReleases.value = ignoredReleases.value.filter((v) => v !== scenario.updateResponse.version);
|
|
||||||
}
|
|
||||||
serverStore.$patch({ updateOsIgnoredReleases: ignoredReleases.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Watch for scenario changes
|
|
||||||
watch([selectedScenario, ignoreRelease], () => {
|
|
||||||
applyScenario();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open modal with scenario
|
|
||||||
const openModal = () => {
|
|
||||||
applyScenario();
|
|
||||||
updateOsStore.checkForUpdatesLoading = checkingForUpdates.value;
|
|
||||||
modalOpen.value = true;
|
|
||||||
updateOsStore.setModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
applyScenario();
|
|
||||||
|
|
||||||
const currentScenario = computed(() => testScenarios.find((s) => s.id === selectedScenario.value));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="container mx-auto max-w-4xl p-6">
|
|
||||||
<div class="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900">
|
|
||||||
<div class="mb-6">
|
|
||||||
<h2 class="mb-2 text-2xl font-bold">Update Modal Test Page</h2>
|
|
||||||
<p class="text-muted-foreground">
|
|
||||||
Test various update scenarios for the CheckUpdateResponseModal component
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- Scenario Selection -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
<Label class="text-lg font-semibold">Select Test Scenario</Label>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div v-for="scenario in testScenarios" :key="scenario.id" class="flex items-start space-x-3">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
:id="scenario.id"
|
|
||||||
:value="scenario.id"
|
|
||||||
v-model="selectedScenario"
|
|
||||||
class="mt-1 rounded-full"
|
|
||||||
/>
|
|
||||||
<div class="flex-1">
|
|
||||||
<Label :for="scenario.id" class="block cursor-pointer font-medium">
|
|
||||||
{{ scenario.name }}
|
|
||||||
</Label>
|
|
||||||
<p class="text-muted-foreground mt-1 text-sm">{{ scenario.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Options -->
|
|
||||||
<div class="space-y-4 border-t pt-4">
|
|
||||||
<h3 class="font-semibold">Options</h3>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<Switch id="ignore-release" v-model:checked="ignoreRelease" />
|
|
||||||
<Label for="ignore-release" class="cursor-pointer">Ignore this release</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<Switch id="checking-updates" v-model:checked="checkingForUpdates" />
|
|
||||||
<Label for="checking-updates" class="cursor-pointer"
|
|
||||||
>Show checking for updates loading state</Label
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Current State Display -->
|
|
||||||
<div class="space-y-2 border-t pt-4">
|
|
||||||
<h3 class="font-semibold">Current Scenario Details</h3>
|
|
||||||
<div class="space-y-1 font-mono text-sm">
|
|
||||||
<p><span class="font-semibold">Server State:</span> {{ currentScenario?.serverState }}</p>
|
|
||||||
<p>
|
|
||||||
<span class="font-semibold">Version:</span> {{ currentScenario?.updateResponse.version }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span class="font-semibold">Is Newer:</span> {{ currentScenario?.updateResponse.isNewer }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span class="font-semibold">Is Eligible:</span>
|
|
||||||
{{ currentScenario?.updateResponse.isEligible }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span class="font-semibold">Has SHA256:</span>
|
|
||||||
{{ !!currentScenario?.updateResponse.sha256 }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span class="font-semibold">Ignored Releases:</span>
|
|
||||||
{{ ignoredReleases.join(', ') || 'None' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Open Modal Button -->
|
|
||||||
<div class="border-t pt-4">
|
|
||||||
<Button @click="openModal" variant="primary" class="w-full"> Open Update Modal </Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- The Modal Component -->
|
|
||||||
<CheckUpdateResponseModal
|
|
||||||
:open="modalOpen"
|
|
||||||
@update:open="
|
|
||||||
(val: boolean) => {
|
|
||||||
modalOpen = val;
|
|
||||||
updateOsStore.setModalOpen(val);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
|
|
||||||
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
|
|
||||||
import { useActivationCodeModalStore } from '~/components/Activation/store/activationCodeModal';
|
|
||||||
import { useWelcomeModalDataStore } from '~/components/Activation/store/welcomeModalData';
|
|
||||||
import WelcomeModalCe from '~/components/Activation/WelcomeModal.standalone.vue';
|
|
||||||
import ModalsCe from '~/components/Modals.standalone.vue';
|
|
||||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
|
||||||
|
|
||||||
const welcomeModalRef = ref<InstanceType<typeof WelcomeModalCe>>();
|
|
||||||
|
|
||||||
const modalStore = useActivationCodeModalStore();
|
|
||||||
const { isVisible } = storeToRefs(modalStore);
|
|
||||||
const { isFreshInstall } = storeToRefs(useActivationCodeDataStore());
|
|
||||||
const { isInitialSetup } = storeToRefs(useWelcomeModalDataStore());
|
|
||||||
const { callbackData } = storeToRefs(useCallbackActionsStore());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forces the activation modal to show - this flag overrides the default logic
|
|
||||||
* which only shows the modal if the server is a fresh install and there is no
|
|
||||||
* callback data.
|
|
||||||
*/
|
|
||||||
const showActivationModal = () => {
|
|
||||||
modalStore.setIsHidden(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showWelcomeModal = () => {
|
|
||||||
if (welcomeModalRef.value) {
|
|
||||||
welcomeModalRef.value.showWelcomeModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col gap-6 p-6">
|
|
||||||
<WelcomeModalCe ref="welcomeModalRef" />
|
|
||||||
<ModalsCe />
|
|
||||||
<div class="border-muted mt-4 rounded border bg-gray-100 p-4 dark:bg-gray-800">
|
|
||||||
<h3 class="mb-2 text-lg font-semibold">Activation Modal Debug Info:</h3>
|
|
||||||
<p>Should Show Modal (`showActivationModal`): {{ isVisible }}</p>
|
|
||||||
<ul class="ml-4 list-inside list-disc">
|
|
||||||
<li>Is Fresh Install - Private (`isFreshInstall`): {{ isFreshInstall }}</li>
|
|
||||||
<li>Is Initial Setup - Public (`isInitialSetup`): {{ isInitialSetup }}</li>
|
|
||||||
<li>Has Callback Data (`callbackData`): {{ !!callbackData }}</li>
|
|
||||||
<li>Manually Hidden (`activationModalHidden`): {{ isVisible }}</li>
|
|
||||||
</ul>
|
|
||||||
<div class="mt-2 flex gap-2">
|
|
||||||
<button
|
|
||||||
class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
|
||||||
@click="showActivationModal"
|
|
||||||
>
|
|
||||||
Show Activation Modal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
|
|
||||||
@click="showWelcomeModal"
|
|
||||||
>
|
|
||||||
Show Welcome Modal
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -24,22 +24,19 @@ import dayjs from 'dayjs';
|
|||||||
import prerelease from 'semver/functions/prerelease';
|
import prerelease from 'semver/functions/prerelease';
|
||||||
|
|
||||||
import type { ApolloQueryResult } from '@apollo/client/core/index.js';
|
import type { ApolloQueryResult } from '@apollo/client/core/index.js';
|
||||||
|
import type { ServerActionTypes, ServerData } from '@unraid/shared-callbacks';
|
||||||
import type { Config, PartialCloudFragment, ServerStateQuery } from '~/composables/gql/graphql';
|
import type { Config, PartialCloudFragment, ServerStateQuery } from '~/composables/gql/graphql';
|
||||||
import type { Error } from '~/store/errors';
|
import type { Error } from '~/store/errors';
|
||||||
import type { Theme } from '~/themes/types';
|
import type { Theme } from '~/themes/types';
|
||||||
import type {
|
import type {
|
||||||
Server,
|
Server,
|
||||||
ServerAccountCallbackSendPayload,
|
|
||||||
ServerconnectPluginInstalled,
|
ServerconnectPluginInstalled,
|
||||||
ServerDateTimeFormat,
|
ServerDateTimeFormat,
|
||||||
ServerKeyTypeForPurchase,
|
|
||||||
ServerOsVersionBranch,
|
ServerOsVersionBranch,
|
||||||
ServerPurchaseCallbackSendPayload,
|
|
||||||
ServerState,
|
ServerState,
|
||||||
ServerStateArray,
|
ServerStateArray,
|
||||||
ServerStateData,
|
ServerStateData,
|
||||||
ServerStateDataAction,
|
ServerStateDataAction,
|
||||||
ServerStateDataKeyActions,
|
|
||||||
ServerUpdateOsResponse,
|
ServerUpdateOsResponse,
|
||||||
} from '~/types/server';
|
} from '~/types/server';
|
||||||
|
|
||||||
@@ -224,73 +221,49 @@ export const useServerStore = defineStore('server', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverPurchasePayload = computed((): ServerPurchaseCallbackSendPayload => {
|
const serverPurchasePayload = computed((): ServerData => {
|
||||||
/** @todo refactor out. Just parse state on craft site to determine */
|
const server: ServerData = {
|
||||||
let keyTypeForPurchase: ServerKeyTypeForPurchase = 'Trial';
|
description: description.value,
|
||||||
switch (state.value) {
|
|
||||||
case 'BASIC':
|
|
||||||
keyTypeForPurchase = 'Basic';
|
|
||||||
break;
|
|
||||||
case 'PLUS':
|
|
||||||
keyTypeForPurchase = 'Plus';
|
|
||||||
break;
|
|
||||||
case 'PRO':
|
|
||||||
keyTypeForPurchase = 'Pro';
|
|
||||||
break;
|
|
||||||
case 'STARTER':
|
|
||||||
keyTypeForPurchase = 'Starter';
|
|
||||||
break;
|
|
||||||
case 'UNLEASHED':
|
|
||||||
keyTypeForPurchase = 'Unleashed';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const server: ServerPurchaseCallbackSendPayload = {
|
|
||||||
apiVersion: apiVersion.value,
|
|
||||||
connectPluginVersion: connectPluginVersion.value,
|
|
||||||
deviceCount: deviceCount.value,
|
deviceCount: deviceCount.value,
|
||||||
email: email.value,
|
expireTime: expireTime.value,
|
||||||
|
flashProduct: flashProduct.value,
|
||||||
|
flashVendor: flashVendor.value,
|
||||||
guid: guid.value,
|
guid: guid.value,
|
||||||
inIframe: inIframe.value,
|
|
||||||
keyTypeForPurchase,
|
|
||||||
locale: locale.value,
|
locale: locale.value,
|
||||||
|
name: name.value,
|
||||||
osVersion: osVersion.value,
|
osVersion: osVersion.value,
|
||||||
osVersionBranch: osVersionBranch.value,
|
osVersionBranch: osVersionBranch.value,
|
||||||
registered: registered.value ?? false,
|
registered: registered.value ?? false,
|
||||||
regExp: regExp.value,
|
regExp: regExp.value,
|
||||||
|
regGen: regGen.value,
|
||||||
|
regGuid: regGuid.value,
|
||||||
regTy: regTy.value,
|
regTy: regTy.value,
|
||||||
regUpdatesExpired: regUpdatesExpired.value,
|
regUpdatesExpired: regUpdatesExpired.value,
|
||||||
state: state.value,
|
state: state.value,
|
||||||
site: site.value,
|
wanFQDN: wanFQDN.value,
|
||||||
};
|
};
|
||||||
return server;
|
return server;
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverAccountPayload = computed((): ServerAccountCallbackSendPayload => {
|
const serverAccountPayload = computed((): ServerData => {
|
||||||
return {
|
return {
|
||||||
apiVersion: apiVersion.value,
|
|
||||||
caseModel: caseModel.value,
|
|
||||||
connectPluginVersion: connectPluginVersion.value,
|
|
||||||
deviceCount: deviceCount.value,
|
deviceCount: deviceCount.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
expireTime: expireTime.value,
|
expireTime: expireTime.value,
|
||||||
flashBackupActivated: flashBackupActivated.value,
|
|
||||||
flashProduct: flashProduct.value,
|
flashProduct: flashProduct.value,
|
||||||
flashVendor: flashVendor.value,
|
flashVendor: flashVendor.value,
|
||||||
guid: guid.value,
|
guid: guid.value,
|
||||||
inIframe: inIframe.value,
|
|
||||||
keyfile: keyfile.value,
|
keyfile: keyfile.value,
|
||||||
lanIp: lanIp.value,
|
locale: locale.value,
|
||||||
name: name.value,
|
name: name.value,
|
||||||
osVersion: osVersion.value,
|
osVersion: osVersion.value,
|
||||||
osVersionBranch: osVersionBranch.value,
|
osVersionBranch: osVersionBranch.value,
|
||||||
rebootType: rebootType.value,
|
|
||||||
rebootVersion: rebootVersion.value,
|
|
||||||
registered: registered.value ?? false,
|
registered: registered.value ?? false,
|
||||||
|
regGen: regGen.value,
|
||||||
regGuid: regGuid.value,
|
regGuid: regGuid.value,
|
||||||
regExp: regExp.value,
|
regExp: regExp.value,
|
||||||
regTy: regTy.value,
|
regTy: regTy.value,
|
||||||
regUpdatesExpired: regUpdatesExpired.value,
|
regUpdatesExpired: regUpdatesExpired.value,
|
||||||
site: site.value,
|
|
||||||
state: state.value,
|
state: state.value,
|
||||||
wanFQDN: wanFQDN.value,
|
wanFQDN: wanFQDN.value,
|
||||||
};
|
};
|
||||||
@@ -1301,16 +1274,14 @@ export const useServerStore = defineStore('server', () => {
|
|||||||
|
|
||||||
const filteredKeyActions = (
|
const filteredKeyActions = (
|
||||||
filterType: 'by' | 'out',
|
filterType: 'by' | 'out',
|
||||||
filters: string | ServerStateDataKeyActions[]
|
filters: ServerActionTypes[]
|
||||||
): ServerStateDataAction[] | undefined => {
|
): ServerStateDataAction[] | undefined => {
|
||||||
if (!stateData.value.actions) {
|
if (!stateData.value.actions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stateData.value.actions.filter((action) => {
|
return stateData.value.actions.filter((action) => {
|
||||||
return filterType === 'out'
|
return filterType === 'out' ? !filters.includes(action.name) : filters.includes(action.name);
|
||||||
? !filters.includes(action.name as ServerStateDataKeyActions)
|
|
||||||
: filters.includes(action.name as ServerStateDataKeyActions);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ServerActionTypes } from '@unraid/shared-callbacks';
|
||||||
import type { ActivationCode, Config, PartialCloudFragment } from '~/composables/gql/graphql';
|
import type { ActivationCode, Config, PartialCloudFragment } from '~/composables/gql/graphql';
|
||||||
import type { Theme } from '~/themes/types';
|
import type { Theme } from '~/themes/types';
|
||||||
import type { UserProfileLink } from '~/types/userProfile';
|
import type { UserProfileLink } from '~/types/userProfile';
|
||||||
@@ -29,6 +30,7 @@ export type ServerState =
|
|||||||
| 'STARTER'
|
| 'STARTER'
|
||||||
| 'UNLEASHED'
|
| 'UNLEASHED'
|
||||||
| 'LIFETIME'
|
| 'LIFETIME'
|
||||||
|
| 'STALE'
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export type ServerOsVersionBranch = 'stable' | 'next' | 'preview' | 'test';
|
export type ServerOsVersionBranch = 'stable' | 'next' | 'preview' | 'test';
|
||||||
@@ -124,78 +126,8 @@ export interface Server {
|
|||||||
wanIp?: string;
|
wanIp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerAccountCallbackSendPayload {
|
export interface ServerStateDataAction extends UserProfileLink<ServerActionTypes> {
|
||||||
activationCodeData?: ActivationCode;
|
name: ServerActionTypes;
|
||||||
apiVersion?: string;
|
|
||||||
caseModel?: string;
|
|
||||||
connectPluginVersion?: string;
|
|
||||||
description?: string;
|
|
||||||
deviceCount?: number;
|
|
||||||
expireTime?: number;
|
|
||||||
flashBackupActivated?: boolean;
|
|
||||||
flashProduct?: string;
|
|
||||||
flashVendor?: string;
|
|
||||||
guid?: string;
|
|
||||||
inIframe: boolean;
|
|
||||||
keyfile?: string;
|
|
||||||
lanIp?: string;
|
|
||||||
locale?: string;
|
|
||||||
name?: string;
|
|
||||||
osVersion?: string;
|
|
||||||
osVersionBranch?: ServerOsVersionBranch;
|
|
||||||
rebootType?: ServerRebootType;
|
|
||||||
rebootVersion?: string;
|
|
||||||
registered: boolean;
|
|
||||||
regExp?: number;
|
|
||||||
regGen?: number;
|
|
||||||
regGuid?: string;
|
|
||||||
regTy?: string;
|
|
||||||
regUpdatesExpired?: boolean;
|
|
||||||
site?: string;
|
|
||||||
state: ServerState;
|
|
||||||
wanFQDN?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ServerKeyTypeForPurchase = 'Basic' | 'Plus' | 'Pro' | 'Starter' | 'Trial' | 'Unleashed';
|
|
||||||
|
|
||||||
export interface ServerPurchaseCallbackSendPayload {
|
|
||||||
activationCodeData?: ActivationCode;
|
|
||||||
apiVersion?: string;
|
|
||||||
connectPluginVersion?: string;
|
|
||||||
deviceCount: number;
|
|
||||||
email: string;
|
|
||||||
guid: string;
|
|
||||||
inIframe: boolean;
|
|
||||||
keyTypeForPurchase: ServerKeyTypeForPurchase;
|
|
||||||
locale: string;
|
|
||||||
osVersion?: string;
|
|
||||||
osVersionBranch?: ServerOsVersionBranch;
|
|
||||||
registered: boolean;
|
|
||||||
regExp?: number;
|
|
||||||
regTy?: string;
|
|
||||||
regUpdatesExpired?: boolean;
|
|
||||||
state: ServerState;
|
|
||||||
site: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ServerStateDataKeyActions =
|
|
||||||
| 'activate'
|
|
||||||
| 'purchase'
|
|
||||||
| 'redeem'
|
|
||||||
| 'upgrade'
|
|
||||||
| 'recover'
|
|
||||||
| 'renew'
|
|
||||||
| 'replace'
|
|
||||||
| 'trialExtend'
|
|
||||||
| 'trialStart'
|
|
||||||
| 'updateOs';
|
|
||||||
|
|
||||||
export type ServerStateDataAccountActions = 'signIn' | 'signOut' | 'troubleshoot';
|
|
||||||
|
|
||||||
export type ServerStateDataActionType = ServerStateDataKeyActions | ServerStateDataAccountActions;
|
|
||||||
|
|
||||||
export interface ServerStateDataAction extends UserProfileLink {
|
|
||||||
name: ServerStateDataActionType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerStateDataError {
|
export interface ServerStateDataError {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export type UserProfileLinkClick =
|
|||||||
| ((...args: UserProfileLinkClickParams[]) => void | Promise<void>)
|
| ((...args: UserProfileLinkClickParams[]) => void | Promise<void>)
|
||||||
| ((...args: UserProfileLinkClickParams[]) => Promise<NodeJS.Timeout | undefined>);
|
| ((...args: UserProfileLinkClickParams[]) => Promise<NodeJS.Timeout | undefined>);
|
||||||
|
|
||||||
export interface UserProfileLink {
|
export interface UserProfileLink<Name extends string = string> {
|
||||||
click?: UserProfileLinkClick;
|
click?: UserProfileLinkClick;
|
||||||
clickParams?: UserProfileLinkClickParams;
|
clickParams?: UserProfileLinkClickParams;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -16,7 +16,7 @@ export interface UserProfileLink {
|
|||||||
external?: boolean;
|
external?: boolean;
|
||||||
href?: string;
|
href?: string;
|
||||||
icon?: typeof ArrowTopRightOnSquareIcon;
|
icon?: typeof ArrowTopRightOnSquareIcon;
|
||||||
name?: string;
|
name?: Name;
|
||||||
text: string;
|
text: string;
|
||||||
textParams?: string[] | number[];
|
textParams?: string[] | number[];
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user