diff --git a/backend/services/mining-admin-service/prisma/migrations/20260310_audit_log_nullable_admin/migration.sql b/backend/services/mining-admin-service/prisma/migrations/20260310_audit_log_nullable_admin/migration.sql new file mode 100644 index 00000000..0a1a0204 --- /dev/null +++ b/backend/services/mining-admin-service/prisma/migrations/20260310_audit_log_nullable_admin/migration.sql @@ -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; diff --git a/backend/services/mining-admin-service/prisma/schema.prisma b/backend/services/mining-admin-service/prisma/schema.prisma index 96bf5048..c9bdaad3 100644 --- a/backend/services/mining-admin-service/prisma/schema.prisma +++ b/backend/services/mining-admin-service/prisma/schema.prisma @@ -85,8 +85,8 @@ model InitializationRecord { model AuditLog { id String @id @default(uuid()) - adminId String - action String // CREATE, UPDATE, DELETE, LOGIN, LOGOUT, INIT + adminId String? + action String // CREATE, UPDATE, DELETE, LOGIN, LOGIN_FAILED, LOGOUT, INIT resource String // CONFIG, USER, SYSTEM_ACCOUNT, MINING resourceId String? oldValue Json? @@ -95,7 +95,7 @@ model AuditLog { userAgent String? createdAt DateTime @default(now()) - admin AdminUser @relation(fields: [adminId], references: [id]) + admin AdminUser? @relation(fields: [adminId], references: [id]) @@index([adminId]) @@index([action]) diff --git a/backend/services/mining-admin-service/src/application/services/auth.service.ts b/backend/services/mining-admin-service/src/application/services/auth.service.ts index 8ddb080d..cee458c6 100644 --- a/backend/services/mining-admin-service/src/application/services/auth.service.ts +++ b/backend/services/mining-admin-service/src/application/services/auth.service.ts @@ -23,11 +23,17 @@ export class AuthService { async login(username: string, password: string, ipAddress?: string, userAgent?: string): Promise<{ token: string; admin: any }> { const admin = await this.prisma.adminUser.findUnique({ where: { username } }); 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'); } const isValid = await bcrypt.compare(password, admin.password); 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'); } diff --git a/frontend/mining-admin-web/src/app/(dashboard)/audit-logs/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/audit-logs/page.tsx index 33784a84..9f7951a0 100644 --- a/frontend/mining-admin-web/src/app/(dashboard)/audit-logs/page.tsx +++ b/frontend/mining-admin-web/src/app/(dashboard)/audit-logs/page.tsx @@ -30,8 +30,9 @@ const actionLabels: Record = { CREATE: { label: '创建', className: 'bg-green-100 text-green-700' }, UPDATE: { label: '更新', className: 'bg-blue-100 text-blue-700' }, DELETE: { label: '删除', className: 'bg-red-100 text-red-700' }, - LOGIN: { label: '登录', className: 'bg-purple-100 text-purple-700' }, - LOGOUT: { label: '登出', className: 'bg-gray-100 text-gray-600' }, + 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' }, ENABLE: { label: '启用', className: 'bg-emerald-100 text-emerald-700' }, DISABLE: { label: '禁用', className: 'bg-orange-100 text-orange-700' }, UNLOCK: { label: '解锁', className: 'bg-yellow-100 text-yellow-700' }, @@ -93,6 +94,7 @@ export default function AuditLogsPage() { 全部 登录 + 登录失败 登出 创建 更新