mirror of
https://github.com/unraid/api.git
synced 2026-01-05 16:09:49 -06:00
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added developer CLI tools for toggling GraphQL sandbox and modal testing utilities. * Introduced a "Show Activation Modal" developer component for UI testing. * Added system initial setup detection and related GraphQL queries. * Enhanced login and welcome pages with dynamic server info and initial setup state. * Improved SSO button with internationalization and error handling. * Added internal CLI admin API key management service and internal GraphQL client service. * Introduced comprehensive API report generation service for system and service status. * Added CLI commands and GraphQL mutations/queries for plugin and SSO user management. * Added new modal target components and improved teleport target detection. * **Enhancements** * Refined modal dialog targeting and teleportation for flexible UI placement. * Updated modal components and stores for improved activation/welcome modal control. * Improved plugin and SSO user management via CLI through GraphQL API. * Refactored partner logo components to use props instead of store dependencies. * Enhanced styling and accessibility for buttons and modals. * Streamlined Tailwind CSS integration with shared styles and updated theme variables. * Improved GraphQL module configuration to avoid directive conflicts in tests. * Adjusted Vite config for better dependency handling in test mode. * Improved error handling and logging in CLI commands and services. * Reordered imports and refined component class bindings for UI consistency. * **Bug Fixes** * Resolved issues with duplicate script tags and component registration in the web UI. * Fixed modal close button visibility and activation modal state handling. * Added error handling and logging improvements across CLI commands and services. * Fixed newline issues in last-download-time fixture files. * **Chores** * Added and updated numerous tests for CLI commands, services, and UI components. * Updated translation files and localization resources for new UI messages. * Adjusted environment, configuration, and dependency files for improved development and test workflows. * Cleaned up unused imports and mocks in tests. * Reorganized exports and barrel files in shared and UI modules. * Added integration and dependency resolution tests for core modules. * **Removals & Refactoring** * Removed legacy Redux state management, configuration, and UPnP logic from the backend. * Eliminated deprecated GraphQL subscriptions and client code related to registration and mothership. * Removed direct store manipulation and replaced with service-based approaches in CLI commands. * Deleted unused or redundant test files and configuration listeners. * Refactored SSO user service to consolidate add/remove operations into a single update method. * Simplified API key services with new methods for automatic key management. * Replaced direct plugin and SSO user service calls with GraphQL client interactions in CLI commands. * Removed complex theme fallback and dark mode CSS rules, replacing with streamlined static theme variables. * Cleaned up Tailwind CSS configuration and removed deprecated local styles. * Removed multiple internal utility files and replaced with simplified or centralized implementations. * Removed deprecated local configuration and synchronization files and listeners. * Removed UPnP helper functions and job management classes. * Refactored server resolver to dynamically construct local server data internally. * Removed CORS handler and replaced with simplified or externalized logic. * Removed store synchronization and registration event pubsub handling. * Removed GraphQL client creation utilities for internal API communication. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
208 lines
6.4 KiB
Vue
208 lines
6.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { useQuery } from '@vue/apollo-composable';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { SSO_ENABLED } from '~/store/account.fragment';
|
|
|
|
import { BrandButton } from '@unraid/ui';
|
|
import { ACCOUNT } from '~/helpers/urls';
|
|
|
|
type CurrentState = 'loading' | 'idle' | 'error';
|
|
|
|
const { t } = useI18n();
|
|
const currentState = ref<CurrentState>('idle');
|
|
const error = ref<string | null>(null);
|
|
|
|
const { result } = useQuery(SSO_ENABLED);
|
|
|
|
const isSsoEnabled = computed<boolean>(
|
|
() => result.value?.isSSOEnabled ?? false
|
|
);
|
|
|
|
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 (!passwordField || !usernameField || !form) {
|
|
console.warn('Could not find form, username, or password field');
|
|
} else {
|
|
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 { form } = getInputFields();
|
|
if (form) {
|
|
form.style.display = 'none';
|
|
}
|
|
};
|
|
|
|
const reEnableFormOnError = () => {
|
|
const { form } = getInputFields();
|
|
if (form) {
|
|
form.style.display = 'block';
|
|
}
|
|
};
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const search = new URLSearchParams(window.location.search);
|
|
const code = search.get('code') ?? '';
|
|
const state = search.get('state') ?? '';
|
|
const ssoError = search.get('sso_error') ?? '';
|
|
const sessionState = getStateToken();
|
|
|
|
// Check for SSO error parameter
|
|
if (ssoError) {
|
|
currentState.value = 'error';
|
|
// Map common SSO errors to user-friendly messages with translation support
|
|
const errorMap: Record<string, string> = {
|
|
'invalid_credentials': t('Invalid Unraid.net credentials'),
|
|
'user_not_authorized': t('This Unraid.net account is not authorized to access this server'),
|
|
'sso_disabled': t('SSO login is not enabled on this server'),
|
|
'token_expired': t('Login session expired. Please try again'),
|
|
'network_error': t('Network error. Please check your connection'),
|
|
};
|
|
error.value = errorMap[ssoError] || t('SSO login failed. Please try again');
|
|
// Clean up the URL
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete('sso_error');
|
|
window.history.replaceState({}, document.title, url.pathname + url.search);
|
|
return;
|
|
}
|
|
|
|
if (code && state === sessionState) {
|
|
disableFormOnSubmit();
|
|
currentState.value = 'loading';
|
|
const token = await fetch(new URL('/api/oauth2/token', ACCOUNT), {
|
|
method: 'POST',
|
|
body: new URLSearchParams({
|
|
code,
|
|
client_id: 'CONNECT_SERVER_SSO',
|
|
grant_type: 'authorization_code',
|
|
}),
|
|
});
|
|
if (token.ok) {
|
|
const tokenBody = await token.json();
|
|
if (!tokenBody.access_token) {
|
|
throw new Error('Token body did not contain access_token');
|
|
}
|
|
enterCallbackTokenIntoField(tokenBody.access_token);
|
|
if (window.location.search) {
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
}
|
|
} else {
|
|
throw new Error('Failed to fetch token');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching token', err);
|
|
|
|
currentState.value = 'error';
|
|
error.value = t('Error fetching token');
|
|
reEnableFormOnError();
|
|
}
|
|
});
|
|
|
|
const buttonText = computed<string>(() => {
|
|
switch (currentState.value) {
|
|
case 'loading':
|
|
return t('Logging in...');
|
|
case 'error':
|
|
return t('Try Again');
|
|
default:
|
|
return t('Log In With Unraid.net');
|
|
}
|
|
});
|
|
|
|
const navigateToExternalSSOUrl = () => {
|
|
const url = new URL('sso', ACCOUNT);
|
|
const callbackUrlLogin = new URL('login', window.location.origin);
|
|
const state = generateStateToken();
|
|
|
|
url.searchParams.append('callbackUrl', callbackUrlLogin.toString());
|
|
url.searchParams.append('state', state);
|
|
|
|
window.location.href = url.toString();
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<template v-if="isSsoEnabled">
|
|
<div class="w-full flex flex-col gap-1 my-1">
|
|
<p v-if="currentState === 'idle' || currentState === 'error'" class="text-center">{{ t('or') }}</p>
|
|
<p v-if="currentState === 'error'" class="text-red-500 text-center">{{ error }}</p>
|
|
<BrandButton
|
|
:disabled="currentState === 'loading'"
|
|
variant="outline-primary"
|
|
class="sso-button"
|
|
@click="navigateToExternalSSOUrl"
|
|
>{{ buttonText }}</BrandButton
|
|
>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
/* Font size overrides for 16px base (standard Tailwind sizing) */
|
|
:host {
|
|
/* Text sizes - standard Tailwind rem values */
|
|
--text-xs: 0.75rem; /* 12px */
|
|
--text-sm: 0.875rem; /* 14px */
|
|
--text-base: 1rem; /* 16px */
|
|
--text-lg: 1.125rem; /* 18px */
|
|
--text-xl: 1.25rem; /* 20px */
|
|
--text-2xl: 1.5rem; /* 24px */
|
|
--text-3xl: 1.875rem; /* 30px */
|
|
--text-4xl: 2.25rem; /* 36px */
|
|
--text-5xl: 3rem; /* 48px */
|
|
--text-6xl: 3.75rem; /* 60px */
|
|
--text-7xl: 4.5rem; /* 72px */
|
|
--text-8xl: 6rem; /* 96px */
|
|
--text-9xl: 8rem; /* 128px */
|
|
|
|
/* Spacing - standard Tailwind value */
|
|
--spacing: 0.25rem; /* 4px */
|
|
}
|
|
|
|
.sso-button {
|
|
font-size: 0.875rem !important;
|
|
font-weight: 600 !important;
|
|
line-height: 1 !important;
|
|
text-transform: uppercase !important;
|
|
letter-spacing: 2px !important;
|
|
padding: 0.75rem 1.5rem !important;
|
|
border-radius: 0.125rem !important;
|
|
}
|
|
</style>
|