Added comprehensive authentication and authorization system: Authentication System: - Better Auth integration with session management - Login/logout pages and API routes - Middleware for route protection - Auth utilities and client libraries User Management: - User list, detail, and invite pages - User API endpoints (CRUD operations) - Session management for users - Profile settings page Role-Based Access Control: - Role management pages (list, create, edit) - Permission system with granular controls - Role assignment to users - Role API endpoints Admin Features: - Audit log page for tracking system events - Admin settings page - Audit service for logging user actions Additional Features: - Quotes management pages and components - SalesBldr API integration - Email service for notifications Configuration & Documentation: - Updated docker-compose.yml - MCP server configuration (mcp.json) - CVE-2025-55182 security review documentation - Standards guide and PRD documents - Re-enabling authentication documentation Database Migrations: - 012: Auth tables (users, sessions, accounts, verifications) - 013: Role tables (roles, permissions, role_permissions, user_roles) - 014: Admin settings table UI Updates: - Updated dashboard layout - Enhanced app layout with auth integration
197 lines
5.1 KiB
TypeScript
197 lines
5.1 KiB
TypeScript
import { Pool } from "pg";
|
|
|
|
const pool = new Pool({
|
|
connectionString: process.env.DATABASE_URL,
|
|
});
|
|
|
|
export interface AuditLogEntry {
|
|
userId?: string;
|
|
userEmail?: string;
|
|
action: string;
|
|
resource: string;
|
|
resourceId?: string;
|
|
details?: Record<string, unknown>;
|
|
ipAddress?: string;
|
|
userAgent?: string;
|
|
}
|
|
|
|
/**
|
|
* Log an audit event
|
|
*/
|
|
export async function log(entry: AuditLogEntry): Promise<void> {
|
|
try {
|
|
const id = crypto.randomUUID();
|
|
await pool.query(
|
|
`INSERT INTO "audit_log"
|
|
(id, user_id, user_email, action, resource, resource_id, details, ip_address, user_agent)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
[
|
|
id,
|
|
entry.userId || null,
|
|
entry.userEmail || null,
|
|
entry.action,
|
|
entry.resource,
|
|
entry.resourceId || null,
|
|
entry.details ? JSON.stringify(entry.details) : null,
|
|
entry.ipAddress || null,
|
|
entry.userAgent || null,
|
|
]
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to write audit log:", error);
|
|
// Don't throw - audit logging should not break the main flow
|
|
}
|
|
}
|
|
|
|
// Convenience functions for common audit events
|
|
export const audit = {
|
|
log,
|
|
|
|
// Auth events
|
|
signIn: (userId: string, email: string, ipAddress?: string, userAgent?: string) =>
|
|
log({
|
|
userId,
|
|
userEmail: email,
|
|
action: "sign_in",
|
|
resource: "auth",
|
|
ipAddress,
|
|
userAgent,
|
|
}),
|
|
|
|
signOut: (userId: string, email: string, ipAddress?: string) =>
|
|
log({
|
|
userId,
|
|
userEmail: email,
|
|
action: "sign_out",
|
|
resource: "auth",
|
|
ipAddress,
|
|
}),
|
|
|
|
signInFailed: (email: string, reason: string, ipAddress?: string) =>
|
|
log({
|
|
userEmail: email,
|
|
action: "sign_in_failed",
|
|
resource: "auth",
|
|
details: { reason },
|
|
ipAddress,
|
|
}),
|
|
|
|
// User management events
|
|
userCreated: (actorId: string, actorEmail: string, targetUserId: string, targetEmail: string) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "create",
|
|
resource: "user",
|
|
resourceId: targetUserId,
|
|
details: { targetEmail },
|
|
}),
|
|
|
|
userUpdated: (actorId: string, actorEmail: string, targetUserId: string, changes: Record<string, unknown>) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "update",
|
|
resource: "user",
|
|
resourceId: targetUserId,
|
|
details: { changes },
|
|
}),
|
|
|
|
userDeleted: (actorId: string, actorEmail: string, targetUserId: string, targetEmail: string) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "delete",
|
|
resource: "user",
|
|
resourceId: targetUserId,
|
|
details: { targetEmail },
|
|
}),
|
|
|
|
userBanned: (actorId: string, actorEmail: string, targetUserId: string, reason?: string) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "ban",
|
|
resource: "user",
|
|
resourceId: targetUserId,
|
|
details: { reason },
|
|
}),
|
|
|
|
userUnbanned: (actorId: string, actorEmail: string, targetUserId: string) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "unban",
|
|
resource: "user",
|
|
resourceId: targetUserId,
|
|
}),
|
|
|
|
userRoleChanged: (actorId: string, actorEmail: string, targetUserId: string, oldRole: string, newRole: string) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "role_change",
|
|
resource: "user",
|
|
resourceId: targetUserId,
|
|
details: { oldRole, newRole },
|
|
}),
|
|
|
|
// Role management events
|
|
roleCreated: (actorId: string, actorEmail: string, roleId: string, roleName: string) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "create",
|
|
resource: "role",
|
|
resourceId: roleId,
|
|
details: { roleName },
|
|
}),
|
|
|
|
roleUpdated: (actorId: string, actorEmail: string, roleId: string, changes: Record<string, unknown>) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "update",
|
|
resource: "role",
|
|
resourceId: roleId,
|
|
details: { changes },
|
|
}),
|
|
|
|
roleDeleted: (actorId: string, actorEmail: string, roleId: string, roleName: string) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "delete",
|
|
resource: "role",
|
|
resourceId: roleId,
|
|
details: { roleName },
|
|
}),
|
|
|
|
// Settings events
|
|
settingsUpdated: (actorId: string, actorEmail: string, setting: string, oldValue: unknown, newValue: unknown) =>
|
|
log({
|
|
userId: actorId,
|
|
userEmail: actorEmail,
|
|
action: "update",
|
|
resource: "settings",
|
|
resourceId: setting,
|
|
details: { oldValue, newValue },
|
|
}),
|
|
};
|
|
|
|
/**
|
|
* Purge old audit logs based on retention policy
|
|
*/
|
|
export async function purgeOldLogs(retentionDays?: number): Promise<number> {
|
|
const days = retentionDays || parseInt(process.env.AUDIT_LOG_RETENTION_DAYS || "90");
|
|
|
|
try {
|
|
const result = await pool.query(
|
|
`DELETE FROM "audit_log" WHERE timestamp < NOW() - INTERVAL '${days} days'`
|
|
);
|
|
return result.rowCount || 0;
|
|
} catch (error) {
|
|
console.error("Failed to purge audit logs:", error);
|
|
return 0;
|
|
}
|
|
}
|