From 9c4f22ff9795c3fd6d5e2540d67c3f774c33238f Mon Sep 17 00:00:00 2001 From: nanxun Date: Mon, 27 Apr 2026 23:16:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E5=AF=86=E7=A0=81=E4=BF=AE=E6=94=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/stores/auth.ts | 8 ++ frontend/src/views/SettingsView.vue | 76 +++++++++++++++++++ .../Abstractions/Auth/IAuthService.cs | 2 + .../Persistence/PersistenceContracts.cs | 2 + .../Models/Auth/AuthModels.cs | 7 ++ .../Services/AuthService.cs | 27 +++++++ .../Persistence/Repositories/Repositories.cs | 3 + .../Controllers/AuthController.cs | 10 +++ 8 files changed, 135 insertions(+) diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index f3663de..a369347 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -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 }; }); diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 5d62646..b633895 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -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 | 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); + +

管理员密码

+

修改当前登录账户的密码,修改后立即生效。

+ + + + + + + + + + + + 修改密码 + +
+

Douyin 请求头

这些伪装参数会同时用于 Douyin API 请求和 ffmpeg 输入拉流,适合处理直播流读取校验问题。

diff --git a/src/LiveRecorder.Application/Abstractions/Auth/IAuthService.cs b/src/LiveRecorder.Application/Abstractions/Auth/IAuthService.cs index 252cb79..6a1f67d 100644 --- a/src/LiveRecorder.Application/Abstractions/Auth/IAuthService.cs +++ b/src/LiveRecorder.Application/Abstractions/Auth/IAuthService.cs @@ -9,4 +9,6 @@ public interface IAuthService Task LogoutAsync(string token, CancellationToken cancellationToken = default); Task ValidateTokenAsync(string token, CancellationToken cancellationToken = default); + + Task ChangePasswordAsync(Guid userId, ChangePasswordRequest request, CancellationToken cancellationToken = default); } diff --git a/src/LiveRecorder.Application/Abstractions/Persistence/PersistenceContracts.cs b/src/LiveRecorder.Application/Abstractions/Persistence/PersistenceContracts.cs index 03f308f..9b0fb53 100644 --- a/src/LiveRecorder.Application/Abstractions/Persistence/PersistenceContracts.cs +++ b/src/LiveRecorder.Application/Abstractions/Persistence/PersistenceContracts.cs @@ -109,6 +109,8 @@ public interface IUserAccountRepository Task GetByUsernameAsync(string username, CancellationToken cancellationToken = default); Task AddAsync(UserAccount userAccount, CancellationToken cancellationToken = default); + + void Update(UserAccount userAccount); } public interface IUserSessionRepository diff --git a/src/LiveRecorder.Application/Models/Auth/AuthModels.cs b/src/LiveRecorder.Application/Models/Auth/AuthModels.cs index e9e5a81..1f0e441 100644 --- a/src/LiveRecorder.Application/Models/Auth/AuthModels.cs +++ b/src/LiveRecorder.Application/Models/Auth/AuthModels.cs @@ -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; +} diff --git a/src/LiveRecorder.Application/Services/AuthService.cs b/src/LiveRecorder.Application/Services/AuthService.cs index 7e9dcf1..904a635 100644 --- a/src/LiveRecorder.Application/Services/AuthService.cs +++ b/src/LiveRecorder.Application/Services/AuthService.cs @@ -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); + } } diff --git a/src/LiveRecorder.Infrastructure/Persistence/Repositories/Repositories.cs b/src/LiveRecorder.Infrastructure/Persistence/Repositories/Repositories.cs index 8c5784d..cbb4719 100644 --- a/src/LiveRecorder.Infrastructure/Persistence/Repositories/Repositories.cs +++ b/src/LiveRecorder.Infrastructure/Persistence/Repositories/Repositories.cs @@ -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 diff --git a/src/LiveRecorder.WebApi/Controllers/AuthController.cs b/src/LiveRecorder.WebApi/Controllers/AuthController.cs index bbd4c4b..292877d 100644 --- a/src/LiveRecorder.WebApi/Controllers/AuthController.cs +++ b/src/LiveRecorder.WebApi/Controllers/AuthController.cs @@ -33,4 +33,14 @@ public sealed class AuthController : ControllerBase await _authService.LogoutAsync(token, cancellationToken); return NoContent(); } + + [HttpPost("change-password")] + public async Task 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(); + } }