mirror of
https://github.com/unraid/api.git
synced 2025-12-31 21:49:57 -06:00
feat: unraid single sign on with account app
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
18
api/src/core/sso/sso-remove.ts
Normal file
18
api/src/core/sso/sso-remove.ts
Normal 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
62
api/src/core/sso/sso-setup.ts
Executable 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.');
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
22
api/src/unraid-api/cli/sso.command.ts
Normal file
22
api/src/unraid-api/cli/sso.command.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: '',
|
||||||
};
|
};
|
||||||
85
web/components/SsoButton.ce.vue
Normal file
85
web/components/SsoButton.ce.vue
Normal 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>
|
||||||
@@ -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',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user