概览页增加后台登录记录

登录成功/失败写入 admin_login_logs,系统概览下方展示全部账号的时间、IP、设备等信息。
This commit is contained in:
yml2213
2026-07-10 21:55:32 +08:00
parent f6ea07912f
commit fa58381aa7
10 changed files with 609 additions and 11 deletions
@@ -0,0 +1,119 @@
import { query } from '../db/client.js'
export type AdminLoginLogRow = {
[column: string]: unknown
id: number
user_id: number | null
username: string
role: string
ip: string
location: string
user_agent: string
success: boolean
failure_reason: string
created_at: string
}
type AdminLoginLogCreateInput = {
userId?: number | string | null
username?: string
role?: string
ip?: string
location?: string
userAgent?: string
success?: boolean
failureReason?: string
createdAt: string
}
type AdminLoginLogListInput = {
username?: string
dateFrom?: string
dateTo?: string
page?: number | string
pageSize?: number | string
}
export async function createAdminLoginLog(
input: AdminLoginLogCreateInput,
): Promise<AdminLoginLogRow | null> {
const result = await query<AdminLoginLogRow>(
`
INSERT INTO admin_login_logs (
user_id,
username,
role,
ip,
location,
user_agent,
success,
failure_reason,
created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *
`,
[
input.userId || null,
String(input.username || '').trim(),
String(input.role || '').trim(),
String(input.ip || '').trim(),
String(input.location || '').trim(),
String(input.userAgent || '').trim(),
input.success !== false,
String(input.failureReason || '').trim(),
input.createdAt,
],
)
return result.rows[0] || null
}
export async function listAdminLoginLogs(
queryInput: AdminLoginLogListInput = {},
): Promise<{ items: AdminLoginLogRow[]; total: number }> {
const conditions: string[] = []
const params: unknown[] = []
if (queryInput.username) {
params.push(String(queryInput.username).trim())
conditions.push(`username = $${params.length}`)
}
if (queryInput.dateFrom) {
params.push(queryInput.dateFrom)
conditions.push(`created_at >= $${params.length}`)
}
if (queryInput.dateTo) {
params.push(queryInput.dateTo)
conditions.push(`created_at <= $${params.length}`)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const page = Math.max(1, Number(queryInput.page) || 1)
const pageSize = Math.min(100, Math.max(1, Number(queryInput.pageSize) || 20))
const offset = (page - 1) * pageSize
const totalResult = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_login_logs ${whereClause}`,
params,
)
params.push(pageSize)
params.push(offset)
const itemsResult = await query<AdminLoginLogRow>(
`
SELECT *
FROM admin_login_logs
${whereClause}
ORDER BY id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
)
return {
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}