feat: add repository pattern, service layer architecture, and scheduler management

- Implement repository pattern for data access layer
- Add comprehensive service layer for business logic
- Create scheduler management system with health monitoring
- Add bounded buffer utility for memory management
- Enhance security audit logging with retention policies
This commit is contained in:
2025-07-12 07:00:37 +02:00
parent e1abedb148
commit 041a1cc3ef
54 changed files with 5755 additions and 878 deletions

View File

@ -0,0 +1,71 @@
/**
* Base repository interface with common CRUD operations
*/
export interface BaseRepository<T, ID = string> {
findById(id: ID): Promise<T | null>;
findMany(options?: FindManyOptions<T>): Promise<T[]>;
create(data: CreateInput<T>): Promise<T>;
update(id: ID, data: UpdateInput<T>): Promise<T | null>;
delete(id: ID): Promise<boolean>;
count(options?: CountOptions<T>): Promise<number>;
}
/**
* Generic find options interface
*/
export interface FindManyOptions<T> {
where?: Partial<T>;
orderBy?: Record<keyof T, "asc" | "desc">;
skip?: number;
take?: number;
include?: Record<string, boolean>;
}
/**
* Generic count options interface
*/
export interface CountOptions<T> {
where?: Partial<T>;
}
/**
* Create input type - excludes auto-generated fields
*/
export type CreateInput<T> = Omit<T, "id" | "createdAt" | "updatedAt">;
/**
* Update input type - excludes auto-generated fields and makes all optional
*/
export type UpdateInput<T> = Partial<Omit<T, "id" | "createdAt" | "updatedAt">>;
/**
* Repository error types
*/
export class RepositoryError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly cause?: Error
) {
super(message);
this.name = "RepositoryError";
}
}
export class NotFoundError extends RepositoryError {
constructor(entity: string, id: string | number) {
super(`${entity} with id ${id} not found`, "NOT_FOUND");
}
}
export class ConflictError extends RepositoryError {
constructor(message: string, cause?: Error) {
super(message, "CONFLICT", cause);
}
}
export class ValidationError extends RepositoryError {
constructor(message: string, cause?: Error) {
super(message, "VALIDATION_ERROR", cause);
}
}

View File

@ -0,0 +1,92 @@
import { SecurityAuditLogRepository } from "./SecurityAuditLogRepository";
import { SessionRepository } from "./SessionRepository";
import { UserRepository } from "./UserRepository";
/**
* Repository factory for centralized repository management
* Implements singleton pattern to ensure single instances
*/
export class RepositoryFactory {
private static instance: RepositoryFactory;
private sessionRepository?: SessionRepository;
private userRepository?: UserRepository;
private securityAuditLogRepository?: SecurityAuditLogRepository;
private constructor() {
// Private constructor for singleton
}
/**
* Get the singleton instance of RepositoryFactory
*/
static getInstance(): RepositoryFactory {
if (!RepositoryFactory.instance) {
RepositoryFactory.instance = new RepositoryFactory();
}
return RepositoryFactory.instance;
}
/**
* Get SessionRepository instance
*/
getSessionRepository(): SessionRepository {
if (!this.sessionRepository) {
this.sessionRepository = new SessionRepository();
}
return this.sessionRepository;
}
/**
* Get UserRepository instance
*/
getUserRepository(): UserRepository {
if (!this.userRepository) {
this.userRepository = new UserRepository();
}
return this.userRepository;
}
/**
* Get SecurityAuditLogRepository instance
*/
getSecurityAuditLogRepository(): SecurityAuditLogRepository {
if (!this.securityAuditLogRepository) {
this.securityAuditLogRepository = new SecurityAuditLogRepository();
}
return this.securityAuditLogRepository;
}
/**
* Get all repository instances
*/
getAllRepositories() {
return {
sessions: this.getSessionRepository(),
users: this.getUserRepository(),
securityAuditLogs: this.getSecurityAuditLogRepository(),
};
}
/**
* Reset all repository instances (useful for testing)
*/
reset(): void {
this.sessionRepository = undefined;
this.userRepository = undefined;
this.securityAuditLogRepository = undefined;
}
}
/**
* Convenience function to get repository factory instance
*/
export const repositories = RepositoryFactory.getInstance();
/**
* Convenience functions to get specific repositories
*/
export const getSessionRepository = () => repositories.getSessionRepository();
export const getUserRepository = () => repositories.getUserRepository();
export const getSecurityAuditLogRepository = () =>
repositories.getSecurityAuditLogRepository();

View File

@ -0,0 +1,476 @@
import type { Prisma, SecurityAuditLog } from "@prisma/client";
import { prisma } from "../prisma";
import {
AuditOutcome,
type AuditSeverity,
SecurityEventType,
} from "../securityAuditLogger";
import {
type BaseRepository,
type CountOptions,
type CreateInput,
type FindManyOptions,
RepositoryError,
type UpdateInput,
} from "./BaseRepository";
/**
* Security audit log with included relations
*/
export type SecurityAuditLogWithRelations = SecurityAuditLog & {
user?: {
id: string;
email: string;
};
company?: {
id: string;
name: string;
};
};
/**
* Security audit analytics interface
*/
export interface SecurityAnalytics {
totalEvents: number;
eventsByType: Record<SecurityEventType, number>;
eventsBySeverity: Record<AuditSeverity, number>;
eventsByOutcome: Record<AuditOutcome, number>;
topIPs: Array<{ ip: string; count: number }>;
topUsers: Array<{ userId: string; email: string; count: number }>;
hourlyDistribution: Array<{ hour: number; count: number }>;
geoDistribution: Record<string, number>;
}
/**
* SecurityAuditLog repository implementing database operations
*/
export class SecurityAuditLogRepository
implements BaseRepository<SecurityAuditLog>
{
/**
* Find audit log by ID
*/
async findById(id: string): Promise<SecurityAuditLogWithRelations | null> {
try {
return await prisma.securityAuditLog.findUnique({
where: { id },
include: {
user: {
select: { id: true, email: true },
},
company: {
select: { id: true, name: true },
},
},
});
} catch (error) {
throw new RepositoryError(
`Failed to find audit log ${id}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find many audit logs with filters
*/
async findMany(
options?: FindManyOptions<SecurityAuditLog>
): Promise<SecurityAuditLogWithRelations[]> {
try {
return await prisma.securityAuditLog.findMany({
where: options?.where as Prisma.SecurityAuditLogWhereInput,
orderBy:
options?.orderBy as Prisma.SecurityAuditLogOrderByWithRelationInput,
skip: options?.skip,
take: options?.take,
include: {
user: {
select: { id: true, email: true },
},
company: {
select: { id: true, name: true },
},
},
});
} catch (error) {
throw new RepositoryError(
"Failed to find audit logs",
"FIND_ERROR",
error as Error
);
}
}
/**
* Find audit logs by event type
*/
async findByEventType(
eventType: SecurityEventType,
limit = 100
): Promise<SecurityAuditLog[]> {
try {
return await prisma.securityAuditLog.findMany({
where: { eventType },
orderBy: { timestamp: "desc" },
take: limit,
});
} catch (error) {
throw new RepositoryError(
`Failed to find audit logs by event type ${eventType}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find audit logs by IP address within time range
*/
async findByIPAddress(
ipAddress: string,
startTime: Date,
endTime?: Date
): Promise<SecurityAuditLog[]> {
try {
return await prisma.securityAuditLog.findMany({
where: {
ipAddress,
timestamp: {
gte: startTime,
...(endTime && { lte: endTime }),
},
},
orderBy: { timestamp: "desc" },
});
} catch (error) {
throw new RepositoryError(
`Failed to find audit logs by IP ${ipAddress}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find failed authentication attempts
*/
async findFailedAuthAttempts(
ipAddress?: string,
timeWindow = 24 * 60 * 60 * 1000 // 24 hours in ms
): Promise<SecurityAuditLog[]> {
try {
const startTime = new Date(Date.now() - timeWindow);
return await prisma.securityAuditLog.findMany({
where: {
eventType: SecurityEventType.AUTHENTICATION,
outcome: AuditOutcome.FAILURE,
timestamp: { gte: startTime },
...(ipAddress && { ipAddress }),
},
orderBy: { timestamp: "desc" },
});
} catch (error) {
throw new RepositoryError(
"Failed to find failed authentication attempts",
"FIND_ERROR",
error as Error
);
}
}
/**
* Create audit log entry
*/
async create(data: CreateInput<SecurityAuditLog>): Promise<SecurityAuditLog> {
try {
return await prisma.securityAuditLog.create({
data: data as Prisma.SecurityAuditLogCreateInput,
});
} catch (error) {
throw new RepositoryError(
"Failed to create audit log",
"CREATE_ERROR",
error as Error
);
}
}
/**
* Update audit log (rarely used, mainly for corrections)
*/
async update(
id: string,
data: UpdateInput<SecurityAuditLog>
): Promise<SecurityAuditLog | null> {
try {
return await prisma.securityAuditLog.update({
where: { id },
data: data as Prisma.SecurityAuditLogUpdateInput,
});
} catch (error) {
throw new RepositoryError(
`Failed to update audit log ${id}`,
"UPDATE_ERROR",
error as Error
);
}
}
/**
* Delete audit log (used for cleanup)
*/
async delete(id: string): Promise<boolean> {
try {
await prisma.securityAuditLog.delete({ where: { id } });
return true;
} catch (error) {
throw new RepositoryError(
`Failed to delete audit log ${id}`,
"DELETE_ERROR",
error as Error
);
}
}
/**
* Count audit logs with filters
*/
async count(options?: CountOptions<SecurityAuditLog>): Promise<number> {
try {
return await prisma.securityAuditLog.count({
where: options?.where as Prisma.SecurityAuditLogWhereInput,
});
} catch (error) {
throw new RepositoryError(
"Failed to count audit logs",
"COUNT_ERROR",
error as Error
);
}
}
/**
* Get security analytics for dashboard
*/
async getSecurityAnalytics(
startDate: Date,
endDate: Date,
companyId?: string
): Promise<SecurityAnalytics> {
try {
const whereClause = {
timestamp: {
gte: startDate,
lte: endDate,
},
...(companyId && { companyId }),
};
const [events, eventsByType, eventsBySeverity, eventsByOutcome] =
await Promise.all([
prisma.securityAuditLog.findMany({
where: whereClause,
include: {
user: { select: { id: true, email: true } },
},
}),
prisma.securityAuditLog.groupBy({
by: ["eventType"],
where: whereClause,
_count: { eventType: true },
}),
prisma.securityAuditLog.groupBy({
by: ["severity"],
where: whereClause,
_count: { severity: true },
}),
prisma.securityAuditLog.groupBy({
by: ["outcome"],
where: whereClause,
_count: { outcome: true },
}),
]);
// Process aggregated data
const totalEvents = events.length;
const eventsByTypeMap = eventsByType.reduce(
(acc, item) => {
acc[item.eventType as SecurityEventType] = item._count.eventType;
return acc;
},
{} as Record<SecurityEventType, number>
);
const eventsBySeverityMap = eventsBySeverity.reduce(
(acc, item) => {
acc[item.severity as AuditSeverity] = item._count.severity;
return acc;
},
{} as Record<AuditSeverity, number>
);
const eventsByOutcomeMap = eventsByOutcome.reduce(
(acc, item) => {
acc[item.outcome as AuditOutcome] = item._count.outcome;
return acc;
},
{} as Record<AuditOutcome, number>
);
// Top IPs
const ipCounts = events.reduce(
(acc, event) => {
if (event.ipAddress) {
acc[event.ipAddress] = (acc[event.ipAddress] || 0) + 1;
}
return acc;
},
{} as Record<string, number>
);
const topIPs = Object.entries(ipCounts)
.map(([ip, count]) => ({ ip, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
// Top users
const userCounts = events
.filter((e) => e.userId && e.user)
.reduce(
(acc, event) => {
const key = event.userId!;
if (!acc[key]) {
acc[key] = {
userId: event.userId!,
email: event.user?.email,
count: 0,
};
}
acc[key].count++;
return acc;
},
{} as Record<string, { userId: string; email: string; count: number }>
);
const topUsers = Object.values(userCounts)
.sort((a, b) => b.count - a.count)
.slice(0, 10);
// Hourly distribution
const hourlyDistribution = Array.from({ length: 24 }, (_, hour) => ({
hour,
count: events.filter((e) => e.timestamp.getHours() === hour).length,
}));
// Geographic distribution
const geoDistribution = events.reduce(
(acc, event) => {
if (event.country) {
acc[event.country] = (acc[event.country] || 0) + 1;
}
return acc;
},
{} as Record<string, number>
);
return {
totalEvents,
eventsByType: eventsByTypeMap,
eventsBySeverity: eventsBySeverityMap,
eventsByOutcome: eventsByOutcomeMap,
topIPs,
topUsers,
hourlyDistribution,
geoDistribution,
};
} catch (error) {
throw new RepositoryError(
"Failed to get security analytics",
"ANALYTICS_ERROR",
error as Error
);
}
}
/**
* Clean up old audit logs based on retention policy
*/
async cleanupOldLogs(retentionDays: number): Promise<number> {
try {
const cutoffDate = new Date(
Date.now() - retentionDays * 24 * 60 * 60 * 1000
);
const result = await prisma.securityAuditLog.deleteMany({
where: {
timestamp: { lt: cutoffDate },
},
});
return result.count;
} catch (error) {
throw new RepositoryError(
"Failed to cleanup old audit logs",
"CLEANUP_ERROR",
error as Error
);
}
}
/**
* Get suspicious activity summary for an IP
*/
async getIPActivitySummary(
ipAddress: string,
hoursBack = 24
): Promise<{
failedLogins: number;
rateLimitViolations: number;
uniqueUsersTargeted: number;
totalEvents: number;
timeSpan: { first: Date | null; last: Date | null };
}> {
try {
const startTime = new Date(Date.now() - hoursBack * 60 * 60 * 1000);
const events = await this.findByIPAddress(ipAddress, startTime);
const failedLogins = events.filter(
(e) =>
e.eventType === SecurityEventType.AUTHENTICATION &&
e.outcome === AuditOutcome.FAILURE
).length;
const rateLimitViolations = events.filter(
(e) => e.outcome === AuditOutcome.RATE_LIMITED
).length;
const uniqueUsersTargeted = new Set(
events.map((e) => e.userId).filter(Boolean)
).size;
const timeSpan = {
first: events.length > 0 ? events[events.length - 1].timestamp : null,
last: events.length > 0 ? events[0].timestamp : null,
};
return {
failedLogins,
rateLimitViolations,
uniqueUsersTargeted,
totalEvents: events.length,
timeSpan,
};
} catch (error) {
throw new RepositoryError(
`Failed to get IP activity summary for ${ipAddress}`,
"ACTIVITY_SUMMARY_ERROR",
error as Error
);
}
}
}

View File

@ -0,0 +1,335 @@
import type { Prisma, Session } from "@prisma/client";
import { prisma } from "../prisma";
import {
type BaseRepository,
type CountOptions,
type CreateInput,
type FindManyOptions,
NotFoundError,
RepositoryError,
type UpdateInput,
} from "./BaseRepository";
/**
* Session with included relations
*/
export type SessionWithRelations = Session & {
messages?: Array<{
id: string;
sessionId: string;
timestamp: Date | null;
role: string;
content: string;
order: number;
createdAt: Date;
}>;
company?: {
id: string;
name: string;
};
sessionImport?: {
id: string;
status: string;
};
};
/**
* Session repository implementing database operations
*/
export class SessionRepository implements BaseRepository<Session> {
/**
* Find session by ID with optional relations
*/
async findById(
id: string,
include?: { messages?: boolean; company?: boolean; sessionImport?: boolean }
): Promise<SessionWithRelations | null> {
try {
return await prisma.session.findUnique({
where: { id },
include: {
messages: include?.messages
? { orderBy: { order: "asc" } }
: undefined,
company: include?.company
? { select: { id: true, name: true } }
: undefined,
sessionImport: include?.sessionImport
? { select: { id: true, status: true } }
: undefined,
},
});
} catch (error) {
throw new RepositoryError(
`Failed to find session ${id}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find sessions by company ID
*/
async findByCompanyId(
companyId: string,
options?: Omit<FindManyOptions<Session>, "where">
): Promise<Session[]> {
try {
return await prisma.session.findMany({
where: { companyId },
orderBy: options?.orderBy as Prisma.SessionOrderByWithRelationInput,
skip: options?.skip,
take: options?.take,
include: options?.include as Prisma.SessionInclude,
});
} catch (error) {
throw new RepositoryError(
`Failed to find sessions for company ${companyId}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find sessions by date range
*/
async findByDateRange(
startDate: Date,
endDate: Date,
companyId?: string
): Promise<Session[]> {
try {
return await prisma.session.findMany({
where: {
startTime: {
gte: startDate,
lte: endDate,
},
...(companyId && { companyId }),
},
orderBy: { startTime: "desc" },
});
} catch (error) {
throw new RepositoryError(
"Failed to find sessions by date range",
"FIND_ERROR",
error as Error
);
}
}
/**
* Find many sessions with filters
*/
async findMany(options?: FindManyOptions<Session>): Promise<Session[]> {
try {
return await prisma.session.findMany({
where: options?.where as Prisma.SessionWhereInput,
orderBy: options?.orderBy as Prisma.SessionOrderByWithRelationInput,
skip: options?.skip,
take: options?.take,
include: options?.include as Prisma.SessionInclude,
});
} catch (error) {
throw new RepositoryError(
"Failed to find sessions",
"FIND_ERROR",
error as Error
);
}
}
/**
* Create a new session
*/
async create(data: CreateInput<Session>): Promise<Session> {
try {
return await prisma.session.create({
data: data as Prisma.SessionCreateInput,
});
} catch (error) {
throw new RepositoryError(
"Failed to create session",
"CREATE_ERROR",
error as Error
);
}
}
/**
* Update session by ID
*/
async update(
id: string,
data: UpdateInput<Session>
): Promise<Session | null> {
try {
const session = await this.findById(id);
if (!session) {
throw new NotFoundError("Session", id);
}
return await prisma.session.update({
where: { id },
data: data as Prisma.SessionUpdateInput,
});
} catch (error) {
if (error instanceof NotFoundError) throw error;
throw new RepositoryError(
`Failed to update session ${id}`,
"UPDATE_ERROR",
error as Error
);
}
}
/**
* Delete session by ID
*/
async delete(id: string): Promise<boolean> {
try {
const session = await this.findById(id);
if (!session) {
throw new NotFoundError("Session", id);
}
await prisma.session.delete({ where: { id } });
return true;
} catch (error) {
if (error instanceof NotFoundError) throw error;
throw new RepositoryError(
`Failed to delete session ${id}`,
"DELETE_ERROR",
error as Error
);
}
}
/**
* Count sessions with optional filters
*/
async count(options?: CountOptions<Session>): Promise<number> {
try {
return await prisma.session.count({
where: options?.where as Prisma.SessionWhereInput,
});
} catch (error) {
throw new RepositoryError(
"Failed to count sessions",
"COUNT_ERROR",
error as Error
);
}
}
/**
* Get session metrics for a company
*/
async getSessionMetrics(
companyId: string,
startDate: Date,
endDate: Date
): Promise<{
totalSessions: number;
avgSessionLength: number | null;
sentimentDistribution: Record<string, number>;
categoryDistribution: Record<string, number>;
}> {
try {
const sessions = await this.findByDateRange(
startDate,
endDate,
companyId
);
const totalSessions = sessions.length;
const avgSessionLength =
sessions.length > 0
? sessions
.filter((s) => s.endTime)
.reduce((sum, s) => {
const duration = s.endTime
? (s.endTime.getTime() - s.startTime.getTime()) / 1000
: 0;
return sum + duration;
}, 0) / sessions.filter((s) => s.endTime).length
: null;
const sentimentDistribution = sessions.reduce(
(acc, session) => {
const sentiment = session.sentiment || "unknown";
acc[sentiment] = (acc[sentiment] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
const categoryDistribution = sessions.reduce(
(acc, session) => {
const category = session.category || "uncategorized";
acc[category] = (acc[category] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
return {
totalSessions,
avgSessionLength,
sentimentDistribution,
categoryDistribution,
};
} catch (error) {
throw new RepositoryError(
"Failed to get session metrics",
"METRICS_ERROR",
error as Error
);
}
}
/**
* Find sessions needing AI processing
*/
async findPendingAIProcessing(limit = 100): Promise<Session[]> {
try {
return await prisma.session.findMany({
where: {
OR: [{ sentiment: null }, { category: null }, { summary: null }],
},
take: limit,
orderBy: { createdAt: "asc" },
});
} catch (error) {
throw new RepositoryError(
"Failed to find sessions pending AI processing",
"FIND_ERROR",
error as Error
);
}
}
/**
* Bulk update sessions
*/
async bulkUpdate(
where: Prisma.SessionWhereInput,
data: Prisma.SessionUpdateInput
): Promise<number> {
try {
const result = await prisma.session.updateMany({
where,
data,
});
return result.count;
} catch (error) {
throw new RepositoryError(
"Failed to bulk update sessions",
"BULK_UPDATE_ERROR",
error as Error
);
}
}
}

View File

@ -0,0 +1,396 @@
import type { Prisma, User } from "@prisma/client";
import { prisma } from "../prisma";
import {
type BaseRepository,
type CountOptions,
type CreateInput,
type FindManyOptions,
NotFoundError,
RepositoryError,
type UpdateInput,
} from "./BaseRepository";
/**
* User with included relations
*/
export type UserWithRelations = User & {
company?: {
id: string;
name: string;
};
securityAuditLogs?: Array<{
id: string;
eventType: string;
timestamp: Date;
outcome: string;
}>;
};
/**
* User repository implementing database operations
*/
export class UserRepository implements BaseRepository<User> {
/**
* Find user by ID with optional relations
*/
async findById(
id: string,
include?: { company?: boolean; securityAuditLogs?: boolean }
): Promise<UserWithRelations | null> {
try {
return await prisma.user.findUnique({
where: { id },
include: {
company: include?.company
? { select: { id: true, name: true } }
: undefined,
securityAuditLogs: include?.securityAuditLogs
? {
select: {
id: true,
eventType: true,
timestamp: true,
outcome: true,
},
take: 100,
orderBy: { timestamp: "desc" },
}
: undefined,
},
});
} catch (error) {
throw new RepositoryError(
`Failed to find user ${id}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find user by email
*/
async findByEmail(email: string): Promise<User | null> {
try {
return await prisma.user.findUnique({
where: { email },
});
} catch (error) {
throw new RepositoryError(
`Failed to find user by email ${email}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find users by company ID
*/
async findByCompanyId(companyId: string): Promise<User[]> {
try {
return await prisma.user.findMany({
where: { companyId },
orderBy: { createdAt: "desc" },
});
} catch (error) {
throw new RepositoryError(
`Failed to find users by company ${companyId}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find users by role
*/
async findByRole(role: string, companyId?: string): Promise<User[]> {
try {
return await prisma.user.findMany({
where: {
role,
...(companyId && { companyId }),
},
orderBy: { createdAt: "desc" },
});
} catch (error) {
throw new RepositoryError(
`Failed to find users by role ${role}`,
"FIND_ERROR",
error as Error
);
}
}
/**
* Find many users with filters
*/
async findMany(options?: FindManyOptions<User>): Promise<User[]> {
try {
return await prisma.user.findMany({
where: options?.where as Prisma.UserWhereInput,
orderBy: options?.orderBy as Prisma.UserOrderByWithRelationInput,
skip: options?.skip,
take: options?.take,
include: options?.include as Prisma.UserInclude,
});
} catch (error) {
throw new RepositoryError(
"Failed to find users",
"FIND_ERROR",
error as Error
);
}
}
/**
* Create a new user
*/
async create(data: CreateInput<User>): Promise<User> {
try {
return await prisma.user.create({
data: data as Prisma.UserCreateInput,
});
} catch (error) {
throw new RepositoryError(
"Failed to create user",
"CREATE_ERROR",
error as Error
);
}
}
/**
* Update user by ID
*/
async update(id: string, data: UpdateInput<User>): Promise<User | null> {
try {
const user = await this.findById(id);
if (!user) {
throw new NotFoundError("User", id);
}
return await prisma.user.update({
where: { id },
data: data as Prisma.UserUpdateInput,
});
} catch (error) {
if (error instanceof NotFoundError) throw error;
throw new RepositoryError(
`Failed to update user ${id}`,
"UPDATE_ERROR",
error as Error
);
}
}
/**
* Delete user by ID
*/
async delete(id: string): Promise<boolean> {
try {
const user = await this.findById(id);
if (!user) {
throw new NotFoundError("User", id);
}
await prisma.user.delete({ where: { id } });
return true;
} catch (error) {
if (error instanceof NotFoundError) throw error;
throw new RepositoryError(
`Failed to delete user ${id}`,
"DELETE_ERROR",
error as Error
);
}
}
/**
* Count users with optional filters
*/
async count(options?: CountOptions<User>): Promise<number> {
try {
return await prisma.user.count({
where: options?.where as Prisma.UserWhereInput,
});
} catch (error) {
throw new RepositoryError(
"Failed to count users",
"COUNT_ERROR",
error as Error
);
}
}
/**
* Update user last login timestamp
*/
async updateLastLogin(id: string): Promise<User | null> {
try {
return await this.update(id, {
lastLoginAt: new Date(),
});
} catch (error) {
throw new RepositoryError(
`Failed to update last login for user ${id}`,
"UPDATE_LOGIN_ERROR",
error as Error
);
}
}
/**
* Find users with recent security events
*/
async findUsersWithRecentSecurityEvents(
hoursBack = 24,
minEvents = 5
): Promise<Array<{ user: User; eventCount: number }>> {
try {
const startTime = new Date(Date.now() - hoursBack * 60 * 60 * 1000);
const usersWithEvents = await prisma.user.findMany({
where: {
securityAuditLogs: {
some: {
timestamp: { gte: startTime },
},
},
},
include: {
securityAuditLogs: {
where: {
timestamp: { gte: startTime },
},
select: { id: true },
},
},
});
return usersWithEvents
.map((user) => ({
user: {
...user,
securityAuditLogs: undefined, // Remove from result
} as User,
eventCount: user.securityAuditLogs?.length || 0,
}))
.filter((item) => item.eventCount >= minEvents)
.sort((a, b) => b.eventCount - a.eventCount);
} catch (error) {
throw new RepositoryError(
"Failed to find users with recent security events",
"SECURITY_EVENTS_ERROR",
error as Error
);
}
}
/**
* Get user activity summary
*/
async getUserActivitySummary(
userId: string,
hoursBack = 24
): Promise<{
totalEvents: number;
failedLogins: number;
successfulLogins: number;
rateLimitViolations: number;
lastActivity: Date | null;
countriesAccessed: string[];
}> {
try {
const startTime = new Date(Date.now() - hoursBack * 60 * 60 * 1000);
const events = await prisma.securityAuditLog.findMany({
where: {
userId,
timestamp: { gte: startTime },
},
orderBy: { timestamp: "desc" },
});
const totalEvents = events.length;
const failedLogins = events.filter(
(e) => e.eventType === "AUTHENTICATION" && e.outcome === "FAILURE"
).length;
const successfulLogins = events.filter(
(e) => e.eventType === "AUTHENTICATION" && e.outcome === "SUCCESS"
).length;
const rateLimitViolations = events.filter(
(e) => e.outcome === "RATE_LIMITED"
).length;
const lastActivity = events.length > 0 ? events[0].timestamp : null;
const countriesAccessed = [
...new Set(events.map((e) => e.country).filter(Boolean)),
];
return {
totalEvents,
failedLogins,
successfulLogins,
rateLimitViolations,
lastActivity,
countriesAccessed,
};
} catch (error) {
throw new RepositoryError(
`Failed to get activity summary for user ${userId}`,
"ACTIVITY_SUMMARY_ERROR",
error as Error
);
}
}
/**
* Find inactive users (no login for specified days)
*/
async findInactiveUsers(daysInactive = 30): Promise<User[]> {
try {
const cutoffDate = new Date(
Date.now() - daysInactive * 24 * 60 * 60 * 1000
);
return await prisma.user.findMany({
where: {
OR: [{ lastLoginAt: { lt: cutoffDate } }, { lastLoginAt: null }],
},
orderBy: { lastLoginAt: "asc" },
});
} catch (error) {
throw new RepositoryError(
"Failed to find inactive users",
"FIND_INACTIVE_ERROR",
error as Error
);
}
}
/**
* Search users by name or email
*/
async searchUsers(query: string, companyId?: string): Promise<User[]> {
try {
return await prisma.user.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
],
...(companyId && { companyId }),
},
orderBy: { name: "asc" },
take: 50, // Limit results
});
} catch (error) {
throw new RepositoryError(
`Failed to search users with query "${query}"`,
"SEARCH_ERROR",
error as Error
);
}
}
}