mirror of https://github.com/veypi/OneAuth.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
737 lines
24 KiB
HTML
737 lines
24 KiB
HTML
|
3 weeks ago
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
|
||
|
|
<head>
|
||
|
|
<meta name="description" content="System Settings Management">
|
||
|
|
<title>{{ $t('nav.settings') }}</title>
|
||
|
|
<style>
|
||
|
|
.settings-container {
|
||
|
|
padding: 20px;
|
||
|
|
max-width: 900px;
|
||
|
|
margin: 0 auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-title {
|
||
|
|
font-size: 24px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--color-text, #1f2937);
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.settings-card {
|
||
|
|
background: var(--bg-color-secondary, #fff);
|
||
|
|
border-radius: var(--border-radius, 8px);
|
||
|
|
box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.05));
|
||
|
|
margin-bottom: 20px;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-header {
|
||
|
|
background: var(--bg-color-tertiary, #f9fafb);
|
||
|
|
padding: 16px 20px;
|
||
|
|
border-bottom: 1px solid var(--color-border, #e5e7eb);
|
||
|
|
font-weight: 600;
|
||
|
|
font-size: 16px;
|
||
|
|
color: var(--color-text, #1f2937);
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-header i {
|
||
|
|
color: var(--color-primary, #4f46e5);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-body {
|
||
|
|
padding: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: flex-start;
|
||
|
|
padding: 16px 0;
|
||
|
|
border-bottom: 1px solid var(--color-border, #e5e7eb);
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-item:last-child {
|
||
|
|
border-bottom: none;
|
||
|
|
padding-bottom: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-item:first-child {
|
||
|
|
padding-top: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-info {
|
||
|
|
flex: 1;
|
||
|
|
margin-right: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-label {
|
||
|
|
font-weight: 500;
|
||
|
|
color: var(--color-text, #1f2937);
|
||
|
|
margin-bottom: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-desc {
|
||
|
|
font-size: 13px;
|
||
|
|
color: var(--color-text-light, #6b7280);
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-control {
|
||
|
|
min-width: 200px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: flex-end;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-input {
|
||
|
|
width: 100%;
|
||
|
|
padding: 8px 12px;
|
||
|
|
border: 1px solid var(--color-border, #d1d5db);
|
||
|
|
border-radius: var(--border-radius, 6px);
|
||
|
|
font-size: 14px;
|
||
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-input:focus {
|
||
|
|
outline: none;
|
||
|
|
border-color: var(--color-primary, #4f46e5);
|
||
|
|
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting-select {
|
||
|
|
width: 100%;
|
||
|
|
padding: 8px 12px;
|
||
|
|
border: 1px solid var(--color-border, #d1d5db);
|
||
|
|
border-radius: var(--border-radius, 6px);
|
||
|
|
font-size: 14px;
|
||
|
|
background-color: var(--bg-color-secondary, #fff);
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toggle-switch {
|
||
|
|
position: relative;
|
||
|
|
display: inline-block;
|
||
|
|
width: 48px;
|
||
|
|
height: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toggle-switch input {
|
||
|
|
opacity: 0;
|
||
|
|
width: 0;
|
||
|
|
height: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toggle-slider {
|
||
|
|
position: absolute;
|
||
|
|
cursor: pointer;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
bottom: 0;
|
||
|
|
background-color: #ccc;
|
||
|
|
transition: .3s;
|
||
|
|
border-radius: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toggle-slider:before {
|
||
|
|
position: absolute;
|
||
|
|
content: "";
|
||
|
|
height: 18px;
|
||
|
|
width: 18px;
|
||
|
|
left: 3px;
|
||
|
|
bottom: 3px;
|
||
|
|
background-color: white;
|
||
|
|
transition: .3s;
|
||
|
|
border-radius: 50%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toggle-switch input:checked + .toggle-slider {
|
||
|
|
background-color: var(--color-primary, #4f46e5);
|
||
|
|
}
|
||
|
|
|
||
|
|
.toggle-switch input:checked + .toggle-slider:before {
|
||
|
|
transform: translateX(24px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.actions-bar {
|
||
|
|
display: flex;
|
||
|
|
justify-content: flex-end;
|
||
|
|
gap: 12px;
|
||
|
|
margin-top: 24px;
|
||
|
|
padding-top: 20px;
|
||
|
|
border-top: 1px solid var(--color-border, #e5e7eb);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn {
|
||
|
|
padding: 10px 20px;
|
||
|
|
border-radius: var(--border-radius, 6px);
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 500;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.2s;
|
||
|
|
border: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary {
|
||
|
|
background-color: var(--color-primary, #4f46e5);
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary:hover {
|
||
|
|
background-color: var(--color-primary-dark, #4338ca);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary:disabled {
|
||
|
|
opacity: 0.6;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-secondary {
|
||
|
|
background-color: var(--bg-color-tertiary, #f3f4f6);
|
||
|
|
color: var(--color-text, #1f2937);
|
||
|
|
border: 1px solid var(--color-border, #d1d5db);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-secondary:hover {
|
||
|
|
background-color: var(--bg-color-secondary, #e5e7eb);
|
||
|
|
}
|
||
|
|
|
||
|
|
.loading-overlay {
|
||
|
|
position: fixed;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
bottom: 0;
|
||
|
|
background: rgba(255, 255, 255, 0.8);
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
z-index: 1000;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loading-spinner {
|
||
|
|
width: 40px;
|
||
|
|
height: 40px;
|
||
|
|
border: 3px solid var(--color-border, #e5e7eb);
|
||
|
|
border-top-color: var(--color-primary, #4f46e5);
|
||
|
|
border-radius: 50%;
|
||
|
|
animation: spin 1s linear infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes spin {
|
||
|
|
to { transform: rotate(360deg); }
|
||
|
|
}
|
||
|
|
|
||
|
|
.json-editor {
|
||
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
|
|
font-size: 13px;
|
||
|
|
min-height: 80px;
|
||
|
|
resize: vertical;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag-list {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 6px;
|
||
|
|
padding: 4px 10px;
|
||
|
|
background: var(--color-primary-light, #e0e7ff);
|
||
|
|
color: var(--color-primary, #4f46e5);
|
||
|
|
border-radius: 4px;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag-remove {
|
||
|
|
cursor: pointer;
|
||
|
|
opacity: 0.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag-remove:hover {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag-input {
|
||
|
|
border: none;
|
||
|
|
background: transparent;
|
||
|
|
padding: 4px 8px;
|
||
|
|
font-size: 13px;
|
||
|
|
min-width: 100px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag-input:focus {
|
||
|
|
outline: none;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
|
||
|
|
<body>
|
||
|
|
<div class="settings-container">
|
||
|
|
<div class="page-header">
|
||
|
|
<h1 class="page-title">{{ $t('nav.settings') }}</h1>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-if="loading" class="loading-overlay">
|
||
|
|
<div class="loading-spinner"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Application Settings -->
|
||
|
|
<div class="settings-card">
|
||
|
|
<div class="card-header">
|
||
|
|
<i class="fa-solid fa-cube"></i>
|
||
|
|
{{ $t('settings.category.app') }}
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.app.name') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.app.name_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input" v:value="settings['app.name']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.app.id') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.app.id_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input" v:value="settings['app.id']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Authentication Settings -->
|
||
|
|
<div class="settings-card">
|
||
|
|
<div class="card-header">
|
||
|
|
<i class="fa-solid fa-shield-halved"></i>
|
||
|
|
{{ $t('settings.category.auth') }}
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.auth.reg_require_email') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.auth.reg_require_email_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<label class="toggle-switch">
|
||
|
|
<input type="checkbox" :checked="settings['auth.reg.require_email'] === 'true'" @change="toggleSetting('auth.reg.require_email')" />
|
||
|
|
<span class="toggle-slider"></span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.auth.reg_require_phone') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.auth.reg_require_phone_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<label class="toggle-switch">
|
||
|
|
<input type="checkbox" :checked="settings['auth.reg.require_phone'] === 'true'" @change="toggleSetting('auth.reg.require_phone')" />
|
||
|
|
<span class="toggle-slider"></span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.auth.login_methods') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.auth.login_methods_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input json-editor" v:value="settings['auth.login.methods']" placeholder='["password", "email_code"]' />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.auth.password_fields') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.auth.password_fields_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input json-editor" v:value="settings['auth.login.password_fields']" placeholder='["username", "email"]' />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Security Settings -->
|
||
|
|
<div class="settings-card">
|
||
|
|
<div class="card-header">
|
||
|
|
<i class="fa-solid fa-lock"></i>
|
||
|
|
{{ $t('settings.category.security') }}
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.security.captcha_enabled') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.security.captcha_enabled_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<label class="toggle-switch">
|
||
|
|
<input type="checkbox" :checked="settings['security.captcha_enabled'] === 'true'" @change="toggleSetting('security.captcha_enabled')" />
|
||
|
|
<span class="toggle-slider"></span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.security.max_login_attempts') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.security.max_login_attempts_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="number" class="setting-input" v:value="settings['security.max_login_attempts']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.security.bcrypt_cost') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.security.bcrypt_cost_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="number" class="setting-input" v:value="settings['security.bcrypt_cost']" min="4" max="31" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Verification Code Settings -->
|
||
|
|
<div class="settings-card">
|
||
|
|
<div class="card-header">
|
||
|
|
<i class="fa-solid fa-key"></i>
|
||
|
|
{{ $t('settings.category.code') }}
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.code.expiry') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.code.expiry_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="number" class="setting-input" v:value="settings['code.expiry']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.code.length') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.code.length_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="number" class="setting-input" v:value="settings['code.length']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.code.max_attempt') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.code.max_attempt_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="number" class="setting-input" v:value="settings['code.max_attempt']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.code.send_interval') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.code.send_interval_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="number" class="setting-input" v:value="settings['code.send_interval']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.code.max_daily_count') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.code.max_daily_count_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="number" class="setting-input" v:value="settings['code.max_daily_count']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Email Settings -->
|
||
|
|
<div class="settings-card">
|
||
|
|
<div class="card-header">
|
||
|
|
<i class="fa-solid fa-envelope"></i>
|
||
|
|
{{ $t('settings.category.email') }}
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.email.enabled') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.email.enabled_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<label class="toggle-switch">
|
||
|
|
<input type="checkbox" :checked="settings['email.enabled'] === 'true'" @change="toggleSetting('email.enabled')" />
|
||
|
|
<span class="toggle-slider"></span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.email.provider') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.email.provider_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<select class="setting-select" v:value="settings['email.provider']">
|
||
|
|
<option value="smtp">SMTP</option>
|
||
|
|
<option value="sendgrid">SendGrid</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.email.smtp_host') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.email.smtp_host_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input" v:value="settings['email.smtp.host']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.email.smtp_port') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.email.smtp_port_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="number" class="setting-input" v:value="settings['email.smtp.port']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.email.smtp_user') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.email.smtp_user_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input" v:value="settings['email.smtp.user']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.email.smtp_pass') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.email.smtp_pass_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="password" class="setting-input" v:value="settings['email.smtp.pass']" placeholder="******" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.email.from') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.email.from_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="email" class="setting-input" v:value="settings['email.from']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.email.from_name') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.email.from_name_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input" v:value="settings['email.from_name']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- SMS Settings -->
|
||
|
|
<div class="settings-card">
|
||
|
|
<div class="card-header">
|
||
|
|
<i class="fa-solid fa-comment-sms"></i>
|
||
|
|
{{ $t('settings.category.sms') }}
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="setting-item">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.sms.enabled') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.sms.enabled_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<label class="toggle-switch">
|
||
|
|
<input type="checkbox" :checked="settings['sms.enabled'] === 'true'" @change="toggleSetting('sms.enabled')" />
|
||
|
|
<span class="toggle-slider"></span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.sms.provider') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.sms.provider_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<select class="setting-select" v:value="settings['sms.provider']">
|
||
|
|
<option value="aliyun">{{ $t('settings.sms.provider_aliyun') }}</option>
|
||
|
|
<option value="tencent">{{ $t('settings.sms.provider_tencent') }}</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.sms.access_key') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.sms.access_key_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input" v:value="settings['sms.access_key']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.sms.access_secret') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.sms.access_secret_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="password" class="setting-input" v:value="settings['sms.access_secret']" placeholder="******" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.sms.sign_name') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.sms.sign_name_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input" v:value="settings['sms.sign_name']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
|
||
|
|
<div class="setting-info">
|
||
|
|
<div class="setting-label">{{ $t('settings.sms.template_code') }}</div>
|
||
|
|
<div class="setting-desc">{{ $t('settings.sms.template_code_desc') }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="setting-control">
|
||
|
|
<input type="text" class="setting-input" v:value="settings['sms.template_code']" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="actions-bar">
|
||
|
|
<button class="btn btn-secondary" @click="resetSettings">{{ $t('common.reset') }}</button>
|
||
|
|
<button class="btn btn-primary" @click="saveSettings" :disabled="saving">
|
||
|
|
{{ saving ? $t('common.saving') : $t('common.save') }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</body>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
loading = true;
|
||
|
|
saving = false;
|
||
|
|
settings = {};
|
||
|
|
originalSettings = {};
|
||
|
|
|
||
|
|
// Setting keys to manage
|
||
|
|
settingKeys = [
|
||
|
|
'app.name',
|
||
|
|
'app.id',
|
||
|
|
'auth.reg.require_email',
|
||
|
|
'auth.reg.require_phone',
|
||
|
|
'auth.login.methods',
|
||
|
|
'auth.login.password_fields',
|
||
|
|
'security.captcha_enabled',
|
||
|
|
'security.max_login_attempts',
|
||
|
|
'security.bcrypt_cost',
|
||
|
|
'code.expiry',
|
||
|
|
'code.length',
|
||
|
|
'code.max_attempt',
|
||
|
|
'code.send_interval',
|
||
|
|
'code.max_daily_count',
|
||
|
|
'email.enabled',
|
||
|
|
'email.provider',
|
||
|
|
'email.smtp.host',
|
||
|
|
'email.smtp.port',
|
||
|
|
'email.smtp.user',
|
||
|
|
'email.smtp.pass',
|
||
|
|
'email.from',
|
||
|
|
'email.from_name',
|
||
|
|
'sms.enabled',
|
||
|
|
'sms.provider',
|
||
|
|
'sms.access_key',
|
||
|
|
'sms.access_secret',
|
||
|
|
'sms.sign_name',
|
||
|
|
'sms.template_code'
|
||
|
|
];
|
||
|
|
|
||
|
|
// Load settings from API
|
||
|
|
loadSettings = async () => {
|
||
|
|
loading = true;
|
||
|
|
try {
|
||
|
|
const response = await $axios.get('/api/settings');
|
||
|
|
if (response && response.items) {
|
||
|
|
const settingsMap = {};
|
||
|
|
response.items.forEach(item => {
|
||
|
|
settingsMap[item.key] = item.value;
|
||
|
|
});
|
||
|
|
settings = settingsMap;
|
||
|
|
originalSettings = { ...settingsMap };
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
$message.error(error.message || $t('settings.load_failed'));
|
||
|
|
} finally {
|
||
|
|
loading = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Toggle boolean setting
|
||
|
|
toggleSetting = (key) => {
|
||
|
|
settings[key] = settings[key] === 'true' ? 'false' : 'true';
|
||
|
|
};
|
||
|
|
|
||
|
|
// Reset to original values
|
||
|
|
resetSettings = () => {
|
||
|
|
settings = { ...originalSettings };
|
||
|
|
$message.info($t('settings.reset_done'));
|
||
|
|
};
|
||
|
|
|
||
|
|
// Save all settings
|
||
|
|
saveSettings = async () => {
|
||
|
|
saving = true;
|
||
|
|
try {
|
||
|
|
const settingsToUpdate = [];
|
||
|
|
settingKeys.forEach(key => {
|
||
|
|
if (settings[key] !== undefined && settings[key] !== originalSettings[key]) {
|
||
|
|
settingsToUpdate.push({
|
||
|
|
key: key,
|
||
|
|
value: String(settings[key])
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (settingsToUpdate.length === 0) {
|
||
|
|
$message.info($t('settings.no_changes'));
|
||
|
|
saving = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await $axios.put('/api/settings', { settings: settingsToUpdate });
|
||
|
|
originalSettings = { ...settings };
|
||
|
|
$message.success($t('settings.save_success'));
|
||
|
|
} catch (error) {
|
||
|
|
$message.error(error.message || $t('settings.save_failed'));
|
||
|
|
} finally {
|
||
|
|
saving = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initialize
|
||
|
|
loadSettings();
|
||
|
|
</script>
|
||
|
|
|
||
|
|
</html>
|