Files
api/web/components/SsoButton.ce.vue
Eli Bosley 345e83bfb0 feat: upgrade nuxt-custom-elements (#1461)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added new modal dialogs and UI components, including activation steps,
OS update feedback, and expanded notification management.
* Introduced a plugin to configure internationalization, state
management, and Apollo client support in web components.
* Added a new Log Viewer page with a streamlined interface for viewing
logs.

* **Improvements**
* Centralized Pinia state management by consolidating all stores to use
a shared global Pinia instance.
* Simplified component templates by removing redundant
internationalization host wrappers.
* Enhanced ESLint configuration with stricter rules and global variable
declarations.
* Refined custom element build process to prevent jQuery conflicts and
optimize minification.
* Updated component imports and templates for consistent structure and
maintainability.
* Streamlined log viewer dropdowns using simplified select components
with improved formatting.
* Improved notification sidebar with filtering by importance and modular
components.
* Replaced legacy notification popups with new UI components and added
automatic root session creation for localhost requests.
* Updated OS version display and user profile UI with refined styling
and component usage.

* **Bug Fixes**
* Fixed component tag capitalization and improved type annotations
across components.

* **Chores**
* Updated development dependencies including ESLint plugins and build
tools.
* Removed deprecated log viewer patch class and cleaned up related test
fixtures.
  * Removed unused imports and simplified Apollo client setup.
* Cleaned up test mocks and removed obsolete i18n host component tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210730229632804

---------

Co-authored-by: Pujit Mehrotra <pujit@lime-technology.com>
Co-authored-by: Zack Spear <zackspear@users.noreply.github.com>
2025-07-08 10:05:39 -04:00

159 lines
4.6 KiB
Vue

<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';
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>(
() => 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 sessionState = getStateToken();
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 = 'Error fetching token';
reEnableFormOnError();
}
});
const buttonText = computed<string>(() => {
switch (currentState.value) {
case 'loading':
return 'Signing you in...';
case 'error':
return 'Error';
default:
return '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">or</p>
<p v-if="currentState === 'error'" class="text-red-500 text-center">{{ error }}</p>
<BrandButton
:disabled="currentState === 'loading'"
variant="outline"
class="rounded-none uppercase tracking-widest"
@click="navigateToExternalSSOUrl"
>{{ buttonText }}</BrandButton
>
</div>
</template>
</div>
</template>
<style lang="postcss">
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>