[Feat] Settings UI Auth (#3) & Prepare for v0.1.1-rel

This commit is contained in:
Minoricew
2025-06-02 00:22:46 +08:00
parent 1320f5397a
commit a86d13431b
12 changed files with 569 additions and 34 deletions

View File

@@ -1,11 +1,34 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
// @ts-check
const fs = require("fs");
const path = require("path");
const os = require("os");
const crypto = require("crypto");
const childProc = require("child_process");
// Constants
const CRYPTO_SETTINGS_AES = {
mode: "aes-256-gcm",
keyLength: 32,
keyIter: 100000,
ivLength: 12,
tagLength: 16,
saltLength: 16,
obfuscateStr: "eCybsseK",
hash: "sha256",
};
/**
*
* @param {Record<any, any>} target
* @param {Record<any, any>} source
* @returns
*/
const deepMerge = (target, source) => {
const result = JSON.parse(JSON.stringify(target));
if (!source || typeof source !== 'object') {
if (!source || typeof source !== "object") {
return {};
}
@@ -15,9 +38,9 @@ const deepMerge = (target, source) => {
if (!(key in source)) {
keysToDelete.push(key);
} else if (
typeof result[key] === 'object' &&
typeof result[key] === "object" &&
result[key] !== null &&
typeof source[key] === 'object' &&
typeof source[key] === "object" &&
source[key] !== null
) {
result[key] = deepMerge(result[key], source[key]);
@@ -42,13 +65,11 @@ const deepMerge = (target, source) => {
class ConfigManager {
constructor() {
this.configPath = path.join(
os.homedir(),
'Documents',
'HugoAura',
'config.json'
);
this.defaultConfigPath = path.join(__dirname, 'default.json');
this.configDir = path.join(os.homedir(), "Documents", "HugoAura");
this.configPath = path.join(this.configDir, "config.json");
this.encConfigPath = path.join(this.configDir, ".cache_2eafc8d0.dat"); // (雾
/* ↑ 不使用 .tmp 扩展名, 不然容易真被清理了 */
this.defaultConfigPath = path.join(__dirname, "default.json");
}
getHugoAuraConfigPath() {
@@ -61,10 +82,10 @@ class ConfigManager {
getDefaultConfig() {
try {
return JSON.parse(fs.readFileSync(this.defaultConfigPath, 'utf8'));
return JSON.parse(fs.readFileSync(this.defaultConfigPath, "utf8"));
} catch (err) {
console.warn(
'[HugoAura / Config] No default config found, using empty config'
"[HugoAura / Config] No default config found, using empty config"
);
return { rewrite: {} };
}
@@ -74,12 +95,12 @@ class ConfigManager {
if (global.__HUGO_AURA__.configInit) return;
const hugoAuraPath = this.getHugoAuraConfigPath();
if (!fs.existsSync(hugoAuraPath)) {
console.log('[HugoAura / Config] Creating HugoAura directory');
console.log("[HugoAura / Config] Creating HugoAura directory");
fs.mkdirSync(hugoAuraPath, { recursive: true });
}
if (!fs.existsSync(this.configPath)) {
console.log('[HugoAura / Config] Creating default config file');
console.log("[HugoAura / Config] Creating default config file");
const defaultConfig = this.getDefaultConfig();
this.writeConfig(defaultConfig);
}
@@ -87,25 +108,30 @@ class ConfigManager {
readConfig() {
try {
const config = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
console.log('[HugoAura / Config] Successfully loaded config:', config);
const config = JSON.parse(fs.readFileSync(this.configPath, "utf8"));
console.log("[HugoAura / Config] Successfully loaded config:", config);
return config;
} catch (err) {
console.error('[HugoAura / Config] Failed to read config:', err);
console.error("[HugoAura / Config] Failed to read config:", err);
return this.getDefaultConfig();
}
}
/**
*
* @param {Record<any, any>} config
* @returns
*/
writeConfig(config) {
try {
fs.writeFileSync(
this.configPath,
JSON.stringify(config, null, 2),
'utf8'
"utf8"
);
return true;
} catch (err) {
console.error('[HugoAura / Config] Failed to write config:', err);
console.error("[HugoAura / Config] Failed to write config:", err);
return false;
}
}
@@ -115,23 +141,177 @@ class ConfigManager {
let config = {};
try {
if (fs.existsSync(this.configPath)) {
const userConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
const userConfig = JSON.parse(fs.readFileSync(this.configPath, "utf8"));
if (global.__HUGO_AURA__.configInit) {
config = userConfig;
return userConfig;
} else {
config = deepMerge(userConfig, defaultConfig);
console.log('[HugoAura / Config] Merged with user config');
console.log("[HugoAura / Config] Merged with user config");
this.writeConfig(config);
}
}
} catch (err) {
console.error('[HugoAura / Config] Failed to load user config:', err);
console.error("[HugoAura / Config] Failed to load user config:", err);
config = defaultConfig;
}
return config;
}
/**
*
* @param {Record<any, any>} configData
* @param {string} passwd
*/
encryptConfig(configData, passwd) {
const salt = crypto.randomBytes(CRYPTO_SETTINGS_AES.saltLength);
const key = crypto.pbkdf2Sync(
passwd,
salt,
CRYPTO_SETTINGS_AES.keyIter,
CRYPTO_SETTINGS_AES.keyLength,
CRYPTO_SETTINGS_AES.hash
);
const iv = crypto.randomBytes(CRYPTO_SETTINGS_AES.ivLength);
const cipherIns = crypto.createCipheriv(CRYPTO_SETTINGS_AES.mode, key, iv, {
// @ts-expect-error
authTagLength: CRYPTO_SETTINGS_AES.tagLength,
});
const stringifyConfigData = JSON.stringify(configData);
let encryptedCfg = cipherIns.update(stringifyConfigData, "utf-8", "hex");
encryptedCfg += cipherIns.final("hex");
const authTag = cipherIns.getAuthTag();
/** @type {EncryptedConfig} */
const encConfigFinal = {};
encConfigFinal.content = encryptedCfg;
encConfigFinal.authTag = authTag.toString("base64");
encConfigFinal.salt = salt.toString("base64");
encConfigFinal.iv = iv.toString("base64");
const base64EncConfig =
CRYPTO_SETTINGS_AES.obfuscateStr +
Buffer.from(JSON.stringify(encConfigFinal)).toString("base64");
try {
fs.writeFileSync(this.encConfigPath, base64EncConfig, "utf-8");
// fs.rmSync(this.configPath);
const _hideFileProc = childProc.spawnSync(
"cmd.exe",
["/c", "attrib", "+h", this.encConfigPath],
{
stdio: "inherit",
}
);
return true;
} catch (err) {
console.error(
"[HugoAura / Config] Failed to write encrypted config:",
err
);
console.error(
"[HugoAura / Config] Pending config data:",
base64EncConfig
);
return false;
}
}
/**
*
* @param {string} passwd
* @returns
*/
decryptConfig(passwd) {
const FAILED_RET = {
success: false,
data: {},
};
let base64EncConfig = null;
try {
if (!fs.existsSync(this.encConfigPath)) {
return FAILED_RET;
}
base64EncConfig = fs.readFileSync(this.encConfigPath, "utf-8");
} catch (err) {
console.error(
"[HugoAura / Config] Failed to read encrypted config:",
err
);
return FAILED_RET;
}
if (base64EncConfig) {
const strip64EncCfg = base64EncConfig.split(
CRYPTO_SETTINGS_AES.obfuscateStr
)[1];
const encryptCfg = Buffer.from(strip64EncCfg, "base64").toString("utf-8");
/** @type {null | EncryptedConfig} */
let parsedEncCfg = null;
try {
parsedEncCfg = JSON.parse(encryptCfg);
} catch (err) {
console.error(
"[HugoAura / Config] Failed to parse encrypted config:",
err
);
console.error("[HugoAura / Config] Pending data:", encryptCfg);
}
if (parsedEncCfg === null) return FAILED_RET;
const salt = Buffer.from(parsedEncCfg.salt, "base64");
const iv = Buffer.from(parsedEncCfg.iv, "base64");
const authTag = Buffer.from(parsedEncCfg.authTag, "base64");
const key = crypto.pbkdf2Sync(
passwd,
salt,
CRYPTO_SETTINGS_AES.keyIter,
CRYPTO_SETTINGS_AES.keyLength,
CRYPTO_SETTINGS_AES.hash
);
const decipherIns = crypto.createDecipheriv(
CRYPTO_SETTINGS_AES.mode,
key,
iv,
{
// @ts-expect-error
authTagLength: CRYPTO_SETTINGS_AES.tagLength,
}
);
decipherIns.setAuthTag(authTag);
let stringifyDecCfg = Buffer.concat([
decipherIns.update(parsedEncCfg.content, "hex"),
decipherIns.final(),
]).toString();
/** @type {null | Record<any, any>} */
let decConfig = null;
try {
decConfig = JSON.parse(stringifyDecCfg);
} catch (err) {
console.error(
"[HugoAura / Config] Failed to parse decrypted config:",
err
);
console.error("[HugoAura / Config] Pending data:", decConfig);
return FAILED_RET;
}
if (decConfig === null) return FAILED_RET;
console.debug(decConfig);
}
}
}
module.exports = new ConfigManager();

View File

@@ -19,5 +19,12 @@
}
},
"plsToken": "66ccff0d000721114514191981023333",
"auraSettings": {
"settingsPasswordEnabled": false,
"settingsPasswordWithSalt": "32703D292460CC9A3B867494D6AD9A8E4A3ADF0FAA4D6867BC4D412CC3927D02E47C6D0B1763BB53E57B2241C6193433561CDA09D7C48CA03983072B876F0965",
"appearance": {
"enablePasswdDialogBlur": true
}
},
"devTools": false
}

9
src/aura/types/shared/config.d.ts vendored Executable file
View File

@@ -0,0 +1,9 @@
type AES256EncryptedConfig = string;
type Base64String = string;
interface EncryptedConfig {
content: AES256EncryptedConfig;
authTag: Base64String;
salt: Base64String;
iv: Base64String;
}

View File

@@ -68,3 +68,29 @@
.aura-settings-entry-property-icon.layui-icon-refresh {
color: rgb(0, 106, 188);
}
/* Animations */
@keyframes invalidShake {
0% {
margin-left: calc(-10px * 2);
}
16% {
margin-left: calc(9px * 2);
}
33% {
margin-left: calc(-6px * 2);
}
50% {
margin-left: calc(5px * 2);
}
66% {
margin-left: calc(-2px * 2);
}
83% {
margin-left: calc(1px * 2);
}
100% {
margin-left: calc(0px * 2);
}
}

View File

@@ -45,6 +45,7 @@
padding-left: 8px;
padding-right: 8px;
color: white;
z-index: 12000;
opacity: 1;
transform: translateY(0);
@@ -368,6 +369,82 @@
font-size: small;
}
/* Auth Dialog */
.aura-config-page-auth-dialog-area {
position: absolute;
height: calc(100% - 40px);
width: 100%;
top: 40px;
left: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
background-color: rgba(255, 255, 255, 0.35);
opacity: 1;
transition: all 0.5s;
}
.aura-config-page-auth-dialog-area.blur-enabled {
height: 100%;
top: 0;
background-color: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(5px);
filter: blur(0.1px);
/* ↑ 似乎会导致性能问题 */
}
.acp-ada-hidden {
opacity: 0;
}
.acp-ada-hidden.blur-enabled {
backdrop-filter: blur(0.1px);
filter: unset;
}
.aura-config-page-auth-dialog {
height: 40%;
width: 100%;
background-color: rgba(255, 255, 255, 0.625);
display: flex;
flex-direction: column;
text-align: center;
align-items: center;
padding-top: 2rem;
padding-bottom: 2rem;
}
.acp-auth-dialog-title {
font-size: x-large;
margin-bottom: 1.5rem;
}
#acp-auth-user-input {
max-width: 50%;
/* background-color: rgba(255, 255, 255, 0.5); */
border-radius: 35px;
margin-bottom: 2rem;
}
#acp-auth-user-input.invalid {
animation: invalidShake 0.6s linear;
}
.acp-auth-confirm-btn {
background-color: transparent;
border-radius: 35px;
border: 1px solid rgba(0, 0, 0, 0.3);
padding: 0.5rem;
}
.acp-auth-confirm-btn .layui-icon {
font-size: 24px;
margin-left: 2px;
}
/* Toast */
.aura-config-page-toast-area {

View File

@@ -80,8 +80,10 @@
</div>
<div
class="operation-el-hidden aura-config-page-operation-el"
onclick="window.__HUGO_AURA_UI_FUNCTIONS__.config.toggleSubConfig('behaviourCtrl', true)"
aura-disabled="true"
>
<!-- Still WIP -->
<!-- onclick="window.__HUGO_AURA_UI_FUNCTIONS__.config.toggleSubConfig('behaviourCtrl', true)" -->
<div class="aura-config-page-operation-body">
<img src="../../aura/ui/static/config/behaviour_mon.svg" />
<div>
@@ -114,6 +116,30 @@
</div>
</div>
<div
class="aura-config-page-auth-dialog-area acp-ada-hidden"
style="display: none"
>
<div class="aura-config-page-auth-dialog">
<p class="acp-auth-dialog-title">验证您的身份</p>
<input
type="password"
class="form-control"
placeholder="请输入密码..."
aria-label="Aura Password"
id="acp-auth-user-input"
/>
<button
class="acp-auth-confirm-btn"
onclick="global.__HUGO_AURA_UI_FUNCTIONS__.config.verifyAuthPassword()"
>
<i class="layui-icon layui-icon-right"></i>
</button>
</div>
</div>
<div class="aura-config-page-toast-area">
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div

View File

@@ -117,6 +117,48 @@ global.__HUGO_AURA_UI_FUNCTIONS__.config = {
global.__HUGO_AURA_UI_REACTIVES__.config.isInSubPage = side;
},
verifyAuthPassword: async () => {
const showFailedAnimation = async (el) => {
el.classList.remove("invalid");
await window.__HUGO_AURA_GLOBAL__.utils.sleep(50);
el.classList.add("invalid"); // Custom Anim
el.classList.add("is-invalid"); // Bootstrap
};
const inputEl = document.getElementById("acp-auth-user-input");
const userPasswdInput = inputEl.value;
if (!userPasswdInput || userPasswdInput.length < 8) {
showFailedAnimation(inputEl);
return false;
}
const crypto = require("crypto");
const encPasswd = crypto
.createHash("sha512")
.update(userPasswdInput + "EndlessX")
.digest("hex")
.toUpperCase();
if (
encPasswd ===
global.__HUGO_AURA_CONFIG__.auraSettings.settingsPasswordWithSalt
) {
const acsDialogAreaEl = document.getElementsByClassName(
"aura-config-page-auth-dialog-area"
)[0];
acsDialogAreaEl.classList.add("acp-ada-hidden");
await window.__HUGO_AURA_GLOBAL__.utils.sleep(500);
acsDialogAreaEl.style = "display: none;";
await window.__HUGO_AURA_GLOBAL__.utils.sleep(250);
global.__HUGO_AURA_UI_FUNCTIONS__.config.showSecondPhaseAnim();
return true;
} else {
showFailedAnimation(inputEl);
return false;
}
},
};
(() => {
@@ -167,23 +209,51 @@ global.__HUGO_AURA_UI_FUNCTIONS__.config = {
});
};
const showAnimation = async () => {
const defaultHeader = document.getElementsByClassName(
"index__header__16DmR2a5"
)[0];
global.__HUGO_AURA_UI_FUNCTIONS__.config.showSecondPhaseAnim = () => {
showOperationsAnimation();
};
const handleSettingsAuth = async () => {
const isAuthEnabled =
global.__HUGO_AURA_CONFIG__.auraSettings.settingsPasswordEnabled;
if (!isAuthEnabled) {
showOperationsAnimation();
} else {
await window.__HUGO_AURA_GLOBAL__.utils.sleep(50);
const acsDialogAreaEl = document.getElementsByClassName(
"aura-config-page-auth-dialog-area"
)[0];
acsDialogAreaEl.style = "";
if (
global.__HUGO_AURA_CONFIG__.auraSettings.appearance
.enablePasswdDialogBlur
) {
acsDialogAreaEl.classList.add("blur-enabled");
}
await window.__HUGO_AURA_GLOBAL__.utils.sleep(500);
acsDialogAreaEl.classList.remove("acp-ada-hidden");
}
};
const showAnimation = async () => {
const auraConfigPageRoot = document.getElementsByClassName(
"aura-config-page-root"
)[0];
await window.__HUGO_AURA_GLOBAL__.utils.sleep(200);
auraConfigPageRoot.className = "aura-config-page-root";
const defaultHeader = document.getElementsByClassName(
"index__header__16DmR2a5"
)[0];
await window.__HUGO_AURA_GLOBAL__.utils.sleep(500);
defaultHeader.style = "display: none;";
showVersionContainerAnimation();
showHeaderAnimation();
await window.__HUGO_AURA_GLOBAL__.utils.sleep(500);
showOperationsAnimation();
await handleSettingsAuth();
};
const onMounted = () => {

View File

@@ -6,6 +6,20 @@
<li class="nav-item" role="presentation">
<button
class="nav-link active"
id="aura-subpage-tab"
data-bs-toggle="pill"
data-bs-target="#aura-subpage"
type="button"
role="tab"
aria-controls="aura-subpage"
aria-selected="true"
>
Aura 设置
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="auth-subpage-tab"
data-bs-toggle="pill"
data-bs-target="#auth-subpage"
@@ -14,7 +28,7 @@
aria-controls="auth-subpage"
aria-selected="true"
>
认证与基础设施
认证与环境
</button>
</li>
<li class="nav-item" role="presentation">
@@ -35,6 +49,12 @@
<div class="tab-content">
<div
class="tab-pane fade show active"
id="aura-subpage"
role="tabpanel"
aria-labelledby="aura-subpage-tab"
></div>
<div
class="tab-pane fade show"
id="auth-subpage"
role="tabpanel"
aria-labelledby="auth-subpage-tab"

View File

@@ -5,9 +5,15 @@
const {
settingsRenderer,
} = require("../../aura/ui/composables/settingsRenderer");
const { auraSettings } = require(`${pathBase}/aura`);
const { authSettings } = require(`${pathBase}/auth`);
const { banAuditSettings } = require(`${pathBase}/audit`);
const initAuraSubPage = () => {
const auraSettingsSubPageEl = document.getElementById("aura-subpage");
settingsRenderer(auraSettingsSubPageEl, auraSettings);
};
const initAuthSubPage = () => {
const authSubPageEl = document.getElementById("auth-subpage");
settingsRenderer(authSubPageEl, authSettings);
@@ -19,6 +25,7 @@
};
const onMounted = () => {
initAuraSubPage();
initAuthSubPage();
initBanAuditSubPage();

View File

@@ -0,0 +1,113 @@
const auraSettings = [
{
id: 0,
categoryName: "安全性",
child: [
{
index: 0,
id: "enableAuraSettingsPasswd",
type: "switch",
name: "启用访问密码",
description: "启用后, Aura 设置 UI 需要输入密码才可访问",
restart: false,
reload: false,
tip: true,
tipTitle: "在 0.1.1-beta 版本发布后, 启用访问密码将加密配置文件",
associateVal: null,
auraIf: () => true,
defaultValue: false,
valueGetter: () => {
return global.__HUGO_AURA_CONFIG__.auraSettings
.settingsPasswordEnabled;
},
callbackFn: (newVal) => {
if (typeof newVal !== "boolean") return;
global.__HUGO_AURA_CONFIG__.auraSettings.settingsPasswordEnabled =
newVal;
// TODO: Trigger enc config
},
},
{
index: 1,
id: "auraSettingsPasswd",
type: "input",
subType: "password",
name: "访问密码",
description: "此密码将用于访问 Aura 设置 UI",
restart: false,
reload: false,
associateVal: ["auraSettings.settingsPasswordEnabled"],
auraIf: () => {
return global.__HUGO_AURA_CONFIG__.auraSettings
.settingsPasswordEnabled;
},
defaultValue: "",
placeHolder: "留空表示不修改, 保留已设置值",
valueGetter: () => {
return "";
},
callbackFn: (newVal) => {
if (newVal === "" || !newVal) return { valid: true };
if (newVal.length < 8)
return { valid: false, hint: "请输入至少 8 位密码" };
const hasNumber = /[0-9]/.test(newVal);
const hasLetter = /[a-zA-Z]/.test(newVal);
const hasSpecial = /[^a-zA-Z0-9]/.test(newVal);
const typeCount = [hasNumber, hasLetter, hasSpecial].filter(
Boolean
).length;
if (typeCount < 2) {
return {
valid: false,
hint: "请包含数字 / 字母 / 特殊字符中的至少 2 种",
};
}
const crypto = require("crypto");
const result = crypto
.createHash("sha512")
.update(newVal + "EndlessX")
.digest("hex")
.toUpperCase();
global.__HUGO_AURA_CONFIG__.auraSettings.settingsPasswordWithSalt =
result;
return { valid: true };
},
},
],
},
{
id: 1,
categoryName: "外观",
child: [
{
index: 0,
id: "enablePasswdDialogBlur",
type: "switch",
name: "密码验证框毛玻璃效果",
description: "启用后, 密码验证时, 背景将具有毛玻璃效果",
restart: false,
reload: false,
tip: true,
tipTitle: "不建议在较旧 (如 i5 8 代) 机型上开启, 可能导致性能问题",
associateVal: null,
auraIf: () => true,
defaultValue: true,
valueGetter: () => {
return global.__HUGO_AURA_CONFIG__.auraSettings.appearance
.enablePasswdDialogBlur;
},
callbackFn: (newVal) => {
if (typeof newVal !== "boolean") return;
global.__HUGO_AURA_CONFIG__.auraSettings.appearance.enablePasswdDialogBlur =
newVal;
},
},
],
},
];
module.exports = { auraSettings };

View File

@@ -1,7 +1,7 @@
const authSettings = [
{
id: 0,
categoryName: "身份验证",
categoryName: "管家身份验证",
child: [
{
index: 0,

View File

@@ -1,4 +1,4 @@
const __AURA_VERSION__ = "0.1.0-beta";
const __AURA_VERSION__ = "0.1.1-pre-I";
(() => {
if (!global.__HUGO_AURA__) {