Assist_Design/apps/bff/src/infra/audit/audit.service.ts
Temuuleenn df742e50bc fix: resolve BFF TypeScript errors and improve mobile UX
BFF fixes:
- Fix pino-http type import by using Params from nestjs-pino
- Use Prisma-generated AuditAction enum instead of local duplicate
- Add null check for sfAccountId in mapping mapper

Portal mobile UX improvements:
- DataTable: Add responsive card view for mobile with stacked layout
- Header: Increase touch targets to 44px minimum, better spacing
- PageLayout: Optimize padding and make breadcrumbs scrollable
- PublicShell: Add iOS safe area support, slide animation, language
  switcher and sign-in button visible in mobile header

Also removes "Trusted by Leading Companies" section from AboutUsView.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 18:29:55 +09:00

169 lines
4.7 KiB
TypeScript

import { Injectable, Inject } from "@nestjs/common";
import { Prisma, AuditAction } from "@prisma/client";
import { PrismaService } from "../database/prisma.service.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino";
import { extractClientIp, extractUserAgent } from "@bff/core/http/request-context.util.js";
// Re-export AuditAction from Prisma for consumers
export { AuditAction } from "@prisma/client";
export interface AuditLogData {
userId?: string | undefined;
action: AuditAction;
resource?: string | undefined;
details?: Record<string, unknown> | string | number | boolean | null | undefined;
ipAddress?: string | undefined;
userAgent?: string | undefined;
success?: boolean | undefined;
error?: string | undefined;
}
/**
* Minimal request shape for audit logging.
* Compatible with Express Request but only requires the fields needed for IP/UA extraction.
* Must be compatible with RequestContextLike from request-context.util.ts.
*/
export type AuditRequest = {
headers?: Record<string, string | string[] | undefined> | undefined;
ip?: string | undefined;
connection?: { remoteAddress?: string | undefined } | undefined;
socket?: { remoteAddress?: string | undefined } | undefined;
};
@Injectable()
export class AuditService {
constructor(
private readonly prisma: PrismaService,
@Inject(Logger) private readonly logger: Logger
) {}
async log(data: AuditLogData): Promise<void> {
try {
const createData: Parameters<typeof this.prisma.auditLog.create>[0]["data"] = {
action: data.action,
success: data.success ?? true,
};
if (data.userId !== undefined) createData.userId = data.userId;
if (data.resource !== undefined) createData.resource = data.resource;
if (data.ipAddress !== undefined) createData.ipAddress = data.ipAddress;
if (data.userAgent !== undefined) createData.userAgent = data.userAgent;
if (data.error !== undefined) createData.error = data.error;
if (data.details !== undefined) {
createData.details =
data.details === null
? Prisma.JsonNull
: (JSON.parse(JSON.stringify(data.details)) as Prisma.InputJsonValue);
}
await this.prisma.auditLog.create({ data: createData });
} catch (error) {
this.logger.error("Audit logging failed", {
errorType: error instanceof Error ? error.constructor.name : "Unknown",
message: extractErrorMessage(error),
});
}
}
async logAuthEvent(
action: AuditAction,
userId?: string,
details?: Record<string, unknown> | string | number | boolean | null,
request?: AuditRequest,
success: boolean = true,
error?: string
): Promise<void> {
const ipAddress = extractClientIp(request);
const userAgent = extractUserAgent(request);
await this.log({
userId,
action,
resource: "auth",
details,
ipAddress,
userAgent,
success,
error,
});
}
async getAuditLogs({
page,
limit,
action,
userId,
}: {
page: number;
limit: number;
action?: AuditAction;
userId?: string;
}) {
const skip = (page - 1) * limit;
const where: Prisma.AuditLogWhereInput = {};
if (action) where.action = action;
if (userId) where.userId = userId;
const [logs, total] = await Promise.all([
this.prisma.auditLog.findMany({
where,
include: {
user: {
select: {
id: true,
email: true,
},
},
},
orderBy: { createdAt: "desc" },
skip,
take: limit,
}),
this.prisma.auditLog.count({ where }),
]);
return { logs, total };
}
async getSecurityStats() {
const today = new Date(new Date().setHours(0, 0, 0, 0));
const [totalUsers, lockedAccounts, failedLoginsToday, successfulLoginsToday] =
await Promise.all([
this.prisma.user.count(),
this.prisma.user.count({
where: {
lockedUntil: {
gt: new Date(),
},
},
}),
this.prisma.auditLog.count({
where: {
action: AuditAction.LOGIN_FAILED,
createdAt: {
gte: today,
},
},
}),
this.prisma.auditLog.count({
where: {
action: AuditAction.LOGIN_SUCCESS,
createdAt: {
gte: today,
},
},
}),
]);
return {
totalUsers,
lockedAccounts,
failedLoginsToday,
successfulLoginsToday,
securityEventsToday: failedLoginsToday + successfulLoginsToday,
};
}
}