feat: 添加管理员密码修改功能
This commit is contained in:
@@ -49,12 +49,20 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function changePassword(currentPassword: string, newPassword: string) {
|
||||
await apiClient.post("/auth/change-password", {
|
||||
currentPassword,
|
||||
newPassword
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
isAuthenticated,
|
||||
login,
|
||||
logout,
|
||||
changePassword,
|
||||
clearSession
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,11 +9,15 @@ import type {
|
||||
WebhookTestResult
|
||||
} from "@/types";
|
||||
import { outputFormatLabelMap, recordingTemplateLabelMap, saveModeLabelMap } from "@/types";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
type ScriptEventType = "live_started" | "live_ended" | "segment_completed";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const changingPassword = ref(false);
|
||||
const testingEmail = ref(false);
|
||||
const testingWebhook = ref(false);
|
||||
const runningRetentionCleanup = ref(false);
|
||||
@@ -263,6 +267,54 @@ async function loadSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
const pwdForm = reactive({
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: ""
|
||||
});
|
||||
|
||||
const pwdRules = {
|
||||
currentPassword: [{ required: true, message: "请输入当前密码", trigger: "blur" }],
|
||||
newPassword: [
|
||||
{ required: true, message: "请输入新密码", trigger: "blur" },
|
||||
{ min: 6, message: "新密码长度不能少于 6 位", trigger: "blur" }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: "请再次输入新密码", trigger: "blur" },
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (value !== pwdForm.newPassword) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const pwdFormRef = ref<InstanceType<typeof import("element-plus").ElForm> | null>(null);
|
||||
|
||||
async function changePassword() {
|
||||
const valid = await pwdFormRef.value?.validate().catch(() => false);
|
||||
if (!valid) return;
|
||||
|
||||
changingPassword.value = true;
|
||||
try {
|
||||
await authStore.changePassword(pwdForm.currentPassword, pwdForm.newPassword);
|
||||
ElMessage.success("密码已修改,请妥善保管新密码。");
|
||||
pwdForm.currentPassword = "";
|
||||
pwdForm.newPassword = "";
|
||||
pwdForm.confirmPassword = "";
|
||||
pwdFormRef.value?.resetFields();
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorMessage(error, "密码修改失败,请稍后重试。"));
|
||||
} finally {
|
||||
changingPassword.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
saving.value = true;
|
||||
|
||||
@@ -1370,6 +1422,30 @@ onMounted(loadSettings);
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
|
||||
<h3 class="section-title">管理员密码</h3>
|
||||
<p class="section-subtitle">修改当前登录账户的密码,修改后立即生效。</p>
|
||||
|
||||
<el-form
|
||||
ref="pwdFormRef"
|
||||
:model="pwdForm"
|
||||
:rules="pwdRules"
|
||||
label-position="top"
|
||||
style="max-width: 480px;"
|
||||
>
|
||||
<el-form-item label="当前密码" prop="currentPassword">
|
||||
<el-input v-model="pwdForm.currentPassword" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input v-model="pwdForm.newPassword" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码" prop="confirmPassword">
|
||||
<el-input v-model="pwdForm.confirmPassword" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="changingPassword" @click="changePassword">修改密码</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
|
||||
<h3 class="section-title">Douyin 请求头</h3>
|
||||
<p class="section-subtitle">这些伪装参数会同时用于 Douyin API 请求和 ffmpeg 输入拉流,适合处理直播流读取校验问题。</p>
|
||||
|
||||
@@ -9,4 +9,6 @@ public interface IAuthService
|
||||
Task LogoutAsync(string token, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AuthenticatedUser?> ValidateTokenAsync(string token, CancellationToken cancellationToken = default);
|
||||
|
||||
Task ChangePasswordAsync(Guid userId, ChangePasswordRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@ public interface IUserAccountRepository
|
||||
Task<UserAccount?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(UserAccount userAccount, CancellationToken cancellationToken = default);
|
||||
|
||||
void Update(UserAccount userAccount);
|
||||
}
|
||||
|
||||
public interface IUserSessionRepository
|
||||
|
||||
@@ -28,3 +28,10 @@ public sealed class LoginResponse
|
||||
|
||||
public required AuthenticatedUser User { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ChangePasswordRequest
|
||||
{
|
||||
public string CurrentPassword { get; set; } = string.Empty;
|
||||
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -112,4 +112,31 @@ public sealed class AuthService : IAuthService
|
||||
ExpiresAt = session.ExpiresAt
|
||||
};
|
||||
}
|
||||
|
||||
public async Task ChangePasswordAsync(Guid userId, ChangePasswordRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.CurrentPassword) || string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
{
|
||||
throw new InvalidOperationException("当前密码和新密码不能为空。");
|
||||
}
|
||||
|
||||
if (request.NewPassword.Length < 6)
|
||||
{
|
||||
throw new InvalidOperationException("新密码长度不能少于 6 位。");
|
||||
}
|
||||
|
||||
var user = await _userAccountRepository.GetByIdAsync(userId, cancellationToken)
|
||||
?? throw new InvalidOperationException("用户不存在。");
|
||||
|
||||
if (!PasswordHasher.Verify(request.CurrentPassword, user.PasswordHash))
|
||||
{
|
||||
throw new InvalidOperationException("当前密码不正确。");
|
||||
}
|
||||
|
||||
user.UpdatePassword(PasswordHasher.Hash(request.NewPassword));
|
||||
_userAccountRepository.Update(user);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +323,9 @@ public sealed class UserAccountRepository : IUserAccountRepository
|
||||
|
||||
public Task AddAsync(UserAccount userAccount, CancellationToken cancellationToken = default) =>
|
||||
_dbContext.UserAccounts.AddAsync(userAccount, cancellationToken).AsTask();
|
||||
|
||||
public void Update(UserAccount userAccount) =>
|
||||
_dbContext.UserAccounts.Update(userAccount);
|
||||
}
|
||||
|
||||
public sealed class UserSessionRepository : IUserSessionRepository
|
||||
|
||||
@@ -33,4 +33,14 @@ public sealed class AuthController : ControllerBase
|
||||
await _authService.LogoutAsync(token, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("change-password")]
|
||||
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as AuthenticatedUser
|
||||
?? throw new InvalidOperationException("未登录。");
|
||||
|
||||
await _authService.ChangePasswordAsync(currentUser.UserId, request, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user