feat(mining-admin): 审计日志记录登录失败事件
- audit_logs.adminId 改为可选字段,支持用户名不存在时的失败记录 - 登录失败时记录 LOGIN_FAILED 操作,resourceId 存储尝试的用户名,newValue 记录失败原因 - 前端审计日志页新增 LOGIN_FAILED 标签(红色)及筛选选项 - 新增 migration: 20260310_audit_log_nullable_admin Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e7d852e25e
commit
ad1c889848
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- Make adminId nullable in audit_logs to support LOGIN_FAILED records without a valid admin account
|
||||||
|
ALTER TABLE "audit_logs" ALTER COLUMN "admin_id" DROP NOT NULL;
|
||||||
|
|
@ -85,8 +85,8 @@ model InitializationRecord {
|
||||||
|
|
||||||
model AuditLog {
|
model AuditLog {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
adminId String
|
adminId String?
|
||||||
action String // CREATE, UPDATE, DELETE, LOGIN, LOGOUT, INIT
|
action String // CREATE, UPDATE, DELETE, LOGIN, LOGIN_FAILED, LOGOUT, INIT
|
||||||
resource String // CONFIG, USER, SYSTEM_ACCOUNT, MINING
|
resource String // CONFIG, USER, SYSTEM_ACCOUNT, MINING
|
||||||
resourceId String?
|
resourceId String?
|
||||||
oldValue Json?
|
oldValue Json?
|
||||||
|
|
@ -95,7 +95,7 @@ model AuditLog {
|
||||||
userAgent String?
|
userAgent String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
admin AdminUser @relation(fields: [adminId], references: [id])
|
admin AdminUser? @relation(fields: [adminId], references: [id])
|
||||||
|
|
||||||
@@index([adminId])
|
@@index([adminId])
|
||||||
@@index([action])
|
@@index([action])
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,17 @@ export class AuthService {
|
||||||
async login(username: string, password: string, ipAddress?: string, userAgent?: string): Promise<{ token: string; admin: any }> {
|
async login(username: string, password: string, ipAddress?: string, userAgent?: string): Promise<{ token: string; admin: any }> {
|
||||||
const admin = await this.prisma.adminUser.findUnique({ where: { username } });
|
const admin = await this.prisma.adminUser.findUnique({ where: { username } });
|
||||||
if (!admin || admin.status !== 'ACTIVE') {
|
if (!admin || admin.status !== 'ACTIVE') {
|
||||||
|
await this.prisma.auditLog.create({
|
||||||
|
data: { action: 'LOGIN_FAILED', resource: 'AUTH', resourceId: username, newValue: { reason: admin ? 'account_disabled' : 'user_not_found' }, ipAddress, userAgent },
|
||||||
|
});
|
||||||
throw new UnauthorizedException('Invalid credentials');
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = await bcrypt.compare(password, admin.password);
|
const isValid = await bcrypt.compare(password, admin.password);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
await this.prisma.auditLog.create({
|
||||||
|
data: { adminId: admin.id, action: 'LOGIN_FAILED', resource: 'AUTH', resourceId: username, newValue: { reason: 'wrong_password' }, ipAddress, userAgent },
|
||||||
|
});
|
||||||
throw new UnauthorizedException('Invalid credentials');
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const actionLabels: Record<string, { label: string; className: string }> = {
|
||||||
UPDATE: { label: '更新', className: 'bg-blue-100 text-blue-700' },
|
UPDATE: { label: '更新', className: 'bg-blue-100 text-blue-700' },
|
||||||
DELETE: { label: '删除', className: 'bg-red-100 text-red-700' },
|
DELETE: { label: '删除', className: 'bg-red-100 text-red-700' },
|
||||||
LOGIN: { label: '登录', className: 'bg-purple-100 text-purple-700' },
|
LOGIN: { label: '登录', className: 'bg-purple-100 text-purple-700' },
|
||||||
|
LOGIN_FAILED: { label: '登录失败', className: 'bg-red-100 text-red-700' },
|
||||||
LOGOUT: { label: '登出', className: 'bg-gray-100 text-gray-600' },
|
LOGOUT: { label: '登出', className: 'bg-gray-100 text-gray-600' },
|
||||||
ENABLE: { label: '启用', className: 'bg-emerald-100 text-emerald-700' },
|
ENABLE: { label: '启用', className: 'bg-emerald-100 text-emerald-700' },
|
||||||
DISABLE: { label: '禁用', className: 'bg-orange-100 text-orange-700' },
|
DISABLE: { label: '禁用', className: 'bg-orange-100 text-orange-700' },
|
||||||
|
|
@ -93,6 +94,7 @@ export default function AuditLogsPage() {
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">全部</SelectItem>
|
<SelectItem value="all">全部</SelectItem>
|
||||||
<SelectItem value="LOGIN">登录</SelectItem>
|
<SelectItem value="LOGIN">登录</SelectItem>
|
||||||
|
<SelectItem value="LOGIN_FAILED">登录失败</SelectItem>
|
||||||
<SelectItem value="LOGOUT">登出</SelectItem>
|
<SelectItem value="LOGOUT">登出</SelectItem>
|
||||||
<SelectItem value="CREATE">创建</SelectItem>
|
<SelectItem value="CREATE">创建</SelectItem>
|
||||||
<SelectItem value="UPDATE">更新</SelectItem>
|
<SelectItem value="UPDATE">更新</SelectItem>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue