feat: unraid single sign on with account app

This commit is contained in:
Eli Bosley
2025-01-20 12:04:26 -05:00
parent 2d3892deb8
commit 2b25537e26
16 changed files with 422 additions and 175 deletions

View File

@@ -9,14 +9,14 @@ import { cliLogger, internalLogger } from '@app/core/log';
import { CliModule } from '@app/unraid-api/cli/cli.module'; import { CliModule } from '@app/unraid-api/cli/cli.module';
try { try {
const shellToUse = execSync('which bash'); const shellToUse = execSync('which bash').toString().trim();
await CommandFactory.run(CliModule, { await CommandFactory.run(CliModule, {
cliName: 'unraid-api', cliName: 'unraid-api',
logger: false, logger: false,
completion: { completion: {
fig: true, fig: true,
cmd: 'unraid-api', cmd: 'unraid-api',
nativeShell: { executablePath: shellToUse.toString('utf-8') }, nativeShell: { executablePath: shellToUse },
}, },
}); });
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,18 @@
import { existsSync, renameSync, unlinkSync } from 'node:fs';
export const removeSso = () => {
const path = '/usr/local/emhttp/plugins/dynamix/include/.login.php';
const backupPath = path + '.bak';
// Remove the SSO login inject file if it exists
if (existsSync(path)) {
unlinkSync(path);
}
// Move the backup file to the original location
if (existsSync(backupPath)) {
renameSync(backupPath, path);
}
console.log('Restored .login php file');
};

62
api/src/core/sso/sso-setup.ts Executable file
View File

@@ -0,0 +1,62 @@
import { existsSync } from 'node:fs';
import { copyFile, readFile, rename, unlink, writeFile } from 'node:fs/promises';
export const setupSso = async () => {
const path = '/usr/local/emhttp/plugins/dynamix/include/.login.php';
// Define the new PHP function to insert
const newFunction = `
function verifyUsernamePasswordAndSSO(string $username, string $password): bool {
if ($username != "root") return false;
$output = exec("/usr/bin/getent shadow $username");
if ($output === false) return false;
$credentials = explode(":", $output);
$valid = password_verify($password, $credentials[1]);
if ($valid) {
return true;
}
// We may have an SSO token, attempt validation
if (strlen($password) > 800) {
$safePassword = escapeshellarg($password);
$response = exec("/usr/local/bin/unraid-api sso validate-token $safePassword", $output, $code);
my_logger("SSO Login Response: $response");
if ($code === 0 && $response && strpos($response, '"valid":true') !== false) {
return true;
}
}
return false;
}`;
const tagToInject = '<?php include "$docroot/plugins/dynamix.my.servers/include/sso-login.php"; ?>';
// Backup the original file if exists
if (existsSync(path + '.bak')) {
await copyFile(path + '.bak', path);
await unlink(path + '.bak');
}
// Read the file content
let fileContent = await readFile(path, 'utf-8');
// Backup the original content
await writeFile(path + '.bak', fileContent);
// Add new function after the opening PHP tag (<?php)
fileContent = fileContent.replace(/<\?php\s*(\r?\n|\r)*/, `<?php\n\n${newFunction}\n`);
// Replace the old function call with the new function name
const functionCallPattern = /!verifyUsernamePassword\(\$username, \$password\)/g;
fileContent = fileContent.replace(
functionCallPattern,
'!verifyUsernamePasswordAndSSO($username, $password)'
);
// Inject the PHP include tag before the closing </body> tag
fileContent = fileContent.replace(/<\/body>/i, `${tagToInject}\n</body>`);
// Write the updated content back to the file
await writeFile(path, fileContent);
console.log('Function replaced successfully.');
};

View File

@@ -14,6 +14,7 @@ import { WebSocket } from 'ws';
import { logger } from '@app/core/log'; import { logger } from '@app/core/log';
import { setupLogRotation } from '@app/core/logrotate/setup-logrotate'; import { setupLogRotation } from '@app/core/logrotate/setup-logrotate';
import { setupSso } from '@app/core/sso/sso-setup';
import { fileExistsSync } from '@app/core/utils/files/file-exists'; import { fileExistsSync } from '@app/core/utils/files/file-exists';
import { environment, PORT } from '@app/environment'; import { environment, PORT } from '@app/environment';
import * as envVars from '@app/environment'; import * as envVars from '@app/environment';
@@ -97,6 +98,11 @@ try {
startMiddlewareListeners(); startMiddlewareListeners();
// If the config contains SSO IDs, enable SSO
if (store.getState().config.remote.ssoSubIds) {
await setupSso();
}
// On process exit stop HTTP server // On process exit stop HTTP server
exitHook((signal) => { exitHook((signal) => {
console.log('exithook', signal); console.log('exithook', signal);

View File

@@ -1,17 +1,18 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigCommand } from '@app/unraid-api/cli/config.command';
import { KeyCommand } from '@app/unraid-api/cli/key.command'; import { KeyCommand } from '@app/unraid-api/cli/key.command';
import { LogService } from '@app/unraid-api/cli/log.service'; import { LogService } from '@app/unraid-api/cli/log.service';
import { LogsCommand } from '@app/unraid-api/cli/logs.command';
import { ReportCommand } from '@app/unraid-api/cli/report.command'; import { ReportCommand } from '@app/unraid-api/cli/report.command';
import { RestartCommand } from '@app/unraid-api/cli/restart.command'; import { RestartCommand } from '@app/unraid-api/cli/restart.command';
import { SSOCommand } from '@app/unraid-api/cli/sso.command';
import { StartCommand } from '@app/unraid-api/cli/start.command'; import { StartCommand } from '@app/unraid-api/cli/start.command';
import { StatusCommand } from '@app/unraid-api/cli/status.command';
import { StopCommand } from '@app/unraid-api/cli/stop.command'; import { StopCommand } from '@app/unraid-api/cli/stop.command';
import { SwitchEnvCommand } from '@app/unraid-api/cli/switch-env.command'; import { SwitchEnvCommand } from '@app/unraid-api/cli/switch-env.command';
import { VersionCommand } from '@app/unraid-api/cli/version.command'; import { VersionCommand } from '@app/unraid-api/cli/version.command';
import { StatusCommand } from '@app/unraid-api/cli/status.command';
import { ValidateTokenCommand } from '@app/unraid-api/cli/validate-token.command'; import { ValidateTokenCommand } from '@app/unraid-api/cli/validate-token.command';
import { LogsCommand } from '@app/unraid-api/cli/logs.command';
import { ConfigCommand } from '@app/unraid-api/cli/config.command';
@Module({ @Module({
providers: [ providers: [
@@ -24,9 +25,10 @@ import { ConfigCommand } from '@app/unraid-api/cli/config.command';
SwitchEnvCommand, SwitchEnvCommand,
VersionCommand, VersionCommand,
StatusCommand, StatusCommand,
SSOCommand,
ValidateTokenCommand, ValidateTokenCommand,
LogsCommand, LogsCommand,
ConfigCommand ConfigCommand,
], ],
}) })
export class CliModule {} export class CliModule {}

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { LogService } from '@app/unraid-api/cli/log.service';
import { ValidateTokenCommand } from '@app/unraid-api/cli/validate-token.command';
@Injectable()
@Command({
name: 'sso',
description: 'Main Command to Configure / Validate SSO Tokens',
subCommands: [ValidateTokenCommand],
})
export class SSOCommand extends CommandRunner {
constructor(private readonly logger: LogService) {
super();
}
async run(): Promise<void> {
this.logger.info('Please provide a subcommand or use --help for more information');
}
}

View File

@@ -1,14 +1,15 @@
import type { JWTPayload } from 'jose'; import type { JWTPayload } from 'jose';
import { createLocalJWKSet, createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'; import { createLocalJWKSet, createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose';
import { Command, CommandRunner } from 'nest-commander'; import { CommandRunner, SubCommand } from 'nest-commander';
import { JWKS_LOCAL_PAYLOAD, JWKS_REMOTE_LINK } from '@app/consts'; import { JWKS_LOCAL_PAYLOAD, JWKS_REMOTE_LINK } from '@app/consts';
import { store } from '@app/store'; import { store } from '@app/store';
import { loadConfigFile } from '@app/store/modules/config'; import { loadConfigFile } from '@app/store/modules/config';
import { LogService } from '@app/unraid-api/cli/log.service'; import { LogService } from '@app/unraid-api/cli/log.service';
@Command({ @SubCommand({
name: 'validate-token', name: 'validate-token',
aliases: ['validate', 'v'],
description: 'Returns JSON: { error: string | null, valid: boolean }', description: 'Returns JSON: { error: string | null, valid: boolean }',
arguments: '<token>', arguments: '<token>',
}) })
@@ -33,11 +34,13 @@ export class ValidateTokenCommand extends CommandRunner {
async run(passedParams: string[]): Promise<void> { async run(passedParams: string[]): Promise<void> {
if (passedParams.length !== 1) { if (passedParams.length !== 1) {
this.logger.error('Please pass token argument only'); this.createErrorAndExit('Please pass token argument only');
process.exit(1);
} }
const token = passedParams[0]; const token = passedParams[0];
if (typeof token !== 'string' || token.trim() === '') {
this.createErrorAndExit('Invalid token provided');
}
let caughtError: null | unknown = null; let caughtError: null | unknown = null;
let tokenPayload: null | JWTPayload = null; let tokenPayload: null | JWTPayload = null;

View File

@@ -0,0 +1,22 @@
<?
/**
* Caveats to get the SSO login button to display
*
* /usr/local/emhttp/auth-request.php must be updated to include the exact URLs of anything that is being loaded.
* Otherwise, the request for the asset will be blocked and redirected to /login.
*
* The modification of these files should be done via the plugin's install script.
*/
require_once("$docroot/plugins/dynamix.my.servers/include/state.php");
require_once("$docroot/plugins/dynamix.my.servers/include/web-components-extractor.php");
$serverState = new ServerState();
$wcExtractor = new WebComponentsExtractor();
echo $wcExtractor->getScriptTagHtml();
?>
<unraid-i18n-host>
<unraid-sso-button server="<?= $serverState->getServerStateJsonForHtmlAttr() ?>"></unraid-sso-button>
</unraid-i18n-host>

View File

@@ -53,6 +53,10 @@ class ServerState
"nokeyserver" => 'NO_KEY_SERVER', "nokeyserver" => 'NO_KEY_SERVER',
"withdrawn" => 'WITHDRAWN', "withdrawn" => 'WITHDRAWN',
]; ];
/**
* SSO Sub IDs from the my servers config file.
*/
private $ssoSubIds = '';
private $osVersion; private $osVersion;
private $osVersionBranch; private $osVersionBranch;
private $rebootDetails; private $rebootDetails;
@@ -193,6 +197,7 @@ class ServerState
$this->registered = !empty($this->myServersFlashCfg['remote']['apikey']) && $this->connectPluginInstalled; $this->registered = !empty($this->myServersFlashCfg['remote']['apikey']) && $this->connectPluginInstalled;
$this->registeredTime = $this->myServersFlashCfg['remote']['regWizTime'] ?? ''; $this->registeredTime = $this->myServersFlashCfg['remote']['regWizTime'] ?? '';
$this->username = $this->myServersFlashCfg['remote']['username'] ?? ''; $this->username = $this->myServersFlashCfg['remote']['username'] ?? '';
$this->ssoSubIds = $this->myServersFlashCfg['remote']['ssoSubIds'] ?? '';
} }
private function getConnectKnownOrigins() { private function getConnectKnownOrigins() {
@@ -321,6 +326,7 @@ class ServerState
"uptime" => 1000 * (time() - round(strtok(exec("cat /proc/uptime"), ' '))), "uptime" => 1000 * (time() - round(strtok(exec("cat /proc/uptime"), ' '))),
"username" => $this->username, "username" => $this->username,
"wanFQDN" => @$this->nginxCfg['NGINX_WANFQDN'] ?? '', "wanFQDN" => @$this->nginxCfg['NGINX_WANFQDN'] ?? '',
"ssoSubIds" => $this->ssoSubIds
]; ];
if ($this->combinedKnownOrigins) { if ($this->combinedKnownOrigins) {

View File

@@ -1,3 +1,4 @@
;
// import dayjs, { extend } from 'dayjs'; // import dayjs, { extend } from 'dayjs';
// import customParseFormat from 'dayjs/plugin/customParseFormat'; // import customParseFormat from 'dayjs/plugin/customParseFormat';
// import relativeTime from 'dayjs/plugin/relativeTime'; // import relativeTime from 'dayjs/plugin/relativeTime';
@@ -6,11 +7,10 @@
// import QueryStringAddon from 'wretch/addons/queryString'; // import QueryStringAddon from 'wretch/addons/queryString';
// import { OS_RELEASES } from '~/helpers/urls'; // import { OS_RELEASES } from '~/helpers/urls';
import type { import type { Server, ServerState
Server, // ServerUpdateOsResponse,
ServerState, } from '~/types/server';
// ServerUpdateOsResponse,
} from "~/types/server";
// dayjs plugins // dayjs plugins
// extend(customParseFormat); // extend(customParseFormat);
@@ -44,10 +44,10 @@ import type {
// EBLACKLISTED2 // EBLACKLISTED2
// ENOCONN // ENOCONN
const state: ServerState = "ENOKEYFILE" as ServerState; const state: ServerState = 'ENOKEYFILE' as ServerState;
const currentFlashGuid = "1111-1111-YIJD-ZACK1234TEST"; // this is the flash drive that's been booted from const currentFlashGuid = '1111-1111-YIJD-ZACK1234TEST'; // this is the flash drive that's been booted from
const regGuid = "1111-1111-YIJD-ZACK1234TEST"; // this guid is registered in key server const regGuid = '1111-1111-YIJD-ZACK1234TEST'; // this guid is registered in key server
const keyfileBase64 = ""; const keyfileBase64 = '';
// const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server // const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered // const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
@@ -65,50 +65,50 @@ let expireTime = 0;
let regExp: number | undefined; let regExp: number | undefined;
let regDevs = 0; let regDevs = 0;
let regTy = ""; let regTy = '';
switch (state) { switch (state) {
case "EEXPIRED": case 'EEXPIRED':
expireTime = uptime; // 1 hour ago expireTime = uptime; // 1 hour ago
break; break;
case "ENOCONN": case 'ENOCONN':
break; break;
case "TRIAL": case 'TRIAL':
expireTime = oneHourFromNow; // in 1 hour expireTime = oneHourFromNow; // in 1 hour
regTy = "Trial"; regTy = 'Trial';
break; break;
case "BASIC": case 'BASIC':
regDevs = 6; regDevs = 6;
regTy = "Basic"; regTy = 'Basic';
break; break;
case "PLUS": case 'PLUS':
regDevs = 12; regDevs = 12;
regTy = "Plus"; regTy = 'Plus';
break; break;
case "PRO": case 'PRO':
regDevs = -1; regDevs = -1;
regTy = "Pro"; regTy = 'Pro';
break; break;
case "STARTER": case 'STARTER':
regDevs = 6; regDevs = 6;
regExp = ninetyDaysAgo; regExp = ninetyDaysAgo;
regTy = "Starter"; regTy = 'Starter';
break; break;
case "UNLEASHED": case 'UNLEASHED':
regDevs = -1; regDevs = -1;
regExp = ninetyDaysAgo; regExp = ninetyDaysAgo;
regTy = "Unleashed"; regTy = 'Unleashed';
break; break;
case "LIFETIME": case 'LIFETIME':
regDevs = -1; regDevs = -1;
regTy = "Lifetime"; regTy = 'Lifetime';
break; break;
} }
// const connectPluginInstalled = 'dynamix.unraid.net.staging.plg'; // const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
const connectPluginInstalled = "dynamix.unraid.net.staging.plg"; const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
const osVersion = "7.0.0-beta.2.10"; const osVersion = '7.0.0-beta.2.10';
const osVersionBranch = "stable"; const osVersionBranch = 'stable';
// const parsedRegExp = regExp ? dayjs(regExp).format('YYYY-MM-DD') : undefined; // const parsedRegExp = regExp ? dayjs(regExp).format('YYYY-MM-DD') : undefined;
// const mimicWebguiUnraidCheck = async (): Promise<ServerUpdateOsResponse | undefined> => { // const mimicWebguiUnraidCheck = async (): Promise<ServerUpdateOsResponse | undefined> => {
@@ -134,62 +134,63 @@ const osVersionBranch = "stable";
export const serverState: Server = { export const serverState: Server = {
activationCodeData: { activationCodeData: {
"code": "CC2KP3TDRF", code: 'CC2KP3TDRF',
"partnerName": "OEM Partner", partnerName: 'OEM Partner',
"partnerUrl": "https://unraid.net/OEM+Partner", partnerUrl: 'https://unraid.net/OEM+Partner',
"sysModel": "OEM Partner v1", sysModel: 'OEM Partner v1',
"comment": "OEM Partner NAS", comment: 'OEM Partner NAS',
"caseIcon": "case-model.png", caseIcon: 'case-model.png',
"header": "#ffffff", header: '#ffffff',
"headermetacolor": "#eeeeee", headermetacolor: '#eeeeee',
"background": "#000000", background: '#000000',
"showBannerGradient": "yes", showBannerGradient: 'yes',
"partnerLogo": true, partnerLogo: true,
}, },
apiKey: "unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810", apiKey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
avatar: "https://source.unsplash.com/300x300/?portrait", avatar: 'https://source.unsplash.com/300x300/?portrait',
config: { config: {
id: 'config-id', id: 'config-id',
error: null, error: null,
valid: false, valid: false,
}, },
connectPluginInstalled, connectPluginInstalled,
description: "DevServer9000", description: 'DevServer9000',
deviceCount: 3, deviceCount: 3,
expireTime, expireTime,
flashBackupActivated: !!connectPluginInstalled, flashBackupActivated: !!connectPluginInstalled,
flashProduct: "SanDisk_3.2Gen1", flashProduct: 'SanDisk_3.2Gen1',
flashVendor: "USB", flashVendor: 'USB',
guid: currentFlashGuid, guid: currentFlashGuid,
// "guid": "0781-5583-8355-81071A2B0211", // "guid": "0781-5583-8355-81071A2B0211",
inIframe: false, inIframe: false,
// keyfile: 'DUMMY_KEYFILE', // keyfile: 'DUMMY_KEYFILE',
keyfile: keyfileBase64, keyfile: keyfileBase64,
lanIp: "192.168.254.36", lanIp: '192.168.254.36',
license: "", license: '',
locale: "en_US", // en_US, ja locale: 'en_US', // en_US, ja
name: "dev-static", name: 'dev-static',
osVersion, osVersion,
osVersionBranch, osVersionBranch,
registered: connectPluginInstalled ? true : false, registered: connectPluginInstalled ? true : false,
// registered: false, // registered: false,
regGen: 0, regGen: 0,
regTm: twoDaysAgo, regTm: twoDaysAgo,
regTo: "Zack Spear", regTo: 'Zack Spear',
regTy, regTy,
regDevs, regDevs,
regExp, regExp,
regGuid, regGuid,
site: "http://localhost:4321", site: 'http://localhost:4321',
ssoSubIds: '1234567890,0987654321,297294e2-b31c-4bcc-a441-88aee0ad609f',
state, state,
theme: { theme: {
banner: false, banner: false,
bannerGradient: false, bannerGradient: false,
bgColor: "", bgColor: '',
descriptionShow: true, descriptionShow: true,
metaColor: "", metaColor: '',
name: "white", name: 'white',
textColor: "", textColor: '',
}, },
// updateOsResponse: { // updateOsResponse: {
// version: '6.12.6', // version: '6.12.6',
@@ -201,6 +202,6 @@ export const serverState: Server = {
// sha256: '2f5debaf80549029cf6dfab0db59180e7e3391c059e6521aace7971419c9c4bf', // sha256: '2f5debaf80549029cf6dfab0db59180e7e3391c059e6521aace7971419c9c4bf',
// }, // },
uptime, uptime,
username: "zspearmint", username: 'zspearmint',
wanFQDN: "", wanFQDN: '',
}; };

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import Button from '~/components/Brand/Button.vue';
import { ACCOUNT } from '~/helpers/urls';
import { useServerStore } from '~/store/server';
import type { Server } from '~/types/server';
export interface Props {
server?: Server | string;
}
const props = defineProps<Props>();
const serverStore = useServerStore();
const { ssoSubIds } = storeToRefs(serverStore);
onBeforeMount(() => {
if (!props.server) {
throw new Error('Server data not present');
}
console.log('props.server', props.server);
if (typeof props.server === 'object') {
// Handles the testing dev Vue component
serverStore.setServer(props.server);
} else if (typeof props.server === 'string') {
// Handle web component
const parsedServerProp = JSON.parse(props.server);
serverStore.setServer(parsedServerProp);
}
});
const queryParams = useUrlSearchParams<{ token: string }>();
const enterCallbackTokenIntoField = (token: string) => {
const passwordField = document.querySelector('input[name=password]') as HTMLInputElement;
const usernameField = document.querySelector('input[name=username]') as HTMLInputElement;
const form = document.querySelector('form[action="/login"]') as HTMLFormElement;
if (!passwordField || !usernameField || !form) {
console.warn('Could not find form, username, or password field');
} else {
usernameField.value = 'root';
passwordField.value = 'password';
// Submit the form
form.requestSubmit();
}
};
const search = new URLSearchParams(window.location.search);
const token = search.get('token') ?? '';
if (token) {
enterCallbackTokenIntoField(token);
// Clear the token from the URL
window.history.replaceState({}, document.title, window.location.pathname);
window.location.search = '';
}
watch(queryParams, (newVal) => {
console.log('newVal', newVal);
if (newVal?.token) {
enterCallbackTokenIntoField(newVal.token);
// Clear the token from the URL
window.history.replaceState({}, document.title, window.location.pathname);
window.location.search = '';
}
});
const externalSSOUrl = computed(() => {
const url = new URL('sso', ACCOUNT);
url.searchParams.append('uids', ssoSubIds.value);
url.searchParams.append('callbackUrl', window.location.href);
return url.toString();
});
</script>
<template>
<template v-if="ssoSubIds">
<Button target="_blank" :href="externalSSOUrl">Sign In With Unraid.net Account</Button>
</template>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -40,119 +40,122 @@ const charsToReserve = '_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
ssr: false, ssr: false,
devServer: { devServer: {
port: 4321, port: 4321,
}, },
devtools: { devtools: {
enabled: true, enabled: true,
}, },
modules: [ modules: [
"@vueuse/nuxt", '@vueuse/nuxt',
"@pinia/nuxt", '@pinia/nuxt',
"@nuxtjs/tailwindcss", '@nuxtjs/tailwindcss',
"nuxt-custom-elements", 'nuxt-custom-elements',
"@nuxt/eslint", '@nuxt/eslint',
"shadcn-nuxt", 'shadcn-nuxt',
], ],
ignore: ['/webGui/images'], ignore: ['/webGui/images'],
components: [ components: [
{ path: "~/components/Brand", prefix: "Brand" }, { path: '~/components/Brand', prefix: 'Brand' },
{ path: "~/components/ConnectSettings", prefix: "ConnectSettings" }, { path: '~/components/ConnectSettings', prefix: 'ConnectSettings' },
{ path: "~/components/Ui", prefix: "Ui" }, { path: '~/components/Ui', prefix: 'Ui' },
{ path: "~/components/UserProfile", prefix: "Upc" }, { path: '~/components/UserProfile', prefix: 'Upc' },
{ path: "~/components/UpdateOs", prefix: "UpdateOs" }, { path: '~/components/UpdateOs', prefix: 'UpdateOs' },
"~/components", '~/components',
], ],
// typescript: { // typescript: {
// typeCheck: true // typeCheck: true
// }, // },
shadcn: { shadcn: {
prefix: "", prefix: '',
componentDir: "./components/shadcn", componentDir: './components/shadcn',
}, },
vite: { vite: {
plugins: [ plugins: [
!process.env.VITE_ALLOW_CONSOLE_LOGS && !process.env.VITE_ALLOW_CONSOLE_LOGS &&
removeConsole({ removeConsole({
includes: ["log", "warn", "error", "info", "debug"], includes: ['log', 'warn', 'error', 'info', 'debug'],
}), }),
], ],
build: { build: {
minify: "terser", minify: 'terser',
terserOptions: { terserOptions: {
mangle: { mangle: {
reserved: terserReservations(charsToReserve), reserved: terserReservations(charsToReserve),
toplevel: true, toplevel: true,
}, },
}, },
}, },
}, },
customElements: { customElements: {
entries: [ entries: [
{ {
name: "UnraidComponents", name: 'UnraidComponents',
tags: [ tags: [
{ {
name: "UnraidI18nHost", name: 'UnraidI18nHost',
path: "@/components/I18nHost.ce", path: '@/components/I18nHost.ce',
}, },
{ {
name: "UnraidAuth", name: 'UnraidAuth',
path: "@/components/Auth.ce", path: '@/components/Auth.ce',
}, },
{ {
name: "UnraidConnectSettings", name: 'UnraidConnectSettings',
path: "@/components/ConnectSettings/ConnectSettings.ce", path: '@/components/ConnectSettings/ConnectSettings.ce',
}, },
{ {
name: "UnraidDownloadApiLogs", name: 'UnraidDownloadApiLogs',
path: "@/components/DownloadApiLogs.ce", path: '@/components/DownloadApiLogs.ce',
}, },
{ {
name: "UnraidHeaderOsVersion", name: 'UnraidHeaderOsVersion',
path: "@/components/HeaderOsVersion.ce", path: '@/components/HeaderOsVersion.ce',
}, },
{ {
name: "UnraidModals", name: 'UnraidModals',
path: "@/components/Modals.ce", path: '@/components/Modals.ce',
}, },
{ {
name: "UnraidUserProfile", name: 'UnraidUserProfile',
path: "@/components/UserProfile.ce", path: '@/components/UserProfile.ce',
}, },
{ {
name: "UnraidUpdateOs", name: 'UnraidUpdateOs',
path: "@/components/UpdateOs.ce", path: '@/components/UpdateOs.ce',
}, },
{ {
name: "UnraidDowngradeOs", name: 'UnraidDowngradeOs',
path: "@/components/DowngradeOs.ce", path: '@/components/DowngradeOs.ce',
}, },
{ {
name: "UnraidRegistration", name: 'UnraidRegistration',
path: "@/components/Registration.ce", path: '@/components/Registration.ce',
}, },
{ {
name: "UnraidWanIpCheck", name: 'UnraidWanIpCheck',
path: "@/components/WanIpCheck.ce", path: '@/components/WanIpCheck.ce',
}, },
{ {
name: "UnraidWelcomeModal", name: 'UnraidWelcomeModal',
path: "@/components/WelcomeModal.ce", path: '@/components/WelcomeModal.ce',
}, },
], { name: 'UnraidSsoButton',
}, path: '@/components/SsoButton.ce'
], },
}, ],
},
],
},
compatibilityDate: "2024-12-05" compatibilityDate: '2024-12-05',
}); });

View File

@@ -4,6 +4,7 @@ import { BrandButton, BrandLogo } from '@unraid/ui';
import { serverState } from '~/_data/serverState'; import { serverState } from '~/_data/serverState';
import type { SendPayloads } from '~/store/callback'; import type { SendPayloads } from '~/store/callback';
import AES from 'crypto-js/aes'; import AES from 'crypto-js/aes';
import SsoButtonCe from '~/components/SsoButton.ce.vue';
const { registerEntry } = useCustomElements(); const { registerEntry } = useCustomElements();
onBeforeMount(() => { onBeforeMount(() => {
@@ -152,6 +153,11 @@ onMounted(() => {
> >
</template> </template>
</div> </div>
<div class="bg-background">
<hr class="border-black dark:border-white" />
<h2 class="text-xl font-semibold font-mono">SSO Button Component</h2>
<SsoButtonCe :server="serverState" />
</div>
</div> </div>
</client-only> </client-only>
</div> </div>

View File

@@ -75,7 +75,12 @@ onBeforeMount(() => {
<h3 class="text-lg font-semibold font-mono"> <h3 class="text-lg font-semibold font-mono">
ModalsCe ModalsCe
</h3> </h3>
<unraid-modals /> <!-- 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 :server="JSON.stringify(serverState)" />
</unraid-i18n-host> </unraid-i18n-host>
</client-only> </client-only>
</template> </template>

View File

@@ -155,6 +155,7 @@ export const useServerStore = defineStore("server", () => {
return today.isAfter(parsedUpdateExpirationDate, "day"); return today.isAfter(parsedUpdateExpirationDate, "day");
}); });
const site = ref<string>(""); const site = ref<string>("");
const ssoSubIds = ref<string>("");
const state = ref<ServerState>(); const state = ref<ServerState>();
const theme = ref<Theme>(); const theme = ref<Theme>();
watch(theme, (newVal) => { watch(theme, (newVal) => {
@@ -1208,6 +1209,9 @@ export const useServerStore = defineStore("server", () => {
if (typeof data?.regTo !== "undefined") { if (typeof data?.regTo !== "undefined") {
regTo.value = data.regTo; regTo.value = data.regTo;
} }
if (typeof data?.ssoSubIds !== "undefined") {
ssoSubIds.value = data.ssoSubIds;
}
if (typeof data.activationCodeData !== "undefined") { if (typeof data.activationCodeData !== "undefined") {
const activationCodeStore = useActivationCodeStore(); const activationCodeStore = useActivationCodeStore();
@@ -1474,6 +1478,7 @@ export const useServerStore = defineStore("server", () => {
parsedRegExp, parsedRegExp,
regUpdatesExpired, regUpdatesExpired,
site, site,
ssoSubIds,
state, state,
theme, theme,
updateOsIgnoredReleases, updateOsIgnoredReleases,

View File

@@ -117,6 +117,7 @@ export interface Server {
username?: string; username?: string;
wanFQDN?: string; wanFQDN?: string;
wanIp?: string; wanIp?: string;
ssoSubIds?: string;
} }
export interface ServerAccountCallbackSendPayload { export interface ServerAccountCallbackSendPayload {