feat: optimize admin password management and add reset

This commit is contained in:
Kuingsmile
2025-07-17 17:32:57 +08:00
parent c9ccf6d1ce
commit e0d3250823
12 changed files with 411 additions and 60 deletions

View File

@@ -28,7 +28,7 @@ if (!getOpenlistArchMap[platformArch]) {
} }
// Rclone version management // Rclone version management
let rcloneVersion = 'v1.70.1' let rcloneVersion = 'v1.70.3'
const rcloneVersionUrl = 'https://github.com/rclone/rclone/releases/latest/download/version.txt' const rcloneVersionUrl = 'https://github.com/rclone/rclone/releases/latest/download/version.txt'
async function getLatestRcloneVersion() { async function getLatestRcloneVersion() {
@@ -42,7 +42,7 @@ async function getLatestRcloneVersion() {
} }
// openlist version management // openlist version management
let openlistVersion = 'v4.0.3' let openlistVersion = 'v4.0.8'
async function getLatestOpenlistVersion() { async function getLatestOpenlistVersion() {
try { try {
@@ -51,7 +51,7 @@ async function getLatestOpenlistVersion() {
getFetchOptions() getFetchOptions()
) )
const data = await response.json() const data = await response.json()
openlistVersion = data.tag_name || 'v4.0.3' openlistVersion = data.tag_name || 'v4.0.8'
console.log(`Latest OpenList version: ${openlistVersion}`) console.log(`Latest OpenList version: ${openlistVersion}`)
} catch (error) { } catch (error) {
console.log('Error fetching latest OpenList version:', error.message) console.log('Error fetching latest OpenList version:', error.message)

View File

@@ -1,16 +1,103 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command;
use once_cell::sync::Lazy;
use regex::Regex;
use tauri::State; use tauri::State;
use crate::object::structs::AppState; use crate::object::structs::AppState;
static ADMIN_PWD_REGEX: Lazy<Regex> = Lazy::new(|| { fn generate_random_password() -> String {
Regex::new(r"Successfully created the admin user and the initial password is: (\w+)") use std::collections::hash_map::DefaultHasher;
.expect("Invalid regex pattern") use std::hash::{Hash, Hasher};
}); use std::time::{SystemTime, UNIX_EPOCH};
let mut hasher = DefaultHasher::new();
if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
duration.as_nanos().hash(&mut hasher);
}
std::process::id().hash(&mut hasher);
let dummy = [1, 2, 3];
(dummy.as_ptr() as usize).hash(&mut hasher);
let hash = hasher.finish();
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut password = String::new();
let mut current_hash = hash;
for _ in 0..16 {
let index = (current_hash % chars.len() as u64) as usize;
password.push(chars.chars().nth(index).unwrap());
current_hash = current_hash.wrapping_mul(1103515245).wrapping_add(12345);
}
password
}
async fn execute_openlist_admin_set(
password: &str,
state: &State<'_, AppState>,
) -> Result<(), String> {
let exe_path =
env::current_exe().map_err(|e| format!("Failed to determine executable path: {e}"))?;
let app_dir = exe_path
.parent()
.ok_or("Executable has no parent directory")?;
let possible_names = ["openlist", "openlist.exe"];
let mut openlist_exe = None;
for name in &possible_names {
let exe_path = app_dir.join(name);
if exe_path.exists() {
openlist_exe = Some(exe_path);
break;
}
}
let openlist_exe = openlist_exe.ok_or_else(|| {
format!(
"OpenList executable not found. Searched for: {:?} in {}",
possible_names,
app_dir.display()
)
})?;
log::info!(
"Setting new admin password using: {}",
openlist_exe.display()
);
let mut cmd = Command::new(&openlist_exe);
cmd.args(["admin", "set", password]);
if let Some(settings) = state.get_settings()
&& !settings.openlist.data_dir.is_empty()
{
cmd.arg("--data");
cmd.arg(&settings.openlist.data_dir);
log::info!("Using data directory: {}", settings.openlist.data_dir);
}
log::info!("Executing command: {cmd:?}");
let output = cmd
.output()
.map_err(|e| format!("Failed to execute openlist command: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
log::error!("OpenList admin set command failed. stdout: {stdout}, stderr: {stderr}");
return Err(format!("OpenList admin set command failed: {stderr}"));
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::info!("Successfully set admin password. Output: {stdout}");
Ok(())
}
fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> { fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> {
let exe_path = let exe_path =
@@ -45,20 +132,78 @@ fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec
#[tauri::command] #[tauri::command]
pub async fn get_admin_password(state: State<'_, AppState>) -> Result<String, String> { pub async fn get_admin_password(state: State<'_, AppState>) -> Result<String, String> {
let data_dir = state if let Some(settings) = state.get_settings()
.get_settings() && let Some(ref stored_password) = settings.app.admin_password
.map(|s| s.openlist.data_dir) && !stored_password.is_empty()
.filter(|d| !d.is_empty()); {
log::info!("Found admin password in local settings");
return Ok(stored_password.clone());
}
let paths = resolve_log_paths(Some("openlist_core"), data_dir.as_deref())?; let new_password = generate_random_password();
let content =
std::fs::read_to_string(&paths[0]).map_err(|e| format!("Failed to read log file: {e}"))?;
ADMIN_PWD_REGEX if let Err(e) = execute_openlist_admin_set(&new_password, &state).await {
.captures_iter(&content) return Err(format!("Failed to set new admin password: {e}"));
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string())) }
.last()
.ok_or_else(|| "No admin password found in logs".into()) log::info!("Successfully generated and set new admin password");
if let Some(mut settings) = state.get_settings() {
settings.app.admin_password = Some(new_password.clone());
state.update_settings(settings.clone());
if let Err(e) = settings.save() {
log::warn!("Failed to save new admin password to settings: {e}");
}
}
Ok(new_password)
}
#[tauri::command]
pub async fn reset_admin_password(state: State<'_, AppState>) -> Result<String, String> {
log::info!("Forcing admin password reset");
let new_password = generate_random_password();
if let Err(e) = execute_openlist_admin_set(&new_password, &state).await {
return Err(format!("Failed to set new admin password: {e}"));
}
log::info!("Successfully generated and set new admin password via force reset");
if let Some(mut settings) = state.get_settings() {
settings.app.admin_password = Some(new_password.clone());
state.update_settings(settings.clone());
if let Err(e) = settings.save() {
log::warn!("Failed to save new admin password to settings: {e}");
}
}
Ok(new_password)
}
#[tauri::command]
pub async fn set_admin_password(
password: String,
state: State<'_, AppState>,
) -> Result<String, String> {
log::info!("Setting custom admin password");
if let Err(e) = execute_openlist_admin_set(&password, &state).await {
return Err(format!("Failed to set admin password: {e}"));
}
log::info!("Successfully set custom admin password");
if let Some(mut settings) = state.get_settings() {
settings.app.admin_password = Some(password.clone());
state.update_settings(settings.clone());
if let Err(e) = settings.save() {
log::warn!("Failed to save admin password to settings: {e}");
}
}
Ok(password)
} }
#[tauri::command] #[tauri::command]

View File

@@ -7,6 +7,7 @@ pub struct AppConfig {
pub gh_proxy: Option<String>, pub gh_proxy: Option<String>,
pub gh_proxy_api: Option<bool>, pub gh_proxy_api: Option<bool>,
pub open_links_in_browser: Option<bool>, pub open_links_in_browser: Option<bool>,
pub admin_password: Option<String>,
} }
impl AppConfig { impl AppConfig {
@@ -17,6 +18,7 @@ impl AppConfig {
gh_proxy: None, gh_proxy: None,
gh_proxy_api: Some(false), gh_proxy_api: Some(false),
open_links_in_browser: Some(false), open_links_in_browser: Some(false),
admin_password: None,
} }
} }
} }

View File

@@ -17,7 +17,9 @@ use cmd::firewall::{add_firewall_rule, check_firewall_rule, remove_firewall_rule
use cmd::http_api::{ use cmd::http_api::{
delete_process, get_process_list, restart_process, start_process, stop_process, update_process, delete_process, get_process_list, restart_process, start_process, stop_process, update_process,
}; };
use cmd::logs::{clear_logs, get_admin_password, get_logs}; use cmd::logs::{
clear_logs, get_admin_password, get_logs, reset_admin_password, set_admin_password,
};
use cmd::openlist_core::{create_openlist_core_process, get_openlist_core_status}; use cmd::openlist_core::{create_openlist_core_process, get_openlist_core_status};
use cmd::os_operate::{ use cmd::os_operate::{
get_available_versions, list_files, open_file, open_folder, open_url, open_url_in_browser, get_available_versions, list_files, open_file, open_folder, open_url, open_url_in_browser,
@@ -148,6 +150,8 @@ pub fn run() {
get_logs, get_logs,
clear_logs, clear_logs,
get_admin_password, get_admin_password,
reset_admin_password,
set_admin_password,
get_binary_version, get_binary_version,
select_directory, select_directory,
get_available_versions, get_available_versions,

View File

@@ -79,7 +79,9 @@ export class TauriAPI {
call('get_logs', { source: src }), call('get_logs', { source: src }),
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<boolean> => clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<boolean> =>
call('clear_logs', { source: src }), call('clear_logs', { source: src }),
adminPassword: (): Promise<string> => call('get_admin_password') adminPassword: (): Promise<string> => call('get_admin_password'),
resetAdminPassword: (): Promise<string> => call('reset_admin_password'),
setAdminPassword: (password: string): Promise<string> => call('set_admin_password', { password })
} }
// --- Binary management --- // --- Binary management ---

View File

@@ -27,12 +27,20 @@
</button> </button>
<button <button
@click="showAdminPassword" @click="copyAdminPassword"
class="action-btn password-btn icon-only-btn" class="action-btn password-btn icon-only-btn"
:title="t('dashboard.quickActions.showAdminPassword')" :title="t('dashboard.quickActions.copyAdminPassword')"
> >
<Key :size="16" /> <Key :size="16" />
</button> </button>
<button
@click="resetAdminPassword"
class="action-btn reset-password-btn icon-only-btn"
:title="t('dashboard.quickActions.resetAdminPassword')"
>
<RotateCcw :size="16" />
</button>
</div> </div>
</div> </div>
@@ -164,7 +172,7 @@ const viewMounts = () => {
router.push({ name: 'Mount' }) router.push({ name: 'Mount' })
} }
const showAdminPassword = async () => { const copyAdminPassword = async () => {
try { try {
const password = await appStore.getAdminPassword() const password = await appStore.getAdminPassword()
if (password) { if (password) {
@@ -237,6 +245,15 @@ const showAdminPassword = async () => {
} }
} catch (error) { } catch (error) {
console.error('Failed to get admin password:', error) console.error('Failed to get admin password:', error)
showNotification('error', 'Failed to get admin password. Please check the logs.')
}
}
const resetAdminPassword = async () => {
try {
const newPassword = await appStore.resetAdminPassword()
if (newPassword) {
await navigator.clipboard.writeText(newPassword)
const notification = document.createElement('div') const notification = document.createElement('div')
notification.innerHTML = ` notification.innerHTML = `
@@ -244,7 +261,7 @@ const showAdminPassword = async () => {
position: fixed; position: fixed;
top: 20px; top: 20px;
right: 20px; right: 20px;
background: linear-gradient(135deg, rgb(239, 68, 68), rgb(220, 38, 38)); background: linear-gradient(135deg, rgb(16, 185, 129), rgb(5, 150, 105));
color: white; color: white;
padding: 12px 20px; padding: 12px 20px;
border-radius: 8px; border-radius: 8px;
@@ -252,12 +269,13 @@ const showAdminPassword = async () => {
z-index: 10000; z-index: 10000;
font-weight: 500; font-weight: 500;
max-width: 300px; max-width: 300px;
word-break: break-all;
"> ">
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
<div style="font-size: 18px;"></div> <div style="font-size: 18px;"></div>
<div> <div>
<div style="font-size: 14px; margin-bottom: 4px;">Failed to get admin password</div> <div style="font-size: 14px; margin-bottom: 4px;">Admin password reset and copied!</div>
<div style="font-size: 12px; opacity: 0.9;">Please check the logs.</div> <div style="font-size: 12px; opacity: 0.9; font-family: monospace;">${newPassword}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -269,6 +287,12 @@ const showAdminPassword = async () => {
notification.parentNode.removeChild(notification) notification.parentNode.removeChild(notification)
} }
}, 4000) }, 4000)
} else {
showNotification('error', 'Failed to reset admin password. Please check the logs.')
}
} catch (error) {
console.error('Failed to reset admin password:', error)
showNotification('error', 'Failed to reset admin password. Please check the logs.')
} }
} }
@@ -542,6 +566,12 @@ onUnmounted(() => {
border-color: rgba(147, 51, 234, 0.3); border-color: rgba(147, 51, 234, 0.3);
} }
.reset-password-btn:hover:not(:disabled) {
background: rgb(239, 68, 68);
color: white;
border-color: rgba(220, 38, 38, 0.3);
}
.service-indicator-btn { .service-indicator-btn {
background: var(--color-surface); background: var(--color-surface);
border-color: var(--color-border-secondary); border-color: var(--color-border-secondary);

View File

@@ -32,7 +32,8 @@
"stopRclone": "Stop RClone", "stopRclone": "Stop RClone",
"manageMounts": "Manage Mounts", "manageMounts": "Manage Mounts",
"autoLaunch": "Auto Launch Core(not app)", "autoLaunch": "Auto Launch Core(not app)",
"showAdminPassword": "Show/Copy admin password from logs", "copyAdminPassword": "Copy admin password",
"resetAdminPassword": "Reset admin password",
"firewall": { "firewall": {
"enable": "Allow Port", "enable": "Allow Port",
"disable": "Remove Port Allow", "disable": "Remove Port Allow",
@@ -150,6 +151,19 @@
"title": "Auto-launch on startup", "title": "Auto-launch on startup",
"description": "Automatically start OpenList service when the application launches" "description": "Automatically start OpenList service when the application launches"
} }
},
"admin": {
"title": "Admin Password",
"subtitle": "Manage the admin password for OpenList Core web interface",
"currentPassword": "Admin Password",
"passwordPlaceholder": "Enter admin password or click reset to generate",
"resetTitle": "Reset admin password to a new random value",
"confirmReset": "Are you sure you want to reset the admin password? This will generate a new password and update the OpenList Core configuration.",
"resetSuccess": "Admin password reset successfully! New password has been generated and saved.",
"resetFailed": "Failed to reset admin password. Please check the logs for more details.",
"passwordUpdated": "Admin password updated successfully!",
"passwordUpdateFailed": "Failed to update admin password. Please check the logs for more details.",
"help": "Enter a custom admin password or click the reset button to generate a new random password. Click 'Save Changes' to apply the password to OpenList Core."
} }
}, },
"rclone": { "rclone": {

View File

@@ -32,7 +32,8 @@
"stopRclone": "停止 RClone", "stopRclone": "停止 RClone",
"manageMounts": "管理挂载", "manageMounts": "管理挂载",
"autoLaunch": "自动启动核心(非桌面app)", "autoLaunch": "自动启动核心(非桌面app)",
"showAdminPassword": "显示/复制日志中的管理员密码", "copyAdminPassword": "复制管理员密码",
"resetAdminPassword": "重置管理员密码",
"firewall": { "firewall": {
"enable": "放行端口", "enable": "放行端口",
"disable": "移除端口放行", "disable": "移除端口放行",
@@ -150,6 +151,19 @@
"title": "开机自启", "title": "开机自启",
"description": "应用程序启动时自动启动 OpenList 服务" "description": "应用程序启动时自动启动 OpenList 服务"
} }
},
"admin": {
"title": "管理员密码",
"subtitle": "管理 OpenList 核心网页界面的管理员密码",
"currentPassword": "管理员密码",
"passwordPlaceholder": "输入管理员密码或点击重置生成",
"resetTitle": "重置管理员密码为新的随机值",
"confirmReset": "您确定要重置管理员密码吗?这将生成一个新密码并更新 OpenList 核心配置。",
"resetSuccess": "管理员密码重置成功!已生成新密码并保存。",
"resetFailed": "重置管理员密码失败。请查看日志了解详细信息。",
"passwordUpdated": "管理员密码更新成功!",
"passwordUpdateFailed": "更新管理员密码失败。请查看日志了解详细信息。",
"help": "输入自定义管理员密码或点击重置按钮生成新的随机密码。点击'保存更改'将密码应用到 OpenList 核心。"
} }
}, },
"rclone": { "rclone": {

View File

@@ -9,7 +9,14 @@ export const useAppStore = defineStore('app', () => {
const settings = ref<MergedSettings>({ const settings = ref<MergedSettings>({
openlist: { port: 5244, data_dir: '', auto_launch: false, ssl_enabled: false }, openlist: { port: 5244, data_dir: '', auto_launch: false, ssl_enabled: false },
rclone: { config: {} }, rclone: { config: {} },
app: { theme: 'light', auto_update_enabled: true, gh_proxy: '', gh_proxy_api: false, open_links_in_browser: false } app: {
theme: 'light',
auto_update_enabled: true,
gh_proxy: '',
gh_proxy_api: false,
open_links_in_browser: false,
admin_password: undefined
}
}) })
const openlistCoreStatus = ref<OpenListCoreStatus>({ running: false }) const openlistCoreStatus = ref<OpenListCoreStatus>({ running: false })
const remoteConfigs = ref<IRemoteConfig>({}) const remoteConfigs = ref<IRemoteConfig>({})
@@ -698,6 +705,36 @@ export const useAppStore = defineStore('app', () => {
} }
} }
async function resetAdminPassword(): Promise<string | null> {
try {
const newPassword = await TauriAPI.logs.resetAdminPassword()
if (newPassword) {
settings.value.app.admin_password = newPassword
await saveSettings()
}
return newPassword
} catch (err) {
console.error('Failed to reset admin password:', err)
return null
}
}
async function setAdminPassword(password: string): Promise<boolean> {
try {
await TauriAPI.logs.setAdminPassword(password)
settings.value.app.admin_password = password
await saveSettings()
return true
} catch (err) {
console.error('Failed to set admin password:', err)
return false
}
}
// Update management functions // Update management functions
function setUpdateAvailable(available: boolean, updateInfo?: UpdateCheck) { function setUpdateAvailable(available: boolean, updateInfo?: UpdateCheck) {
updateAvailable.value = available updateAvailable.value = available
@@ -756,6 +793,8 @@ export const useAppStore = defineStore('app', () => {
clearError, clearError,
init, init,
getAdminPassword, getAdminPassword,
resetAdminPassword,
setAdminPassword,
setTheme, setTheme,
toggleTheme, toggleTheme,

View File

@@ -49,6 +49,7 @@ interface AppConfig {
gh_proxy?: string gh_proxy?: string
gh_proxy_api?: boolean gh_proxy_api?: boolean
open_links_in_browser?: boolean open_links_in_browser?: boolean
admin_password?: string
} }
interface MergedSettings { interface MergedSettings {

View File

@@ -16,6 +16,7 @@ const messageType = ref<'success' | 'error' | 'info'>('info')
const activeTab = ref('openlist') const activeTab = ref('openlist')
const rcloneConfigJson = ref('') const rcloneConfigJson = ref('')
const autoStartApp = ref(false) const autoStartApp = ref(false)
const isResettingPassword = ref(false)
const openlistCoreSettings = reactive({ ...appStore.settings.openlist }) const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
const rcloneSettings = reactive({ ...appStore.settings.rclone }) const rcloneSettings = reactive({ ...appStore.settings.rclone })
@@ -73,8 +74,12 @@ onMounted(async () => {
if (!appSettings.gh_proxy) appSettings.gh_proxy = '' if (!appSettings.gh_proxy) appSettings.gh_proxy = ''
if (appSettings.gh_proxy_api === undefined) appSettings.gh_proxy_api = false if (appSettings.gh_proxy_api === undefined) appSettings.gh_proxy_api = false
if (appSettings.open_links_in_browser === undefined) appSettings.open_links_in_browser = false if (appSettings.open_links_in_browser === undefined) appSettings.open_links_in_browser = false
if (!appSettings.admin_password) appSettings.admin_password = ''
originalOpenlistPort = openlistCoreSettings.port || 5244 originalOpenlistPort = openlistCoreSettings.port || 5244
originalDataDir = openlistCoreSettings.data_dir originalDataDir = openlistCoreSettings.data_dir
// Load current admin password
await loadCurrentAdminPassword()
}) })
const hasUnsavedChanges = computed(() => { const hasUnsavedChanges = computed(() => {
@@ -111,17 +116,33 @@ const handleSave = async () => {
appStore.settings.openlist = { ...openlistCoreSettings } appStore.settings.openlist = { ...openlistCoreSettings }
appStore.settings.rclone = { ...rcloneSettings } appStore.settings.rclone = { ...rcloneSettings }
appStore.settings.app = { ...appSettings } appStore.settings.app = { ...appSettings }
const originalAdminPassword = appStore.settings.app.admin_password
const needsPasswordUpdate = originalAdminPassword !== appSettings.admin_password && appSettings.admin_password
if (originalOpenlistPort !== openlistCoreSettings.port || originalDataDir !== openlistCoreSettings.data_dir) { if (originalOpenlistPort !== openlistCoreSettings.port || originalDataDir !== openlistCoreSettings.data_dir) {
await appStore.saveSettingsWithCoreUpdate() await appStore.saveSettingsWithCoreUpdate()
} else { } else {
await appStore.saveSettings() await appStore.saveSettings()
} }
originalOpenlistPort = openlistCoreSettings.port || 5244 if (needsPasswordUpdate) {
originalDataDir = openlistCoreSettings.data_dir try {
await appStore.setAdminPassword(appSettings.admin_password!)
message.value = t('settings.service.admin.passwordUpdated')
messageType.value = 'success'
} catch (error) {
console.error('Failed to update admin password:', error)
message.value = t('settings.service.admin.passwordUpdateFailed')
messageType.value = 'error'
}
} else {
message.value = t('settings.saved') message.value = t('settings.saved')
messageType.value = 'success' messageType.value = 'success'
}
originalOpenlistPort = openlistCoreSettings.port || 5244
originalDataDir = openlistCoreSettings.data_dir
} catch (error) { } catch (error) {
message.value = t('settings.saveFailed') message.value = t('settings.saveFailed')
messageType.value = 'error' messageType.value = 'error'
@@ -177,6 +198,42 @@ const handleSelectDataDir = async () => {
}, 3000) }, 3000)
} }
} }
const handleResetAdminPassword = async () => {
isResettingPassword.value = true
try {
const newPassword = await appStore.resetAdminPassword()
if (newPassword) {
appSettings.admin_password = newPassword
message.value = t('settings.service.admin.resetSuccess')
messageType.value = 'success'
await navigator.clipboard.writeText(newPassword)
} else {
message.value = t('settings.service.admin.resetFailed')
messageType.value = 'error'
}
} catch (error) {
console.error('Failed to reset admin password:', error)
message.value = t('settings.service.admin.resetFailed')
messageType.value = 'error'
} finally {
isResettingPassword.value = false
setTimeout(() => {
message.value = ''
}, 3000)
}
}
const loadCurrentAdminPassword = async () => {
try {
const password = await appStore.getAdminPassword()
if (password) {
appSettings.admin_password = password
}
} catch (error) {
console.error('Failed to load admin password:', error)
}
}
</script> </script>
<template> <template>
@@ -288,6 +345,33 @@ const handleSelectDataDir = async () => {
</label> </label>
</div> </div>
</div> </div>
<div class="settings-section">
<h2>{{ t('settings.service.admin.title') }}</h2>
<p>{{ t('settings.service.admin.subtitle') }}</p>
<div class="form-group">
<label>{{ t('settings.service.admin.currentPassword') }}</label>
<div class="input-group">
<input
v-model="appSettings.admin_password"
type="text"
class="form-input"
:placeholder="t('settings.service.admin.passwordPlaceholder')"
/>
<button
type="button"
@click="handleResetAdminPassword"
:disabled="isResettingPassword"
class="input-addon-btn reset-password-btn"
:title="t('settings.service.admin.resetTitle')"
>
<RotateCcw :size="16" />
</button>
</div>
<small>{{ t('settings.service.admin.help') }}</small>
</div>
</div>
</div> </div>
<div v-if="activeTab === 'rclone'" class="tab-content"> <div v-if="activeTab === 'rclone'" class="tab-content">

View File

@@ -384,6 +384,22 @@
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.input-addon-btn.reset-password-btn {
background: var(--color-error-background, #fef2f2);
color: var(--color-error, #dc2626);
border-color: var(--color-error-border, #fecaca);
}
.input-addon-btn.reset-password-btn:hover:not(:disabled) {
background: var(--color-error, #dc2626);
color: white;
}
.input-addon-btn.reset-password-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Switch */ /* Switch */
.switch-label { .switch-label {
display: flex; display: flex;