diff --git a/frontends/nextjs/src/lib/security/secure-db/execute-query.ts b/frontends/nextjs/src/lib/security/secure-db/execute-query.ts new file mode 100644 index 000000000..aeea3501e --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/execute-query.ts @@ -0,0 +1,40 @@ +import type { SecurityContext, OperationType, ResourceType } from './types' +import { checkRateLimit } from './check-rate-limit' +import { checkAccess } from './check-access' +import { logOperation } from './log-operation' + +/** + * Execute a secure database query with rate limiting, access control, and audit logging + */ +export async function executeQuery( + ctx: SecurityContext, + resource: ResourceType, + operation: OperationType, + queryFn: () => Promise, + resourceId: string = 'unknown' +): Promise { + // Check rate limit + const canProceed = checkRateLimit(ctx.user.id) + if (!canProceed) { + await logOperation(ctx, operation, resource, resourceId, false, 'Rate limit exceeded') + throw new Error('Rate limit exceeded. Please try again later.') + } + + // Check access permissions + const hasAccess = await checkAccess(ctx, resource, operation, resourceId) + if (!hasAccess) { + await logOperation(ctx, operation, resource, resourceId, false, 'Access denied') + throw new Error('Access denied. Insufficient permissions.') + } + + // Execute query + try { + const result = await queryFn() + await logOperation(ctx, operation, resource, resourceId, true) + return result + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + await logOperation(ctx, operation, resource, resourceId, false, errorMessage) + throw error + } +} diff --git a/frontends/nextjs/src/lib/security/secure-db/index.ts b/frontends/nextjs/src/lib/security/secure-db/index.ts new file mode 100644 index 000000000..2b9600d6f --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/index.ts @@ -0,0 +1,61 @@ +// Types +export type { + OperationType, + ResourceType, + AuditLog, + SecurityContext, + AccessRule +} from './types' + +// Core functions +export { ACCESS_RULES } from './access-rules' +export { checkAccess } from './check-access' +export { checkRateLimit, clearRateLimit, clearAllRateLimits } from './check-rate-limit' +export { sanitizeInput } from './sanitize-input' +export { logOperation } from './log-operation' +export { executeQuery } from './execute-query' + +// Operations +export { getUsers } from './operations/get-users' +export { getUserById } from './operations/get-user-by-id' +export { createUser } from './operations/create-user' +export { updateUser } from './operations/update-user' +export { deleteUser } from './operations/delete-user' +export { verifyCredentials } from './operations/verify-credentials' + +// Import all for namespace class +import { ACCESS_RULES } from './access-rules' +import { checkAccess } from './check-access' +import { checkRateLimit, clearRateLimit, clearAllRateLimits } from './check-rate-limit' +import { sanitizeInput } from './sanitize-input' +import { logOperation } from './log-operation' +import { executeQuery } from './execute-query' +import { getUsers } from './operations/get-users' +import { getUserById } from './operations/get-user-by-id' +import { createUser } from './operations/create-user' +import { updateUser } from './operations/update-user' +import { deleteUser } from './operations/delete-user' +import { verifyCredentials } from './operations/verify-credentials' + +/** + * SecureDatabase namespace class - groups all secure DB operations as static methods + */ +export class SecureDatabase { + // Core + static ACCESS_RULES = ACCESS_RULES + static checkAccess = checkAccess + static checkRateLimit = checkRateLimit + static clearRateLimit = clearRateLimit + static clearAllRateLimits = clearAllRateLimits + static sanitizeInput = sanitizeInput + static logOperation = logOperation + static executeQuery = executeQuery + + // Operations + static getUsers = getUsers + static getUserById = getUserById + static createUser = createUser + static updateUser = updateUser + static deleteUser = deleteUser + static verifyCredentials = verifyCredentials +} diff --git a/frontends/nextjs/src/lib/security/secure-db/log-operation.ts b/frontends/nextjs/src/lib/security/secure-db/log-operation.ts new file mode 100644 index 000000000..4189f1535 --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/log-operation.ts @@ -0,0 +1,38 @@ +import type { SecurityContext, OperationType, ResourceType, AuditLog } from './types' + +/** + * Log an operation for audit trail + */ +export async function logOperation( + ctx: SecurityContext, + operation: OperationType, + resource: ResourceType, + resourceId: string, + success: boolean, + errorMessage?: string +): Promise { + const log: AuditLog = { + id: `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + timestamp: Date.now(), + userId: ctx.user.id, + username: ctx.user.username, + operation, + resource, + resourceId, + success, + errorMessage, + ipAddress: ctx.ipAddress, + } + + try { + // TODO: Replace with proper audit log storage + // For now, just log to console in development + if (process.env.NODE_ENV === 'development') { + console.log('[AUDIT]', log) + } + // In production, this would write to a persistent audit log table + // await Database.addAuditLog(log) + } catch (error) { + console.error('Failed to log operation:', error) + } +} diff --git a/frontends/nextjs/src/lib/security/secure-db/operations/create-user.ts b/frontends/nextjs/src/lib/security/secure-db/operations/create-user.ts new file mode 100644 index 000000000..f7a971a7f --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/operations/create-user.ts @@ -0,0 +1,48 @@ +import { prisma } from '../../db/prisma' +import type { User } from '../../types/level-types' +import type { SecurityContext } from './types' +import { executeQuery } from './execute-query' +import { sanitizeInput } from './sanitize-input' + +/** + * Create a new user with security checks + */ +export async function createUser( + ctx: SecurityContext, + userData: Omit +): Promise { + const sanitized = sanitizeInput(userData) + + return executeQuery( + ctx, + 'user', + 'CREATE', + async () => { + const user = await prisma.user.create({ + data: { + id: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + username: sanitized.username, + email: sanitized.email, + role: sanitized.role, + profilePicture: sanitized.profilePicture, + bio: sanitized.bio, + createdAt: BigInt(Date.now()), + tenantId: sanitized.tenantId, + isInstanceOwner: sanitized.isInstanceOwner || false, + }, + }) + return { + id: user.id, + username: user.username, + email: user.email, + role: user.role as User['role'], + profilePicture: user.profilePicture || undefined, + bio: user.bio || undefined, + createdAt: Number(user.createdAt), + tenantId: user.tenantId || undefined, + isInstanceOwner: user.isInstanceOwner, + } + }, + 'new_user' + ) +} diff --git a/frontends/nextjs/src/lib/security/secure-db/operations/delete-user.ts b/frontends/nextjs/src/lib/security/secure-db/operations/delete-user.ts new file mode 100644 index 000000000..fa3ee2899 --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/operations/delete-user.ts @@ -0,0 +1,18 @@ +import { prisma } from '../../db/prisma' +import type { SecurityContext } from './types' +import { executeQuery } from './execute-query' + +/** + * Delete a user with security checks + */ +export async function deleteUser(ctx: SecurityContext, userId: string): Promise { + return executeQuery( + ctx, + 'user', + 'DELETE', + async () => { + await prisma.user.delete({ where: { id: userId } }) + }, + userId + ) +} diff --git a/frontends/nextjs/src/lib/security/secure-db/operations/get-user-by-id.ts b/frontends/nextjs/src/lib/security/secure-db/operations/get-user-by-id.ts new file mode 100644 index 000000000..2e84eb934 --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/operations/get-user-by-id.ts @@ -0,0 +1,31 @@ +import { prisma } from '../../db/prisma' +import type { User } from '../../types/level-types' +import type { SecurityContext } from './types' +import { executeQuery } from './execute-query' + +/** + * Get a user by ID with security checks + */ +export async function getUserById(ctx: SecurityContext, userId: string): Promise { + return executeQuery( + ctx, + 'user', + 'READ', + async () => { + const user = await prisma.user.findUnique({ where: { id: userId } }) + if (!user) return null + return { + id: user.id, + username: user.username, + email: user.email, + role: user.role as User['role'], + profilePicture: user.profilePicture || undefined, + bio: user.bio || undefined, + createdAt: Number(user.createdAt), + tenantId: user.tenantId || undefined, + isInstanceOwner: user.isInstanceOwner, + } + }, + userId + ) +} diff --git a/frontends/nextjs/src/lib/security/secure-db/operations/get-users.ts b/frontends/nextjs/src/lib/security/secure-db/operations/get-users.ts new file mode 100644 index 000000000..2fd1368b1 --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/operations/get-users.ts @@ -0,0 +1,30 @@ +import { prisma } from '../../db/prisma' +import type { User } from '../../types/level-types' +import type { SecurityContext } from './types' +import { executeQuery } from './execute-query' + +/** + * Get all users with security checks + */ +export async function getUsers(ctx: SecurityContext): Promise { + return executeQuery( + ctx, + 'user', + 'READ', + async () => { + const users = await prisma.user.findMany() + return users.map(u => ({ + id: u.id, + username: u.username, + email: u.email, + role: u.role as User['role'], + profilePicture: u.profilePicture || undefined, + bio: u.bio || undefined, + createdAt: Number(u.createdAt), + tenantId: u.tenantId || undefined, + isInstanceOwner: u.isInstanceOwner, + })) + }, + 'all_users' + ) +} diff --git a/frontends/nextjs/src/lib/security/secure-db/operations/update-user.ts b/frontends/nextjs/src/lib/security/secure-db/operations/update-user.ts new file mode 100644 index 000000000..49dfbd952 --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/operations/update-user.ts @@ -0,0 +1,47 @@ +import { prisma } from '../../db/prisma' +import type { User } from '../../types/level-types' +import type { SecurityContext } from './types' +import { executeQuery } from './execute-query' +import { sanitizeInput } from './sanitize-input' + +/** + * Update a user with security checks + */ +export async function updateUser( + ctx: SecurityContext, + userId: string, + updates: Partial +): Promise { + const sanitized = sanitizeInput(updates) + + return executeQuery( + ctx, + 'user', + 'UPDATE', + async () => { + const user = await prisma.user.update({ + where: { id: userId }, + data: { + username: sanitized.username, + email: sanitized.email, + role: sanitized.role, + profilePicture: sanitized.profilePicture, + bio: sanitized.bio, + tenantId: sanitized.tenantId, + }, + }) + return { + id: user.id, + username: user.username, + email: user.email, + role: user.role as User['role'], + profilePicture: user.profilePicture || undefined, + bio: user.bio || undefined, + createdAt: Number(user.createdAt), + tenantId: user.tenantId || undefined, + isInstanceOwner: user.isInstanceOwner, + } + }, + userId + ) +} diff --git a/frontends/nextjs/src/lib/security/secure-db/operations/verify-credentials.ts b/frontends/nextjs/src/lib/security/secure-db/operations/verify-credentials.ts new file mode 100644 index 000000000..275d8e96b --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/operations/verify-credentials.ts @@ -0,0 +1,37 @@ +import { prisma } from '../../db/prisma' +import type { SecurityContext } from './types' +import { executeQuery } from './execute-query' +import { sanitizeInput } from './sanitize-input' + +/** + * Verify user credentials with security checks + */ +export async function verifyCredentials( + ctx: SecurityContext, + username: string, + password: string +): Promise { + const sanitizedUsername = sanitizeInput(username) + + return executeQuery( + ctx, + 'credential', + 'READ', + async () => { + const credential = await prisma.credential.findUnique({ + where: { username: sanitizedUsername }, + }) + + if (!credential) return false + + const encoder = new TextEncoder() + const data = encoder.encode(password) + const hashBuffer = await crypto.subtle.digest('SHA-512', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') + + return hashHex === credential.passwordHash + }, + sanitizedUsername + ) +}