fix: parsing of ssoEnabled in state.php (#1455)

read `ssoSubIds` in state.php from `api.json`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added a new query to check if Single Sign-On (SSO) is enabled.
* Updated UI components to dynamically reflect SSO availability via live
data.
* **Refactor**
* Streamlined internal handling of SSO status detection for improved
reliability and maintainability.
* **Tests**
* Enhanced tests for SSO button behavior with mocked live data and added
edge case coverage for SSO callback handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Pujit Mehrotra
2025-07-02 10:24:38 -04:00
committed by GitHub
parent 038c582aed
commit f542c8e0bd
11 changed files with 162 additions and 53 deletions

View File

@@ -1666,6 +1666,7 @@ type Query {
disk(id: PrefixedID!): Disk! disk(id: PrefixedID!): Disk!
rclone: RCloneBackupSettings! rclone: RCloneBackupSettings!
settings: Settings! settings: Settings!
isSSOEnabled: Boolean!
"""List all installed plugins with their metadata""" """List all installed plugins with their metadata"""
plugins: [Plugin!]! plugins: [Plugin!]!

View File

@@ -13,6 +13,8 @@ import { GraphQLJSON } from 'graphql-scalars';
import { ENVIRONMENT } from '@app/environment.js'; import { ENVIRONMENT } from '@app/environment.js';
import { LifecycleService } from '@app/unraid-api/app/lifecycle.service.js'; import { LifecycleService } from '@app/unraid-api/app/lifecycle.service.js';
import { Public } from '@app/unraid-api/auth/public.decorator.js';
import { SsoUserService } from '@app/unraid-api/auth/sso-user.service.js';
import { import {
Settings, Settings,
UnifiedSettings, UnifiedSettings,
@@ -22,7 +24,10 @@ import { ApiSettings } from '@app/unraid-api/graph/resolvers/settings/settings.s
@Resolver(() => Settings) @Resolver(() => Settings)
export class SettingsResolver { export class SettingsResolver {
constructor(private readonly apiSettings: ApiSettings) {} constructor(
private readonly apiSettings: ApiSettings,
private readonly ssoUserService: SsoUserService
) {}
@Query(() => Settings) @Query(() => Settings)
async settings() { async settings() {
@@ -45,6 +50,12 @@ export class SettingsResolver {
id: 'unified-settings', id: 'unified-settings',
}; };
} }
@Query(() => Boolean)
@Public()
public async isSSOEnabled(): Promise<boolean> {
return this.ssoUserService.getSsoUsers().then((users) => users.length > 0);
}
} }
@Resolver(() => UnifiedSettings) @Resolver(() => UnifiedSettings)

View File

@@ -6,6 +6,9 @@
*/ */
class ApiConfig class ApiConfig
{ {
/** Home of API-specific configuration files */
public const CONFIG_DIR = '/boot/config/plugins/dynamix.my.servers/configs';
private static $scriptsDir = "/usr/local/share/dynamix.unraid.net/scripts"; private static $scriptsDir = "/usr/local/share/dynamix.unraid.net/scripts";
/** /**
@@ -95,3 +98,24 @@ class ApiConfig
return !empty($version) ? $version : 'unknown'; return !empty($version) ? $version : 'unknown';
} }
} }
class ApiUserConfig
{
public const CONFIG_PATH = ApiConfig::CONFIG_DIR . '/api.json';
public static function getConfig()
{
try {
return json_decode(file_get_contents(self::CONFIG_PATH), true) ?? [];
} catch (Throwable $e) {
return [];
}
}
public static function isSSOEnabled()
{
$config = self::getConfig();
return !empty($config['ssoSubIds'] ?? '');
}
}

View File

@@ -202,7 +202,7 @@ class ServerState
$this->registered = !empty($connectConfig['apikey']) && $this->connectPluginInstalled; $this->registered = !empty($connectConfig['apikey']) && $this->connectPluginInstalled;
$this->registeredTime = $connectConfig['regWizTime'] ?? ''; $this->registeredTime = $connectConfig['regWizTime'] ?? '';
$this->username = $connectConfig['username'] ?? ''; $this->username = $connectConfig['username'] ?? '';
$this->ssoEnabled = !empty($connectConfig['ssoSubIds'] ?? ''); $this->ssoEnabled = ApiUserConfig::isSSOEnabled();
} }
private function getConnectKnownOrigins() private function getConnectKnownOrigins()

View File

@@ -2,6 +2,7 @@
* SsoButton Component Test Coverage * SsoButton Component Test Coverage
*/ */
import { useQuery } from '@vue/apollo-composable';
import { flushPromises, mount } from '@vue/test-utils'; import { flushPromises, mount } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -15,6 +16,11 @@ const BrandButtonStub = {
props: ['disabled', 'variant', 'class'], props: ['disabled', 'variant', 'class'],
}; };
// Mock the GraphQL composable
vi.mock('@vue/apollo-composable', () => ({
useQuery: vi.fn(),
}));
vi.mock('~/helpers/urls', () => ({ vi.mock('~/helpers/urls', () => ({
ACCOUNT: 'http://mock-account-url.net', ACCOUNT: 'http://mock-account-url.net',
})); }));
@@ -60,10 +66,13 @@ const mockUsernameField = { value: '' };
describe('SsoButton.ce.vue', () => { describe('SsoButton.ce.vue', () => {
let querySelectorSpy: MockInstance; let querySelectorSpy: MockInstance;
let mockUseQuery: Mock;
beforeEach(() => { beforeEach(async () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
mockUseQuery = useQuery as Mock;
(sessionStorage.getItem as Mock).mockReturnValue(null); (sessionStorage.getItem as Mock).mockReturnValue(null);
(sessionStorage.setItem as Mock).mockClear(); (sessionStorage.setItem as Mock).mockClear();
mockForm.requestSubmit.mockClear(); mockForm.requestSubmit.mockClear();
@@ -73,6 +82,7 @@ describe('SsoButton.ce.vue', () => {
mockLocation.search = ''; mockLocation.search = '';
mockLocation.href = ''; mockLocation.href = '';
(fetch as Mock).mockClear(); (fetch as Mock).mockClear();
mockUseQuery.mockClear();
// Spy on document.querySelector and provide mock implementation // Spy on document.querySelector and provide mock implementation
querySelectorSpy = vi.spyOn(document, 'querySelector'); querySelectorSpy = vi.spyOn(document, 'querySelector');
@@ -93,9 +103,12 @@ describe('SsoButton.ce.vue', () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
it('renders the button when ssoenabled prop is true (boolean)', () => { it('renders the button when SSO is enabled via GraphQL', () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
const wrapper = mount(SsoButton, { const wrapper = mount(SsoButton, {
props: { ssoenabled: true },
global: { global: {
stubs: { BrandButton: BrandButtonStub }, stubs: { BrandButton: BrandButtonStub },
}, },
@@ -105,33 +118,12 @@ describe('SsoButton.ce.vue', () => {
expect(wrapper.text()).toContain('Log In With Unraid.net'); expect(wrapper.text()).toContain('Log In With Unraid.net');
}); });
it('renders the button when ssoenabled prop is true (string)', () => { it('does not render the button when SSO is disabled via GraphQL', () => {
const wrapper = mount(SsoButton, { mockUseQuery.mockReturnValue({
props: { ssoenabled: 'true' }, result: { value: { isSSOEnabled: false } },
global: {
stubs: { BrandButton: BrandButtonStub },
},
}); });
expect(wrapper.findComponent(BrandButtonStub).exists()).toBe(true);
expect(wrapper.text()).toContain('or');
expect(wrapper.text()).toContain('Log In With Unraid.net');
});
it('renders the button when ssoEnabled prop is true', () => {
const wrapper = mount(SsoButton, { const wrapper = mount(SsoButton, {
props: { ssoEnabled: true },
global: {
stubs: { BrandButton: BrandButtonStub },
},
});
expect(wrapper.findComponent(BrandButtonStub).exists()).toBe(true);
expect(wrapper.text()).toContain('or');
expect(wrapper.text()).toContain('Log In With Unraid.net');
});
it('does not render the button when ssoenabled prop is false', () => {
const wrapper = mount(SsoButton, {
props: { ssoenabled: false },
global: { global: {
stubs: { BrandButton: BrandButtonStub }, stubs: { BrandButton: BrandButtonStub },
}, },
@@ -140,9 +132,12 @@ describe('SsoButton.ce.vue', () => {
expect(wrapper.text()).not.toContain('or'); expect(wrapper.text()).not.toContain('or');
}); });
it('does not render the button when ssoEnabled prop is false', () => { it('does not render the button when GraphQL result is null/undefined', () => {
mockUseQuery.mockReturnValue({
result: { value: null },
});
const wrapper = mount(SsoButton, { const wrapper = mount(SsoButton, {
props: { ssoEnabled: false },
global: { global: {
stubs: { BrandButton: BrandButtonStub }, stubs: { BrandButton: BrandButtonStub },
}, },
@@ -151,7 +146,11 @@ describe('SsoButton.ce.vue', () => {
expect(wrapper.text()).not.toContain('or'); expect(wrapper.text()).not.toContain('or');
}); });
it('does not render the button when props are not provided', () => { it('does not render the button when GraphQL result is undefined', () => {
mockUseQuery.mockReturnValue({
result: { value: undefined },
});
const wrapper = mount(SsoButton, { const wrapper = mount(SsoButton, {
global: { global: {
stubs: { BrandButton: BrandButtonStub }, stubs: { BrandButton: BrandButtonStub },
@@ -162,8 +161,11 @@ describe('SsoButton.ce.vue', () => {
}); });
it('navigates to the external SSO URL on button click', async () => { it('navigates to the external SSO URL on button click', async () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
const wrapper = mount(SsoButton, { const wrapper = mount(SsoButton, {
props: { ssoenabled: true },
global: { global: {
stubs: { BrandButton: BrandButtonStub }, stubs: { BrandButton: BrandButtonStub },
}, },
@@ -186,6 +188,10 @@ describe('SsoButton.ce.vue', () => {
}); });
it('handles SSO callback in onMounted hook successfully', async () => { it('handles SSO callback in onMounted hook successfully', async () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
const mockCode = 'mock_auth_code'; const mockCode = 'mock_auth_code';
const mockState = 'mock_session_state_value'; const mockState = 'mock_session_state_value';
const mockAccessToken = 'mock_access_token_123'; const mockAccessToken = 'mock_access_token_123';
@@ -199,7 +205,6 @@ describe('SsoButton.ce.vue', () => {
// Mount the component so that onMounted hook is called // Mount the component so that onMounted hook is called
mount(SsoButton, { mount(SsoButton, {
props: { ssoenabled: true },
global: { global: {
stubs: { BrandButton: BrandButtonStub }, stubs: { BrandButton: BrandButtonStub },
}, },
@@ -225,6 +230,10 @@ describe('SsoButton.ce.vue', () => {
}); });
it('handles SSO callback error in onMounted hook', async () => { it('handles SSO callback error in onMounted hook', async () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
const mockCode = 'mock_auth_code_error'; const mockCode = 'mock_auth_code_error';
const mockState = 'mock_session_state_error'; const mockState = 'mock_session_state_error';
@@ -235,7 +244,6 @@ describe('SsoButton.ce.vue', () => {
(fetch as Mock).mockRejectedValueOnce(fetchError); (fetch as Mock).mockRejectedValueOnce(fetchError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const wrapper = mount(SsoButton, { const wrapper = mount(SsoButton, {
props: { ssoenabled: true },
global: { global: {
stubs: { BrandButton: BrandButtonStub }, stubs: { BrandButton: BrandButtonStub },
}, },
@@ -261,4 +269,52 @@ describe('SsoButton.ce.vue', () => {
consoleErrorSpy.mockRestore(); consoleErrorSpy.mockRestore();
}); });
it('handles SSO callback when state does not match', async () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
const mockCode = 'mock_auth_code';
const mockState = 'mock_session_state_value';
const differentState = 'different_state_value';
mockLocation.search = `?code=${mockCode}&state=${mockState}`;
(sessionStorage.getItem as Mock).mockReturnValue(differentState);
const wrapper = mount(SsoButton, {
global: {
stubs: { BrandButton: BrandButtonStub },
},
});
await flushPromises();
// Should not make any fetch calls when state doesn't match
expect(fetch).not.toHaveBeenCalled();
expect(mockForm.requestSubmit).not.toHaveBeenCalled();
expect(wrapper.findComponent(BrandButtonStub).text()).toBe('Log In With Unraid.net');
});
it('handles SSO callback when no code is present', async () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
mockLocation.search = '?state=some_state';
(sessionStorage.getItem as Mock).mockReturnValue('some_state');
const wrapper = mount(SsoButton, {
global: {
stubs: { BrandButton: BrandButtonStub },
},
});
await flushPromises();
// Should not make any fetch calls when no code is present
expect(fetch).not.toHaveBeenCalled();
expect(mockForm.requestSubmit).not.toHaveBeenCalled();
expect(wrapper.findComponent(BrandButtonStub).text()).toBe('Log In With Unraid.net');
});
}); });

View File

@@ -1,22 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useQuery } from '@vue/apollo-composable';
import { SSO_ENABLED } from '~/store/account.fragment';
import { BrandButton } from '@unraid/ui'; import { BrandButton } from '@unraid/ui';
import { ACCOUNT } from '~/helpers/urls'; import { ACCOUNT } from '~/helpers/urls';
export interface Props {
ssoenabled?: boolean | string;
ssoEnabled?: boolean;
}
const props = defineProps<Props>();
type CurrentState = 'loading' | 'idle' | 'error'; type CurrentState = 'loading' | 'idle' | 'error';
const currentState = ref<CurrentState>('idle'); const currentState = ref<CurrentState>('idle');
const error = ref<string | null>(null); const error = ref<string | null>(null);
const { result } = useQuery(SSO_ENABLED);
const isSsoEnabled = computed<boolean>( const isSsoEnabled = computed<boolean>(
() => props['ssoenabled'] === true || props['ssoenabled'] === 'true' || props.ssoEnabled () => result.value?.isSSOEnabled ?? false
); );
const getInputFields = (): { const getInputFields = (): {

View File

@@ -45,6 +45,7 @@ type Documents = {
"\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n": typeof types.ListRCloneRemotesDocument, "\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n": typeof types.ListRCloneRemotesDocument,
"\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": typeof types.ConnectSignInDocument, "\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": typeof types.ConnectSignInDocument,
"\n mutation SignOut {\n connectSignOut\n }\n": typeof types.SignOutDocument, "\n mutation SignOut {\n connectSignOut\n }\n": typeof types.SignOutDocument,
"\n query IsSSOEnabled {\n isSSOEnabled\n }\n": typeof types.IsSsoEnabledDocument,
"\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": typeof types.PartialCloudFragmentDoc, "\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": typeof types.PartialCloudFragmentDoc,
"\n query cloudState {\n cloud {\n ...PartialCloud\n }\n }\n": typeof types.CloudStateDocument, "\n query cloudState {\n cloud {\n ...PartialCloud\n }\n }\n": typeof types.CloudStateDocument,
"\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n": typeof types.ServerStateDocument, "\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n": typeof types.ServerStateDocument,
@@ -82,6 +83,7 @@ const documents: Documents = {
"\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n": types.ListRCloneRemotesDocument, "\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n": types.ListRCloneRemotesDocument,
"\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": types.ConnectSignInDocument, "\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": types.ConnectSignInDocument,
"\n mutation SignOut {\n connectSignOut\n }\n": types.SignOutDocument, "\n mutation SignOut {\n connectSignOut\n }\n": types.SignOutDocument,
"\n query IsSSOEnabled {\n isSSOEnabled\n }\n": types.IsSsoEnabledDocument,
"\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": types.PartialCloudFragmentDoc, "\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": types.PartialCloudFragmentDoc,
"\n query cloudState {\n cloud {\n ...PartialCloud\n }\n }\n": types.CloudStateDocument, "\n query cloudState {\n cloud {\n ...PartialCloud\n }\n }\n": types.CloudStateDocument,
"\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n": types.ServerStateDocument, "\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n": types.ServerStateDocument,
@@ -226,6 +228,10 @@ export function graphql(source: "\n mutation ConnectSignIn($input: ConnectSignI
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n mutation SignOut {\n connectSignOut\n }\n"): (typeof documents)["\n mutation SignOut {\n connectSignOut\n }\n"]; export function graphql(source: "\n mutation SignOut {\n connectSignOut\n }\n"): (typeof documents)["\n mutation SignOut {\n connectSignOut\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query IsSSOEnabled {\n isSSOEnabled\n }\n"): (typeof documents)["\n query IsSSOEnabled {\n isSSOEnabled\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

View File

@@ -1264,6 +1264,7 @@ export type Query = {
docker: Docker; docker: Docker;
flash: Flash; flash: Flash;
info: Info; info: Info;
isSSOEnabled: Scalars['Boolean']['output'];
logFile: LogFileContent; logFile: LogFileContent;
logFiles: Array<LogFile>; logFiles: Array<LogFile>;
me: UserAccount; me: UserAccount;
@@ -2199,6 +2200,11 @@ export type SignOutMutationVariables = Exact<{ [key: string]: never; }>;
export type SignOutMutation = { __typename?: 'Mutation', connectSignOut: boolean }; export type SignOutMutation = { __typename?: 'Mutation', connectSignOut: boolean };
export type IsSsoEnabledQueryVariables = Exact<{ [key: string]: never; }>;
export type IsSsoEnabledQuery = { __typename?: 'Query', isSSOEnabled: boolean };
export type PartialCloudFragment = { __typename?: 'Cloud', error?: string | null, apiKey: { __typename?: 'ApiKeyResponse', valid: boolean, error?: string | null }, cloud: { __typename?: 'CloudResponse', status: string, error?: string | null }, minigraphql: { __typename?: 'MinigraphqlResponse', status: MinigraphStatus, error?: string | null }, relay?: { __typename?: 'RelayResponse', status: string, error?: string | null } | null } & { ' $fragmentName'?: 'PartialCloudFragment' }; export type PartialCloudFragment = { __typename?: 'Cloud', error?: string | null, apiKey: { __typename?: 'ApiKeyResponse', valid: boolean, error?: string | null }, cloud: { __typename?: 'CloudResponse', status: string, error?: string | null }, minigraphql: { __typename?: 'MinigraphqlResponse', status: MinigraphStatus, error?: string | null }, relay?: { __typename?: 'RelayResponse', status: string, error?: string | null } | null } & { ' $fragmentName'?: 'PartialCloudFragment' };
export type CloudStateQueryVariables = Exact<{ [key: string]: never; }>; export type CloudStateQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2251,6 +2257,7 @@ export const GetRCloneConfigFormDocument = {"kind":"Document","definitions":[{"k
export const ListRCloneRemotesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListRCloneRemotes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rclone"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remotes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"config"}}]}}]}}]}}]} as unknown as DocumentNode<ListRCloneRemotesQuery, ListRCloneRemotesQueryVariables>; export const ListRCloneRemotesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListRCloneRemotes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rclone"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remotes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"config"}}]}}]}}]}}]} as unknown as DocumentNode<ListRCloneRemotesQuery, ListRCloneRemotesQueryVariables>;
export const ConnectSignInDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ConnectSignIn"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectSignInInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignIn"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<ConnectSignInMutation, ConnectSignInMutationVariables>; export const ConnectSignInDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ConnectSignIn"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectSignInInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignIn"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<ConnectSignInMutation, ConnectSignInMutationVariables>;
export const SignOutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignOut"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignOut"}}]}}]} as unknown as DocumentNode<SignOutMutation, SignOutMutationVariables>; export const SignOutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignOut"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignOut"}}]}}]} as unknown as DocumentNode<SignOutMutation, SignOutMutationVariables>;
export const IsSsoEnabledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"IsSSOEnabled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isSSOEnabled"}}]}}]} as unknown as DocumentNode<IsSsoEnabledQuery, IsSsoEnabledQueryVariables>;
export const CloudStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"cloudState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialCloud"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<CloudStateQuery, CloudStateQueryVariables>; export const CloudStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"cloudState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialCloud"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<CloudStateQuery, CloudStateQueryVariables>;
export const ServerStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}}]} as unknown as DocumentNode<ServerStateQuery, ServerStateQueryVariables>; export const ServerStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}}]} as unknown as DocumentNode<ServerStateQuery, ServerStateQueryVariables>;
export const GetThemeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getTheme"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicTheme"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerImage"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerGradient"}},{"kind":"Field","name":{"kind":"Name","value":"headerBackgroundColor"}},{"kind":"Field","name":{"kind":"Name","value":"showHeaderDescription"}},{"kind":"Field","name":{"kind":"Name","value":"headerPrimaryTextColor"}},{"kind":"Field","name":{"kind":"Name","value":"headerSecondaryTextColor"}}]}}]}}]} as unknown as DocumentNode<GetThemeQuery, GetThemeQueryVariables>; export const GetThemeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getTheme"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicTheme"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerImage"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerGradient"}},{"kind":"Field","name":{"kind":"Name","value":"headerBackgroundColor"}},{"kind":"Field","name":{"kind":"Name","value":"showHeaderDescription"}},{"kind":"Field","name":{"kind":"Name","value":"headerPrimaryTextColor"}},{"kind":"Field","name":{"kind":"Name","value":"headerSecondaryTextColor"}}]}}]}}]} as unknown as DocumentNode<GetThemeQuery, GetThemeQueryVariables>;

View File

@@ -187,7 +187,7 @@ watch(
<div class="bg-background"> <div class="bg-background">
<hr class="border-black dark:border-white" /> <hr class="border-black dark:border-white" />
<h2 class="text-xl font-semibold font-mono">SSO Button Component</h2> <h2 class="text-xl font-semibold font-mono">SSO Button Component</h2>
<SsoButtonCe :ssoenabled="serverState.ssoEnabled" /> <SsoButtonCe />
</div> </div>
<div class="bg-background"> <div class="bg-background">
<hr class="border-black dark:border-white" /> <hr class="border-black dark:border-white" />

View File

@@ -56,7 +56,7 @@ onBeforeMount(() => {
<!-- uncomment to test modals <unraid-modals />--> <!-- uncomment to test modals <unraid-modals />-->
<hr class="border-black dark:border-white" /> <hr class="border-black dark:border-white" />
<h3 class="text-lg font-semibold font-mono">SSOSignInButtonCe</h3> <h3 class="text-lg font-semibold font-mono">SSOSignInButtonCe</h3>
<unraid-sso-button :ssoenabled="serverState.ssoEnabled" /> <unraid-sso-button />
<hr class="border-black dark:border-white" /> <hr class="border-black dark:border-white" />
<h3 class="text-lg font-semibold font-mono">ApiKeyManagerCe</h3> <h3 class="text-lg font-semibold font-mono">ApiKeyManagerCe</h3>
<unraid-api-key-manager /> <unraid-api-key-manager />

View File

@@ -11,3 +11,9 @@ export const CONNECT_SIGN_OUT = graphql(/* GraphQL */`
connectSignOut connectSignOut
} }
`); `);
export const SSO_ENABLED = graphql(/* GraphQL */`
query IsSSOEnabled {
isSSOEnabled
}
`);