Applying Settings...
-
The API will restart after settings are applied.
diff --git a/web/components/SsoButton.ce.vue b/web/components/SsoButton.ce.vue
index 88eda5c99..232a10b37 100644
--- a/web/components/SsoButton.ce.vue
+++ b/web/components/SsoButton.ce.vue
@@ -1,176 +1,9 @@
-
-
-
-
{{ t('or') }}
-
{{ error }}
-
{{ buttonText }}
-
-
-
+
diff --git a/web/components/queries/oidc-providers.query.ts b/web/components/queries/oidc-providers.query.ts
new file mode 100644
index 000000000..f14dc6103
--- /dev/null
+++ b/web/components/queries/oidc-providers.query.ts
@@ -0,0 +1,28 @@
+import gql from 'graphql-tag';
+
+export const OIDC_PROVIDERS = gql`
+ query OidcProviders {
+ settings {
+ sso {
+ oidcProviders {
+ id
+ name
+ clientId
+ issuer
+ authorizationEndpoint
+ tokenEndpoint
+ jwksUri
+ scopes
+ authorizationRules {
+ claim
+ operator
+ value
+ }
+ authorizationRuleMode
+ buttonText
+ buttonIcon
+ }
+ }
+ }
+ }
+`;
diff --git a/web/components/queries/public-oidc-providers.query.ts b/web/components/queries/public-oidc-providers.query.ts
new file mode 100644
index 000000000..f932bb11d
--- /dev/null
+++ b/web/components/queries/public-oidc-providers.query.ts
@@ -0,0 +1,14 @@
+import { graphql } from '~/composables/gql/gql.js';
+
+export const PUBLIC_OIDC_PROVIDERS = graphql(/* GraphQL */`
+ query PublicOidcProviders {
+ publicOidcProviders {
+ id
+ name
+ buttonText
+ buttonIcon
+ buttonVariant
+ buttonStyle
+ }
+ }
+`);
diff --git a/web/components/sso/SsoButtons.vue b/web/components/sso/SsoButtons.vue
new file mode 100644
index 000000000..90d3670f8
--- /dev/null
+++ b/web/components/sso/SsoButtons.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
diff --git a/web/components/sso/SsoProviderButton.vue b/web/components/sso/SsoProviderButton.vue
new file mode 100644
index 000000000..a79f1dd2c
--- /dev/null
+++ b/web/components/sso/SsoProviderButton.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
diff --git a/web/components/sso/useSsoAuth.ts b/web/components/sso/useSsoAuth.ts
new file mode 100644
index 000000000..e68c01f08
--- /dev/null
+++ b/web/components/sso/useSsoAuth.ts
@@ -0,0 +1,146 @@
+import { ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+export type AuthState = 'loading' | 'idle' | 'error';
+
+export function useSsoAuth() {
+ const { t } = useI18n();
+ const currentState = ref
('idle');
+ const error = ref(null);
+
+ const getInputFields = (): {
+ form: HTMLFormElement;
+ passwordField: HTMLInputElement;
+ usernameField: HTMLInputElement;
+ } => {
+ const form = document.querySelector('form[action="/login"]') as HTMLFormElement;
+ const passwordField = document.querySelector('input[name=password]') as HTMLInputElement;
+ const usernameField = document.querySelector('input[name=username]') as HTMLInputElement;
+ if (!form || !passwordField || !usernameField) {
+ console.warn('Could not find form, username, or password field');
+ }
+ return { form, passwordField, usernameField };
+ };
+
+ const enterCallbackTokenIntoField = (token: string) => {
+ const { form, passwordField, usernameField } = getInputFields();
+ if (!form || !passwordField || !usernameField) {
+ console.warn('Could not find form, username, or password field');
+ return;
+ }
+
+ usernameField.value = 'root';
+ passwordField.value = token;
+ // Submit the form
+ form.requestSubmit();
+ };
+
+ const getStateToken = (): string | null => {
+ const state = sessionStorage.getItem('sso_state');
+ return state ?? null;
+ };
+
+ const generateStateToken = (): string => {
+ const array = new Uint8Array(32);
+ window.crypto.getRandomValues(array);
+ const state = Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
+ sessionStorage.setItem('sso_state', state);
+ return state;
+ };
+
+ const disableFormOnSubmit = () => {
+ const fields = getInputFields();
+ if (fields?.form) {
+ fields.form.style.display = 'none';
+ }
+ };
+
+ const reEnableFormOnError = () => {
+ const fields = getInputFields();
+ if (fields?.form) {
+ fields.form.style.display = 'block';
+ }
+ };
+
+ const navigateToProvider = (providerId: string) => {
+ // Generate state token for CSRF protection
+ const state = generateStateToken();
+
+ // Store provider ID separately since state must be alphanumeric only
+ sessionStorage.setItem('sso_state', state);
+ sessionStorage.setItem('sso_provider', providerId);
+
+ // Redirect to OIDC authorization endpoint with just the state token
+ const authUrl = `/graphql/api/auth/oidc/authorize/${encodeURIComponent(providerId)}?state=${encodeURIComponent(state)}`;
+ window.location.href = authUrl;
+ };
+
+ const handleOAuthCallback = async () => {
+ try {
+ // First check hash parameters (for token and error - keeps them out of server logs)
+ const hashParams = new URLSearchParams(window.location.hash.slice(1));
+ const hashToken = hashParams.get('token');
+ const hashError = hashParams.get('error');
+
+ // Then check query parameters (for OAuth code/state from provider redirects)
+ const search = new URLSearchParams(window.location.search);
+ const code = search.get('code') ?? '';
+ const state = search.get('state') ?? '';
+ const sessionState = getStateToken();
+
+ // Check for error in hash (preferred) or query params (fallback)
+ const errorParam = hashError || search.get('error') || '';
+ if (errorParam) {
+ currentState.value = 'error';
+ error.value = errorParam;
+
+ // Clean up the URL (both hash and query params)
+ window.history.replaceState({}, document.title, window.location.pathname);
+ return;
+ }
+
+ // Handle OAuth callback if we have a token in hash (from OIDC redirect)
+ const token = hashToken || search.get('token'); // Check hash first, query as fallback
+ if (token) {
+ currentState.value = 'loading';
+ disableFormOnSubmit();
+ enterCallbackTokenIntoField(token);
+
+ // Clean up the URL (both hash and query params)
+ window.history.replaceState({}, document.title, window.location.pathname);
+ return;
+ }
+
+ // Handle Unraid.net SSO callback (comes to /login with code and state)
+ if (code && state && window.location.pathname === '/login') {
+ currentState.value = 'loading';
+
+ // Redirect to our OIDC callback endpoint to exchange the code
+ const callbackUrl = `/graphql/api/auth/oidc/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
+ window.location.href = callbackUrl;
+ return;
+ }
+
+ // Error if we have mismatched state
+ if (code && state && state !== sessionState) {
+ currentState.value = 'error';
+ error.value = t('Invalid callback parameters');
+ }
+ } catch (err) {
+ console.error('Error fetching token', err);
+ currentState.value = 'error';
+ error.value = t('Error fetching token');
+ reEnableFormOnError();
+ }
+ };
+
+ onMounted(() => {
+ handleOAuthCallback();
+ });
+
+ return {
+ currentState,
+ error,
+ navigateToProvider,
+ };
+}
diff --git a/web/components/sso/useSsoProviders.ts b/web/components/sso/useSsoProviders.ts
new file mode 100644
index 000000000..e66ee0607
--- /dev/null
+++ b/web/components/sso/useSsoProviders.ts
@@ -0,0 +1,77 @@
+import { computed, ref, onMounted, onUnmounted } from 'vue';
+import { useQuery } from '@vue/apollo-composable';
+import { PUBLIC_OIDC_PROVIDERS } from '../queries/public-oidc-providers.query';
+
+export interface OidcProvider {
+ id: string;
+ name: string;
+ buttonText?: string | null;
+ buttonIcon?: string | null;
+ buttonVariant?: string | null;
+ buttonStyle?: string | null;
+}
+
+export function useSsoProviders() {
+ const pollInterval = ref(null);
+ const apiAvailable = ref(false);
+ const checkingApi = ref(true);
+
+ // Query for OIDC providers with polling
+ const { result: providersResult, refetch: refetchProviders } = useQuery(
+ PUBLIC_OIDC_PROVIDERS,
+ null,
+ {
+ fetchPolicy: 'network-only',
+ errorPolicy: 'all',
+ }
+ );
+
+ const oidcProviders = computed(() =>
+ providersResult.value?.publicOidcProviders ?? []
+ );
+
+ // Check if there are any providers configured
+ const hasProviders = computed(() => oidcProviders.value.length > 0);
+
+ // Check if API is available
+ const checkApiAvailability = async () => {
+ try {
+ await refetchProviders();
+ apiAvailable.value = true;
+ checkingApi.value = false;
+ // Stop polling once API is available
+ if (pollInterval.value) {
+ clearInterval(pollInterval.value);
+ pollInterval.value = null;
+ }
+ } catch {
+ apiAvailable.value = false;
+ // Continue polling if API is not available
+ }
+ };
+
+ // Start polling when component mounts
+ onMounted(() => {
+ checkApiAvailability();
+ // Poll every 2 seconds if API is not available
+ pollInterval.value = setInterval(() => {
+ if (!apiAvailable.value) {
+ checkApiAvailability();
+ }
+ }, 2000);
+ });
+
+ // Clean up polling on unmount
+ onUnmounted(() => {
+ if (pollInterval.value) {
+ clearInterval(pollInterval.value);
+ }
+ });
+
+ return {
+ oidcProviders,
+ hasProviders,
+ checkingApi,
+ apiAvailable,
+ };
+}
diff --git a/web/composables/gql/gql.ts b/web/composables/gql/gql.ts
index 0377519be..93b52cff7 100644
--- a/web/composables/gql/gql.ts
+++ b/web/composables/gql/gql.ts
@@ -44,6 +44,8 @@ type Documents = {
"\n mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {\n rclone {\n deleteRCloneRemote(input: $input)\n }\n }\n": typeof types.DeleteRCloneRemoteDocument,
"\n query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {\n rclone {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n": typeof types.GetRCloneConfigFormDocument,
"\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n": typeof types.ListRCloneRemotesDocument,
+ "\n query OidcProviders {\n settings {\n sso {\n oidcProviders {\n id\n name\n clientId\n issuer\n authorizationEndpoint\n tokenEndpoint\n jwksUri\n scopes\n authorizationRules {\n claim\n operator\n value\n }\n authorizationRuleMode\n buttonText\n buttonIcon\n }\n }\n }\n }\n": typeof types.OidcProvidersDocument,
+ "\n query PublicOidcProviders {\n publicOidcProviders {\n id\n name\n buttonText\n buttonIcon\n buttonVariant\n buttonStyle\n }\n }\n": typeof types.PublicOidcProvidersDocument,
"\n query serverInfo {\n info {\n os {\n hostname\n }\n }\n vars {\n comment\n }\n }\n": typeof types.ServerInfoDocument,
"\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": typeof types.ConnectSignInDocument,
"\n mutation SignOut {\n connectSignOut\n }\n": typeof types.SignOutDocument,
@@ -84,6 +86,8 @@ const documents: Documents = {
"\n mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {\n rclone {\n deleteRCloneRemote(input: $input)\n }\n }\n": types.DeleteRCloneRemoteDocument,
"\n query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {\n rclone {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n": types.GetRCloneConfigFormDocument,
"\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n": types.ListRCloneRemotesDocument,
+ "\n query OidcProviders {\n settings {\n sso {\n oidcProviders {\n id\n name\n clientId\n issuer\n authorizationEndpoint\n tokenEndpoint\n jwksUri\n scopes\n authorizationRules {\n claim\n operator\n value\n }\n authorizationRuleMode\n buttonText\n buttonIcon\n }\n }\n }\n }\n": types.OidcProvidersDocument,
+ "\n query PublicOidcProviders {\n publicOidcProviders {\n id\n name\n buttonText\n buttonIcon\n buttonVariant\n buttonStyle\n }\n }\n": types.PublicOidcProvidersDocument,
"\n query serverInfo {\n info {\n os {\n hostname\n }\n }\n vars {\n comment\n }\n }\n": types.ServerInfoDocument,
"\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": types.ConnectSignInDocument,
"\n mutation SignOut {\n connectSignOut\n }\n": types.SignOutDocument,
@@ -228,6 +232,14 @@ export function graphql(source: "\n query GetRCloneConfigForm($formOptions: RCl
* 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 ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n"): (typeof documents)["\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\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 OidcProviders {\n settings {\n sso {\n oidcProviders {\n id\n name\n clientId\n issuer\n authorizationEndpoint\n tokenEndpoint\n jwksUri\n scopes\n authorizationRules {\n claim\n operator\n value\n }\n authorizationRuleMode\n buttonText\n buttonIcon\n }\n }\n }\n }\n"): (typeof documents)["\n query OidcProviders {\n settings {\n sso {\n oidcProviders {\n id\n name\n clientId\n issuer\n authorizationEndpoint\n tokenEndpoint\n jwksUri\n scopes\n authorizationRules {\n claim\n operator\n value\n }\n authorizationRuleMode\n buttonText\n buttonIcon\n }\n }\n }\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 PublicOidcProviders {\n publicOidcProviders {\n id\n name\n buttonText\n buttonIcon\n buttonVariant\n buttonStyle\n }\n }\n"): (typeof documents)["\n query PublicOidcProviders {\n publicOidcProviders {\n id\n name\n buttonText\n buttonIcon\n buttonVariant\n buttonStyle\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
diff --git a/web/composables/gql/graphql.ts b/web/composables/gql/graphql.ts
index 9acc952f5..24c259650 100644
--- a/web/composables/gql/graphql.ts
+++ b/web/composables/gql/graphql.ts
@@ -385,6 +385,20 @@ export enum AuthPossession {
OWN_ANY = 'OWN_ANY'
}
+/** Operators for authorization rule matching */
+export enum AuthorizationOperator {
+ CONTAINS = 'CONTAINS',
+ ENDS_WITH = 'ENDS_WITH',
+ EQUALS = 'EQUALS',
+ STARTS_WITH = 'STARTS_WITH'
+}
+
+/** Mode for evaluating authorization rules - OR (any rule passes) or AND (all rules must pass) */
+export enum AuthorizationRuleMode {
+ AND = 'AND',
+ OR = 'OR'
+}
+
export type Baseboard = Node & {
__typename?: 'Baseboard';
assetTag?: Maybe;
@@ -669,6 +683,7 @@ export type Docker = Node & {
containers: Array;
id: Scalars['PrefixedID']['output'];
networks: Array;
+ organizer: ResolvedOrganizerV1;
};
@@ -936,23 +951,28 @@ export type Mutation = {
archiveNotification: Notification;
archiveNotifications: NotificationOverview;
array: ArrayMutations;
+ configureUps: Scalars['Boolean']['output'];
connectSignIn: Scalars['Boolean']['output'];
connectSignOut: Scalars['Boolean']['output'];
+ createDockerFolder: ResolvedOrganizerV1;
/** Creates a new notification record */
createNotification: Notification;
/** Deletes all archived notifications on server. */
deleteArchivedNotifications: NotificationOverview;
+ deleteDockerEntries: ResolvedOrganizerV1;
deleteNotification: NotificationOverview;
docker: DockerMutations;
enableDynamicRemoteAccess: Scalars['Boolean']['output'];
/** Initiates a flash drive backup using a configured remote. */
initiateFlashBackup: FlashBackupStatus;
+ moveDockerEntriesToFolder: ResolvedOrganizerV1;
parityCheck: ParityCheckMutations;
rclone: RCloneMutations;
/** Reads each notification to recompute & update the overview. */
recalculateOverview: NotificationOverview;
/** Remove one or more plugins from the API. Returns false if restart was triggered automatically, true if manual restart is required. */
removePlugin: Scalars['Boolean']['output'];
+ setDockerFolderChildren: ResolvedOrganizerV1;
setupRemoteAccess: Scalars['Boolean']['output'];
unarchiveAll: NotificationOverview;
unarchiveNotifications: NotificationOverview;
@@ -984,16 +1004,33 @@ export type MutationArchiveNotificationsArgs = {
};
+export type MutationConfigureUpsArgs = {
+ config: UpsConfigInput;
+};
+
+
export type MutationConnectSignInArgs = {
input: ConnectSignInInput;
};
+export type MutationCreateDockerFolderArgs = {
+ childrenIds?: InputMaybe>;
+ name: Scalars['String']['input'];
+ parentId?: InputMaybe;
+};
+
+
export type MutationCreateNotificationArgs = {
input: NotificationData;
};
+export type MutationDeleteDockerEntriesArgs = {
+ entryIds: Array;
+};
+
+
export type MutationDeleteNotificationArgs = {
id: Scalars['PrefixedID']['input'];
type: NotificationType;
@@ -1010,11 +1047,23 @@ export type MutationInitiateFlashBackupArgs = {
};
+export type MutationMoveDockerEntriesToFolderArgs = {
+ destinationFolderId: Scalars['String']['input'];
+ sourceEntryIds: Array;
+};
+
+
export type MutationRemovePluginArgs = {
input: PluginManagementInput;
};
+export type MutationSetDockerFolderChildrenArgs = {
+ childrenIds: Array;
+ folderId?: InputMaybe;
+};
+
+
export type MutationSetupRemoteAccessArgs = {
input: SetupRemoteAccessInput;
};
@@ -1122,6 +1171,72 @@ export type NotificationsListArgs = {
filter: NotificationFilter;
};
+export type OidcAuthorizationRule = {
+ __typename?: 'OidcAuthorizationRule';
+ /** The claim to check (e.g., email, sub, groups, hd) */
+ claim: Scalars['String']['output'];
+ /** The comparison operator */
+ operator: AuthorizationOperator;
+ /** The value(s) to match against */
+ value: Array;
+};
+
+export type OidcProvider = {
+ __typename?: 'OidcProvider';
+ /** OAuth2 authorization endpoint URL. If omitted, will be auto-discovered from issuer/.well-known/openid-configuration */
+ authorizationEndpoint?: Maybe;
+ /** Mode for evaluating authorization rules - OR (any rule passes) or AND (all rules must pass). Defaults to OR. */
+ authorizationRuleMode?: Maybe;
+ /** Flexible authorization rules based on claims */
+ authorizationRules?: Maybe>;
+ /** URL or base64 encoded icon for the login button */
+ buttonIcon?: Maybe;
+ /** Custom CSS styles for the button (e.g., "background: linear-gradient(to right, #4f46e5, #7c3aed); border-radius: 9999px;") */
+ buttonStyle?: Maybe;
+ /** Custom text for the login button */
+ buttonText?: Maybe;
+ /** Button variant style from Reka UI. See https://reka-ui.com/docs/components/button */
+ buttonVariant?: Maybe;
+ /** OAuth2 client ID registered with the provider */
+ clientId: Scalars['String']['output'];
+ /** OAuth2 client secret (if required by provider) */
+ clientSecret?: Maybe;
+ /** The unique identifier for the OIDC provider */
+ id: Scalars['PrefixedID']['output'];
+ /** OIDC issuer URL (e.g., https://accounts.google.com). Required for auto-discovery via /.well-known/openid-configuration */
+ issuer: Scalars['String']['output'];
+ /** JSON Web Key Set URI for token validation. If omitted, will be auto-discovered from issuer/.well-known/openid-configuration */
+ jwksUri?: Maybe;
+ /** Display name of the OIDC provider */
+ name: Scalars['String']['output'];
+ /** OAuth2 scopes to request (e.g., openid, profile, email) */
+ scopes: Array;
+ /** OAuth2 token endpoint URL. If omitted, will be auto-discovered from issuer/.well-known/openid-configuration */
+ tokenEndpoint?: Maybe;
+};
+
+export type OidcSessionValidation = {
+ __typename?: 'OidcSessionValidation';
+ username?: Maybe;
+ valid: Scalars['Boolean']['output'];
+};
+
+export type OrganizerContainerResource = {
+ __typename?: 'OrganizerContainerResource';
+ id: Scalars['String']['output'];
+ meta?: Maybe;
+ name: Scalars['String']['output'];
+ type: Scalars['String']['output'];
+};
+
+export type OrganizerResource = {
+ __typename?: 'OrganizerResource';
+ id: Scalars['String']['output'];
+ meta?: Maybe;
+ name: Scalars['String']['output'];
+ type: Scalars['String']['output'];
+};
+
export type Os = Node & {
__typename?: 'Os';
arch?: Maybe;
@@ -1235,6 +1350,16 @@ export type ProfileModel = Node & {
username: Scalars['String']['output'];
};
+export type PublicOidcProvider = {
+ __typename?: 'PublicOidcProvider';
+ buttonIcon?: Maybe;
+ buttonStyle?: Maybe;
+ buttonText?: Maybe;
+ buttonVariant?: Maybe;
+ id: Scalars['ID']['output'];
+ name: Scalars['String']['output'];
+};
+
export type PublicPartnerInfo = {
__typename?: 'PublicPartnerInfo';
/** Indicates if a partner logo exists */
@@ -1272,11 +1397,17 @@ export type Query = {
network: Network;
/** Get all notifications */
notifications: Notifications;
+ /** Get a specific OIDC provider by ID */
+ oidcProvider?: Maybe;
+ /** Get all configured OIDC providers (admin only) */
+ oidcProviders: Array;
online: Scalars['Boolean']['output'];
owner: Owner;
parityHistory: Array;
/** List all installed plugins with their metadata */
plugins: Array;
+ /** Get public OIDC provider information for login buttons */
+ publicOidcProviders: Array;
publicPartnerInfo?: Maybe;
publicTheme: Theme;
rclone: RCloneBackupSettings;
@@ -1287,6 +1418,11 @@ export type Query = {
services: Array;
settings: Settings;
shares: Array;
+ upsConfiguration: UpsConfiguration;
+ upsDeviceById?: Maybe;
+ upsDevices: Array;
+ /** Validate an OIDC session token (internal use for CLI validation) */
+ validateOidcSession: OidcSessionValidation;
vars: Vars;
/** Get information about all VMs on the system */
vms: Vms;
@@ -1309,6 +1445,21 @@ export type QueryLogFileArgs = {
startLine?: InputMaybe;
};
+
+export type QueryOidcProviderArgs = {
+ id: Scalars['PrefixedID']['input'];
+};
+
+
+export type QueryUpsDeviceByIdArgs = {
+ id: Scalars['String']['input'];
+};
+
+
+export type QueryValidateOidcSessionArgs = {
+ token: Scalars['String']['input'];
+};
+
export type RCloneBackupConfigForm = {
__typename?: 'RCloneBackupConfigForm';
dataSchema: Scalars['JSON']['output'];
@@ -1433,6 +1584,30 @@ export type RemoveRoleFromApiKeyInput = {
role: Role;
};
+export type ResolvedOrganizerEntry = OrganizerContainerResource | OrganizerResource | ResolvedOrganizerFolder;
+
+export type ResolvedOrganizerFolder = {
+ __typename?: 'ResolvedOrganizerFolder';
+ children: Array;
+ id: Scalars['String']['output'];
+ name: Scalars['String']['output'];
+ type: Scalars['String']['output'];
+};
+
+export type ResolvedOrganizerV1 = {
+ __typename?: 'ResolvedOrganizerV1';
+ version: Scalars['Float']['output'];
+ views: Array;
+};
+
+export type ResolvedOrganizerView = {
+ __typename?: 'ResolvedOrganizerView';
+ id: Scalars['String']['output'];
+ name: Scalars['String']['output'];
+ prefs?: Maybe;
+ root: ResolvedOrganizerEntry;
+};
+
/** Available resources for permissions */
export enum Resource {
ACTIVATION_CODE = 'ACTIVATION_CODE',
@@ -1508,6 +1683,8 @@ export type Settings = Node & {
/** The API setting values */
api: ApiConfig;
id: Scalars['PrefixedID']['output'];
+ /** SSO settings */
+ sso: SsoSettings;
/** A view of all settings */
unified: UnifiedSettings;
};
@@ -1556,6 +1733,13 @@ export type Share = Node & {
used?: Maybe;
};
+export type SsoSettings = Node & {
+ __typename?: 'SsoSettings';
+ id: Scalars['PrefixedID']['output'];
+ /** List of configured OIDC providers */
+ oidcProviders: Array;
+};
+
export type Subscription = {
__typename?: 'Subscription';
arraySubscription: UnraidArray;
@@ -1567,6 +1751,7 @@ export type Subscription = {
ownerSubscription: Owner;
parityHistorySubscription: ParityCheck;
serversSubscription: Server;
+ upsUpdates: UpsDevice;
};
@@ -1617,6 +1802,129 @@ export enum ThemeName {
WHITE = 'white'
}
+export type UpsBattery = {
+ __typename?: 'UPSBattery';
+ /** Battery charge level as a percentage (0-100). Unit: percent (%). Example: 100 means battery is fully charged */
+ chargeLevel: Scalars['Int']['output'];
+ /** Estimated runtime remaining on battery power. Unit: seconds. Example: 3600 means 1 hour of runtime remaining */
+ estimatedRuntime: Scalars['Int']['output'];
+ /** Battery health status. Possible values: 'Good', 'Replace', 'Unknown'. Indicates if the battery needs replacement */
+ health: Scalars['String']['output'];
+};
+
+/** UPS cable connection types */
+export enum UpsCableType {
+ CUSTOM = 'CUSTOM',
+ ETHER = 'ETHER',
+ SIMPLE = 'SIMPLE',
+ SMART = 'SMART',
+ USB = 'USB'
+}
+
+export type UpsConfigInput = {
+ /** Battery level percentage to initiate shutdown. Unit: percent (%) - Valid range: 0-100 */
+ batteryLevel?: InputMaybe;
+ /** Custom cable configuration (only used when upsCable is CUSTOM). Format depends on specific UPS model */
+ customUpsCable?: InputMaybe;
+ /** Device path or network address for UPS connection. Examples: '/dev/ttyUSB0' for USB, '192.168.1.100:3551' for network */
+ device?: InputMaybe;
+ /** Turn off UPS power after system shutdown. Useful for ensuring complete power cycle */
+ killUps?: InputMaybe;
+ /** Runtime left in minutes to initiate shutdown. Unit: minutes */
+ minutes?: InputMaybe;
+ /** Override UPS capacity for runtime calculations. Unit: watts (W). Leave unset to use UPS-reported capacity */
+ overrideUpsCapacity?: InputMaybe;
+ /** Enable or disable the UPS monitoring service */
+ service?: InputMaybe;
+ /** Time on battery before shutdown. Unit: seconds. Set to 0 to disable timeout-based shutdown */
+ timeout?: InputMaybe;
+ /** Type of cable connecting the UPS to the server */
+ upsCable?: InputMaybe;
+ /** UPS communication protocol */
+ upsType?: InputMaybe;
+};
+
+export type UpsConfiguration = {
+ __typename?: 'UPSConfiguration';
+ /** Battery level threshold for shutdown. Unit: percent (%). Example: 10 means shutdown when battery reaches 10%. System will shutdown when battery drops to this level */
+ batteryLevel?: Maybe;
+ /** Custom cable configuration string. Only used when upsCable is set to 'custom'. Format depends on specific UPS model */
+ customUpsCable?: Maybe;
+ /** Device path or network address for UPS connection. Examples: '/dev/ttyUSB0' for USB, '192.168.1.100:3551' for network. Depends on upsType setting */
+ device?: Maybe;
+ /** Kill UPS power after shutdown. Values: 'yes' or 'no'. If 'yes', tells UPS to cut power after system shutdown. Useful for ensuring complete power cycle */
+ killUps?: Maybe;
+ /** Runtime threshold for shutdown. Unit: minutes. Example: 5 means shutdown when 5 minutes runtime remaining. System will shutdown when estimated runtime drops below this */
+ minutes?: Maybe;
+ /** Override UPS model name. Used for display purposes. Leave unset to use UPS-reported model */
+ modelName?: Maybe;
+ /** Network server mode. Values: 'on' or 'off'. Enable to allow network clients to monitor this UPS */
+ netServer?: Maybe;
+ /** Network Information Server (NIS) IP address. Default: '0.0.0.0' (listen on all interfaces). IP address for apcupsd network information server */
+ nisIp?: Maybe;
+ /** Override UPS capacity for runtime calculations. Unit: volt-amperes (VA). Example: 1500 for a 1500VA UPS. Leave unset to use UPS-reported capacity */
+ overrideUpsCapacity?: Maybe;
+ /** UPS service state. Values: 'enable' or 'disable'. Controls whether the UPS monitoring service is running */
+ service?: Maybe;
+ /** Timeout for UPS communications. Unit: seconds. Example: 0 means no timeout. Time to wait for UPS response before considering it offline */
+ timeout?: Maybe;
+ /** Type of cable connecting the UPS to the server. Common values: 'usb', 'smart', 'ether', 'custom'. Determines communication protocol */
+ upsCable?: Maybe;
+ /** UPS name for network monitoring. Used to identify this UPS on the network. Example: 'SERVER_UPS' */
+ upsName?: Maybe;
+ /** UPS communication type. Common values: 'usb', 'net', 'snmp', 'dumb', 'pcnet', 'modbus'. Defines how the server communicates with the UPS */
+ upsType?: Maybe;
+};
+
+export type UpsDevice = {
+ __typename?: 'UPSDevice';
+ /** Battery-related information */
+ battery: UpsBattery;
+ /** Unique identifier for the UPS device. Usually based on the model name or a generated ID */
+ id: Scalars['ID']['output'];
+ /** UPS model name/number. Example: 'APC Back-UPS Pro 1500' */
+ model: Scalars['String']['output'];
+ /** Display name for the UPS device. Can be customized by the user */
+ name: Scalars['String']['output'];
+ /** Power-related information */
+ power: UpsPower;
+ /** Current operational status of the UPS. Common values: 'Online', 'On Battery', 'Low Battery', 'Replace Battery', 'Overload', 'Offline'. 'Online' means running on mains power, 'On Battery' means running on battery backup */
+ status: Scalars['String']['output'];
+};
+
+/** Kill UPS power after shutdown option */
+export enum UpsKillPower {
+ NO = 'NO',
+ YES = 'YES'
+}
+
+export type UpsPower = {
+ __typename?: 'UPSPower';
+ /** Input voltage from the wall outlet/mains power. Unit: volts (V). Example: 120.5 for typical US household voltage */
+ inputVoltage: Scalars['Float']['output'];
+ /** Current load on the UPS as a percentage of its capacity. Unit: percent (%). Example: 25 means UPS is loaded at 25% of its maximum capacity */
+ loadPercentage: Scalars['Int']['output'];
+ /** Output voltage being delivered to connected devices. Unit: volts (V). Example: 120.5 - should match input voltage when on mains power */
+ outputVoltage: Scalars['Float']['output'];
+};
+
+/** Service state for UPS daemon */
+export enum UpsServiceState {
+ DISABLE = 'DISABLE',
+ ENABLE = 'ENABLE'
+}
+
+/** UPS communication protocols */
+export enum UpsType {
+ APCSMART = 'APCSMART',
+ DUMB = 'DUMB',
+ MODBUS = 'MODBUS',
+ NET = 'NET',
+ PCNET = 'PCNET',
+ SNMP = 'SNMP',
+ USB = 'USB'
+}
+
export enum UrlType {
DEFAULT = 'DEFAULT',
LAN = 'LAN',
@@ -1668,6 +1976,8 @@ export type UpdateSettingsResponse = {
restartRequired: Scalars['Boolean']['output'];
/** The updated settings values */
values: Scalars['JSON']['output'];
+ /** Warning messages about configuration issues found during validation */
+ warnings?: Maybe>;
};
export type Uptime = {
@@ -2193,6 +2503,16 @@ export type ListRCloneRemotesQueryVariables = Exact<{ [key: string]: never; }>;
export type ListRCloneRemotesQuery = { __typename?: 'Query', rclone: { __typename?: 'RCloneBackupSettings', remotes: Array<{ __typename?: 'RCloneRemote', name: string, type: string, parameters: any, config: any }> } };
+export type OidcProvidersQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type OidcProvidersQuery = { __typename?: 'Query', settings: { __typename?: 'Settings', sso: { __typename?: 'SsoSettings', oidcProviders: Array<{ __typename?: 'OidcProvider', id: string, name: string, clientId: string, issuer: string, authorizationEndpoint?: string | null, tokenEndpoint?: string | null, jwksUri?: string | null, scopes: Array, authorizationRuleMode?: AuthorizationRuleMode | null, buttonText?: string | null, buttonIcon?: string | null, authorizationRules?: Array<{ __typename?: 'OidcAuthorizationRule', claim: string, operator: AuthorizationOperator, value: Array }> | null }> } } };
+
+export type PublicOidcProvidersQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type PublicOidcProvidersQuery = { __typename?: 'Query', publicOidcProviders: Array<{ __typename?: 'PublicOidcProvider', id: string, name: string, buttonText?: string | null, buttonIcon?: string | null, buttonVariant?: string | null, buttonStyle?: string | null }> };
+
export type ServerInfoQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2266,6 +2586,8 @@ export const CreateRCloneRemoteDocument = {"kind":"Document","definitions":[{"ki
export const DeleteRCloneRemoteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteRCloneRemote"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteRCloneRemoteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rclone"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteRCloneRemote"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode;
export const GetRCloneConfigFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRCloneConfigForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"formOptions"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"RCloneConfigFormInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rclone"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configForm"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"formOptions"},"value":{"kind":"Variable","name":{"kind":"Name","value":"formOptions"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}}]}}]}}]}}]} as unknown as DocumentNode;
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;
+export const OidcProvidersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"OidcProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sso"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"oidcProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"clientId"}},{"kind":"Field","name":{"kind":"Name","value":"issuer"}},{"kind":"Field","name":{"kind":"Name","value":"authorizationEndpoint"}},{"kind":"Field","name":{"kind":"Name","value":"tokenEndpoint"}},{"kind":"Field","name":{"kind":"Name","value":"jwksUri"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"authorizationRules"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"claim"}},{"kind":"Field","name":{"kind":"Name","value":"operator"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authorizationRuleMode"}},{"kind":"Field","name":{"kind":"Name","value":"buttonText"}},{"kind":"Field","name":{"kind":"Name","value":"buttonIcon"}}]}}]}}]}}]}}]} as unknown as DocumentNode;
+export const PublicOidcProvidersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PublicOidcProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicOidcProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"buttonText"}},{"kind":"Field","name":{"kind":"Name","value":"buttonIcon"}},{"kind":"Field","name":{"kind":"Name","value":"buttonVariant"}},{"kind":"Field","name":{"kind":"Name","value":"buttonStyle"}}]}}]}}]} as unknown as DocumentNode;
export const ServerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"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":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comment"}}]}}]}}]} as unknown as DocumentNode;
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;
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;
diff --git a/web/nuxt.config.ts b/web/nuxt.config.ts
index 65ae89723..7983e0a36 100644
--- a/web/nuxt.config.ts
+++ b/web/nuxt.config.ts
@@ -148,9 +148,23 @@ export default defineNuxtConfig({
dir: assetsDir,
},
],
+ devProxy: {
+ '/graphql': {
+ target: 'http://localhost:3001',
+ changeOrigin: true,
+ ws: true,
+ secure: false,
+ // Important: preserve the host header
+ headers: {
+ 'X-Forwarded-Host': 'localhost:3000',
+ 'X-Forwarded-Proto': 'http',
+ 'X-Forwarded-For': '127.0.0.1',
+ },
+ },
+ },
},
devServer: {
- port: 4321,
+ port: 3000,
},
css: ['@/assets/main.css'],
diff --git a/web/package.json b/web/package.json
index d22b01f27..d6b4c81a9 100644
--- a/web/package.json
+++ b/web/package.json
@@ -105,6 +105,7 @@
"@vue/apollo-composable": "4.2.2",
"@vueuse/components": "13.6.0",
"@vueuse/integrations": "13.6.0",
+ "ajv": "8.17.1",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"crypto-js": "4.2.0",
diff --git a/web/pages/login.vue b/web/pages/login.vue
index 4456a71fc..b7c9ebe44 100644
--- a/web/pages/login.vue
+++ b/web/pages/login.vue
@@ -1,7 +1,9 @@
-
+
+
+
+
+
+