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!
rclone: RCloneBackupSettings!
settings: Settings!
isSSOEnabled: Boolean!
"""List all installed plugins with their metadata"""
plugins: [Plugin!]!

View File

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

View File

@@ -6,6 +6,9 @@
*/
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";
/**
@@ -27,9 +30,9 @@ class ApiConfig
{
$output = [];
$exitCode = 0;
exec($command, $output, $exitCode);
return implode("\n", $output);
}
@@ -45,7 +48,7 @@ class ApiConfig
}
$apiUtilsScript = self::getApiUtilsScript();
if (!is_executable($apiUtilsScript)) {
return false;
}
@@ -53,10 +56,10 @@ class ApiConfig
$escapedScript = escapeshellarg($apiUtilsScript);
$escapedPlugin = escapeshellarg($pluginName);
$command = "$escapedScript is_api_plugin_enabled $escapedPlugin 2>/dev/null";
$exitCode = 0;
self::executeCommand($command, $exitCode);
return $exitCode === 0;
}
@@ -76,22 +79,43 @@ class ApiConfig
public static function getApiVersion()
{
$apiUtilsScript = self::getApiUtilsScript();
if (!is_executable($apiUtilsScript)) {
return 'unknown';
}
$escapedScript = escapeshellarg($apiUtilsScript);
$command = "$escapedScript get_api_version 2>/dev/null";
$exitCode = 0;
$output = self::executeCommand($command, $exitCode);
if ($exitCode !== 0) {
return 'unknown';
}
$version = trim($output);
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->registeredTime = $connectConfig['regWizTime'] ?? '';
$this->username = $connectConfig['username'] ?? '';
$this->ssoEnabled = !empty($connectConfig['ssoSubIds'] ?? '');
$this->ssoEnabled = ApiUserConfig::isSSOEnabled();
}
private function getConnectKnownOrigins()

View File

@@ -2,6 +2,7 @@
* SsoButton Component Test Coverage
*/
import { useQuery } from '@vue/apollo-composable';
import { flushPromises, mount } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -15,6 +16,11 @@ const BrandButtonStub = {
props: ['disabled', 'variant', 'class'],
};
// Mock the GraphQL composable
vi.mock('@vue/apollo-composable', () => ({
useQuery: vi.fn(),
}));
vi.mock('~/helpers/urls', () => ({
ACCOUNT: 'http://mock-account-url.net',
}));
@@ -60,10 +66,13 @@ const mockUsernameField = { value: '' };
describe('SsoButton.ce.vue', () => {
let querySelectorSpy: MockInstance;
let mockUseQuery: Mock;
beforeEach(() => {
beforeEach(async () => {
vi.restoreAllMocks();
mockUseQuery = useQuery as Mock;
(sessionStorage.getItem as Mock).mockReturnValue(null);
(sessionStorage.setItem as Mock).mockClear();
mockForm.requestSubmit.mockClear();
@@ -73,6 +82,7 @@ describe('SsoButton.ce.vue', () => {
mockLocation.search = '';
mockLocation.href = '';
(fetch as Mock).mockClear();
mockUseQuery.mockClear();
// Spy on document.querySelector and provide mock implementation
querySelectorSpy = vi.spyOn(document, 'querySelector');
@@ -93,9 +103,12 @@ describe('SsoButton.ce.vue', () => {
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, {
props: { ssoenabled: true },
global: {
stubs: { BrandButton: BrandButtonStub },
},
@@ -105,33 +118,12 @@ describe('SsoButton.ce.vue', () => {
expect(wrapper.text()).toContain('Log In With Unraid.net');
});
it('renders the button when ssoenabled prop is true (string)', () => {
const wrapper = mount(SsoButton, {
props: { ssoenabled: 'true' },
global: {
stubs: { BrandButton: BrandButtonStub },
},
it('does not render the button when SSO is disabled via GraphQL', () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: false } },
});
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, {
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: {
stubs: { BrandButton: BrandButtonStub },
},
@@ -140,9 +132,12 @@ describe('SsoButton.ce.vue', () => {
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, {
props: { ssoEnabled: false },
global: {
stubs: { BrandButton: BrandButtonStub },
},
@@ -151,7 +146,11 @@ describe('SsoButton.ce.vue', () => {
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, {
global: {
stubs: { BrandButton: BrandButtonStub },
@@ -162,8 +161,11 @@ describe('SsoButton.ce.vue', () => {
});
it('navigates to the external SSO URL on button click', async () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
const wrapper = mount(SsoButton, {
props: { ssoenabled: true },
global: {
stubs: { BrandButton: BrandButtonStub },
},
@@ -186,6 +188,10 @@ describe('SsoButton.ce.vue', () => {
});
it('handles SSO callback in onMounted hook successfully', async () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
const mockCode = 'mock_auth_code';
const mockState = 'mock_session_state_value';
const mockAccessToken = 'mock_access_token_123';
@@ -199,7 +205,6 @@ describe('SsoButton.ce.vue', () => {
// Mount the component so that onMounted hook is called
mount(SsoButton, {
props: { ssoenabled: true },
global: {
stubs: { BrandButton: BrandButtonStub },
},
@@ -225,6 +230,10 @@ describe('SsoButton.ce.vue', () => {
});
it('handles SSO callback error in onMounted hook', async () => {
mockUseQuery.mockReturnValue({
result: { value: { isSSOEnabled: true } },
});
const mockCode = 'mock_auth_code_error';
const mockState = 'mock_session_state_error';
@@ -235,7 +244,6 @@ describe('SsoButton.ce.vue', () => {
(fetch as Mock).mockRejectedValueOnce(fetchError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const wrapper = mount(SsoButton, {
props: { ssoenabled: true },
global: {
stubs: { BrandButton: BrandButtonStub },
},
@@ -261,4 +269,52 @@ describe('SsoButton.ce.vue', () => {
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">
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 { ACCOUNT } from '~/helpers/urls';
export interface Props {
ssoenabled?: boolean | string;
ssoEnabled?: boolean;
}
const props = defineProps<Props>();
type CurrentState = 'loading' | 'idle' | 'error';
const currentState = ref<CurrentState>('idle');
const error = ref<string | null>(null);
const { result } = useQuery(SSO_ENABLED);
const isSsoEnabled = computed<boolean>(
() => props['ssoenabled'] === true || props['ssoenabled'] === 'true' || props.ssoEnabled
() => result.value?.isSSOEnabled ?? false
);
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 mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": typeof types.ConnectSignInDocument,
"\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 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,
@@ -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 mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": types.ConnectSignInDocument,
"\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 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,
@@ -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.
*/
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.
*/

View File

@@ -1264,6 +1264,7 @@ export type Query = {
docker: Docker;
flash: Flash;
info: Info;
isSSOEnabled: Scalars['Boolean']['output'];
logFile: LogFileContent;
logFiles: Array<LogFile>;
me: UserAccount;
@@ -2199,6 +2200,11 @@ export type SignOutMutationVariables = Exact<{ [key: string]: never; }>;
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 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 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 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 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>;

View File

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

View File

@@ -56,7 +56,7 @@ onBeforeMount(() => {
<!-- uncomment to test modals <unraid-modals />-->
<hr class="border-black dark:border-white" />
<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" />
<h3 class="text-lg font-semibold font-mono">ApiKeyManagerCe</h3>
<unraid-api-key-manager />

View File

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