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 {
|
return {
|
||||||
token,
|
token,
|
||||||
user,
|
user,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
changePassword,
|
||||||
clearSession
|
clearSession
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ import type {
|
|||||||
WebhookTestResult
|
WebhookTestResult
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import { outputFormatLabelMap, recordingTemplateLabelMap, saveModeLabelMap } from "@/types";
|
import { outputFormatLabelMap, recordingTemplateLabelMap, saveModeLabelMap } from "@/types";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
type ScriptEventType = "live_started" | "live_ended" | "segment_completed";
|
type ScriptEventType = "live_started" | "live_ended" | "segment_completed";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
const changingPassword = ref(false);
|
||||||
const testingEmail = ref(false);
|
const testingEmail = ref(false);
|
||||||
const testingWebhook = ref(false);
|
const testingWebhook = ref(false);
|
||||||
const runningRetentionCleanup = 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() {
|
async function saveSettings() {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
|
||||||
@@ -1370,6 +1422,30 @@ onMounted(loadSettings);
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</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">
|
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
|
||||||
<h3 class="section-title">Douyin 请求头</h3>
|
<h3 class="section-title">Douyin 请求头</h3>
|
||||||
<p class="section-subtitle">这些伪装参数会同时用于 Douyin API 请求和 ffmpeg 输入拉流,适合处理直播流读取校验问题。</p>
|
<p class="section-subtitle">这些伪装参数会同时用于 Douyin API 请求和 ffmpeg 输入拉流,适合处理直播流读取校验问题。</p>
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ public interface IAuthService
|
|||||||
Task LogoutAsync(string token, CancellationToken cancellationToken = default);
|
Task LogoutAsync(string token, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<AuthenticatedUser?> ValidateTokenAsync(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<UserAccount?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task AddAsync(UserAccount userAccount, CancellationToken cancellationToken = default);
|
Task AddAsync(UserAccount userAccount, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
void Update(UserAccount userAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IUserSessionRepository
|
public interface IUserSessionRepository
|
||||||
|
|||||||
@@ -28,3 +28,10 @@ public sealed class LoginResponse
|
|||||||
|
|
||||||
public required AuthenticatedUser User { get; init; }
|
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
|
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) =>
|
public Task AddAsync(UserAccount userAccount, CancellationToken cancellationToken = default) =>
|
||||||
_dbContext.UserAccounts.AddAsync(userAccount, cancellationToken).AsTask();
|
_dbContext.UserAccounts.AddAsync(userAccount, cancellationToken).AsTask();
|
||||||
|
|
||||||
|
public void Update(UserAccount userAccount) =>
|
||||||
|
_dbContext.UserAccounts.Update(userAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class UserSessionRepository : IUserSessionRepository
|
public sealed class UserSessionRepository : IUserSessionRepository
|
||||||
|
|||||||
@@ -33,4 +33,14 @@ public sealed class AuthController : ControllerBase
|
|||||||
await _authService.LogoutAsync(token, cancellationToken);
|
await _authService.LogoutAsync(token, cancellationToken);
|
||||||
return NoContent();
|
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