localgreenchain/lib/db/audit.ts
Claude 3d2ccdc29a
feat(db): implement PostgreSQL database integration with Prisma ORM
Agent 2 - Database Integration (P0 Critical):

- Add Prisma ORM with PostgreSQL for persistent data storage
- Create comprehensive database schema with 20+ models:
  - User & authentication models
  - Plant & lineage tracking
  - Transport events & supply chain
  - Vertical farming (farms, zones, batches, recipes)
  - Demand & market matching
  - Audit logging & blockchain storage

- Implement complete database service layer (lib/db/):
  - users.ts: User CRUD with search and stats
  - plants.ts: Plant operations with lineage tracking
  - transport.ts: Transport events and carbon tracking
  - farms.ts: Vertical farm and crop batch management
  - demand.ts: Consumer preferences and market matching
  - audit.ts: Audit logging and blockchain integrity

- Add PlantChainDB for database-backed blockchain
- Create development seed script with sample data
- Add database documentation (docs/DATABASE.md)
- Update package.json with Prisma dependencies and scripts

This provides the foundation for all other agents to build upon
with persistent, scalable data storage.
2025-11-23 03:56:40 +00:00

296 lines
7.8 KiB
TypeScript

/**
* Audit & Blockchain Database Service
* Operations for audit logging and blockchain block storage
*/
import prisma from './prisma';
import type { AuditLog, BlockchainBlock, Prisma } from '@prisma/client';
import type { PaginationOptions, PaginatedResult, DateRangeFilter } from './types';
import { createPaginatedResult } from './types';
// ============================================
// AUDIT LOG OPERATIONS
// ============================================
// Create an audit log entry
export async function createAuditLog(data: {
userId?: string;
action: string;
entityType: string;
entityId?: string;
previousValue?: Record<string, unknown>;
newValue?: Record<string, unknown>;
metadata?: Record<string, unknown>;
ipAddress?: string;
userAgent?: string;
}): Promise<AuditLog> {
return prisma.auditLog.create({ data });
}
// Get audit logs with pagination
export async function getAuditLogs(
options: PaginationOptions = {},
filters?: {
userId?: string;
action?: string;
entityType?: string;
entityId?: string;
dateRange?: DateRangeFilter;
}
): Promise<PaginatedResult<AuditLog>> {
const page = options.page || 1;
const limit = options.limit || 50;
const skip = (page - 1) * limit;
const where: Prisma.AuditLogWhereInput = {};
if (filters?.userId) where.userId = filters.userId;
if (filters?.action) where.action = { contains: filters.action, mode: 'insensitive' };
if (filters?.entityType) where.entityType = filters.entityType;
if (filters?.entityId) where.entityId = filters.entityId;
if (filters?.dateRange) {
where.timestamp = {
gte: filters.dateRange.start,
lte: filters.dateRange.end,
};
}
const [logs, total] = await Promise.all([
prisma.auditLog.findMany({
where,
skip,
take: limit,
orderBy: { timestamp: 'desc' },
include: { user: true },
}),
prisma.auditLog.count({ where }),
]);
return createPaginatedResult(logs, total, page, limit);
}
// Get audit logs for entity
export async function getEntityAuditLogs(
entityType: string,
entityId: string
): Promise<AuditLog[]> {
return prisma.auditLog.findMany({
where: { entityType, entityId },
orderBy: { timestamp: 'desc' },
include: { user: true },
});
}
// Get audit logs by user
export async function getUserAuditLogs(
userId: string,
options: PaginationOptions = {}
): Promise<PaginatedResult<AuditLog>> {
const page = options.page || 1;
const limit = options.limit || 50;
const skip = (page - 1) * limit;
const [logs, total] = await Promise.all([
prisma.auditLog.findMany({
where: { userId },
skip,
take: limit,
orderBy: { timestamp: 'desc' },
}),
prisma.auditLog.count({ where: { userId } }),
]);
return createPaginatedResult(logs, total, page, limit);
}
// Get recent audit logs summary
export async function getAuditLogsSummary(hours: number = 24) {
const since = new Date(Date.now() - hours * 60 * 60 * 1000);
const [
totalLogs,
actionBreakdown,
entityBreakdown,
recentLogs,
] = await Promise.all([
prisma.auditLog.count({
where: { timestamp: { gte: since } },
}),
prisma.auditLog.groupBy({
by: ['action'],
_count: { action: true },
where: { timestamp: { gte: since } },
}),
prisma.auditLog.groupBy({
by: ['entityType'],
_count: { entityType: true },
where: { timestamp: { gte: since } },
}),
prisma.auditLog.findMany({
where: { timestamp: { gte: since } },
orderBy: { timestamp: 'desc' },
take: 10,
include: { user: true },
}),
]);
return {
totalLogs,
actionBreakdown: Object.fromEntries(
actionBreakdown.map(a => [a.action, a._count.action])
),
entityBreakdown: Object.fromEntries(
entityBreakdown.map(e => [e.entityType, e._count.entityType])
),
recentLogs,
};
}
// ============================================
// BLOCKCHAIN BLOCK OPERATIONS
// ============================================
// Create a blockchain block
export async function createBlockchainBlock(data: {
index: number;
timestamp: Date;
previousHash: string;
hash: string;
nonce: number;
blockType: string;
plantId?: string;
transportEventId?: string;
content: Record<string, unknown>;
}): Promise<BlockchainBlock> {
return prisma.blockchainBlock.create({ data });
}
// Get blockchain block by index
export async function getBlockchainBlockByIndex(index: number): Promise<BlockchainBlock | null> {
return prisma.blockchainBlock.findUnique({
where: { index },
});
}
// Get blockchain block by hash
export async function getBlockchainBlockByHash(hash: string): Promise<BlockchainBlock | null> {
return prisma.blockchainBlock.findUnique({
where: { hash },
});
}
// Get latest blockchain block
export async function getLatestBlockchainBlock(): Promise<BlockchainBlock | null> {
return prisma.blockchainBlock.findFirst({
orderBy: { index: 'desc' },
});
}
// Get blockchain blocks with pagination
export async function getBlockchainBlocks(
options: PaginationOptions = {},
filters?: {
blockType?: string;
plantId?: string;
transportEventId?: string;
}
): Promise<PaginatedResult<BlockchainBlock>> {
const page = options.page || 1;
const limit = options.limit || 50;
const skip = (page - 1) * limit;
const where: Prisma.BlockchainBlockWhereInput = {};
if (filters?.blockType) where.blockType = filters.blockType;
if (filters?.plantId) where.plantId = filters.plantId;
if (filters?.transportEventId) where.transportEventId = filters.transportEventId;
const [blocks, total] = await Promise.all([
prisma.blockchainBlock.findMany({
where,
skip,
take: limit,
orderBy: { index: 'desc' },
}),
prisma.blockchainBlock.count({ where }),
]);
return createPaginatedResult(blocks, total, page, limit);
}
// Verify blockchain integrity
export async function verifyBlockchainIntegrity(): Promise<{
isValid: boolean;
invalidBlocks: number[];
totalBlocks: number;
}> {
const blocks = await prisma.blockchainBlock.findMany({
orderBy: { index: 'asc' },
});
const invalidBlocks: number[] = [];
for (let i = 1; i < blocks.length; i++) {
const currentBlock = blocks[i];
const previousBlock = blocks[i - 1];
// Check if previous hash matches
if (currentBlock.previousHash !== previousBlock.hash) {
invalidBlocks.push(currentBlock.index);
}
}
return {
isValid: invalidBlocks.length === 0,
invalidBlocks,
totalBlocks: blocks.length,
};
}
// Get blockchain statistics
export async function getBlockchainStats() {
const [
totalBlocks,
blocksByType,
latestBlock,
oldestBlock,
] = await Promise.all([
prisma.blockchainBlock.count(),
prisma.blockchainBlock.groupBy({
by: ['blockType'],
_count: { blockType: true },
}),
prisma.blockchainBlock.findFirst({ orderBy: { index: 'desc' } }),
prisma.blockchainBlock.findFirst({ orderBy: { index: 'asc' } }),
]);
return {
totalBlocks,
blocksByType: Object.fromEntries(
blocksByType.map(b => [b.blockType, b._count.blockType])
),
latestBlockIndex: latestBlock?.index ?? -1,
latestBlockHash: latestBlock?.hash,
oldestBlockTimestamp: oldestBlock?.timestamp,
latestBlockTimestamp: latestBlock?.timestamp,
};
}
// Helper to log entity changes
export async function logEntityChange<T extends Record<string, unknown>>(
userId: string | undefined,
action: 'CREATE' | 'UPDATE' | 'DELETE',
entityType: string,
entityId: string,
previousValue: T | null,
newValue: T | null,
metadata?: Record<string, unknown>
): Promise<AuditLog> {
return createAuditLog({
userId,
action,
entityType,
entityId,
previousValue: previousValue || undefined,
newValue: newValue || undefined,
metadata,
});
}