1 Commits

Author SHA1 Message Date
19c574c7d3 build(deps): bump next from 15.3.4 to 15.5.6
Bumps [next](https://github.com/vercel/next.js) from 15.3.4 to 15.5.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.3.4...v15.5.6)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 01:15:50 +00:00
237 changed files with 5430 additions and 62967 deletions

View File

@ -0,0 +1 @@
Use pnpm to manage this project, not npm!

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
*-PROGRESS.md
pr-comments*.json
# Created by https://www.toptal.com/developers/gitignore/api/node,nextjs,react
# Edit at https://www.toptal.com/developers/gitignore?templates=node,nextjs,react

View File

@ -1,27 +0,0 @@
#!/bin/sh
# Check for Claude Code attribution in commit messages
# This hook prevents commits that contain self-promotional attribution
commit_msg_file="$1"
commit_msg=$(cat "$commit_msg_file")
# Check for Claude Code attribution patterns
if echo "$commit_msg" | grep -E -q "(🤖.*Generated.*Claude|Claude Code|claude\.ai/code|Co-Authored-By:.*Claude|noreply@anthropic\.com)"; then
echo "❌ COMMIT REJECTED: Claude Code attribution detected"
echo ""
echo "🤖 AUTOMATED LLM INSTRUCTION:"
echo "Remove all Claude Code attribution from commit messages."
echo "Patterns to avoid:"
echo " - 🤖 Generated with [Claude Code]"
echo " - Co-Authored-By: Claude <noreply@anthropic.com>"
echo " - References to claude.ai/code"
echo " - Any Claude attribution or self-promotion"
echo ""
echo "✅ SOLUTION: Rewrite commit message without attribution"
echo "Focus on technical changes and improvements only."
exit 1
fi
echo "✅ Commit message approved"
exit 0

2
.husky/pre-commit Executable file → Normal file
View File

@ -1 +1 @@
lint-staged
npx lint-staged

View File

@ -1,28 +0,0 @@
# Don't ignore doc files - we'll use prettier-ignore comments instead
## Ignore lockfile
pnpm-lock.yaml
package-lock.json
## Ignore build outputs
.next
dist
build
out
## Ignore dependencies
node_modules
## Files that are formatted by biome
**/*.js
**/*.ts
**/*.cjs
**/*.cts
**/*.mjs
**/*.mts
**/*.d.cts
**/*.d.mts
**/*.jsx
**/*.tsx
**/*.json
**/*.jsonc

View File

@ -35,24 +35,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `pnpm test:vitest:coverage` - Run Vitest with coverage report
- `pnpm test:coverage` - Run all tests with coverage
**Security Testing:**
- `pnpm test:security` - Run security-specific tests
- `pnpm test:security-headers` - Test HTTP security headers implementation
- `pnpm test:csp` - Test CSP implementation and nonce generation
- `pnpm test:csp:validate` - Validate CSP implementation with security scoring
- `pnpm test:csp:full` - Comprehensive CSP test suite
**Migration & Deployment:**
- `pnpm migration:backup` - Create database backup
- `pnpm migration:validate-db` - Validate database schema and integrity
- `pnpm migration:validate-env` - Validate environment configuration
- `pnpm migration:pre-check` - Run pre-deployment validation checks
- `pnpm migration:health-check` - Run system health checks
- `pnpm migration:deploy` - Execute full deployment process
- `pnpm migration:rollback` - Rollback failed migration
**Markdown:**
- `pnpm lint:md` - Lint Markdown files
@ -105,9 +87,6 @@ The system processes user sessions through distinct stages tracked in `SessionPr
- `lib/processingScheduler.ts` - AI analysis pipeline
- `lib/transcriptFetcher.ts` - External transcript fetching
- `lib/transcriptParser.ts` - Message parsing from transcripts
- `lib/batchProcessor.ts` - OpenAI Batch API integration for cost-efficient processing
- `lib/batchScheduler.ts` - Automated batch job lifecycle management
- `lib/rateLimiter.ts` - In-memory rate limiting utility for API endpoints
### Development Environment
@ -138,11 +117,10 @@ Environment variables are managed through `lib/env.ts` with .env.local file supp
- Schedulers are optional and controlled by `SCHEDULER_ENABLED` environment variable
- Use `pnpm dev:next-only` to run without schedulers for pure frontend development
- Four separate schedulers handle different pipeline stages:
- Three separate schedulers handle different pipeline stages:
- CSV Import Scheduler (`lib/scheduler.ts`)
- Import Processing Scheduler (`lib/importProcessor.ts`)
- Session Processing Scheduler (`lib/processingScheduler.ts`)
- Batch Processing Scheduler (`lib/batchScheduler.ts`) - Manages OpenAI Batch API lifecycle
**Database Migrations:**
@ -157,11 +135,6 @@ Environment variables are managed through `lib/env.ts` with .env.local file supp
- Support for multiple AI models per company
- Time-based pricing management for accurate cost calculation
- Processing stages can be retried on failure with retry count tracking
- **Batch API Integration**: 50% cost reduction using OpenAI Batch API
- Automatic batching of AI requests every 5 minutes
- Batch status checking every 2 minutes
- Result processing every minute
- Failed request retry with individual API calls
**Code Quality Standards:**
@ -169,50 +142,3 @@ Environment variables are managed through `lib/env.ts` with .env.local file supp
- TypeScript with ES modules (type: "module" in package.json)
- React 19 with Next.js 15 App Router
- TailwindCSS 4 for styling
**Security Features:**
- **Comprehensive CSRF Protection**: Multi-layer CSRF protection with automatic token management
- Middleware-level protection for all state-changing endpoints
- tRPC integration with CSRF-protected procedures
- Client-side hooks and components for seamless integration
- HTTP-only cookies with SameSite protection
- **Enhanced Content Security Policy (CSP)**:
- Nonce-based script execution for maximum XSS protection
- Environment-specific policies (strict production, permissive development)
- Real-time violation reporting and bypass detection
- Automated policy optimization recommendations
- **Security Monitoring & Audit System**:
- Real-time threat detection and alerting
- Comprehensive security audit logging with retention management
- Geographic anomaly detection and IP threat analysis
- Security scoring and automated incident response
- **Advanced Rate Limiting**: In-memory rate limiting system
- Authentication endpoints: Login (5/15min), Registration (3/hour), Password Reset (5/15min)
- CSP reporting: 10 reports per minute per IP
- Admin endpoints: Configurable thresholds
- **Input Validation & Security Headers**:
- Comprehensive Zod schemas for all user inputs with XSS/injection prevention
- HTTP security headers (HSTS, X-Frame-Options, X-Content-Type-Options, Permissions Policy)
- Strong password requirements and email validation
- **Session Security**:
- JWT tokens with 24-hour expiration and secure cookie settings
- HttpOnly, Secure, SameSite cookies with proper CSP integration
- Company isolation and multi-tenant security
**Code Quality & Linting:**
- **Biome Integration**: Primary linting and formatting tool
- Pre-commit hooks enforce code quality standards
- Some security-critical patterns require `biome-ignore` comments
- Non-null assertions (`!`) used intentionally in authenticated contexts require ignore comments
- Complex functions may need refactoring to meet complexity thresholds (max 15)
- Performance classes use static-only patterns which may trigger warnings
- **TypeScript Strict Mode**: Comprehensive type checking
- Avoid `any` types where possible; use proper type definitions
- Optional chaining vs non-null assertions: choose based on security context
- In authenticated API handlers, non-null assertions are often safer than optional chaining
- **Security vs Linting Balance**:
- Security takes precedence over linting rules when they conflict
- Document security-critical choices with detailed comments
- Use `// biome-ignore` with explanations for intentional rule violations

View File

@ -1,285 +0,0 @@
# Documentation Audit Summary
## Overview
This document summarizes the comprehensive documentation audit performed on the LiveDash-Node project, identifying gaps, outdated information, and newly created documentation to address missing coverage.
## Audit Findings
### Well-Documented Areas ✅
The following areas were found to have comprehensive, accurate documentation:
1. **CSRF Protection** (`docs/CSRF_PROTECTION.md`)
- Multi-layer protection implementation
- Client-side integration guide
- tRPC integration details
- Comprehensive examples
2. **Enhanced CSP Implementation** (`docs/security/enhanced-csp.md`)
- Nonce-based script execution
- Environment-specific policies
- Violation reporting and monitoring
- Testing framework
3. **Security Headers** (`docs/security-headers.md`)
- Complete header implementation details
- Testing procedures
- Compatibility information
4. **Security Monitoring System** (`docs/security-monitoring.md`)
- Real-time threat detection
- Alert management
- API usage examples
- Performance considerations
5. **Migration Guide** (`MIGRATION_GUIDE.md`)
- Comprehensive v2.0.0 migration procedures
- Rollback procedures
- Health checks and validation
### Major Issues Identified ❌
#### 1. README.md - Critically Outdated
**Problems Found:**
- Listed database as "SQLite (default)" when project uses PostgreSQL
- Missing all new security features (CSRF, CSP, security monitoring)
- Incomplete environment setup section
- Outdated tech stack (missing tRPC, security features)
- Project structure didn't reflect new admin/security directories
**Actions Taken:**
- ✅ Updated features section to include security and admin capabilities
- ✅ Corrected tech stack to include PostgreSQL, tRPC, security features
- ✅ Updated environment setup with proper PostgreSQL configuration
- ✅ Revised project structure to reflect current codebase
- ✅ Added comprehensive script documentation
#### 2. Undocumented API Endpoints
**Missing Documentation:**
- `/api/admin/audit-logs/` (GET) - Audit log retrieval with filtering
- `/api/admin/audit-logs/retention/` (POST) - Retention management
- `/api/admin/security-monitoring/` (GET/POST) - Security metrics and config
- `/api/admin/security-monitoring/alerts/` - Alert management
- `/api/admin/security-monitoring/export/` - Data export
- `/api/admin/security-monitoring/threat-analysis/` - Threat analysis
- `/api/admin/batch-monitoring/` - Batch processing monitoring
- `/api/csp-report/` (POST) - CSP violation reporting
- `/api/csp-metrics/` (GET) - CSP metrics and analytics
- `/api/csrf-token/` (GET) - CSRF token endpoint
**Actions Taken:**
- ✅ Created `docs/admin-audit-logs-api.md` - Comprehensive audit logs API documentation
- ✅ Created `docs/csp-metrics-api.md` - CSP monitoring and metrics API documentation
- ✅ Created `docs/api-reference.md` - Complete API reference for all endpoints
#### 3. Undocumented Features and Components
**Missing Feature Documentation:**
- Batch monitoring dashboard and UI components
- Security monitoring UI components
- Nonce-based CSP context provider
- Enhanced rate limiting system
- Security audit retention system
**Actions Taken:**
- ✅ Created `docs/batch-monitoring-dashboard.md` - Complete batch monitoring documentation
#### 4. CLAUDE.md - Missing New Commands
**Problems Found:**
- Missing security testing commands
- Missing CSP testing commands
- Missing migration/deployment commands
- Outdated security features section
**Actions Taken:**
- ✅ Added security testing command section
- ✅ Added CSP testing commands
- ✅ Added migration and deployment commands
- ✅ Updated security features section with comprehensive details
## New Documentation Created
### 1. Admin Audit Logs API Documentation
**File:** `docs/admin-audit-logs-api.md`
**Contents:**
- Complete API endpoint documentation with examples
- Authentication and authorization requirements
- Query parameters and filtering options
- Response formats and error handling
- Retention management procedures
- Security features and rate limiting
- Usage examples and integration patterns
- Performance considerations and troubleshooting
### 2. CSP Metrics and Monitoring API Documentation
**File:** `docs/csp-metrics-api.md`
**Contents:**
- CSP violation reporting endpoint documentation
- Metrics API with real-time violation tracking
- Risk assessment and bypass detection features
- Policy optimization recommendations
- Configuration and setup instructions
- Performance considerations and security features
- Usage examples for monitoring and analysis
- Integration with existing security systems
### 3. Batch Monitoring Dashboard Documentation
**File:** `docs/batch-monitoring-dashboard.md`
**Contents:**
- Comprehensive batch processing monitoring guide
- Real-time monitoring capabilities and features
- API endpoints for batch job tracking
- Dashboard component documentation
- Performance analytics and cost analysis
- Administrative controls and error handling
- Configuration and alert management
- Troubleshooting and optimization guides
### 4. Complete API Reference
**File:** `docs/api-reference.md`
**Contents:**
- Comprehensive reference for all API endpoints
- Authentication and CSRF protection requirements
- Detailed request/response formats
- Error codes and status descriptions
- Rate limiting information
- Security headers and CORS configuration
- Pagination and filtering standards
- Testing and integration examples
## Updated Documentation
### 1. README.md - Complete Overhaul
**Key Updates:**
- ✅ Updated project description to include security and admin features
- ✅ Corrected tech stack to reflect current implementation
- ✅ Fixed database information (PostgreSQL vs SQLite)
- ✅ Added comprehensive environment configuration
- ✅ Updated project structure to match current codebase
- ✅ Added security, migration, and testing command sections
- ✅ Enhanced features section with detailed capabilities
### 2. CLAUDE.md - Enhanced Developer Guide
**Key Updates:**
- ✅ Added security testing commands section
- ✅ Added CSP testing and validation commands
- ✅ Added migration and deployment commands
- ✅ Enhanced security features documentation
- ✅ Updated with comprehensive CSRF, CSP, and monitoring details
## Documentation Quality Assessment
### Coverage Analysis
| Area | Before | After | Status |
| ------------------ | ------ | ----- | ------------ |
| Core Features | 85% | 95% | ✅ Excellent |
| Security Features | 70% | 98% | ✅ Excellent |
| API Endpoints | 40% | 95% | ✅ Excellent |
| Admin Features | 20% | 90% | ✅ Excellent |
| Developer Workflow | 80% | 95% | ✅ Excellent |
| Testing Procedures | 60% | 90% | ✅ Excellent |
### Documentation Standards
All new and updated documentation follows these standards:
- ✅ Clear, actionable examples
- ✅ Comprehensive API documentation with request/response examples
- ✅ Security considerations and best practices
- ✅ Troubleshooting sections
- ✅ Integration patterns and usage examples
- ✅ Performance considerations
- ✅ Cross-references to related documentation
## Recommendations for Maintenance
### 1. Regular Review Schedule
- **Monthly**: Review API documentation for new endpoints
- **Quarterly**: Update security feature documentation
- **Per Release**: Validate all examples and code snippets
- **Annually**: Comprehensive documentation audit
### 2. Documentation Automation
- Add documentation checks to CI/CD pipeline
- Implement API documentation generation from OpenAPI specs
- Set up automated link checking
- Create documentation review templates
### 3. Developer Onboarding
- Use updated documentation for new developer onboarding
- Create documentation feedback process
- Maintain documentation contribution guidelines
- Track documentation usage and feedback
### 4. Continuous Improvement
- Monitor documentation gaps through developer feedback
- Update examples with real-world usage patterns
- Enhance troubleshooting sections based on support issues
- Keep security documentation current with threat landscape
## Summary
The documentation audit identified significant gaps in API documentation, outdated project information, and missing coverage of new security features. Through comprehensive updates and new documentation creation, the project now has:
- **Complete API Reference**: All endpoints documented with examples
- **Accurate Project Information**: README and CLAUDE.md reflect current state
- **Comprehensive Security Documentation**: All security features thoroughly documented
- **Developer-Friendly Guides**: Clear setup, testing, and deployment procedures
- **Administrative Documentation**: Complete coverage of admin and monitoring features
The documentation is now production-ready and provides comprehensive guidance for developers, administrators, and security teams working with the LiveDash-Node application.
## Files Modified/Created
### Modified Files
1. `README.md` - Complete overhaul with accurate project information
2. `CLAUDE.md` - Enhanced with security testing and migration commands
### New Documentation Files
1. `docs/admin-audit-logs-api.md` - Admin audit logs API documentation
2. `docs/csp-metrics-api.md` - CSP monitoring and metrics API documentation
3. `docs/batch-monitoring-dashboard.md` - Batch monitoring dashboard documentation
4. `docs/api-reference.md` - Comprehensive API reference
5. `DOCUMENTATION_AUDIT_SUMMARY.md` - This summary document
All documentation is now current, comprehensive, and ready for production use.

View File

@ -3,7 +3,6 @@
## Issues Identified
From your logs:
```
Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.neon.tech:5432`
[NODE-CRON] [WARN] missed execution! Possible blocking IO or high CPU
@ -19,26 +18,22 @@ Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.ne
## Fixes Applied ✅
### 1. Connection Retry Logic (`lib/database-retry.ts`)
- **Automatic retry** for connection errors
- **Exponential backoff** (1s → 2s → 4s → 10s max)
- **Smart error detection** (only retry connection issues)
- **Configurable retry attempts** (default: 3 retries)
### 2. Enhanced Schedulers
- **Import Processor**: Added retry wrapper around main processing
- **Session Processor**: Added retry wrapper around AI processing
- **Graceful degradation** when database is temporarily unavailable
### 3. Singleton Pattern Enforced
- **All schedulers now use** `import { prisma } from "./prisma.js"`
- **No more separate** `new PrismaClient()` instances
- **Shared connection pool** across all operations
### 4. Neon-Specific Optimizations
- **Connection limit guidance**: 15 connections (below Neon's 20 limit)
- **Extended timeouts**: 30s for cold start handling
- **SSL mode requirements**: `sslmode=require` for Neon
@ -47,7 +42,6 @@ Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.ne
## Immediate Actions Needed
### 1. Update Environment Variables
```bash
# Add to .env.local
USE_ENHANCED_POOLING=true
@ -59,7 +53,6 @@ DATABASE_URL="postgresql://user:pass@ep-tiny-math-a2zsshve-pooler.eu-central-1.a
```
### 2. Reduce Scheduler Frequency (Optional)
```bash
# Less aggressive intervals
CSV_IMPORT_INTERVAL="*/30 * * * *" # Every 30 min (was 15)
@ -68,7 +61,6 @@ SESSION_PROCESSING_INTERVAL="0 */2 * * *" # Every 2 hours (was 1)
```
### 3. Run Configuration Check
```bash
pnpm db:check
```

View File

@ -1,450 +0,0 @@
# LiveDash Node Migration Guide v2.0.0
## Overview
This guide provides step-by-step instructions for migrating LiveDash Node to version 2.0.0, which introduces tRPC implementation and OpenAI Batch API integration for improved performance and cost efficiency.
## 🚀 New Features
### tRPC Implementation
- **Type-safe APIs**: End-to-end TypeScript safety from client to server
- **Improved Performance**: Optimized query batching and caching
- **Better Developer Experience**: Auto-completion and type checking
- **Simplified Authentication**: Integrated with existing NextAuth.js setup
### OpenAI Batch API Integration
- **50% Cost Reduction**: Batch processing reduces OpenAI API costs by half
- **Enhanced Rate Limiting**: Better throughput management
- **Improved Reliability**: Automatic retry mechanisms and error handling
- **Automated Processing**: Background batch job lifecycle management
### Enhanced Security & Performance
- **Rate Limiting**: In-memory rate limiting for all authentication endpoints
- **Input Validation**: Comprehensive Zod schemas for all user inputs
- **Performance Monitoring**: Built-in metrics collection and monitoring
- **Database Optimizations**: New indexes and query optimizations
## 📋 Pre-Migration Checklist
### System Requirements
- [ ] Node.js 18+ installed
- [ ] PostgreSQL 13+ database
- [ ] `pg_dump` and `pg_restore` utilities available
- [ ] Git repository with clean working directory
- [ ] OpenAI API key (for production)
- [ ] Sufficient disk space for backups (at least 2GB)
### Environment Preparation
- [ ] Review current environment variables
- [ ] Ensure database connection is working
- [ ] Verify all tests are passing
- [ ] Create a backup of your current deployment
- [ ] Notify team members of planned downtime
## 🔧 Migration Process
### Phase 1: Pre-Migration Setup
#### 1.1 Install Migration Tools
```bash
# Ensure you have the latest dependencies
pnpm install
# Verify migration scripts are available
pnpm migration:validate-env --help
```
#### 1.2 Run Pre-Deployment Checks
```bash
# Run comprehensive pre-deployment validation
pnpm migration:pre-check
# This will validate:
# - Environment configuration
# - Database connection and schema
# - Dependencies
# - File system permissions
# - OpenAI API access
# - tRPC infrastructure readiness
```
#### 1.3 Environment Configuration
```bash
# Generate new environment variables
pnpm migration:migrate-env
# Review the generated files:
# - .env.migration.template
# - ENVIRONMENT_MIGRATION_GUIDE.md
```
**Add these new environment variables to your `.env.local`:**
```bash
# tRPC Configuration
TRPC_ENDPOINT_URL="http://localhost:3000/api/trpc"
TRPC_BATCH_TIMEOUT="30000"
TRPC_MAX_BATCH_SIZE="100"
# Batch Processing Configuration
BATCH_PROCESSING_ENABLED="true"
BATCH_CREATE_INTERVAL="*/5 * * * *"
BATCH_STATUS_CHECK_INTERVAL="*/2 * * * *"
BATCH_RESULT_PROCESSING_INTERVAL="*/1 * * * *"
BATCH_MAX_REQUESTS="1000"
BATCH_TIMEOUT_HOURS="24"
# Security & Performance
RATE_LIMIT_WINDOW_MS="900000"
RATE_LIMIT_MAX_REQUESTS="100"
PERFORMANCE_MONITORING_ENABLED="true"
METRICS_COLLECTION_INTERVAL="60"
# Migration Settings (temporary)
MIGRATION_MODE="production"
MIGRATION_BACKUP_ENABLED="true"
MIGRATION_ROLLBACK_ENABLED="true"
```
### Phase 2: Database Migration
#### 2.1 Create Database Backup
```bash
# Create full database backup
pnpm migration:backup
# Verify backup was created
pnpm migration:backup list
```
#### 2.2 Validate Database Schema
```bash
# Validate current database state
pnpm migration:validate-db
```
#### 2.3 Apply Database Migrations
```bash
# Run Prisma migrations
pnpm prisma:migrate
# Apply additional schema changes
psql $DATABASE_URL -f scripts/migration/01-schema-migrations.sql
# Verify migration success
pnpm migration:validate-db
```
### Phase 3: Application Deployment
#### 3.1 Dry Run Deployment
```bash
# Test deployment process without making changes
pnpm migration:deploy:dry-run
```
#### 3.2 Full Deployment
```bash
# Execute full deployment
pnpm migration:deploy
# This will:
# 1. Apply database schema changes
# 2. Deploy new application code
# 3. Restart services with minimal downtime
# 4. Enable tRPC endpoints progressively
# 5. Activate batch processing system
# 6. Run post-deployment validation
```
### Phase 4: Post-Migration Validation
#### 4.1 System Health Check
```bash
# Run comprehensive health checks
pnpm migration:health-check
# Generate detailed health report
pnpm migration:health-report
```
#### 4.2 Feature Validation
```bash
# Test tRPC endpoints
pnpm exec tsx scripts/migration/trpc-endpoint-tests.ts
# Test batch processing system
pnpm exec tsx scripts/migration/batch-processing-tests.ts
# Run full test suite
pnpm migration:test
```
## 🔄 Rollback Procedure
If issues occur during migration, you can rollback using these steps:
### Automatic Rollback
```bash
# Quick rollback (if migration failed)
pnpm migration:rollback
# Dry run rollback to see what would happen
pnpm migration:rollback:dry-run
```
### Manual Rollback Steps
1. **Stop the application**
2. **Restore database from backup**
3. **Revert to previous code version**
4. **Restart services**
5. **Verify system functionality**
### Rollback Commands
```bash
# Create rollback snapshot (before migration)
pnpm migration:rollback:snapshot
# Restore from specific backup
pnpm migration:rollback --backup /path/to/backup.sql
# Skip database rollback (code only)
pnpm migration:rollback --no-database
```
## 📊 Monitoring and Validation
### Post-Migration Monitoring
#### 1. Application Health
```bash
# Check system health every hour for the first day
*/60 * * * * cd /path/to/livedash && pnpm migration:health-check
# Monitor logs for errors
tail -f logs/migration.log
```
#### 2. tRPC Performance
- Monitor response times for tRPC endpoints
- Check error rates in application logs
- Verify type safety is working correctly
#### 3. Batch Processing
- Monitor batch job completion rates
- Check OpenAI API cost reduction
- Verify AI processing pipeline functionality
### Key Metrics to Monitor
#### Performance Metrics
- **Response Times**: tRPC endpoints should respond within 500ms
- **Database Queries**: Complex queries should complete within 1s
- **Memory Usage**: Should remain below 80% of allocated memory
- **CPU Usage**: Process should remain responsive
#### Business Metrics
- **AI Processing Cost**: Should see ~50% reduction in OpenAI costs
- **Processing Throughput**: Batch processing should handle larger volumes
- **Error Rates**: Should remain below 1% for critical operations
- **User Experience**: No degradation in dashboard performance
## 🛠 Troubleshooting
### Common Issues and Solutions
#### tRPC Endpoints Not Working
```bash
# Check if tRPC files exist
ls -la app/api/trpc/[trpc]/route.ts
ls -la server/routers/_app.ts
# Verify tRPC router exports
pnpm exec tsx -e "import('./server/routers/_app').then(m => console.log(Object.keys(m)))"
# Test endpoints manually
curl -X POST http://localhost:3000/api/trpc/auth.getSession \
-H "Content-Type: application/json" \
-d '{"json": null}'
```
#### Batch Processing Issues
```bash
# Check batch processing components
pnpm exec tsx scripts/migration/batch-processing-tests.ts
# Verify OpenAI API access
curl -H "Authorization: Bearer $OPENAI_API_KEY" \
https://api.openai.com/v1/models
# Check batch job status
psql $DATABASE_URL -c "SELECT status, COUNT(*) FROM \"AIBatchRequest\" GROUP BY status;"
```
#### Database Issues
```bash
# Check database connection
pnpm db:check
# Verify schema integrity
pnpm migration:validate-db
# Check for missing indexes
psql $DATABASE_URL -c "
SELECT schemaname, tablename, indexname
FROM pg_indexes
WHERE tablename IN ('Session', 'AIProcessingRequest', 'AIBatchRequest')
ORDER BY tablename, indexname;
"
```
#### Environment Configuration Issues
```bash
# Validate environment variables
pnpm migration:validate-env
# Check for missing variables
env | grep -E "(TRPC|BATCH|RATE_LIMIT)" | sort
# Verify environment file syntax
node -e "require('dotenv').config({path: '.env.local'}); console.log('✅ Environment file is valid')"
```
### Getting Help
#### Support Channels
1. **Check Migration Logs**: Review `logs/migration.log` for detailed error information
2. **Run Diagnostics**: Use the built-in health check and validation tools
3. **Documentation**: Refer to component-specific documentation in `docs/`
4. **Emergency Rollback**: Use rollback procedures if issues persist
#### Useful Commands
```bash
# Get detailed system information
pnpm migration:health-report
# Check all migration script availability
ls -la scripts/migration/
# Verify package integrity
pnpm install --frozen-lockfile
# Test database connectivity
pnpm prisma db pull --print
```
## 📝 Post-Migration Tasks
### Immediate Tasks (First 24 Hours)
- [ ] Monitor application logs for errors
- [ ] Verify all tRPC endpoints are responding correctly
- [ ] Check batch processing job completion
- [ ] Validate AI cost reduction in OpenAI dashboard
- [ ] Run full test suite to ensure no regressions
- [ ] Update documentation and team knowledge
### Medium-term Tasks (First Week)
- [ ] Optimize batch processing parameters based on usage
- [ ] Fine-tune rate limiting settings
- [ ] Set up monitoring alerts for new components
- [ ] Train team on new tRPC APIs
- [ ] Plan gradual feature adoption
### Long-term Tasks (First Month)
- [ ] Analyze cost savings and performance improvements
- [ ] Consider additional tRPC endpoint implementations
- [ ] Optimize batch processing schedules
- [ ] Review and adjust security settings
- [ ] Plan next phase improvements
## 🔒 Security Considerations
### New Security Features
- **Enhanced Rate Limiting**: Applied to all authentication endpoints
- **Input Validation**: Comprehensive Zod schemas prevent injection attacks
- **Secure Headers**: HTTPS enforcement in production
- **Token Security**: JWT with proper expiration and rotation
### Security Checklist
- [ ] Verify rate limiting is working correctly
- [ ] Test input validation on all forms
- [ ] Ensure HTTPS is enforced in production
- [ ] Validate JWT token handling
- [ ] Check for proper error message sanitization
- [ ] Verify OpenAI API key is not exposed in logs
## 📈 Expected Improvements
### Performance Improvements
- **50% reduction** in OpenAI API costs through batch processing
- **30% improvement** in API response times with tRPC
- **25% reduction** in database query time with new indexes
- **Enhanced scalability** for processing larger session volumes
### Developer Experience
- **Type Safety**: End-to-end TypeScript types from client to server
- **Better APIs**: Self-documenting tRPC procedures
- **Improved Testing**: More reliable test suite with better validation
- **Enhanced Monitoring**: Detailed health checks and reporting
### Operational Benefits
- **Automated Batch Processing**: Reduced manual intervention
- **Better Error Handling**: Comprehensive retry mechanisms
- **Improved Monitoring**: Real-time health status and metrics
- **Simplified Deployment**: Automated migration and rollback procedures
---
## 📞 Support
For issues during migration:
1. **Check the logs**: `logs/migration.log`
2. **Run health checks**: `pnpm migration:health-check`
3. **Review troubleshooting section** above
4. **Use rollback if needed**: `pnpm migration:rollback`
**Migration completed successfully? 🎉**
Your LiveDash Node application is now running version 2.0.0 with tRPC and Batch API integration!
---
_Migration Guide v2.0.0 - Updated January 2025_

159
README.md
View File

@ -1,6 +1,6 @@
# LiveDash-Node
A comprehensive real-time analytics dashboard for monitoring user sessions with AI-powered analysis, enterprise-grade security features, and advanced processing pipeline.
A real-time analytics dashboard for monitoring user sessions and interactions with interactive data visualizations and detailed metrics.
![Next.js](<https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fraw.githubusercontent.com%2Fkjanat%2Flivedash-node%2Fmaster%2Fpackage.json&search=%22next%22%5Cs*%3A%5Cs*%22%5C%5E(%3F%3Cversion%3E%5Cd%2B%5C.%5Cd*).*%22&replace=%24%3Cversion%3E&logo=nextdotjs&label=Nextjs&color=%23000000>)
![React](<https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fraw.githubusercontent.com%2Fkjanat%2Flivedash-node%2Fmaster%2Fpackage.json&search=%22react%22%5Cs*%3A%5Cs*%22%5C%5E(%3F%3Cversion%3E%5Cd%2B%5C.%5Cd*).*%22&replace=%24%3Cversion%3E&logo=react&label=React&color=%2361DAFB>)
@ -10,48 +10,28 @@ A comprehensive real-time analytics dashboard for monitoring user sessions with
## Features
### Core Analytics
- **Real-time Session Monitoring**: Track and analyze user sessions as they happen
- **Interactive Visualizations**: Geographic maps, response time distributions, and advanced charts
- **AI-Powered Analysis**: OpenAI integration with 50% cost reduction through batch processing
- **Advanced Analytics**: Detailed metrics and insights about user behavior patterns
- **Session Details**: In-depth analysis of individual user sessions with transcript parsing
### Security & Admin Features
- **Enterprise Security**: Multi-layer security with CSRF protection, CSP, and rate limiting
- **Security Monitoring**: Real-time threat detection and alerting system
- **Audit Logging**: Comprehensive security audit trails with retention management
- **Admin Dashboard**: Advanced administration tools for user and system management
- **Geographic Threat Detection**: IP-based threat analysis and anomaly detection
### Platform Management
- **Multi-tenant Architecture**: Company-based data isolation and management
- **User Management**: Role-based access control with platform admin capabilities
- **Batch Processing**: Optimized AI processing pipeline with automated scheduling
- **Data Export**: CSV/JSON export capabilities for analytics and audit data
- **Interactive Visualizations**: Geographic maps, response time distributions, and more
- **Advanced Analytics**: Detailed metrics and insights about user behavior
- **User Management**: Secure authentication with role-based access control
- **Customizable Dashboard**: Filter and sort data based on your specific needs
- **Session Details**: In-depth analysis of individual user sessions
## Tech Stack
- **Frontend**: React 19, Next.js 15, TailwindCSS 4
- **Backend**: Next.js API Routes, tRPC, Custom Node.js server
- **Database**: PostgreSQL with Prisma ORM and connection pooling
- **Authentication**: NextAuth.js with enhanced security features
- **Security**: CSRF protection, CSP with nonce-based scripts, comprehensive rate limiting
- **AI Processing**: OpenAI API with batch processing for cost optimization
- **Visualization**: D3.js, React Leaflet, Recharts, custom chart components
- **Monitoring**: Real-time security monitoring, audit logging, threat detection
- **Data Processing**: Node-cron schedulers for automated batch processing and AI analysis
- **Backend**: Next.js API Routes, Node.js
- **Database**: Prisma ORM with SQLite (default), compatible with PostgreSQL
- **Authentication**: NextAuth.js
- **Visualization**: Chart.js, D3.js, React Leaflet
- **Data Processing**: Node-cron for scheduled tasks
## Getting Started
### Prerequisites
- Node.js 18+ (LTS version recommended)
- Node.js (LTS version recommended)
- pnpm (recommended package manager)
- PostgreSQL 13+ database
### Installation
@ -68,119 +48,50 @@ cd livedash-node
pnpm install
```
3. Set up environment variables:
3. Set up the database:
```bash
cp .env.example .env.local
# Edit .env.local with your configuration
pnpm run prisma:generate
pnpm run prisma:migrate
pnpm run prisma:seed
```
4. Set up the database:
4. Start the development server:
```bash
pnpm prisma:generate
pnpm prisma:migrate
pnpm prisma:seed
pnpm run dev
```
5. Start the development server:
```bash
pnpm dev
```
6. Open your browser and navigate to <http://localhost:3000>
5. Open your browser and navigate to <http://localhost:3000>
## Environment Setup
Create a `.env.local` file in the root directory with the following variables:
Create a `.env` file in the root directory with the following variables:
```env
# Database Configuration
DATABASE_URL="postgresql://user:password@localhost:5432/livedash"
DATABASE_URL_DIRECT="postgresql://user:password@localhost:5432/livedash"
# Authentication
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-nextauth-secret-key"
# AI Processing (optional - for AI features)
OPENAI_API_KEY="your-openai-api-key"
# Security Configuration
CSRF_SECRET="your-csrf-secret-key"
# Scheduler Configuration (optional)
SCHEDULER_ENABLED="true"
CSV_IMPORT_INTERVAL="*/10 * * * *"
IMPORT_PROCESSING_INTERVAL="*/5 * * * *"
SESSION_PROCESSING_INTERVAL="*/2 * * * *"
BATCH_PROCESSING_INTERVAL="*/1 * * * *"
# Batch Processing (optional)
BATCH_PROCESSING_ENABLED="true"
BATCH_CREATE_INTERVAL="*/5 * * * *"
BATCH_STATUS_CHECK_INTERVAL="*/2 * * * *"
BATCH_RESULT_PROCESSING_INTERVAL="*/1 * * * *"
DATABASE_URL="file:./dev.db"
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-here
```
## Project Structure
- `app/`: Next.js App Router pages and API routes
- `api/`: API endpoints including admin, security, and tRPC routes
- `dashboard/`: Main analytics dashboard pages
- `platform/`: Platform administration interface
- `app/`: Next.js App Router components and pages
- `components/`: Reusable React components
- `admin/`: Administrative dashboard components
- `security/`: Security monitoring UI components
- `forms/`: CSRF-protected forms and form utilities
- `providers/`: Context providers (CSRF, tRPC, themes)
- `lib/`: Core utilities and business logic
- Security modules (CSRF, CSP, rate limiting, audit logging)
- Processing pipelines (batch processing, AI analysis)
- Database utilities and authentication
- `server/`: tRPC server configuration and routers
- `prisma/`: Database schema, migrations, and seed scripts
- `tests/`: Comprehensive test suite (unit, integration, E2E)
- `docs/`: Detailed project documentation
- `scripts/`: Migration and utility scripts
- `lib/`: Utility functions and shared code
- `pages/`: API routes and server-side code
- `prisma/`: Database schema and migrations
- `public/`: Static assets
- `docs/`: Project documentation
## Available Scripts
### Development
- `pnpm dev`: Start development server with all features
- `pnpm dev:next-only`: Start Next.js only (no background schedulers)
- `pnpm build`: Build the application for production
- `pnpm start`: Run the production build
### Code Quality
- `pnpm lint`: Run ESLint
- `pnpm lint:fix`: Fix ESLint issues automatically
- `pnpm format`: Format code with Prettier
- `pnpm format:check`: Check code formatting
### Database
- `pnpm prisma:studio`: Open Prisma Studio to view database
- `pnpm prisma:migrate`: Run database migrations
- `pnpm prisma:generate`: Generate Prisma client
- `pnpm prisma:seed`: Seed database with test data
### Testing
- `pnpm test`: Run all tests (Vitest + Playwright)
- `pnpm test:vitest`: Run unit and integration tests
- `pnpm test:coverage`: Run tests with coverage reports
- `pnpm test:security`: Run security-specific tests
- `pnpm test:csp`: Test CSP implementation
### Security & Migration
- `pnpm migration:backup`: Create database backup
- `pnpm migration:health-check`: Run system health checks
- `pnpm test:security-headers`: Test HTTP security headers
- `pnpm run dev`: Start the development server
- `pnpm run build`: Build the application for production
- `pnpm run start`: Run the production build
- `pnpm run lint`: Run ESLint
- `pnpm run format`: Format code with Prettier
- `pnpm run prisma:studio`: Open Prisma Studio to view database
## Contributing

27
TODO
View File

@ -3,7 +3,6 @@
## 🚀 CRITICAL PRIORITY - Architectural Refactoring
### Phase 1: Service Decomposition & Platform Management (Weeks 1-4)
- [x] **Create Platform Management Layer** (80% Complete)
- [x] Add Organization/PlatformUser models to Prisma schema
- [x] Implement super-admin authentication system (/platform/login)
@ -32,7 +31,6 @@
- [ ] Add proper error handling and validation
### Phase 2: AI Service Separation & Compliance (Weeks 5-8)
- [ ] **Extract AI Processing Service**
- [ ] Separate lib/processingScheduler.ts into standalone service
- [ ] Implement async AI processing with queues
@ -48,7 +46,6 @@
- [ ] Implement data export/deletion workflows (Right to be Forgotten)
### Phase 3: Performance & Monitoring (Weeks 9-12)
- [ ] **Monitoring & Observability**
- [ ] Add distributed tracing across services (Jaeger/Zipkin)
- [ ] Implement health checks for all services
@ -65,7 +62,6 @@
## High Priority
### PR #20 Feedback Actions (Code Review)
- [ ] **Fix Environment Variable Testing**
- [ ] Replace process.env access with proper environment mocking in tests
- [ ] Update existing tests to avoid direct environment variable dependencies
@ -97,21 +93,18 @@
- [ ] Test platform login flow end-to-end after fixes
### Testing & Quality Assurance
- [ ] Add comprehensive test coverage for API endpoints (currently minimal)
- [ ] Implement integration tests for the data processing pipeline
- [ ] Add unit tests for validation schemas and authentication logic
- [ ] Create E2E tests for critical user flows (registration, login, dashboard)
### Error Handling & Monitoring
- [ ] Implement global error boundaries for React components
- [ ] Add structured logging with correlation IDs for request tracing
- [ ] Set up error monitoring and alerting (e.g., Sentry integration)
- [ ] Add proper error pages for 404, 500, and other HTTP status codes
### Performance Optimization
- [ ] Implement database query optimization and indexing strategy
- [ ] Add caching layer for frequently accessed data (Redis/in-memory)
- [ ] Optimize React components with proper memoization
@ -120,7 +113,6 @@
## Medium Priority
### Security Enhancements
- [ ] Add CSRF protection for state-changing operations
- [ ] Implement session timeout and refresh token mechanism
- [ ] Add API rate limiting with Redis-backed storage (replace in-memory)
@ -128,21 +120,18 @@
- [ ] Add audit logging for sensitive operations
### Code Quality & Maintenance
- [ ] Resolve remaining ESLint warnings and type issues
- [ ] Standardize chart library usage (currently mixing Chart.js and other libraries)
- [ ] Add proper TypeScript strict mode configuration
- [ ] Implement consistent API response formats across all endpoints
### Database & Schema
- [ ] Add database connection pooling configuration
- [ ] Implement proper database migrations for production deployment
- [ ] Add data retention policies for session data
- [ ] Consider database partitioning for large-scale data
### User Experience
- [ ] Add loading states and skeleton components throughout the application
- [ ] Implement proper form validation feedback and error messages
- [ ] Add pagination for large data sets in dashboard tables
@ -151,35 +140,30 @@
## Low Priority
### Documentation & Development
- [ ] Add API documentation (OpenAPI/Swagger)
- [ ] Create deployment guides for different environments
- [ ] Add contributing guidelines and code review checklist
- [ ] Implement development environment setup automation
### Feature Enhancements
- [ ] Add data export functionality (CSV, PDF reports)
- [ ] Implement dashboard customization and user preferences
- [ ] Add multi-language support (i18n)
- [ ] Create admin panel for system configuration
### Infrastructure & DevOps
- [ ] Add Docker configuration for containerized deployment
- [ ] Implement CI/CD pipeline with automated testing
- [ ] Add environment-specific configuration management
- [ ] Set up monitoring and health check endpoints
### Analytics & Insights
- [ ] Add more detailed analytics and reporting features
- [ ] Implement A/B testing framework for UI improvements
- [ ] Add user behavior tracking and analytics
- [ ] Create automated report generation and scheduling
## Completed ✅
- [x] Fix duplicate MetricCard components
- [x] Add input validation schema with Zod
- [x] Strengthen password requirements (12+ chars, complexity)
@ -192,8 +176,7 @@
## 📊 Test Coverage Status (< 30% Overall)
### ✅ Features WITH Tests
### ✅ Features WITH Tests:
- User Authentication (regular users)
- User Management UI & API
- Basic database connectivity
@ -207,8 +190,7 @@
- Platform dashboard (NEW)
- Platform API endpoints (NEW)
### ❌ Features WITHOUT Tests (Critical Gaps)
### ❌ Features WITHOUT Tests (Critical Gaps):
- **Data Processing Pipeline** (0 tests)
- CSV import scheduler
- Import processor
@ -232,7 +214,6 @@
## 🏛️ Architectural Decisions & Rationale
### Service Technology Choices
- **Dashboard Service**: Next.js + tRPC (existing, proven stack)
- **Data Ingestion Service**: Golang (high-performance CSV processing, concurrency)
- **AI Processing Service**: Node.js/Python (existing AI integrations, async processing)
@ -240,7 +221,6 @@
- **Database**: PostgreSQL (existing, excellent for multi-tenancy)
### Why Golang for Data Ingestion?
- **Performance**: 10-100x faster CSV processing than Node.js
- **Concurrency**: Native goroutines for parallel transcript fetching
- **Memory Efficiency**: Lower memory footprint for large CSV files
@ -248,21 +228,18 @@
- **Team Growth**: Easy to hire Golang developers for data processing
### Migration Strategy
1. **Keep existing working system** while building new services
2. **Feature flagging** to gradually migrate companies to new processing
3. **Dual-write approach** during transition period
4. **Zero-downtime migration** with careful rollback plans
### Compliance Benefits
- **Data Isolation**: Each service has limited database access
- **Audit Trail**: All inter-service communication logged
- **Data Retention**: Automated per-company data lifecycle
- **Security Boundaries**: DMZ for ingestion, private network for processing
## Notes
- **CRITICAL**: Architectural refactoring must be priority #1 for scalability
- **Platform Management**: Notso AI needs self-service customer onboarding
- **Compliance First**: GDPR/ISO 27001 requirements drive service boundaries

View File

@ -1,222 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import {
AuditLogRetentionManager,
DEFAULT_RETENTION_POLICIES,
executeScheduledRetention,
} from "../../../../../lib/auditLogRetention";
import { auditLogScheduler } from "../../../../../lib/auditLogScheduler";
import { authOptions } from "../../../../../lib/auth";
import { extractClientIP } from "../../../../../lib/rateLimiter";
import {
AuditOutcome,
createAuditMetadata,
securityAuditLogger,
} from "../../../../../lib/securityAuditLogger";
// GET /api/admin/audit-logs/retention - Get retention statistics and policy status
export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions);
try {
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
if (!session?.user) {
await securityAuditLogger.logAuthorization(
"audit_retention_unauthorized_access",
AuditOutcome.BLOCKED,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "no_session",
}),
},
"Unauthorized attempt to access audit retention management"
);
return NextResponse.json(
{ success: false, error: "Unauthorized" },
{ status: 401 }
);
}
// Only allow ADMIN users to manage audit log retention
if (session.user.role !== "ADMIN") {
await securityAuditLogger.logAuthorization(
"audit_retention_insufficient_permissions",
AuditOutcome.BLOCKED,
{
userId: session.user.id,
companyId: session.user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
userRole: session.user.role,
requiredRole: "ADMIN",
}),
},
"Insufficient permissions to access audit retention management"
);
return NextResponse.json(
{ success: false, error: "Insufficient permissions" },
{ status: 403 }
);
}
const manager = new AuditLogRetentionManager();
// Get retention statistics and policy information
const [statistics, policyValidation, schedulerStatus] = await Promise.all([
manager.getRetentionStatistics(),
manager.validateRetentionPolicies(),
Promise.resolve(auditLogScheduler.getStatus()),
]);
// Log successful retention info access
await securityAuditLogger.logDataPrivacy(
"audit_retention_info_accessed",
AuditOutcome.SUCCESS,
{
userId: session.user.id,
companyId: session.user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
totalLogs: statistics.totalLogs,
schedulerRunning: schedulerStatus.isRunning,
}),
},
"Audit retention information accessed by admin"
);
return NextResponse.json({
success: true,
data: {
statistics,
policies: DEFAULT_RETENTION_POLICIES,
policyValidation,
scheduler: schedulerStatus,
},
});
} catch (error) {
console.error("Error fetching audit retention info:", error);
await securityAuditLogger.logDataPrivacy(
"audit_retention_info_error",
AuditOutcome.FAILURE,
{
userId: session?.user?.id,
companyId: session?.user?.companyId,
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error while fetching audit retention info: ${error}`
);
return NextResponse.json(
{ success: false, error: "Internal server error" },
{ status: 500 }
);
}
}
// POST /api/admin/audit-logs/retention - Execute retention policies manually
export async function POST(request: NextRequest) {
const session = await getServerSession(authOptions);
try {
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
if (!session?.user || session.user.role !== "ADMIN") {
await securityAuditLogger.logAuthorization(
"audit_retention_execute_unauthorized",
AuditOutcome.BLOCKED,
{
userId: session?.user?.id,
companyId: session?.user?.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "insufficient_permissions",
}),
},
"Unauthorized attempt to execute audit retention"
);
return NextResponse.json(
{ success: false, error: "Unauthorized" },
{ status: 401 }
);
}
const body = await request.json();
const { action, isDryRun = true } = body;
if (action !== "execute") {
return NextResponse.json(
{ success: false, error: "Invalid action. Use 'execute'" },
{ status: 400 }
);
}
// Log retention execution attempt
await securityAuditLogger.logDataPrivacy(
"audit_retention_manual_execution",
AuditOutcome.SUCCESS,
{
userId: session.user.id,
companyId: session.user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
isDryRun,
triggerType: "manual_admin",
}),
},
`Admin manually triggered audit retention (dry run: ${isDryRun})`
);
// Execute retention policies
const results = await executeScheduledRetention(isDryRun);
return NextResponse.json({
success: true,
data: {
message: isDryRun
? "Dry run completed successfully"
: "Retention policies executed successfully",
isDryRun,
results,
},
});
} catch (error) {
console.error("Error executing audit retention:", error);
await securityAuditLogger.logDataPrivacy(
"audit_retention_execution_error",
AuditOutcome.FAILURE,
{
userId: session?.user?.id,
companyId: session?.user?.companyId,
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error while executing audit retention: ${error}`
);
return NextResponse.json(
{ success: false, error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@ -1,244 +0,0 @@
import type { Prisma } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import { extractClientIP } from "../../../../lib/rateLimiter";
import {
AuditOutcome,
type AuditSeverity,
createAuditMetadata,
type SecurityEventType,
securityAuditLogger,
} from "../../../../lib/securityAuditLogger";
/**
* Validates user authorization for audit logs access
*/
async function validateAuditLogAccess(
session: { user?: { id?: string; companyId?: string; role?: string } } | null,
ip: string,
userAgent?: string
) {
if (!session?.user) {
await securityAuditLogger.logAuthorization(
"audit_logs_unauthorized_access",
AuditOutcome.BLOCKED,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "no_session",
}),
},
"Unauthorized attempt to access audit logs"
);
return { valid: false, status: 401, error: "Unauthorized" };
}
if (session?.user?.role !== "ADMIN") {
await securityAuditLogger.logAuthorization(
"audit_logs_insufficient_permissions",
AuditOutcome.BLOCKED,
{
userId: session?.user?.id,
companyId: session?.user?.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
userRole: session?.user?.role,
requiredRole: "ADMIN",
}),
},
"Insufficient permissions to access audit logs"
);
return { valid: false, status: 403, error: "Insufficient permissions" };
}
return { valid: true };
}
/**
* Parses query parameters for audit log filtering
*/
function parseAuditLogFilters(url: URL) {
const page = Number.parseInt(url.searchParams.get("page") || "1");
const limit = Math.min(
Number.parseInt(url.searchParams.get("limit") || "50"),
100
);
const eventType = url.searchParams.get("eventType");
const outcome = url.searchParams.get("outcome");
const severity = url.searchParams.get("severity");
const userId = url.searchParams.get("userId");
const startDate = url.searchParams.get("startDate");
const endDate = url.searchParams.get("endDate");
return {
page,
limit,
eventType,
outcome,
severity,
userId,
startDate,
endDate,
};
}
/**
* Builds where clause for audit log filtering
*/
function buildAuditLogWhereClause(
companyId: string,
filters: ReturnType<typeof parseAuditLogFilters>
): Prisma.SecurityAuditLogWhereInput {
const { eventType, outcome, severity, userId, startDate, endDate } = filters;
const where: Prisma.SecurityAuditLogWhereInput = {
companyId, // Only show logs for user's company
};
if (eventType) where.eventType = eventType as SecurityEventType;
if (outcome) where.outcome = outcome as AuditOutcome;
if (severity) where.severity = severity as AuditSeverity;
if (userId) where.userId = userId;
if (startDate || endDate) {
where.timestamp = {};
if (startDate) where.timestamp.gte = new Date(startDate);
if (endDate) where.timestamp.lte = new Date(endDate);
}
return where;
}
export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions);
try {
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
// Validate access authorization
const authResult = await validateAuditLogAccess(session, ip, userAgent);
if (!authResult.valid) {
return NextResponse.json(
{ success: false, error: authResult.error },
{ status: authResult.status }
);
}
const url = new URL(request.url);
const filters = parseAuditLogFilters(url);
const {
page,
limit,
eventType,
outcome,
severity,
userId,
startDate,
endDate,
} = filters;
const skip = (page - 1) * limit;
// Build filter conditions
const where = buildAuditLogWhereClause(
session?.user?.companyId || "",
filters
);
// Get audit logs with pagination
const [auditLogs, totalCount] = await Promise.all([
prisma.securityAuditLog.findMany({
where,
skip,
take: limit,
orderBy: { timestamp: "desc" },
include: {
user: {
select: {
id: true,
email: true,
name: true,
role: true,
},
},
platformUser: {
select: {
id: true,
email: true,
name: true,
role: true,
},
},
},
}),
prisma.securityAuditLog.count({ where }),
]);
// Log successful audit log access
await securityAuditLogger.logDataPrivacy(
"audit_logs_accessed",
AuditOutcome.SUCCESS,
{
userId: session?.user?.id,
companyId: session?.user?.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
page,
limit,
filters: {
eventType,
outcome,
severity,
userId,
startDate,
endDate,
},
recordsReturned: auditLogs.length,
}),
},
"Audit logs accessed by admin user"
);
return NextResponse.json({
success: true,
data: {
auditLogs,
pagination: {
page,
limit,
totalCount,
totalPages: Math.ceil(totalCount / limit),
hasNext: skip + limit < totalCount,
hasPrev: page > 1,
},
},
});
} catch (error) {
console.error("Error fetching audit logs:", error);
await securityAuditLogger.logDataPrivacy(
"audit_logs_server_error",
AuditOutcome.FAILURE,
{
userId: session?.user?.id,
companyId: session?.user?.companyId,
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error while fetching audit logs: ${error}`
);
return NextResponse.json(
{ success: false, error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@ -1,246 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import {
type BatchOperation,
batchLogger,
logBatchMetrics,
} from "@/lib/batchLogger";
import { getCircuitBreakerStatus } from "@/lib/batchProcessor";
import { getBatchSchedulerStatus } from "@/lib/batchProcessorIntegration";
// Helper function for proper CSV escaping
function escapeCSVField(field: string | number | boolean): string {
if (typeof field === "number" || typeof field === "boolean") {
return String(field);
}
const strField = String(field);
// If field contains comma, quote, or newline, wrap in quotes and escape internal quotes
if (
strField.includes(",") ||
strField.includes('"') ||
strField.includes("\n")
) {
return `"${strField.replace(/"/g, '""')}"`;
}
return strField;
}
/**
* GET /api/admin/batch-monitoring
* Get comprehensive batch processing monitoring data
*/
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user || session.user.role !== "ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const url = new URL(request.url);
const companyId = url.searchParams.get("companyId");
const operationParam = url.searchParams.get("operation");
const format = url.searchParams.get("format") || "json";
// Validate operation parameter
const isValidBatchOperation = (
value: string | null
): value is BatchOperation => {
return (
value !== null &&
Object.values(BatchOperation).includes(value as BatchOperation)
);
};
if (operationParam && !isValidBatchOperation(operationParam)) {
return NextResponse.json(
{
error: "Invalid operation parameter",
validOperations: Object.values(BatchOperation),
},
{ status: 400 }
);
}
const operation = operationParam as BatchOperation | null;
// Get batch processing metrics
const metrics = batchLogger.getMetrics(companyId || undefined);
// Get scheduler status
const schedulerStatus = getBatchSchedulerStatus();
// Get circuit breaker status
const circuitBreakerStatus = getCircuitBreakerStatus();
// Generate performance metrics for specific operation if requested
if (operation) {
await logBatchMetrics(operation);
}
const monitoringData = {
timestamp: new Date().toISOString(),
metrics,
schedulerStatus,
circuitBreakerStatus,
systemHealth: {
schedulerRunning: schedulerStatus.isRunning,
circuitBreakersOpen: Object.values(circuitBreakerStatus).some(
(cb) => cb.isOpen
),
pausedDueToErrors: schedulerStatus.isPaused,
consecutiveErrors: schedulerStatus.consecutiveErrors,
},
};
if (
format === "csv" &&
typeof metrics === "object" &&
!Array.isArray(metrics)
) {
// Convert metrics to CSV format
const headers = [
"company_id",
"operation_start_time",
"request_count",
"success_count",
"failure_count",
"retry_count",
"total_cost",
"average_latency",
"circuit_breaker_trips",
].join(",");
const rows = Object.entries(metrics).map(([companyId, metric]) =>
[
escapeCSVField(companyId),
escapeCSVField(new Date(metric.operationStartTime).toISOString()),
escapeCSVField(metric.requestCount),
escapeCSVField(metric.successCount),
escapeCSVField(metric.failureCount),
escapeCSVField(metric.retryCount),
escapeCSVField(metric.totalCost.toFixed(4)),
escapeCSVField(metric.averageLatency.toFixed(2)),
escapeCSVField(metric.circuitBreakerTrips),
].join(",")
);
return new NextResponse([headers, ...rows].join("\n"), {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="batch-monitoring-${Date.now()}.csv"`,
},
});
}
return NextResponse.json(monitoringData);
} catch (error) {
console.error("Batch monitoring API error:", error);
return NextResponse.json(
{ error: "Failed to fetch batch monitoring data" },
{ status: 500 }
);
}
}
/**
* POST /api/admin/batch-monitoring/export
* Export batch processing logs
*/
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user || session.user.role !== "ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { startDate, endDate, format = "json" } = body;
if (!startDate || !endDate) {
return NextResponse.json(
{ error: "Start date and end date are required" },
{ status: 400 }
);
}
const timeRange = {
start: new Date(startDate),
end: new Date(endDate),
};
const exportDataJson = batchLogger.exportLogs(timeRange);
if (format === "csv") {
// Convert JSON to CSV format
const data = JSON.parse(exportDataJson);
// Flatten the data structure for CSV
const csvRows: string[] = [];
// Add headers
csvRows.push(
"Metric,Company ID,Operation,Batch ID,Request Count,Success Count,Failure Count,Average Latency,Last Updated"
);
// Add metrics data
if (data.metrics) {
interface MetricData {
companyId?: string;
operation?: string;
batchId?: string;
requestCount?: number;
successCount?: number;
failureCount?: number;
averageLatency?: number;
lastUpdated?: string;
}
Object.entries(data.metrics).forEach(
([key, metric]: [string, MetricData]) => {
csvRows.push(
[
escapeCSVField(key),
escapeCSVField(metric.companyId || ""),
escapeCSVField(metric.operation || ""),
escapeCSVField(metric.batchId || ""),
escapeCSVField(metric.requestCount || 0),
escapeCSVField(metric.successCount || 0),
escapeCSVField(metric.failureCount || 0),
escapeCSVField(metric.averageLatency || 0),
escapeCSVField(metric.lastUpdated || ""),
].join(",")
);
}
);
}
const csvContent = csvRows.join("\n");
return new NextResponse(csvContent, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="batch-logs-${startDate}-${endDate}.csv"`,
},
});
}
return new NextResponse(exportDataJson, {
headers: {
"Content-Type": "application/json",
"Content-Disposition": `attachment; filename="batch-logs-${startDate}-${endDate}.json"`,
},
});
} catch (error) {
console.error("Batch log export error:", error);
return NextResponse.json(
{ error: "Failed to export batch logs" },
{ status: 500 }
);
}
}

View File

@ -1,230 +0,0 @@
/**
* Cache Invalidation API Endpoint
*
* Allows administrators to manually invalidate cache entries or patterns
* for troubleshooting and cache management.
*/
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { z } from "zod";
import { authOptions } from "../../../../../lib/auth";
import { invalidateCompanyCache } from "../../../../../lib/batchProcessorOptimized";
import { Cache } from "../../../../../lib/cache";
import {
AuditOutcome,
AuditSeverity,
createAuditMetadata,
SecurityEventType,
} from "../../../../../lib/securityAuditLogger";
import { enhancedSecurityLog } from "../../../../../lib/securityMonitoring";
const invalidationSchema = z.object({
type: z.enum(["key", "pattern", "company", "user", "all"]),
value: z.string().optional(),
});
async function validateCacheAccess(
session: { user?: { id?: string; companyId?: string; role?: string } } | null
) {
if (!session?.user) {
await enhancedSecurityLog(
SecurityEventType.AUTHORIZATION,
"cache_invalidation_access_denied",
AuditOutcome.BLOCKED,
{
metadata: createAuditMetadata({
endpoint: "/api/admin/cache/invalidate",
reason: "not_authenticated",
}),
},
AuditSeverity.MEDIUM,
"Unauthenticated access attempt to cache invalidation endpoint"
);
return { valid: false, status: 401, error: "Authentication required" };
}
if (session.user.role !== "ADMIN") {
await enhancedSecurityLog(
SecurityEventType.AUTHORIZATION,
"cache_invalidation_access_denied",
AuditOutcome.BLOCKED,
{
userId: session.user.id,
companyId: session.user.companyId,
metadata: createAuditMetadata({
endpoint: "/api/admin/cache/invalidate",
userRole: session.user.role,
reason: "insufficient_privileges",
}),
},
AuditSeverity.HIGH,
"Non-admin user attempted to access cache invalidation"
);
return { valid: false, status: 403, error: "Admin access required" };
}
return { valid: true };
}
async function performCacheInvalidation(type: string, value?: string) {
let deletedCount = 0;
let operation = "";
switch (type) {
case "key": {
if (!value) {
return {
error: "Key value required for key invalidation",
status: 400,
};
}
const deleted = await Cache.delete(value);
deletedCount = deleted ? 1 : 0;
operation = `key: ${value}`;
break;
}
case "pattern": {
if (!value) {
return {
error: "Pattern value required for pattern invalidation",
status: 400,
};
}
deletedCount = await Cache.invalidatePattern(value);
operation = `pattern: ${value}`;
break;
}
case "company": {
if (!value) {
return {
error: "Company ID required for company invalidation",
status: 400,
};
}
deletedCount = await Cache.invalidateCompany(value);
await invalidateCompanyCache();
operation = `company: ${value}`;
break;
}
case "user": {
if (!value) {
return { error: "User ID required for user invalidation", status: 400 };
}
await Cache.invalidateUser(value);
await Cache.invalidatePattern("user:email:*");
deletedCount = 1;
operation = `user: ${value}`;
break;
}
case "all": {
await Promise.all([
Cache.invalidatePattern("user:*"),
Cache.invalidatePattern("company:*"),
Cache.invalidatePattern("session:*"),
Cache.invalidatePattern("*"),
invalidateCompanyCache(),
]);
deletedCount = 1;
operation = "all caches";
break;
}
default:
return { error: "Invalid invalidation type", status: 400 };
}
return { success: true, deletedCount, operation };
}
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
const authResult = await validateCacheAccess(session);
if (!authResult.valid) {
return NextResponse.json(
{ success: false, error: authResult.error },
{ status: authResult.status }
);
}
const body = await request.json();
const validation = invalidationSchema.safeParse(body);
if (!validation.success) {
return NextResponse.json(
{
success: false,
error: "Invalid request format",
details: validation.error.issues,
},
{ status: 400 }
);
}
const { type, value } = validation.data;
const result = await performCacheInvalidation(type, value);
if (!result.success) {
return NextResponse.json(
{ success: false, error: result.error },
{ status: result.status }
);
}
const response = {
success: true,
data: {
type,
value,
deletedCount: result.deletedCount,
operation: result.operation,
timestamp: new Date().toISOString(),
},
};
await enhancedSecurityLog(
SecurityEventType.PLATFORM_ADMIN,
"cache_invalidation_executed",
AuditOutcome.SUCCESS,
{
userId: session?.user?.id,
companyId: session?.user?.companyId,
metadata: createAuditMetadata({
endpoint: "/api/admin/cache/invalidate",
invalidationType: type,
invalidationValue: value,
deletedCount: result.deletedCount,
}),
},
AuditSeverity.MEDIUM,
`Cache invalidation executed: ${result.operation}`
);
return NextResponse.json(response);
} catch (error) {
console.error("[Cache Invalidation API] Error:", error);
await enhancedSecurityLog(
SecurityEventType.API_SECURITY,
"cache_invalidation_error",
AuditOutcome.FAILURE,
{
metadata: createAuditMetadata({
endpoint: "/api/admin/cache/invalidate",
error: error instanceof Error ? error.message : "Unknown error",
}),
},
AuditSeverity.HIGH,
"Cache invalidation API encountered an error"
);
return NextResponse.json(
{
success: false,
error: "Internal server error",
},
{ status: 500 }
);
}
}

View File

@ -1,157 +0,0 @@
/**
* Cache Statistics API Endpoint
*
* Provides comprehensive cache performance metrics and health status
* for monitoring Redis + in-memory cache performance.
*/
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../../lib/auth";
import { Cache } from "../../../../../lib/cache";
import {
AuditOutcome,
AuditSeverity,
createAuditMetadata,
SecurityEventType,
} from "../../../../../lib/securityAuditLogger";
import { enhancedSecurityLog } from "../../../../../lib/securityMonitoring";
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
await enhancedSecurityLog(
SecurityEventType.AUTHORIZATION,
"cache_stats_access_denied",
AuditOutcome.BLOCKED,
{
metadata: createAuditMetadata({
endpoint: "/api/admin/cache/stats",
reason: "not_authenticated",
}),
},
AuditSeverity.MEDIUM,
"Unauthenticated access attempt to cache stats endpoint"
);
return NextResponse.json(
{ success: false, error: "Authentication required" },
{ status: 401 }
);
}
if (session.user.role !== "ADMIN") {
await enhancedSecurityLog(
SecurityEventType.AUTHORIZATION,
"cache_stats_access_denied",
AuditOutcome.BLOCKED,
{
userId: session.user.id,
companyId: session.user.companyId,
metadata: createAuditMetadata({
endpoint: "/api/admin/cache/stats",
userRole: session.user.role,
reason: "insufficient_privileges",
}),
},
AuditSeverity.HIGH,
"Non-admin user attempted to access cache stats"
);
return NextResponse.json(
{ success: false, error: "Admin access required" },
{ status: 403 }
);
}
// Get cache statistics and health information
const [stats, healthCheck] = await Promise.all([
Cache.getStats(),
Cache.healthCheck(),
]);
const response = {
success: true,
data: {
performance: {
hits: stats.hits,
misses: stats.misses,
sets: stats.sets,
deletes: stats.deletes,
errors: stats.errors,
hitRate: Number((stats.hitRate * 100).toFixed(2)), // Convert to percentage
redisHits: stats.redisHits,
memoryHits: stats.memoryHits,
},
health: {
redis: {
connected: healthCheck.redis.connected,
latency: healthCheck.redis.latency,
error: healthCheck.redis.error,
},
memory: {
available: healthCheck.memory.available,
size: healthCheck.memory.size,
valid: healthCheck.memory.valid,
expired: healthCheck.memory.expired,
},
overall: {
available: healthCheck.overall.available,
fallbackMode: healthCheck.overall.fallbackMode,
},
},
configuration: {
redisAvailable: stats.redisAvailable,
fallbackActive: !stats.redisAvailable,
},
timestamp: new Date().toISOString(),
},
};
// Log successful access
await enhancedSecurityLog(
SecurityEventType.PLATFORM_ADMIN,
"cache_stats_accessed",
AuditOutcome.SUCCESS,
{
userId: session.user.id,
companyId: session.user.companyId,
metadata: createAuditMetadata({
endpoint: "/api/admin/cache/stats",
hitRate: response.data.performance.hitRate,
redisConnected: response.data.health.redis.connected,
}),
},
AuditSeverity.INFO,
"Cache statistics accessed by admin"
);
return NextResponse.json(response);
} catch (error) {
console.error("[Cache Stats API] Error:", error);
await enhancedSecurityLog(
SecurityEventType.API_SECURITY,
"cache_stats_error",
AuditOutcome.FAILURE,
{
metadata: createAuditMetadata({
endpoint: "/api/admin/cache/stats",
error: error instanceof Error ? error.message : "Unknown error",
}),
},
AuditSeverity.HIGH,
"Cache stats API encountered an error"
);
return NextResponse.json(
{
success: false,
error: "Internal server error",
},
{ status: 500 }
);
}
}

View File

@ -33,8 +33,14 @@ export async function GET(request: NextRequest) {
prisma.session.count(),
// Count processing status records
prisma.sessionProcessingStatus.count(),
// Count total AI requests
prisma.aIProcessingRequest.count(),
// Count recent AI requests
prisma.aIProcessingRequest.count({
where: {
createdAt: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24 hours
},
},
}),
]);
const [sessionsResult, statusResult, aiRequestsResult] = metrics;

View File

@ -1,717 +0,0 @@
/**
* Performance Dashboard API
*
* Provides real-time performance metrics, bottleneck detection,
* and optimization recommendations for system monitoring.
*/
import { NextResponse } from "next/server";
import { withErrorHandling } from "@/lib/api/errors";
import { createAPIHandler, UserRole } from "@/lib/api/handler";
import { cacheManager } from "@/lib/performance/cache";
import { deduplicationManager } from "@/lib/performance/deduplication";
import {
PerformanceUtils,
performanceMonitor,
} from "@/lib/performance/monitor";
/**
* GET /api/admin/performance
* Get comprehensive performance metrics and recommendations
*/
export const GET = withErrorHandling(
createAPIHandler(
async (context) => {
const url = new URL(context.request.url);
const type = url.searchParams.get("type") || "summary";
const limit = Math.min(
100,
Number.parseInt(url.searchParams.get("limit") || "50", 10)
);
switch (type) {
case "summary":
return await getPerformanceSummary();
case "history":
return await getPerformanceHistory(limit);
case "cache":
return await getCacheMetrics();
case "deduplication":
return await getDeduplicationMetrics();
case "recommendations":
return await getOptimizationRecommendations();
case "bottlenecks":
return await getBottleneckAnalysis();
default:
return await getPerformanceSummary();
}
},
{
requireAuth: true,
requiredRole: [UserRole.PLATFORM_ADMIN],
auditLog: true,
}
)
);
/**
* POST /api/admin/performance/action
* Execute performance optimization actions
*/
export const POST = withErrorHandling(
createAPIHandler(
async (context, validatedData) => {
const { action, target, options } =
validatedData || (await context.request.json());
switch (action) {
case "clear_cache":
return await clearCache(target);
case "start_monitoring":
return await startMonitoring(options);
case "stop_monitoring":
return await stopMonitoring();
case "optimize_cache":
return await optimizeCache(target, options);
case "invalidate_pattern":
return await invalidatePattern(target, options);
default:
throw new Error(`Unknown action: ${action}`);
}
},
{
requireAuth: true,
requiredRole: [UserRole.PLATFORM_ADMIN],
auditLog: true,
}
)
);
async function getPerformanceSummary() {
const { result: summary } = await PerformanceUtils.measureAsync(
"performance-summary-generation",
async () => {
const performanceSummary = performanceMonitor.getPerformanceSummary();
const cacheReport = cacheManager.getPerformanceReport();
const deduplicationStats = deduplicationManager.getAllStats();
return {
timestamp: new Date().toISOString(),
system: {
status: getSystemStatus(performanceSummary),
uptime: process.uptime(),
nodeVersion: process.version,
platform: process.platform,
},
performance: {
current: performanceSummary.currentMetrics,
trends: performanceSummary.trends,
score: calculatePerformanceScore(performanceSummary),
},
bottlenecks: performanceSummary.bottlenecks,
recommendations: performanceSummary.recommendations,
caching: {
...cacheReport,
efficiency: calculateCacheEfficiency(cacheReport),
},
deduplication: {
totalDeduplicators: Object.keys(deduplicationStats).length,
overallStats: calculateOverallDeduplicationStats(deduplicationStats),
byCategory: deduplicationStats,
},
};
}
);
return NextResponse.json(summary);
}
async function getPerformanceHistory(limit: number) {
const history = performanceMonitor.getHistory(limit);
// history is already typed as PerformanceMetrics[], no casting needed
return NextResponse.json({
history,
analytics: {
averageMemoryUsage:
history.length > 0
? history.reduce((sum, item) => sum + item.memoryUsage.heapUsed, 0) /
history.length
: 0,
averageResponseTime:
history.length > 0
? history.reduce(
(sum, item) => sum + item.requestMetrics.averageResponseTime,
0
) / history.length
: 0,
memoryTrend: calculateTrend(
history as unknown as Record<string, unknown>[],
"memoryUsage.heapUsed"
),
responseTrend: calculateTrend(
history as unknown as Record<string, unknown>[],
"requestMetrics.averageResponseTime"
),
},
});
}
async function getCacheMetrics() {
const report = cacheManager.getPerformanceReport();
const detailedStats = cacheManager.getAllStats();
return NextResponse.json({
overview: report,
detailed: detailedStats,
insights: {
mostEfficient: findMostEfficientCache(detailedStats),
leastEfficient: findLeastEfficientCache(detailedStats),
memoryDistribution: calculateMemoryDistribution(detailedStats),
},
});
}
async function getDeduplicationMetrics() {
const allStats = deduplicationManager.getAllStats();
return NextResponse.json({
overview: calculateOverallDeduplicationStats(allStats),
byCategory: allStats,
insights: {
mostEffective: findMostEffectiveDeduplicator(allStats),
optimization: generateDeduplicationOptimizations(allStats),
},
});
}
async function getOptimizationRecommendations() {
const currentMetrics = performanceMonitor.getCurrentMetrics();
const recommendations =
performanceMonitor.generateRecommendations(currentMetrics);
const enhancedRecommendations = recommendations.map((rec) => ({
...rec,
urgency: calculateUrgency(rec),
complexity: estimateComplexity(rec),
timeline: estimateTimeline(rec),
}));
return NextResponse.json({
recommendations: enhancedRecommendations,
quickWins: enhancedRecommendations.filter(
(r) => r.complexity === "low" && r.estimatedImpact > 50
),
highImpact: enhancedRecommendations.filter((r) => r.estimatedImpact > 70),
});
}
async function getBottleneckAnalysis() {
const currentMetrics = performanceMonitor.getCurrentMetrics();
const bottlenecks = performanceMonitor.detectBottlenecks(currentMetrics);
return NextResponse.json({
bottlenecks,
analysis: {
criticalCount: bottlenecks.filter((b) => b.severity === "critical")
.length,
warningCount: bottlenecks.filter((b) => b.severity === "warning").length,
totalImpact: bottlenecks.reduce((sum, b) => sum + b.impact, 0),
prioritizedActions: prioritizeBottleneckActions(bottlenecks),
},
});
}
async function clearCache(target?: string) {
if (target) {
const success = cacheManager.removeCache(target);
return NextResponse.json({
success,
message: success
? `Cache '${target}' cleared`
: `Cache '${target}' not found`,
});
}
cacheManager.clearAll();
return NextResponse.json({
success: true,
message: "All caches cleared",
});
}
async function startMonitoring(options: { interval?: number } = {}) {
const interval = options.interval || 30000;
performanceMonitor.start(interval);
return NextResponse.json({
success: true,
message: `Performance monitoring started with ${interval}ms interval`,
});
}
async function stopMonitoring() {
performanceMonitor.stop();
return NextResponse.json({
success: true,
message: "Performance monitoring stopped",
});
}
async function optimizeCache(
target: string,
_options: Record<string, unknown> = {}
) {
try {
const optimizationResults: string[] = [];
switch (target) {
case "memory": {
// Trigger garbage collection and memory cleanup
if (global.gc) {
global.gc();
optimizationResults.push("Forced garbage collection");
}
// Get current memory usage before optimization
const beforeMemory = cacheManager.getTotalMemoryUsage();
optimizationResults.push(
`Memory usage before optimization: ${beforeMemory.toFixed(2)} MB`
);
break;
}
case "lru": {
// Clear all LRU caches to free memory
const beforeClearStats = cacheManager.getAllStats();
const totalCachesBefore = Object.keys(beforeClearStats).length;
cacheManager.clearAll();
optimizationResults.push(`Cleared ${totalCachesBefore} LRU caches`);
break;
}
case "all": {
// Comprehensive cache optimization
if (global.gc) {
global.gc();
optimizationResults.push("Forced garbage collection");
}
const allStats = cacheManager.getAllStats();
const totalCaches = Object.keys(allStats).length;
const memoryBefore = cacheManager.getTotalMemoryUsage();
cacheManager.clearAll();
const memoryAfter = cacheManager.getTotalMemoryUsage();
const memorySaved = memoryBefore - memoryAfter;
optimizationResults.push(
`Cleared ${totalCaches} caches`,
`Memory freed: ${memorySaved.toFixed(2)} MB`
);
break;
}
default:
return NextResponse.json(
{
success: false,
error: `Unknown optimization target: ${target}. Valid targets: memory, lru, all`,
},
{ status: 400 }
);
}
// Get post-optimization metrics
const metrics = cacheManager.getPerformanceReport();
return NextResponse.json({
success: true,
message: `Cache optimization applied to '${target}'`,
optimizations: optimizationResults,
metrics: {
totalMemoryUsage: metrics.totalMemoryUsage,
averageHitRate: metrics.averageHitRate,
totalCaches: metrics.totalCaches,
},
});
} catch (error) {
console.error("Cache optimization failed:", error);
return NextResponse.json(
{
success: false,
error: "Cache optimization failed",
details: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 }
);
}
}
async function invalidatePattern(
target: string,
options: { pattern?: string } = {}
) {
const { pattern } = options;
if (!pattern) {
throw new Error("Pattern is required for invalidation");
}
try {
let invalidatedCount = 0;
const invalidationResults: string[] = [];
switch (target) {
case "all": {
// Clear all caches (pattern-based clearing not available in current implementation)
const allCacheStats = cacheManager.getAllStats();
const allCacheNames = Object.keys(allCacheStats);
cacheManager.clearAll();
invalidatedCount = allCacheNames.length;
invalidationResults.push(
`Cleared all ${invalidatedCount} caches (pattern matching not supported)`
);
break;
}
case "memory": {
// Get memory usage and clear if pattern would match memory operations
const memoryBefore = cacheManager.getTotalMemoryUsage();
cacheManager.clearAll();
const memoryAfter = cacheManager.getTotalMemoryUsage();
invalidatedCount = 1;
invalidationResults.push(
`Cleared memory caches, freed ${(memoryBefore - memoryAfter).toFixed(2)} MB`
);
break;
}
case "lru": {
// Clear all LRU caches
const lruStats = cacheManager.getAllStats();
const lruCacheCount = Object.keys(lruStats).length;
cacheManager.clearAll();
invalidatedCount = lruCacheCount;
invalidationResults.push(`Cleared ${invalidatedCount} LRU caches`);
break;
}
default: {
// Try to remove a specific cache by name
const removed = cacheManager.removeCache(target);
if (!removed) {
return NextResponse.json(
{
success: false,
error: `Cache '${target}' not found. Valid targets: all, memory, lru, or specific cache name`,
},
{ status: 400 }
);
}
invalidatedCount = 1;
invalidationResults.push(`Removed cache '${target}'`);
break;
}
}
// Get post-invalidation metrics
const metrics = cacheManager.getPerformanceReport();
return NextResponse.json({
success: true,
message: `Pattern '${pattern}' invalidated in cache '${target}'`,
invalidated: invalidatedCount,
details: invalidationResults,
metrics: {
totalMemoryUsage: metrics.totalMemoryUsage,
totalCaches: metrics.totalCaches,
averageHitRate: metrics.averageHitRate,
},
});
} catch (error) {
console.error("Pattern invalidation failed:", error);
return NextResponse.json(
{
success: false,
error: "Pattern invalidation failed",
details: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 }
);
}
}
// Helper functions
function getSystemStatus(summary: {
bottlenecks: Array<{ severity: string }>;
}): "healthy" | "warning" | "critical" {
const criticalBottlenecks = summary.bottlenecks.filter(
(b: { severity: string }) => b.severity === "critical"
);
const warningBottlenecks = summary.bottlenecks.filter(
(b: { severity: string }) => b.severity === "warning"
);
if (criticalBottlenecks.length > 0) return "critical";
if (warningBottlenecks.length > 2) return "warning";
return "healthy";
}
function calculatePerformanceScore(summary: {
bottlenecks: Array<{ severity: string }>;
currentMetrics: { memoryUsage: { heapUsed: number } };
}): number {
let score = 100;
// Deduct points for bottlenecks
summary.bottlenecks.forEach((bottleneck: { severity: string }) => {
if (bottleneck.severity === "critical") score -= 25;
else if (bottleneck.severity === "warning") score -= 10;
});
// Factor in memory usage
const memUsage = summary.currentMetrics.memoryUsage.heapUsed;
if (memUsage > 400) score -= 20;
else if (memUsage > 200) score -= 10;
return Math.max(0, score);
}
function calculateCacheEfficiency(report: { averageHitRate: number }): number {
return Math.round(report.averageHitRate * 100);
}
function calculateOverallDeduplicationStats(
stats: Record<
string,
{ hits: number; misses: number; deduplicatedRequests: number }
>
) {
const values = Object.values(stats);
if (values.length === 0) return { hitRate: 0, totalSaved: 0 };
const totalHits = values.reduce(
(sum: number, stat: { hits: number }) => sum + stat.hits,
0
);
const totalRequests = values.reduce(
(sum: number, stat: { hits: number; misses: number }) =>
sum + stat.hits + stat.misses,
0
);
const totalSaved = values.reduce(
(sum: number, stat: { deduplicatedRequests: number }) =>
sum + stat.deduplicatedRequests,
0
);
return {
hitRate: totalRequests > 0 ? totalHits / totalRequests : 0,
totalSaved,
efficiency: totalRequests > 0 ? (totalSaved / totalRequests) * 100 : 0,
};
}
function _calculateAverage(
history: Record<string, unknown>[],
path: string
): number {
if (history.length === 0) return 0;
const values = history
.map((item) => getNestedValue(item, path))
.filter((v) => v !== undefined && typeof v === "number") as number[];
return values.length > 0
? values.reduce((sum, val) => sum + val, 0) / values.length
: 0;
}
function calculateTrend<T extends Record<string, unknown>>(
history: Array<T>,
path: string
): "increasing" | "decreasing" | "stable" {
if (history.length < 2) return "stable";
const recent = history.slice(-5);
const older = history.slice(-10, -5);
if (older.length === 0) return "stable";
const recentAvg =
recent.length > 0
? recent.reduce(
(sum, item) => sum + getNestedPropertyValue(item, path),
0
) / recent.length
: 0;
const olderAvg =
older.length > 0
? older.reduce(
(sum, item) => sum + getNestedPropertyValue(item, path),
0
) / older.length
: 0;
if (recentAvg > olderAvg * 1.1) return "increasing";
if (recentAvg < olderAvg * 0.9) return "decreasing";
return "stable";
}
function getNestedPropertyValue(
obj: Record<string, unknown>,
path: string
): number {
const result = path.split(".").reduce((current, key) => {
if (current && typeof current === "object" && key in current) {
return (current as Record<string, unknown>)[key];
}
return 0;
}, obj as unknown);
return typeof result === "number" ? result : 0;
}
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
return path
.split(".")
.reduce((current, key) => (current as Record<string, unknown>)?.[key], obj);
}
function findMostEfficientCache(stats: Record<string, { hitRate: number }>) {
return Object.entries(stats).reduce(
(best, [name, stat]) =>
stat.hitRate > best.hitRate ? { name, ...stat } : best,
{ name: "", hitRate: -1 }
);
}
function findLeastEfficientCache(stats: Record<string, { hitRate: number }>) {
return Object.entries(stats).reduce(
(worst, [name, stat]) =>
stat.hitRate < worst.hitRate ? { name, ...stat } : worst,
{ name: "", hitRate: 2 }
);
}
function calculateMemoryDistribution(
stats: Record<string, { memoryUsage: number }>
) {
const total = Object.values(stats).reduce(
(sum: number, stat: { memoryUsage: number }) => sum + stat.memoryUsage,
0
);
return Object.entries(stats).map(([name, stat]) => ({
name,
percentage: total > 0 ? (stat.memoryUsage / total) * 100 : 0,
memoryUsage: stat.memoryUsage,
}));
}
function findMostEffectiveDeduplicator(
stats: Record<string, { deduplicationRate: number }>
) {
return Object.entries(stats).reduce(
(best, [name, stat]) =>
stat.deduplicationRate > best.deduplicationRate
? { name, ...stat }
: best,
{ name: "", deduplicationRate: -1 }
);
}
function generateDeduplicationOptimizations(
stats: Record<string, { hitRate: number; deduplicationRate: number }>
) {
const optimizations: string[] = [];
Object.entries(stats).forEach(([name, stat]) => {
if (stat.hitRate < 0.3) {
optimizations.push(`Increase TTL for '${name}' deduplicator`);
}
if (stat.deduplicationRate < 0.1) {
optimizations.push(`Review key generation strategy for '${name}'`);
}
});
return optimizations;
}
function calculateUrgency(rec: {
priority: string;
estimatedImpact: number;
}): "low" | "medium" | "high" {
if (rec.priority === "high" && rec.estimatedImpact > 70) return "high";
if (rec.priority === "medium" || rec.estimatedImpact > 50) return "medium";
return "low";
}
function estimateComplexity(rec: {
category: string;
}): "low" | "medium" | "high" {
if (rec.category === "Caching" || rec.category === "Configuration")
return "low";
if (rec.category === "Performance" || rec.category === "Memory")
return "medium";
return "high";
}
function estimateTimeline(rec: { category: string }): string {
const complexity = estimateComplexity(rec);
switch (complexity) {
case "low":
return "1-2 hours";
case "medium":
return "4-8 hours";
case "high":
return "1-3 days";
default:
return "Unknown";
}
}
function prioritizeBottleneckActions(
bottlenecks: Array<{
severity: string;
impact: number;
recommendations: string[];
description: string;
}>
) {
return bottlenecks
.sort((a, b) => {
// Sort by severity first, then by impact
if (a.severity !== b.severity) {
const severityOrder = { critical: 3, warning: 2, info: 1 };
return (
severityOrder[b.severity as keyof typeof severityOrder] -
severityOrder[a.severity as keyof typeof severityOrder]
);
}
return b.impact - a.impact;
})
.slice(0, 5) // Top 5 actions
.map((bottleneck, index) => ({
priority: index + 1,
action: bottleneck.recommendations[0] || "No specific action available",
bottleneck: bottleneck.description,
estimatedImpact: bottleneck.impact,
}));
}

View File

@ -6,7 +6,29 @@ import { prisma } from "../../../../lib/prisma";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { companyId } = body;
let { companyId } = body;
if (!companyId) {
// Try to get user from prisma based on session cookie
try {
const session = await prisma.session.findFirst({
orderBy: { createdAt: "desc" },
where: {
/* Add session check criteria here */
},
});
if (session) {
companyId = session.companyId;
}
} catch (error) {
// Log error for server-side debugging
const errorMessage =
error instanceof Error ? error.message : String(error);
// Use a server-side logging approach instead of console
process.stderr.write(`Error fetching session: ${errorMessage}\n`);
}
}
if (!companyId) {
return NextResponse.json(

View File

@ -1,61 +0,0 @@
import { NextResponse } from "next/server";
import { getSchedulerIntegration } from "@/lib/services/schedulers/ServerSchedulerIntegration";
/**
* Health check endpoint for schedulers
* Used by load balancers and orchestrators for health monitoring
*/
export async function GET() {
try {
const integration = getSchedulerIntegration();
const health = integration.getHealthStatus();
// Return appropriate HTTP status based on health
const status = health.healthy ? 200 : 503;
return NextResponse.json(
{
healthy: health.healthy,
status: health.healthy ? "healthy" : "unhealthy",
timestamp: new Date().toISOString(),
schedulers: {
total: health.totalSchedulers,
running: health.runningSchedulers,
errors: health.errorSchedulers,
},
details: health.schedulerStatuses,
},
{ status }
);
} catch (error) {
console.error("[Scheduler Health API] Error:", error);
return NextResponse.json(
{
healthy: false,
status: "error",
timestamp: new Date().toISOString(),
error: "Failed to get scheduler health status",
},
{ status: 500 }
);
}
}
/**
* Readiness check endpoint
* Used by Kubernetes and other orchestrators
*/
export async function HEAD() {
try {
const integration = getSchedulerIntegration();
const health = integration.getHealthStatus();
// Return 200 if healthy, 503 if not
const status = health.healthy ? 200 : 503;
return new NextResponse(null, { status });
} catch (_error) {
return new NextResponse(null, { status: 500 });
}
}

View File

@ -1,99 +0,0 @@
import { z } from "zod";
import { createAdminHandler } from "@/lib/api";
import { getSchedulerIntegration } from "@/lib/services/schedulers/ServerSchedulerIntegration";
/**
* Get all schedulers with their status and metrics
* Requires admin authentication
*/
export const GET = createAdminHandler(async (_context) => {
const integration = getSchedulerIntegration();
const schedulers = integration.getSchedulersList();
const health = integration.getHealthStatus();
return {
success: true,
data: {
health,
schedulers,
timestamp: new Date().toISOString(),
},
};
});
const PostInputSchema = z
.object({
action: z.enum(["start", "stop", "trigger", "startAll", "stopAll"]),
schedulerId: z.string().optional(),
})
.refine(
(data) => {
// schedulerId is required for individual scheduler actions
const actionsRequiringSchedulerId = ["start", "stop", "trigger"];
if (actionsRequiringSchedulerId.includes(data.action)) {
return data.schedulerId !== undefined && data.schedulerId.length > 0;
}
return true;
},
{
message: "schedulerId is required for start, stop, and trigger actions",
path: ["schedulerId"],
}
);
/**
* Control scheduler operations (start/stop/trigger)
* Requires admin authentication
*/
export const POST = createAdminHandler(
async (_context, validatedData) => {
const { action, schedulerId } = validatedData as z.infer<
typeof PostInputSchema
>;
const integration = getSchedulerIntegration();
switch (action) {
case "start":
if (schedulerId) {
await integration.startScheduler(schedulerId);
}
break;
case "stop":
if (schedulerId) {
await integration.stopScheduler(schedulerId);
}
break;
case "trigger":
if (schedulerId) {
await integration.triggerScheduler(schedulerId);
}
break;
case "startAll":
await integration.getManager().startAll();
break;
case "stopAll":
await integration.getManager().stopAll();
break;
default:
return {
success: false,
error: `Unknown action: ${action}`,
};
}
return {
success: true,
message: `Action '${action}' completed successfully`,
timestamp: new Date().toISOString(),
};
},
{
validateInput: PostInputSchema,
}
);

View File

@ -1,152 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { z } from "zod";
import { authOptions } from "@/lib/auth";
import {
AuditOutcome,
createAuditContext,
securityAuditLogger,
} from "@/lib/securityAuditLogger";
import {
type AlertSeverity,
securityMonitoring,
} from "@/lib/securityMonitoring";
const alertQuerySchema = z.object({
severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional(),
acknowledged: z.enum(["true", "false"]).optional(),
limit: z
.string()
.transform((val) => Number.parseInt(val, 10))
.optional(),
offset: z
.string()
.transform((val) => Number.parseInt(val, 10))
.optional(),
});
const acknowledgeAlertSchema = z.object({
alertId: z.string().uuid(),
action: z.literal("acknowledge"),
});
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user || !session.user.isPlatformUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const url = new URL(request.url);
const params = Object.fromEntries(url.searchParams.entries());
const query = alertQuerySchema.parse(params);
const context = await createAuditContext(request, session);
// Get alerts based on filters
let alerts = securityMonitoring.getActiveAlerts(
query.severity as AlertSeverity
);
// Apply acknowledged filter if provided
if (query.acknowledged !== undefined) {
const showAcknowledged = query.acknowledged === "true";
alerts = alerts.filter((alert) =>
showAcknowledged ? alert.acknowledged : !alert.acknowledged
);
}
// Apply pagination
const limit = query.limit || 50;
const offset = query.offset || 0;
const paginatedAlerts = alerts.slice(offset, offset + limit);
// Log alert access
await securityAuditLogger.logPlatformAdmin(
"security_alerts_access",
AuditOutcome.SUCCESS,
{
...context,
metadata: {
alertCount: alerts.length,
filters: query,
},
}
);
return NextResponse.json({
alerts: paginatedAlerts,
total: alerts.length,
limit,
offset,
});
} catch (error) {
console.error("Security alerts API error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid query parameters", details: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user || !session.user.isPlatformUser || !session.user.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { alertId, action } = acknowledgeAlertSchema.parse(body);
const context = await createAuditContext(request, session);
if (action === "acknowledge") {
const success = await securityMonitoring.acknowledgeAlert(
alertId,
session.user.id
);
if (!success) {
return NextResponse.json({ error: "Alert not found" }, { status: 404 });
}
// Log alert acknowledgment
await securityAuditLogger.logPlatformAdmin(
"security_alert_acknowledged",
AuditOutcome.SUCCESS,
{
...context,
metadata: { alertId },
}
);
return NextResponse.json({ success: true });
}
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
} catch (error) {
console.error("Security alert action error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request", details: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@ -1,91 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { z } from "zod";
import { authOptions } from "@/lib/auth";
import {
AuditOutcome,
createAuditContext,
securityAuditLogger,
} from "@/lib/securityAuditLogger";
import { securityMonitoring } from "@/lib/securityMonitoring";
const exportQuerySchema = z.object({
format: z.enum(["json", "csv"]).default("json"),
startDate: z.string().datetime(),
endDate: z.string().datetime(),
type: z.enum(["alerts", "metrics"]).default("alerts"),
});
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user || !session.user.isPlatformUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const url = new URL(request.url);
const params = Object.fromEntries(url.searchParams.entries());
const query = exportQuerySchema.parse(params);
const context = await createAuditContext(request, session);
const timeRange = {
start: new Date(query.startDate),
end: new Date(query.endDate),
};
let data: string;
let filename: string;
let contentType: string;
if (query.type === "alerts") {
data = securityMonitoring.exportSecurityData(query.format, timeRange);
filename = `security-alerts-${query.startDate.split("T")[0]}-to-${query.endDate.split("T")[0]}.${query.format}`;
contentType = query.format === "csv" ? "text/csv" : "application/json";
} else {
// Export metrics
const metrics = await securityMonitoring.getSecurityMetrics(timeRange);
data = JSON.stringify(metrics, null, 2);
filename = `security-metrics-${query.startDate.split("T")[0]}-to-${query.endDate.split("T")[0]}.json`;
contentType = "application/json";
}
// Log data export
await securityAuditLogger.logPlatformAdmin(
"security_data_export",
AuditOutcome.SUCCESS,
{
...context,
metadata: {
exportType: query.type,
format: query.format,
timeRange,
dataSize: data.length,
},
}
);
const headers = new Headers({
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
"Content-Length": data.length.toString(),
});
return new NextResponse(data, { headers });
} catch (error) {
console.error("Security data export error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid query parameters", details: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@ -1,192 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { z } from "zod";
import { authOptions } from "@/lib/auth";
import {
AuditOutcome,
createAuditContext,
securityAuditLogger,
} from "@/lib/securityAuditLogger";
import {
AlertChannel,
type AlertSeverity,
type MonitoringConfig,
securityMonitoring,
} from "@/lib/securityMonitoring";
// Type for partial config updates that allows optional nested properties
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type ConfigUpdate = DeepPartial<MonitoringConfig>;
const metricsQuerySchema = z.object({
startDate: z.string().datetime().optional(),
endDate: z.string().datetime().optional(),
companyId: z.string().uuid().optional(),
severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional(),
});
const configUpdateSchema = z.object({
thresholds: z
.object({
failedLoginsPerMinute: z.number().min(1).max(100).optional(),
failedLoginsPerHour: z.number().min(1).max(1000).optional(),
rateLimitViolationsPerMinute: z.number().min(1).max(100).optional(),
cspViolationsPerMinute: z.number().min(1).max(100).optional(),
adminActionsPerHour: z.number().min(1).max(100).optional(),
massDataAccessThreshold: z.number().min(10).max(10000).optional(),
suspiciousIPThreshold: z.number().min(1).max(100).optional(),
})
.optional(),
alerting: z
.object({
enabled: z.boolean().optional(),
channels: z.array(z.nativeEnum(AlertChannel)).optional(),
suppressDuplicateMinutes: z.number().min(1).max(1440).optional(),
escalationTimeoutMinutes: z.number().min(5).max(1440).optional(),
})
.optional(),
retention: z
.object({
alertRetentionDays: z.number().min(1).max(3650).optional(),
metricsRetentionDays: z.number().min(1).max(3650).optional(),
})
.optional(),
});
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Only platform admins can access security monitoring
if (!session.user.isPlatformUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const url = new URL(request.url);
const params = Object.fromEntries(url.searchParams.entries());
const query = metricsQuerySchema.parse(params);
const context = await createAuditContext(request, session);
const timeRange = {
start: query.startDate
? new Date(query.startDate)
: new Date(Date.now() - 24 * 60 * 60 * 1000),
end: query.endDate ? new Date(query.endDate) : new Date(),
};
// Get security metrics
const metrics = await securityMonitoring.getSecurityMetrics(
timeRange,
query.companyId
);
// Get active alerts
const alerts = securityMonitoring.getActiveAlerts(
query.severity as AlertSeverity
);
// Get monitoring configuration
const config = securityMonitoring.getConfig();
// Log access to security monitoring
await securityAuditLogger.logPlatformAdmin(
"security_monitoring_access",
AuditOutcome.SUCCESS,
context
);
return NextResponse.json({
metrics,
alerts,
config,
timeRange,
});
} catch (error) {
console.error("Security monitoring API error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid query parameters", details: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!session.user.isPlatformUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const body = await request.json();
const validatedConfig = configUpdateSchema.parse(body);
const context = await createAuditContext(request, session);
// Build the config update object with proper type safety
const configUpdate: ConfigUpdate = {};
if (validatedConfig.thresholds) {
configUpdate.thresholds = validatedConfig.thresholds;
}
if (validatedConfig.alerting) {
configUpdate.alerting = validatedConfig.alerting;
}
if (validatedConfig.retention) {
configUpdate.retention = validatedConfig.retention;
}
// Update monitoring configuration
securityMonitoring.updateConfig(configUpdate);
// Log configuration change
await securityAuditLogger.logPlatformAdmin(
"security_monitoring_config_update",
AuditOutcome.SUCCESS,
{
...context,
metadata: { configChanges: validatedConfig },
}
);
return NextResponse.json({
success: true,
config: securityMonitoring.getConfig(),
});
} catch (error) {
console.error("Security monitoring config update error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid configuration", details: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@ -1,198 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { z } from "zod";
import { authOptions } from "@/lib/auth";
import {
AuditOutcome,
createAuditContext,
securityAuditLogger,
} from "@/lib/securityAuditLogger";
import {
type AlertType,
type SecurityMetrics,
securityMonitoring,
type ThreatLevel,
} from "@/lib/securityMonitoring";
interface ThreatAnalysisResults {
ipThreatAnalysis?: {
ipAddress: string;
threatLevel: ThreatLevel;
isBlacklisted: boolean;
riskFactors: string[];
recommendations: string[];
};
timeRangeAnalysis?: {
timeRange: { start: Date; end: Date };
securityScore: number;
threatLevel: string;
topThreats: Array<{ type: AlertType; count: number }>;
geoDistribution: Record<string, number>;
riskUsers: Array<{ userId: string; email: string; riskScore: number }>;
};
overallThreatLandscape?: {
currentThreatLevel: string;
securityScore: number;
activeAlerts: number;
criticalEvents: number;
recommendations: string[];
};
}
const threatAnalysisSchema = z.object({
ipAddress: z.string().optional(),
userId: z.string().uuid().optional(),
timeRange: z
.object({
start: z.string().datetime(),
end: z.string().datetime(),
})
.optional(),
});
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user || session.user.role !== "ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const analysis = threatAnalysisSchema.parse(body);
const context = await createAuditContext(request, session);
const results: ThreatAnalysisResults = {};
// IP threat analysis
if (analysis.ipAddress) {
const ipThreat = await securityMonitoring.calculateIPThreatLevel(
analysis.ipAddress
);
results.ipThreatAnalysis = {
ipAddress: analysis.ipAddress,
...ipThreat,
};
}
// Time-based analysis
if (analysis.timeRange) {
const timeRange = {
start: new Date(analysis.timeRange.start),
end: new Date(analysis.timeRange.end),
};
const metrics = await securityMonitoring.getSecurityMetrics(timeRange);
results.timeRangeAnalysis = {
timeRange,
securityScore: metrics.securityScore,
threatLevel: metrics.threatLevel,
topThreats: metrics.topThreats,
geoDistribution: metrics.geoDistribution,
riskUsers: metrics.userRiskScores.slice(0, 5),
};
}
// General threat landscape
const defaultTimeRange = {
start: new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24 hours
end: new Date(),
};
const overallMetrics =
await securityMonitoring.getSecurityMetrics(defaultTimeRange);
results.overallThreatLandscape = {
currentThreatLevel: overallMetrics.threatLevel,
securityScore: overallMetrics.securityScore,
activeAlerts: overallMetrics.activeAlerts,
criticalEvents: overallMetrics.criticalEvents,
recommendations: generateThreatRecommendations(overallMetrics),
};
// Log threat analysis request
await securityAuditLogger.logPlatformAdmin(
"threat_analysis_performed",
AuditOutcome.SUCCESS,
{
...context,
metadata: {
analysisType: Object.keys(analysis),
threatLevel: results.overallThreatLandscape?.currentThreatLevel,
},
}
);
return NextResponse.json(results);
} catch (error) {
console.error("Threat analysis error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request", details: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
function generateThreatRecommendations(metrics: SecurityMetrics): string[] {
const recommendations: string[] = [];
if (metrics.securityScore < 70) {
recommendations.push(
"Security score is below acceptable threshold - immediate action required"
);
}
if (metrics.activeAlerts > 5) {
recommendations.push(
"High number of active alerts - prioritize alert resolution"
);
}
if (metrics.criticalEvents > 0) {
recommendations.push(
"Critical security events detected - investigate immediately"
);
}
const highRiskUsers = metrics.userRiskScores.filter(
(user) => user.riskScore > 50
);
if (highRiskUsers.length > 0) {
recommendations.push(
`${highRiskUsers.length} users have elevated risk scores - review accounts`
);
}
// Check for geographic anomalies
const countries = Object.keys(metrics.geoDistribution);
if (countries.length > 10) {
recommendations.push(
"High geographic diversity detected - review for suspicious activity"
);
}
// Check for common attack patterns
const bruteForceAlerts = metrics.topThreats.filter(
(threat) => threat.type === "BRUTE_FORCE_ATTACK"
);
if (bruteForceAlerts.length > 0) {
recommendations.push(
"Brute force attacks detected - strengthen authentication controls"
);
}
if (recommendations.length === 0) {
recommendations.push(
"Security posture appears stable - continue monitoring"
);
}
return recommendations;
}

View File

@ -4,7 +4,7 @@ import { getServerSession } from "next-auth";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import { processUnprocessedSessions } from "../../../../lib/processingScheduler";
import { getSessionsNeedingProcessing } from "../../../../lib/processingStatusManager";
import { ProcessingStatusManager } from "../../../../lib/processingStatusManager";
interface SessionUser {
email: string;
@ -65,7 +65,8 @@ export async function POST(request: NextRequest) {
: 5;
// Check how many sessions need AI processing using the new status system
const sessionsNeedingAI = await getSessionsNeedingProcessing(
const sessionsNeedingAI =
await ProcessingStatusManager.getSessionsNeedingProcessing(
ProcessingStage.AI_ANALYSIS,
1000 // Get count only
);
@ -87,6 +88,7 @@ export async function POST(request: NextRequest) {
}
// Start processing (this will run asynchronously)
const _startTime = Date.now();
// Note: We're calling the function but not awaiting it to avoid timeout
// The processing will continue in the background

View File

@ -1,127 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { cspMonitoring } from "@/lib/csp-monitoring";
import { extractClientIP, rateLimiter } from "@/lib/rateLimiter";
export async function GET(request: NextRequest) {
try {
// Authentication check for security metrics endpoint
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Check for ADMIN role as CSP metrics contain sensitive security data
if (session.user.role !== "ADMIN") {
return NextResponse.json(
{ error: "Forbidden - Admin access required" },
{ status: 403 }
);
}
// Rate limiting for metrics endpoint
const ip = extractClientIP(request);
const rateLimitResult = await rateLimiter.check(
`csp-metrics:${ip}`,
30, // 30 requests
60 * 1000 // per minute
);
if (!rateLimitResult.success) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
// Parse query parameters
const url = new URL(request.url);
const timeRange = url.searchParams.get("range") || "24h";
const format = url.searchParams.get("format") || "json";
// Calculate time range
const now = new Date();
let start: Date;
switch (timeRange) {
case "1h":
start = new Date(now.getTime() - 60 * 60 * 1000);
break;
case "6h":
start = new Date(now.getTime() - 6 * 60 * 60 * 1000);
break;
case "24h":
start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
break;
case "7d":
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case "30d":
start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
default:
start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
}
// Get metrics from monitoring service
const metrics = cspMonitoring.getMetrics({ start, end: now });
// Get policy recommendations
const recommendations = cspMonitoring.generatePolicyRecommendations({
start,
end: now,
});
const response = {
timeRange: {
start: start.toISOString(),
end: now.toISOString(),
range: timeRange,
},
summary: {
totalViolations: metrics.totalViolations,
criticalViolations: metrics.criticalViolations,
bypassAttempts: metrics.bypassAttempts,
violationRate:
metrics.totalViolations /
((now.getTime() - start.getTime()) / (60 * 60 * 1000)), // per hour
},
topViolatedDirectives: metrics.topViolatedDirectives,
topBlockedUris: metrics.topBlockedUris,
violationTrends: metrics.violationTrends,
recommendations: recommendations,
lastUpdated: now.toISOString(),
};
// Export format handling
if (format === "csv") {
const csv = cspMonitoring.exportViolations("csv");
return new NextResponse(csv, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="csp-violations-${timeRange}.csv"`,
},
});
}
return NextResponse.json(response);
} catch (error) {
console.error("Error fetching CSP metrics:", error);
return NextResponse.json(
{ error: "Failed to fetch metrics" },
{ status: 500 }
);
}
}
// Handle preflight requests
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin":
process.env.ALLOWED_ORIGINS || "https://livedash.notso.ai",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
},
});
}

View File

@ -1,129 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import {
type CSPViolationReport,
detectCSPBypass,
parseCSPViolation,
} from "@/lib/csp";
import { cspMonitoring } from "@/lib/csp-monitoring";
import { rateLimiter } from "@/lib/rateLimiter";
export async function POST(request: NextRequest) {
try {
// Rate limiting for CSP reports
const ip = request.headers.get("x-forwarded-for") || "unknown";
const rateLimitResult = await rateLimiter.check(
`csp-report:${ip}`,
10, // 10 reports
60 * 1000 // per minute
);
if (!rateLimitResult.success) {
return NextResponse.json(
{ error: "Too many CSP reports" },
{ status: 429 }
);
}
const contentType = request.headers.get("content-type");
if (
!contentType?.includes("application/csp-report") &&
!contentType?.includes("application/json")
) {
return NextResponse.json(
{ error: "Invalid content type" },
{ status: 400 }
);
}
const report: CSPViolationReport = await request.json();
if (!report["csp-report"]) {
return NextResponse.json(
{ error: "Invalid CSP report format" },
{ status: 400 }
);
}
// Process violation through monitoring service
const monitoringResult = await cspMonitoring.processViolation(
report,
ip,
request.headers.get("user-agent") || undefined
);
// Enhanced logging based on monitoring analysis
const logEntry = {
timestamp: new Date().toISOString(),
ip,
userAgent: request.headers.get("user-agent"),
violation: parseCSPViolation(report),
bypassDetection: detectCSPBypass(
report["csp-report"]["blocked-uri"] +
" " +
(report["csp-report"]["script-sample"] || "")
),
originalReport: report,
alertLevel: monitoringResult.alertLevel,
shouldAlert: monitoringResult.shouldAlert,
recommendations: monitoringResult.recommendations,
};
// In development, log to console with recommendations
if (process.env.NODE_ENV === "development") {
console.warn("🚨 CSP Violation Detected:", {
...logEntry,
recommendations: monitoringResult.recommendations,
});
if (monitoringResult.recommendations.length > 0) {
console.info("💡 Recommendations:", monitoringResult.recommendations);
}
}
// Enhanced alerting based on monitoring service analysis
if (monitoringResult.shouldAlert) {
const alertEmoji = {
low: "🟡",
medium: "🟠",
high: "🔴",
critical: "🚨",
}[monitoringResult.alertLevel];
console.error(
`${alertEmoji} CSP ${monitoringResult.alertLevel.toUpperCase()} ALERT:`,
{
directive: logEntry.violation.directive,
blockedUri: logEntry.violation.blockedUri,
isBypassAttempt: logEntry.bypassDetection.isDetected,
riskLevel: logEntry.bypassDetection.riskLevel,
recommendations: monitoringResult.recommendations.slice(0, 3), // Limit to 3 recommendations
}
);
}
// Clean up old violations periodically (every 100 requests)
if (Math.random() < 0.01) {
cspMonitoring.cleanupOldViolations();
}
return new NextResponse(null, { status: 204 });
} catch (error) {
console.error("Error processing CSP report:", error);
return NextResponse.json(
{ error: "Failed to process report" },
{ status: 500 }
);
}
}
// Handle preflight requests
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}

View File

@ -1,18 +0,0 @@
/**
* CSRF Token API Endpoint
*
* This endpoint provides CSRF tokens to clients for secure form submissions.
* It generates a new token and sets it as an HTTP-only cookie.
*/
import { generateCSRFTokenResponse } from "../../../middleware/csrfProtection";
/**
* GET /api/csrf-token
*
* Generates and returns a new CSRF token.
* The token is also set as an HTTP-only cookie for automatic inclusion in requests.
*/
export function GET() {
return generateCSRFTokenResponse();
}

View File

@ -3,7 +3,7 @@ import { getServerSession } from "next-auth";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
export async function GET() {
export async function GET(_request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Not logged in" }, { status: 401 });

View File

@ -1,432 +0,0 @@
/**
* Enhanced Dashboard Metrics API with Performance Optimization
*
* This demonstrates integration of caching, deduplication, and performance monitoring
* into existing API endpoints for significant performance improvements.
*/
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { withErrorHandling } from "@/lib/api/errors";
import { createSuccessResponse } from "@/lib/api/response";
import { caches } from "@/lib/performance/cache";
import { deduplicators } from "@/lib/performance/deduplication";
// Performance system imports
import {
PerformanceUtils,
performanceMonitor,
} from "@/lib/performance/monitor";
import { authOptions } from "../../../../lib/auth";
import { sessionMetrics } from "../../../../lib/metrics";
import { prisma } from "../../../../lib/prisma";
import type { ChatSession, MetricsResult } from "../../../../lib/types";
/**
* Converts a Prisma session to ChatSession format for metrics
*/
function convertToMockChatSession(
ps: {
id: string;
companyId: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
},
questions: string[]
): ChatSession {
// Convert questions to mock messages for backward compatibility
const mockMessages = questions.map((q, index) => ({
id: `question-${index}`,
sessionId: ps.id,
timestamp: ps.createdAt,
role: "User",
content: q,
order: index,
createdAt: ps.createdAt,
}));
return {
id: ps.id,
sessionId: ps.id,
companyId: ps.companyId,
startTime: new Date(ps.startTime),
endTime: ps.endTime ? new Date(ps.endTime) : null,
transcriptContent: "",
createdAt: new Date(ps.createdAt),
updatedAt: new Date(ps.createdAt),
category: ps.category || undefined,
language: ps.language || undefined,
country: ps.country || undefined,
ipAddress: ps.ipAddress || undefined,
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent,
avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined,
fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
summary: ps.summary || undefined,
messages: mockMessages, // Use questions as messages for metrics
userId: undefined,
};
}
interface SessionUser {
email: string;
name?: string;
}
interface SessionData {
user: SessionUser;
}
interface MetricsRequestParams {
companyId: string;
startDate?: string;
endDate?: string;
}
interface MetricsResponse {
metrics: MetricsResult;
csvUrl: string | null;
company: {
id: string;
name: string;
csvUrl: string;
status: string;
};
dateRange: { minDate: string; maxDate: string } | null;
performanceMetrics?: {
cacheHit: boolean;
deduplicationHit: boolean;
executionTime: number;
dataFreshness: string;
};
}
/**
* Generate a cache key for metrics based on company and date range
*/
function generateMetricsCacheKey(params: MetricsRequestParams): string {
const { companyId, startDate, endDate } = params;
return `metrics:${companyId}:${startDate || "all"}:${endDate || "all"}`;
}
/**
* Fetch sessions with performance monitoring and caching
*/
const fetchSessionsWithCache = deduplicators.database.memoize(
async (params: MetricsRequestParams) => {
return PerformanceUtils.measureAsync("metrics-session-fetch", async () => {
const whereClause: {
companyId: string;
startTime?: {
gte: Date;
lte: Date;
};
} = {
companyId: params.companyId,
};
if (params.startDate && params.endDate) {
whereClause.startTime = {
gte: new Date(params.startDate),
lte: new Date(`${params.endDate}T23:59:59.999Z`),
};
}
// Fetch sessions
const sessions = await prisma.session.findMany({
where: whereClause,
select: {
id: true,
companyId: true,
startTime: true,
endTime: true,
createdAt: true,
category: true,
language: true,
country: true,
ipAddress: true,
sentiment: true,
messagesSent: true,
avgResponseTime: true,
escalated: true,
forwardedHr: true,
initialMsg: true,
fullTranscriptUrl: true,
summary: true,
},
});
return sessions;
});
},
{
keyGenerator: (params: MetricsRequestParams) => JSON.stringify(params),
ttl: 2 * 60 * 1000, // 2 minutes
}
);
/**
* Fetch questions for sessions with deduplication
*/
const fetchQuestionsWithDeduplication = deduplicators.database.memoize(
async (sessionIds: string[]) => {
return PerformanceUtils.measureAsync(
"metrics-questions-fetch",
async () => {
const questions = await prisma.sessionQuestion.findMany({
where: { sessionId: { in: sessionIds } },
include: { question: true },
orderBy: { order: "asc" },
});
return questions;
}
);
},
{
keyGenerator: (sessionIds: string[]) =>
`questions:${sessionIds.sort().join(",")}`,
ttl: 5 * 60 * 1000, // 5 minutes
}
);
/**
* Calculate metrics with caching
*/
const calculateMetricsWithCache = async (
chatSessions: ChatSession[],
companyConfig: Record<string, unknown>,
cacheKey: string
): Promise<{
result: {
metrics: MetricsResult;
calculatedAt: string;
sessionCount: number;
};
fromCache: boolean;
}> => {
return caches.metrics
.getOrCompute(
cacheKey,
() =>
PerformanceUtils.measureAsync("metrics-calculation", async () => {
const metrics = sessionMetrics(chatSessions, companyConfig);
return {
metrics,
calculatedAt: new Date().toISOString(),
sessionCount: chatSessions.length,
};
}).then(({ result }) => result),
5 * 60 * 1000 // 5 minutes cache
)
.then((cached) => ({
result: cached,
fromCache: caches.metrics.has(cacheKey),
}));
};
/**
* Enhanced GET endpoint with performance optimizations
*/
export const GET = withErrorHandling(async (request: NextRequest) => {
const requestTimer = PerformanceUtils.createTimer("metrics-request-total");
let _cacheHit = false;
let deduplicationHit = false;
try {
// Authentication with performance monitoring
const { result: session } = await PerformanceUtils.measureAsync(
"metrics-auth-check",
async () => (await getServerSession(authOptions)) as SessionData | null
);
if (!session?.user) {
performanceMonitor.recordRequest(requestTimer.end(), true);
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
}
// User lookup with caching
const user = await caches.sessions.getOrCompute(
`user:${session.user.email}`,
async () => {
const { result } = await PerformanceUtils.measureAsync(
"metrics-user-lookup",
async () =>
prisma.user.findUnique({
where: { email: session.user.email },
select: {
id: true,
companyId: true,
company: {
select: {
id: true,
name: true,
csvUrl: true,
status: true,
},
},
},
})
);
return result;
},
15 * 60 * 1000 // 15 minutes
);
if (!user) {
performanceMonitor.recordRequest(requestTimer.end(), true);
return NextResponse.json({ error: "No user" }, { status: 401 });
}
// Extract request parameters
const { searchParams } = new URL(request.url);
const startDate = searchParams.get("startDate") || undefined;
const endDate = searchParams.get("endDate") || undefined;
const params: MetricsRequestParams = {
companyId: user.companyId,
startDate,
endDate,
};
const cacheKey = generateMetricsCacheKey(params);
// Try to get complete cached response first
const cachedResponse = await caches.apiResponses.get(
`full-metrics:${cacheKey}`
);
if (cachedResponse) {
_cacheHit = true;
const duration = requestTimer.end();
performanceMonitor.recordRequest(duration, false);
return NextResponse.json(
createSuccessResponse({
...cachedResponse,
performanceMetrics: {
cacheHit: true,
deduplicationHit: false,
executionTime: duration,
dataFreshness: "cached",
},
})
);
}
// Fetch sessions with deduplication and monitoring
const sessionResult = await fetchSessionsWithCache(params);
const prismaSessions = sessionResult.result;
// Track if this was a deduplication hit
deduplicationHit = deduplicators.database.getStats().hitRate > 0;
// Fetch questions with deduplication
const sessionIds = prismaSessions.map((s) => s.id);
const questionsResult = await fetchQuestionsWithDeduplication(sessionIds);
const sessionQuestions = questionsResult.result;
// Group questions by session with performance monitoring
const { result: questionsBySession } = await PerformanceUtils.measureAsync(
"metrics-questions-grouping",
async () => {
return sessionQuestions.reduce(
(acc, sq) => {
if (!acc[sq.sessionId]) acc[sq.sessionId] = [];
acc[sq.sessionId].push(sq.question.content);
return acc;
},
{} as Record<string, string[]>
);
}
);
// Convert to ChatSession format with monitoring
const { result: chatSessions } = await PerformanceUtils.measureAsync(
"metrics-session-conversion",
async () => {
return prismaSessions.map((ps) => {
const questions = questionsBySession[ps.id] || [];
return convertToMockChatSession(ps, questions);
});
}
);
// Calculate metrics with caching
const companyConfigForMetrics = {};
const { result: metricsData, fromCache: metricsFromCache } =
await calculateMetricsWithCache(
chatSessions,
companyConfigForMetrics,
`calc:${cacheKey}`
);
// Calculate date range with monitoring
const { result: dateRange } = await PerformanceUtils.measureAsync(
"metrics-date-range-calc",
async () => {
if (prismaSessions.length === 0) return null;
const dates = prismaSessions
.map((s) => new Date(s.startTime))
.sort((a: Date, b: Date) => a.getTime() - b.getTime());
return {
minDate: dates[0].toISOString().split("T")[0],
maxDate: dates[dates.length - 1].toISOString().split("T")[0],
};
}
);
const responseData: MetricsResponse = {
metrics: metricsData.metrics,
csvUrl: user.company.csvUrl,
company: user.company,
dateRange,
performanceMetrics: {
cacheHit: metricsFromCache,
deduplicationHit,
executionTime: 0, // Will be set below
dataFreshness: metricsFromCache ? "cached" : "fresh",
},
};
// Cache the complete response for faster subsequent requests
await caches.apiResponses.set(
`full-metrics:${cacheKey}`,
responseData,
2 * 60 * 1000 // 2 minutes
);
const duration = requestTimer.end();
// biome-ignore lint/style/noNonNullAssertion: performanceMetrics is guaranteed to exist as we just created it
responseData.performanceMetrics!.executionTime = duration;
performanceMonitor.recordRequest(duration, false);
return NextResponse.json(createSuccessResponse(responseData));
} catch (error) {
const duration = requestTimer.end();
performanceMonitor.recordRequest(duration, true);
throw error; // Re-throw for error handler
}
});
// Export enhanced endpoint as default
export { GET as default };

View File

@ -5,69 +5,6 @@ import { sessionMetrics } from "../../../../lib/metrics";
import { prisma } from "../../../../lib/prisma";
import type { ChatSession } from "../../../../lib/types";
/**
* Converts a Prisma session to ChatSession format for metrics
*/
function convertToMockChatSession(
ps: {
id: string;
companyId: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
},
questions: string[]
): ChatSession {
// Convert questions to mock messages for backward compatibility
const mockMessages = questions.map((q, index) => ({
id: `question-${index}`,
sessionId: ps.id,
timestamp: ps.createdAt,
role: "User",
content: q,
order: index,
createdAt: ps.createdAt,
}));
return {
id: ps.id,
sessionId: ps.id,
companyId: ps.companyId,
startTime: new Date(ps.startTime),
endTime: ps.endTime ? new Date(ps.endTime) : null,
transcriptContent: "",
createdAt: new Date(ps.createdAt),
updatedAt: new Date(ps.createdAt),
category: ps.category || undefined,
language: ps.language || undefined,
country: ps.country || undefined,
ipAddress: ps.ipAddress || undefined,
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent,
avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined,
fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
summary: ps.summary || undefined,
messages: mockMessages, // Use questions as messages for metrics
userId: undefined,
};
}
interface SessionUser {
email: string;
name?: string;
@ -170,8 +107,45 @@ export async function GET(request: NextRequest) {
// Convert Prisma sessions to ChatSession[] type for sessionMetrics
const chatSessions: ChatSession[] = prismaSessions.map((ps) => {
// Get questions for this session or empty array
const questions = questionsBySession[ps.id] || [];
return convertToMockChatSession(ps, questions);
// Convert questions to mock messages for backward compatibility
const mockMessages = questions.map((q, index) => ({
id: `question-${index}`,
sessionId: ps.id,
timestamp: ps.createdAt,
role: "User",
content: q,
order: index,
createdAt: ps.createdAt,
}));
return {
id: ps.id,
sessionId: ps.id,
companyId: ps.companyId,
startTime: new Date(ps.startTime),
endTime: ps.endTime ? new Date(ps.endTime) : null,
transcriptContent: "",
createdAt: new Date(ps.createdAt),
updatedAt: new Date(ps.createdAt),
category: ps.category || undefined,
language: ps.language || undefined,
country: ps.country || undefined,
ipAddress: ps.ipAddress || undefined,
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent,
avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined,
fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
summary: ps.summary || undefined,
messages: mockMessages, // Use questions as messages for metrics
userId: undefined,
};
});
// Pass company config to metrics

View File

@ -1,9 +1,9 @@
import { NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
export async function GET() {
export async function GET(_request: NextRequest) {
const authSession = await getServerSession(authOptions);
if (!authSession || !authSession.user?.companyId) {
@ -14,8 +14,6 @@ export async function GET() {
try {
// Use groupBy for better performance with distinct values
// Limit results to prevent unbounded queries
const MAX_FILTER_OPTIONS = 1000;
const [categoryGroups, languageGroups] = await Promise.all([
prisma.session.groupBy({
by: ["category"],
@ -26,7 +24,6 @@ export async function GET() {
orderBy: {
category: "asc",
},
take: MAX_FILTER_OPTIONS,
}),
prisma.session.groupBy({
by: ["language"],
@ -37,7 +34,6 @@ export async function GET() {
orderBy: {
language: "asc",
},
take: MAX_FILTER_OPTIONS,
}),
]);

View File

@ -2,78 +2,6 @@ import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../../../lib/prisma";
import type { ChatSession } from "../../../../../lib/types";
/**
* Maps Prisma session object to ChatSession type
*/
function mapPrismaSessionToChatSession(prismaSession: {
id: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
messages: Array<{
id: string;
sessionId: string;
timestamp: Date | null;
role: string;
content: string;
order: number;
createdAt: Date;
}>;
}): ChatSession {
return {
// Spread prismaSession to include all its properties
...prismaSession,
// Override properties that need conversion or specific mapping
id: prismaSession.id, // ChatSession.id from Prisma.Session.id
sessionId: prismaSession.id, // ChatSession.sessionId from Prisma.Session.id
startTime: new Date(prismaSession.startTime),
endTime: prismaSession.endTime ? new Date(prismaSession.endTime) : null,
createdAt: new Date(prismaSession.createdAt),
// Prisma.Session does not have an `updatedAt` field. We'll use `createdAt` as a fallback.
updatedAt: new Date(prismaSession.createdAt), // Fallback to createdAt
// Prisma.Session does not have a `userId` field.
userId: null, // Explicitly set to null or map if available from another source
// Prisma.Session does not have a `companyId` field.
companyId: "", // Explicitly set to empty string - should be resolved from session context
// Ensure nullable fields from Prisma are correctly mapped to ChatSession's optional or nullable fields
category: prismaSession.category ?? null,
language: prismaSession.language ?? null,
country: prismaSession.country ?? null,
ipAddress: prismaSession.ipAddress ?? null,
sentiment: prismaSession.sentiment ?? null,
messagesSent: prismaSession.messagesSent ?? undefined,
avgResponseTime: prismaSession.avgResponseTime ?? null,
escalated: prismaSession.escalated ?? undefined,
forwardedHr: prismaSession.forwardedHr ?? undefined,
initialMsg: prismaSession.initialMsg ?? undefined,
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
summary: prismaSession.summary ?? null,
transcriptContent: undefined, // Not available in Session model
messages:
prismaSession.messages?.map((msg) => ({
id: msg.id,
sessionId: msg.sessionId,
timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
role: msg.role,
content: msg.content,
order: msg.order,
createdAt: new Date(msg.createdAt),
})) ?? [], // New field - parsed messages
};
}
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
@ -102,7 +30,45 @@ export async function GET(
}
// Map Prisma session object to ChatSession type
const session: ChatSession = mapPrismaSessionToChatSession(prismaSession);
const session: ChatSession = {
// Spread prismaSession to include all its properties
...prismaSession,
// Override properties that need conversion or specific mapping
id: prismaSession.id, // ChatSession.id from Prisma.Session.id
sessionId: prismaSession.id, // ChatSession.sessionId from Prisma.Session.id
startTime: new Date(prismaSession.startTime),
endTime: prismaSession.endTime ? new Date(prismaSession.endTime) : null,
createdAt: new Date(prismaSession.createdAt),
// Prisma.Session does not have an `updatedAt` field. We'll use `createdAt` as a fallback.
// Or, if your business logic implies an update timestamp elsewhere, use that.
updatedAt: new Date(prismaSession.createdAt), // Fallback to createdAt
// Prisma.Session does not have a `userId` field.
userId: null, // Explicitly set to null or map if available from another source
// Ensure nullable fields from Prisma are correctly mapped to ChatSession's optional or nullable fields
category: prismaSession.category ?? null,
language: prismaSession.language ?? null,
country: prismaSession.country ?? null,
ipAddress: prismaSession.ipAddress ?? null,
sentiment: prismaSession.sentiment ?? null,
messagesSent: prismaSession.messagesSent ?? undefined, // Use undefined if ChatSession expects number | undefined
avgResponseTime: prismaSession.avgResponseTime ?? null,
escalated: prismaSession.escalated ?? undefined,
forwardedHr: prismaSession.forwardedHr ?? undefined,
initialMsg: prismaSession.initialMsg ?? undefined,
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
summary: prismaSession.summary ?? null, // New field
transcriptContent: null, // Not available in Session model
messages:
prismaSession.messages?.map((msg) => ({
id: msg.id,
sessionId: msg.sessionId,
timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
role: msg.role,
content: msg.content,
order: msg.order,
createdAt: new Date(msg.createdAt),
})) ?? [], // New field - parsed messages
};
return NextResponse.json({ session });
} catch (error) {

View File

@ -1,303 +0,0 @@
/**
* Refactored Sessions API Endpoint
*
* This demonstrates how to use the new standardized API architecture
* for consistent error handling, validation, authentication, and response formatting.
*
* BEFORE: Manual auth, inconsistent errors, no validation, mixed response format
* AFTER: Standardized middleware, typed validation, consistent responses, audit logging
*/
import type { Prisma } from "@prisma/client";
import { SessionCategory } from "@prisma/client";
import { z } from "zod";
import {
calculatePaginationMeta,
createAuthenticatedHandler,
createPaginatedResponse,
DatabaseError,
} from "@/lib/api";
import { prisma } from "@/lib/prisma";
import type { ChatSession } from "@/lib/types";
/**
* Input validation schema for session queries
*/
const SessionQuerySchema = z.object({
// Search parameters
searchTerm: z.string().max(100).optional(),
category: z.nativeEnum(SessionCategory).optional(),
language: z.string().min(2).max(5).optional(),
// Date filtering
startDate: z.string().date().optional(),
endDate: z.string().date().optional(),
// Sorting
sortKey: z
.enum([
"startTime",
"category",
"language",
"sentiment",
"messagesSent",
"avgResponseTime",
])
.default("startTime"),
sortOrder: z.enum(["asc", "desc"]).default("desc"),
// Pagination (handled by middleware but included for completeness)
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(20),
});
type SessionQueryInput = z.infer<typeof SessionQuerySchema>;
/**
* Build where clause for session filtering
*/
function buildWhereClause(
companyId: string,
filters: SessionQueryInput
): Prisma.SessionWhereInput {
const whereClause: Prisma.SessionWhereInput = { companyId };
// Search across multiple fields
if (filters.searchTerm?.trim()) {
whereClause.OR = [
{ id: { contains: filters.searchTerm, mode: "insensitive" } },
{ initialMsg: { contains: filters.searchTerm, mode: "insensitive" } },
{ summary: { contains: filters.searchTerm, mode: "insensitive" } },
];
}
// Category filter
if (filters.category) {
whereClause.category = filters.category;
}
// Language filter
if (filters.language) {
whereClause.language = filters.language;
}
// Date range filter
if (filters.startDate || filters.endDate) {
whereClause.startTime = {};
if (filters.startDate) {
whereClause.startTime.gte = new Date(filters.startDate);
}
if (filters.endDate) {
// Make end date inclusive by adding one day
const inclusiveEndDate = new Date(filters.endDate);
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
whereClause.startTime.lt = inclusiveEndDate;
}
}
return whereClause;
}
/**
* Build order by clause for session sorting
*/
function buildOrderByClause(
filters: SessionQueryInput
):
| Prisma.SessionOrderByWithRelationInput
| Prisma.SessionOrderByWithRelationInput[] {
if (filters.sortKey === "startTime") {
return { startTime: filters.sortOrder };
}
// For non-time fields, add secondary sort by startTime
return [{ [filters.sortKey]: filters.sortOrder }, { startTime: "desc" }];
}
/**
* Convert Prisma session to ChatSession format
*/
function convertPrismaSessionToChatSession(ps: {
id: string;
companyId: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
updatedAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
}): ChatSession {
return {
id: ps.id,
sessionId: ps.id, // Using ID as sessionId for consistency
companyId: ps.companyId,
startTime: ps.startTime,
endTime: ps.endTime,
createdAt: ps.createdAt,
updatedAt: ps.updatedAt,
userId: null, // Not stored at session level
category: ps.category,
language: ps.language,
country: ps.country,
ipAddress: ps.ipAddress,
sentiment: ps.sentiment,
messagesSent: ps.messagesSent ?? undefined,
avgResponseTime: ps.avgResponseTime,
escalated: ps.escalated ?? undefined,
forwardedHr: ps.forwardedHr ?? undefined,
initialMsg: ps.initialMsg ?? undefined,
fullTranscriptUrl: ps.fullTranscriptUrl,
summary: ps.summary,
transcriptContent: null, // Not included in list view for performance
};
}
/**
* GET /api/dashboard/sessions
*
* Retrieve paginated list of sessions with filtering and sorting capabilities.
*
* Features:
* - Automatic authentication and company access validation
* - Input validation with Zod schemas
* - Consistent error handling and response format
* - Audit logging for security monitoring
* - Rate limiting protection
* - Pagination with metadata
*/
export const GET = createAuthenticatedHandler(
async (context, _, validatedQuery) => {
const filters = validatedQuery as SessionQueryInput;
// biome-ignore lint/style/noNonNullAssertion: pagination is guaranteed to exist when enablePagination is true
const { page, limit } = context.pagination!;
try {
// Validate company access (users can only see their company's sessions)
// biome-ignore lint/style/noNonNullAssertion: user is guaranteed to exist in authenticated handler
const companyId = context.user!.companyId;
// Build query conditions
const whereClause = buildWhereClause(companyId, filters);
const orderByClause = buildOrderByClause(filters);
// Execute queries in parallel for better performance
const [sessions, totalCount] = await Promise.all([
prisma.session.findMany({
where: whereClause,
orderBy: orderByClause,
skip: (page - 1) * limit,
take: limit,
// Only select needed fields for performance
select: {
id: true,
companyId: true,
startTime: true,
endTime: true,
createdAt: true,
updatedAt: true,
category: true,
language: true,
country: true,
ipAddress: true,
sentiment: true,
messagesSent: true,
avgResponseTime: true,
escalated: true,
forwardedHr: true,
initialMsg: true,
fullTranscriptUrl: true,
summary: true,
},
}),
prisma.session.count({ where: whereClause }),
]);
// Transform data
const transformedSessions: ChatSession[] = sessions.map(
convertPrismaSessionToChatSession
);
// Calculate pagination metadata
const paginationMeta = calculatePaginationMeta(page, limit, totalCount);
// Return paginated response with metadata
return createPaginatedResponse(transformedSessions, paginationMeta);
} catch (error) {
// Database errors are automatically handled by the error system
if (error instanceof Error) {
throw new DatabaseError("Failed to fetch sessions", {
// biome-ignore lint/style/noNonNullAssertion: user is guaranteed to exist in authenticated handler
companyId: context.user!.companyId,
filters,
error: error.message,
});
}
throw error;
}
},
{
// Configuration
validateQuery: SessionQuerySchema,
enablePagination: true,
auditLog: true,
rateLimit: {
maxRequests: 60, // 60 requests per window
windowMs: 60 * 1000, // 1 minute window
},
cacheControl: "private, max-age=30", // Cache for 30 seconds
}
);
/*
COMPARISON: Before vs After Refactoring
BEFORE (Original Implementation):
- ❌ Manual session authentication with repetitive code
- ❌ Inconsistent error responses: { error: "...", details: "..." }
- ❌ No input validation - accepts any query parameters
- ❌ No rate limiting protection
- ❌ No audit logging for security monitoring
- ❌ Manual pagination parameter extraction
- ❌ Inconsistent response format: { sessions, totalSessions }
- ❌ Basic error logging without context
- ❌ No company access validation
- ❌ Performance issue: sequential database queries
AFTER (Refactored with New Architecture):
- ✅ Automatic authentication via createAuthenticatedHandler middleware
- ✅ Standardized error responses with proper status codes and request IDs
- ✅ Strong input validation with Zod schemas and type safety
- ✅ Built-in rate limiting (60 req/min) with configurable limits
- ✅ Automatic audit logging for security compliance
- ✅ Automatic pagination handling via middleware
- ✅ Consistent API response format with metadata
- ✅ Comprehensive error handling with proper categorization
- ✅ Automatic company access validation for multi-tenant security
- ✅ Performance optimization: parallel database queries
BENEFITS:
1. **Consistency**: All endpoints follow the same patterns
2. **Security**: Built-in auth, rate limiting, audit logging, company isolation
3. **Maintainability**: Less boilerplate, centralized logic, type safety
4. **Performance**: Optimized queries, caching headers, parallel execution
5. **Developer Experience**: Better error messages, validation, debugging
6. **Scalability**: Standardized patterns that can be applied across all endpoints
MIGRATION STRATEGY:
1. Replace the original route.ts with this refactored version
2. Update any frontend code to expect the new response format
3. Test thoroughly to ensure backward compatibility where needed
4. Repeat this pattern for other endpoints
*/

View File

@ -1,24 +1,35 @@
import type { Prisma, SessionCategory } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import type { ChatSession } from "../../../../lib/types";
/**
* Build where clause for session filtering
*/
function buildWhereClause(
companyId: string,
searchParams: URLSearchParams
): Prisma.SessionWhereInput {
const whereClause: Prisma.SessionWhereInput = { companyId };
export async function GET(request: NextRequest) {
const authSession = await getServerSession(authOptions);
if (!authSession || !authSession.user?.companyId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const companyId = authSession.user.companyId;
const { searchParams } = new URL(request.url);
const searchTerm = searchParams.get("searchTerm");
const category = searchParams.get("category");
const language = searchParams.get("language");
const startDate = searchParams.get("startDate");
const endDate = searchParams.get("endDate");
const sortKey = searchParams.get("sortKey");
const sortOrder = searchParams.get("sortOrder");
const queryPage = searchParams.get("page");
const queryPageSize = searchParams.get("pageSize");
const page = Number(queryPage) || 1;
const pageSize = Number(queryPageSize) || 10;
try {
const whereClause: Prisma.SessionWhereInput = { companyId };
// Search Term
if (searchTerm && searchTerm.trim() !== "") {
@ -32,7 +43,8 @@ function buildWhereClause(
// Category Filter
if (category && category.trim() !== "") {
whereClause.category = category as SessionCategory;
// Cast to SessionCategory enum if it's a valid value
whereClause.category = category;
}
// Language Filter
@ -41,36 +53,22 @@ function buildWhereClause(
}
// Date Range Filter
const dateFilters: { gte?: Date; lt?: Date } = {};
if (startDate) {
dateFilters.gte = new Date(startDate);
whereClause.startTime = {
...((whereClause.startTime as object) || {}),
gte: new Date(startDate),
};
}
if (endDate) {
const inclusiveEndDate = new Date(endDate);
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
dateFilters.lt = inclusiveEndDate;
whereClause.startTime = {
...((whereClause.startTime as object) || {}),
lt: inclusiveEndDate,
};
}
if (Object.keys(dateFilters).length > 0) {
whereClause.startTime = dateFilters;
}
return whereClause;
}
/**
* Build order by clause for session sorting
*/
function buildOrderByClause(
searchParams: URLSearchParams
):
| Prisma.SessionOrderByWithRelationInput
| Prisma.SessionOrderByWithRelationInput[] {
const sortKey = searchParams.get("sortKey");
const sortOrder = searchParams.get("sortOrder");
// Sorting
const validSortKeys: { [key: string]: string } = {
startTime: "startTime",
category: "category",
@ -80,40 +78,37 @@ function buildOrderByClause(
avgResponseTime: "avgResponseTime",
};
let orderByCondition:
| Prisma.SessionOrderByWithRelationInput
| Prisma.SessionOrderByWithRelationInput[];
const primarySortField =
sortKey && validSortKeys[sortKey] ? validSortKeys[sortKey] : "startTime";
sortKey && validSortKeys[sortKey] ? validSortKeys[sortKey] : "startTime"; // Default to startTime field if sortKey is invalid/missing
const primarySortOrder =
sortOrder === "asc" || sortOrder === "desc" ? sortOrder : "desc";
sortOrder === "asc" || sortOrder === "desc" ? sortOrder : "desc"; // Default to desc order
if (primarySortField === "startTime") {
return { [primarySortField]: primarySortOrder };
// If sorting by startTime, it's the only sort criteria
orderByCondition = { [primarySortField]: primarySortOrder };
} else {
// If sorting by another field, use startTime: "desc" as secondary sort
orderByCondition = [
{ [primarySortField]: primarySortOrder },
{ startTime: "desc" },
];
}
return [{ [primarySortField]: primarySortOrder }, { startTime: "desc" }];
}
const prismaSessions = await prisma.session.findMany({
where: whereClause,
orderBy: orderByCondition,
skip: (page - 1) * pageSize,
take: pageSize,
});
/**
* Convert Prisma session to ChatSession format
*/
function convertPrismaSessionToChatSession(ps: {
id: string;
companyId: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
}): ChatSession {
return {
const totalSessions = await prisma.session.count({ where: whereClause });
const sessions: ChatSession[] = prismaSessions.map((ps) => ({
id: ps.id,
sessionId: ps.id,
companyId: ps.companyId,
@ -133,41 +128,8 @@ function convertPrismaSessionToChatSession(ps: {
forwardedHr: ps.forwardedHr ?? undefined,
initialMsg: ps.initialMsg ?? undefined,
fullTranscriptUrl: ps.fullTranscriptUrl ?? null,
transcriptContent: null,
};
}
export async function GET(request: NextRequest) {
const authSession = await getServerSession(authOptions);
if (!authSession || !authSession.user?.companyId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const companyId = authSession.user.companyId;
const { searchParams } = new URL(request.url);
const queryPage = searchParams.get("page");
const queryPageSize = searchParams.get("pageSize");
const page = Number(queryPage) || 1;
const pageSize = Number(queryPageSize) || 10;
try {
const whereClause = buildWhereClause(companyId, searchParams);
const orderByCondition = buildOrderByClause(searchParams);
const prismaSessions = await prisma.session.findMany({
where: whereClause,
orderBy: orderByCondition,
skip: (page - 1) * pageSize,
take: pageSize,
});
const totalSessions = await prisma.session.count({ where: whereClause });
const sessions: ChatSession[] = prismaSessions.map(
convertPrismaSessionToChatSession
);
transcriptContent: null, // Transcript content is now fetched from fullTranscriptUrl when needed
}));
return NextResponse.json({ sessions, totalSessions });
} catch (error) {

View File

@ -11,7 +11,7 @@ interface UserBasicInfo {
role: string;
}
export async function GET() {
export async function GET(_request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user || session.user.role !== "ADMIN") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
@ -27,8 +27,6 @@ export async function GET() {
const users = await prisma.user.findMany({
where: { companyId: user.companyId },
take: 1000, // Limit to prevent unbounded queries
orderBy: { createdAt: "desc" },
});
const mappedUsers: UserBasicInfo[] = users.map((u) => ({
@ -77,17 +75,6 @@ export async function POST(request: NextRequest) {
},
});
const { sendPasswordResetEmail } = await import("../../../../lib/sendEmail");
const emailResult = await sendPasswordResetEmail(email, tempPassword);
if (!emailResult.success) {
console.warn("Failed to send password email:", emailResult.error);
}
return NextResponse.json({
ok: true,
tempPassword,
emailSent: emailResult.success,
emailError: emailResult.error,
});
// TODO: Email user their temp password (stub, for demo) - Implement a robust and secure email sending mechanism. Consider using a transactional email service.
return NextResponse.json({ ok: true, tempPassword });
}

View File

@ -1,46 +1,38 @@
import crypto from "node:crypto";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/prisma";
import { extractClientIP, InMemoryRateLimiter } from "../../../lib/rateLimiter";
import {
AuditOutcome,
createAuditMetadata,
securityAuditLogger,
} from "../../../lib/securityAuditLogger";
import { sendEmail } from "../../../lib/sendEmail";
import { forgotPasswordSchema, validateInput } from "../../../lib/validation";
// Rate limiting for password reset endpoint
const passwordResetLimiter = new InMemoryRateLimiter({
maxAttempts: 5,
windowMs: 15 * 60 * 1000, // 15 minutes
maxEntries: 10000,
cleanupIntervalMs: 5 * 60 * 1000, // 5 minutes
});
// In-memory rate limiting for password reset requests
const resetAttempts = new Map<string, { count: number; resetTime: number }>();
function checkRateLimit(ip: string): boolean {
const now = Date.now();
const attempts = resetAttempts.get(ip);
if (!attempts || now > attempts.resetTime) {
resetAttempts.set(ip, { count: 1, resetTime: now + 15 * 60 * 1000 }); // 15 minute window
return true;
}
if (attempts.count >= 5) {
// Max 5 reset requests per 15 minutes per IP
return false;
}
attempts.count++;
return true;
}
export async function POST(request: NextRequest) {
try {
// Rate limiting check using shared utility
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
const rateLimitResult = passwordResetLimiter.checkRateLimit(ip);
if (!rateLimitResult.allowed) {
await securityAuditLogger.logPasswordReset(
"password_reset_rate_limited",
AuditOutcome.RATE_LIMITED,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
resetTime: rateLimitResult.resetTime,
maxAttempts: 5,
windowMs: 15 * 60 * 1000,
}),
},
"Password reset rate limit exceeded"
);
// Rate limiting check
const ip =
request.headers.get("x-forwarded-for") ||
request.headers.get("x-real-ip") ||
"unknown";
if (!checkRateLimit(ip)) {
return NextResponse.json(
{
success: false,
@ -55,19 +47,6 @@ export async function POST(request: NextRequest) {
// Validate input
const validation = validateInput(forgotPasswordSchema, body);
if (!validation.success) {
await securityAuditLogger.logPasswordReset(
"password_reset_invalid_input",
AuditOutcome.FAILURE,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "invalid_email_format",
}),
},
"Invalid email format in password reset request"
);
return NextResponse.json(
{
success: false,
@ -94,60 +73,16 @@ export async function POST(request: NextRequest) {
});
const resetUrl = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`;
await sendEmail({
to: email,
subject: "Password Reset",
text: `Reset your password: ${resetUrl}`,
});
await securityAuditLogger.logPasswordReset(
"password_reset_email_sent",
AuditOutcome.SUCCESS,
{
userId: user.id,
companyId: user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
email: "[REDACTED]",
tokenExpiry: expiry.toISOString(),
}),
},
"Password reset email sent successfully"
);
} else {
// Log attempt for non-existent user
await securityAuditLogger.logPasswordReset(
"password_reset_user_not_found",
AuditOutcome.FAILURE,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
email: "[REDACTED]",
}),
},
"Password reset attempt for non-existent user"
await sendEmail(
email,
"Password Reset",
`Reset your password: ${resetUrl}`
);
}
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Forgot password error:", error);
await securityAuditLogger.logPasswordReset(
"password_reset_server_error",
AuditOutcome.FAILURE,
{
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error in password reset: ${error}`
);
return NextResponse.json(
{
success: false,

View File

@ -51,36 +51,20 @@ export async function POST(
);
}
// Check if user already exists (emails must be globally unique)
const existingUser = await prisma.user.findUnique({
// Check if user already exists in this company
const existingUser = await prisma.user.findFirst({
where: {
email,
},
select: {
id: true,
companyId: true,
company: {
select: {
name: true,
},
},
companyId,
},
});
if (existingUser) {
if (existingUser.companyId === companyId) {
return NextResponse.json(
{ error: "User already exists in this company" },
{ status: 400 }
);
}
return NextResponse.json(
{
error: `Email already in use by a user in company: ${existingUser.company.name}. Each email address can only be used once across all companies.`,
},
{ status: 400 }
);
}
// Generate a temporary password (in a real app, you'd send an invitation email)
const tempPassword = `temp${Math.random().toString(36).slice(-8)}`;

View File

@ -1,38 +1,15 @@
import type { CompanyStatus } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession, type Session } from "next-auth";
import { getServerSession } from "next-auth";
import { platformAuthOptions } from "../../../../lib/platform-auth";
import { prisma } from "../../../../lib/prisma";
import { extractClientIP } from "../../../../lib/rateLimiter";
import {
AuditOutcome,
createAuditMetadata,
securityAuditLogger,
} from "../../../../lib/securityAuditLogger";
// GET /api/platform/companies - List all companies
export async function GET(request: NextRequest) {
let session: Session | null = null;
try {
session = await getServerSession(platformAuthOptions);
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
const session = await getServerSession(platformAuthOptions);
if (!session?.user?.isPlatformUser) {
await securityAuditLogger.logPlatformAdmin(
"platform_companies_unauthorized_access",
AuditOutcome.BLOCKED,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "no_platform_session",
}),
},
"Unauthorized attempt to access platform companies list"
);
return NextResponse.json(
{ error: "Platform access required" },
{ status: 401 }
@ -86,24 +63,6 @@ export async function GET(request: NextRequest) {
prisma.company.count({ where }),
]);
// Log successful platform companies access
await securityAuditLogger.logPlatformAdmin(
"platform_companies_list_accessed",
AuditOutcome.SUCCESS,
{
platformUserId: session.user.id,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
companiesReturned: companies.length,
totalCompanies: total,
filters: { status, search },
pagination: { page, limit },
}),
},
"Platform companies list accessed"
);
return NextResponse.json({
companies,
pagination: {
@ -115,21 +74,6 @@ export async function GET(request: NextRequest) {
});
} catch (error) {
console.error("Platform companies list error:", error);
await securityAuditLogger.logPlatformAdmin(
"platform_companies_list_error",
AuditOutcome.FAILURE,
{
platformUserId: session?.user?.id,
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error in platform companies list: ${error}`
);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
@ -139,33 +83,13 @@ export async function GET(request: NextRequest) {
// POST /api/platform/companies - Create new company
export async function POST(request: NextRequest) {
let session: Session | null = null;
try {
session = await getServerSession(platformAuthOptions);
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
const session = await getServerSession(platformAuthOptions);
if (
!session?.user?.isPlatformUser ||
session.user.platformRole === "SUPPORT"
) {
await securityAuditLogger.logPlatformAdmin(
"platform_company_create_unauthorized",
AuditOutcome.BLOCKED,
{
platformUserId: session?.user?.id,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "insufficient_permissions",
requiredRole: "ADMIN",
currentRole: session?.user?.platformRole,
}),
},
"Unauthorized attempt to create platform company"
);
return NextResponse.json(
{ error: "Admin access required" },
{ status: 403 }
@ -229,7 +153,7 @@ export async function POST(request: NextRequest) {
name: adminName,
role: "ADMIN",
companyId: company.id,
invitedBy: session?.user?.email || "platform",
invitedBy: session.user.email || "platform",
invitedAt: new Date(),
},
});
@ -241,27 +165,6 @@ export async function POST(request: NextRequest) {
};
});
// Log successful company creation
await securityAuditLogger.logCompanyManagement(
"platform_company_created",
AuditOutcome.SUCCESS,
{
platformUserId: session.user.id,
companyId: result.company.id,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
companyName: result.company.name,
companyStatus: result.company.status,
adminUserEmail: "[REDACTED]",
adminUserName: result.adminUser.name,
maxUsers: result.company.maxUsers,
hasGeneratedPassword: !!result.generatedPassword,
}),
},
"Platform company created successfully"
);
return NextResponse.json(
{
company: result.company,
@ -276,21 +179,6 @@ export async function POST(request: NextRequest) {
);
} catch (error) {
console.error("Platform company creation error:", error);
await securityAuditLogger.logCompanyManagement(
"platform_company_create_error",
AuditOutcome.FAILURE,
{
platformUserId: session?.user?.id,
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error in platform company creation: ${error}`
);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }

View File

@ -1,24 +1,38 @@
import bcrypt from "bcryptjs";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/prisma";
import { extractClientIP, InMemoryRateLimiter } from "../../../lib/rateLimiter";
import { registerSchema, validateInput } from "../../../lib/validation";
// Rate limiting for registration endpoint
const registrationLimiter = new InMemoryRateLimiter({
maxAttempts: 3,
windowMs: 60 * 60 * 1000, // 1 hour
maxEntries: 10000,
cleanupIntervalMs: 5 * 60 * 1000, // 5 minutes
});
// In-memory rate limiting (for production, use Redis or similar)
const registrationAttempts = new Map<
string,
{ count: number; resetTime: number }
>();
function checkRateLimit(ip: string): boolean {
const now = Date.now();
const attempts = registrationAttempts.get(ip);
if (!attempts || now > attempts.resetTime) {
registrationAttempts.set(ip, { count: 1, resetTime: now + 60 * 60 * 1000 }); // 1 hour window
return true;
}
if (attempts.count >= 3) {
// Max 3 registrations per hour per IP
return false;
}
attempts.count++;
return true;
}
export async function POST(request: NextRequest) {
try {
// Rate limiting check using shared utility
const ip = extractClientIP(request);
const rateLimitResult = registrationLimiter.checkRateLimit(ip);
if (!rateLimitResult.allowed) {
// Rate limiting check
const ip =
request.ip || request.headers.get("x-forwarded-for") || "unknown";
if (!checkRateLimit(ip)) {
return NextResponse.json(
{
success: false,

View File

@ -2,37 +2,15 @@ import crypto from "node:crypto";
import bcrypt from "bcryptjs";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/prisma";
import { extractClientIP } from "../../../lib/rateLimiter";
import {
AuditOutcome,
createAuditMetadata,
securityAuditLogger,
} from "../../../lib/securityAuditLogger";
import { resetPasswordSchema, validateInput } from "../../../lib/validation";
export async function POST(request: NextRequest) {
try {
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
const body = await request.json();
// Validate input with strong password requirements
const validation = validateInput(resetPasswordSchema, body);
if (!validation.success) {
await securityAuditLogger.logPasswordReset(
"password_reset_validation_failed",
AuditOutcome.FAILURE,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "validation_failed",
validationErrors: validation.errors,
}),
},
"Password reset validation failed"
);
return NextResponse.json(
{
success: false,
@ -56,19 +34,6 @@ export async function POST(request: NextRequest) {
});
if (!user) {
await securityAuditLogger.logPasswordReset(
"password_reset_invalid_token",
AuditOutcome.FAILURE,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "invalid_or_expired_token",
}),
},
"Password reset attempt with invalid or expired token"
);
return NextResponse.json(
{
success: false,
@ -91,22 +56,6 @@ export async function POST(request: NextRequest) {
},
});
await securityAuditLogger.logPasswordReset(
"password_reset_completed",
AuditOutcome.SUCCESS,
{
userId: user.id,
companyId: user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
email: "[REDACTED]",
passwordChanged: true,
}),
},
"Password reset completed successfully"
);
return NextResponse.json(
{
success: true,
@ -116,20 +65,6 @@ export async function POST(request: NextRequest) {
);
} catch (error) {
console.error("Reset password error:", error);
await securityAuditLogger.logPasswordReset(
"password_reset_server_error",
AuditOutcome.FAILURE,
{
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error in password reset completion: ${error}`
);
return NextResponse.json(
{
success: false,

View File

@ -1,29 +0,0 @@
/**
* tRPC API Route Handler
*
* This file creates the Next.js API route that handles all tRPC requests.
* All tRPC procedures will be accessible via /api/trpc/*
*/
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import type { NextRequest } from "next/server";
import { createTRPCContext } from "@/lib/trpc";
import { appRouter } from "@/server/routers/_app";
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: createTRPCContext,
onError:
process.env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
);
}
: undefined,
});
export { handler as GET, handler as POST };

View File

@ -1,610 +0,0 @@
"use client";
import { formatDistanceToNow } from "date-fns";
import { useSession } from "next-auth/react";
import { useCallback, useEffect, useId, useState } from "react";
import { Alert, AlertDescription } from "../../../components/ui/alert";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { Input } from "../../../components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../../components/ui/table";
interface AuditLog {
id: string;
eventType: string;
action: string;
outcome: string;
severity: string;
userId?: string;
platformUserId?: string;
ipAddress?: string;
userAgent?: string;
country?: string;
metadata?: Record<string, unknown>;
errorMessage?: string;
sessionId?: string;
requestId?: string;
timestamp: string;
user?: {
id: string;
email: string;
name?: string;
role: string;
};
platformUser?: {
id: string;
email: string;
name?: string;
role: string;
};
}
interface AuditLogsResponse {
success: boolean;
data?: {
auditLogs: AuditLog[];
pagination: {
page: number;
limit: number;
totalCount: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
};
error?: string;
}
const eventTypeLabels: Record<string, string> = {
AUTHENTICATION: "Authentication",
AUTHORIZATION: "Authorization",
USER_MANAGEMENT: "User Management",
COMPANY_MANAGEMENT: "Company Management",
RATE_LIMITING: "Rate Limiting",
CSRF_PROTECTION: "CSRF Protection",
SECURITY_HEADERS: "Security Headers",
PASSWORD_RESET: "Password Reset",
PLATFORM_ADMIN: "Platform Admin",
DATA_PRIVACY: "Data Privacy",
SYSTEM_CONFIG: "System Config",
API_SECURITY: "API Security",
};
const outcomeColors: Record<string, string> = {
SUCCESS: "bg-green-100 text-green-800",
FAILURE: "bg-red-100 text-red-800",
BLOCKED: "bg-orange-100 text-orange-800",
RATE_LIMITED: "bg-yellow-100 text-yellow-800",
SUSPICIOUS: "bg-purple-100 text-purple-800",
};
const severityColors: Record<string, string> = {
INFO: "bg-blue-100 text-blue-800",
LOW: "bg-gray-100 text-gray-800",
MEDIUM: "bg-yellow-100 text-yellow-800",
HIGH: "bg-orange-100 text-orange-800",
CRITICAL: "bg-red-100 text-red-800",
};
export default function AuditLogsPage() {
const { data: session } = useSession();
const eventTypeId = useId();
const outcomeId = useId();
const severityId = useId();
const startDateId = useId();
const endDateId = useId();
const modalTitleId = useId();
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] = useState({
page: 1,
limit: 50,
totalCount: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
});
// Filter states
const [filters, setFilters] = useState({
eventType: "",
outcome: "",
severity: "",
userId: "",
startDate: "",
endDate: "",
});
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const [hasFetched, setHasFetched] = useState(false);
const fetchAuditLogs = useCallback(async () => {
if (hasFetched) return;
try {
setLoading(true);
const params = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
...filters,
});
Object.keys(filters).forEach((key) => {
if (!filters[key as keyof typeof filters]) {
params.delete(key);
}
});
const response = await fetch(
`/api/admin/audit-logs?${params.toString()}`
);
const data: AuditLogsResponse = await response.json();
if (data.success && data.data) {
setAuditLogs(data.data.auditLogs);
setPagination(data.data.pagination);
setError(null);
setHasFetched(true);
} else {
setError(data.error || "Failed to fetch audit logs");
}
} catch (err) {
setError("An error occurred while fetching audit logs");
console.error("Audit logs fetch error:", err);
} finally {
setLoading(false);
}
}, [pagination.page, pagination.limit, filters, hasFetched]);
useEffect(() => {
if (session?.user?.role === "ADMIN" && !hasFetched) {
fetchAuditLogs();
}
}, [session?.user?.role, hasFetched, fetchAuditLogs]);
// Function to refresh audit logs (for filter changes)
const refreshAuditLogs = useCallback((newPage?: number) => {
if (newPage !== undefined) {
setPagination((prev) => ({ ...prev, page: newPage }));
}
setHasFetched(false);
}, []);
const handleFilterChange = (key: keyof typeof filters, value: string) => {
setFilters((prev) => ({ ...prev, [key]: value }));
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
refreshAuditLogs(); // Trigger fresh fetch with new filters
};
const clearFilters = () => {
setFilters({
eventType: "",
outcome: "",
severity: "",
userId: "",
startDate: "",
endDate: "",
});
refreshAuditLogs(); // Trigger fresh fetch with cleared filters
};
if (session?.user?.role !== "ADMIN") {
return (
<div className="container mx-auto py-8">
<Alert>
<AlertDescription>
You don&apos;t have permission to view audit logs. Only
administrators can access this page.
</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto py-8 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">Security Audit Logs</h1>
<Button onClick={fetchAuditLogs} disabled={loading}>
{loading ? "Loading..." : "Refresh"}
</Button>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle>Filters</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label htmlFor={eventTypeId} className="text-sm font-medium">
Event Type
</label>
<Select
value={filters.eventType}
onValueChange={(value) =>
handleFilterChange("eventType", value)
}
>
<SelectTrigger id={eventTypeId}>
<SelectValue placeholder="All event types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All event types</SelectItem>
{Object.entries(eventTypeLabels).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label htmlFor={outcomeId} className="text-sm font-medium">
Outcome
</label>
<Select
value={filters.outcome}
onValueChange={(value) => handleFilterChange("outcome", value)}
>
<SelectTrigger id={outcomeId}>
<SelectValue placeholder="All outcomes" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All outcomes</SelectItem>
<SelectItem value="SUCCESS">Success</SelectItem>
<SelectItem value="FAILURE">Failure</SelectItem>
<SelectItem value="BLOCKED">Blocked</SelectItem>
<SelectItem value="RATE_LIMITED">Rate Limited</SelectItem>
<SelectItem value="SUSPICIOUS">Suspicious</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label htmlFor={severityId} className="text-sm font-medium">
Severity
</label>
<Select
value={filters.severity}
onValueChange={(value) => handleFilterChange("severity", value)}
>
<SelectTrigger id={severityId}>
<SelectValue placeholder="All severities" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All severities</SelectItem>
<SelectItem value="INFO">Info</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="CRITICAL">Critical</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label htmlFor={startDateId} className="text-sm font-medium">
Start Date
</label>
<Input
id={startDateId}
type="datetime-local"
value={filters.startDate}
onChange={(e) =>
handleFilterChange("startDate", e.target.value)
}
/>
</div>
<div>
<label htmlFor={endDateId} className="text-sm font-medium">
End Date
</label>
<Input
id={endDateId}
type="datetime-local"
value={filters.endDate}
onChange={(e) => handleFilterChange("endDate", e.target.value)}
/>
</div>
<div className="flex items-end">
<Button variant="outline" onClick={clearFilters}>
Clear Filters
</Button>
</div>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Audit Logs Table */}
<Card>
<CardHeader>
<CardTitle>Audit Logs ({pagination.totalCount} total)</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Timestamp</TableHead>
<TableHead>Event Type</TableHead>
<TableHead>Action</TableHead>
<TableHead>Outcome</TableHead>
<TableHead>Severity</TableHead>
<TableHead>User</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Details</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{auditLogs.map((log) => (
<TableRow
key={log.id}
className="cursor-pointer hover:bg-gray-50 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset"
onClick={() => setSelectedLog(log)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setSelectedLog(log);
}
}}
tabIndex={0}
aria-label={`View details for ${eventTypeLabels[log.eventType] || log.eventType} event`}
>
<TableCell className="font-mono text-sm">
{formatDistanceToNow(new Date(log.timestamp), {
addSuffix: true,
})}
</TableCell>
<TableCell>
<Badge variant="outline">
{eventTypeLabels[log.eventType] || log.eventType}
</Badge>
</TableCell>
<TableCell className="max-w-48 truncate">
{log.action}
</TableCell>
<TableCell>
<Badge
className={
outcomeColors[log.outcome] ||
"bg-gray-100 text-gray-800"
}
>
{log.outcome}
</Badge>
</TableCell>
<TableCell>
<Badge
className={
severityColors[log.severity] ||
"bg-gray-100 text-gray-800"
}
>
{log.severity}
</Badge>
</TableCell>
<TableCell>
{log.user?.email || log.platformUser?.email || "System"}
</TableCell>
<TableCell className="font-mono text-sm">
{log.ipAddress || "N/A"}
</TableCell>
<TableCell>
<Button variant="ghost" size="sm">
View
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-600">
Showing {(pagination.page - 1) * pagination.limit + 1} to{" "}
{Math.min(
pagination.page * pagination.limit,
pagination.totalCount
)}{" "}
of {pagination.totalCount} results
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={!pagination.hasPrev}
onClick={() => {
const newPage = pagination.page - 1;
refreshAuditLogs(newPage);
}}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={!pagination.hasNext}
onClick={() => {
const newPage = pagination.page + 1;
refreshAuditLogs(newPage);
}}
>
Next
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Log Detail Modal */}
{selectedLog && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-labelledby={modalTitleId}
onKeyDown={(e) => {
if (e.key === "Escape") {
setSelectedLog(null);
}
}}
>
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 id={modalTitleId} className="text-xl font-bold">
Audit Log Details
</h2>
<Button variant="ghost" onClick={() => setSelectedLog(null)}>
×
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<span className="font-medium">Timestamp:</span>
<p className="font-mono text-sm">
{new Date(selectedLog.timestamp).toLocaleString()}
</p>
</div>
<div>
<span className="font-medium">Event Type:</span>
<p>
{eventTypeLabels[selectedLog.eventType] ||
selectedLog.eventType}
</p>
</div>
<div>
<span className="font-medium">Action:</span>
<p>{selectedLog.action}</p>
</div>
<div>
<span className="font-medium">Outcome:</span>
<Badge className={outcomeColors[selectedLog.outcome]}>
{selectedLog.outcome}
</Badge>
</div>
<div>
<span className="font-medium">Severity:</span>
<Badge className={severityColors[selectedLog.severity]}>
{selectedLog.severity}
</Badge>
</div>
<div>
<span className="font-medium">IP Address:</span>
<p className="font-mono text-sm">
{selectedLog.ipAddress || "N/A"}
</p>
</div>
{selectedLog.user && (
<div>
<span className="font-medium">User:</span>
<p>
{selectedLog.user.email} ({selectedLog.user.role})
</p>
</div>
)}
{selectedLog.platformUser && (
<div>
<span className="font-medium">Platform User:</span>
<p>
{selectedLog.platformUser.email} (
{selectedLog.platformUser.role})
</p>
</div>
)}
{selectedLog.country && (
<div>
<span className="font-medium">Country:</span>
<p>{selectedLog.country}</p>
</div>
)}
{selectedLog.sessionId && (
<div>
<span className="font-medium">Session ID:</span>
<p className="font-mono text-sm">{selectedLog.sessionId}</p>
</div>
)}
{selectedLog.requestId && (
<div>
<span className="font-medium">Request ID:</span>
<p className="font-mono text-sm">{selectedLog.requestId}</p>
</div>
)}
</div>
{selectedLog.errorMessage && (
<div className="mt-4">
<span className="font-medium">Error Message:</span>
<p className="text-red-600 bg-red-50 p-2 rounded text-sm">
{selectedLog.errorMessage}
</p>
</div>
)}
{selectedLog.userAgent && (
<div className="mt-4">
<span className="font-medium">User Agent:</span>
<p className="text-sm break-all">{selectedLog.userAgent}</p>
</div>
)}
{selectedLog.metadata && (
<div className="mt-4">
<span className="font-medium">Metadata:</span>
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto max-h-40">
{JSON.stringify(selectedLog.metadata, null, 2)}
</pre>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -15,7 +15,9 @@ export default function CompanySettingsPage() {
const csvUsernameId = useId();
const csvPasswordId = useId();
const { data: session, status } = useSession();
const [, setCompany] = useState<Company | null>(null);
// We store the full company object for future use and updates after save operations
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const [_company, setCompany] = useState<Company | null>(null);
const [csvUrl, setCsvUrl] = useState<string>("");
const [csvUsername, setCsvUsername] = useState<string>("");
const [csvPassword, setCsvPassword] = useState<string>("");

View File

@ -28,7 +28,6 @@ import {
} from "@/components/ui/dropdown-menu";
import { Skeleton } from "@/components/ui/skeleton";
import { formatEnumValue } from "@/lib/format-enums";
import { trpc } from "@/lib/trpc-client";
import ModernBarChart from "../../../components/charts/bar-chart";
import ModernDonutChart from "../../../components/charts/donut-chart";
import ModernLineChart from "../../../components/charts/line-chart";
@ -39,10 +38,92 @@ import MetricCard from "../../../components/ui/metric-card";
import WordCloud from "../../../components/WordCloud";
import type { Company, MetricsResult, WordCloudWord } from "../../../lib/types";
/**
* Loading states component for better organization
*/
function DashboardLoadingStates({ status }: { status: string }) {
// Safely wrapped component with useSession
function DashboardContent() {
const { data: session, status } = useSession();
const router = useRouter();
const [metrics, setMetrics] = useState<MetricsResult | null>(null);
const [company, setCompany] = useState<Company | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false);
const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true);
const refreshStatusId = useId();
const isAuditor = session?.user?.role === "AUDITOR";
// Function to fetch metrics with optional date range
const fetchMetrics = useCallback(
async (startDate?: string, endDate?: string, isInitial = false) => {
setLoading(true);
try {
let url = "/api/dashboard/metrics";
if (startDate && endDate) {
url += `?startDate=${startDate}&endDate=${endDate}`;
}
const res = await fetch(url);
const data = await res.json();
setMetrics(data.metrics);
setCompany(data.company);
// Set initial load flag
if (isInitial) {
setIsInitialLoad(false);
}
} catch (error) {
console.error("Error fetching metrics:", error);
} finally {
setLoading(false);
}
},
[]
);
useEffect(() => {
// Redirect if not authenticated
if (status === "unauthenticated") {
router.push("/login");
return;
}
// Fetch metrics and company on mount if authenticated
if (status === "authenticated" && isInitialLoad) {
fetchMetrics(undefined, undefined, true);
}
}, [status, router, isInitialLoad, fetchMetrics]);
async function handleRefresh() {
if (isAuditor) return;
try {
setRefreshing(true);
if (!company?.id) {
setRefreshing(false);
alert("Cannot refresh: Company ID is missing");
return;
}
const res = await fetch("/api/admin/refresh-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ companyId: company.id }),
});
if (res.ok) {
const metricsRes = await fetch("/api/dashboard/metrics");
const data = await metricsRes.json();
setMetrics(data.metrics);
} else {
const errorData = await res.json();
alert(`Failed to refresh sessions: ${errorData.error}`);
}
} finally {
setRefreshing(false);
}
}
// Show loading state while session status is being determined
if (status === "loading") {
return (
<div className="flex items-center justify-center min-h-[60vh]">
@ -64,13 +145,7 @@ function DashboardLoadingStates({ status }: { status: string }) {
);
}
return null;
}
/**
* Loading skeleton component
*/
function DashboardSkeleton() {
if (loading || !metrics || !company) {
return (
<div className="space-y-8">
{/* Header Skeleton */}
@ -134,13 +209,10 @@ function DashboardSkeleton() {
</div>
</div>
);
}
}
/**
* Data processing utilities
*/
function useDashboardData(metrics: MetricsResult | null) {
const getSentimentData = useCallback(() => {
// Data preparation functions
const getSentimentData = () => {
if (!metrics) return [];
const sentimentData = {
@ -166,9 +238,9 @@ function useDashboardData(metrics: MetricsResult | null) {
color: "hsl(var(--chart-3))",
},
];
}, [metrics]);
};
const getSessionsOverTimeData = useCallback(() => {
const getSessionsOverTimeData = () => {
if (!metrics?.days) return [];
return Object.entries(metrics.days).map(([date, value]) => ({
@ -178,9 +250,9 @@ function useDashboardData(metrics: MetricsResult | null) {
}),
value: value as number,
}));
}, [metrics?.days]);
};
const getCategoriesData = useCallback(() => {
const getCategoriesData = () => {
if (!metrics?.categories) return [];
return Object.entries(metrics.categories).map(([name, value]) => {
@ -193,23 +265,23 @@ function useDashboardData(metrics: MetricsResult | null) {
value: value as number,
};
});
}, [metrics?.categories]);
};
const getLanguagesData = useCallback(() => {
const getLanguagesData = () => {
if (!metrics?.languages) return [];
return Object.entries(metrics.languages).map(([name, value]) => ({
name,
value: value as number,
}));
}, [metrics?.languages]);
};
const getWordCloudData = useCallback((): WordCloudWord[] => {
const getWordCloudData = (): WordCloudWord[] => {
if (!metrics?.wordCloudData) return [];
return metrics.wordCloudData;
}, [metrics?.wordCloudData]);
};
const getCountryData = useCallback(() => {
const getCountryData = () => {
if (!metrics?.countries) return {};
return Object.entries(metrics.countries).reduce(
(acc, [code, count]) => {
@ -220,10 +292,10 @@ function useDashboardData(metrics: MetricsResult | null) {
},
{} as Record<string, number>
);
}, [metrics?.countries]);
};
const getResponseTimeData = useCallback(() => {
const avgTime = metrics?.avgResponseTime || 1.5;
const getResponseTimeData = () => {
const avgTime = metrics.avgResponseTime || 1.5;
const simulatedData: number[] = [];
for (let i = 0; i < 50; i++) {
@ -232,38 +304,11 @@ function useDashboardData(metrics: MetricsResult | null) {
}
return simulatedData;
}, [metrics?.avgResponseTime]);
return {
getSentimentData,
getSessionsOverTimeData,
getCategoriesData,
getLanguagesData,
getWordCloudData,
getCountryData,
getResponseTimeData,
};
}
/**
* Dashboard header component
*/
function DashboardHeader({
company,
metrics,
isAuditor,
refreshing,
onRefresh,
}: {
company: Company;
metrics: MetricsResult;
isAuditor: boolean;
refreshing: boolean;
onRefresh: () => void;
}) {
const refreshStatusId = useId();
return (
<div className="space-y-8">
{/* Modern Header */}
<Card className="border-0 bg-linear-to-r from-primary/5 via-primary/10 to-primary/5">
<CardHeader>
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
@ -286,7 +331,7 @@ function DashboardHeader({
<div className="flex items-center gap-2">
<Button
onClick={onRefresh}
onClick={handleRefresh}
disabled={refreshing || isAuditor}
size="sm"
className="gap-2"
@ -304,7 +349,11 @@ function DashboardHeader({
{refreshing ? "Refreshing..." : "Refresh"}
</Button>
{refreshing && (
<div id={refreshStatusId} className="sr-only" aria-live="polite">
<div
id={refreshStatusId}
className="sr-only"
aria-live="polite"
>
Dashboard data is being refreshed
</div>
)}
@ -328,332 +377,8 @@ function DashboardHeader({
</div>
</CardHeader>
</Card>
);
}
/**
* Individual metric card components for better organization
*/
function SessionMetricCard({ metrics }: { metrics: MetricsResult }) {
return (
<MetricCard
title="Total Sessions"
value={metrics.totalSessions?.toLocaleString()}
icon={<MessageSquare className="h-5 w-5" />}
trend={{
value: metrics.sessionTrend ?? 0,
isPositive: (metrics.sessionTrend ?? 0) >= 0,
}}
variant="primary"
/>
);
}
function UsersMetricCard({ metrics }: { metrics: MetricsResult }) {
return (
<MetricCard
title="Unique Users"
value={metrics.uniqueUsers?.toLocaleString()}
icon={<Users className="h-5 w-5" />}
trend={{
value: metrics.usersTrend ?? 0,
isPositive: (metrics.usersTrend ?? 0) >= 0,
}}
variant="success"
/>
);
}
function SessionTimeMetricCard({ metrics }: { metrics: MetricsResult }) {
return (
<MetricCard
title="Avg. Session Time"
value={`${Math.round(metrics.avgSessionLength || 0)}s`}
icon={<Clock className="h-5 w-5" />}
trend={{
value: metrics.avgSessionTimeTrend ?? 0,
isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0,
}}
/>
);
}
function ResponseTimeMetricCard({ metrics }: { metrics: MetricsResult }) {
return (
<MetricCard
title="Avg. Response Time"
value={`${metrics.avgResponseTime?.toFixed(1) || 0}s`}
icon={<Zap className="h-5 w-5" />}
trend={{
value: metrics.avgResponseTimeTrend ?? 0,
isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0,
}}
variant="warning"
/>
);
}
function CostsMetricCard({ metrics }: { metrics: MetricsResult }) {
return (
<MetricCard
title="Daily Costs"
value={`${metrics.avgDailyCosts?.toFixed(4) || "0.0000"}`}
icon={<Euro className="h-5 w-5" />}
description="Average per day"
/>
);
}
function PeakUsageMetricCard({ metrics }: { metrics: MetricsResult }) {
return (
<MetricCard
title="Peak Usage"
value={metrics.peakUsageTime || "N/A"}
icon={<TrendingUp className="h-5 w-5" />}
description="Busiest hour"
/>
);
}
function ResolutionRateMetricCard({ metrics }: { metrics: MetricsResult }) {
return (
<MetricCard
title="Resolution Rate"
value={`${metrics.resolvedChatsPercentage?.toFixed(1) || "0.0"}%`}
icon={<CheckCircle className="h-5 w-5" />}
trend={{
value: metrics.resolvedChatsPercentage ?? 0,
isPositive: (metrics.resolvedChatsPercentage ?? 0) >= 80,
}}
variant={
metrics.resolvedChatsPercentage && metrics.resolvedChatsPercentage >= 80
? "success"
: "warning"
}
/>
);
}
function LanguagesMetricCard({ metrics }: { metrics: MetricsResult }) {
return (
<MetricCard
title="Active Languages"
value={Object.keys(metrics.languages || {}).length}
icon={<Globe className="h-5 w-5" />}
description="Languages detected"
/>
);
}
/**
* Simplified metrics grid component
*/
function MetricsGrid({ metrics }: { metrics: MetricsResult }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<SessionMetricCard metrics={metrics} />
<UsersMetricCard metrics={metrics} />
<SessionTimeMetricCard metrics={metrics} />
<ResponseTimeMetricCard metrics={metrics} />
<CostsMetricCard metrics={metrics} />
<PeakUsageMetricCard metrics={metrics} />
<ResolutionRateMetricCard metrics={metrics} />
<LanguagesMetricCard metrics={metrics} />
</div>
);
}
/**
* Main dashboard content with reduced complexity
*/
function DashboardContent() {
const { data: session, status } = useSession();
const router = useRouter();
const [metrics, setMetrics] = useState<MetricsResult | null>(null);
// Remove unused company state that was causing skeleton view to always show
const [refreshing, setRefreshing] = useState<boolean>(false);
const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true);
const isAuditor = session?.user?.role === "AUDITOR";
const dataHelpers = useDashboardData(metrics);
// Function to fetch metrics with optional date range
// tRPC query for dashboard metrics
const {
data: overviewData,
isLoading: isLoadingMetrics,
refetch: refetchMetrics,
error: metricsError,
} = trpc.dashboard.getOverview.useQuery(
{
// Add date range parameters when implemented
// startDate: dateRange?.startDate,
// endDate: dateRange?.endDate,
},
{
enabled: status === "authenticated",
}
);
// Update state when data changes
useEffect(() => {
if (overviewData) {
// Map overview data to metrics format expected by the component
const mappedMetrics: Partial<MetricsResult> = {
totalSessions: overviewData.totalSessions,
avgSessionsPerDay: overviewData.avgSessionsPerDay || 0,
avgSessionLength: overviewData.avgSessionLength || 0,
days:
overviewData.timeSeriesData?.reduce(
(acc, item) => {
if (item.date) {
acc[item.date] = item.sessionCount || 0;
}
return acc;
},
{} as Record<string, number>
) || {},
languages:
overviewData.languageDistribution?.reduce(
(acc, item) => {
if (item.language) {
acc[item.language] = item.count;
}
return acc;
},
{} as Record<string, number>
) || {},
countries:
overviewData.geographicDistribution?.reduce(
(acc, item) => {
if (item.country) {
acc[item.country] = item.count;
}
return acc;
},
{} as Record<string, number>
) || {},
belowThresholdCount: overviewData.belowThresholdCount || 0,
// Map sentiment data to individual counts
sentimentPositiveCount:
overviewData.sentimentDistribution?.find(
(s) => s.sentiment === "POSITIVE"
)?.count || 0,
sentimentNeutralCount:
overviewData.sentimentDistribution?.find(
(s) => s.sentiment === "NEUTRAL"
)?.count || 0,
sentimentNegativeCount:
overviewData.sentimentDistribution?.find(
(s) => s.sentiment === "NEGATIVE"
)?.count || 0,
// Map category data to CategoryMetrics format
...(overviewData.categoryDistribution && {
categories: overviewData.categoryDistribution.reduce(
(acc, item) => {
if (item.category) {
acc[item.category] = item.count;
}
return acc;
},
{} as Record<string, number>
),
}),
};
setMetrics(mappedMetrics as MetricsResult);
if (isInitialLoad) {
setIsInitialLoad(false);
}
}
}, [overviewData, isInitialLoad]);
// Admin refresh sessions mutation
const refreshSessionsMutation = trpc.admin.refreshSessions.useMutation({
onSuccess: () => {
// Refetch metrics after successful refresh
refetchMetrics();
},
onError: (error) => {
alert(`Failed to refresh sessions: ${error.message}`);
},
});
useEffect(() => {
// Redirect if not authenticated
if (status === "unauthenticated") {
router.push("/login");
return;
}
// tRPC queries handle data fetching automatically
}, [status, router]);
// Enhanced error handling with user feedback
if (metricsError) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center space-y-4">
<div className="text-red-600 text-lg font-semibold">
Failed to load dashboard data
</div>
<p className="text-gray-600">
There was an error loading your dashboard metrics. Please try
refreshing the page.
</p>
<button
type="button"
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Refresh Page
</button>
</div>
</div>
);
}
async function handleRefresh() {
if (isAuditor) return;
setRefreshing(true);
try {
await refreshSessionsMutation.mutateAsync();
} finally {
setRefreshing(false);
}
}
// Show loading state while session status is being determined
const loadingState = DashboardLoadingStates({ status });
if (loadingState) return loadingState;
// Show loading state while data is being fetched
if (isLoadingMetrics && !metrics) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto" />
<p className="text-muted-foreground">Loading dashboard data...</p>
</div>
</div>
);
}
if (!metrics) {
return <DashboardSkeleton />;
}
return (
<div className="space-y-8">
<DashboardHeader
company={{ name: "Analytics Dashboard" } as Company}
metrics={metrics}
isAuditor={isAuditor}
refreshing={refreshing}
onRefresh={handleRefresh}
/>
{/* Date Range Picker */}
{/* Date Range Picker - Temporarily disabled to debug infinite loop */}
{/* {dateRange && (
<DateRangePicker
minDate={dateRange.minDate}
@ -664,19 +389,100 @@ function DashboardContent() {
/>
)} */}
<MetricsGrid metrics={metrics} />
{/* Modern Metrics Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<MetricCard
title="Total Sessions"
value={metrics.totalSessions?.toLocaleString()}
icon={<MessageSquare className="h-5 w-5" />}
trend={{
value: metrics.sessionTrend ?? 0,
isPositive: (metrics.sessionTrend ?? 0) >= 0,
}}
variant="primary"
/>
<MetricCard
title="Unique Users"
value={metrics.uniqueUsers?.toLocaleString()}
icon={<Users className="h-5 w-5" />}
trend={{
value: metrics.usersTrend ?? 0,
isPositive: (metrics.usersTrend ?? 0) >= 0,
}}
variant="success"
/>
<MetricCard
title="Avg. Session Time"
value={`${Math.round(metrics.avgSessionLength || 0)}s`}
icon={<Clock className="h-5 w-5" />}
trend={{
value: metrics.avgSessionTimeTrend ?? 0,
isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0,
}}
/>
<MetricCard
title="Avg. Response Time"
value={`${metrics.avgResponseTime?.toFixed(1) || 0}s`}
icon={<Zap className="h-5 w-5" />}
trend={{
value: metrics.avgResponseTimeTrend ?? 0,
isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0,
}}
variant="warning"
/>
<MetricCard
title="Daily Costs"
value={`${metrics.avgDailyCosts?.toFixed(4) || "0.0000"}`}
icon={<Euro className="h-5 w-5" />}
description="Average per day"
/>
<MetricCard
title="Peak Usage"
value={metrics.peakUsageTime || "N/A"}
icon={<TrendingUp className="h-5 w-5" />}
description="Busiest hour"
/>
<MetricCard
title="Resolution Rate"
value={`${metrics.resolvedChatsPercentage?.toFixed(1) || "0.0"}%`}
icon={<CheckCircle className="h-5 w-5" />}
trend={{
value: metrics.resolvedChatsPercentage ?? 0,
isPositive: (metrics.resolvedChatsPercentage ?? 0) >= 80,
}}
variant={
metrics.resolvedChatsPercentage &&
metrics.resolvedChatsPercentage >= 80
? "success"
: "warning"
}
/>
<MetricCard
title="Active Languages"
value={Object.keys(metrics.languages || {}).length}
icon={<Globe className="h-5 w-5" />}
description="Languages detected"
/>
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<ModernLineChart
data={dataHelpers.getSessionsOverTimeData()}
data={getSessionsOverTimeData()}
title="Sessions Over Time"
className="lg:col-span-2"
height={350}
/>
<ModernDonutChart
data={dataHelpers.getSentimentData()}
data={getSentimentData()}
title="Conversation Sentiment"
centerText={{
title: "Total",
@ -688,13 +494,13 @@ function DashboardContent() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ModernBarChart
data={dataHelpers.getCategoriesData()}
data={getCategoriesData()}
title="Sessions by Category"
height={350}
/>
<ModernDonutChart
data={dataHelpers.getLanguagesData()}
data={getLanguagesData()}
title="Languages Used"
height={350}
/>
@ -710,7 +516,7 @@ function DashboardContent() {
</CardTitle>
</CardHeader>
<CardContent>
<GeographicMap countries={dataHelpers.getCountryData()} />
<GeographicMap countries={getCountryData()} />
</CardContent>
</Card>
@ -723,11 +529,7 @@ function DashboardContent() {
</CardHeader>
<CardContent>
<div className="h-[300px]">
<WordCloud
words={dataHelpers.getWordCloudData()}
width={500}
height={300}
/>
<WordCloud words={getWordCloudData()} width={500} height={300} />
</div>
</CardContent>
</Card>
@ -743,7 +545,7 @@ function DashboardContent() {
</CardHeader>
<CardContent>
<ResponseTimeDistribution
data={dataHelpers.getResponseTimeData()}
data={getResponseTimeData()}
average={metrics.avgResponseTime || 0}
/>
</CardContent>

View File

@ -23,24 +23,24 @@ import MessageViewer from "../../../../components/MessageViewer";
import SessionDetails from "../../../../components/SessionDetails";
import type { ChatSession } from "../../../../lib/types";
/**
* Custom hook for managing session data fetching and state
*/
function useSessionData(id: string | undefined, authStatus: string) {
export default function SessionViewPage() {
const params = useParams();
const router = useRouter(); // Initialize useRouter
const { status } = useSession(); // Get session status, removed unused sessionData
const id = params?.id as string;
const [session, setSession] = useState<ChatSession | null>(null);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(true); // This will now primarily be for data fetching
const [error, setError] = useState<string | null>(null);
const router = useRouter();
useEffect(() => {
if (authStatus === "unauthenticated") {
if (status === "unauthenticated") {
router.push("/login");
return;
}
if (authStatus === "authenticated" && id) {
if (status === "authenticated" && id) {
const fetchSession = async () => {
setLoading(true);
setLoading(true); // Always set loading before fetch
setError(null);
try {
const response = await fetch(`/api/dashboard/session/${id}`);
@ -63,36 +63,55 @@ function useSessionData(id: string | undefined, authStatus: string) {
}
};
fetchSession();
} else if (authStatus === "authenticated" && !id) {
} else if (status === "authenticated" && !id) {
setError("Session ID is missing.");
setLoading(false);
}
}, [id, authStatus, router]);
}, [id, status, router]); // session removed from dependencies
return { session, loading, error };
}
/**
* Component for rendering loading state
*/
function LoadingCard({ message }: { message: string }) {
if (status === "loading") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
{message}
Loading session...
</div>
</CardContent>
</Card>
</div>
);
}
}
/**
* Component for rendering error state
*/
function ErrorCard({ error }: { error: string }) {
if (status === "unauthenticated") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Redirecting to login...
</div>
</CardContent>
</Card>
</div>
);
}
if (loading && status === "authenticated") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Loading session details...
</div>
</CardContent>
</Card>
</div>
);
}
if (error) {
return (
<div className="space-y-6">
<Card>
@ -111,12 +130,9 @@ function ErrorCard({ error }: { error: string }) {
</Card>
</div>
);
}
}
/**
* Component for rendering session not found state
*/
function SessionNotFoundCard() {
if (!session) {
return (
<div className="space-y-6">
<Card>
@ -137,13 +153,11 @@ function SessionNotFoundCard() {
</Card>
</div>
);
}
}
/**
* Component for rendering session header with navigation and badges
*/
function SessionHeader({ session }: { session: ChatSession }) {
return (
<div className="space-y-6 max-w-6xl mx-auto">
{/* Header */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
@ -202,14 +216,8 @@ function SessionHeader({ session }: { session: ChatSession }) {
</div>
</CardContent>
</Card>
);
}
/**
* Component for rendering session overview cards
*/
function SessionOverview({ session }: { session: ChatSession }) {
return (
{/* Session Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
@ -271,39 +279,6 @@ function SessionOverview({ session }: { session: ChatSession }) {
</CardContent>
</Card>
</div>
);
}
export default function SessionViewPage() {
const params = useParams();
const { status } = useSession();
const id = params?.id as string;
const { session, loading, error } = useSessionData(id, status);
if (status === "loading") {
return <LoadingCard message="Loading session..." />;
}
if (status === "unauthenticated") {
return <LoadingCard message="Redirecting to login..." />;
}
if (loading && status === "authenticated") {
return <LoadingCard message="Loading session details..." />;
}
if (error) {
return <ErrorCard error={error} />;
}
if (!session) {
return <SessionNotFoundCard />;
}
return (
<div className="space-y-6 max-w-6xl mx-auto">
<SessionHeader session={session} />
<SessionOverview session={session} />
{/* Session Details */}
<SessionDetails session={session} />

View File

@ -13,171 +13,265 @@ import {
Search,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useId, useState } from "react";
import type { z } from "zod";
import { useCallback, useEffect, useId, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { formatCategory } from "@/lib/format-enums";
import { trpc } from "@/lib/trpc-client";
import type { sessionFilterSchema } from "@/lib/validation";
import type { ChatSession } from "../../../lib/types";
// Placeholder for a SessionListItem component to be created later
// For now, we'll display some basic info directly.
// import SessionListItem from "../../../components/SessionListItem";
// TODO: Consider moving filter/sort types to lib/types.ts if they become complex
interface FilterOptions {
categories: string[];
languages: string[];
}
interface FilterSectionProps {
filtersExpanded: boolean;
setFiltersExpanded: (expanded: boolean) => void;
searchTerm: string;
setSearchTerm: (term: string) => void;
selectedCategory: string;
setSelectedCategory: (category: string) => void;
selectedLanguage: string;
setSelectedLanguage: (language: string) => void;
startDate: string;
setStartDate: (date: string) => void;
endDate: string;
setEndDate: (date: string) => void;
sortKey: string;
setSortKey: (key: string) => void;
sortOrder: string;
setSortOrder: (order: string) => void;
filterOptions: FilterOptions;
searchHeadingId: string;
searchId: string;
filtersHeadingId: string;
filterContentId: string;
categoryFilterId: string;
categoryHelpId: string;
languageFilterId: string;
languageHelpId: string;
startDateId: string;
endDateId: string;
sortById: string;
sortOrderId: string;
sortOrderHelpId: string;
}
export default function SessionsPage() {
const [sessions, setSessions] = useState<ChatSession[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
function FilterSection({
filtersExpanded,
setFiltersExpanded,
searchTerm,
setSearchTerm,
const searchHeadingId = useId();
const filtersHeadingId = useId();
const filterContentId = useId();
const categoryFilterId = useId();
const categoryHelpId = useId();
const languageFilterId = useId();
const languageHelpId = useId();
const sortOrderId = useId();
const sortOrderHelpId = useId();
const resultsHeadingId = useId();
const startDateFilterId = useId();
const startDateHelpId = useId();
const endDateFilterId = useId();
const endDateHelpId = useId();
const sortKeyId = useId();
const sortKeyHelpId = useId();
// Filter states
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
categories: [],
languages: [],
});
const [selectedCategory, setSelectedCategory] = useState<string>("");
const [selectedLanguage, setSelectedLanguage] = useState<string>("");
const [startDate, setStartDate] = useState<string>("");
const [endDate, setEndDate] = useState<string>("");
// Sort states
const [sortKey, setSortKey] = useState<string>("startTime"); // Default sort key
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); // Default sort order
// Debounce search term to avoid excessive API calls
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
// Pagination states
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const [pageSize, _setPageSize] = useState(10); // Or make this configurable
// UI states
const [filtersExpanded, setFiltersExpanded] = useState(false);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500); // 500ms delay
return () => {
clearTimeout(timerId);
};
}, [searchTerm]);
const fetchFilterOptions = useCallback(async () => {
try {
const response = await fetch("/api/dashboard/session-filter-options");
if (!response.ok) {
throw new Error("Failed to fetch filter options");
}
const data = await response.json();
setFilterOptions(data);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to load filter options"
);
}
}, []);
const fetchSessions = useCallback(async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (debouncedSearchTerm) params.append("searchTerm", debouncedSearchTerm);
if (selectedCategory) params.append("category", selectedCategory);
if (selectedLanguage) params.append("language", selectedLanguage);
if (startDate) params.append("startDate", startDate);
if (endDate) params.append("endDate", endDate);
if (sortKey) params.append("sortKey", sortKey);
if (sortOrder) params.append("sortOrder", sortOrder);
params.append("page", currentPage.toString());
params.append("pageSize", pageSize.toString());
const response = await fetch(
`/api/dashboard/sessions?${params.toString()}`
);
if (!response.ok) {
throw new Error(`Failed to fetch sessions: ${response.statusText}`);
}
const data = await response.json();
setSessions(data.sessions || []);
setTotalPages(Math.ceil((data.totalSessions || 0) / pageSize));
} catch (err) {
setError(
err instanceof Error ? err.message : "An unknown error occurred"
);
setSessions([]);
} finally {
setLoading(false);
}
}, [
debouncedSearchTerm,
selectedCategory,
setSelectedCategory,
selectedLanguage,
setSelectedLanguage,
startDate,
setStartDate,
endDate,
setEndDate,
sortKey,
setSortKey,
sortOrder,
setSortOrder,
filterOptions,
searchHeadingId,
searchId,
filtersHeadingId,
filterContentId,
categoryFilterId,
categoryHelpId,
languageFilterId,
languageHelpId,
startDateId,
endDateId,
sortById,
sortOrderId,
sortOrderHelpId,
}: FilterSectionProps) {
return (
<section aria-labelledby={searchHeadingId}>
<h2 id={searchHeadingId} className="sr-only">
Search and Filter Sessions
</h2>
currentPage,
pageSize,
]);
useEffect(() => {
fetchSessions();
}, [fetchSessions]);
useEffect(() => {
fetchFilterOptions();
}, [fetchFilterOptions]);
return (
<div className="space-y-6">
{/* Page heading for screen readers */}
<h1 className="sr-only">Sessions Management</h1>
{/* Header */}
<Card>
<CardHeader>
<div className="space-y-4">
<div className="flex items-center gap-3">
<MessageSquare className="h-6 w-6" />
<CardTitle as="h2">Chat Sessions</CardTitle>
</div>
</CardHeader>
</Card>
{/* Search Input */}
<section aria-labelledby={searchHeadingId}>
<h2 id={searchHeadingId} className="sr-only">
Search Sessions
</h2>
<Card>
<CardContent className="pt-6">
<div className="relative">
<Label htmlFor={searchId} className="sr-only">
Search sessions
</Label>
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
<Input
id={searchId}
type="text"
placeholder="Search sessions..."
placeholder="Search sessions (ID, category, initial message...)"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
aria-label="Search sessions by ID, category, or message content"
/>
</div>
</div>
</CardContent>
</Card>
</section>
{/* Filter and Sort Controls */}
<section aria-labelledby={filtersHeadingId}>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Filter className="h-5 w-5" aria-hidden="true" />
<CardTitle as="h2" id={filtersHeadingId} className="text-lg">
Filters & Sorting
</CardTitle>
</div>
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={() => setFiltersExpanded(!filtersExpanded)}
className="w-full justify-between"
className="gap-2"
aria-expanded={filtersExpanded}
aria-controls={filterContentId}
aria-describedby={filtersHeadingId}
>
<span id={filtersHeadingId}>Advanced Filters</span>
{filtersExpanded ? (
<>
<ChevronUp className="h-4 w-4" />
Hide
</>
) : (
<>
<ChevronDown className="h-4 w-4" />
Show
</>
)}
</Button>
</div>
</CardHeader>
{filtersExpanded && (
<CardContent id={filterContentId}>
<fieldset>
<legend className="sr-only">Filter and sort options</legend>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<legend className="sr-only">
Session Filters and Sorting Options
</legend>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
{/* Category Filter */}
<div className="space-y-2">
<Label htmlFor={categoryFilterId}>Category</Label>
<select
id={categoryFilterId}
className="w-full h-10 px-3 py-2 text-sm rounded-md border border-input bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="w-full mt-1 p-2 border border-gray-300 rounded-md"
aria-describedby={categoryHelpId}
>
<option value="">All Categories</option>
{filterOptions.categories.map((category) => (
<option key={category} value={category}>
{formatCategory(category)}
{filterOptions.categories.map((cat) => (
<option key={cat} value={cat}>
{formatCategory(cat)}
</option>
))}
</select>
<div id={categoryHelpId} className="sr-only">
Filter sessions by category
Filter sessions by category type
</div>
</div>
<div>
{/* Language Filter */}
<div className="space-y-2">
<Label htmlFor={languageFilterId}>Language</Label>
<select
id={languageFilterId}
className="w-full h-10 px-3 py-2 text-sm rounded-md border border-input bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
value={selectedLanguage}
onChange={(e) => setSelectedLanguage(e.target.value)}
className="w-full mt-1 p-2 border border-gray-300 rounded-md"
aria-describedby={languageHelpId}
>
<option value="">All Languages</option>
{filterOptions.languages.map((language) => (
<option key={language} value={language}>
{language.toUpperCase()}
{filterOptions.languages.map((lang) => (
<option key={lang} value={lang}>
{lang.toUpperCase()}
</option>
))}
</select>
@ -186,54 +280,74 @@ function FilterSection({
</div>
</div>
<div>
<Label htmlFor={startDateId}>Start Date</Label>
{/* Start Date Filter */}
<div className="space-y-2">
<Label htmlFor={startDateFilterId}>Start Date</Label>
<Input
id={startDateId}
type="date"
id={startDateFilterId}
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="mt-1"
aria-describedby={startDateHelpId}
/>
<div id={startDateHelpId} className="sr-only">
Filter sessions from this date onwards
</div>
</div>
<div>
<Label htmlFor={endDateId}>End Date</Label>
{/* End Date Filter */}
<div className="space-y-2">
<Label htmlFor={endDateFilterId}>End Date</Label>
<Input
id={endDateId}
type="date"
id={endDateFilterId}
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="mt-1"
aria-describedby={endDateHelpId}
/>
<div id={endDateHelpId} className="sr-only">
Filter sessions up to this date
</div>
</div>
<div>
<Label htmlFor={sortById}>Sort By</Label>
{/* Sort Key */}
<div className="space-y-2">
<Label htmlFor={sortKeyId}>Sort By</Label>
<select
id={sortById}
id={sortKeyId}
className="w-full h-10 px-3 py-2 text-sm rounded-md border border-input bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
value={sortKey}
onChange={(e) => setSortKey(e.target.value)}
className="w-full mt-1 p-2 border border-gray-300 rounded-md"
aria-describedby={sortKeyHelpId}
>
<option value="startTime">Start Time</option>
<option value="sessionId">Session ID</option>
<option value="category">Category</option>
<option value="language">Language</option>
<option value="sentiment">Sentiment</option>
<option value="messagesSent">Messages Sent</option>
<option value="avgResponseTime">
Avg. Response Time
</option>
</select>
<div id={sortKeyHelpId} className="sr-only">
Choose field to sort sessions by
</div>
</div>
<div>
<Label htmlFor={sortOrderId}>Sort Order</Label>
{/* Sort Order */}
<div className="space-y-2">
<Label htmlFor={sortOrderId}>Order</Label>
<select
id={sortOrderId}
className="w-full h-10 px-3 py-2 text-sm rounded-md border border-input bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
className="w-full mt-1 p-2 border border-gray-300 rounded-md"
onChange={(e) =>
setSortOrder(e.target.value as "asc" | "desc")
}
aria-describedby={sortOrderHelpId}
>
<option value="desc">Newest First</option>
<option value="asc">Oldest First</option>
<option value="desc">Descending</option>
<option value="asc">Ascending</option>
</select>
<div id={sortOrderHelpId} className="sr-only">
Choose ascending or descending order
@ -245,28 +359,14 @@ function FilterSection({
)}
</Card>
</section>
);
}
interface SessionListProps {
sessions: ChatSession[];
loading: boolean;
error: string | null;
resultsHeadingId: string;
}
function SessionList({
sessions,
loading,
error,
resultsHeadingId,
}: SessionListProps) {
return (
{/* Results section */}
<section aria-labelledby={resultsHeadingId}>
<h2 id={resultsHeadingId} className="sr-only">
Session Results
</h2>
{/* Live region for screen reader announcements */}
<output aria-live="polite" className="sr-only">
{loading && "Loading sessions..."}
{error && `Error loading sessions: ${error}`}
@ -277,6 +377,7 @@ function SessionList({
{!loading && !error && sessions.length === 0 && "No sessions found"}
</output>
{/* Loading State */}
{loading && (
<Card>
<CardContent className="pt-6">
@ -290,6 +391,7 @@ function SessionList({
</Card>
)}
{/* Error State */}
{error && (
<Card>
<CardContent className="pt-6">
@ -298,36 +400,53 @@ function SessionList({
role="alert"
aria-hidden="true"
>
Error loading sessions: {error}
Error: {error}
</div>
</CardContent>
</Card>
)}
{/* Empty State */}
{!loading && !error && sessions.length === 0 && (
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
No sessions found. Try adjusting your search criteria.
{debouncedSearchTerm
? `No sessions found for "${debouncedSearchTerm}".`
: "No sessions found."}
</div>
</CardContent>
</Card>
)}
{/* Sessions List */}
{!loading && !error && sessions.length > 0 && (
<ul className="space-y-4">
<ul aria-label="Chat sessions" className="grid gap-4">
{sessions.map((session) => (
<li key={session.id}>
<Card>
<Card className="hover:shadow-md transition-shadow">
<CardContent className="pt-6">
<article>
<header className="flex justify-between items-start mb-3">
<div>
<h3 className="font-medium text-base mb-1">
Session{" "}
{session.sessionId ||
`${session.id.substring(0, 8)}...`}
<article aria-labelledby={`session-${session.id}-title`}>
<header className="flex justify-between items-start mb-4">
<div className="space-y-2 flex-1">
<h3
id={`session-${session.id}-title`}
className="sr-only"
>
Session {session.sessionId || session.id} from{" "}
{new Date(session.startTime).toLocaleDateString()}
</h3>
<div className="flex items-center gap-3">
<Badge
variant="outline"
className="font-mono text-xs"
>
ID
</Badge>
<code className="text-sm text-muted-foreground font-mono truncate max-w-24">
{session.sessionId || session.id}
</code>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
<Clock
@ -349,7 +468,9 @@ function SessionList({
aria-label={`View details for session ${session.sessionId || session.id}`}
>
<Eye className="h-4 w-4" aria-hidden="true" />
<span className="hidden sm:inline">View Details</span>
<span className="hidden sm:inline">
View Details
</span>
</Button>
</Link>
</header>
@ -385,30 +506,17 @@ function SessionList({
))}
</ul>
)}
</section>
);
}
interface PaginationProps {
currentPage: number;
totalPages: number;
setCurrentPage: (page: number | ((prev: number) => number)) => void;
}
function Pagination({
currentPage,
totalPages,
setCurrentPage,
}: PaginationProps) {
if (totalPages === 0) return null;
return (
{/* Pagination */}
{totalPages > 0 && (
<Card>
<CardContent className="pt-6">
<div className="flex justify-center items-center gap-4">
<Button
variant="outline"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
onClick={() =>
setCurrentPage((prev) => Math.max(prev - 1, 1))
}
disabled={currentPage === 1}
className="gap-2"
>
@ -432,168 +540,8 @@ function Pagination({
</div>
</CardContent>
</Card>
);
}
export default function SessionsPage() {
const [sessions, setSessions] = useState<ChatSession[]>([]);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const searchHeadingId = useId();
const searchId = useId();
const filtersHeadingId = useId();
const filterContentId = useId();
const categoryFilterId = useId();
const categoryHelpId = useId();
const languageFilterId = useId();
const languageHelpId = useId();
const startDateId = useId();
const endDateId = useId();
const sortById = useId();
const sortOrderId = useId();
const sortOrderHelpId = useId();
const resultsHeadingId = useId();
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
const [selectedLanguage, setSelectedLanguage] = useState("");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [sortKey, setSortKey] = useState("startTime");
const [sortOrder, setSortOrder] = useState("desc");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [pageSize] = useState(10);
const [filtersExpanded, setFiltersExpanded] = useState(false);
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
categories: [],
languages: [],
});
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500);
return () => clearTimeout(timerId);
}, [searchTerm]);
// TODO: Implement getSessionFilterOptions in tRPC dashboard router
// For now, we'll set default filter options
useEffect(() => {
setFilterOptions({
categories: [
"SCHEDULE_HOURS",
"LEAVE_VACATION",
"SICK_LEAVE_RECOVERY",
"SALARY_COMPENSATION",
],
languages: ["en", "nl", "de", "fr", "es"],
});
}, []);
// tRPC query for sessions
const {
data: sessionsData,
isLoading,
error: sessionsError,
} = trpc.dashboard.getSessions.useQuery(
{
search: debouncedSearchTerm || undefined,
category: selectedCategory
? (selectedCategory as z.infer<typeof sessionFilterSchema>["category"])
: undefined,
language: selectedLanguage || undefined,
startDate: startDate || undefined,
endDate: endDate || undefined,
sortKey: sortKey || undefined,
sortOrder: sortOrder || undefined,
page: currentPage,
limit: pageSize,
},
{
// Enable the query by default
enabled: true,
}
);
// Update state when data changes
useEffect(() => {
if (sessionsData) {
setSessions(sessionsData.sessions || []);
setTotalPages(sessionsData.pagination.totalPages);
setError(null);
}
}, [sessionsData]);
useEffect(() => {
if (sessionsError) {
setError(sessionsError.message || "An unknown error occurred");
setSessions([]);
}
}, [sessionsError]);
// tRPC queries handle data fetching automatically
return (
<div className="space-y-6">
<h1 className="sr-only">Sessions Management</h1>
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<MessageSquare className="h-6 w-6" />
<CardTitle>Chat Sessions</CardTitle>
</div>
</CardHeader>
</Card>
<FilterSection
filtersExpanded={filtersExpanded}
setFiltersExpanded={setFiltersExpanded}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
selectedLanguage={selectedLanguage}
setSelectedLanguage={setSelectedLanguage}
startDate={startDate}
setStartDate={setStartDate}
endDate={endDate}
setEndDate={setEndDate}
sortKey={sortKey}
setSortKey={setSortKey}
sortOrder={sortOrder}
setSortOrder={setSortOrder}
filterOptions={filterOptions}
searchHeadingId={searchHeadingId}
searchId={searchId}
filtersHeadingId={filtersHeadingId}
filterContentId={filterContentId}
categoryFilterId={categoryFilterId}
categoryHelpId={categoryHelpId}
languageFilterId={languageFilterId}
languageHelpId={languageHelpId}
startDateId={startDateId}
endDateId={endDateId}
sortById={sortById}
sortOrderId={sortOrderId}
sortOrderHelpId={sortOrderHelpId}
/>
<SessionList
sessions={sessions}
loading={isLoading}
error={error}
resultsHeadingId={resultsHeadingId}
/>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
setCurrentPage={setCurrentPage}
/>
)}
</section>
</div>
);
}

View File

@ -2,8 +2,6 @@
import "./globals.css";
import type { ReactNode } from "react";
import { Toaster } from "@/components/ui/sonner";
import { NonceProvider } from "@/lib/nonce-context";
import { getNonce } from "@/lib/nonce-utils";
import { Providers } from "./providers";
export const metadata = {
@ -90,13 +88,7 @@ export const metadata = {
},
};
export default async function RootLayout({
children,
}: {
children: ReactNode;
}) {
const nonce = await getNonce();
export default function RootLayout({ children }: { children: ReactNode }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
@ -134,8 +126,7 @@ export default async function RootLayout({
<head>
<script
type="application/ld+json"
nonce={nonce}
// biome-ignore lint/security/noDangerouslySetInnerHtml: Safe use for JSON-LD structured data with CSP nonce
// biome-ignore lint/security/noDangerouslySetInnerHtml: Safe use for JSON-LD structured data
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
@ -147,9 +138,7 @@ export default async function RootLayout({
>
Skip to main content
</a>
<NonceProvider nonce={nonce}>
<Providers>{children}</Providers>
</NonceProvider>
<Toaster />
</body>
</html>

View File

@ -39,43 +39,6 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useToast } from "@/hooks/use-toast";
type ToastFunction = (props: {
title: string;
description: string;
variant?: "default" | "destructive";
}) => void;
interface CompanyManagementState {
company: Company | null;
setCompany: (company: Company | null) => void;
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
isSaving: boolean;
setIsSaving: (saving: boolean) => void;
editData: Partial<Company>;
setEditData: (
data: Partial<Company> | ((prev: Partial<Company>) => Partial<Company>)
) => void;
originalData: Partial<Company>;
setOriginalData: (data: Partial<Company>) => void;
showInviteUser: boolean;
setShowInviteUser: (show: boolean) => void;
inviteData: { name: string; email: string; role: string };
setInviteData: (
data:
| { name: string; email: string; role: string }
| ((prev: { name: string; email: string; role: string }) => {
name: string;
email: string;
role: string;
})
) => void;
showUnsavedChangesDialog: boolean;
setShowUnsavedChangesDialog: (show: boolean) => void;
pendingNavigation: string | null;
setPendingNavigation: (navigation: string | null) => void;
}
interface User {
id: string;
name: string;
@ -101,10 +64,51 @@ interface Company {
};
}
/**
* Custom hook for company management state
*/
function useCompanyManagementState() {
export default function CompanyManagement() {
const { data: session, status } = useSession();
const router = useRouter();
const params = useParams();
const { toast } = useToast();
const companyNameFieldId = useId();
const companyEmailFieldId = useId();
const maxUsersFieldId = useId();
const inviteNameFieldId = useId();
const inviteEmailFieldId = useId();
const fetchCompany = useCallback(async () => {
try {
const response = await fetch(`/api/platform/companies/${params.id}`);
if (response.ok) {
const data = await response.json();
setCompany(data);
const companyData = {
name: data.name,
email: data.email,
status: data.status,
maxUsers: data.maxUsers,
};
setEditData(companyData);
setOriginalData(companyData);
} else {
toast({
title: "Error",
description: "Failed to load company data",
variant: "destructive",
});
}
} catch (error) {
console.error("Failed to fetch company:", error);
toast({
title: "Error",
description: "Failed to load company data",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
}, [params.id, toast]);
const [company, setCompany] = useState<Company | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
@ -122,55 +126,9 @@ function useCompanyManagementState() {
null
);
return {
company,
setCompany,
isLoading,
setIsLoading,
isSaving,
setIsSaving,
editData,
setEditData,
originalData,
setOriginalData,
showInviteUser,
setShowInviteUser,
inviteData,
setInviteData,
showUnsavedChangesDialog,
setShowUnsavedChangesDialog,
pendingNavigation,
setPendingNavigation,
};
}
/**
* Custom hook for form IDs
*/
function useCompanyFormIds() {
const companyNameFieldId = useId();
const companyEmailFieldId = useId();
const maxUsersFieldId = useId();
const inviteNameFieldId = useId();
const inviteEmailFieldId = useId();
return {
companyNameFieldId,
companyEmailFieldId,
maxUsersFieldId,
inviteNameFieldId,
inviteEmailFieldId,
};
}
/**
* Custom hook for data validation and comparison
*/
function useDataComparison(
editData: Partial<Company>,
originalData: Partial<Company>
) {
// Function to check if data has been modified
const hasUnsavedChanges = useCallback(() => {
// Normalize data for comparison (handle null/undefined/empty string equivalence)
const normalizeValue = (value: string | number | null | undefined) => {
if (value === null || value === undefined || value === "") {
return "";
@ -198,104 +156,16 @@ function useDataComparison(
);
}, [editData, originalData]);
return { hasUnsavedChanges };
}
/**
* Custom hook for company data fetching
*/
function useCompanyData(
params: { id: string | string[] },
toast: ToastFunction,
state: CompanyManagementState
) {
const { setCompany, setEditData, setOriginalData, setIsLoading } = state;
const [hasFetched, setHasFetched] = useState(false);
const fetchCompany = useCallback(async () => {
if (hasFetched) return;
try {
const response = await fetch(`/api/platform/companies/${params.id}`);
if (response.ok) {
const data = await response.json();
setCompany(data);
const companyData = {
name: data.name,
email: data.email,
status: data.status,
maxUsers: data.maxUsers,
};
setEditData(companyData);
setOriginalData(companyData);
setHasFetched(true);
} else {
const errorText = await response.text();
const errorMessage = `Failed to load company data (${response.status}: ${response.statusText})`;
console.error("Failed to fetch company - HTTP Error:", {
status: response.status,
statusText: response.statusText,
response: errorText,
url: response.url,
});
toast({
title: "Error",
description: errorMessage,
variant: "destructive",
});
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred";
console.error("Failed to fetch company - Network/Parse Error:", {
message: errorMessage,
error: error,
stack: error instanceof Error ? error.stack : undefined,
url: `/api/platform/companies/${params.id}`,
});
toast({
title: "Error",
description: `Failed to load company data: ${errorMessage}`,
variant: "destructive",
});
} finally {
setIsLoading(false);
}
}, [
params.id,
hasFetched,
toast,
setCompany,
setEditData,
setOriginalData,
setIsLoading,
]);
return { fetchCompany };
}
/**
* Custom hook for navigation handling
*/
function useNavigationControl(
router: { push: (url: string) => void },
params: { id: string | string[] },
hasUnsavedChanges: () => boolean,
state: CompanyManagementState
) {
const { setPendingNavigation, setShowUnsavedChangesDialog } = state;
// Handle navigation protection - must be at top level
const handleNavigation = useCallback(
(url: string) => {
// Allow navigation within the same company (different tabs, etc.)
if (url.includes(`/platform/companies/${params.id}`)) {
router.push(url);
return;
}
// If there are unsaved changes, show confirmation dialog
if (hasUnsavedChanges()) {
setPendingNavigation(url);
setShowUnsavedChangesDialog(true);
@ -303,225 +173,7 @@ function useNavigationControl(
router.push(url);
}
},
[
router,
params.id,
hasUnsavedChanges,
setPendingNavigation,
setShowUnsavedChangesDialog,
]
);
return { handleNavigation };
}
/**
* Helper function to render company information card
*/
function renderCompanyInfoCard(
state: CompanyManagementState,
canEdit: boolean,
companyNameFieldId: string,
companyEmailFieldId: string,
maxUsersFieldId: string,
hasUnsavedChanges: () => boolean,
handleSave: () => Promise<void>
) {
return (
<Card>
<CardHeader>
<CardTitle>Company Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor={companyNameFieldId}>Company Name</Label>
<Input
id={companyNameFieldId}
value={state.editData.name || ""}
onChange={(e) =>
state.setEditData((prev) => ({
...prev,
name: e.target.value,
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor={companyEmailFieldId}>Contact Email</Label>
<Input
id={companyEmailFieldId}
type="email"
value={state.editData.email || ""}
onChange={(e) =>
state.setEditData((prev) => ({
...prev,
email: e.target.value,
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor={maxUsersFieldId}>Max Users</Label>
<Input
id={maxUsersFieldId}
type="number"
value={state.editData.maxUsers || 0}
onChange={(e) => {
const value = e.target.value;
const parsedValue = Number.parseInt(value, 10);
// Validate input: must be a positive number
const maxUsers =
!Number.isNaN(parsedValue) && parsedValue > 0
? parsedValue
: 1; // Default to 1 for invalid/negative values
state.setEditData((prev) => ({
...prev,
maxUsers,
}));
}}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor="status">Status</Label>
<Select
value={state.editData.status}
onValueChange={(value) =>
state.setEditData((prev) => ({
...prev,
status: value,
}))
}
disabled={!canEdit}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACTIVE">Active</SelectItem>
<SelectItem value="TRIAL">Trial</SelectItem>
<SelectItem value="SUSPENDED">Suspended</SelectItem>
<SelectItem value="ARCHIVED">Archived</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{canEdit && hasUnsavedChanges() && (
<div className="flex gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => {
state.setEditData(state.originalData);
}}
>
Cancel Changes
</Button>
<Button onClick={handleSave} disabled={state.isSaving}>
<Save className="w-4 h-4 mr-2" />
{state.isSaving ? "Saving..." : "Save Changes"}
</Button>
</div>
)}
</CardContent>
</Card>
);
}
/**
* Helper function to render users tab content
*/
function renderUsersTab(state: CompanyManagementState, canEdit: boolean) {
return (
<TabsContent value="users" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Users className="w-5 h-5" />
Users ({state.company?.users.length || 0})
</span>
{canEdit && (
<Button size="sm" onClick={() => state.setShowInviteUser(true)}>
<UserPlus className="w-4 h-4 mr-2" />
Invite User
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{state.company?.users.map((user) => (
<div
key={user.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-blue-600 dark:text-blue-300">
{user.name?.charAt(0) ||
user.email.charAt(0).toUpperCase()}
</span>
</div>
<div>
<div className="font-medium">{user.name || "No name"}</div>
<div className="text-sm text-muted-foreground">
{user.email}
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline">{user.role}</Badge>
<div className="text-sm text-muted-foreground">
Joined {new Date(user.createdAt).toLocaleDateString()}
</div>
</div>
</div>
))}
{(state.company?.users.length || 0) === 0 && (
<div className="text-center py-8 text-muted-foreground">
No users found. Invite the first user to get started.
</div>
)}
</div>
</CardContent>
</Card>
</TabsContent>
);
}
export default function CompanyManagement() {
const { data: session, status } = useSession();
const router = useRouter();
const params = useParams();
const { toast } = useToast();
const state = useCompanyManagementState();
const {
companyNameFieldId,
companyEmailFieldId,
maxUsersFieldId,
inviteNameFieldId,
inviteEmailFieldId,
} = useCompanyFormIds();
const { hasUnsavedChanges } = useDataComparison(
state.editData,
state.originalData
);
const { fetchCompany } = useCompanyData(
{ id: params.id as string },
toast,
state
);
const { handleNavigation } = useNavigationControl(
router,
{ id: params.id as string },
hasUnsavedChanges,
state
[router, params.id, hasUnsavedChanges]
);
useEffect(() => {
@ -533,27 +185,27 @@ export default function CompanyManagement() {
}
fetchCompany();
}, [status, session?.user?.isPlatformUser, fetchCompany, router]);
}, [session, status, router, fetchCompany]);
const handleSave = async () => {
state.setIsSaving(true);
setIsSaving(true);
try {
const response = await fetch(`/api/platform/companies/${params.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(state.editData),
body: JSON.stringify(editData),
});
if (response.ok) {
const updatedCompany = await response.json();
state.setCompany(updatedCompany);
setCompany(updatedCompany);
const companyData = {
name: updatedCompany.name,
email: updatedCompany.email,
status: updatedCompany.status,
maxUsers: updatedCompany.maxUsers,
};
state.setOriginalData(companyData);
setOriginalData(companyData);
toast({
title: "Success",
description: "Company updated successfully",
@ -568,7 +220,7 @@ export default function CompanyManagement() {
variant: "destructive",
});
} finally {
state.setIsSaving(false);
setIsSaving(false);
}
};
@ -583,10 +235,8 @@ export default function CompanyManagement() {
});
if (response.ok) {
state.setCompany((prev) =>
prev ? { ...prev, status: newStatus } : null
);
state.setEditData((prev) => ({ ...prev, status: newStatus }));
setCompany((prev) => (prev ? { ...prev, status: newStatus } : null));
setEditData((prev) => ({ ...prev, status: newStatus }));
toast({
title: "Success",
description: `Company ${statusAction}d successfully`,
@ -604,54 +254,16 @@ export default function CompanyManagement() {
};
const confirmNavigation = () => {
if (state.pendingNavigation) {
router.push(state.pendingNavigation);
state.setPendingNavigation(null);
if (pendingNavigation) {
router.push(pendingNavigation);
setPendingNavigation(null);
}
state.setShowUnsavedChangesDialog(false);
setShowUnsavedChangesDialog(false);
};
const cancelNavigation = () => {
state.setPendingNavigation(null);
state.setShowUnsavedChangesDialog(false);
};
const handleInviteUser = async () => {
try {
const response = await fetch(
`/api/platform/companies/${params.id}/users`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(state.inviteData),
}
);
if (response.ok) {
state.setShowInviteUser(false);
state.setInviteData({ name: "", email: "", role: "USER" });
// Refresh company data to show new user
const updatedResponse = await fetch(
`/api/platform/companies/${params.id}`
);
if (updatedResponse.ok) {
const updatedData = await updatedResponse.json();
state.setCompany(updatedData);
}
toast({
title: "Success",
description: "User invited successfully",
});
} else {
throw new Error("Failed to invite user");
}
} catch (_error) {
toast({
title: "Error",
description: "Failed to invite user",
variant: "destructive",
});
}
setPendingNavigation(null);
setShowUnsavedChangesDialog(false);
};
// Protect against browser back/forward and other navigation
@ -669,6 +281,7 @@ export default function CompanyManagement() {
"You have unsaved changes. Are you sure you want to leave this page?"
);
if (!confirmLeave) {
// Push the current state back to prevent navigation
window.history.pushState(null, "", window.location.href);
e.preventDefault();
}
@ -684,6 +297,37 @@ export default function CompanyManagement() {
};
}, [hasUnsavedChanges]);
const handleInviteUser = async () => {
try {
const response = await fetch(
`/api/platform/companies/${params.id}/users`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(inviteData),
}
);
if (response.ok) {
setShowInviteUser(false);
setInviteData({ name: "", email: "", role: "USER" });
fetchCompany(); // Refresh company data
toast({
title: "Success",
description: "User invited successfully",
});
} else {
throw new Error("Failed to invite user");
}
} catch (_error) {
toast({
title: "Error",
description: "Failed to invite user",
variant: "destructive",
});
}
};
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case "ACTIVE":
@ -699,7 +343,7 @@ export default function CompanyManagement() {
}
};
if (status === "loading" || state.isLoading) {
if (status === "loading" || isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">Loading company details...</div>
@ -707,7 +351,7 @@ export default function CompanyManagement() {
);
}
if (!session?.user?.isPlatformUser || !state.company) {
if (!session?.user?.isPlatformUser || !company) {
return null;
}
@ -730,10 +374,10 @@ export default function CompanyManagement() {
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{state.company.name}
{company.name}
</h1>
<Badge variant={getStatusBadgeVariant(state.company.status)}>
{state.company.status}
<Badge variant={getStatusBadgeVariant(company.status)}>
{company.status}
</Badge>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
@ -746,7 +390,7 @@ export default function CompanyManagement() {
<Button
variant="outline"
size="sm"
onClick={() => state.setShowInviteUser(true)}
onClick={() => setShowInviteUser(true)}
>
<UserPlus className="w-4 h-4 mr-2" />
Invite User
@ -778,10 +422,10 @@ export default function CompanyManagement() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{state.company.users.length}
{company.users.length}
</div>
<p className="text-xs text-muted-foreground">
of {state.company.maxUsers} maximum
of {company.maxUsers} maximum
</p>
</CardContent>
</Card>
@ -795,7 +439,7 @@ export default function CompanyManagement() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{state.company._count.sessions}
{company._count.sessions}
</div>
</CardContent>
</Card>
@ -809,7 +453,7 @@ export default function CompanyManagement() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{state.company._count.imports}
{company._count.imports}
</div>
</CardContent>
</Card>
@ -821,25 +465,160 @@ export default function CompanyManagement() {
</CardHeader>
<CardContent>
<div className="text-sm font-bold">
{new Date(state.company.createdAt).toLocaleDateString()}
{new Date(company.createdAt).toLocaleDateString()}
</div>
</CardContent>
</Card>
</div>
{/* Company Info */}
{renderCompanyInfoCard(
state,
canEdit,
companyNameFieldId,
companyEmailFieldId,
maxUsersFieldId,
hasUnsavedChanges,
handleSave
<Card>
<CardHeader>
<CardTitle>Company Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor={companyNameFieldId}>Company Name</Label>
<Input
id={companyNameFieldId}
value={editData.name || ""}
onChange={(e) =>
setEditData((prev) => ({
...prev,
name: e.target.value,
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor={companyEmailFieldId}>Contact Email</Label>
<Input
id={companyEmailFieldId}
type="email"
value={editData.email || ""}
onChange={(e) =>
setEditData((prev) => ({
...prev,
email: e.target.value,
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor={maxUsersFieldId}>Max Users</Label>
<Input
id={maxUsersFieldId}
type="number"
value={editData.maxUsers || 0}
onChange={(e) =>
setEditData((prev) => ({
...prev,
maxUsers: Number.parseInt(e.target.value),
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor="status">Status</Label>
<Select
value={editData.status}
onValueChange={(value) =>
setEditData((prev) => ({ ...prev, status: value }))
}
disabled={!canEdit}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACTIVE">Active</SelectItem>
<SelectItem value="TRIAL">Trial</SelectItem>
<SelectItem value="SUSPENDED">Suspended</SelectItem>
<SelectItem value="ARCHIVED">Archived</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{canEdit && hasUnsavedChanges() && (
<div className="flex gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => {
setEditData(originalData);
}}
>
Cancel Changes
</Button>
<Button onClick={handleSave} disabled={isSaving}>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "Saving..." : "Save Changes"}
</Button>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{renderUsersTab(state, canEdit)}
<TabsContent value="users" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Users className="w-5 h-5" />
Users ({company.users.length})
</span>
{canEdit && (
<Button size="sm" onClick={() => setShowInviteUser(true)}>
<UserPlus className="w-4 h-4 mr-2" />
Invite User
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{company.users.map((user) => (
<div
key={user.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-blue-600 dark:text-blue-300">
{user.name?.charAt(0) ||
user.email.charAt(0).toUpperCase()}
</span>
</div>
<div>
<div className="font-medium">
{user.name || "No name"}
</div>
<div className="text-sm text-muted-foreground">
{user.email}
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline">{user.role}</Badge>
<div className="text-sm text-muted-foreground">
Joined {new Date(user.createdAt).toLocaleDateString()}
</div>
</div>
</div>
))}
{company.users.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No users found. Invite the first user to get started.
</div>
)}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="settings" className="space-y-6">
<Card>
@ -862,9 +641,9 @@ export default function CompanyManagement() {
<AlertDialogTrigger asChild>
<Button
variant="destructive"
disabled={state.company.status === "SUSPENDED"}
disabled={company.status === "SUSPENDED"}
>
{state.company.status === "SUSPENDED"
{company.status === "SUSPENDED"
? "Already Suspended"
: "Suspend"}
</Button>
@ -889,7 +668,7 @@ export default function CompanyManagement() {
</AlertDialog>
</div>
{state.company.status === "SUSPENDED" && (
{company.status === "SUSPENDED" && (
<div className="flex items-center justify-between p-4 border border-green-200 dark:border-green-800 rounded-lg">
<div>
<h3 className="font-medium">Reactivate Company</h3>
@ -927,7 +706,7 @@ export default function CompanyManagement() {
</div>
{/* Invite User Dialog */}
{state.showInviteUser && (
{showInviteUser && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-md mx-4">
<CardHeader>
@ -938,12 +717,9 @@ export default function CompanyManagement() {
<Label htmlFor={inviteNameFieldId}>Name</Label>
<Input
id={inviteNameFieldId}
value={state.inviteData.name}
value={inviteData.name}
onChange={(e) =>
state.setInviteData((prev) => ({
...prev,
name: e.target.value,
}))
setInviteData((prev) => ({ ...prev, name: e.target.value }))
}
placeholder="User's full name"
/>
@ -953,9 +729,9 @@ export default function CompanyManagement() {
<Input
id={inviteEmailFieldId}
type="email"
value={state.inviteData.email}
value={inviteData.email}
onChange={(e) =>
state.setInviteData((prev) => ({
setInviteData((prev) => ({
...prev,
email: e.target.value,
}))
@ -966,9 +742,9 @@ export default function CompanyManagement() {
<div>
<Label htmlFor="inviteRole">Role</Label>
<Select
value={state.inviteData.role}
value={inviteData.role}
onValueChange={(value) =>
state.setInviteData((prev) => ({ ...prev, role: value }))
setInviteData((prev) => ({ ...prev, role: value }))
}
>
<SelectTrigger>
@ -983,7 +759,7 @@ export default function CompanyManagement() {
<div className="flex gap-2 pt-4">
<Button
variant="outline"
onClick={() => state.setShowInviteUser(false)}
onClick={() => setShowInviteUser(false)}
className="flex-1"
>
Cancel
@ -991,7 +767,7 @@ export default function CompanyManagement() {
<Button
onClick={handleInviteUser}
className="flex-1"
disabled={!state.inviteData.email || !state.inviteData.name}
disabled={!inviteData.email || !inviteData.name}
>
<Mail className="w-4 h-4 mr-2" />
Send Invite
@ -1004,8 +780,8 @@ export default function CompanyManagement() {
{/* Unsaved Changes Dialog */}
<AlertDialog
open={state.showUnsavedChangesDialog}
onOpenChange={state.setShowUnsavedChangesDialog}
open={showUnsavedChangesDialog}
onOpenChange={setShowUnsavedChangesDialog}
>
<AlertDialogContent>
<AlertDialogHeader>

File diff suppressed because it is too large Load Diff

View File

@ -1,595 +0,0 @@
"use client";
import {
Activity,
AlertTriangle,
Bell,
BellOff,
CheckCircle,
Download,
Settings,
Shield,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { SecurityConfigModal } from "@/components/security/SecurityConfigModal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface SecurityMetrics {
totalEvents: number;
criticalEvents: number;
activeAlerts: number;
resolvedAlerts: number;
securityScore: number;
threatLevel: string;
eventsByType: Record<string, number>;
alertsByType: Record<string, number>;
topThreats: Array<{ type: string; count: number }>;
geoDistribution: Record<string, number>;
timeDistribution: Array<{ hour: number; count: number }>;
userRiskScores: Array<{ userId: string; email: string; riskScore: number }>;
}
interface SecurityAlert {
id: string;
timestamp: string;
severity: string;
type: string;
title: string;
description: string;
eventType: string;
context: Record<string, unknown>;
metadata: Record<string, unknown>;
acknowledged: boolean;
}
/**
* Custom hook for security monitoring UI state (UI-only, no data fetching)
*/
function useSecurityMonitoringState() {
const [selectedTimeRange, setSelectedTimeRange] = useState("24h");
const [showConfig, setShowConfig] = useState(false);
const [autoRefresh, setAutoRefresh] = useState(true);
return {
selectedTimeRange,
setSelectedTimeRange,
showConfig,
setShowConfig,
autoRefresh,
setAutoRefresh,
};
}
/**
* Custom hook for security data fetching
*/
function useSecurityData(selectedTimeRange: string, autoRefresh: boolean) {
const [metrics, setMetrics] = useState<SecurityMetrics | null>(null);
const [alerts, setAlerts] = useState<SecurityAlert[]>([]);
const [loading, setLoading] = useState(true);
const loadSecurityData = useCallback(async () => {
try {
const startDate = getStartDateForRange(selectedTimeRange);
const endDate = new Date().toISOString();
const response = await fetch(
`/api/admin/security-monitoring?startDate=${startDate}&endDate=${endDate}`
);
if (!response.ok) throw new Error("Failed to load security data");
const data = await response.json();
setMetrics(data.metrics);
setAlerts(data.alerts);
} catch (error) {
console.error("Error loading security data:", error);
} finally {
setLoading(false);
}
}, [selectedTimeRange]);
useEffect(() => {
loadSecurityData();
if (autoRefresh) {
const interval = setInterval(loadSecurityData, 30000);
return () => clearInterval(interval);
}
}, [autoRefresh, loadSecurityData]);
return { metrics, alerts, loading, loadSecurityData, setAlerts };
}
/**
* Helper function to get date range for filtering
*/
function getStartDateForRange(range: string): string {
const now = new Date();
switch (range) {
case "1h":
return new Date(now.getTime() - 60 * 60 * 1000).toISOString();
case "24h":
return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
case "7d":
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
case "30d":
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
default:
return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
}
}
/**
* Helper function to get threat level color
*/
function getThreatLevelColor(level: string) {
switch (level?.toLowerCase()) {
case "critical":
return "bg-red-500";
case "high":
return "bg-orange-500";
case "moderate":
return "bg-yellow-500";
case "low":
return "bg-green-500";
default:
return "bg-gray-500";
}
}
/**
* Helper function to get severity color
*/
function getSeverityColor(severity: string) {
switch (severity?.toLowerCase()) {
case "critical":
return "destructive";
case "high":
return "destructive";
case "medium":
return "secondary";
case "low":
return "outline";
default:
return "outline";
}
}
/**
* Helper function to render dashboard header
*/
function renderDashboardHeader(
autoRefresh: boolean,
setAutoRefresh: (refresh: boolean) => void,
setShowConfig: (show: boolean) => void,
exportData: (format: "json" | "csv", type: "alerts" | "metrics") => void
) {
return (
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Security Monitoring
</h1>
<p className="text-muted-foreground">
Real-time security monitoring and threat detection
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? (
<Bell className="h-4 w-4" />
) : (
<BellOff className="h-4 w-4" />
)}
Auto Refresh
</Button>
<Button variant="outline" size="sm" onClick={() => setShowConfig(true)}>
<Settings className="h-4 w-4" />
Configure
</Button>
<Button
variant="outline"
size="sm"
onClick={() => exportData("json", "alerts")}
>
<Download className="h-4 w-4" />
Export
</Button>
</div>
</div>
);
}
/**
* Helper function to render time range selector
*/
function renderTimeRangeSelector(
selectedTimeRange: string,
setSelectedTimeRange: (range: string) => void
) {
return (
<div className="flex gap-2">
{["1h", "24h", "7d", "30d"].map((range) => (
<Button
key={range}
variant={selectedTimeRange === range ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTimeRange(range)}
>
{range}
</Button>
))}
</div>
);
}
/**
* Helper function to render security overview cards
*/
function renderSecurityOverview(metrics: SecurityMetrics | null) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Security Score</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics?.securityScore || 0}/100
</div>
<div
className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${getThreatLevelColor(metrics?.threatLevel || "")}`}
>
{metrics?.threatLevel || "Unknown"} Threat Level
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Alerts</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.activeAlerts || 0}</div>
<p className="text-xs text-muted-foreground">
{metrics?.resolvedAlerts || 0} resolved
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Security Events</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.totalEvents || 0}</div>
<p className="text-xs text-muted-foreground">
{metrics?.criticalEvents || 0} critical
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Top Threat</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-sm font-bold">
{metrics?.topThreats?.[0]?.type?.replace(/_/g, " ") || "None"}
</div>
<p className="text-xs text-muted-foreground">
{metrics?.topThreats?.[0]?.count || 0} instances
</p>
</CardContent>
</Card>
</div>
);
}
export default function SecurityMonitoringPage() {
const {
selectedTimeRange,
setSelectedTimeRange,
showConfig,
setShowConfig,
autoRefresh,
setAutoRefresh,
} = useSecurityMonitoringState();
const { metrics, alerts, loading, setAlerts, loadSecurityData } =
useSecurityData(selectedTimeRange, autoRefresh);
const acknowledgeAlert = async (alertId: string) => {
try {
const response = await fetch("/api/admin/security-monitoring/alerts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ alertId, action: "acknowledge" }),
});
if (response.ok) {
setAlerts(
alerts.map((alert) =>
alert.id === alertId ? { ...alert, acknowledged: true } : alert
)
);
}
} catch (error) {
console.error("Error acknowledging alert:", error);
}
};
const exportData = async (
format: "json" | "csv",
type: "alerts" | "metrics"
) => {
try {
const startDate = getStartDateForRange(selectedTimeRange);
const endDate = new Date().toISOString();
const response = await fetch(
`/api/admin/security-monitoring/export?format=${format}&type=${type}&startDate=${startDate}&endDate=${endDate}`
);
if (!response.ok) throw new Error("Export failed");
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `security-${type}-${new Date().toISOString().split("T")[0]}.${format}`;
a.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error("Error exporting data:", error);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
</div>
);
}
return (
<div className="container mx-auto px-4 py-6 space-y-6">
{renderDashboardHeader(
autoRefresh,
setAutoRefresh,
setShowConfig,
exportData
)}
{renderTimeRangeSelector(selectedTimeRange, setSelectedTimeRange)}
{renderSecurityOverview(metrics)}
<Tabs defaultValue="alerts" className="space-y-4">
<TabsList>
<TabsTrigger value="alerts">Active Alerts</TabsTrigger>
<TabsTrigger value="metrics">Security Metrics</TabsTrigger>
<TabsTrigger value="threats">Threat Analysis</TabsTrigger>
<TabsTrigger value="geography">Geographic View</TabsTrigger>
</TabsList>
<TabsContent value="alerts" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Active Security Alerts</CardTitle>
<CardDescription>
Real-time security alerts requiring attention
</CardDescription>
</CardHeader>
<CardContent>
{alerts.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<CheckCircle className="h-12 w-12 mx-auto mb-4" />
<p>No active alerts - system is secure</p>
</div>
) : (
<div className="space-y-4">
{alerts.map((alert) => (
<div
key={alert.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant={getSeverityColor(alert.severity)}>
{alert.severity}
</Badge>
<span className="font-medium">{alert.title}</span>
</div>
<p className="text-sm text-muted-foreground">
{alert.description}
</p>
<p className="text-xs text-muted-foreground">
{new Date(alert.timestamp).toLocaleString()}
</p>
</div>
{!alert.acknowledged && (
<Button
size="sm"
onClick={() => acknowledgeAlert(alert.id)}
>
Acknowledge
</Button>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="metrics" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Event Distribution</CardTitle>
</CardHeader>
<CardContent>
{metrics?.eventsByType && (
<div className="space-y-2">
{Object.entries(metrics.eventsByType).map(
([type, count]) => (
<div key={type} className="flex justify-between">
<span className="text-sm">
{type.replace(/_/g, " ")}
</span>
<span className="font-medium">{count}</span>
</div>
)
)}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>High-Risk Users</CardTitle>
</CardHeader>
<CardContent>
{metrics?.userRiskScores?.length ? (
<div className="space-y-2">
{metrics.userRiskScores.slice(0, 5).map((user) => (
<div key={user.userId} className="flex justify-between">
<span className="text-sm truncate">{user.email}</span>
<Badge
variant={
user.riskScore > 70
? "destructive"
: user.riskScore > 40
? "secondary"
: "outline"
}
>
{user.riskScore}
</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">
No high-risk users detected
</p>
)}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="threats" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Threat Analysis</CardTitle>
<CardDescription>
Analysis of current security threats and recommendations
</CardDescription>
</CardHeader>
<CardContent>
{metrics?.topThreats?.length ? (
<div className="space-y-4">
{metrics.topThreats.map((threat, index) => (
<div
key={threat.type}
className="flex items-center justify-between p-3 border rounded"
>
<div>
<span className="font-medium">
{threat.type.replace(/_/g, " ")}
</span>
<p className="text-sm text-muted-foreground">
{threat.count} occurrences
</p>
</div>
<Badge
variant={index === 0 ? "destructive" : "secondary"}
>
{index === 0 ? "Highest Priority" : "Monitor"}
</Badge>
</div>
))}
</div>
) : (
<p className="text-center py-8 text-muted-foreground">
No significant threats detected
</p>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="geography" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Geographic Distribution</CardTitle>
<CardDescription>
Security events by geographic location
</CardDescription>
</CardHeader>
<CardContent>
{metrics?.geoDistribution &&
Object.keys(metrics.geoDistribution).length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Object.entries(metrics.geoDistribution)
.sort(([, a], [, b]) => b - a)
.slice(0, 12)
.map(([country, count]) => (
<div
key={country}
className="text-center p-3 border rounded"
>
<div className="text-2xl font-bold">{count}</div>
<div className="text-sm text-muted-foreground">
{country}
</div>
</div>
))}
</div>
) : (
<p className="text-center py-8 text-muted-foreground">
No geographic data available
</p>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
{showConfig && (
<SecurityConfigModal
onClose={() => setShowConfig(false)}
onSave={() => {
setShowConfig(false);
loadSecurityData();
}}
/>
)}
</div>
);
}

View File

@ -1,428 +0,0 @@
"use client";
import { ArrowLeft, Key, Shield, User } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useId, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useToast } from "@/hooks/use-toast";
// Platform session hook - same as in dashboard
function usePlatformSession() {
const [session, setSession] = useState<{
user: {
id: string;
email: string;
name?: string;
role: string;
companyId?: string;
isPlatformUser?: boolean;
platformRole?: string;
};
} | null>(null);
const [status, setStatus] = useState<
"loading" | "authenticated" | "unauthenticated"
>("loading");
useEffect(() => {
const abortController = new AbortController();
const handleAuthSuccess = (sessionData: {
user?: {
id?: string;
email?: string;
name?: string;
role?: string;
companyId?: string;
isPlatformUser?: boolean;
platformRole?: string;
};
}) => {
if (sessionData?.user?.isPlatformUser) {
setSession({
user: {
id: sessionData.user.id || "",
email: sessionData.user.email || "",
name: sessionData.user.name,
role: sessionData.user.role || "",
companyId: sessionData.user.companyId,
isPlatformUser: sessionData.user.isPlatformUser,
platformRole: sessionData.user.platformRole,
},
});
setStatus("authenticated");
} else {
handleAuthFailure();
}
};
const handleAuthFailure = (error?: unknown) => {
if (error instanceof Error && error.name === "AbortError") return;
if (error) console.error("Platform session fetch error:", error);
setSession(null);
setStatus("unauthenticated");
};
const fetchSession = async () => {
try {
const response = await fetch("/api/platform/auth/session", {
signal: abortController.signal,
});
if (!response.ok) {
if (response.status === 401) return handleAuthFailure();
throw new Error(`Failed to fetch session: ${response.status}`);
}
const sessionData = await response.json();
handleAuthSuccess(sessionData);
} catch (error) {
handleAuthFailure(error);
}
};
fetchSession();
return () => {
abortController.abort();
};
}, []);
return { data: session, status };
}
export default function PlatformSettings() {
const { data: session, status } = usePlatformSession();
const router = useRouter();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
// Generate unique IDs for form elements
const nameId = useId();
const emailId = useId();
const currentPasswordId = useId();
const newPasswordId = useId();
const confirmPasswordId = useId();
const [profileData, setProfileData] = useState({
name: "",
email: "",
});
const [passwordData, setPasswordData] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
useEffect(() => {
if (status === "unauthenticated") {
router.push("/platform/login");
}
}, [status, router]);
useEffect(() => {
if (session?.user) {
setProfileData({
name: session.user.name || "",
email: session.user.email || "",
});
}
}, [session]);
const handleProfileUpdate = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
// TODO: Implement profile update API endpoint
toast({
title: "Profile Updated",
description: "Your profile has been updated successfully.",
});
} catch (_error) {
toast({
title: "Error",
description: "Failed to update profile. Please try again.",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
const handlePasswordChange = async (e: React.FormEvent) => {
e.preventDefault();
if (passwordData.newPassword !== passwordData.confirmPassword) {
toast({
title: "Error",
description: "New passwords do not match.",
variant: "destructive",
});
return;
}
if (passwordData.newPassword.length < 12) {
toast({
title: "Error",
description: "Password must be at least 12 characters long.",
variant: "destructive",
});
return;
}
setIsLoading(true);
try {
// TODO: Implement password change API endpoint
toast({
title: "Password Changed",
description: "Your password has been changed successfully.",
});
setPasswordData({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
} catch (_error) {
toast({
title: "Error",
description: "Failed to change password. Please try again.",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
if (status === "loading") {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto" />
<p className="mt-4 text-muted-foreground">Loading...</p>
</div>
</div>
);
}
if (!session?.user?.isPlatformUser) {
return null;
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="border-b bg-white dark:bg-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-6">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/platform/dashboard")}
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Dashboard
</Button>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Platform Settings
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
Manage your platform account settings
</p>
</div>
</div>
</div>
</div>
</div>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Tabs defaultValue="profile" className="space-y-6">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="profile">
<User className="w-4 h-4 mr-2" />
Profile
</TabsTrigger>
<TabsTrigger value="security">
<Key className="w-4 h-4 mr-2" />
Security
</TabsTrigger>
<TabsTrigger value="advanced">
<Shield className="w-4 h-4 mr-2" />
Advanced
</TabsTrigger>
</TabsList>
<TabsContent value="profile" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>
Update your platform account profile
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleProfileUpdate} className="space-y-4">
<div>
<Label htmlFor={nameId}>Name</Label>
<Input
id={nameId}
value={profileData.name}
onChange={(e) =>
setProfileData({ ...profileData, name: e.target.value })
}
placeholder="Your name"
/>
</div>
<div>
<Label htmlFor={emailId}>Email</Label>
<Input
id={emailId}
type="email"
value={profileData.email}
disabled
className="bg-gray-50"
/>
<p className="text-sm text-muted-foreground mt-1">
Email cannot be changed
</p>
</div>
<div>
<Label>Role</Label>
<Input
value={session.user.platformRole || "N/A"}
disabled
className="bg-gray-50"
/>
</div>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Saving..." : "Save Changes"}
</Button>
</form>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="security" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Change Password</CardTitle>
<CardDescription>
Update your platform account password
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handlePasswordChange} className="space-y-4">
<div>
<Label htmlFor={currentPasswordId}>Current Password</Label>
<Input
id={currentPasswordId}
type="password"
value={passwordData.currentPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
currentPassword: e.target.value,
})
}
required
/>
</div>
<div>
<Label htmlFor={newPasswordId}>New Password</Label>
<Input
id={newPasswordId}
type="password"
value={passwordData.newPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
newPassword: e.target.value,
})
}
required
/>
<p className="text-sm text-muted-foreground mt-1">
Must be at least 12 characters long
</p>
</div>
<div>
<Label htmlFor={confirmPasswordId}>
Confirm New Password
</Label>
<Input
id={confirmPasswordId}
type="password"
value={passwordData.confirmPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
confirmPassword: e.target.value,
})
}
required
/>
</div>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Changing..." : "Change Password"}
</Button>
</form>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="advanced" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Advanced Settings</CardTitle>
<CardDescription>
Platform administration options
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-lg border p-4">
<h3 className="font-medium mb-2">Platform Role</h3>
<p className="text-sm text-muted-foreground">
You are logged in as a{" "}
<strong>
{session.user.platformRole || "Platform User"}
</strong>
</p>
</div>
<div className="rounded-lg border p-4">
<h3 className="font-medium mb-2">Session Information</h3>
<div className="space-y-1 text-sm text-muted-foreground">
<p>User ID: {session.user.id}</p>
<p>Session Type: Platform</p>
</div>
</div>
{session.user.platformRole === "SUPER_ADMIN" && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<h3 className="font-medium mb-2 text-red-900">
Super Admin Options
</h3>
<p className="text-sm text-red-700 mb-3">
Advanced administrative options are available in the
individual company management pages.
</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
}

View File

@ -2,8 +2,6 @@
import { SessionProvider } from "next-auth/react";
import type { ReactNode } from "react";
import { CSRFProvider } from "@/components/providers/CSRFProvider";
import { TRPCProvider } from "@/components/providers/TRPCProvider";
import { ThemeProvider } from "@/components/theme-provider";
export function Providers({ children }: { children: ReactNode }) {
@ -20,9 +18,7 @@ export function Providers({ children }: { children: ReactNode }) {
refetchInterval={30 * 60}
refetchOnWindowFocus={false}
>
<CSRFProvider>
<TRPCProvider>{children}</TRPCProvider>
</CSRFProvider>
{children}
</SessionProvider>
</ThemeProvider>
);

View File

@ -2,24 +2,29 @@ import { PrismaClient } from "@prisma/client";
import { ProcessingStatusManager } from "./lib/processingStatusManager";
const prisma = new PrismaClient();
const statusManager = new ProcessingStatusManager(prisma);
const PIPELINE_STAGES = [
async function checkRefactoredPipelineStatus() {
try {
console.log("=== REFACTORED PIPELINE STATUS ===\n");
// Get pipeline status using the new system
const pipelineStatus = await ProcessingStatusManager.getPipelineStatus();
console.log(`Total Sessions: ${pipelineStatus.totalSessions}\n`);
// Display status for each stage
const stages = [
"CSV_IMPORT",
"TRANSCRIPT_FETCH",
"SESSION_CREATION",
"AI_ANALYSIS",
"QUESTION_EXTRACTION",
];
];
/**
* Display status for a single pipeline stage
*/
function displayStageStatus(
stage: string,
stageData: Record<string, number> = {}
) {
for (const stage of stages) {
console.log(`${stage}:`);
const stageData = pipelineStatus.pipeline[stage] || {};
const pending = stageData.PENDING || 0;
const inProgress = stageData.IN_PROGRESS || 0;
const completed = stageData.COMPLETED || 0;
@ -32,17 +37,12 @@ function displayStageStatus(
console.log(` FAILED: ${failed}`);
console.log(` SKIPPED: ${skipped}`);
console.log("");
}
}
/**
* Display what needs processing across all stages
*/
function displayProcessingNeeds(pipelineStatus: {
pipeline: Record<string, unknown>;
}) {
// Show what needs processing
console.log("=== WHAT NEEDS PROCESSING ===");
for (const stage of PIPELINE_STAGES) {
for (const stage of stages) {
const stageData = pipelineStatus.pipeline[stage] || {};
const pending = stageData.PENDING || 0;
const failed = stageData.FAILED || 0;
@ -51,76 +51,38 @@ function displayProcessingNeeds(pipelineStatus: {
console.log(`${stage}: ${pending} pending, ${failed} failed`);
}
}
}
/**
* Display failed sessions summary
*/
function displayFailedSessions(failedSessions: unknown[]) {
if (failedSessions.length === 0) return;
// Show failed sessions if any
const failedSessions = await ProcessingStatusManager.getFailedSessions();
if (failedSessions.length > 0) {
console.log("\n=== FAILED SESSIONS ===");
// biome-ignore lint/suspicious/noExplicitAny: Function parameter types from external API
failedSessions.slice(0, 5).forEach((failure: any) => {
failedSessions.slice(0, 5).forEach((failure) => {
console.log(
` ${failure.session.import?.externalSessionId || failure.sessionId}: ${failure.stage} - ${failure.errorMessage}`
);
});
if (failedSessions.length > 5) {
console.log(` ... and ${failedSessions.length - 5} more failed sessions`);
console.log(
` ... and ${failedSessions.length - 5} more failed sessions`
);
}
}
}
/**
* Display sessions ready for AI processing
*/
function displayReadyForAI(
readyForAI: Array<{
sessionId: string;
session: {
import?: { externalSessionId?: string };
createdAt: Date;
};
}>
) {
if (readyForAI.length === 0) return;
// Show sessions ready for AI processing
const readyForAI =
await ProcessingStatusManager.getSessionsNeedingProcessing(
"AI_ANALYSIS",
5
);
if (readyForAI.length > 0) {
console.log("\n=== SESSIONS READY FOR AI PROCESSING ===");
readyForAI.forEach((status) => {
console.log(
` ${status.session.import?.externalSessionId || status.sessionId} (created: ${status.session.createdAt})`
);
});
}
async function checkRefactoredPipelineStatus() {
try {
console.log("=== REFACTORED PIPELINE STATUS ===\n");
// Get pipeline status using the new system
const pipelineStatus = await statusManager.getPipelineStatus();
console.log(`Total Sessions: ${pipelineStatus.totalSessions}\n`);
// Display status for each stage
for (const stage of PIPELINE_STAGES) {
const stageData = pipelineStatus.pipeline[stage] || {};
displayStageStatus(stage, stageData);
}
// Show what needs processing
displayProcessingNeeds(pipelineStatus);
// Show failed sessions if any
const failedSessions = await statusManager.getFailedSessions();
displayFailedSessions(failedSessions);
// Show sessions ready for AI processing
const readyForAI = await statusManager.getSessionsNeedingProcessing(
"AI_ANALYSIS",
5
);
displayReadyForAI(readyForAI);
} catch (error) {
console.error("Error checking pipeline status:", error);
} finally {

308
components/Charts.tsx Normal file
View File

@ -0,0 +1,308 @@
"use client";
import Chart from "chart.js/auto";
import { useEffect, useRef } from "react";
import { getLocalizedLanguageName } from "../lib/localization"; // Corrected import path
interface SessionsData {
[date: string]: number;
}
interface CategoriesData {
[category: string]: number;
}
interface LanguageData {
[language: string]: number;
}
interface SessionsLineChartProps {
sessionsPerDay: SessionsData;
}
interface CategoriesBarChartProps {
categories: CategoriesData;
}
interface LanguagePieChartProps {
languages: LanguageData;
}
interface SentimentChartProps {
sentimentData: {
positive: number;
neutral: number;
negative: number;
};
}
interface TokenUsageChartProps {
tokenData: {
labels: string[];
values: number[];
costs: number[];
};
}
// Basic line and bar chart for metrics. Extend as needed.
export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current || !sessionsPerDay) return;
const ctx = ref.current.getContext("2d");
if (!ctx) return;
const chart = new Chart(ctx, {
type: "line",
data: {
labels: Object.keys(sessionsPerDay),
datasets: [
{
label: "Sessions",
data: Object.values(sessionsPerDay),
borderColor: "rgb(59, 130, 246)",
backgroundColor: "rgba(59, 130, 246, 0.1)",
borderWidth: 2,
tension: 0.3,
fill: true,
},
],
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } },
},
});
return () => chart.destroy();
}, [sessionsPerDay]);
return <canvas ref={ref} height={180} />;
}
export function CategoriesBarChart({ categories }: CategoriesBarChartProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current || !categories) return;
const ctx = ref.current.getContext("2d");
if (!ctx) return;
const chart = new Chart(ctx, {
type: "bar",
data: {
labels: Object.keys(categories),
datasets: [
{
label: "Categories",
data: Object.values(categories),
backgroundColor: "rgba(59, 130, 246, 0.7)",
borderWidth: 1,
},
],
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } },
},
});
return () => chart.destroy();
}, [categories]);
return <canvas ref={ref} height={180} />;
}
export function SentimentChart({ sentimentData }: SentimentChartProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current || !sentimentData) return;
const ctx = ref.current.getContext("2d");
if (!ctx) return;
const chart = new Chart(ctx, {
type: "doughnut",
data: {
labels: ["Positive", "Neutral", "Negative"],
datasets: [
{
data: [
sentimentData.positive,
sentimentData.neutral,
sentimentData.negative,
],
backgroundColor: [
"rgba(34, 197, 94, 0.8)", // green
"rgba(249, 115, 22, 0.8)", // orange
"rgba(239, 68, 68, 0.8)", // red
],
borderWidth: 1,
},
],
},
options: {
responsive: true,
plugins: {
legend: {
position: "right",
labels: {
usePointStyle: true,
padding: 20,
},
},
},
cutout: "65%",
},
});
return () => chart.destroy();
}, [sentimentData]);
return <canvas ref={ref} height={180} />;
}
export function LanguagePieChart({ languages }: LanguagePieChartProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current || !languages) return;
const ctx = ref.current.getContext("2d");
if (!ctx) return;
// Get top 5 languages, combine others
const entries = Object.entries(languages);
const topLanguages = entries.sort((a, b) => b[1] - a[1]).slice(0, 5);
// Sum the count of all other languages
const otherCount = entries
.slice(5)
.reduce((sum, [, count]) => sum + count, 0);
if (otherCount > 0) {
topLanguages.push(["Other", otherCount]);
}
// Store original ISO codes for tooltip
const isoCodes = topLanguages.map(([lang]) => lang);
const labels = topLanguages.map(([lang]) => {
if (lang === "Other") {
return "Other";
}
// Use getLocalizedLanguageName for robust name resolution
// Pass "en" to maintain consistency with previous behavior if navigator.language is different
return getLocalizedLanguageName(lang, "en");
});
const data = topLanguages.map(([, count]) => count);
const chart = new Chart(ctx, {
type: "pie",
data: {
labels,
datasets: [
{
data,
backgroundColor: [
"rgba(59, 130, 246, 0.8)",
"rgba(16, 185, 129, 0.8)",
"rgba(249, 115, 22, 0.8)",
"rgba(236, 72, 153, 0.8)",
"rgba(139, 92, 246, 0.8)",
"rgba(107, 114, 128, 0.8)",
],
borderWidth: 1,
},
],
},
options: {
responsive: true,
plugins: {
legend: {
position: "right",
labels: {
usePointStyle: true,
padding: 20,
},
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.label || "";
const value = context.formattedValue || "";
const index = context.dataIndex;
const originalIsoCode = isoCodes[index]; // Get the original code
// Only show ISO code if it's not "Other"
// and it's a valid 2-letter code (check lowercase version)
if (
originalIsoCode &&
originalIsoCode !== "Other" &&
/^[a-z]{2}$/.test(originalIsoCode.toLowerCase())
) {
return `${label} (${originalIsoCode.toUpperCase()}): ${value}`;
}
return `${label}: ${value}`;
},
},
},
},
},
});
return () => chart.destroy();
}, [languages]);
return <canvas ref={ref} height={180} />;
}
export function TokenUsageChart({ tokenData }: TokenUsageChartProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current || !tokenData) return;
const ctx = ref.current.getContext("2d");
if (!ctx) return;
const chart = new Chart(ctx, {
type: "bar",
data: {
labels: tokenData.labels,
datasets: [
{
label: "Tokens",
data: tokenData.values,
backgroundColor: "rgba(59, 130, 246, 0.7)",
borderWidth: 1,
yAxisID: "y",
},
{
label: "Cost (EUR)",
data: tokenData.costs,
backgroundColor: "rgba(16, 185, 129, 0.7)",
borderWidth: 1,
type: "line",
yAxisID: "y1",
},
],
},
options: {
responsive: true,
plugins: { legend: { display: true } },
scales: {
y: {
beginAtZero: true,
position: "left",
title: {
display: true,
text: "Token Count",
},
},
y1: {
beginAtZero: true,
position: "right",
grid: {
drawOnChartArea: false,
},
title: {
display: true,
text: "Cost (EUR)",
},
},
},
},
});
return () => chart.destroy();
}, [tokenData]);
return <canvas ref={ref} height={180} />;
}

155
components/DonutChart.tsx Normal file
View File

@ -0,0 +1,155 @@
"use client";
import Chart, { type BubbleDataPoint, type Point } from "chart.js/auto";
import { useEffect, useRef } from "react";
interface DonutChartProps {
data: {
labels: string[];
values: number[];
colors?: string[];
};
centerText?: {
title?: string;
value?: string | number;
};
}
export default function DonutChart({ data, centerText }: DonutChartProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current || !data.values.length) return;
const ctx = ref.current.getContext("2d");
if (!ctx) return;
// Default colors if not provided
const defaultColors: string[] = [
"rgba(59, 130, 246, 0.8)", // blue
"rgba(16, 185, 129, 0.8)", // green
"rgba(249, 115, 22, 0.8)", // orange
"rgba(236, 72, 153, 0.8)", // pink
"rgba(139, 92, 246, 0.8)", // purple
"rgba(107, 114, 128, 0.8)", // gray
];
const colors: string[] = data.colors || defaultColors;
// Helper to create an array of colors based on the data length
const getColors = () => {
const result: string[] = [];
for (let i = 0; i < data.values.length; i++) {
result.push(colors[i % colors.length]);
}
return result;
};
const chart = new Chart(ctx, {
type: "doughnut",
data: {
labels: data.labels,
datasets: [
{
data: data.values,
backgroundColor: getColors(),
borderWidth: 1,
hoverOffset: 5,
},
],
},
options: {
responsive: true,
maintainAspectRatio: true,
cutout: "70%",
plugins: {
legend: {
position: "right",
labels: {
boxWidth: 12,
padding: 20,
usePointStyle: true,
},
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.label || "";
const value = context.formattedValue;
const total = context.chart.data.datasets[0].data.reduce(
(
a: number,
b:
| number
| Point
| [number, number]
| BubbleDataPoint
| null
) => {
if (typeof b === "number") {
return a + b;
}
// Handle other types like Point, [number, number], BubbleDataPoint if necessary
// For now, we'll assume they don't contribute to the sum or are handled elsewhere
return a;
},
0
) as number;
const percentage = Math.round((context.parsed * 100) / total);
return `${label}: ${value} (${percentage}%)`;
},
},
},
},
},
plugins: centerText
? [
{
id: "centerText",
beforeDraw: (chart: Chart<"doughnut">) => {
const height = chart.height;
const ctx = chart.ctx;
ctx.restore();
// Calculate the actual chart area width (excluding legend)
// Legend is positioned on the right, so we adjust the center X coordinate
const chartArea = chart.chartArea;
const chartWidth = chartArea.right - chartArea.left;
// Get the center of just the chart area (not including the legend)
const centerX = chartArea.left + chartWidth / 2;
const centerY = height / 2;
// Title text
if (centerText.title) {
ctx.font = "1rem sans-serif"; // Consistent font
ctx.fillStyle = "#6B7280"; // Tailwind gray-500
ctx.textAlign = "center";
ctx.textBaseline = "middle"; // Align vertically
ctx.fillText(centerText.title, centerX, centerY - 10); // Adjust Y offset
}
// Value text
if (centerText.value !== undefined) {
ctx.font = "bold 1.5rem sans-serif"; // Consistent font, larger
ctx.fillStyle = "#1F2937"; // Tailwind gray-800
ctx.textAlign = "center";
ctx.textBaseline = "middle"; // Align vertically
ctx.fillText(
centerText.value.toString(),
centerX,
centerY + 15
); // Adjust Y offset
}
ctx.save();
},
},
]
: [],
});
return () => chart.destroy();
}, [data, centerText]);
return <canvas ref={ref} height={300} />;
}

View File

@ -1,7 +1,7 @@
"use client";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import "leaflet/dist/leaflet.css";
import * as countryCoder from "@rapideditor/country-coder";
@ -18,64 +18,45 @@ interface GeographicMapProps {
height?: number; // Optional height for the container
}
/**
* Get coordinates for a country using the country-coder library
* This automatically extracts coordinates from the country geometry
*/
function getCoordinatesFromCountryCoder(
countryCode: string
): [number, number] | undefined {
try {
const feature = countryCoder.feature(countryCode);
if (!feature?.geometry) {
return undefined;
}
// Get country coordinates from the @rapideditor/country-coder package
const getCountryCoordinates = (): Record<string, [number, number]> => {
// Initialize with some fallback coordinates for common countries
const coordinates: Record<string, [number, number]> = {
US: [37.0902, -95.7129],
GB: [55.3781, -3.436],
BA: [43.9159, 17.6791],
NL: [52.1326, 5.2913],
DE: [51.1657, 10.4515],
FR: [46.6034, 1.8883],
IT: [41.8719, 12.5674],
ES: [40.4637, -3.7492],
CA: [56.1304, -106.3468],
PL: [51.9194, 19.1451],
SE: [60.1282, 18.6435],
NO: [60.472, 8.4689],
FI: [61.9241, 25.7482],
CH: [46.8182, 8.2275],
AT: [47.5162, 14.5501],
BE: [50.8503, 4.3517],
DK: [56.2639, 9.5018],
CZ: [49.8175, 15.473],
HU: [47.1625, 19.5033],
PT: [39.3999, -8.2245],
GR: [39.0742, 21.8243],
RO: [45.9432, 24.9668],
IE: [53.4129, -8.2439],
BG: [42.7339, 25.4858],
HR: [45.1, 15.2],
SK: [48.669, 19.699],
SI: [46.1512, 14.9955],
};
// This function now primarily returns fallbacks.
// The actual fetching using @rapideditor/country-coder will be in the component's useEffect.
return coordinates;
};
// Extract center coordinates from the geometry
if (feature.geometry.type === "Point") {
const [lon, lat] = feature.geometry.coordinates;
return [lat, lon]; // Leaflet expects [lat, lon]
}
if (
feature.geometry.type === "Polygon" &&
feature.geometry.coordinates?.[0]?.[0]
) {
// For polygons, calculate centroid from the first ring
const coordinates = feature.geometry.coordinates[0];
let lat = 0;
let lon = 0;
for (const [lng, ltd] of coordinates) {
lon += lng;
lat += ltd;
}
return [lat / coordinates.length, lon / coordinates.length];
}
if (
feature.geometry.type === "MultiPolygon" &&
feature.geometry.coordinates?.[0]?.[0]?.[0]
) {
// For multipolygons, use the first polygon's first ring for centroid
const coordinates = feature.geometry.coordinates[0][0];
let lat = 0;
let lon = 0;
for (const [lng, ltd] of coordinates) {
lon += lng;
lat += ltd;
}
return [lat / coordinates.length, lon / coordinates.length];
}
return undefined;
} catch (error) {
console.warn(
`Failed to get coordinates for country ${countryCode}:`,
error
);
return undefined;
}
}
// Load coordinates once when module is imported
const DEFAULT_COORDINATES = getCountryCoordinates();
// Dynamically import the Map component to avoid SSR issues
// This ensures the component only loads on the client side
@ -90,7 +71,7 @@ const CountryMapComponent = dynamic(() => import("./Map"), {
export default function GeographicMap({
countries,
countryCoordinates = {},
countryCoordinates = DEFAULT_COORDINATES,
height = 400,
}: GeographicMapProps) {
const [countryData, setCountryData] = useState<CountryData[]>([]);
@ -101,82 +82,67 @@ export default function GeographicMap({
setIsClient(true);
}, []);
/**
* Get coordinates for a country code
*/
const getCountryCoordinates = useCallback(
(
code: string,
countryCoordinates: Record<string, [number, number]>
): [number, number] | undefined => {
// Try custom coordinates first (allows overrides)
let coords: [number, number] | undefined = countryCoordinates[code];
// Process country data when client is ready and dependencies change
useEffect(() => {
if (!isClient || !countries) return;
if (!coords) {
// Automatically get coordinates from country-coder library
coords = getCoordinatesFromCountryCoder(code);
try {
// Generate CountryData array for the Map component
const data: CountryData[] = Object.entries(countries || {})
.map(([code, count]) => {
let countryCoords: [number, number] | undefined =
countryCoordinates[code] || DEFAULT_COORDINATES[code];
if (!countryCoords) {
const feature = countryCoder.feature(code);
if (feature?.geometry) {
if (feature.geometry.type === "Point") {
const [lon, lat] = feature.geometry.coordinates;
countryCoords = [lat, lon]; // Leaflet expects [lat, lon]
} else if (
feature.geometry.type === "Polygon" &&
feature.geometry.coordinates &&
feature.geometry.coordinates[0] &&
feature.geometry.coordinates[0][0]
) {
// For Polygons, use the first coordinate of the first ring as a fallback representative point
const [lon, lat] = feature.geometry.coordinates[0][0];
countryCoords = [lat, lon]; // Leaflet expects [lat, lon]
} else if (
feature.geometry.type === "MultiPolygon" &&
feature.geometry.coordinates &&
feature.geometry.coordinates[0] &&
feature.geometry.coordinates[0][0] &&
feature.geometry.coordinates[0][0][0]
) {
// For MultiPolygons, use the first coordinate of the first ring of the first polygon
const [lon, lat] = feature.geometry.coordinates[0][0][0];
countryCoords = [lat, lon]; // Leaflet expects [lat, lon]
}
}
}
return coords;
},
[]
);
/**
* Process a single country entry into CountryData
*/
const processCountryEntry = useCallback(
(
code: string,
count: number,
countryCoordinates: Record<string, [number, number]>
): CountryData | null => {
const coordinates = getCountryCoordinates(code, countryCoordinates);
if (coordinates) {
return { code, count, coordinates };
if (countryCoords) {
return {
code,
count,
coordinates: countryCoords,
};
}
return null; // Skip if no coordinates found
},
[getCountryCoordinates]
);
/**
* Process all countries data into CountryData array
*/
const processCountriesData = useCallback(
(
countries: Record<string, number>,
countryCoordinates: Record<string, [number, number]>
): CountryData[] => {
const data = Object.entries(countries || {})
.map(([code, count]) =>
processCountryEntry(code, count, countryCoordinates)
)
})
.filter((item): item is CountryData => item !== null);
console.log(
`Found ${data.length} countries with coordinates out of ${Object.keys(countries).length} total countries`
);
return data;
},
[processCountryEntry]
);
// Process country data when client is ready and dependencies change
useEffect(() => {
if (!isClient || !countries) return;
try {
const data = processCountriesData(countries, countryCoordinates);
setCountryData(data);
} catch (error) {
console.error("Error processing geographic data:", error);
setCountryData([]);
}
}, [countries, countryCoordinates, isClient, processCountriesData]);
}, [countries, countryCoordinates, isClient]);
// Find the max count for scaling circles - handle empty or null countries object
const countryValues = countries ? Object.values(countries) : [];

View File

@ -70,7 +70,6 @@ export default function MessageViewer({ messages }: MessageViewerProps) {
? new Date(messages[0].timestamp).toLocaleString()
: "No timestamp"}
</span>
{/* prettier-ignore */}
<span>
Last message: {(() => {
const lastMessage = messages[messages.length - 1];

View File

@ -14,135 +14,80 @@ interface SessionDetailsProps {
}
/**
* Component for basic session information
* Component to display session details with formatted country and language names
*/
function SessionBasicInfo({ session }: { session: ChatSession }) {
export default function SessionDetails({ session }: SessionDetailsProps) {
// Using centralized formatCategory utility
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Session Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-2">
Basic Information
</h4>
<div className="space-y-2">
<div>
<span className="text-xs text-muted-foreground">Session ID:</span>
<code className="ml-2 text-xs font-mono bg-muted px-1 py-0.5 rounded">
<p className="text-sm text-muted-foreground">Session ID</p>
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">
{session.id.slice(0, 8)}...
</code>
</div>
<div>
<span className="text-xs text-muted-foreground">Start Time:</span>
<span className="ml-2 text-sm">
<p className="text-sm text-muted-foreground">Start Time</p>
<p className="font-medium">
{new Date(session.startTime).toLocaleString()}
</span>
</p>
</div>
{session.endTime && (
<div>
<span className="text-xs text-muted-foreground">End Time:</span>
<span className="ml-2 text-sm">
<p className="text-sm text-muted-foreground">End Time</p>
<p className="font-medium">
{new Date(session.endTime).toLocaleString()}
</span>
</p>
</div>
)}
</div>
</div>
</div>
);
}
/**
* Component for session location and language
*/
function SessionLocationInfo({ session }: { session: ChatSession }) {
return (
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-2">
Location & Language
</h4>
<div className="space-y-2">
{session.country && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Country:</span>
<CountryDisplay countryCode={session.country} />
</div>
)}
{session.language && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Language:</span>
<LanguageDisplay languageCode={session.language} />
</div>
)}
{session.ipAddress && (
<div>
<span className="text-xs text-muted-foreground">IP Address:</span>
<span className="ml-2 font-mono text-sm">
{session.ipAddress}
</span>
</div>
)}
</div>
</div>
</div>
);
}
/**
* Component for session metrics
*/
function SessionMetrics({ session }: { session: ChatSession }) {
return (
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-2">
Session Metrics
</h4>
<div className="space-y-2">
{session.messagesSent !== null &&
session.messagesSent !== undefined && (
<div>
<span className="text-xs text-muted-foreground">
Messages Sent:
</span>
<span className="ml-2 text-sm font-medium">
{session.messagesSent}
</span>
</div>
)}
{session.userId && (
<div>
<span className="text-xs text-muted-foreground">User ID:</span>
<span className="ml-2 text-sm">{session.userId}</span>
</div>
)}
</div>
</div>
</div>
);
}
/**
* Component for session analysis and status
*/
function SessionAnalysis({ session }: { session: ChatSession }) {
return (
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-2">
AI Analysis
</h4>
<div className="space-y-2">
{session.category && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Category:</span>
<Badge variant="secondary" className="text-xs">
<div>
<p className="text-sm text-muted-foreground">Category</p>
<Badge variant="secondary">
{formatCategory(session.category)}
</Badge>
</div>
)}
{session.sentiment && (
{session.language && (
<div>
<p className="text-sm text-muted-foreground">Language</p>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Sentiment:</span>
<LanguageDisplay languageCode={session.language} />
<Badge variant="outline" className="text-xs">
{session.language.toUpperCase()}
</Badge>
</div>
</div>
)}
{session.country && (
<div>
<p className="text-sm text-muted-foreground">Country</p>
<div className="flex items-center gap-2">
<CountryDisplay countryCode={session.country} />
<Badge variant="outline" className="text-xs">
{session.country}
</Badge>
</div>
</div>
)}
</div>
<div className="space-y-3">
{session.sentiment !== null && session.sentiment !== undefined && (
<div>
<p className="text-sm text-muted-foreground">Sentiment</p>
<Badge
variant={
session.sentiment === "positive"
@ -151,116 +96,83 @@ function SessionAnalysis({ session }: { session: ChatSession }) {
? "destructive"
: "secondary"
}
className="text-xs"
>
{session.sentiment.charAt(0).toUpperCase() +
session.sentiment.slice(1)}
</Badge>
</div>
)}
</div>
</div>
</div>
);
}
/**
* Component for session status flags
*/
function SessionStatusFlags({ session }: { session: ChatSession }) {
const hasStatusFlags =
session.escalated !== null || session.forwardedHr !== null;
if (!hasStatusFlags) return null;
return (
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-2">
Status Flags
</h4>
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Messages Sent</p>
<p className="font-medium">{session.messagesSent || 0}</p>
</div>
{session.avgResponseTime !== null &&
session.avgResponseTime !== undefined && (
<div>
<p className="text-sm text-muted-foreground">
Avg Response Time
</p>
<p className="font-medium">
{session.avgResponseTime.toFixed(2)}s
</p>
</div>
)}
{session.escalated !== null && session.escalated !== undefined && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Escalated:</span>
<Badge
variant={session.escalated ? "destructive" : "outline"}
className="text-xs"
>
<div>
<p className="text-sm text-muted-foreground">Escalated</p>
<Badge variant={session.escalated ? "destructive" : "default"}>
{session.escalated ? "Yes" : "No"}
</Badge>
</div>
)}
{session.forwardedHr !== null &&
session.forwardedHr !== undefined && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
Forwarded to HR:
</span>
<div>
<p className="text-sm text-muted-foreground">
Forwarded to HR
</p>
<Badge
variant={session.forwardedHr ? "destructive" : "outline"}
className="text-xs"
variant={session.forwardedHr ? "secondary" : "default"}
>
{session.forwardedHr ? "Yes" : "No"}
</Badge>
</div>
)}
</div>
</div>
</div>
);
}
/**
* Component for session summary
*/
function SessionSummary({ session }: { session: ChatSession }) {
if (!session.summary) return null;
{session.ipAddress && (
<div>
<p className="text-sm text-muted-foreground">IP Address</p>
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">
{session.ipAddress}
</code>
</div>
)}
</div>
</div>
return (
<div className="space-y-2">
<h4 className="text-sm font-medium text-muted-foreground">AI Summary</h4>
<p className="text-sm leading-relaxed border-l-4 border-muted pl-4 italic">
{(session.summary || session.initialMsg) && <Separator />}
{session.summary && (
<div>
<p className="text-sm text-muted-foreground mb-2">AI Summary</p>
<div className="bg-muted p-3 rounded-md text-sm">
{session.summary}
</p>
</div>
);
}
/**
* Component to display session details with formatted country and language names
*/
export default function SessionDetails({ session }: SessionDetailsProps) {
return (
<Card>
<CardHeader>
<CardTitle>Session Information</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<SessionBasicInfo session={session} />
<SessionLocationInfo session={session} />
</div>
<Separator />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<SessionMetrics session={session} />
<SessionAnalysis session={session} />
</div>
<SessionStatusFlags session={session} />
<SessionSummary session={session} />
)}
{!session.summary && session.initialMsg && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-muted-foreground">
<div>
<p className="text-sm text-muted-foreground mb-2">
Initial Message
</h4>
<p className="text-sm leading-relaxed border-l-4 border-muted pl-4 italic">
&quot;{session.initialMsg}&quot;
</p>
<div className="bg-muted p-3 rounded-md text-sm italic">
&quot;{session.initialMsg}&quot;
</div>
</div>
)}

View File

@ -99,24 +99,6 @@ const SessionsIcon = () => (
</svg>
);
const AuditLogIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<title>Audit Logs</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
);
const LogoutIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
@ -370,14 +352,6 @@ export default function Sidebar({
isActive={pathname === "/dashboard/users"}
onNavigate={onNavigate}
/>
<NavItem
href="/dashboard/audit-logs"
label="Audit Logs"
icon={<AuditLogIcon />}
isExpanded={isExpanded}
isActive={pathname === "/dashboard/audit-logs"}
onNavigate={onNavigate}
/>
</nav>
<div className="p-4 border-t mt-auto space-y-2">
{/* Theme Toggle */}

View File

@ -39,7 +39,7 @@ export default function TopQuestionsChart({
</CardHeader>
<CardContent>
<div className="space-y-4">
{data.map((question, index) => {
{data.map((question) => {
const percentage =
maxCount > 0 ? (question.count / maxCount) * 100 : 0;
@ -64,11 +64,7 @@ export default function TopQuestionsChart({
</div>
{/* Rank indicator */}
<div
className="absolute -left-1 top-0 w-6 h-6 bg-primary text-primary-foreground text-xs font-bold rounded-full flex items-center justify-center"
role="img"
aria-label={`Rank ${index + 1}`}
>
<div className="absolute -left-1 top-0 w-6 h-6 bg-primary text-primary-foreground text-xs font-bold rounded-full flex items-center justify-center">
{index + 1}
</div>
</div>

View File

@ -10,29 +10,53 @@ interface TranscriptViewerProps {
}
/**
* Renders a message bubble with proper styling
* Format the transcript content into a more readable format with styling
*/
function renderMessageBubble(
speaker: string,
messages: string[],
key: string
): React.ReactNode {
return (
<div key={key} className={`mb-3 ${speaker === "User" ? "text-right" : ""}`}>
function formatTranscript(content: string): React.ReactNode[] {
if (!content.trim()) {
return [<p key="empty">No transcript content available.</p>];
}
// Split the transcript by lines
const lines = content.split("\n");
const elements: React.ReactNode[] = [];
let currentSpeaker: string | null = null;
let currentMessages: string[] = [];
// Process each line
lines.forEach((line) => {
const trimmedLine = line.trim();
if (!trimmedLine) {
// Empty line, ignore
return;
}
// Check if this is a new speaker line
if (line.startsWith("User:") || line.startsWith("Assistant:")) {
// If we have accumulated messages for a previous speaker, add them
if (currentSpeaker && currentMessages.length > 0) {
elements.push(
<div
key={`message-${elements.length}`}
className={`mb-3 ${currentSpeaker === "User" ? "text-right" : ""}`}
>
<div
className={`inline-block px-4 py-2 rounded-lg ${
speaker === "User"
currentSpeaker === "User"
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-800"
}`}
>
{messages.map((msg, i) => (
{currentMessages.map((msg, i) => (
// Use ReactMarkdown to render each message part
<ReactMarkdown
key={`msg-${msg.substring(0, 20).replace(/\s/g, "-")}-${i}`}
rehypePlugins={[rehypeRaw]}
rehypePlugins={[rehypeRaw]} // Add rehypeRaw to plugins
components={{
p: "span",
a: ({ node, ...props }) => (
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
a: ({ node: _node, ...props }) => (
<a
className="text-sky-600 hover:text-sky-800 underline"
{...props}
@ -46,85 +70,61 @@ function renderMessageBubble(
</div>
</div>
);
}
/**
* Checks if a line indicates a new speaker
*/
function isNewSpeakerLine(line: string): boolean {
return line.startsWith("User:") || line.startsWith("Assistant:");
}
/**
* Extracts speaker and message content from a speaker line
*/
function extractSpeakerInfo(line: string): {
speaker: string;
content: string;
} {
const speaker = line.startsWith("User:") ? "User" : "Assistant";
const content = line.substring(line.indexOf(":") + 1).trim();
return { speaker, content };
}
/**
* Processes accumulated messages for a speaker
*/
function processAccumulatedMessages(
currentSpeaker: string | null,
currentMessages: string[],
elements: React.ReactNode[]
): void {
if (currentSpeaker && currentMessages.length > 0) {
elements.push(
renderMessageBubble(
currentSpeaker,
currentMessages,
`message-${elements.length}`
)
);
}
}
/**
* Format the transcript content into a more readable format with styling
*/
function formatTranscript(content: string): React.ReactNode[] {
if (!content.trim()) {
return [<p key="empty">No transcript content available.</p>];
}
const lines = content.split("\n");
const elements: React.ReactNode[] = [];
let currentSpeaker: string | null = null;
let currentMessages: string[] = [];
// Process each line
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine) {
continue; // Skip empty lines
}
if (isNewSpeakerLine(line)) {
// Process any accumulated messages from previous speaker
processAccumulatedMessages(currentSpeaker, currentMessages, elements);
currentMessages = [];
}
// Set new speaker and add initial content
const { speaker, content } = extractSpeakerInfo(trimmedLine);
currentSpeaker = speaker;
if (content) {
currentMessages.push(content);
// Set the new current speaker
currentSpeaker = trimmedLine.startsWith("User:") ? "User" : "Assistant";
// Add the content after "User:" or "Assistant:"
const messageContent = trimmedLine
.substring(trimmedLine.indexOf(":") + 1)
.trim();
if (messageContent) {
currentMessages.push(messageContent);
}
} else if (currentSpeaker) {
// Continuation of current speaker's message
// This is a continuation of the current speaker's message
currentMessages.push(trimmedLine);
}
}
});
// Process any remaining messages
processAccumulatedMessages(currentSpeaker, currentMessages, elements);
// Add any remaining messages
if (currentSpeaker && currentMessages.length > 0) {
elements.push(
<div
key={`message-${elements.length}`}
className={`mb-3 ${currentSpeaker === "User" ? "text-right" : ""}`}
>
<div
className={`inline-block px-4 py-2 rounded-lg ${
currentSpeaker === "User"
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-800"
}`}
>
{currentMessages.map((msg, i) => (
// Use ReactMarkdown to render each message part
<ReactMarkdown
key={`msg-final-${msg.substring(0, 20).replace(/\s/g, "-")}-${i}`}
rehypePlugins={[rehypeRaw]} // Add rehypeRaw to plugins
components={{
p: "span",
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
a: ({ node: _node, ...props }) => (
<a
className="text-sky-600 hover:text-sky-800 underline"
{...props}
/>
),
}}
>
{msg}
</ReactMarkdown>
))}
</div>
</div>
);
}
return elements;
}

View File

@ -1,545 +0,0 @@
"use client";
import {
Activity,
AlertTriangle,
CheckCircle,
Clock,
Download,
RefreshCw,
Shield,
TrendingUp,
XCircle,
Zap,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useToast } from "@/hooks/use-toast";
interface BatchMetrics {
operationStartTime: number;
requestCount: number;
successCount: number;
failureCount: number;
retryCount: number;
totalCost: number;
averageLatency: number;
circuitBreakerTrips: number;
performanceStats: {
p50: number;
p95: number;
p99: number;
};
}
interface CircuitBreakerStatus {
isOpen: boolean;
failures: number;
lastFailureTime: number;
}
interface SchedulerConfig {
enabled: boolean;
intervals: {
batchCreation: number;
statusCheck: number;
resultProcessing: number;
retryFailures: number;
};
thresholds: {
maxRetries: number;
circuitBreakerThreshold: number;
batchSize: number;
};
}
interface SchedulerStatus {
isRunning: boolean;
createBatchesRunning: boolean;
checkStatusRunning: boolean;
processResultsRunning: boolean;
retryFailedRunning: boolean;
isPaused: boolean;
consecutiveErrors: number;
lastErrorTime: Date | null;
circuitBreakers: Record<string, CircuitBreakerStatus>;
config: SchedulerConfig;
}
interface MonitoringData {
timestamp: string;
metrics: Record<string, BatchMetrics> | BatchMetrics;
schedulerStatus: SchedulerStatus;
circuitBreakerStatus: Record<string, CircuitBreakerStatus>;
systemHealth: {
schedulerRunning: boolean;
circuitBreakersOpen: boolean;
pausedDueToErrors: boolean;
consecutiveErrors: number;
};
}
function HealthStatusIcon({ status }: { status: string }) {
if (status === "healthy")
return <CheckCircle className="h-5 w-5 text-green-500" />;
if (status === "warning")
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
if (status === "critical")
return <XCircle className="h-5 w-5 text-red-500" />;
return null;
}
function SystemHealthCard({
health,
schedulerStatus,
}: {
health: { status: string; message: string };
schedulerStatus: SchedulerStatus;
}) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
System Health
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2 mb-4">
<HealthStatusIcon status={health.status} />
<span className="font-medium text-sm">{health.message}</span>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Batch Creation:</span>
<Badge
variant={
schedulerStatus?.createBatchesRunning ? "default" : "secondary"
}
>
{schedulerStatus?.createBatchesRunning ? "Running" : "Stopped"}
</Badge>
</div>
<div className="flex justify-between text-sm">
<span>Status Check:</span>
<Badge
variant={
schedulerStatus?.checkStatusRunning ? "default" : "secondary"
}
>
{schedulerStatus?.checkStatusRunning ? "Running" : "Stopped"}
</Badge>
</div>
<div className="flex justify-between text-sm">
<span>Result Processing:</span>
<Badge
variant={
schedulerStatus?.processResultsRunning ? "default" : "secondary"
}
>
{schedulerStatus?.processResultsRunning ? "Running" : "Stopped"}
</Badge>
</div>
</div>
</CardContent>
</Card>
);
}
function CircuitBreakerCard({
circuitBreakerStatus,
}: {
circuitBreakerStatus: Record<string, CircuitBreakerStatus> | null;
}) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Circuit Breakers
</CardTitle>
</CardHeader>
<CardContent>
{circuitBreakerStatus &&
Object.keys(circuitBreakerStatus).length > 0 ? (
<div className="space-y-2">
{Object.entries(circuitBreakerStatus).map(([key, status]) => (
<div key={key} className="flex justify-between text-sm">
<span>{key}:</span>
<Badge variant={!status.isOpen ? "default" : "destructive"}>
{status.isOpen ? "OPEN" : "CLOSED"}
</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">
No circuit breakers configured
</p>
)}
</CardContent>
</Card>
);
}
export default function BatchMonitoringDashboard() {
const [monitoringData, setMonitoringData] = useState<MonitoringData | null>(
null
);
const [isLoading, setIsLoading] = useState(true);
const [selectedCompany, setSelectedCompany] = useState<string>("all");
const [autoRefresh, setAutoRefresh] = useState(true);
const { toast } = useToast();
const fetchMonitoringData = useCallback(async () => {
try {
const params = new URLSearchParams();
if (selectedCompany !== "all") {
params.set("companyId", selectedCompany);
}
const response = await fetch(`/api/admin/batch-monitoring?${params}`);
if (response.ok) {
const data = await response.json();
setMonitoringData(data);
} else {
throw new Error("Failed to fetch monitoring data");
}
} catch (error) {
console.error("Failed to fetch batch monitoring data:", error);
toast({
title: "Error",
description: "Failed to load batch monitoring data",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
}, [selectedCompany, toast]);
useEffect(() => {
fetchMonitoringData();
}, [fetchMonitoringData]);
useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(fetchMonitoringData, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, [autoRefresh, fetchMonitoringData]);
const exportLogs = async (format: "json" | "csv") => {
try {
const response = await fetch("/api/admin/batch-monitoring/export", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
startDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Last 24 hours
endDate: new Date().toISOString(),
format,
}),
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `batch-logs-${Date.now()}.${format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast({
title: "Success",
description: `Batch logs exported as ${format.toUpperCase()}`,
});
}
} catch (_error) {
toast({
title: "Error",
description: "Failed to export logs",
variant: "destructive",
});
}
};
const getHealthStatus = () => {
if (!monitoringData)
return {
status: "unknown",
color: "gray",
message: "No monitoring data",
};
const { systemHealth } = monitoringData;
if (!systemHealth.schedulerRunning) {
return {
status: "critical",
color: "red",
message: "Scheduler not running",
};
}
if (systemHealth.pausedDueToErrors) {
return {
status: "warning",
color: "yellow",
message: "Paused due to errors",
};
}
if (systemHealth.circuitBreakersOpen) {
return {
status: "warning",
color: "yellow",
message: "Circuit breakers open",
};
}
if (systemHealth.consecutiveErrors > 0) {
return {
status: "warning",
color: "yellow",
message: `${systemHealth.consecutiveErrors} consecutive errors`,
};
}
return {
status: "healthy",
color: "green",
message: "All systems operational",
};
};
const renderMetricsCards = () => {
if (!monitoringData) return null;
const metrics = Array.isArray(monitoringData.metrics)
? monitoringData.metrics[0]
: typeof monitoringData.metrics === "object" &&
"operationStartTime" in monitoringData.metrics
? monitoringData.metrics
: Object.values(monitoringData.metrics)[0];
if (!metrics) return null;
const successRate =
metrics.requestCount > 0
? ((metrics.successCount / metrics.requestCount) * 100).toFixed(1)
: "0";
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Requests
</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics.requestCount}</div>
<p className="text-xs text-muted-foreground">
{metrics.successCount} successful, {metrics.failureCount} failed
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Success Rate</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{successRate}%</div>
<p className="text-xs text-muted-foreground">
{metrics.retryCount} retries performed
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Average Latency
</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics.averageLatency.toFixed(0)}ms
</div>
<p className="text-xs text-muted-foreground">
P95: {metrics.performanceStats.p95}ms
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Cost</CardTitle>
<Zap className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics.totalCost.toFixed(4)}
</div>
<p className="text-xs text-muted-foreground">
Circuit breaker trips: {metrics.circuitBreakerTrips}
</p>
</CardContent>
</Card>
</div>
);
};
const renderSystemStatus = () => {
if (!monitoringData) return null;
const health = getHealthStatus();
const { schedulerStatus, circuitBreakerStatus } = monitoringData;
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<SystemHealthCard health={health} schedulerStatus={schedulerStatus} />
<CircuitBreakerCard circuitBreakerStatus={circuitBreakerStatus} />
</div>
);
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4" />
<p>Loading batch monitoring data...</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold">Batch Processing Monitor</h2>
<p className="text-sm text-muted-foreground">
Real-time monitoring of OpenAI Batch API operations
</p>
</div>
<div className="flex gap-2">
<Select value={selectedCompany} onValueChange={setSelectedCompany}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Select company" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Companies</SelectItem>
{/* Add company options here */}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${autoRefresh ? "animate-spin" : ""}`}
/>
{autoRefresh ? "Auto" : "Manual"}
</Button>
<Button variant="outline" size="sm" onClick={fetchMonitoringData}>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
</div>
{renderSystemStatus()}
{renderMetricsCards()}
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="export">Export</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Batch Processing Overview</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-muted-foreground mb-4">
Last updated:{" "}
{monitoringData?.timestamp
? new Date(monitoringData.timestamp).toLocaleString()
: "Never"}
</div>
{monitoringData && (
<pre className="bg-muted p-4 rounded text-xs overflow-auto">
{JSON.stringify(monitoringData, null, 2)}
</pre>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="logs" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Recent Batch Processing Logs</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Real-time batch processing logs will be displayed here. For
detailed log analysis, use the export feature.
</p>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="export" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Export Batch Processing Data</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Export batch processing logs and metrics for detailed analysis.
</p>
<div className="flex gap-2">
<Button onClick={() => exportLogs("json")}>
<Download className="h-4 w-4 mr-2" />
Export JSON
</Button>
<Button variant="outline" onClick={() => exportLogs("csv")}>
<Download className="h-4 w-4 mr-2" />
Export CSV
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

View File

@ -1,286 +0,0 @@
/**
* tRPC Demo Component
*
* This component demonstrates how to use tRPC hooks for queries and mutations.
* Can be used as a reference for migrating existing components.
*/
"use client";
import { Loader2, RefreshCw } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { trpc } from "@/lib/trpc-client";
export function TRPCDemo() {
const [sessionFilters, setSessionFilters] = useState({
search: "",
page: 1,
limit: 5,
});
// Queries
const {
data: sessions,
isLoading: sessionsLoading,
error: sessionsError,
refetch: refetchSessions,
} = trpc.dashboard.getSessions.useQuery(sessionFilters);
const {
data: overview,
isLoading: overviewLoading,
error: overviewError,
} = trpc.dashboard.getOverview.useQuery({});
const {
data: topQuestions,
isLoading: questionsLoading,
error: questionsError,
} = trpc.dashboard.getTopQuestions.useQuery({ limit: 3 });
// Mutations
const refreshSessionsMutation = trpc.dashboard.refreshSessions.useMutation({
onSuccess: (data) => {
toast.success(data.message);
// Invalidate and refetch sessions
refetchSessions();
},
onError: (error) => {
toast.error(`Failed to refresh sessions: ${error.message}`);
},
});
const handleRefreshSessions = () => {
refreshSessionsMutation.mutate();
};
const handleSearchChange = (search: string) => {
setSessionFilters((prev) => ({ ...prev, search, page: 1 }));
};
return (
<div className="space-y-6 p-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">tRPC Demo</h2>
<Button
onClick={handleRefreshSessions}
disabled={refreshSessionsMutation.isPending}
variant="outline"
>
{refreshSessionsMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<RefreshCw className="h-4 w-4 mr-2" />
)}
Refresh Sessions
</Button>
</div>
{/* Overview Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">
Total Sessions
</CardTitle>
</CardHeader>
<CardContent>
{overviewError && (
<div className="text-red-600 text-sm mb-2">
Error: {overviewError.message}
</div>
)}
{overviewLoading ? (
<div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading...
</div>
) : (
<div className="text-2xl font-bold">
{overview?.totalSessions || 0}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Avg Messages</CardTitle>
</CardHeader>
<CardContent>
{overviewError && (
<div className="text-red-600 text-sm mb-2">
Error: {overviewError.message}
</div>
)}
{overviewLoading ? (
<div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading...
</div>
) : (
<div className="text-2xl font-bold">
{Math.round(overview?.avgMessagesSent || 0)}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">
Sentiment Distribution
</CardTitle>
</CardHeader>
<CardContent>
{overviewError && (
<div className="text-red-600 text-sm mb-2">
Error: {overviewError.message}
</div>
)}
{overviewLoading ? (
<div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading...
</div>
) : (
<div className="space-y-1">
{overview?.sentimentDistribution?.map((item) => (
<div
key={item.sentiment}
className="flex justify-between text-sm"
>
<span>{item.sentiment}</span>
<Badge variant="outline">{item.count}</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
{/* Top Questions */}
<Card>
<CardHeader>
<CardTitle>Top Questions</CardTitle>
</CardHeader>
<CardContent>
{questionsError && (
<div className="text-red-600 mb-4">
Error loading questions: {questionsError.message}
</div>
)}
{questionsLoading ? (
<div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading questions...
</div>
) : (
<div className="space-y-2">
{topQuestions?.map((item) => (
<div
key={item.question}
className="flex justify-between items-center"
>
<span className="text-sm">{item.question}</span>
<Badge>{item.count}</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Sessions List */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
Sessions
<div className="flex items-center space-x-2">
<Input
placeholder="Search sessions..."
value={sessionFilters.search}
onChange={(e) => handleSearchChange(e.target.value)}
className="w-64"
/>
</div>
</CardTitle>
</CardHeader>
<CardContent>
{sessionsError && (
<div className="text-red-600 mb-4">
Error loading sessions: {sessionsError.message}
</div>
)}
{sessionsLoading ? (
<div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading sessions...
</div>
) : (
<div className="space-y-4">
{sessions?.sessions?.map((session) => (
<div key={session.id} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<span className="font-medium">Session {session.id}</span>
<Badge
variant={
session.sentiment === "POSITIVE"
? "default"
: session.sentiment === "NEGATIVE"
? "destructive"
: "secondary"
}
>
{session.sentiment}
</Badge>
</div>
<span className="text-sm text-muted-foreground">
{session.messagesSent} messages
</span>
</div>
<p className="text-sm text-muted-foreground mb-2">
{session.summary}
</p>
{session.questions && session.questions.length > 0 && (
<div className="flex flex-wrap gap-1">
{session.questions.slice(0, 3).map((question) => (
<Badge
key={question}
variant="outline"
className="text-xs"
>
{question.length > 50
? `${question.slice(0, 50)}...`
: question}
</Badge>
))}
</div>
)}
</div>
))}
{/* Pagination Info */}
{sessions && (
<div className="text-center text-sm text-muted-foreground">
Showing {sessions.sessions.length} of{" "}
{sessions.pagination.totalCount} sessions (Page{" "}
{sessions.pagination.page} of {sessions.pagination.totalPages}
)
</div>
)}
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -1,179 +0,0 @@
/**
* CSRF Protected Form Component
*
* A wrapper component that automatically adds CSRF protection to forms.
* This component demonstrates how to integrate CSRF tokens into form submissions.
*/
"use client";
import type { FormEvent, ReactNode } from "react";
import { useId } from "react";
import { useCSRFForm } from "../../lib/hooks/useCSRF";
interface CSRFProtectedFormProps {
children: ReactNode;
action: string;
method?: "POST" | "PUT" | "DELETE" | "PATCH";
onSubmit?: (formData: FormData) => Promise<void> | void;
onError?: (error: Error) => void;
className?: string;
encType?: string;
}
/**
* Form component with automatic CSRF protection
*/
export function CSRFProtectedForm({
children,
action,
method = "POST",
onSubmit,
onError,
className,
encType,
}: CSRFProtectedFormProps) {
const { token, submitForm, addTokenToFormData } = useCSRFForm();
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
// Add CSRF token to form data
addTokenToFormData(formData);
try {
if (onSubmit) {
// Use custom submit handler
await onSubmit(formData);
} else {
// Use default form submission with CSRF protection
const response = await submitForm(action, formData);
if (!response.ok) {
throw new Error(`Form submission failed: ${response.status}`);
}
// Handle successful submission
console.log("Form submitted successfully");
}
} catch (error) {
console.error("Form submission error:", error);
// Notify user of the error
if (onError && error instanceof Error) {
onError(error);
} else {
// Fallback: show alert if no error handler provided
alert("An error occurred while submitting the form. Please try again.");
}
}
};
return (
<form
onSubmit={handleSubmit}
method={method}
action={action}
className={className}
encType={encType}
>
{/* Hidden CSRF token field for non-JS fallback */}
{token && <input type="hidden" name="csrf_token" value={token} />}
{children}
</form>
);
}
/**
* Example usage component showing how to use CSRF protected forms
*/
export function ExampleCSRFForm() {
// Generate unique IDs for form elements
const nameId = useId();
const emailId = useId();
const messageId = useId();
const handleCustomSubmit = async (formData: FormData) => {
// Custom form submission logic
// Filter out CSRF token for security when logging
const data = Object.fromEntries(formData.entries());
// biome-ignore lint/correctness/noUnusedVariables: csrf_token is intentionally extracted and discarded for security
const { csrf_token, ...safeData } = data;
console.log("Form data (excluding CSRF token):", safeData);
// You can process the form data here before submission
// The CSRF token is automatically included in formData
};
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">
CSRF Protected Form Example
</h2>
<CSRFProtectedForm
action="/api/example-endpoint"
onSubmit={handleCustomSubmit}
className="space-y-4"
>
<div>
<label
htmlFor={nameId}
className="block text-sm font-medium text-gray-700"
>
Name
</label>
<input
type="text"
id={nameId}
name="name"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div>
<label
htmlFor={emailId}
className="block text-sm font-medium text-gray-700"
>
Email
</label>
<input
type="email"
id={emailId}
name="email"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div>
<label
htmlFor={messageId}
className="block text-sm font-medium text-gray-700"
>
Message
</label>
<textarea
id={messageId}
name="message"
rows={4}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Submit
</button>
</CSRFProtectedForm>
</div>
);
}

View File

@ -18,7 +18,7 @@ import {
useRef,
} from "react";
import { Button } from "@/components/ui/button";
import { Button, type ButtonProps } from "@/components/ui/button";
type Api = {
fire: (options?: ConfettiOptions) => void;
@ -110,7 +110,7 @@ ConfettiComponent.displayName = "Confetti";
// Export as Confetti
export const Confetti = ConfettiComponent;
interface ConfettiButtonProps extends React.ComponentProps<typeof Button> {
interface ConfettiButtonProps extends ButtonProps {
options?: ConfettiOptions &
ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
children?: React.ReactNode;

View File

@ -26,7 +26,7 @@ export function Pointer({
style,
children,
...props
}: PointerProps): React.ReactElement {
}: PointerProps): JSX.Element {
const x = useMotionValue(0);
const y = useMotionValue(0);
const [isActive, setIsActive] = useState<boolean>(false);

View File

@ -1,161 +0,0 @@
/**
* CSRF Provider Component
*
* Provides CSRF token management for the entire application.
* Automatically fetches and manages CSRF tokens for client-side requests.
*/
"use client";
import type React from "react";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { CSRFClient } from "../../lib/csrf-client";
interface CSRFContextType {
token: string | null;
loading: boolean;
error: string | null;
refreshToken: () => Promise<void>;
addTokenToFetch: (options: RequestInit) => RequestInit;
addTokenToFormData: (formData: FormData) => FormData;
addTokenToObject: <T extends Record<string, unknown>>(
obj: T
) => T & { csrfToken: string };
}
const CSRFContext = createContext<CSRFContextType | undefined>(undefined);
interface CSRFProviderProps {
children: React.ReactNode;
}
/**
* CSRF Provider Component
*/
export function CSRFProvider({ children }: CSRFProviderProps) {
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
/**
* Fetch CSRF token from server
*/
const fetchToken = useCallback(async () => {
try {
setLoading(true);
setError(null);
// First check if we already have a token in cookies
const existingToken = CSRFClient.getToken();
if (existingToken) {
setToken(existingToken);
setLoading(false);
return;
}
// Fetch new token from server
const response = await fetch("/api/csrf-token", {
method: "GET",
credentials: "include",
});
if (!response.ok) {
throw new Error(`Failed to fetch CSRF token: ${response.status}`);
}
const data = await response.json();
if (data.success && data.token) {
setToken(data.token);
} else {
throw new Error("Invalid response from CSRF endpoint");
}
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Failed to fetch CSRF token";
setError(errorMessage);
console.error("CSRF token fetch error:", errorMessage);
} finally {
setLoading(false);
}
}, []);
/**
* Refresh token manually
*/
const refreshToken = async () => {
await fetchToken();
};
/**
* Initialize token on mount
*/
useEffect(() => {
fetchToken();
}, [fetchToken]);
/**
* Monitor token changes in cookies
*/
useEffect(() => {
const checkToken = () => {
const currentToken = CSRFClient.getToken();
if (currentToken !== token) {
setToken(currentToken);
}
};
// Check token every 30 seconds
const interval = setInterval(checkToken, 30 * 1000);
return () => clearInterval(interval);
}, [token]);
const contextValue: CSRFContextType = {
token,
loading,
error,
refreshToken,
addTokenToFetch: CSRFClient.addTokenToFetch,
addTokenToFormData: CSRFClient.addTokenToFormData,
addTokenToObject: CSRFClient.addTokenToObject,
};
return (
<CSRFContext.Provider value={contextValue}>{children}</CSRFContext.Provider>
);
}
/**
* Hook to use CSRF context
*/
export function useCSRFContext(): CSRFContextType {
const context = useContext(CSRFContext);
if (context === undefined) {
throw new Error("useCSRFContext must be used within a CSRFProvider");
}
return context;
}
/**
* Higher-order component to wrap components with CSRF protection
*/
export function withCSRF<P extends object>(Component: React.ComponentType<P>) {
const WrappedComponent = (props: P) => (
<CSRFProvider>
<Component {...props} />
</CSRFProvider>
);
WrappedComponent.displayName = `withCSRF(${Component.displayName || Component.name})`;
return WrappedComponent;
}

View File

@ -1,67 +0,0 @@
/**
* tRPC Provider Component
*
* Simplified provider for tRPC integration.
* The tRPC client is configured in trpc-client.ts and used directly in components.
*/
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";
interface TRPCProviderProps {
children: React.ReactNode;
}
export function TRPCProvider({ children }: TRPCProviderProps) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// Optimize refetching behavior for better performance
refetchOnWindowFocus: false,
refetchOnReconnect: true,
refetchOnMount: false, // Only refetch if stale
retry: (failureCount, error) => {
// Smart retry logic based on error type
if (
error?.message?.includes("401") ||
error?.message?.includes("403")
) {
return false; // Don't retry auth errors
}
return failureCount < 3;
},
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
// Optimized cache times based on data type
staleTime: 2 * 60 * 1000, // 2 minutes - data is fresh for 2 minutes
gcTime: 10 * 60 * 1000, // 10 minutes - keep unused data for 10 minutes
// Performance optimizations
networkMode: "online", // Only run queries when online
notifyOnChangeProps: ["data", "error", "isLoading"], // Reduce re-renders
},
mutations: {
// Optimize mutation behavior
retry: 2,
networkMode: "online",
throwOnError: false, // Handle errors gracefully in components
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
{process.env.NODE_ENV === "development" && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
);
}

View File

@ -1,132 +0,0 @@
"use client";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { COUNTRY_NAMES } from "../../lib/constants/countries";
interface GeographicThreatMapProps {
geoDistribution: Record<string, number>;
title?: string;
}
// Threat level configuration with colors
const THREAT_LEVELS = {
high: { color: "destructive", bgColor: "bg-red-500" },
medium: { color: "secondary", bgColor: "bg-yellow-500" },
low: { color: "outline", bgColor: "bg-blue-500" },
minimal: { color: "outline", bgColor: "bg-gray-400" },
} as const;
type ThreatLevel = keyof typeof THREAT_LEVELS;
export function GeographicThreatMap({
geoDistribution,
title = "Geographic Threat Distribution",
}: GeographicThreatMapProps) {
// Calculate values once for efficiency
const totalEvents = Object.values(geoDistribution).reduce(
(sum, count) => sum + count,
0
);
const maxEventCount = Math.max(...Object.values(geoDistribution));
const sortedCountries = Object.entries(geoDistribution)
.sort(([, a], [, b]) => b - a)
.slice(0, 12);
const getThreatLevel = (count: number, total: number): ThreatLevel => {
const percentage = (count / total) * 100;
if (percentage > 50) return "high";
if (percentage > 20) return "medium";
if (percentage > 5) return "low";
return "minimal";
};
const getCountryName = (code: string) => {
return COUNTRY_NAMES[code] || code;
};
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>
Security events by country ({totalEvents} total events)
</CardDescription>
</CardHeader>
<CardContent>
{sortedCountries.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<p>No geographic data available</p>
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{sortedCountries.map(([countryCode, count]) => {
const threatLevel = getThreatLevel(count, totalEvents);
const percentage = ((count / totalEvents) * 100).toFixed(1);
return (
<div
key={countryCode}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="font-medium">
{getCountryName(countryCode)}
</span>
<Badge
variant={
THREAT_LEVELS[threatLevel].color as
| "default"
| "secondary"
| "destructive"
| "outline"
}
className="text-xs"
>
{threatLevel}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{count} events ({percentage}%)
</p>
</div>
<div className="text-right">
<div className="text-2xl font-bold">{count}</div>
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${THREAT_LEVELS[threatLevel].bgColor}`}
style={{
width: `${Math.min(100, (count / maxEventCount) * 100)}%`,
}}
/>
</div>
</div>
</div>
);
})}
</div>
{Object.keys(geoDistribution).length > 12 && (
<div className="text-center pt-4 border-t">
<p className="text-sm text-muted-foreground">
And {Object.keys(geoDistribution).length - 12} more
countries...
</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
);
}

View File

@ -1,284 +0,0 @@
"use client";
import { AlertTriangle, CheckCircle, Eye, EyeOff } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface SecurityAlert {
id: string;
timestamp: string;
severity: string;
type: string;
title: string;
description: string;
eventType: string;
context: Record<string, unknown>;
metadata: Record<string, unknown>;
acknowledged: boolean;
}
interface SecurityAlertsTableProps {
alerts: SecurityAlert[];
onAcknowledge: (alertId: string) => void;
}
export function SecurityAlertsTable({
alerts,
onAcknowledge,
}: SecurityAlertsTableProps) {
const [showAcknowledged, setShowAcknowledged] = useState(false);
const [selectedAlert, setSelectedAlert] = useState<SecurityAlert | null>(
null
);
const getSeverityColor = (severity: string) => {
switch (severity?.toLowerCase()) {
case "critical":
return "destructive";
case "high":
return "destructive";
case "medium":
return "secondary";
case "low":
return "outline";
default:
return "outline";
}
};
const filteredAlerts = alerts.filter(
(alert) => showAcknowledged || !alert.acknowledged
);
const formatTimestamp = (timestamp: string) => {
return new Date(timestamp).toLocaleString();
};
const formatAlertType = (type: string) => {
return type
.replace(/_/g, " ")
.toLowerCase()
.replace(/\b\w/g, (l) => l.toUpperCase());
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<h3 className="text-lg font-semibold">Security Alerts</h3>
<p className="text-sm text-muted-foreground">
{filteredAlerts.length} alerts{" "}
{showAcknowledged ? "total" : "active"}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setShowAcknowledged(!showAcknowledged)}
>
{showAcknowledged ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
{showAcknowledged ? "Hide Acknowledged" : "Show All"}
</Button>
</div>
{filteredAlerts.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-8">
<CheckCircle className="h-12 w-12 text-green-500 mb-4" />
<h3 className="text-lg font-semibold mb-2">No Active Alerts</h3>
<p className="text-muted-foreground text-center">
All security alerts have been addressed. System is operating
normally.
</p>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Severity</TableHead>
<TableHead>Type</TableHead>
<TableHead>Description</TableHead>
<TableHead>Timestamp</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredAlerts.map((alert) => (
<TableRow
key={alert.id}
className={alert.acknowledged ? "opacity-60" : ""}
>
<TableCell>
<Badge variant={getSeverityColor(alert.severity)}>
{alert.severity}
</Badge>
</TableCell>
<TableCell>
<div className="space-y-1">
<span className="font-medium">
{formatAlertType(alert.type)}
</span>
<p className="text-xs text-muted-foreground">
{alert.eventType}
</p>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<span className="font-medium">{alert.title}</span>
<p className="text-sm text-muted-foreground line-clamp-2">
{alert.description}
</p>
</div>
</TableCell>
<TableCell>
<span className="text-sm">
{formatTimestamp(alert.timestamp)}
</span>
</TableCell>
<TableCell>
{alert.acknowledged ? (
<Badge variant="outline">
<CheckCircle className="h-3 w-3 mr-1" />
Acknowledged
</Badge>
) : (
<Badge variant="secondary">
<AlertTriangle className="h-3 w-3 mr-1" />
Active
</Badge>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setSelectedAlert(alert)}
>
<Eye className="h-3 w-3" />
</Button>
{!alert.acknowledged && (
<Button
size="sm"
onClick={() => onAcknowledge(alert.id)}
>
Acknowledge
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{/* Alert Details Modal */}
<Dialog
open={!!selectedAlert}
onOpenChange={() => setSelectedAlert(null)}
>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{selectedAlert?.title}
<div className="flex items-center gap-2">
<Badge
variant={getSeverityColor(selectedAlert?.severity || "")}
>
{selectedAlert?.severity}
</Badge>
<Badge variant="outline">
{formatAlertType(selectedAlert?.type || "")}
</Badge>
</div>
</DialogTitle>
<DialogDescription>
Security alert details and context information
</DialogDescription>
</DialogHeader>
{selectedAlert && (
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2">Description</h4>
<p className="text-sm text-muted-foreground">
{selectedAlert.description}
</p>
</div>
<div>
<h4 className="font-medium mb-2">Context</h4>
<div className="bg-muted p-3 rounded-md">
<pre className="text-xs overflow-auto">
{JSON.stringify(selectedAlert.context, null, 2)}
</pre>
</div>
</div>
{selectedAlert.metadata &&
Object.keys(selectedAlert.metadata).length > 0 && (
<div>
<h4 className="font-medium mb-2">Metadata</h4>
<div className="bg-muted p-3 rounded-md">
<pre className="text-xs overflow-auto">
{JSON.stringify(selectedAlert.metadata, null, 2)}
</pre>
</div>
</div>
)}
</div>
)}
<DialogFooter className="flex items-center justify-between pt-4 border-t">
<span className="text-sm text-muted-foreground">
{selectedAlert && formatTimestamp(selectedAlert.timestamp)}
</span>
<div className="flex gap-2">
{selectedAlert && !selectedAlert.acknowledged && (
<Button
onClick={() => {
onAcknowledge(selectedAlert.id);
setSelectedAlert(null);
}}
>
Acknowledge Alert
</Button>
)}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,502 +0,0 @@
"use client";
import { useCallback, useEffect, useId, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface SecurityConfig {
thresholds: {
failedLoginsPerMinute: number;
failedLoginsPerHour: number;
rateLimitViolationsPerMinute: number;
cspViolationsPerMinute: number;
adminActionsPerHour: number;
massDataAccessThreshold: number;
suspiciousIPThreshold: number;
};
alerting: {
enabled: boolean;
channels: string[];
suppressDuplicateMinutes: number;
escalationTimeoutMinutes: number;
};
retention: {
alertRetentionDays: number;
metricsRetentionDays: number;
};
}
interface SecurityConfigModalProps {
onClose: () => void;
onSave: () => void;
}
export function SecurityConfigModal({
onClose,
onSave,
}: SecurityConfigModalProps) {
const [config, setConfig] = useState<SecurityConfig | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Generate unique IDs for form elements
const failedLoginsPerMinuteId = useId();
const failedLoginsPerHourId = useId();
const rateLimitViolationsPerMinuteId = useId();
const cspViolationsPerMinuteId = useId();
const adminActionsPerHourId = useId();
const suspiciousIPThresholdId = useId();
const alertingEnabledId = useId();
const suppressDuplicateMinutesId = useId();
const escalationTimeoutMinutesId = useId();
const alertRetentionDaysId = useId();
const metricsRetentionDaysId = useId();
const loadConfig = useCallback(async () => {
try {
const response = await fetch("/api/admin/security-monitoring");
if (!response.ok) throw new Error("Failed to load config");
const data = await response.json();
setConfig(data.config);
} catch (error) {
console.error("Error loading config:", error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadConfig();
}, [loadConfig]);
const saveConfig = async () => {
if (!config) return;
setSaving(true);
try {
const response = await fetch("/api/admin/security-monitoring", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config),
});
if (!response.ok) throw new Error("Failed to save config");
onSave();
} catch (error) {
console.error("Error saving config:", error);
} finally {
setSaving(false);
}
};
const updateThreshold = (
key: keyof SecurityConfig["thresholds"],
value: number
) => {
if (!config) return;
setConfig({
...config,
thresholds: {
...config.thresholds,
[key]: value,
},
});
};
const updateAlerting = (
key: keyof SecurityConfig["alerting"],
value: unknown
) => {
if (!config) return;
setConfig({
...config,
alerting: {
...config.alerting,
[key]: value,
},
});
};
const updateRetention = (
key: keyof SecurityConfig["retention"],
value: number
) => {
if (!config) return;
setConfig({
...config,
retention: {
...config.retention,
[key]: value,
},
});
};
const toggleAlertChannel = (channel: string) => {
if (!config) return;
const channels = config.alerting.channels.includes(channel)
? config.alerting.channels.filter((c) => c !== channel)
: [...config.alerting.channels, channel];
updateAlerting("channels", channels);
};
if (loading) {
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
</div>
</DialogContent>
</Dialog>
);
}
if (!config) {
return (
<Dialog open onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Error</DialogTitle>
<DialogDescription>
Failed to load security configuration
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={onClose}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Security Monitoring Configuration</DialogTitle>
<DialogDescription>
Configure security monitoring thresholds, alerting, and data
retention
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="thresholds" className="space-y-4">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="thresholds">Thresholds</TabsTrigger>
<TabsTrigger value="alerting">Alerting</TabsTrigger>
<TabsTrigger value="retention">Data Retention</TabsTrigger>
</TabsList>
<TabsContent value="thresholds" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Detection Thresholds</CardTitle>
<CardDescription>
Configure when security alerts should be triggered
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor={failedLoginsPerMinuteId}>
Failed Logins per Minute
</Label>
<Input
id={failedLoginsPerMinuteId}
type="number"
min="1"
max="100"
value={config.thresholds.failedLoginsPerMinute}
onChange={(e) =>
updateThreshold(
"failedLoginsPerMinute",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor={failedLoginsPerHourId}>
Failed Logins per Hour
</Label>
<Input
id={failedLoginsPerHourId}
type="number"
min="1"
max="1000"
value={config.thresholds.failedLoginsPerHour}
onChange={(e) =>
updateThreshold(
"failedLoginsPerHour",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor={rateLimitViolationsPerMinuteId}>
Rate Limit Violations per Minute
</Label>
<Input
id={rateLimitViolationsPerMinuteId}
type="number"
min="1"
max="100"
value={config.thresholds.rateLimitViolationsPerMinute}
onChange={(e) =>
updateThreshold(
"rateLimitViolationsPerMinute",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor={cspViolationsPerMinuteId}>
CSP Violations per Minute
</Label>
<Input
id={cspViolationsPerMinuteId}
type="number"
min="1"
max="100"
value={config.thresholds.cspViolationsPerMinute}
onChange={(e) =>
updateThreshold(
"cspViolationsPerMinute",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor={adminActionsPerHourId}>
Admin Actions per Hour
</Label>
<Input
id={adminActionsPerHourId}
type="number"
min="1"
max="100"
value={config.thresholds.adminActionsPerHour}
onChange={(e) =>
updateThreshold(
"adminActionsPerHour",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor={suspiciousIPThresholdId}>
Suspicious IP Threshold
</Label>
<Input
id={suspiciousIPThresholdId}
type="number"
min="1"
max="100"
value={config.thresholds.suspiciousIPThreshold}
onChange={(e) =>
updateThreshold(
"suspiciousIPThreshold",
Number.parseInt(e.target.value)
)
}
/>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="alerting" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Alert Configuration</CardTitle>
<CardDescription>
Configure how and when alerts are sent
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Switch
id={alertingEnabledId}
checked={config.alerting.enabled}
onCheckedChange={(checked) =>
updateAlerting("enabled", checked)
}
/>
<Label htmlFor={alertingEnabledId}>
Enable Security Alerting
</Label>
</div>
<div className="space-y-2">
<Label>Alert Channels</Label>
<div className="flex flex-wrap gap-2">
{["EMAIL", "WEBHOOK", "SLACK", "DISCORD", "PAGERDUTY"].map(
(channel) => (
<Badge
key={channel}
variant={
config.alerting.channels.includes(channel)
? "default"
: "outline"
}
className="cursor-pointer"
onClick={() => toggleAlertChannel(channel)}
>
{channel}
</Badge>
)
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor={suppressDuplicateMinutesId}>
Suppress Duplicates (minutes)
</Label>
<Input
id={suppressDuplicateMinutesId}
type="number"
min="1"
max="1440"
value={config.alerting.suppressDuplicateMinutes}
onChange={(e) =>
updateAlerting(
"suppressDuplicateMinutes",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor={escalationTimeoutMinutesId}>
Escalation Timeout (minutes)
</Label>
<Input
id={escalationTimeoutMinutesId}
type="number"
min="5"
max="1440"
value={config.alerting.escalationTimeoutMinutes}
onChange={(e) =>
updateAlerting(
"escalationTimeoutMinutes",
Number.parseInt(e.target.value)
)
}
/>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="retention" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Data Retention</CardTitle>
<CardDescription>
Configure how long security data is stored
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor={alertRetentionDaysId}>
Alert Retention (days)
</Label>
<Input
id={alertRetentionDaysId}
type="number"
min="1"
max="3650"
value={config.retention.alertRetentionDays}
onChange={(e) =>
updateRetention(
"alertRetentionDays",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor={metricsRetentionDaysId}>
Metrics Retention (days)
</Label>
<Input
id={metricsRetentionDaysId}
type="number"
min="1"
max="3650"
value={config.retention.metricsRetentionDays}
onChange={(e) =>
updateRetention(
"metricsRetentionDays",
Number.parseInt(e.target.value)
)
}
/>
</div>
</div>
<div className="text-sm text-muted-foreground">
<p>
Alert data includes security alerts and acknowledgments
</p>
<p> Metrics data includes aggregated security statistics</p>
<p>
Audit logs are retained separately according to audit
policy
</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={saveConfig} disabled={saving}>
{saving ? "Saving..." : "Save Configuration"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -1,71 +0,0 @@
"use client";
import {
Bar,
BarChart,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
interface SecurityMetricsChartProps {
data: Array<{ hour: number; count: number }>;
type?: "line" | "bar";
title?: string;
}
export function SecurityMetricsChart({
data,
type = "line",
title,
}: SecurityMetricsChartProps) {
const chartData = data.map((item) => ({
hour: `${item.hour}:00`,
count: item.count,
}));
const ChartComponent = type === "line" ? LineChart : BarChart;
const DataComponent =
type === "line" ? (
<Line
type="monotone"
dataKey="count"
stroke="#8884d8"
strokeWidth={2}
dot={{ fill: "#8884d8", strokeWidth: 2 }}
/>
) : (
<Bar dataKey="count" fill="#8884d8" />
);
return (
<div className="space-y-2">
{title && <h3 className="text-lg font-semibold">{title}</h3>}
<ResponsiveContainer width="100%" height={300}>
<ChartComponent data={chartData}>
<XAxis
dataKey="hour"
tick={{ fontSize: 12 }}
tickLine={{ stroke: "#e5e7eb" }}
/>
<YAxis
tick={{ fontSize: 12 }}
tickLine={{ stroke: "#e5e7eb" }}
axisLine={{ stroke: "#e5e7eb" }}
/>
<Tooltip
contentStyle={{
backgroundColor: "#f9fafb",
border: "1px solid #e5e7eb",
borderRadius: "6px",
}}
/>
{DataComponent}
</ChartComponent>
</ResponsiveContainer>
</div>
);
}

View File

@ -1,93 +0,0 @@
"use client";
import { AlertCircle, AlertTriangle, Shield, Zap } from "lucide-react";
import { Badge } from "@/components/ui/badge";
interface ThreatLevelIndicatorProps {
level: "LOW" | "MODERATE" | "HIGH" | "CRITICAL";
score?: number;
size?: "sm" | "md" | "lg";
}
export function ThreatLevelIndicator({
level,
score,
size = "md",
}: ThreatLevelIndicatorProps) {
const getConfig = (threatLevel: string) => {
switch (threatLevel) {
case "CRITICAL":
return {
color: "destructive",
bgColor: "bg-red-500",
icon: Zap,
text: "Critical Threat",
description: "Immediate action required",
};
case "HIGH":
return {
color: "destructive",
bgColor: "bg-orange-500",
icon: AlertCircle,
text: "High Threat",
description: "Urgent attention needed",
};
case "MODERATE":
return {
color: "secondary",
bgColor: "bg-yellow-500",
icon: AlertTriangle,
text: "Moderate Threat",
description: "Monitor closely",
};
default:
return {
color: "outline",
bgColor: "bg-green-500",
icon: Shield,
text: "Low Threat",
description: "System is secure",
};
}
};
const config = getConfig(level);
const Icon = config.icon;
const sizeClasses = {
sm: { icon: "h-4 w-4", text: "text-sm", badge: "text-xs" },
md: { icon: "h-5 w-5", text: "text-base", badge: "text-sm" },
lg: { icon: "h-6 w-6", text: "text-lg", badge: "text-base" },
};
const classes = sizeClasses[size];
return (
<div className="flex items-center gap-2">
<div className={`p-2 rounded-full ${config.bgColor}`}>
<Icon className={`${classes.icon} text-white`} />
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge
variant={
config.color as
| "default"
| "secondary"
| "destructive"
| "outline"
}
className={classes.badge}
>
{config.text}
</Badge>
{score !== undefined && (
<span className={`font-medium ${classes.text}`}>{score}/100</span>
)}
</div>
<p className="text-xs text-muted-foreground">{config.description}</p>
</div>
</div>
);
}

View File

@ -1,17 +1,7 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
type Attribute = "class" | "data-theme" | "data-mode";
interface ThemeProviderProps {
children: React.ReactNode;
attribute?: Attribute | Attribute[];
defaultTheme?: string;
enableSystem?: boolean;
disableTransitionOnChange?: boolean;
forcedTheme?: string;
}
import type { ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

@ -1,5 +1,10 @@
"use client";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react";
import * as React from "react";
import {
type DayButton,
@ -9,6 +14,69 @@ import {
import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface CalendarRootProps {
className?: string;
rootRef?: React.Ref<HTMLDivElement>;
[key: string]: unknown;
}
const CalendarRoot = ({ className, rootRef, ...props }: CalendarRootProps) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
);
};
interface CalendarChevronProps {
className?: string;
orientation: "left" | "right" | "up" | "down";
[key: string]: unknown;
}
const CalendarChevron = ({
className,
orientation,
...props
}: CalendarChevronProps) => {
if (orientation === "left") {
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
}
if (orientation === "right") {
return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
}
if (orientation === "up") {
return (
<ChevronDownIcon
className={cn("size-4 rotate-180", className)}
{...props}
/>
);
}
return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
};
interface CalendarWeekNumberProps {
children: React.ReactNode;
[key: string]: unknown;
}
const CalendarWeekNumber = ({
children,
...props
}: CalendarWeekNumberProps) => {
return (
<td {...props}>
<div className="flex size-9 items-center justify-center p-0 text-sm">
{children}
</div>
</td>
);
};
function Calendar({
className,
classNames,
@ -120,7 +188,10 @@ function Calendar({
...classNames,
}}
components={{
Root: CalendarRoot,
Chevron: CalendarChevron,
DayButton: CalendarDayButton,
WeekNumber: CalendarWeekNumber,
...components,
}}
{...props}

View File

@ -3,13 +3,16 @@ import { ProcessingStatusManager } from "./lib/processingStatusManager";
const prisma = new PrismaClient();
/**
* Log pipeline status for each processing stage
*/
async function logPipelineStatus() {
async function debugImportStatus() {
try {
console.log("=== DEBUGGING PROCESSING STATUS (REFACTORED SYSTEM) ===\n");
// Get pipeline status using the new system
const pipelineStatus = await ProcessingStatusManager.getPipelineStatus();
console.log(`Total Sessions: ${pipelineStatus.totalSessions}\n`);
// Display status for each stage
const stages = [
"CSV_IMPORT",
"TRANSCRIPT_FETCH",
@ -25,130 +28,58 @@ async function logPipelineStatus() {
const pending = stageData.PENDING || 0;
const inProgress = stageData.IN_PROGRESS || 0;
const completed = stageData.COMPLETED || 0;
const skipped = stageData.SKIPPED || 0;
const failed = stageData.FAILED || 0;
const skipped = stageData.SKIPPED || 0;
console.log(` PENDING: ${pending}`);
console.log(` IN_PROGRESS: ${inProgress}`);
console.log(` COMPLETED: ${completed}`);
console.log(` FAILED: ${failed}`);
console.log(` SKIPPED: ${skipped}`);
console.log(` FAILED: ${failed}\n`);
console.log("");
}
}
/**
* Log session import relationship analysis
*/
async function logSessionImportRelationship() {
console.log("=== SESSION <-> IMPORT RELATIONSHIP ===");
const sessionWithImport = await prisma.session.count({
// Check Sessions vs SessionImports
console.log("=== SESSION IMPORT RELATIONSHIP ===");
const sessionsWithImports = await prisma.session.count({
where: { importId: { not: null } },
});
const totalSessions = await prisma.session.count();
const sessionWithoutImport = await prisma.session.count({
where: { importId: null },
});
const importWithSession = await prisma.sessionImport.count({
where: { session: { isNot: null } },
});
const importWithoutSession = await prisma.sessionImport.count({
where: { session: null },
});
console.log(`Sessions with ImportId: ${sessionWithImport}`);
console.log(`Sessions without ImportId: ${sessionWithoutImport}`);
console.log(`Imports with Session: ${importWithSession}`);
console.log(`Imports without Session: ${importWithoutSession}\n`);
}
/**
* Log failed processing sessions
*/
async function logFailedSessions() {
console.log("=== FAILED PROCESSING ANALYSIS ===");
const failedSessions = await prisma.sessionProcessingStatus.findMany({
where: { status: "FAILED" },
include: {
session: {
select: {
id: true,
import: {
select: { externalSessionId: true },
},
},
},
},
take: 5,
});
console.log(` Sessions with importId: ${sessionsWithImports}`);
console.log(` Total sessions: ${totalSessions}`);
// Show failed sessions if any
const failedSessions = await ProcessingStatusManager.getFailedSessions();
if (failedSessions.length > 0) {
console.log("Sample failed sessions:");
for (const failed of failedSessions) {
console.log("\n=== FAILED SESSIONS ===");
failedSessions.slice(0, 10).forEach((failure) => {
console.log(
` Session ${failed.session?.import?.externalSessionId || failed.sessionId} - Stage: ${failed.stage}, Error: ${failed.error}`
` ${failure.session.import?.externalSessionId || failure.sessionId}: ${failure.stage} - ${failure.errorMessage}`
);
});
if (failedSessions.length > 10) {
console.log(
` ... and ${failedSessions.length - 10} more failed sessions`
);
}
} else {
console.log("No failed processing found");
}
console.log("");
}
/**
* Log processing pipeline needs analysis
*/
async function logProcessingNeeds(pipelineStatus: {
pipeline: Record<string, Record<string, number>>;
}) {
console.log("=== WHAT NEEDS PROCESSING? ===");
const needsTranscriptFetch =
pipelineStatus.pipeline.TRANSCRIPT_FETCH?.PENDING || 0;
const needsSessionCreation =
pipelineStatus.pipeline.SESSION_CREATION?.PENDING || 0;
const needsAIAnalysis = pipelineStatus.pipeline.AI_ANALYSIS?.PENDING || 0;
const needsQuestionExtraction =
pipelineStatus.pipeline.QUESTION_EXTRACTION?.PENDING || 0;
if (needsTranscriptFetch > 0) {
console.log(`${needsTranscriptFetch} sessions need transcript fetching`);
}
if (needsSessionCreation > 0) {
console.log(`${needsSessionCreation} sessions need session creation`);
}
if (needsAIAnalysis > 0) {
console.log(`${needsAIAnalysis} sessions need AI analysis`);
}
if (needsQuestionExtraction > 0) {
console.log(`${needsQuestionExtraction} sessions need question extraction`);
console.log("\n✓ No failed sessions found");
}
if (
needsTranscriptFetch +
needsSessionCreation +
needsAIAnalysis +
needsQuestionExtraction ===
0
) {
console.log("All sessions are fully processed!");
// Show what needs processing
console.log("\n=== WHAT NEEDS PROCESSING ===");
for (const stage of stages) {
const stageData = pipelineStatus.pipeline[stage] || {};
const pending = stageData.PENDING || 0;
const failed = stageData.FAILED || 0;
if (pending > 0 || failed > 0) {
console.log(`${stage}: ${pending} pending, ${failed} failed`);
}
}
console.log("");
}
async function debugImportStatus() {
try {
console.log("=== DEBUGGING PROCESSING STATUS (REFACTORED SYSTEM) ===\n");
const pipelineStatus = await ProcessingStatusManager.getPipelineStatus();
await logPipelineStatus();
await logSessionImportRelationship();
await logFailedSessions();
await logProcessingNeeds(pipelineStatus);
} catch (error) {
console.error("Error debugging processing status:", error);
} finally {

View File

@ -1,352 +0,0 @@
# CSRF Protection Implementation
This document describes the comprehensive CSRF (Cross-Site Request Forgery) protection implemented in the LiveDash application.
## Overview
CSRF protection has been implemented to prevent cross-site request forgery attacks on state-changing operations. The implementation follows industry best practices and provides protection at multiple layers:
- **Middleware Level**: Automatic CSRF validation for protected endpoints
- **tRPC Level**: CSRF protection for all state-changing tRPC procedures
- **Client Level**: Automatic token management and inclusion in requests
- **Component Level**: React components and hooks for easy integration
## Implementation Components
### 1. Core CSRF Library (`lib/csrf.ts`)
The core CSRF functionality includes:
- **Token Generation**: Cryptographically secure token generation using the `csrf` library
- **Token Verification**: Server-side token validation
- **Request Parsing**: Support for tokens in headers, JSON bodies, and form data
- **Client Utilities**: Browser-side token management and request enhancement
**Key Functions:**
- `generateCSRFToken()` - Creates new CSRF tokens
- `verifyCSRFToken()` - Validates tokens server-side
- `CSRFProtection.validateRequest()` - Request validation middleware
- `CSRFClient.*` - Client-side utilities
### 2. Middleware Protection (`middleware/csrfProtection.ts`)
Provides automatic CSRF protection for API endpoints:
**Protected Endpoints:**
- `/api/auth/*` - Authentication endpoints
- `/api/register` - User registration
- `/api/forgot-password` - Password reset requests
- `/api/reset-password` - Password reset completion
- `/api/dashboard/*` - Dashboard API endpoints
- `/api/platform/*` - Platform admin endpoints
- `/api/trpc/*` - All tRPC endpoints
**Protected Methods:**
- `POST` - Create operations
- `PUT` - Update operations
- `DELETE` - Delete operations
- `PATCH` - Partial update operations
**Safe Methods (Not Protected):**
- `GET` - Read operations
- `HEAD` - Metadata requests
- `OPTIONS` - CORS preflight requests
### 3. tRPC Integration (`lib/trpc.ts`)
CSRF protection integrated into tRPC procedures:
**New Procedure Types:**
- `csrfProtectedProcedure` - Basic CSRF protection
- `csrfProtectedAuthProcedure` - CSRF + authentication protection
- `csrfProtectedCompanyProcedure` - CSRF + company access protection
- `csrfProtectedAdminProcedure` - CSRF + admin access protection
**Updated Router Example:**
```typescript
// Before
register: rateLimitedProcedure.input(registerSchema).mutation(async ({ input, ctx }) => {
/* ... */
});
// After
register: csrfProtectedProcedure.input(registerSchema).mutation(async ({ input, ctx }) => {
/* ... */
});
```
### 4. Client-Side Integration
#### tRPC Client (`lib/trpc-client.ts`)
- Automatic CSRF token inclusion in tRPC requests
- Token extracted from cookies and added to request headers
#### React Hooks (`lib/hooks/useCSRF.ts`)
- `useCSRF()` - Basic token management
- `useCSRFFetch()` - Enhanced fetch with automatic CSRF tokens
- `useCSRFForm()` - Form submission with CSRF protection
#### Provider Component (`components/providers/CSRFProvider.tsx`)
- Application-wide CSRF token management
- Automatic token fetching and refresh
- Context-based token sharing
#### Protected Form Component (`components/forms/CSRFProtectedForm.tsx`)
- Ready-to-use form component with CSRF protection
- Automatic token inclusion in form submissions
- Graceful fallback for non-JavaScript environments
### 5. API Endpoint (`app/api/csrf-token/route.ts`)
Provides CSRF tokens to client applications:
- `GET /api/csrf-token` - Returns new CSRF token
- Sets HTTP-only cookie for automatic inclusion
- Used by client-side hooks and components
## Configuration
### Environment Variables
```bash
# CSRF Secret (optional - defaults to NEXTAUTH_SECRET)
CSRF_SECRET=your-csrf-secret-key
```
### CSRF Configuration (`lib/csrf.ts`)
```typescript
export const CSRF_CONFIG = {
cookieName: "csrf-token",
headerName: "x-csrf-token",
secret: env.CSRF_SECRET || env.NEXTAUTH_SECRET,
cookie: {
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24, // 24 hours
},
};
```
## Usage Examples
### 1. Using CSRF in React Components
```tsx
import { useCSRFFetch } from "@/lib/hooks/useCSRF";
function MyComponent() {
const { csrfFetch } = useCSRFFetch();
const handleSubmit = async () => {
// CSRF token automatically included
const response = await csrfFetch("/api/dashboard/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: "example" }),
});
};
}
```
### 2. Using CSRF Protected Forms
```tsx
import { CSRFProtectedForm } from "@/components/forms/CSRFProtectedForm";
function RegistrationForm() {
return (
<CSRFProtectedForm action="/api/register" method="POST">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Register</button>
</CSRFProtectedForm>
);
}
```
### 3. Using CSRF in tRPC Procedures
```typescript
// In your router file
export const userRouter = router({
updateProfile: csrfProtectedAuthProcedure
.input(userUpdateSchema)
.mutation(async ({ input, ctx }) => {
// CSRF validation automatically performed
// User authentication automatically verified
return updateUserProfile(input, ctx.user);
}),
});
```
### 4. Manual CSRF Token Handling
```typescript
import { CSRFClient } from "@/lib/csrf";
// Get token from cookies
const token = CSRFClient.getToken();
// Add to fetch options
const options = CSRFClient.addTokenToFetch({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
// Add to form data
const formData = new FormData();
CSRFClient.addTokenToFormData(formData);
// Add to object
const dataWithToken = CSRFClient.addTokenToObject({ data: "example" });
```
## Security Features
### 1. Token Properties
- **Cryptographically Secure**: Uses the `csrf` library with secure random generation
- **Short-Lived**: 24-hour expiration by default
- **HTTP-Only Cookies**: Prevents XSS-based token theft
- **SameSite Protection**: Reduces CSRF attack surface
### 2. Validation Process
1. Extract token from request (header, form data, or JSON body)
2. Retrieve stored token from HTTP-only cookie
3. Verify tokens match
4. Validate token cryptographic integrity
5. Allow or reject request based on validation
### 3. Error Handling
- **Graceful Degradation**: Form fallbacks for JavaScript-disabled browsers
- **Clear Error Messages**: Specific error codes for debugging
- **Rate Limiting Integration**: Works with existing auth rate limiting
- **Logging**: Comprehensive logging for security monitoring
## Testing
### Test Coverage
- **Unit Tests**: Token generation, validation, and client utilities
- **Integration Tests**: Middleware behavior and endpoint protection
- **Component Tests**: React hooks and form components
- **End-to-End**: Full request/response cycle testing
### Running Tests
```bash
# Run all CSRF tests
pnpm test:vitest tests/unit/csrf*.test.ts tests/integration/csrf*.test.ts
# Run specific test files
pnpm test:vitest tests/unit/csrf.test.ts
pnpm test:vitest tests/integration/csrf-protection.test.ts
pnpm test:vitest tests/unit/csrf-hooks.test.tsx
```
## Monitoring and Debugging
### CSRF Validation Logs
Failed CSRF validations are logged with details:
```
CSRF validation failed for POST /api/dashboard/sessions: CSRF token missing from request
```
### Common Issues and Solutions
1. **Token Missing from Request**
- Ensure CSRFProvider is wrapping your app
- Check that hooks are being used correctly
- Verify network requests include credentials
2. **Token Mismatch**
- Clear browser cookies and refresh
- Check for multiple token sources conflicting
- Verify server and client time synchronization
3. **Integration Issues**
- Ensure middleware is properly configured
- Check tRPC client configuration
- Verify protected procedures are using correct types
## Migration Guide
### For Existing Endpoints
1. Update tRPC procedures to use CSRF-protected variants:
```typescript
// Old
someAction: protectedProcedure.mutation(async ({ ctx, input }) => {
// mutation logic
});
// New
someAction: csrfProtectedAuthProcedure.mutation(async ({ ctx, input }) => {
// mutation logic
});
```
2. Update client components to use CSRF hooks:
```tsx
// Old
const { data, mutate } = trpc.user.update.useMutation();
// New - no changes needed, CSRF automatically handled
const { data, mutate } = trpc.user.update.useMutation();
```
3. Update manual API calls to include CSRF tokens:
<!-- prettier-ignore -->
```typescript
// Old
fetch("/api/endpoint", { method: "POST", body: data });
// New
const { csrfFetch } = useCSRFFetch();
csrfFetch("/api/endpoint", { method: "POST", body: data });
```
## Performance Considerations
- **Minimal Overhead**: Token validation adds ~1ms per request
- **Efficient Caching**: Tokens cached in memory and cookies
- **Selective Protection**: Only state-changing operations protected
- **Optimized Parsing**: Smart content-type detection for token extraction
## Security Best Practices
1. **Always use HTTPS in production** - CSRF tokens should never be transmitted over HTTP
2. **Monitor CSRF failures** - Implement alerting for unusual CSRF failure patterns
3. **Regular secret rotation** - Consider rotating CSRF secrets periodically
4. **Validate referrer headers** - Additional protection layer (not implemented but recommended)
5. **Content Security Policy** - Use CSP headers to prevent XSS attacks that could steal tokens
## Conclusion
The CSRF protection implementation provides comprehensive defense against cross-site request forgery attacks while maintaining ease of use for developers. The multi-layer approach ensures protection at the middleware, application, and component levels, with automatic token management reducing the risk of developer error.
For questions or issues related to CSRF protection, refer to the test files for examples and the security documentation for additional context.

View File

@ -1,434 +0,0 @@
# Admin Audit Logs API
This document describes the Admin Audit Logs API endpoints for retrieving and managing security audit logs in the LiveDash application.
## Overview
The Admin Audit Logs API provides secure access to security audit trails for administrative users. It includes comprehensive filtering, pagination, and retention management capabilities.
## Authentication & Authorization
- **Authentication**: NextAuth.js session required
- **Authorization**: ADMIN role required for all endpoints
- **Rate-Limiting**: Integrated with existing authentication rate-limiting system
- **Audit Trail**: All API access is logged for security monitoring
## API Endpoints
### Get Audit Logs
Retrieve paginated audit logs with optional filtering.
```http
GET /api/admin/audit-logs
```
#### Query Parameters
| Parameter | Type | Description | Default | Example |
| ----------- | ------ | --------------------------- | ------- | --------------------------------- |
| `page` | number | Page number (1-based) | 1 | `?page=2` |
| `limit` | number | Records per page (max 100) | 50 | `?limit=25` |
| `eventType` | string | Filter by event type | - | `?eventType=login_attempt` |
| `outcome` | string | Filter by outcome | - | `?outcome=FAILURE` |
| `severity` | string | Filter by severity level | - | `?severity=HIGH` |
| `userId` | string | Filter by specific user ID | - | `?userId=user-123` |
| `startDate` | string | Filter from date (ISO 8601) | - | `?startDate=2024-01-01T00:00:00Z` |
| `endDate` | string | Filter to date (ISO 8601) | - | `?endDate=2024-01-02T00:00:00Z` |
#### Example Request
```javascript
const response = await fetch(
"/api/admin/audit-logs?" +
new URLSearchParams({
page: "1",
limit: "25",
eventType: "login_attempt",
outcome: "FAILURE",
startDate: "2024-01-01T00:00:00Z",
endDate: "2024-01-02T00:00:00Z",
})
);
const data = await response.json();
```
#### Response Format
```json
{
"success": true,
"data": {
"auditLogs": [
{
"id": "log-123",
"eventType": "login_attempt",
"outcome": "FAILURE",
"severity": "HIGH",
"userId": "user-456",
"companyId": "company-789",
"ipAddress": "192.168.1.***",
"userAgent": "Mozilla/5.0 (masked)",
"timestamp": "2024-01-01T12:00:00Z",
"description": "Failed login attempt",
"metadata": {
"error": "invalid_password",
"endpoint": "/api/auth/signin"
},
"user": {
"id": "user-456",
"email": "user@example.com",
"name": "John Doe",
"role": "USER"
},
"platformUser": null
}
],
"pagination": {
"page": 1,
"limit": 25,
"totalCount": 150,
"totalPages": 6,
"hasNext": true,
"hasPrev": false
}
}
}
```
#### Error Responses
**Unauthorized (401)**
```json
{
"success": false,
"error": "Unauthorized"
}
```
**Insufficient permissions (403)**
```json
{
"success": false,
"error": "Insufficient permissions"
}
```
**Server error (500)**
```json
{
"success": false,
"error": "Internal server error"
}
```
### Audit Log Retention Management
Manage audit log retention policies and cleanup.
```http
POST /api/admin/audit-logs/retention
```
#### Request Body
```json
{
"action": "cleanup",
"retentionDays": 90,
"dryRun": true
}
```
<!-- prettier-ignore -->
**Note**: `action` field accepts one of: `"cleanup"`, `"configure"`, or `"status"`
#### Parameters
| Parameter | Type | Required | Description |
| --------------- | ------- | -------- | ------------------------------------------------------ |
| `action` | string | Yes | Action to perform: `cleanup`, `configure`, or `status` |
| `retentionDays` | number | No | Retention period in days (for configure action) |
| `dryRun` | boolean | No | Preview changes without executing (for cleanup) |
#### Example Requests
**Check retention status:**
```javascript
const response = await fetch("/api/admin/audit-logs/retention", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "status" }),
});
```
**Configure retention policy:**
```javascript
const response = await fetch("/api/admin/audit-logs/retention", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "configure",
retentionDays: 365,
}),
});
```
**Cleanup old logs (dry run):**
```javascript
const response = await fetch("/api/admin/audit-logs/retention", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "cleanup",
dryRun: true,
}),
});
```
## Security Features
### Access Control
- **Role-based Access**: Only ADMIN users can access audit logs
- **Company Isolation**: Users only see logs for their company
- **Session Validation**: Active NextAuth session required
### Audit Trail
- **Access Logging**: All audit log access is recorded
- **Metadata Tracking**: Request parameters and results are logged
- **IP Tracking**: Client IP addresses are recorded for all requests
### Rate Limiting
- **Integrated Protection**: Uses existing authentication rate-limiting
- **Abuse Prevention**: Protects against excessive API usage
- **Error Tracking**: Failed attempts are monitored
## Event Types
Common event types available for filtering:
| Event Type | Description |
| ------------------------- | -------------------------- |
| `login_attempt` | User login attempts |
| `login_success` | Successful logins |
| `logout` | User logouts |
| `password_reset_request` | Password reset requests |
| `password_reset_complete` | Password reset completions |
| `user_creation` | New user registrations |
| `user_modification` | User profile changes |
| `admin_action` | Administrative actions |
| `data_export` | Data export activities |
| `security_violation` | Security policy violations |
## Outcome Types
| Outcome | Description |
| -------------- | ---------------------------------------- |
| `SUCCESS` | Operation completed successfully |
| `FAILURE` | Operation failed |
| `BLOCKED` | Operation was blocked by security policy |
| `WARNING` | Operation completed with warnings |
| `RATE_LIMITED` | Operation was rate limited |
## Severity Levels
| Severity | Description | Use Case |
| ---------- | ------------------------ | ------------------------- |
| `LOW` | Informational events | Normal operations |
| `MEDIUM` | Notable events | Configuration changes |
| `HIGH` | Security events | Failed logins, violations |
| `CRITICAL` | Critical security events | Breaches, attacks |
## Usage Examples
### Daily Security Report
```javascript
async function getDailySecurityReport() {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const response = await fetch(
"/api/admin/audit-logs?" +
new URLSearchParams({
startDate: yesterday.toISOString(),
endDate: today.toISOString(),
limit: "100",
})
);
const data = await response.json();
return data.data.auditLogs;
}
```
### Failed Login Analysis
```javascript
async function getFailedLogins(hours = 24) {
const since = new Date();
since.setHours(since.getHours() - hours);
const response = await fetch(
"/api/admin/audit-logs?" +
new URLSearchParams({
eventType: "login_attempt",
outcome: "FAILURE",
startDate: since.toISOString(),
limit: "100",
})
);
const data = await response.json();
return data.data.auditLogs;
}
```
### User Activity Tracking
```javascript
async function getUserActivity(userId, days = 7) {
const since = new Date();
since.setDate(since.getDate() - days);
const response = await fetch(
"/api/admin/audit-logs?" +
new URLSearchParams({
userId: userId,
startDate: since.toISOString(),
limit: "50",
})
);
const data = await response.json();
return data.data.auditLogs;
}
```
## Performance Considerations
### Database Optimization
- **Indexed Queries**: All filter columns are properly indexed
- **Pagination**: Efficient offset-based pagination with limits
- **Time Range Filtering**: Optimized for date range queries
### Memory Usage
- **Limited Results**: Maximum 100 records per request
- **Streaming**: Large exports use streaming for memory efficiency
- **Connection Pooling**: Database connections are pooled
### Caching Considerations
- **No Caching**: Audit logs are never cached for security reasons
- **Fresh Data**: All queries hit the database for real-time results
- **Read Replicas**: Consider using read replicas for heavy reporting
## Error Handling
### Common Errors
```javascript
try {
const response = await fetch("/api/admin/audit-logs");
const data = await response.json();
if (!data.success) {
switch (response.status) {
case 401:
console.error("User not authenticated");
break;
case 403:
console.error("User lacks admin permissions");
break;
case 500:
console.error("Server error:", data.error);
break;
}
}
} catch (error) {
console.error("Network error:", error);
}
```
### Rate-Limiting Handling
```javascript
async function fetchWithRetry(url, options = {}, maxRetries = 3, retryCount = 0) {
const response = await fetch(url, options);
if (response.status === 429 && retryCount < maxRetries) {
// Rate limited, wait with exponential backoff and retry
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
await new Promise((resolve) => setTimeout(resolve, delay));
return fetchWithRetry(url, options, maxRetries, retryCount + 1);
}
if (response.status === 429) {
throw new Error(`Rate limited after ${maxRetries} retries`);
}
return response;
}
```
## Monitoring and Alerting
### Key Metrics to Monitor
- **Request Volume**: Track API usage patterns
- **Error Rates**: Monitor authentication and authorization failures
- **Query Performance**: Track slow queries and optimize
- **Data Growth**: Monitor audit log size and plan retention
### Alert Conditions
- **High Error Rates**: >5% of requests failing
- **Unusual Access Patterns**: Off-hours access, high-volume usage
- **Performance Degradation**: Query times >2 seconds
- **Security Events**: Multiple failed admin access attempts
## Best Practices
### Security
- Always validate user permissions before displaying UI
- Log all administrative access to audit logs
- Use HTTPS in production environments
- Implement proper error handling to avoid information leakage
### Performance
- Use appropriate page sizes (25-50 records typical)
- Implement client-side pagination for better UX
- Cache results only in memory, never persist
- Use date range filters to limit query scope
### User Experience
- Provide clear filtering options in the UI
- Show loading states for long-running queries
- Implement export functionality for reports
- Provide search and sort capabilities
## Related Documentation
- [Security Audit Logging](./security-audit-logging.md)
- [Security Monitoring](./security-monitoring.md)
- [CSRF Protection](./CSRF_PROTECTION.md)
- [Authentication System](../lib/auth.ts)

View File

@ -1,722 +0,0 @@
# LiveDash-Node API Reference
This document provides a comprehensive reference for all API endpoints in the LiveDash-Node application, including authentication, security monitoring, audit logging, and administrative functions.
## Base URL
```
Local Development: http://localhost:3000
Production: https://your-domain.com
```
## Authentication
All API endpoints (except public endpoints) require authentication via NextAuth.js session cookies.
### Authentication Headers
```http
Cookie: next-auth.session-token=<session-token>
```
### CSRF Protection
State-changing endpoints require CSRF tokens:
```http
X-CSRF-Token: <csrf-token>
```
Get CSRF token:
```http
GET /api/csrf-token
```
## API Endpoints Overview
### Public Endpoints
- `POST /api/csp-report` - CSP violation reporting (no auth required)
- `OPTIONS /api/csp-report` - CORS preflight
### Authentication Endpoints
- `POST /api/auth/[...nextauth]` - NextAuth.js authentication
- `GET /api/csrf-token` - Get CSRF token
- `POST /api/register` - User registration
- `POST /api/forgot-password` - Password reset request
- `POST /api/reset-password` - Password reset completion
### Admin Endpoints (ADMIN role required)
- `GET /api/admin/audit-logs` - Retrieve audit logs
- `POST /api/admin/audit-logs/retention` - Manage audit log retention
- `GET /api/admin/batch-monitoring` - Batch processing monitoring
- `POST /api/admin/batch-monitoring/{id}/retry` - Retry failed batch job
### Platform Admin Endpoints (Platform admin only)
- `GET /api/admin/security-monitoring` - Security monitoring metrics
- `POST /api/admin/security-monitoring` - Update security configuration
- `GET /api/admin/security-monitoring/alerts` - Alert management
- `POST /api/admin/security-monitoring/alerts` - Acknowledge alerts
- `GET /api/admin/security-monitoring/export` - Export security data
- `POST /api/admin/security-monitoring/threat-analysis` - Threat analysis
### Security Monitoring Endpoints
- `GET /api/csp-metrics` - CSP violation metrics
- `POST /api/csp-report` - CSP violation reporting
### Dashboard Endpoints
- `GET /api/dashboard/sessions` - Session data
- `GET /api/dashboard/session/{id}` - Individual session details
- `GET /api/dashboard/metrics` - Dashboard metrics
- `GET /api/dashboard/config` - Dashboard configuration
### Platform Management
- `GET /api/platform/companies` - Company management
- `POST /api/platform/companies` - Create company
- `GET /api/platform/companies/{id}` - Company details
- `GET /api/platform/companies/{id}/users` - Company users
- `POST /api/platform/companies/{id}/users` - Add company user
### tRPC Endpoints
- `POST /api/trpc/[trpc]` - tRPC procedure calls
## Detailed Endpoint Documentation
### Admin Audit Logs
#### Get Audit Logs
```http
GET /api/admin/audit-logs
```
**Authorization**: ADMIN role required
**Query Parameters**:
- `page` (number, optional): Page number (default: 1)
- `limit` (number, optional): Records per page, max 100 (default: 50)
- `eventType` (string, optional): Filter by event type
- `outcome` (string, optional): Filter by outcome (SUCCESS, FAILURE, BLOCKED, etc.)
- `severity` (string, optional): Filter by severity (LOW, MEDIUM, HIGH, CRITICAL)
- `userId` (string, optional): Filter by user ID
- `startDate` (string, optional): Start date (ISO 8601)
- `endDate` (string, optional): End date (ISO 8601)
**Response**:
```json
{
"success": true,
"data": {
"auditLogs": ["// Array of audit log entries"],
"pagination": {
"page": 1,
"limit": 50,
"totalCount": 150,
"totalPages": 3,
"hasNext": true,
"hasPrev": false
}
}
}
```
**Rate Limit**: Inherits from auth rate limiting
#### Manage Audit Log Retention
```http
POST /api/admin/audit-logs/retention
```
**Authorization**: ADMIN role required
**Request Body**:
<!-- prettier-ignore -->
```json
{
"action": "cleanup" | "configure" | "status",
"retentionDays": 90,
"dryRun": true
}
```
**Response**:
```json
{
"success": true,
"data": {
"action": "cleanup",
"recordsAffected": 1250,
"retentionDays": 90,
"dryRun": true
}
}
```
### Security Monitoring
#### Get Security Metrics
```http
GET /api/admin/security-monitoring
```
**Authorization**: Platform admin required
**Query Parameters**:
- `startDate` (string, optional): Start date (ISO 8601)
- `endDate` (string, optional): End date (ISO 8601)
- `companyId` (string, optional): Filter by company
- `severity` (string, optional): Filter by severity
**Response**:
```json
{
"metrics": {
"securityScore": 85,
"threatLevel": "LOW",
"eventCounts": {
"// Event count statistics": null
},
"anomalies": ["// Array of security anomalies"]
},
"alerts": ["// Array of security alerts"],
"config": {
"// Security configuration": null
},
"timeRange": {
"// Time range for the data": null
}
}
```
#### Update Security Configuration
```http
POST /api/admin/security-monitoring
```
**Authorization**: Platform admin required
**Request Body**:
```json
{
"thresholds": {
"failedLoginsPerMinute": 5,
"rateLimitViolationsPerMinute": 10
},
"alerting": {
"enabled": true,
"channels": ["EMAIL", "WEBHOOK"]
}
}
```
### CSP Monitoring
#### CSP Violation Reporting
```http
POST /api/csp-report
```
**Authorization**: None (public endpoint)
**Headers**:
- `Content-Type`: `application/csp-report` or `application/json`
**Request Body** (automatic from browser):
```json
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://malicious.com/script.js",
"source-file": "https://example.com/page",
"line-number": 42
}
}
```
**Rate Limit**: 10 reports per minute per IP
**Response**: `204 No Content`
#### Get CSP Metrics
```http
GET /api/csp-metrics
```
**Authorization**: Admin role required
**Query Parameters**:
- `timeRange` (string, optional): Time range (1h, 6h, 24h, 7d, 30d)
- `format` (string, optional): Response format (json, csv)
- `groupBy` (string, optional): Group by field (hour, directive, etc.)
- `includeDetails` (boolean, optional): Include violation details
**Response**:
```json
{
"success": true,
"data": {
"summary": {
"totalViolations": 45,
"uniqueViolations": 12,
"highRiskViolations": 3,
"bypassAttempts": 1
},
"trends": {
"// CSP trend data": null
},
"topViolations": ["// Array of top CSP violations"],
"riskAnalysis": {
"// CSP risk analysis data": null
},
"violations": ["// Array of CSP violations"]
}
}
```
### Batch Monitoring
#### Get Batch Monitoring Data
```http
GET /api/admin/batch-monitoring
```
**Authorization**: ADMIN role required
**Query Parameters**:
- `timeRange` (string, optional): Time range (1h, 6h, 24h, 7d, 30d)
- `status` (string, optional): Filter by status (pending, completed, failed)
- `jobType` (string, optional): Filter by job type
- `includeDetails` (boolean, optional): Include detailed job information
- `page` (number, optional): Page number
- `limit` (number, optional): Records per page
**Response**:
```json
{
"success": true,
"data": {
"summary": {
"totalJobs": 156,
"completedJobs": 142,
"failedJobs": 8,
"costSavings": {}
},
"queues": {
"// Queue statistics": null
},
"performance": {
"// Performance metrics": null
},
"jobs": ["// Array of batch jobs"]
}
}
```
#### Retry Batch Job
```http
POST /api/admin/batch-monitoring/{jobId}/retry
```
**Authorization**: ADMIN role required
**Response**:
```json
{
"success": true,
"data": {
"jobId": "batch-job-123",
"status": "retrying",
"message": "Job queued for retry"
}
}
```
### CSRF Token
#### Get CSRF Token
```http
GET /api/csrf-token
```
**Authorization**: None
**Response**:
```json
{
"csrfToken": "abc123..."
}
```
**Headers Set**:
- `Set-Cookie`: HTTP-only CSRF token cookie
### Authentication
#### User Registration
```http
POST /api/register
```
**Authorization**: None
**Headers Required**:
- `X-CSRF-Token`: CSRF token
**Request Body**:
```json
{
"email": "user@example.com",
"password": "SecurePassword123!",
"name": "John Doe",
"companyName": "Acme Corp"
}
```
**Rate Limit**: 3 attempts per hour per IP
**Response**:
```json
{
"success": true,
"message": "User registered successfully",
"userId": "user-123"
}
```
#### Password Reset Request
```http
POST /api/forgot-password
```
**Authorization**: None
**Headers Required**:
- `X-CSRF-Token`: CSRF token
**Request Body**:
```json
{
"email": "user@example.com"
}
```
**Rate Limit**: 5 attempts per 15 minutes per IP
**Response**:
```json
{
"success": true,
"message": "Password reset email sent"
}
```
#### Password Reset Completion
```http
POST /api/reset-password
```
**Authorization**: None
**Headers Required**:
- `X-CSRF-Token`: CSRF token
**Request Body**:
```json
{
"token": "reset-token-123",
"password": "NewSecurePassword123!"
}
```
**Response**:
```json
{
"success": true,
"message": "Password reset successfully"
}
```
## Error Responses
### Standard Error Format
```json
{
"success": false,
"error": "Error message",
"code": "ERROR_CODE",
"details": {}
}
```
### Common HTTP Status Codes
| Status | Description | Common Causes |
| ------ | --------------------- | ---------------------------------------- |
| 200 | OK | Successful request |
| 201 | Created | Resource created successfully |
| 204 | No Content | Successful request with no response body |
| 400 | Bad Request | Invalid request parameters or body |
| 401 | Unauthorized | Authentication required or invalid |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource not found |
| 409 | Conflict | Resource already exists or conflict |
| 422 | Unprocessable Entity | Validation errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error |
### Error Codes
| Code | Description | Resolution |
| ------------------ | ------------------------ | -------------------- |
| `UNAUTHORIZED` | No valid session | Login required |
| `FORBIDDEN` | Insufficient permissions | Check user role |
| `VALIDATION_ERROR` | Invalid input data | Check request format |
| `RATE_LIMITED` | Too many requests | Wait and retry |
| `CSRF_INVALID` | Invalid CSRF token | Get new token |
| `NOT_FOUND` | Resource not found | Check resource ID |
| `CONFLICT` | Resource conflict | Check existing data |
## Rate Limiting
### Authentication Endpoints
- **Login**: 5 attempts per 15 minutes per IP
- **Registration**: 3 attempts per hour per IP
- **Password Reset**: 5 attempts per 15 minutes per IP
### Security Endpoints
- **CSP Reports**: 10 reports per minute per IP
- **Admin Endpoints**: 60 requests per minute per user
- **Security Monitoring**: 30 requests per minute per user
### General API
- **Dashboard Endpoints**: 120 requests per minute per user
- **Platform Management**: 60 requests per minute per user
## Security Headers
All API responses include security headers:
```http
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: [CSP directives]
```
## CORS Configuration
### Allowed Origins
- Development: `http://localhost:3000`
- Production: `https://your-domain.com`
### Allowed Methods
- `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`
### Allowed Headers
- `Content-Type`, `Authorization`, `X-CSRF-Token`, `X-Requested-With`
## Pagination
### Standard Pagination Format
```json
{
"data": ["// Array of response data"],
"pagination": {
"page": 1,
"limit": 50,
"totalCount": 150,
"totalPages": 3,
"hasNext": true,
"hasPrev": false
}
}
```
### Pagination Parameters
- `page`: Page number (1-based, default: 1)
- `limit`: Records per page (default: 50, max: 100)
## Filtering and Sorting
### Common Filter Parameters
- `startDate` / `endDate`: Date range filtering (ISO 8601)
- `status`: Status filtering
- `userId` / `companyId`: Entity filtering
- `eventType`: Event type filtering
- `severity`: Severity level filtering
### Sorting Parameters
- `sortBy`: Field to sort by
- `sortOrder`: `asc` or `desc` (default: `desc`)
## Response Caching
### Cache Headers
```http
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
```
### Cache Strategy
- **Security data**: Never cached
- **Static data**: Browser cache for 5 minutes
- **User data**: No cache for security
## API Versioning
### Current Version
- Version: `v1` (implied, no version prefix required)
- Introduced: January 2025
### Future Versioning
- Breaking changes will introduce new versions
- Format: `/api/v2/endpoint`
- Backward compatibility maintained for 12 months
## SDK and Client Libraries
### JavaScript/TypeScript Client
```javascript
// Initialize client
const client = new LiveDashClient({
baseURL: "https://your-domain.com",
apiKey: "your-api-key", // For future API key auth
});
// Get audit logs
const auditLogs = await client.admin.getAuditLogs({
page: 1,
limit: 50,
eventType: "login_attempt",
});
// Get security metrics
const metrics = await client.security.getMetrics({
timeRange: "24h",
});
```
### tRPC Client
```javascript
import { createTRPCNext } from "@trpc/next";
const trpc = createTRPCNext({
config() {
return {
url: "/api/trpc",
};
},
});
// Use tRPC procedures
const { data: user } = trpc.auth.getUser.useQuery();
const updateProfile = trpc.user.updateProfile.useMutation();
```
## Testing
### API Testing Tools
```bash
# Test with curl
curl -X GET "http://localhost:3000/api/admin/audit-logs" \
-H "Cookie: next-auth.session-token=..." \
-H "X-CSRF-Token: ..."
# Test with HTTPie
http GET localhost:3000/api/csp-metrics \
timeRange==24h \
Cookie:next-auth.session-token=...
```
### Integration Tests
```javascript
// Example test
describe("Admin Audit Logs API", () => {
test("should return paginated audit logs", async () => {
const response = await request(app)
.get("/api/admin/audit-logs?page=1&limit=10")
.set("Cookie", "next-auth.session-token=...")
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.auditLogs).toHaveLength(10);
expect(response.body.data.pagination.page).toBe(1);
});
});
```
## Related Documentation
- [Admin Audit Logs API](./admin-audit-logs-api.md)
- [CSP Metrics API](./csp-metrics-api.md)
- [Security Monitoring](./security-monitoring.md)
- [CSRF Protection](./CSRF_PROTECTION.md)
- [Batch Monitoring Dashboard](./batch-monitoring-dashboard.md)
This API reference provides comprehensive documentation for all endpoints in the LiveDash-Node application. For specific implementation details, refer to the individual documentation files for each feature area.

View File

@ -1,549 +0,0 @@
# Batch Processing Monitoring Dashboard
This document describes the batch processing monitoring dashboard and API endpoints for tracking OpenAI Batch API operations in the LiveDash application.
## Overview
The Batch Monitoring Dashboard provides real-time visibility into the OpenAI Batch API processing pipeline, including job status tracking, cost analysis, and performance monitoring. This system enables 50% cost reduction on AI processing while maintaining comprehensive oversight.
## Features
### Real-time Monitoring
- **Job Status Tracking**: Monitor batch jobs from creation to completion
- **Queue Management**: View pending, running, and completed batch queues
- **Processing Metrics**: Track throughput, success rates, and error patterns
- **Cost Analysis**: Monitor API costs and savings compared to individual requests
### Performance Analytics
- **Batch Efficiency**: Analyze batch size optimization and processing times
- **Success Rates**: Track completion and failure rates across different job types
- **Resource Utilization**: Monitor API quota usage and rate limiting
- **Historical Trends**: View processing patterns over time
### Administrative Controls
- **Manual Intervention**: Pause, resume, or cancel batch operations
- **Priority Management**: Adjust processing priorities for urgent requests
- **Error Handling**: Review and retry failed batch operations
- **Configuration Management**: Adjust batch parameters and thresholds
## API Endpoints
### Batch Monitoring API
Retrieve comprehensive batch processing metrics and status information.
```http
GET /api/admin/batch-monitoring
```
#### Query Parameters
| Parameter | Type | Description | Default | Example |
| ---------------- | ------- | -------------------------------- | ------- | ---------------------- |
| `timeRange` | string | Time range for metrics | `24h` | `?timeRange=7d` |
| `status` | string | Filter by batch status | - | `?status=completed` |
| `jobType` | string | Filter by job type | - | `?jobType=ai_analysis` |
| `includeDetails` | boolean | Include detailed job information | `false` | `?includeDetails=true` |
| `page` | number | Page number for pagination | 1 | `?page=2` |
| `limit` | number | Records per page (max 100) | 50 | `?limit=25` |
#### Example Request
```javascript
const response = await fetch(
"/api/admin/batch-monitoring?" +
new URLSearchParams({
timeRange: "24h",
status: "completed",
includeDetails: "true",
})
);
const data = await response.json();
```
#### Response Format
```json
{
"success": true,
"data": {
"summary": {
"totalJobs": 156,
"completedJobs": 142,
"failedJobs": 8,
"pendingJobs": 6,
"totalRequests": 15600,
"processedRequests": 14200,
"costSavings": {
"currentPeriod": 234.56,
"projectedMonthly": 7038.45,
"savingsPercentage": 48.2
},
"averageProcessingTime": 1800000,
"successRate": 95.2
},
"queues": {
"pending": 12,
"processing": 3,
"completed": 142,
"failed": 8
},
"performance": {
"throughput": {
"requestsPerHour": 650,
"jobsPerHour": 6.5,
"averageBatchSize": 100
},
"efficiency": {
"batchUtilization": 87.3,
"processingEfficiency": 92.1,
"errorRate": 4.8
}
},
"jobs": [
{
"id": "batch-job-123",
"batchId": "batch_abc123",
"status": "completed",
"jobType": "ai_analysis",
"requestCount": 100,
"completedCount": 98,
"failedCount": 2,
"createdAt": "2024-01-01T10:00:00Z",
"startedAt": "2024-01-01T10:05:00Z",
"completedAt": "2024-01-01T10:35:00Z",
"processingTimeMs": 1800000,
"costEstimate": 12.5,
"errorSummary": [
{
"error": "token_limit_exceeded",
"count": 2,
"percentage": 2.0
}
]
}
]
}
}
```
## Dashboard Components
### BatchMonitoringDashboard Component
The main dashboard component (`components/admin/BatchMonitoringDashboard.tsx`) provides:
#### Key Metrics Cards
```tsx
// Real-time overview cards
<>
<MetricCard
title="Total Jobs"
value={data.summary.totalJobs}
change={"+12 from yesterday"}
trend="up"
/>
<MetricCard
title="Success Rate"
value={`${data.summary.successRate}%`}
change={"+2.1% from last week"}
trend="up"
/>
<MetricCard
title="Cost Savings"
value={`$${data.summary.costSavings.currentPeriod}`}
change={`${data.summary.costSavings.savingsPercentage}% vs individual API`}
trend="up"
/>
</>
```
#### Queue Status Visualization
```tsx
// Visual representation of batch job queues
<QueueStatusChart
pending={data.queues.pending}
processing={data.queues.processing}
completed={data.queues.completed}
failed={data.queues.failed}
/>
```
#### Performance Charts
<!-- prettier-ignore -->
```tsx
// Processing throughput over time
<ThroughputChart
data={data.performance.throughput}
timeRange={timeRange}
/>
// Cost savings trend
<CostSavingsChart
savings={data.summary.costSavings}
historical={data.historical}
/>
```
#### Job Management Table
```tsx
// Detailed job listing with actions
<BatchJobTable
jobs={data.jobs}
onRetry={handleRetryJob}
onCancel={handleCancelJob}
onViewDetails={handleViewDetails}
/>
```
## Usage Examples
### Monitor Batch Performance
```javascript
async function monitorBatchPerformance() {
const response = await fetch("/api/admin/batch-monitoring?timeRange=24h");
const data = await response.json();
const performance = data.data.performance;
// Check if performance is within acceptable ranges
if (performance.efficiency.errorRate > 10) {
console.warn("High error rate detected:", performance.efficiency.errorRate + "%");
// Get failed jobs for analysis
const failedJobs = await fetch("/api/admin/batch-monitoring?status=failed");
const failures = await failedJobs.json();
// Analyze common failure patterns
const errorSummary = failures.data.jobs.reduce((acc, job) => {
job.errorSummary?.forEach((error) => {
acc[error.error] = (acc[error.error] || 0) + error.count;
});
return acc;
}, {});
console.log("Error patterns:", errorSummary);
}
}
```
### Cost Savings Analysis
```javascript
async function analyzeCostSavings() {
const response = await fetch("/api/admin/batch-monitoring?timeRange=30d&includeDetails=true");
const data = await response.json();
const savings = data.data.summary.costSavings;
return {
currentSavings: savings.currentPeriod,
projectedAnnual: savings.projectedMonthly * 12,
savingsRate: savings.savingsPercentage,
totalProcessed: data.data.summary.processedRequests,
averageCostPerRequest: savings.currentPeriod / data.data.summary.processedRequests,
};
}
```
### Retry Failed Jobs
```javascript
async function retryFailedJobs() {
// Get failed jobs
const response = await fetch("/api/admin/batch-monitoring?status=failed");
const data = await response.json();
const retryableJobs = data.data.jobs.filter((job) => {
// Only retry jobs that failed due to temporary issues
const hasRetryableErrors = job.errorSummary?.some((error) =>
["rate_limit_exceeded", "temporary_error", "timeout"].includes(error.error)
);
return hasRetryableErrors;
});
// Retry jobs individually
for (const job of retryableJobs) {
try {
await fetch(`/api/admin/batch-monitoring/${job.id}/retry`, {
method: "POST",
});
console.log(`Retried job ${job.id}`);
} catch (error) {
console.error(`Failed to retry job ${job.id}:`, error);
}
}
}
```
### Real-time Dashboard Updates
```javascript
function useRealtimeBatchMonitoring() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("/api/admin/batch-monitoring?timeRange=1h");
const result = await response.json();
setData(result.data);
} catch (error) {
console.error("Failed to fetch batch monitoring data:", error);
} finally {
setIsLoading(false);
}
};
// Initial fetch
fetchData();
// Update every 30 seconds
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, []);
return { data, isLoading };
}
```
## Configuration
### Batch Processing Settings
Configure batch processing parameters in environment variables:
```bash
# Batch Processing Configuration
BATCH_PROCESSING_ENABLED="true"
BATCH_CREATE_INTERVAL="*/5 * * * *" # Create batches every 5 minutes
BATCH_STATUS_CHECK_INTERVAL="*/2 * * * *" # Check status every 2 minutes
BATCH_RESULT_PROCESSING_INTERVAL="*/1 * * * *" # Process results every minute
# Batch Size and Limits
BATCH_MAX_REQUESTS="1000" # Maximum requests per batch
BATCH_TIMEOUT_HOURS="24" # Batch timeout in hours
BATCH_MIN_SIZE="10" # Minimum batch size
# Monitoring Configuration
BATCH_MONITORING_RETENTION_DAYS="30" # How long to keep monitoring data
BATCH_ALERT_THRESHOLD_ERROR_RATE="10" # Alert if error rate exceeds 10%
BATCH_ALERT_THRESHOLD_PROCESSING_TIME="3600" # Alert if processing takes >1 hour
```
### Dashboard Refresh Settings
```javascript
// Configure dashboard update intervals
const DASHBOARD_CONFIG = {
refreshInterval: 30000, // 30 seconds
alertRefreshInterval: 10000, // 10 seconds for alerts
detailRefreshInterval: 60000, // 1 minute for detailed views
maxRetries: 3, // Maximum retry attempts
retryDelay: 5000, // Delay between retries
};
```
## Alerts and Notifications
### Automated Alerts
The system automatically generates alerts for:
```javascript
const alertConditions = {
highErrorRate: {
threshold: 10, // Error rate > 10%
severity: "high",
notification: "immediate",
},
longProcessingTime: {
threshold: 3600000, // > 1 hour
severity: "medium",
notification: "hourly",
},
lowThroughput: {
threshold: 0.5, // < 0.5 jobs per hour
severity: "medium",
notification: "daily",
},
batchFailure: {
threshold: 1, // Any complete batch failure
severity: "critical",
notification: "immediate",
},
};
```
### Custom Alert Configuration
```javascript
// Configure custom alerts through the admin interface
async function configureAlerts(alertConfig) {
const response = await fetch("/api/admin/batch-monitoring/alerts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
errorRateThreshold: alertConfig.errorRate,
processingTimeThreshold: alertConfig.processingTime,
notificationChannels: alertConfig.channels,
alertSuppression: alertConfig.suppression,
}),
});
return response.json();
}
```
## Troubleshooting
### Common Issues
#### High Error Rates
```javascript
// Investigate high error rates
async function investigateErrors() {
const response = await fetch("/api/admin/batch-monitoring?status=failed&includeDetails=true");
const data = await response.json();
// Group errors by type
const errorAnalysis = data.data.jobs.reduce((acc, job) => {
job.errorSummary?.forEach((error) => {
if (!acc[error.error]) {
acc[error.error] = { count: 0, jobs: [] };
}
acc[error.error].count += error.count;
acc[error.error].jobs.push(job.id);
});
return acc;
}, {});
console.log("Error analysis:", errorAnalysis);
return errorAnalysis;
}
```
#### Slow Processing
```javascript
// Analyze processing bottlenecks
async function analyzePerformance() {
const response = await fetch("/api/admin/batch-monitoring?timeRange=24h&includeDetails=true");
const data = await response.json();
const slowJobs = data.data.jobs
.filter((job) => job.processingTimeMs > 3600000) // > 1 hour
.sort((a, b) => b.processingTimeMs - a.processingTimeMs);
console.log("Slowest jobs:", slowJobs.slice(0, 5));
// Analyze patterns
const avgByType = slowJobs.reduce((acc, job) => {
if (!acc[job.jobType]) {
acc[job.jobType] = { total: 0, count: 0 };
}
acc[job.jobType].total += job.processingTimeMs;
acc[job.jobType].count++;
return acc;
}, {});
Object.keys(avgByType).forEach((type) => {
avgByType[type].average = avgByType[type].total / avgByType[type].count;
});
return avgByType;
}
```
### Performance Optimization
#### Batch Size Optimization
```javascript
// Analyze optimal batch sizes
async function optimizeBatchSizes() {
const response = await fetch("/api/admin/batch-monitoring?timeRange=7d&includeDetails=true");
const data = await response.json();
// Group by batch size ranges
const sizePerformance = data.data.jobs.reduce((acc, job) => {
const sizeRange = Math.floor(job.requestCount / 50) * 50; // Group by 50s
if (!acc[sizeRange]) {
acc[sizeRange] = {
jobs: 0,
totalTime: 0,
totalRequests: 0,
successRate: 0,
};
}
acc[sizeRange].jobs++;
acc[sizeRange].totalTime += job.processingTimeMs;
acc[sizeRange].totalRequests += job.requestCount;
acc[sizeRange].successRate += job.completedCount / job.requestCount;
return acc;
}, {});
// Calculate averages
Object.keys(sizePerformance).forEach((range) => {
const perf = sizePerformance[range];
perf.avgTimePerRequest = perf.totalTime / perf.totalRequests;
perf.avgSuccessRate = perf.successRate / perf.jobs;
});
return sizePerformance;
}
```
## Integration with Existing Systems
### Security Audit Integration
All batch monitoring activities are logged through the security audit system:
```javascript
// Automatic audit logging for monitoring activities
await securityAuditLogger.logPlatformAdmin(
"batch_monitoring_access",
AuditOutcome.SUCCESS,
context,
"Admin accessed batch monitoring dashboard"
);
```
### Rate Limiting Integration
Monitoring API endpoints use the existing rate limiting system:
```javascript
// Protected by admin rate limiting
const rateLimitResult = await rateLimiter.check(
`admin-batch-monitoring:${userId}`,
60, // 60 requests
60 * 1000 // per minute
);
```
## Related Documentation
- [Batch Processing Optimizations](./batch-processing-optimizations.md)
- [Security Monitoring](./security-monitoring.md)
- [Admin Audit Logs API](./admin-audit-logs-api.md)
- [OpenAI Batch API Integration](../lib/batchProcessor.ts)
The batch monitoring dashboard provides comprehensive visibility into the AI processing pipeline, enabling administrators to optimize performance, monitor costs, and ensure reliable operation of the batch processing system.

View File

@ -1,223 +0,0 @@
# Batch Processing Database Query Optimizations
This document outlines the database query optimizations implemented to improve the performance of the OpenAI Batch API processing pipeline.
## Overview
The batch processing system was optimized to reduce database load and improve response times through several key strategies:
1. **Database Index Optimization**
2. **Query Pattern Improvements**
3. **Company Caching**
4. **Batch Operations**
5. **Integration Layer with Fallback**
## Database Index Improvements
### New Indexes Added
The following composite indexes were added to the `AIProcessingRequest` table in the Prisma schema:
```sql
-- Optimize time-based status queries
@@index([processingStatus, requestedAt])
-- Optimize batch-related queries
@@index([batchId])
-- Composite index for batch status filtering
@@index([processingStatus, batchId])
```
### Query Performance Impact
These indexes specifically optimize:
- Finding pending requests by status and creation time
- Batch-related lookups by batch ID
- Combined status and batch filtering operations
## Query Optimization Strategies
### 1. Selective Data Fetching
**Before:**
```typescript
// Loaded full session with all messages
const queryOptions = {
include: {
session: {
include: {
messages: {
orderBy: { order: "asc" },
},
},
},
},
};
```
**After:**
<!-- prettier-ignore -->
```typescript
// Only essential data with message count
include: {
session: {
select: {
id: true,
companyId: true,
_count: { select: { messages: true } }
},
},
}
```
### 2. Company Caching
Implemented a 5-minute TTL cache for active companies to eliminate redundant database lookups:
```typescript
class CompanyCache {
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async getActiveCompanies(): Promise<CachedCompany[]> {
// Returns cached data if available and fresh
// Otherwise refreshes from database
}
}
```
### 3. Batch Operations
**Before:** N+1 queries for each company
```typescript
// Sequential processing per company
for (const company of companies) {
const requests = await getPendingRequests(company.id);
// Process each company separately
}
```
**After:** Single query for all companies
```typescript
// Batch query for all companies at once
const allRequests = await prisma.aIProcessingRequest.findMany({
where: {
session: {
companyId: { in: companies.map((c) => c.id) },
},
processingStatus: AIRequestStatus.PENDING_BATCHING,
},
});
// Group results by company in memory
const requestsByCompany = groupByCompany(allRequests);
```
## Performance Improvements
### Query Count Reduction
- **Company lookups:** Reduced from 4 separate queries per scheduler run to 1 cached lookup
- **Pending requests:** Reduced from N queries (one per company) to 1 batch query
- **Status checks:** Reduced from N queries to 1 batch query
- **Failed requests:** Reduced from N queries to 1 batch query
### Parallel Processing
Added configurable parallel processing with batching:
```typescript
const SCHEDULER_CONFIG = {
MAX_CONCURRENT_COMPANIES: 5,
USE_BATCH_OPERATIONS: true,
PARALLEL_COMPANY_PROCESSING: true,
};
```
### Memory Optimization
- Eliminated loading unnecessary message content
- Used `select` instead of `include` where possible
- Implemented automatic cache cleanup
## Integration Layer
Created a unified interface that can switch between original and optimized implementations:
### Environment Configuration
```bash
# Enable optimizations (default: true)
ENABLE_BATCH_OPTIMIZATION=true
ENABLE_BATCH_OPERATIONS=true
ENABLE_PARALLEL_PROCESSING=true
# Fallback behavior
FALLBACK_ON_ERRORS=true
```
### Performance Tracking
The integration layer automatically tracks performance metrics and can fall back to the original implementation if optimizations fail:
```typescript
class PerformanceTracker {
shouldUseOptimized(): boolean {
// Uses optimized if faster and success rate > 90%
return optimizedAvg < originalAvg && optimizedSuccess > 0.9;
}
}
```
## Files Modified
### New Files
- `lib/batchProcessorOptimized.ts` - Optimized query implementations
- `lib/batchSchedulerOptimized.ts` - Optimized scheduler
- `lib/batchProcessorIntegration.ts` - Integration layer with fallback
### Modified Files
- `prisma/schema.prisma` - Added composite indexes
- `server.ts` - Updated to use integration layer
- `app/api/admin/batch-monitoring/route.ts` - Updated import
## Monitoring
The optimizations include comprehensive logging and monitoring:
- Performance metrics for each operation type
- Cache hit/miss statistics
- Fallback events tracking
- Query execution time monitoring
## Rollback Strategy
The integration layer allows for easy rollback:
1. Set `ENABLE_BATCH_OPTIMIZATION=false`
2. System automatically uses original implementation
3. No database schema changes needed for rollback
4. Indexes remain beneficial for manual queries
## Expected Performance Gains
- **Database Query Count:** 60-80% reduction in scheduler operations
- **Memory Usage:** 40-60% reduction from selective data loading
- **Response Time:** 30-50% improvement for batch operations
- **Cache Hit Rate:** 95%+ for company lookups after warmup
## Testing
Performance improvements can be validated by:
1. Monitoring the batch monitoring dashboard
2. Checking performance metrics in logs
3. Comparing execution times before/after optimization
4. Load testing with multiple companies and large batches

View File

@ -1,515 +0,0 @@
# CSP Metrics and Monitoring API
This document describes the Content Security Policy (CSP) metrics and violation reporting APIs that provide real-time monitoring and analysis of CSP violations.
## Overview
The CSP Metrics API provides comprehensive monitoring of Content Security Policy violations, including:
- Real-time violation tracking and metrics
- Bypass attempt detection and risk assessment
- Policy optimization recommendations
- Historical trend analysis
- Export capabilities for security analysis
## API Endpoints
### CSP Violation Reporting
Endpoint for browsers to report CSP violations (automatic).
```http
POST /api/csp-report
```
#### Request Headers
- `Content-Type`: `application/csp-report` or `application/json`
#### Request Body (Automatic from Browser)
```json
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://malicious.com/script.js",
"source-file": "https://example.com/page",
"line-number": 42,
"script-sample": "eval(maliciousCode)"
}
}
```
#### Features
- **Rate Limiting**: 10 reports per minute per IP
- **Risk Assessment**: Automatic classification of violation severity
- **Bypass Detection**: Identifies potential CSP bypass attempts
- **Real-time Processing**: Immediate analysis and alerting
### CSP Metrics API
Retrieve CSP violation metrics and analytics.
```http
GET /api/csp-metrics
```
#### Query Parameters
| Parameter | Type | Description | Default | Example |
| ---------------- | ------- | ------------------------------------------------------------------------------- | ------- | ---------------------- |
| `timeRange` | string | Time range for metrics | `24h` | `?timeRange=7d` |
| `format` | string | Response format | `json` | `?format=csv` |
| `groupBy` | string | Group results by field | `hour` | `?groupBy=directive` |
| `includeDetails` | boolean | Include violation details | `false` | `?includeDetails=true` |
| `offset` | string | Shift the queried time-window backwards by the given duration (for comparisons) | `0` | `?offset=24h` |
#### Time Range Options
- `1h` - Last 1 hour
- `6h` - Last 6 hours
- `24h` - Last 24 hours (default)
- `7d` - Last 7 days
- `30d` - Last 30 days
#### Example Request
```javascript
const response = await fetch(
"/api/csp-metrics?" +
new URLSearchParams({
timeRange: "24h",
groupBy: "directive",
includeDetails: "true",
})
);
const metrics = await response.json();
```
#### Response Format
```json
{
"success": true,
"data": {
"summary": {
"totalViolations": 45,
"uniqueViolations": 12,
"highRiskViolations": 3,
"bypassAttempts": 1,
"timeRange": "24h",
"generatedAt": "2024-01-01T12:00:00Z"
},
"trends": {
"hourlyCount": [
{ "hour": "2024-01-01T11:00:00Z", "count": 5 },
{ "hour": "2024-01-01T12:00:00Z", "count": 8 }
],
"trendDirection": "increasing",
"changePercent": 25.5
},
"topViolations": [
{
"directive": "script-src",
"count": 15,
"percentage": 33.3,
"riskLevel": "medium",
"topBlockedUris": ["https://malicious.com/script.js", "inline"]
}
],
"riskAnalysis": {
"overallRiskScore": 65,
"riskLevel": "medium",
"criticalIssues": 1,
"recommendations": [
"Review script-src policy for external domains",
"Consider implementing nonce-based CSP"
]
},
"violations": [
{
"timestamp": "2024-01-01T12:00:00Z",
"directive": "script-src",
"blockedUri": "https://malicious.com/script.js",
"sourceFile": "https://example.com/page",
"riskLevel": "high",
"bypassAttempt": true,
"ipAddress": "192.168.1.***",
"userAgent": "Mozilla/5.0 (masked)"
}
]
}
}
```
## CSP Monitoring Service
The monitoring service (`lib/csp-monitoring.ts`) provides advanced violation analysis.
### Key Features
#### 1. Real-time Violation Processing
```javascript
// Automatic processing when violations are reported
const result = await cspMonitoring.processViolation(violationReport, clientIP, userAgent);
console.log(result.alertLevel); // low, medium, high, critical
console.log(result.shouldAlert); // boolean
console.log(result.recommendations); // array of suggestions
```
#### 2. Risk Assessment
The service automatically assesses violation risk based on:
- **Directive Type**: Script violations are higher risk than style violations
- **Source Pattern**: External domains vs inline vs data URIs
- **Bypass Indicators**: Known CSP bypass techniques
- **Frequency**: Repeated violations from same source
- **Geographic Factors**: Unusual source locations
#### 3. Bypass Detection
Automatic detection of common CSP bypass attempts:
```javascript
const bypassPatterns = [
/javascript:/i, // javascript: protocol injection
/data:text\/html/i, // HTML data URI injection
/eval\(/i, // Direct eval() calls
/Function\(/i, // Function constructor
/setTimeout.*string/i, // Timer string execution
/location\s*=/i, // Location manipulation
/document\.write/i, // Document.write injection
];
```
#### 4. Policy Recommendations
Based on violation patterns, the service provides actionable recommendations:
- **Tighten Policies**: Suggest removing broad allowlists
- **Add Domains**: Recommend allowing legitimate external resources
- **Implement Nonces**: Suggest nonce-based policies for inline content
- **Upgrade Directives**: Recommend modern CSP features
## Violation Analysis
### Risk Levels
| Risk Level | Score | Description | Action |
| ------------ | ------ | --------------------------------------------- | ----------------------- |
| **Critical** | 90-100 | Active bypass attempts, known attack patterns | Immediate investigation |
| **High** | 70-89 | Suspicious patterns, potential security risks | Urgent review |
| **Medium** | 40-69 | Policy violations, may need attention | Regular monitoring |
| **Low** | 0-39 | Minor violations, likely legitimate | Log for trends |
### Alert Conditions
```javascript
// High-risk violations trigger immediate alerts
const alertConditions = {
critical: {
bypassAttempt: true,
unknownExternalDomain: true,
suspiciousUserAgent: true,
},
high: {
repeatedViolations: ">5 in 10 minutes",
scriptInjectionAttempt: true,
dataUriWithScript: true,
},
medium: {
newExternalDomain: true,
inlineScriptViolation: true,
unknownSource: true,
},
};
```
## Usage Examples
### Real-time Violation Monitoring
```javascript
// Monitor violations in real-time
async function monitorViolations() {
const metrics = await fetch("/api/csp-metrics?timeRange=1h");
const data = await metrics.json();
if (data.data.summary.highRiskViolations > 0) {
console.warn("High-risk CSP violations detected:", data.data.summary.highRiskViolations);
// Get violation details
const details = await fetch("/api/csp-metrics?includeDetails=true");
const violations = await details.json();
violations.data.violations
.filter((v) => v.riskLevel === "high")
.forEach((violation) => {
console.error("High-risk violation:", {
directive: violation.directive,
blockedUri: violation.blockedUri,
timestamp: violation.timestamp,
});
});
}
}
// Run every 5 minutes
setInterval(monitorViolations, 5 * 60 * 1000);
```
### Security Dashboard Integration
```javascript
// Get CSP metrics for security dashboard
async function getCSPDashboardData() {
const [current, previous] = await Promise.all([
fetch("/api/csp-metrics?timeRange=24h").then((r) => r.json()),
fetch("/api/csp-metrics?timeRange=24h&offset=24h").then((r) => r.json()),
]);
return {
currentViolations: current.data.summary.totalViolations,
previousViolations: previous.data.summary.totalViolations,
trend: current.data.trends.trendDirection,
riskScore: current.data.riskAnalysis.overallRiskScore,
recommendations: current.data.riskAnalysis.recommendations.slice(0, 3),
};
}
```
### Export Violation Data
```javascript
// Export violations for external analysis
async function exportViolations(format = "csv", timeRange = "7d") {
const response = await fetch(`/api/csp-metrics?format=${format}&timeRange=${timeRange}`);
if (format === "csv") {
const csvData = await response.text();
downloadFile(csvData, `csp-violations-${timeRange}.csv`, "text/csv");
} else {
const jsonData = await response.json();
downloadFile(
JSON.stringify(jsonData, null, 2),
`csp-violations-${timeRange}.json`,
"application/json"
);
}
}
function downloadFile(content, filename, contentType) {
const blob = new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
```
### Policy Optimization
```javascript
// Analyze violations to optimize CSP policy
async function optimizeCSPPolicy() {
const metrics = await fetch("/api/csp-metrics?timeRange=30d&includeDetails=true");
const data = await metrics.json();
// Group violations by directive
const violationsByDirective = data.data.violations.reduce((acc, violation) => {
if (!acc[violation.directive]) {
acc[violation.directive] = [];
}
acc[violation.directive].push(violation);
return acc;
}, {});
// Generate recommendations
const recommendations = Object.entries(violationsByDirective).map(([directive, violations]) => {
const uniqueDomains = [...new Set(violations.map((v) => v.blockedUri))];
const legitimateCount = violations.filter((v) => v.riskLevel === "low").length;
if (legitimateCount > violations.length * 0.8) {
return {
directive,
action: "allow",
domains: uniqueDomains.slice(0, 5),
confidence: "high",
};
} else {
return {
directive,
action: "investigate",
riskDomains: uniqueDomains.filter(
(_, i) => violations.find((v) => v.blockedUri === uniqueDomains[i])?.riskLevel === "high"
),
confidence: "medium",
};
}
});
return recommendations;
}
```
## Configuration and Setup
### CSP Header Configuration
The CSP metrics system requires proper CSP headers with reporting:
```javascript
// In next.config.js or middleware
const cspDirectives = {
"default-src": "'self'",
"script-src": "'self' 'nonce-{NONCE}'",
"report-uri": "/api/csp-report",
"report-to": "csp-endpoint",
};
```
### Report-To Header
For modern browsers, configure the Report-To header:
```javascript
const reportToHeader = JSON.stringify({
group: "csp-endpoint",
max_age: 86400,
endpoints: [{ url: "/api/csp-report" }],
});
// Add to response headers
headers["Report-To"] = reportToHeader;
```
### Environment Configuration
```bash
# Enable CSP monitoring in production
NODE_ENV=production
# Optional: Configure monitoring sensitivity
CSP_MONITORING_SENSITIVITY=medium # low, medium, high
CSP_ALERT_THRESHOLD=5 # violations per 10 minutes
```
## Performance Considerations
### Rate Limiting
- **10 reports per minute per IP** prevents spam attacks
- **Exponential backoff** for repeated violations from same source
- **Memory cleanup** removes old violations automatically
### Memory Management
- **Violation buffer** limited to 7 days of data in memory
- **Hard cap** of 10,000 violation entries to prevent memory exhaustion
- **Automatic cleanup** runs every 100 requests (1% probability)
- **Efficient storage** using Map data structures
### Database Impact
- **No persistent storage** for real-time metrics (memory only)
- **Optional logging** to database for long-term analysis
- **Indexed queries** for historical data retrieval
## Security Considerations
### Privacy Protection
**⚠️ Data Collection Notice:**
- **IP addresses** are collected and stored in memory for security monitoring
- **User agent strings** are stored for browser compatibility analysis
- **Legal basis**: Processing is necessary for legitimate interests (GDPR Article 6(1)(f)) - specifically for security incident detection, prevention of CSP bypass attacks, and protection of website integrity
- **Retention**: In-memory storage only, automatically purged after 7 days or application restart
- **Data minimization**: Only violation-related metadata is retained, not page content
- **Balancing test**: The processing is limited to security purposes, uses temporary storage, and employs data minimization principles to ensure user privacy rights are respected
**Planned Privacy Enhancements:**
- IP anonymization options for GDPR compliance (roadmap)
- User agent sanitization to remove sensitive information (roadmap)
### Rate-Limiting Protection
- **Per-IP limits** prevent DoS attacks on reporting endpoint
- **Content-type validation** ensures proper report format
- **Request size limits** prevent memory exhaustion
### False Positive Handling
- **Learning mode** for new deployments
- **Whitelist support** for known legitimate violations
- **Risk score adjustment** based on historical patterns
## Troubleshooting
### Common Issues
#### High False Positive Rate
```javascript
// Check for legitimate violations being flagged
const metrics = await fetch("/api/csp-metrics?includeDetails=true");
const data = await metrics.json();
const falsePositives = data.data.violations.filter(
(v) => v.riskLevel === "high" && v.blockedUri.includes("legitimate-domain.com")
);
if (falsePositives.length > 0) {
console.log("Consider whitelisting:", falsePositives[0].blockedUri);
}
```
#### Missing Violation Reports
```javascript
// Check if CSP headers are properly configured
fetch("/").then((response) => {
const csp = response.headers.get("Content-Security-Policy");
if (!csp.includes("report-uri")) {
console.error("CSP report-uri directive missing");
}
});
```
#### Performance Issues
```javascript
// Monitor API response times
const start = performance.now();
const response = await fetch("/api/csp-metrics");
const duration = performance.now() - start;
if (duration > 2000) {
console.warn("CSP metrics API slow response:", duration + "ms");
}
```
## Related Documentation
- [Enhanced CSP Implementation](./security/enhanced-csp.md)
- [Security Monitoring](./security-monitoring.md)
- [Security Headers](./security-headers.md)
- [Rate Limiting](../lib/rateLimiter.ts)
## API Reference Summary
| Endpoint | Method | Purpose | Auth Required |
| ------------------ | ------ | ----------------------------------- | ------------- |
| `/api/csp-report` | POST | Receive CSP violation reports | No (public) |
| `/api/csp-metrics` | GET | Get violation metrics and analytics | Admin |
Both APIs are production-ready and provide comprehensive CSP monitoring capabilities for enterprise security requirements.

View File

@ -45,19 +45,16 @@ DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeo
### Fixed Issues
1. **Multiple PrismaClient Instances**:
- ❌ Before: Each scheduler created its own PrismaClient
- ✅ After: All modules use singleton pattern from `lib/prisma.ts`
- ❌ Before: Each scheduler created its own PrismaClient
- ✅ After: All modules use singleton pattern from `lib/prisma.ts`
2. **No Connection Management**:
- ❌ Before: No graceful shutdown or connection cleanup
- ✅ After: Proper cleanup on process termination
- ❌ Before: No graceful shutdown or connection cleanup
- ✅ After: Proper cleanup on process termination
3. **No Monitoring**:
- ❌ Before: No visibility into connection usage
- ✅ After: Health check endpoint and connection metrics
- ❌ Before: No visibility into connection usage
- ✅ After: Health check endpoint and connection metrics
### Key Files Modified

View File

@ -1,255 +0,0 @@
# Database Performance Optimizations
This document outlines the comprehensive database performance optimizations implemented for the LiveDash application, including strategic composite indexes and query optimization strategies.
## Overview
The optimization focuses on the most frequently queried patterns in the application, particularly around:
- AI processing request tracking and batching
- Session analytics and filtering
- Security audit log analysis
- Multi-tenant data isolation performance
## Applied Optimizations
### 1. AI Processing Request Optimizations
**Problem**: Heavy queries for batch processing and cost analysis
**Solution**: Strategic composite indexes with covering columns
```sql
-- Query pattern: companyId + processingStatus + requestedAt
CREATE INDEX "AIProcessingRequest_companyId_processingStatus_requestedAt_idx"
ON "AIProcessingRequest" ("companyId", "processingStatus", "requestedAt");
-- Covering index for batch processing
CREATE INDEX "AIProcessingRequest_companyId_processingStatus_covering_idx"
ON "AIProcessingRequest" ("companyId")
INCLUDE ("processingStatus", "batchId", "requestedAt", "sessionId");
```
**Impact**:
- ~70% faster batch job queries
- Reduced I/O for cost analysis reports
- Improved scheduler performance
### 2. Session Analytics Optimizations
**Problem**: Dashboard queries scanning large session tables
**Solution**: Composite indexes for common filtering patterns
```sql
-- Time-range queries with sentiment filtering
CREATE INDEX "Session_companyId_startTime_sentiment_covering_idx"
ON "Session" ("companyId", "startTime", "sentiment")
INCLUDE ("endTime", "category", "escalated", "messagesSent");
-- Performance analysis queries
CREATE INDEX "Session_companyId_performance_idx"
ON "Session" ("companyId", "avgResponseTime", "escalated")
INCLUDE ("startTime", "messagesSent");
```
**Impact**:
- ~85% faster dashboard load times
- Efficient date range filtering
- Optimized sentiment analysis queries
### 3. Security Audit Log Optimizations
**Problem**: Slow security monitoring and compliance queries
**Solution**: Specialized indexes for audit patterns
```sql
-- Admin security dashboard
CREATE INDEX "SecurityAuditLog_companyId_eventType_outcome_timestamp_idx"
ON "SecurityAuditLog" ("companyId", "eventType", "outcome", "timestamp");
-- Threat detection queries
CREATE INDEX "SecurityAuditLog_geographic_threat_idx"
ON "SecurityAuditLog" ("ipAddress", "country", "timestamp")
WHERE "outcome" IN ('FAILURE', 'BLOCKED', 'SUSPICIOUS')
INCLUDE ("eventType", "severity", "userId", "companyId");
```
**Impact**:
- ~90% faster security monitoring
- Efficient threat detection
- Improved compliance reporting
### 4. Message Processing Optimizations
**Problem**: Slow conversation timeline queries
**Solution**: Covering indexes for message retrieval
```sql
-- Message timeline with role filtering
CREATE INDEX "Message_sessionId_timestamp_role_covering_idx"
ON "Message" ("sessionId", "timestamp", "role")
INCLUDE ("content");
```
**Impact**:
- ~60% faster conversation loading
- Reduced memory usage for message queries
### 5. Processing Pipeline Optimizations
**Problem**: Inefficient status tracking for processing stages
**Solution**: Stage-specific indexes with error analysis
```sql
-- Processing pipeline monitoring
CREATE INDEX "SessionProcessingStatus_stage_status_startedAt_idx"
ON "SessionProcessingStatus" ("stage", "status", "startedAt")
INCLUDE ("sessionId", "completedAt", "retryCount");
-- Error analysis (partial index)
CREATE INDEX "SessionProcessingStatus_error_analysis_idx"
ON "SessionProcessingStatus" ("status", "stage")
WHERE "status" IN ('FAILED', 'RETRY_PENDING')
INCLUDE ("sessionId", "errorMessage", "retryCount", "startedAt");
```
**Impact**:
- ~75% faster processing monitoring
- Efficient error tracking
- Improved retry logic performance
## Index Strategy Principles
### 1. Composite Index Design
- **Leading column**: Most selective filter (usually companyId for multi-tenancy)
- **Secondary columns**: Common WHERE clause filters
- **Covering columns**: SELECT list columns via INCLUDE
### 2. Partial Indexes
- Used for error analysis and specific status filtering
- Reduces index size and maintenance overhead
- Improves write performance
### 3. Covering Indexes
- Include frequently accessed columns to avoid table lookups
- Reduces I/O for read-heavy operations
- Particularly effective for dashboard queries
## Query Pattern Analysis
### Most Optimized Patterns
1. **Multi-tenant filtering**: `companyId + filter + timestamp`
2. **Status tracking**: `processingStatus + entity + timestamp`
3. **Time-range analysis**: `timestamp + entity + filters`
4. **Geographic analysis**: `ipAddress + country + timestamp`
5. **Error tracking**: `status + stage + timestamp`
### Before vs After Performance
| Query Type | Before (ms) | After (ms) | Improvement |
| ------------------- | ----------- | ---------- | ----------- |
| Dashboard load | 2,500 | 375 | 85% |
| Batch queries | 1,800 | 540 | 70% |
| Security monitoring | 3,200 | 320 | 90% |
| Message timeline | 800 | 320 | 60% |
| Processing status | 1,200 | 300 | 75% |
## Maintenance Considerations
### Index Monitoring
- Monitor index usage with `pg_stat_user_indexes`
- Track bloat with `pg_stat_user_tables`
- Regular ANALYZE after bulk operations
### Write Performance Impact
- Composite indexes add ~15% write overhead
- Offset by dramatic read performance gains
- Monitored via slow query logs
### Storage Impact
- Indexes add ~25% to total storage
- Covering indexes reduce need for table scans
- Partial indexes minimize storage overhead
## Migration Safety
### CONCURRENTLY Operations
- All indexes created with `CREATE INDEX CONCURRENTLY`
- No table locks during creation
- Production-safe deployment
### Rollback Strategy
```sql
-- If performance degrades, indexes can be dropped individually
DROP INDEX CONCURRENTLY "specific_index_name";
```
### Monitoring Commands
```sql
-- Check index usage
SELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE tablename IN ('Session', 'AIProcessingRequest', 'SecurityAuditLog');
-- Monitor query performance
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
```
## Implementation Guidelines
### Development Environment
1. Apply migration: `pnpm prisma migrate deploy`
2. Run ANALYZE: `psql -c "ANALYZE;"`
3. Monitor performance: Enable slow query logging
### Production Environment
1. Apply during low-traffic window
2. Monitor index creation progress
3. Verify performance improvements
4. Update query plans via ANALYZE
## Future Optimizations
### Potential Improvements
1. **Partitioning**: Time-based partitioning for large audit logs
2. **Materialized views**: Pre-computed analytics for dashboards
3. **Query optimization**: Additional covering indexes based on usage patterns
4. **Connection pooling**: Enhanced database connection management
### Monitoring Strategy
- Set up automated index usage monitoring
- Track slow query evolution
- Monitor storage growth patterns
- Implement performance alerting
## Conclusion
These database optimizations provide:
- **70-90% improvement** in query performance
- **Reduced server load** through efficient indexing
- **Better user experience** with faster dashboards
- **Scalable foundation** for future growth
The optimizations are designed to be production-safe and monitoring-friendly, ensuring both immediate performance gains and long-term maintainability.

View File

@ -5,8 +5,7 @@ This document provides specific recommendations for optimizing database connecti
## Current Issues Observed
From your logs, we can see:
```bash
```
Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.neon.tech:5432`
[NODE-CRON] [WARN] missed execution at Sun Jun 29 2025 12:00:00 GMT+0200! Possible blocking IO or high CPU
```
@ -14,19 +13,16 @@ Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.ne
## Root Causes
### 1. Neon Connection Limits
- **Free Tier**: 20 concurrent connections
- **Pro Tier**: 100 concurrent connections
- **Multiple schedulers** can quickly exhaust connections
### 2. Connection Pooling Issues
- Each scheduler was creating separate PrismaClient instances
- No connection reuse between operations
- No retry logic for temporary failures
### 3. Neon-Specific Challenges
- **Auto-pause**: Databases pause after inactivity
- **Cold starts**: First connection after pause takes longer
- **Regional latency**: eu-central-1 may have variable latency
@ -34,7 +30,6 @@ Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.ne
## Solutions Implemented
### 1. Fixed Multiple PrismaClient Instances ✅
```typescript
// Before: Each file created its own client
const prisma = new PrismaClient(); // ❌
@ -44,30 +39,30 @@ import { prisma } from "./prisma.js"; // ✅
```
### 2. Added Connection Retry Logic ✅
```typescript
// Automatic retry for connection errors
await withRetry(async () => await databaseOperation(), {
await withRetry(
async () => await databaseOperation(),
{
maxRetries: 3,
initialDelay: 2000,
maxDelay: 10000,
backoffMultiplier: 2,
});
}
);
```
### 3. Enhanced Connection Pooling ✅
```typescript
// Production-ready pooling with @prisma/adapter-pg
USE_ENHANCED_POOLING = true;
DATABASE_CONNECTION_LIMIT = 20;
DATABASE_POOL_TIMEOUT = 10;
USE_ENHANCED_POOLING=true
DATABASE_CONNECTION_LIMIT=20
DATABASE_POOL_TIMEOUT=10
```
## Neon-Specific Configuration
### Environment Variables
```bash
# Optimized for Neon
DATABASE_URL="postgresql://user:pass@ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.neon.tech:5432/db?sslmode=require&connection_limit=15"
@ -84,7 +79,6 @@ SESSION_PROCESSING_INTERVAL="0 */2 * * *" # Every 2 hours instead of 1
```
### Connection String Optimization
```bash
# Add these parameters to your DATABASE_URL
?sslmode=require # Required for Neon
@ -97,21 +91,18 @@ SESSION_PROCESSING_INTERVAL="0 */2 * * *" # Every 2 hours instead of 1
## Monitoring & Troubleshooting
### 1. Health Check Endpoint
```bash
# Check connection health
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
curl -H "Authorization: Bearer your-token" \
http://localhost:3000/api/admin/database-health
```
### 2. Neon Dashboard Monitoring
- Monitor "Active connections" in Neon dashboard
- Check for connection spikes during scheduler runs
- Review query performance and slow queries
### 3. Application Logs
```bash
# Look for connection patterns
grep "Database connection" logs/*.log
@ -122,77 +113,65 @@ grep "retry" logs/*.log
## Performance Optimizations
### 1. Reduce Scheduler Frequency
```typescript
// Current intervals may be too aggressive
CSV_IMPORT_INTERVAL = "*/15 * * * *"; // ➜ "*/30 * * * *"
IMPORT_PROCESSING_INTERVAL = "*/5 * * * *"; // ➜ "*/10 * * * *"
SESSION_PROCESSING_INTERVAL = "0 * * * *"; // ➜ "0 */2 * * *"
CSV_IMPORT_INTERVAL="*/15 * * * *" // ➜ "*/30 * * * *"
IMPORT_PROCESSING_INTERVAL="*/5 * * * *" // ➜ "*/10 * * * *"
SESSION_PROCESSING_INTERVAL="0 * * * *" // ➜ "0 */2 * * *"
```
### 2. Batch Size Optimization
```typescript
// Reduce batch sizes to avoid long-running transactions
CSV_IMPORT_BATCH_SIZE = 50; // ➜ 25
IMPORT_PROCESSING_BATCH_SIZE = 50; // ➜ 25
SESSION_PROCESSING_BATCH_SIZE = 20; // ➜ 10
CSV_IMPORT_BATCH_SIZE=50 // ➜ 25
IMPORT_PROCESSING_BATCH_SIZE=50 // ➜ 25
SESSION_PROCESSING_BATCH_SIZE=20 // ➜ 10
```
### 3. Connection Keepalive
```typescript
// Keep connections warm to avoid cold starts
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL + "&keepalive=true",
},
},
url: process.env.DATABASE_URL + "&keepalive=true"
}
}
});
```
## Troubleshooting Common Issues
### "Can't reach database server"
**Causes:**
- Neon database auto-paused
- Connection limit exceeded
- Network issues
**Solutions:**
1. Enable enhanced pooling: `USE_ENHANCED_POOLING=true`
2. Reduce connection limit: `DATABASE_CONNECTION_LIMIT=15`
3. Implement retry logic (already done)
4. Check Neon dashboard for database status
### "Connection terminated"
**Causes:**
- Idle connection timeout
- Neon maintenance
- Long-running transactions
**Solutions:**
1. Increase pool timeout: `DATABASE_POOL_TIMEOUT=30`
2. Add connection cycling
3. Break large operations into smaller batches
### "Missed cron execution"
**Causes:**
- Blocking database operations
- Scheduler overlap
- High CPU usage
**Solutions:**
1. Reduce scheduler frequency
2. Add concurrency limits
3. Monitor scheduler execution time
@ -200,7 +179,6 @@ const prisma = new PrismaClient({
## Recommended Production Settings
### For Neon Free Tier (20 connections)
```bash
DATABASE_CONNECTION_LIMIT=15
DATABASE_POOL_TIMEOUT=30
@ -211,7 +189,6 @@ SESSION_PROCESSING_INTERVAL="0 */3 * * *"
```
### For Neon Pro Tier (100 connections)
```bash
DATABASE_CONNECTION_LIMIT=50
DATABASE_POOL_TIMEOUT=20

View File

@ -7,7 +7,6 @@ Successfully refactored the session processing pipeline from a simple status-bas
## Problems Solved
### Original Issues
1. **Inconsistent Status Tracking**: The old system used a simple enum on SessionImport that didn't properly track the multi-stage processing pipeline
2. **Poor Error Visibility**: Error messages were buried in the SessionImport table and not easily accessible
3. **No Stage-Specific Tracking**: The system couldn't track which specific stage of processing failed
@ -15,7 +14,6 @@ Successfully refactored the session processing pipeline from a simple status-bas
5. **Linting Errors**: Multiple TypeScript files referencing removed database fields
### Schema Changes Made
- **Removed** old `status`, `errorMsg`, and `processedAt` columns from SessionImport
- **Removed** `processed` field from Session
- **Added** new `SessionProcessingStatus` table with granular stage tracking
@ -24,8 +22,6 @@ Successfully refactored the session processing pipeline from a simple status-bas
## New Processing Pipeline
### Processing Stages
<!-- prettier-ignore -->
```typescript
enum ProcessingStage {
CSV_IMPORT // SessionImport created
@ -43,9 +39,7 @@ enum ProcessingStatus {
### Key Components
#### 1. ProcessingStatusManager
Centralized class for managing processing status with methods:
- `initializeSession()` - Set up processing status for new sessions
- `startStage()`, `completeStage()`, `failStage()`, `skipStage()` - Stage management
- `getSessionsNeedingProcessing()` - Query sessions by stage and status
@ -54,14 +48,12 @@ Centralized class for managing processing status with methods:
- `resetStageForRetry()` - Reset failed stages
#### 2. Updated Processing Scheduler
- Integrated with new `ProcessingStatusManager`
- Tracks AI analysis and question extraction stages
- Records detailed processing metadata
- Proper error handling and retry capabilities
#### 3. Migration System
- Successfully migrated all 109 existing sessions
- Determined current state based on existing data
- Preserved all existing functionality
@ -69,7 +61,6 @@ Centralized class for managing processing status with methods:
## Current Pipeline Status
After migration and refactoring:
- **CSV_IMPORT**: 109 completed
- **TRANSCRIPT_FETCH**: 109 completed
- **SESSION_CREATION**: 109 completed
@ -79,21 +70,18 @@ After migration and refactoring:
## Files Updated/Created
### New Files
- `lib/processingStatusManager.ts` - Core processing status management
- `check-refactored-pipeline-status.ts` - New pipeline status checker
- `migrate-to-refactored-system.ts` - Migration script
- `docs/processing-system-refactor.md` - This documentation
### Updated Files
- `prisma/schema.prisma` - Added new processing status tables
- `lib/processingScheduler.ts` - Integrated with new status system
- `debug-import-status.ts` - Updated to use new system
- `fix-import-status.ts` - Updated to use new system
### Removed Files
- `check-pipeline-status.ts` - Replaced by refactored version
## Benefits Achieved
@ -109,25 +97,21 @@ After migration and refactoring:
## Usage Examples
### Check Pipeline Status
```bash
npx tsx check-refactored-pipeline-status.ts
```
### Debug Processing Issues
```bash
npx tsx debug-import-status.ts
```
### Fix/Retry Failed Sessions
```bash
npx tsx fix-import-status.ts
```
### Process Sessions
```bash
npx tsx test-ai-processing.ts
```

View File

@ -1,428 +0,0 @@
# Scheduler Architecture for Horizontal Scaling
This document describes the extracted scheduler architecture that enables horizontal scaling of background processing tasks.
## Architecture Overview
The scheduler system has been refactored from a monolithic approach to a service-oriented architecture that supports:
- **Individual Scheduler Services** - Each scheduler runs as a separate service
- **Horizontal Scaling** - Multiple instances of the same scheduler can run across different machines
- **Health Monitoring** - Built-in health checks for load balancers and orchestrators
- **Graceful Shutdown** - Proper handling of shutdown signals for zero-downtime deployments
- **Centralized Management** - Optional scheduler manager for coordinated operations
## Components
### 1. BaseSchedulerService
Abstract base class providing common functionality:
```typescript
export abstract class BaseSchedulerService extends EventEmitter {
// Common scheduler functionality
protected abstract executeTask(): Promise<void>;
async start(): Promise<void>;
async stop(): Promise<void>;
pause(): void;
resume(): void;
getHealthStatus(): HealthStatus;
getMetrics(): SchedulerMetrics;
}
```
**Features:**
- Status management (STOPPED, STARTING, RUNNING, PAUSED, ERROR)
- Metrics collection (run counts, timing, success/failure rates)
- Event emission for monitoring
- Configurable intervals and timeouts
- Automatic retry handling
### 2. Individual Scheduler Services
#### CsvImportSchedulerService
Handles periodic CSV data import from companies:
```typescript
const csvScheduler = new CsvImportSchedulerService({
interval: "*/10 * * * *", // Every 10 minutes
batchSize: 10,
maxConcurrentImports: 5,
timeout: 300000, // 5 minutes
});
```
**Features:**
- Batch processing with configurable concurrency
- Duplicate detection
- Company-specific error handling
- Progress monitoring
#### Additional Schedulers (To Be Implemented)
- `ImportProcessingSchedulerService` - Process imported CSV data into sessions
- `SessionProcessingSchedulerService` - AI analysis and categorization
- `BatchProcessingSchedulerService` - OpenAI Batch API integration
### 3. SchedulerManager
Orchestrates multiple schedulers in a single process:
```typescript
const manager = new SchedulerManager();
manager.registerScheduler({
id: "csv-import",
name: "CSV Import Scheduler",
service: new CsvImportSchedulerService(),
autoStart: true,
critical: true, // Auto-restart on failure
});
await manager.startAll();
```
**Features:**
- Automatic restart of failed critical schedulers
- Health monitoring across all schedulers
- Coordinated start/stop operations
- Event aggregation and logging
### 4. Standalone Scheduler Runner
Runs individual schedulers as separate processes:
```bash
# Run CSV import scheduler as standalone process
npx tsx lib/services/schedulers/StandaloneSchedulerRunner.ts --scheduler=csv-import
# List available schedulers
npx tsx lib/services/schedulers/StandaloneSchedulerRunner.ts --list
```
**Features:**
- Independent process execution
- Environment variable configuration
- Graceful shutdown handling
- Health reporting for monitoring
## Deployment Patterns
### 1. Single Process (Current Default)
All schedulers run within the main Next.js server process:
```typescript
// server.ts
import { initializeSchedulers } from "./lib/services/schedulers/ServerSchedulerIntegration";
await initializeSchedulers();
```
**Pros:**
- Simple deployment
- Lower resource usage
- Easy local development
**Cons:**
- Limited scalability
- Single point of failure
- Resource contention
### 2. Separate Processes
Each scheduler runs as an independent process:
```bash
# Terminal 1: Main application
npm run dev
# Terminal 2: CSV Import Scheduler
npm run scheduler:csv-import
# Terminal 3: Session Processing Scheduler
npm run scheduler:session-processing
```
**Pros:**
- Independent scaling
- Fault isolation
- Resource optimization per scheduler
**Cons:**
- More complex deployment
- Higher resource overhead
- Inter-process coordination needed
### 3. Container Orchestration (Recommended for Production)
Each scheduler runs in separate containers managed by Kubernetes/Docker Swarm:
```yaml
# docker-compose.yml
version: "3.8"
services:
app:
build: .
environment:
- SCHEDULER_ENABLED=false # Disable in-process schedulers
csv-import-scheduler:
build: .
command: npx tsx lib/services/schedulers/StandaloneSchedulerRunner.ts --scheduler=csv-import
environment:
- CSV_IMPORT_INTERVAL=*/10 * * * *
- CSV_IMPORT_BATCH_SIZE=10
session-processing-scheduler:
build: .
command: npx tsx lib/services/schedulers/StandaloneSchedulerRunner.ts --scheduler=session-processing
environment:
- SESSION_PROCESSING_INTERVAL=*/5 * * * *
```
**Pros:**
- Full horizontal scaling
- Independent resource allocation
- Health monitoring integration
- Zero-downtime deployments
**Cons:**
- Complex orchestration setup
- Network latency considerations
- Distributed system challenges
## Configuration
### Environment Variables
```bash
# Global Scheduler Settings
SCHEDULER_ENABLED=true
SCHEDULER_AUTO_RESTART=true
# CSV Import Scheduler
CSV_IMPORT_INTERVAL="*/10 * * * *"
CSV_IMPORT_BATCH_SIZE=10
CSV_IMPORT_MAX_CONCURRENT=5
CSV_IMPORT_TIMEOUT=300000
# Import Processing Scheduler
IMPORT_PROCESSING_INTERVAL="*/2 * * * *"
IMPORT_PROCESSING_TIMEOUT=120000
# Session Processing Scheduler
SESSION_PROCESSING_INTERVAL="*/5 * * * *"
SESSION_PROCESSING_BATCH_SIZE=50
# Batch Processing Scheduler
BATCH_PROCESSING_INTERVAL="*/5 * * * *"
BATCH_PROCESSING_CHECK_INTERVAL="*/2 * * * *"
```
### Package.json Scripts
```json
{
"scripts": {
"scheduler:csv-import": "tsx lib/services/schedulers/StandaloneSchedulerRunner.ts --scheduler=csv-import",
"scheduler:import-processing": "tsx lib/services/schedulers/StandaloneSchedulerRunner.ts --scheduler=import-processing",
"scheduler:session-processing": "tsx lib/services/schedulers/StandaloneSchedulerRunner.ts --scheduler=session-processing",
"scheduler:batch-processing": "tsx lib/services/schedulers/StandaloneSchedulerRunner.ts --scheduler=batch-processing"
}
}
```
## Health Monitoring
### Health Check Endpoints
```bash
# Overall scheduler health
GET /api/admin/schedulers/health
# Scheduler management
GET /api/admin/schedulers
POST /api/admin/schedulers
```
### Response Format
```json
{
"healthy": true,
"status": "healthy",
"timestamp": "2024-01-15T10:30:00.000Z",
"schedulers": {
"total": 4,
"running": 4,
"errors": 0
},
"details": {
"csv-import": {
"status": "RUNNING",
"healthy": true,
"lastSuccess": "2024-01-15T10:25:00.000Z"
}
}
}
```
### Kubernetes Integration
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: csv-import-scheduler
spec:
template:
spec:
containers:
- name: scheduler
image: livedash:latest
command:
[
"npx",
"tsx",
"lib/services/schedulers/StandaloneSchedulerRunner.ts",
"--scheduler=csv-import",
]
livenessProbe:
httpGet:
path: /api/admin/schedulers/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/admin/schedulers/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
```
## Scaling Strategies
### 1. Vertical Scaling
Increase resources for scheduler processes:
```yaml
# docker-compose.yml
csv-import-scheduler:
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
cpus: "1.0"
memory: 1G
```
### 2. Horizontal Scaling
Run multiple instances of the same scheduler:
```yaml
# Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: csv-import-scheduler
spec:
replicas: 3 # Multiple instances
template:
spec:
containers:
- name: scheduler
env:
- name: SCHEDULER_INSTANCE_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
```
**Note:** Ensure scheduler logic handles multiple instances correctly (e.g., using database locks or partitioning).
### 3. Geographic Distribution
Deploy schedulers across different regions:
```yaml
# Region-specific scheduling
csv-import-scheduler-us:
environment:
- REGION=us
- CSV_COMPANIES_FILTER=region:us
csv-import-scheduler-eu:
environment:
- REGION=eu
- CSV_COMPANIES_FILTER=region:eu
```
## Migration Guide
### From Current Architecture
1. **Phase 1: Extract Schedulers**
- ✅ Create BaseSchedulerService
- ✅ Implement CsvImportSchedulerService
- ✅ Create SchedulerManager
- ⏳ Implement remaining scheduler services
2. **Phase 2: Deployment Options**
- ✅ Add ServerSchedulerIntegration for backwards compatibility
- ✅ Create StandaloneSchedulerRunner
- ✅ Add health check endpoints
3. **Phase 3: Container Support**
- ⏳ Create Dockerfile for scheduler containers
- ⏳ Add Kubernetes manifests
- ⏳ Implement distributed coordination
4. **Phase 4: Production Migration**
- ⏳ Deploy separate scheduler containers
- ⏳ Monitor performance and stability
- ⏳ Gradually increase horizontal scaling
### Breaking Changes
- Scheduler initialization moved from `server.ts` to `ServerSchedulerIntegration`
- Individual scheduler functions replaced with service classes
- Configuration moved to environment variables
## Benefits
1. **Scalability**: Independent scaling of different scheduler types
2. **Reliability**: Fault isolation prevents cascading failures
3. **Performance**: Optimized resource allocation per scheduler
4. **Monitoring**: Granular health checks and metrics
5. **Deployment**: Zero-downtime updates and rollbacks
6. **Development**: Easier testing and debugging of individual schedulers
## Next Steps
1. Implement remaining scheduler services (ImportProcessing, SessionProcessing, BatchProcessing)
2. Add distributed coordination for multi-instance schedulers
3. Create Kubernetes operators for automatic scaling
4. Implement scheduler-specific metrics and dashboards
5. Add scheduler performance optimization tools

View File

@ -24,11 +24,7 @@ The ProcessingScheduler picks up sessions where `processed` is **NOT** `true`, w
**Query used:**
```javascript
{
processed: {
not: true;
}
} // Either false or null
{ processed: { not: true } } // Either false or null
```
## Complete Workflow
@ -98,19 +94,17 @@ node scripts/manual-triggers.js both
1. **Check if sessions have transcripts:**
```bash
node scripts/manual-triggers.js status
```
```bash
node scripts/manual-triggers.js status
```
2. **If "Sessions with transcript" is 0:**
- Sessions exist but transcripts haven't been fetched yet
- Run session refresh: `node scripts/manual-triggers.js refresh`
- Sessions exist but transcripts haven't been fetched yet
- Run session refresh: `node scripts/manual-triggers.js refresh`
3. **If "Ready for processing" is 0 but "Sessions with transcript" > 0:**
- All sessions with transcripts have already been processed
- Check if `OPENAI_API_KEY` is set in environment
- All sessions with transcripts have already been processed
- Check if `OPENAI_API_KEY` is set in environment
### Common Issues
@ -133,7 +127,6 @@ node scripts/manual-triggers.js status
### Before AI Processing
<!-- prettier-ignore -->
```javascript
{
id: "session-uuid",
@ -148,7 +141,6 @@ node scripts/manual-triggers.js status
### After AI Processing
<!-- prettier-ignore -->
```javascript
{
id: "session-uuid",
@ -200,20 +192,20 @@ NEXTAUTH_URL="http://localhost:3000"
1. **Trigger session refresh** to fetch transcripts:
```bash
node scripts/manual-triggers.js refresh
```
```bash
node scripts/manual-triggers.js refresh
```
2. **Check status** to see if transcripts were fetched:
```bash
node scripts/manual-triggers.js status
```
```bash
node scripts/manual-triggers.js status
```
3. **Trigger processing** if transcripts are available:
```bash
node scripts/manual-triggers.js process
```
```bash
node scripts/manual-triggers.js process
```
4. **View results** in the dashboard session details pages

View File

@ -1,263 +0,0 @@
# Security Audit Logging System
This document provides an overview of the comprehensive security audit logging system implemented in LiveDash.
## Overview
The security audit logging system provides comprehensive tracking of security-critical events, authentication activities, and administrative actions across the platform. It is designed for compliance, incident investigation, and security monitoring.
## Features
### 1. Comprehensive Event Tracking
The system logs the following event types:
- **Authentication Events**: Login attempts, password changes, session management
- **Authorization Events**: Permission checks, access denied events
- **User Management**: User creation, modification, deletion, invitations
- **Company Management**: Company suspension, settings changes
- **Rate Limiting**: Abuse prevention and rate limit violations
- **CSRF Protection**: Cross-site request forgery protection events
- **Security Headers**: Security header violations
- **Password Reset**: Password reset flows and token validation
- **Platform Admin**: Administrative activities by platform users
- **Data Privacy**: Data export and privacy-related events
- **System Configuration**: System setting changes
- **API Security**: API-related security events
### 2. Structured Logging
Each audit log entry includes:
- **Event Type**: Categorizes the security event
- **Action**: Specific action performed
- **Outcome**: Success, failure, blocked, rate limited, or suspicious
- **Severity**: Info, low, medium, high, or critical
- **Context**: User ID, company ID, platform user ID, IP address, user agent
- **Metadata**: Structured additional information
- **Timestamp**: Immutable timestamp for chronological ordering
### 3. Multi-Tenant Security
- Company-scoped audit logs ensure data isolation
- Platform admin actions tracked separately
- Role-based access controls for audit log viewing
### 4. Log Retention and Management
- **Configurable Retention Policies**: Different retention periods based on event type and severity
- **Automatic Archival**: Critical and high-severity events archived before deletion
- **Scheduled Cleanup**: Weekly automated retention policy execution
- **Manual Controls**: Admin interface for manual retention execution
### 5. Administrative Interface
- **Audit Log Viewer**: Comprehensive filtering and search capabilities
- **Retention Management**: View statistics and execute retention policies
- **Real-time Monitoring**: Track security events as they occur
## Architecture
### Core Components
1. **SecurityAuditLogger** (`lib/securityAuditLogger.ts`): Centralized logging service
2. **AuditLogRetentionManager** (`lib/auditLogRetention.ts`): Retention policy management
3. **AuditLogScheduler** (`lib/auditLogScheduler.ts`): Scheduled retention execution
4. **Admin API** (`app/api/admin/audit-logs/`): REST API for audit log access
5. **Admin UI** (`app/dashboard/audit-logs/`): Administrative interface
### Database Schema
The `SecurityAuditLog` model includes:
```prisma
model SecurityAuditLog {
id String @id @default(uuid())
eventType SecurityEventType
action String @db.VarChar(255)
outcome AuditOutcome
severity AuditSeverity @default(INFO)
userId String?
companyId String?
platformUserId String?
ipAddress String? @db.Inet
userAgent String?
country String? @db.VarChar(3)
metadata Json?
errorMessage String?
sessionId String? @db.VarChar(255)
requestId String? @db.VarChar(255)
timestamp DateTime @default(now()) @db.Timestamptz(6)
// Relations and indexes...
}
```
## Usage
### Logging Security Events
```typescript
import { securityAuditLogger, AuditOutcome } from "./lib/securityAuditLogger";
// Log authentication event
await securityAuditLogger.logAuthentication("user_login_success", AuditOutcome.SUCCESS, {
userId: "user-123",
companyId: "company-456",
ipAddress: "192.168.1.***",
userAgent: "Mozilla/5.0 (masked)",
metadata: { loginMethod: "password" },
});
// Log authorization failure
await securityAuditLogger.logAuthorization(
"admin_access_denied",
AuditOutcome.BLOCKED,
{
userId: "user-123",
companyId: "company-456",
metadata: { requiredRole: "ADMIN", currentRole: "USER" },
},
"Insufficient permissions for admin access"
);
```
### Viewing Audit Logs
Administrators can access audit logs through:
1. **Dashboard UI**: Navigate to "Audit Logs" in the sidebar
2. **API Access**: GET `/api/admin/audit-logs` with filtering parameters
3. **Retention Management**: GET/POST `/api/admin/audit-logs/retention`
### Filtering Options
- Event type (authentication, authorization, etc.)
- Outcome (success, failure, blocked, etc.)
- Severity level (info, low, medium, high, critical)
- Date range
- User ID
- Pagination support
## Configuration
### Environment Variables
```bash
# Enable/disable audit logging (default: true)
AUDIT_LOGGING_ENABLED=true
# Enable/disable retention scheduler (default: true)
AUDIT_LOG_RETENTION_ENABLED=true
# Retention schedule (cron format, default: 2 AM every Sunday)
AUDIT_LOG_RETENTION_SCHEDULE="0 2 * * 0"
# Dry run mode for retention (default: false)
AUDIT_LOG_RETENTION_DRY_RUN=false
```
### Default Retention Policies
1. **Critical Events**: 7 years retention with archival
2. **High Severity Events**: 3 years retention with archival
3. **Authentication Events**: 2 years retention with archival
4. **Platform Admin Events**: 3 years retention with archival
5. **User Management Events**: 2 years retention with archival
6. **General Events**: 1 year retention without archival
## Security Considerations
### Data Protection
- **IP Address Storage**: Client IP addresses stored for geographic analysis
- **Sensitive Data Redaction**: Passwords, tokens, and emails marked as `[REDACTED]`
- **Metadata Sanitization**: Complex objects sanitized to prevent data leakage
### Access Controls
- **Admin-Only Access**: Only users with `ADMIN` role can view audit logs
- **Company Isolation**: Users can only view logs for their own company
- **Platform Separation**: Platform admin logs tracked separately
### Performance
- **Async Logging**: All logging operations are asynchronous to avoid blocking
- **Error Handling**: Logging failures don't affect application functionality
- **Indexed Queries**: Database indexes optimize common query patterns
- **Batch Operations**: Retention policies use batch operations for efficiency
## Compliance Features
### Audit Standards
- **Immutable Records**: Audit logs cannot be modified after creation
- **Chronological Ordering**: Precise timestamps for event sequencing
- **Non-Repudiation**: User actions clearly attributed and timestamped
- **Comprehensive Coverage**: All security-relevant events logged
### Reporting
- **Event Statistics**: Summary statistics by event type, severity, and time period
- **Export Capabilities**: Structured data export for compliance reporting
- **Retention Tracking**: Detailed logging of retention policy execution
## Monitoring and Alerting
### System Health
- **Scheduler Status**: Monitor retention scheduler health
- **Error Tracking**: Log retention and audit logging errors
- **Performance Metrics**: Track logging performance and database impact
### Security Monitoring
- **Failed Authentication Patterns**: Track repeated login failures
- **Privilege Escalation**: Monitor administrative action patterns
- **Suspicious Activity**: Identify unusual access patterns
## Troubleshooting
### Common Issues
1. **Audit Logging Disabled**: Check `AUDIT_LOGGING_ENABLED` environment variable
2. **Retention Not Running**: Verify `AUDIT_LOG_RETENTION_ENABLED` and scheduler status
3. **Access Denied**: Ensure user has `ADMIN` role for audit log access
4. **Performance Issues**: Review retention policies and database indexes
### Debug Information
- Check application logs for scheduler startup messages
- Monitor database query performance for audit log operations
- Review retention policy validation warnings
## Best Practices
### Implementation
1. **Always use the centralized logger**: Don't bypass the `securityAuditLogger`
2. **Include relevant context**: Provide user, company, and IP information
3. **Use appropriate severity levels**: Follow the severity assignment guidelines
4. **Sanitize sensitive data**: Use `createAuditMetadata()` for safe metadata
### Operations
1. **Regular retention review**: Monitor retention statistics and adjust policies
2. **Archive critical data**: Ensure important logs are archived before deletion
3. **Monitor storage usage**: Track audit log database growth
4. **Test restoration**: Verify archived data can be restored when needed
## Future Enhancements
### Planned Features
- **Real-time Alerting**: Immediate notifications for critical security events
- **Advanced Analytics**: ML-based anomaly detection and pattern recognition
- **Export Formats**: Additional export formats for compliance reporting
- **External Integration**: SIEM and security tool integrations
### Performance Optimizations
- **Log Partitioning**: Database partitioning for improved query performance
- **Compression**: Log compression for storage efficiency
- **Streaming**: Real-time log streaming for external systems

View File

@ -1,231 +0,0 @@
# HTTP Security Headers Implementation
This document describes the comprehensive HTTP security headers implementation in LiveDash-Node to protect against XSS, clickjacking, and other web vulnerabilities.
## Overview
The application implements multiple layers of HTTP security headers to provide defense-in-depth protection against common web vulnerabilities identified in OWASP Top 10 and security best practices.
## Implemented Security Headers
### Core Security Headers
#### X-Content-Type-Options: nosniff
- **Purpose**: Prevents MIME type sniffing attacks
- **Protection**: Stops browsers from interpreting files as different MIME types than declared
- **Value**: `nosniff`
#### X-Frame-Options: DENY
- **Purpose**: Prevents clickjacking attacks
- **Protection**: Blocks embedding the site in frames/iframes
- **Value**: `DENY`
#### X-XSS-Protection: 1; mode=block
- **Purpose**: Enables XSS protection in legacy browsers
- **Protection**: Activates built-in XSS filtering (primarily for older browsers)
- **Value**: `1; mode=block`
#### Referrer-Policy: strict-origin-when-cross-origin
- **Purpose**: Controls referrer information leakage
- **Protection**: Limits referrer data sent to external sites
- **Value**: `strict-origin-when-cross-origin`
#### X-DNS-Prefetch-Control: off
- **Purpose**: Prevents DNS rebinding attacks
- **Protection**: Disables DNS prefetching to reduce attack surface
- **Value**: `off`
### Content Security Policy (CSP)
Comprehensive CSP implementation with the following directives:
```
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests
```
#### Key CSP Directives
- **default-src 'self'**: Restrictive default for all resource types
- **script-src 'self' 'unsafe-eval' 'unsafe-inline'**: Allows Next.js dev tools and React functionality
- **style-src 'self' 'unsafe-inline'**: Enables TailwindCSS and component styles
- **img-src 'self' data: https:**: Allows secure image sources
- **frame-ancestors 'none'**: Prevents embedding (reinforces X-Frame-Options)
- **object-src 'none'**: Blocks dangerous plugins and embeds
- **upgrade-insecure-requests**: Automatically upgrades HTTP to HTTPS
### Permissions Policy
Controls browser feature access:
```
Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=()
```
- **camera=()**: Disables camera access
- **microphone=()**: Disables microphone access
- **geolocation=()**: Disables location tracking
- **interest-cohort=()**: Blocks FLoC (privacy protection)
- **browsing-topics=()**: Blocks Topics API (privacy protection)
### Strict Transport Security (HSTS)
**Production Only**: `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`
- **max-age=31536000**: 1 year HSTS policy
- **includeSubDomains**: Applies to all subdomains
- **preload**: Ready for HSTS preload list inclusion
## Configuration
### Next.js Configuration
Headers are configured in `next.config.js`:
```javascript
headers: async () => {
return [
{
source: "/(.*)",
headers: [
// Security headers configuration
],
},
{
source: "/(.*)",
headers:
process.env.NODE_ENV === "production"
? [
// HSTS header for production only
]
: [],
},
];
};
```
### Environment-Specific Behavior
- **Development**: All headers except HSTS
- **Production**: All headers including HSTS
## Testing
### Unit Tests
Location: `tests/unit/http-security-headers.test.ts`
Tests cover:
- Individual header validation
- CSP directive verification
- Permissions Policy validation
- Environment-specific configuration
- Next.js compatibility checks
### Integration Tests
Location: `tests/integration/security-headers-basic.test.ts`
Tests cover:
- Next.js configuration validation
- Header generation verification
- Environment-based header differences
### Manual Testing
Use the security headers testing script:
```bash
# Test local development server
pnpm test:security-headers http://localhost:3000
# Test production deployment
pnpm test:security-headers https://your-domain.com
```
## Security Benefits
### Protection Against OWASP Top 10
1. **A03:2021 - Injection**: CSP prevents script injection
2. **A05:2021 - Security Misconfiguration**: Comprehensive headers reduce attack surface
3. **A06:2021 - Vulnerable Components**: CSP limits execution context
4. **A07:2021 - Identification and Authentication Failures**: HSTS prevents downgrade attacks
### Additional Security Benefits
- **Clickjacking Protection**: X-Frame-Options + CSP frame-ancestors
- **MIME Sniffing Prevention**: X-Content-Type-Options
- **Information Leakage Reduction**: Referrer-Policy
- **Privacy Protection**: Permissions Policy restrictions
- **Transport Security**: HSTS enforcement
## Maintenance
### Regular Reviews
1. **Quarterly CSP Review**: Analyze CSP violations and tighten policies
2. **Annual Header Audit**: Review new security headers and standards
3. **Dependency Updates**: Ensure compatibility with framework updates
### Monitoring
- Monitor CSP violation reports (when implemented)
- Use online tools like securityheaders.com for validation
- Include security header tests in CI/CD pipeline
### Future Enhancements
Planned improvements:
1. CSP violation reporting endpoint
2. Nonce-based CSP for inline scripts
3. Additional Permissions Policy restrictions
4. Content-Type validation middleware
## Compatibility
### Next.js Compatibility
Headers are configured to be compatible with:
- Next.js 15+ App Router
- React 19 development tools
- TailwindCSS 4 styling system
- Development hot reload functionality
### Browser Support
Security headers are supported by:
- All modern browsers (Chrome 60+, Firefox 60+, Safari 12+)
- Graceful degradation for older browsers
- Progressive enhancement approach
## Troubleshooting
### Common Issues
1. **CSP Violations**: Check browser console for CSP errors
2. **Styling Issues**: Verify style-src allows 'unsafe-inline'
3. **Script Errors**: Ensure script-src permits necessary scripts
4. **Development Issues**: Use `pnpm dev:next-only` to isolate Next.js
### Debug Tools
- Browser DevTools Security tab
- CSP Evaluator: <https://csp-evaluator.withgoogle.com/>
- Security Headers Scanner: <https://securityheaders.com/>
## References
- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/)
- [MDN Security Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#security)
- [Next.js Security Headers](https://nextjs.org/docs/app/api-reference/config/headers)
- [Content Security Policy Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)

View File

@ -1,457 +0,0 @@
# Security Monitoring and Alerting System
## Overview
The Security Monitoring and Alerting System provides comprehensive real-time security monitoring, anomaly detection, and threat alerting for the LiveDash-Node application. It integrates with the existing audit logging system to provide proactive security monitoring and incident response capabilities.
## Architecture
### Core Components
1. **Security Monitoring Service** (`lib/securityMonitoring.ts`)
- Real-time event processing
- Anomaly detection algorithms
- Alert generation and management
- Security score calculation
- Threat level assessment
2. **Enhanced Security Logging** (`enhancedSecurityLog`)
- Integrates with existing audit logger
- Processes events through monitoring system
- Triggers immediate threat detection
3. **API Endpoints** (`app/api/admin/security-monitoring/`)
- `/api/admin/security-monitoring` - Main metrics and configuration
- `/api/admin/security-monitoring/alerts` - Alert management
- `/api/admin/security-monitoring/export` - Data export
- `/api/admin/security-monitoring/threat-analysis` - Threat analysis
4. **Dashboard UI** (`app/platform/security/page.tsx`)
- Real-time security metrics
- Active alerts management
- Threat analysis visualization
- Configuration management
## Features
### Real-time Monitoring
- **Authentication Events**: Login attempts, failures, brute force attacks
- **Rate Limiting**: Excessive request patterns, API abuse
- **Admin Activity**: Unusual administrative actions
- **Geographic Anomalies**: Logins from unusual locations
- **Temporal Anomalies**: Activity spikes outside normal patterns
### Alert Types
```typescript
enum AlertType {
AUTHENTICATION_ANOMALY = "AUTHENTICATION_ANOMALY",
RATE_LIMIT_BREACH = "RATE_LIMIT_BREACH",
MULTIPLE_FAILED_LOGINS = "MULTIPLE_FAILED_LOGINS",
SUSPICIOUS_IP_ACTIVITY = "SUSPICIOUS_IP_ACTIVITY",
PRIVILEGE_ESCALATION = "PRIVILEGE_ESCALATION",
DATA_BREACH_ATTEMPT = "DATA_BREACH_ATTEMPT",
CSRF_ATTACK = "CSRF_ATTACK",
CSP_VIOLATION_SPIKE = "CSP_VIOLATION_SPIKE",
ACCOUNT_ENUMERATION = "ACCOUNT_ENUMERATION",
BRUTE_FORCE_ATTACK = "BRUTE_FORCE_ATTACK",
UNUSUAL_ADMIN_ACTIVITY = "UNUSUAL_ADMIN_ACTIVITY",
GEOLOCATION_ANOMALY = "GEOLOCATION_ANOMALY",
MASS_DATA_ACCESS = "MASS_DATA_ACCESS",
SUSPICIOUS_USER_AGENT = "SUSPICIOUS_USER_AGENT",
SESSION_HIJACKING = "SESSION_HIJACKING",
}
```
### Anomaly Detection
The system implements several anomaly detection algorithms:
1. **Geographic Anomaly Detection**
- Detects logins from unusual countries
- Compares against historical user patterns
- Confidence scoring based on deviation
2. **Temporal Anomaly Detection**
- Identifies activity spikes during unusual hours
- Compares current activity to historical averages
- Configurable thresholds for different event types
3. **Behavioral Anomaly Detection**
- Multiple failed login attempts
- Rapid succession of actions
- Pattern deviation analysis
### Security Scoring
The system calculates a real-time security score (0-100) based on:
- Critical security events (weight: 25)
- Active unresolved alerts (weight: 30)
- High-severity threats (weight: 20)
- Overall event volume (weight: 15)
- System stability factors (weight: 10)
### Threat Levels
```typescript
enum ThreatLevel {
LOW = "LOW", // Score: 85-100
MODERATE = "MODERATE", // Score: 70-84
HIGH = "HIGH", // Score: 50-69
CRITICAL = "CRITICAL", // Score: 0-49
}
```
## Configuration
### Default Thresholds
```typescript
const defaultThresholds = {
failedLoginsPerMinute: 5,
failedLoginsPerHour: 20,
rateLimitViolationsPerMinute: 10,
cspViolationsPerMinute: 15,
adminActionsPerHour: 25,
massDataAccessThreshold: 100,
suspiciousIPThreshold: 10,
};
```
### Alerting Configuration
```typescript
const alertingConfig = {
enabled: true,
channels: ["EMAIL", "WEBHOOK", "SLACK", "DISCORD", "PAGERDUTY"],
suppressDuplicateMinutes: 10,
escalationTimeoutMinutes: 60,
};
```
### Data Retention
```typescript
const retentionConfig = {
alertRetentionDays: 90,
metricsRetentionDays: 365,
};
```
## API Usage
### Get Security Metrics
```javascript
const response = await fetch(
"/api/admin/security-monitoring?startDate=2024-01-01T00:00:00Z&endDate=2024-01-02T00:00:00Z"
);
const data = await response.json();
console.log(data.metrics.securityScore); // 0-100
console.log(data.metrics.threatLevel); // LOW, MODERATE, HIGH, CRITICAL
console.log(data.alerts); // Active alerts array
```
### Acknowledge Alert
```javascript
await fetch("/api/admin/security-monitoring/alerts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
alertId: "alert-123",
action: "acknowledge",
}),
});
```
### Export Security Data
```javascript
// Export alerts as CSV
const response = await fetch(
"/api/admin/security-monitoring/export?format=csv&type=alerts&startDate=2024-01-01T00:00:00Z&endDate=2024-01-02T00:00:00Z"
);
const csvData = await response.text();
// Export metrics as JSON
const response = await fetch(
"/api/admin/security-monitoring/export?format=json&type=metrics&startDate=2024-01-01T00:00:00Z&endDate=2024-01-02T00:00:00Z"
);
const jsonData = await response.json();
```
### Perform Threat Analysis
```javascript
const analysis = await fetch("/api/admin/security-monitoring/threat-analysis", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ipAddress: "192.168.1.***",
timeRange: {
start: "2024-01-01T00:00:00Z",
end: "2024-01-02T00:00:00Z",
},
}),
});
const data = await analysis.json();
console.log(data.ipThreatAnalysis.threatLevel);
console.log(data.ipThreatAnalysis.riskFactors);
console.log(data.ipThreatAnalysis.recommendations);
```
## Integration with Existing Systems
### Enhanced Security Logging
Replace existing `securityAuditLogger.log()` calls with `enhancedSecurityLog()`:
```typescript
// Before
await securityAuditLogger.logAuthentication(
"login_attempt",
AuditOutcome.FAILURE,
context,
"Invalid password"
);
// After
await enhancedSecurityLog(
SecurityEventType.AUTHENTICATION,
"login_attempt",
AuditOutcome.FAILURE,
context,
AuditSeverity.HIGH,
"Invalid password",
{
attemptType: "invalid_password",
endpoint: "/api/auth/signin",
}
);
```
### Rate Limiting Integration
The system automatically integrates with existing rate limiting middleware:
```typescript
// middleware/authRateLimit.ts
await enhancedSecurityLog(
SecurityEventType.RATE_LIMITING,
"auth_rate_limit_exceeded",
AuditOutcome.RATE_LIMITED,
context,
AuditSeverity.HIGH,
"Authentication rate limit exceeded"
);
```
## Dashboard Features
### Security Overview
- Real-time security score (0-100)
- Current threat level indicator
- Active alerts count
- Security events summary
### Alert Management
- View active and historical alerts
- Filter by severity and type
- Acknowledge alerts with tracking
- Detailed alert context and metadata
### Threat Analysis
- Geographic distribution of events
- Top threat types and patterns
- User risk scoring
- IP threat level analysis
### Configuration Management
- Adjust detection thresholds
- Configure alerting channels
- Set data retention policies
- Export capabilities
## Performance Considerations
### Memory Management
- Event buffer limited to 1 hour of data
- Automatic cleanup of old alerts (configurable)
- Efficient in-memory storage for real-time analysis
### Database Impact
- Leverages existing audit log indexes
- Optimized queries for time-range filtering
- Background processing to avoid blocking operations
### Scalability
- Stateless architecture (except for buffering)
- Horizontal scaling support
- Configurable processing intervals
## Security Considerations
### Access Control
- Platform admin authentication required
- Role-based access to security endpoints
- Audit logging of all monitoring activities
### Data Privacy
- Sensitive data redaction in logs
- IP address anonymization options
- Configurable data retention periods
### Alert Suppression
- Duplicate alert suppression (configurable window)
- Rate limiting on alert generation
- Escalation policies for critical threats
## Monitoring and Maintenance
### Health Checks
- Monitor service availability
- Check alert generation pipeline
- Verify data export functionality
### Regular Tasks
- Review and adjust thresholds quarterly
- Analyze false positive rates
- Update threat detection patterns
- Clean up old alert data
### Performance Metrics
- Alert response time
- False positive/negative rates
- System resource usage
- User engagement with alerts
## Future Enhancements
### Planned Features
1. **Machine Learning Integration**
- Behavioral pattern recognition
- Adaptive threshold adjustment
- Predictive threat modeling
2. **Advanced Analytics**
- Threat intelligence integration
- Cross-correlation analysis
- Risk trend analysis
3. **Integration Enhancements**
- SIEM system connectors
- Webhook customization
- Mobile app notifications
4. **Automated Response**
- IP blocking automation
- Account suspension workflows
- Incident response orchestration
## Troubleshooting
### Common Issues
**High False Positive Rate**
- Review and adjust detection thresholds
- Analyze user behavior patterns
- Consider geographical variations
**Missing Alerts**
- Check service configuration
- Verify audit log integration
- Review threshold settings
**Performance Issues**
- Monitor memory usage
- Adjust cleanup intervals
- Optimize database queries
**Export Failures**
- Check file permissions
- Verify date range validity
- Monitor server resources
### Debugging
Enable debug logging:
```typescript
securityMonitoring.updateConfig({
alerting: {
enabled: true,
debugMode: true,
},
});
```
Check alert generation:
```typescript
const alerts = securityMonitoring.getActiveAlerts();
console.log("Active alerts:", alerts.length);
```
## Testing
### Unit Tests
- Alert generation logic
- Anomaly detection algorithms
- Configuration management
- Data export functionality
### Integration Tests
- API endpoint security
- Database integration
- Real-time event processing
- Alert acknowledgment flow
### Load Testing
- High-volume event processing
- Concurrent alert generation
- Database performance under load
- Memory usage patterns
Run tests:
```bash
pnpm test tests/unit/security-monitoring.test.ts
pnpm test tests/integration/security-monitoring-api.test.ts
```

View File

@ -1,413 +0,0 @@
# Enhanced Content Security Policy (CSP) Implementation
> **Task 5 Completed**: Refined and strengthened Content Security Policy for maximum XSS protection while maintaining functionality
This document outlines the comprehensive Content Security Policy implementation for maximum XSS protection while maintaining application functionality.
## Overview
The enhanced CSP implementation provides:
- **Nonce-based script execution** for maximum security in production
- **Strict mode policies** with configurable external domain allowlists
- **Environment-specific configurations** for development vs production
- **CSP violation reporting and monitoring** system with real-time analysis
- **Advanced bypass detection and alerting** capabilities with risk assessment
- **Comprehensive testing framework** with automated validation
- **Performance metrics and policy recommendations**
- **Framework compatibility** with Next.js, TailwindCSS, and Leaflet maps
## Architecture
### Core Components
1. **CSP Utility Library** (`lib/csp.ts`)
- Nonce generation with cryptographic security
- Dynamic CSP building based on environment
- Violation parsing and bypass detection
- Policy validation and testing
2. **Middleware Implementation** (`middleware.ts`)
- Automatic nonce generation per request
- Environment-aware policy application
- Enhanced security headers
- Route-based CSP filtering
3. **Violation Reporting** (`app/api/csp-report/route.ts`)
- Real-time violation monitoring with intelligent analysis
- Rate-limited endpoint protection (10 reports/minute per IP)
- Advanced bypass attempt detection with risk assessment
- Automated alerting for critical violations with recommendations
4. **Monitoring Service** (`lib/csp-monitoring.ts`)
- Violation tracking and metrics collection
- Policy recommendation engine based on violation patterns
- Export capabilities for external analysis (JSON/CSV)
- Automatic cleanup of old violation data
5. **Metrics API** (`app/api/csp-metrics/route.ts`)
- Real-time CSP violation metrics (1h, 6h, 24h, 7d, 30d ranges)
- Top violated directives and blocked URIs analysis
- Violation trend tracking and visualization data
- Policy optimization recommendations
6. **Testing Framework**
- Comprehensive unit and integration tests
- Enhanced CSP validation tools with security scoring
- Automated compliance verification
- Real-world scenario testing for application compatibility
## CSP Policies
### Production Environment (Standard Mode)
```javascript
// Nonce-based CSP with broad HTTPS allowlist
const productionCSP = {
"default-src": ["'self'"],
"script-src": ["'self'", "'nonce-{generated}'", "'strict-dynamic'"],
"style-src": ["'self'", "'nonce-{generated}'"],
"img-src": [
"'self'",
"data:",
"https://schema.org",
"https://livedash.notso.ai",
"https://*.basemaps.cartocdn.com",
"https://*.openstreetmap.org",
],
"font-src": ["'self'", "data:"],
"connect-src": ["'self'", "https://api.openai.com", "https://livedash.notso.ai", "https:"],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"form-action": ["'self'"],
"frame-ancestors": ["'none'"],
"upgrade-insecure-requests": true,
"report-uri": ["/api/csp-report"],
"report-to": ["csp-endpoint"],
};
```
### Production Environment (Strict Mode)
```javascript
// Strict CSP with minimal external domain allowlist
const strictCSP = buildCSP({
isDevelopment: false,
nonce: generateNonce(),
strictMode: true,
allowedExternalDomains: ["https://api.openai.com", "https://schema.org"],
reportUri: "/api/csp-report",
});
// Results in:
// connect-src 'self' https://api.openai.com https://livedash.notso.ai https://schema.org
// (No broad "https:" allowlist)
```
### Development Environment
```javascript
// Permissive CSP for development tools
const developmentCSP = {
"default-src": ["'self'"],
"script-src": ["'self'", "'unsafe-eval'", "'unsafe-inline'"], // HMR & dev tools
"style-src": ["'self'", "'unsafe-inline'"], // Hot reload
"connect-src": ["'self'", "https:", "wss:", "ws:"], // Dev server
// ... other directives remain strict
};
```
## Security Features
### 1. Nonce-Based Script Execution
- **128-bit cryptographically secure nonces** generated per request
- **Strict-dynamic policy** prevents inline script execution
- **Automatic nonce injection** into layout components
```tsx
// Layout with nonce support
export default async function RootLayout({ children }: { children: ReactNode }) {
const nonce = await getNonce();
return (
<html>
<head>
<script
type="application/ld+json"
nonce={nonce}
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body>
<NonceProvider nonce={nonce}>{children}</NonceProvider>
</body>
</html>
);
}
```
### 2. Content Source Restrictions
#### Script Sources
- **Production**: Only `'self'` and nonce-approved scripts
- **Development**: Additional `'unsafe-eval'` for dev tools
- **Blocked**: All external CDNs, inline scripts without nonce
#### Style Sources
- **Production**: Nonce-based inline styles preferred
- **Fallback**: `'unsafe-inline'` for TailwindCSS compatibility
- **External**: Only self-hosted stylesheets
#### Image Sources
- **Allowed**: Self, data URIs, schema.org, application domain
- **Blocked**: All other external domains
#### Connection Sources
- **Production**: Self, OpenAI API, application domain
- **Development**: Additional WebSocket for HMR
- **Blocked**: All other external connections
### 3. XSS Protection Mechanisms
#### Inline Script Prevention
<!-- prettier-ignore -->
```javascript
// Blocked by CSP
<script>alert('xss')</script>
// Allowed with nonce
<script nonce="abc123">legitCode()</script>
```
#### Object Injection Prevention
<!-- prettier-ignore -->
```javascript
// Completely blocked
object-src 'none'
```
#### Base Tag Injection Prevention
<!-- prettier-ignore -->
```javascript
// Restricted to same origin
base-uri 'self'
```
#### Clickjacking Protection
<!-- prettier-ignore -->
```javascript
// No framing allowed
frame-ancestors 'none'
```
### 4. Bypass Detection
The system actively monitors for common CSP bypass attempts:
```javascript
const bypassPatterns = [
/javascript:/i, // Protocol injection
/data:text\/html/i, // Data URI injection
/eval\(/i, // Code evaluation
/Function\(/i, // Constructor injection
/setTimeout.*string/i, // Timer string execution
];
```
## Violation Reporting
### Report Format
CSP violations are automatically reported to `/api/csp-report`:
```json
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://evil.com/script.js",
"source-file": "https://example.com/page",
"line-number": 42
}
}
```
### Violation Processing
1. **Rate Limiting**: 10 reports per minute per IP
2. **Parsing**: Extract violation details and context
3. **Risk Assessment**: Classify as low/medium/high risk
4. **Bypass Detection**: Check for known attack patterns
5. **Alerting**: Immediate notifications for critical violations
### Monitoring Dashboard
Violations are logged with:
- Timestamp and source IP
- User agent and referer
- Violation type and blocked content
- Risk level and bypass indicators
- Response actions taken
## Testing and Validation
### Automated Testing
```bash
# Run CSP-specific tests
pnpm test:csp
# Validate CSP implementation
pnpm test:csp:validate
# Full CSP test suite
pnpm test:csp:full
```
### Manual Testing
1. **Nonce Validation**: Verify unique nonces per request
2. **Policy Compliance**: Check all required directives
3. **Bypass Resistance**: Test common XSS techniques
4. **Framework Compatibility**: Ensure Next.js/TailwindCSS work
5. **Performance Impact**: Measure overhead
### Security Scoring
The validation framework provides a security score:
- **90-100%**: Excellent implementation
- **80-89%**: Good with minor improvements needed
- **70-79%**: Needs attention
- **<70%**: Serious security issues
## Deployment Considerations
### Environment Variables
```bash
# CSP is automatically environment-aware
NODE_ENV=production # Enables strict CSP
NODE_ENV=development # Enables permissive CSP
```
### Performance Impact
- **Nonce generation**: ~0.1ms per request
- **Header processing**: ~0.05ms per request
- **Total overhead**: <1ms per request
### Browser Compatibility
- **Modern browsers**: Full CSP Level 3 support
- **Legacy browsers**: Graceful degradation with X-XSS-Protection
- **Reporting**: Supported in all major browsers
## Maintenance
### Regular Reviews
1. **Monthly**: Review violation reports and patterns
2. **Quarterly**: Update content source restrictions
3. **Per release**: Validate CSP with new features
4. **Annually**: Security audit and penetration testing
### Updates and Modifications
When adding new content sources:
1. Update `buildCSP()` function in `lib/csp.ts`
2. Add tests for new directives
3. Validate security impact
4. Update documentation
### Incident Response
For CSP violations:
1. **High-risk violations**: Immediate investigation
2. **Bypass attempts**: Security team notification
3. **Mass violations**: Check for policy issues
4. **False positives**: Adjust policies as needed
## Best Practices
### Development
- Always test CSP changes in development first
- Use nonce provider for new inline scripts
- Validate external resources before adding
- Monitor console for CSP violations
### Production
- Never disable CSP in production
- Monitor violation rates and patterns
- Keep nonce generation entropy high
- Regular security audits
### Code Review
- Check all inline scripts have nonce
- Verify external resources are approved
- Ensure CSP tests pass
- Document any policy changes
## Troubleshooting
### Common Issues
1. **Inline styles blocked**: Use nonce or move to external CSS
2. **Third-party scripts blocked**: Add to approved sources
3. **Dev tools not working**: Ensure development CSP allows unsafe-eval
4. **Images not loading**: Check image source restrictions
### Debug Tools
```bash
# Test CSP generation
pnpm test:csp
# Validate current implementation
pnpm test:csp:validate
# Check specific violations
curl -X POST /api/csp-report -d '{"csp-report": {...}}'
```
### Emergency Procedures
If CSP breaks production:
1. Check violation reports for patterns
2. Identify blocking directive
3. Test fix in staging environment
4. Deploy emergency policy update
5. Monitor for resolved issues
## Compliance
This CSP implementation addresses:
- **OWASP Top 10**: XSS prevention
- **CSP Level 3**: Modern security standards
- **GDPR**: Privacy-preserving monitoring
- **SOC 2**: Security controls documentation
The enhanced CSP provides defense-in-depth against XSS attacks while maintaining application functionality and performance.

View File

@ -126,7 +126,6 @@ node scripts/manual-triggers.js status
### Database States
<!-- prettier-ignore -->
```javascript
// After CSV fetch
{

View File

@ -1,241 +0,0 @@
# TypeScript Compilation Fixes and Build Troubleshooting
This document outlines the fixes applied to resolve TypeScript compilation errors and achieve a successful production build.
## Issues Resolved
### 1. Missing Type Imports
**Problem:** `lib/api/index.ts` was missing required type imports
**Error:** `Cannot find name 'APIHandler'`, `Cannot find name 'Permission'`
**Fix:** Added proper imports at the top of the file
```typescript
import type { APIContext, APIHandler, APIHandlerOptions } from "./handler";
import { createAPIHandler } from "./handler";
import { Permission, createPermissionChecker } from "./authorization";
```
### 2. Zod API Breaking Change
**Problem:** Zod error property name changed from `errors` to `issues`
**Error:** `Property 'errors' does not exist on type 'ZodError'`
**Fix:** Updated all references to use `error.issues` instead of `error.errors`
```typescript
// Before
error.errors.map((e) => `${e.path.join(".")}: ${e.message}`);
// After
error.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
```
### 3. Missing LRU Cache Dependency
**Problem:** `lru-cache` package was missing from dependencies
**Error:** `Cannot find module 'lru-cache'`
**Fix:** Installed the missing dependency
```bash
pnpm add lru-cache
```
### 4. LRU Cache Generic Type Constraints
**Problem:** TypeScript generic constraints not satisfied
**Error:** `Type 'K' does not satisfy the constraint '{}'`
**Fix:** Added proper generic type constraints
<!-- prettier-ignore -->
```typescript
// Before
<K = string, V = any>
// After
<K extends {} = string, V = any>
```
### 5. Map Iteration ES5 Compatibility
**Problem:** Map iteration requires downlevel iteration flag
**Error:** `can only be iterated through when using the '--downlevelIteration' flag`
**Fix:** Used `Array.from()` pattern for compatibility
<!-- prettier-ignore -->
```typescript
// Before
for (const [key, value] of map) { ... }
// After
for (const [key, value] of Array.from(map.entries())) { ... }
```
### 6. Redis Configuration Issues
**Problem:** Invalid Redis socket options
**Error:** Redis connection failed with unsupported options
**Fix:** Simplified Redis configuration to only include supported options
```typescript
this.client = createClient({
url: env.REDIS_URL,
socket: {
connectTimeout: 5000,
},
});
```
### 7. Prisma Relationship Naming Mismatches
**Problem:** Code referenced non-existent Prisma relationships
**Error:** `securityAuditLogs` and `sessionImport` don't exist
**Fix:** Used correct relationship names
```typescript
// Before
user.securityAuditLogs;
session.sessionImport;
// After
user.auditLogs;
session.import;
```
### 8. Missing Schema Fields
**Problem:** Code referenced fields that don't exist in the database schema
**Error:** `Property 'userId' does not exist on type`
**Fix:** Applied type casting where schema fields were missing
```typescript
userId: (session as any).userId || null;
```
### 9. Deprecated Package Dependencies
**Problem:** `critters` package is deprecated and caused build failures
**Error:** `Cannot find module 'critters'`
**Fix:** Disabled CSS optimization feature that required critters
<!-- prettier-ignore -->
```javascript
experimental: {
optimizeCss: false, // Disabled due to critters dependency
}
```
### 10. ESLint vs Biome Conflict
**Problem:** ESLint warnings treated as build errors
**Error:** Build failed due to linting warnings
**Fix:** Disabled ESLint during build since Biome is used for linting
<!-- prettier-ignore -->
```javascript
eslint: {
ignoreDuringBuilds: true,
},
```
## Schema Enhancements
### Enhanced User Management
Added comprehensive user management fields to the User model:
```prisma
model User {
// ... existing fields
// User management fields
lastLoginAt DateTime? @db.Timestamptz(6)
isActive Boolean @default(true)
emailVerified Boolean @default(false)
emailVerificationToken String? @db.VarChar(255)
emailVerificationExpiry DateTime? @db.Timestamptz(6)
failedLoginAttempts Int @default(0)
lockedAt DateTime? @db.Timestamptz(6)
preferences Json? @db.Json
timezone String? @db.VarChar(50)
preferredLanguage String? @db.VarChar(10)
@@index([lastLoginAt])
@@index([isActive])
@@index([emailVerified])
}
```
### Updated Repository Methods
Enhanced UserRepository with new methods:
- `updateLastLogin()` - Tracks user login times
- `incrementFailedLoginAttempts()` - Security feature for account locking
- `verifyEmail()` - Email verification management
- `deactivateUser()` - Account management
- `unlockUser()` - Security administration
- `updatePreferences()` - User settings management
- `findInactiveUsers()` - Now uses `lastLoginAt` instead of `createdAt`
## Prevention Measures
### 1. Regular Dependency Updates
- Monitor for breaking changes in dependencies like Zod
- Use `pnpm outdated` to check for deprecated packages
- Test builds after dependency updates
### 2. TypeScript Strict Checking
- Enable strict TypeScript checking to catch type errors early
- Use proper type imports and exports
- Avoid `any` types where possible
### 3. Build Pipeline Validation
- Run `pnpm build` before committing
- Include type checking in CI/CD pipeline
- Separate linting from build process
### 4. Schema Management
- Regenerate Prisma client after schema changes: `pnpm prisma:generate`
- Validate schema changes with database migrations
- Use proper TypeScript types for database operations
### 5. Development Workflow
```bash
# Recommended development workflow
pnpm prisma:generate # After schema changes
pnpm build # Verify compilation
pnpm lint # Check code quality (using Biome)
```
## Build Success Metrics
**TypeScript Compilation:** All 47 pages compile successfully
**No Type Errors:** Zero TypeScript compilation errors
**Production Ready:** Optimized bundle generated
**No Deprecated Dependencies:** All packages up to date
**Enhanced User Management:** Comprehensive user fields added
## Commands for Troubleshooting
```bash
# Check for TypeScript errors
pnpm build
# Check for outdated/deprecated packages
pnpm outdated
# Regenerate Prisma client
pnpm prisma:generate
# Check for linting issues
pnpm lint
# Install missing dependencies
pnpm install
```
---
_Last updated: 2025-07-12_
_Build Status: ✅ Success (47/47 pages generated)_

View File

@ -1,557 +0,0 @@
/**
* E2E tests for CSV upload and session processing workflow
*
* Tests the complete data processing pipeline:
* 1. CSV import configuration
* 2. Data import and validation
* 3. Session processing and AI analysis
* 4. Dashboard visualization
* 5. Data filtering and search
*/
import { test, expect, type Page } from "@playwright/test";
// Test data
const testAdmin = {
email: "admin@csvtest.com",
password: "AdminTestPassword123!",
};
const mockCsvData = `sessionId,userId,language,country,ipAddress,sentiment,messagesSent,startTime,endTime,escalated,forwardedHr,summary
session1,user1,en,US,192.168.1.1,positive,5,2024-01-15T10:00:00Z,2024-01-15T10:30:00Z,false,false,User requested vacation time
session2,user2,nl,NL,192.168.1.2,neutral,3,2024-01-15T11:00:00Z,2024-01-15T11:20:00Z,true,false,User had login issues
session3,user3,de,DE,192.168.1.3,negative,8,2024-01-15T12:00:00Z,2024-01-15T12:45:00Z,false,true,User complained about salary`;
// Helper functions
async function loginAsAdmin(page: Page) {
await page.goto("http://localhost:3000/login");
await page.fill('[data-testid="email"]', testAdmin.email);
await page.fill('[data-testid="password"]', testAdmin.password);
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL(/\/dashboard/);
}
async function waitForDataProcessing(page: Page, timeout = 30000) {
// Wait for processing indicators to disappear
await page.waitForSelector('[data-testid="processing-indicator"]', {
state: "hidden",
timeout,
});
}
async function setupMockCsvEndpoint(page: Page) {
// Mock the CSV endpoint to return test data
await page.route("**/test-csv-data", (route) => {
route.fulfill({
status: 200,
contentType: "text/csv",
body: mockCsvData,
});
});
}
test.describe("CSV Processing Workflow", () => {
test.beforeEach(async ({ page }) => {
// Setup mock CSV endpoint
await setupMockCsvEndpoint(page);
// Login as admin
await loginAsAdmin(page);
});
test.describe("CSV Import Configuration", () => {
test("should configure CSV import settings", async ({ page }) => {
// Navigate to company settings
await page.click('[data-testid="nav-company"]');
await expect(page).toHaveURL(/\/dashboard\/company/);
// Update CSV configuration
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/test-csv-data"
);
await page.fill('[data-testid="csv-username"]', "testuser");
await page.fill('[data-testid="csv-password"]', "testpass");
// Save settings
await page.click('[data-testid="save-settings"]');
// Should show success message
await expect(
page.locator('[data-testid="success-message"]')
).toContainText("Settings saved successfully");
});
test("should validate CSV URL format", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/company");
// Enter invalid URL
await page.fill('[data-testid="csv-url"]', "invalid-url");
await page.click('[data-testid="save-settings"]');
// Should show validation error
await expect(page.locator('[data-testid="csv-url-error"]')).toContainText(
"Invalid URL format"
);
});
});
test.describe("Manual CSV Import", () => {
test.beforeEach(async ({ page }) => {
// Configure CSV settings first
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/test-csv-data"
);
await page.click('[data-testid="save-settings"]');
await expect(
page.locator('[data-testid="success-message"]')
).toBeVisible();
});
test("should trigger manual CSV import", async ({ page }) => {
// Navigate to overview
await page.goto("http://localhost:3000/dashboard/overview");
// Trigger manual refresh
await page.click('[data-testid="refresh-data-button"]');
// Should show processing indicator
await expect(
page.locator('[data-testid="processing-indicator"]')
).toBeVisible();
// Wait for processing to complete
await waitForDataProcessing(page);
// Should show success message
await expect(
page.locator('[data-testid="import-success"]')
).toContainText("Data imported successfully");
});
test("should display import progress", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Start import
await page.click('[data-testid="refresh-data-button"]');
// Check progress indicators
await expect(
page.locator('[data-testid="import-progress"]')
).toBeVisible();
// Progress should show stages
await expect(
page.locator('[data-testid="stage-csv-import"]')
).toContainText("CSV Import");
await expect(
page.locator('[data-testid="stage-processing"]')
).toContainText("Processing");
await expect(
page.locator('[data-testid="stage-ai-analysis"]')
).toContainText("AI Analysis");
});
test("should handle import errors gracefully", async ({ page }) => {
// Configure invalid CSV URL
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/nonexistent-csv"
);
await page.click('[data-testid="save-settings"]');
// Try to import
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
// Should show error message
await expect(page.locator('[data-testid="import-error"]')).toContainText(
"Failed to fetch CSV data"
);
});
});
test.describe("Data Visualization", () => {
test.beforeEach(async ({ page }) => {
// Import test data first
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/test-csv-data"
);
await page.click('[data-testid="save-settings"]');
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
await waitForDataProcessing(page);
});
test("should display session metrics correctly", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Check metric cards show correct data
await expect(
page.locator('[data-testid="total-sessions"]')
).toContainText("3");
// Check sentiment distribution
const sentimentChart = page.locator('[data-testid="sentiment-chart"]');
await expect(sentimentChart).toBeVisible();
// Verify sentiment data
await expect(
page.locator('[data-testid="positive-sentiment"]')
).toContainText("1");
await expect(
page.locator('[data-testid="neutral-sentiment"]')
).toContainText("1");
await expect(
page.locator('[data-testid="negative-sentiment"]')
).toContainText("1");
});
test("should display geographic distribution", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Check geographic map
const geoMap = page.locator('[data-testid="geographic-map"]');
await expect(geoMap).toBeVisible();
// Check country data
await expect(page.locator('[data-testid="country-us"]')).toContainText(
"US: 1"
);
await expect(page.locator('[data-testid="country-nl"]')).toContainText(
"NL: 1"
);
await expect(page.locator('[data-testid="country-de"]')).toContainText(
"DE: 1"
);
});
test("should display escalation metrics", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Check escalation rate
await expect(
page.locator('[data-testid="escalation-rate"]')
).toContainText("33%");
// Check HR forwarding rate
await expect(
page.locator('[data-testid="hr-forwarding-rate"]')
).toContainText("33%");
});
});
test.describe("Session Management", () => {
test.beforeEach(async ({ page }) => {
// Import test data
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/test-csv-data"
);
await page.click('[data-testid="save-settings"]');
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
await waitForDataProcessing(page);
});
test("should display sessions list", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/sessions");
// Should show all sessions
await expect(page.locator('[data-testid="session-list"]')).toBeVisible();
await expect(page.locator('[data-testid="session-item"]')).toHaveCount(3);
// Check session details
const firstSession = page.locator('[data-testid="session-item"]').first();
await expect(firstSession).toContainText("session1");
await expect(firstSession).toContainText("positive");
await expect(firstSession).toContainText("US");
});
test("should filter sessions by sentiment", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/sessions");
// Filter by positive sentiment
await page.selectOption('[data-testid="sentiment-filter"]', "POSITIVE");
// Should show only positive sessions
await expect(page.locator('[data-testid="session-item"]')).toHaveCount(1);
await expect(page.locator('[data-testid="session-item"]')).toContainText(
"session1"
);
});
test("should filter sessions by country", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/sessions");
// Filter by Germany
await page.selectOption('[data-testid="country-filter"]', "DE");
// Should show only German sessions
await expect(page.locator('[data-testid="session-item"]')).toHaveCount(1);
await expect(page.locator('[data-testid="session-item"]')).toContainText(
"session3"
);
});
test("should search sessions by content", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/sessions");
// Search for "vacation"
await page.fill('[data-testid="search-input"]', "vacation");
// Should show matching sessions
await expect(page.locator('[data-testid="session-item"]')).toHaveCount(1);
await expect(page.locator('[data-testid="session-item"]')).toContainText(
"vacation time"
);
});
test("should paginate sessions", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/sessions");
// Set small page size
await page.selectOption('[data-testid="page-size"]', "2");
// Should show pagination
await expect(page.locator('[data-testid="pagination"]')).toBeVisible();
await expect(page.locator('[data-testid="session-item"]')).toHaveCount(2);
// Go to next page
await page.click('[data-testid="next-page"]');
await expect(page.locator('[data-testid="session-item"]')).toHaveCount(1);
});
});
test.describe("Session Details", () => {
test.beforeEach(async ({ page }) => {
// Import test data
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/test-csv-data"
);
await page.click('[data-testid="save-settings"]');
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
await waitForDataProcessing(page);
});
test("should view individual session details", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/sessions");
// Click on first session
await page.click('[data-testid="session-item"]');
// Should navigate to session detail page
await expect(page).toHaveURL(/\/dashboard\/sessions\/[^/]+/);
// Check session details
await expect(page.locator('[data-testid="session-id"]')).toContainText(
"session1"
);
await expect(
page.locator('[data-testid="sentiment-badge"]')
).toContainText("positive");
await expect(page.locator('[data-testid="country-badge"]')).toContainText(
"US"
);
await expect(
page.locator('[data-testid="session-summary"]')
).toContainText("vacation time");
});
test("should display session timeline", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/sessions");
await page.click('[data-testid="session-item"]');
// Check timeline
const timeline = page.locator('[data-testid="session-timeline"]');
await expect(timeline).toBeVisible();
// Should show start and end times
await expect(page.locator('[data-testid="start-time"]')).toContainText(
"10:00"
);
await expect(page.locator('[data-testid="end-time"]')).toContainText(
"10:30"
);
await expect(page.locator('[data-testid="duration"]')).toContainText(
"30 minutes"
);
});
test("should display extracted questions", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/sessions");
await page.click('[data-testid="session-item"]');
// Check questions section
const questionsSection = page.locator(
'[data-testid="extracted-questions"]'
);
await expect(questionsSection).toBeVisible();
// Should show AI-extracted questions (if any)
const questionsList = page.locator('[data-testid="questions-list"]');
if (await questionsList.isVisible()) {
await expect(
questionsList.locator('[data-testid="question-item"]')
).toHaveCount.greaterThan(0);
}
});
});
test.describe("Real-time Updates", () => {
test("should show real-time processing status", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Configure CSV
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/test-csv-data"
);
await page.click('[data-testid="save-settings"]');
// Start import and monitor status
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
// Should show real-time status updates
await expect(
page.locator('[data-testid="status-importing"]')
).toBeVisible();
// Status should progress through stages
await page.waitForSelector('[data-testid="status-processing"]', {
timeout: 10000,
});
await page.waitForSelector('[data-testid="status-analyzing"]', {
timeout: 10000,
});
await page.waitForSelector('[data-testid="status-complete"]', {
timeout: 30000,
});
});
test("should update metrics in real-time", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Get initial metrics
const initialSessions = await page
.locator('[data-testid="total-sessions"]')
.textContent();
// Import data
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/test-csv-data"
);
await page.click('[data-testid="save-settings"]');
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
await waitForDataProcessing(page);
// Metrics should be updated
const updatedSessions = await page
.locator('[data-testid="total-sessions"]')
.textContent();
expect(updatedSessions).not.toBe(initialSessions);
});
});
test.describe("Error Handling", () => {
test("should handle CSV parsing errors", async ({ page }) => {
// Mock invalid CSV data
await page.route("**/invalid-csv", (route) => {
route.fulfill({
status: 200,
contentType: "text/csv",
body: "invalid,csv,format\nwithout,proper,headers",
});
});
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/invalid-csv"
);
await page.click('[data-testid="save-settings"]');
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
// Should show parsing error
await expect(page.locator('[data-testid="parsing-error"]')).toContainText(
"Invalid CSV format"
);
});
test("should handle AI processing failures", async ({ page }) => {
// Mock AI service failure
await page.route("**/api/openai/**", (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: "AI service unavailable" }),
});
});
await page.goto("http://localhost:3000/dashboard/company");
await page.fill(
'[data-testid="csv-url"]',
"http://localhost:3000/api/test-csv-data"
);
await page.click('[data-testid="save-settings"]');
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
// Should show AI processing error
await expect(page.locator('[data-testid="ai-error"]')).toContainText(
"AI analysis failed"
);
});
test("should retry failed operations", async ({ page }) => {
let attemptCount = 0;
// Mock failing then succeeding API
await page.route("**/api/process-batch", (route) => {
attemptCount++;
if (attemptCount === 1) {
route.fulfill({ status: 500, body: "Server error" });
} else {
route.fulfill({
status: 200,
body: JSON.stringify({ success: true }),
});
}
});
await page.goto("http://localhost:3000/dashboard/overview");
await page.click('[data-testid="refresh-data-button"]');
// Should show retry attempt
await expect(
page.locator('[data-testid="retry-indicator"]')
).toBeVisible();
// Should eventually succeed
await waitForDataProcessing(page);
await expect(
page.locator('[data-testid="import-success"]')
).toBeVisible();
});
});
});

View File

@ -1,641 +0,0 @@
/**
* E2E tests for dashboard navigation and data visualization
*
* Tests the dashboard user experience:
* 1. Navigation between dashboard sections
* 2. Data visualization components
* 3. Interactive filtering and search
* 4. Responsive design
* 5. Accessibility features
*/
import { test, expect, type Page } from "@playwright/test";
// Test data
const testUser = {
email: "dashboard@test.com",
password: "DashboardTest123!",
};
// Helper functions
async function loginUser(page: Page) {
await page.goto("http://localhost:3000/login");
await page.fill('[data-testid="email"]', testUser.email);
await page.fill('[data-testid="password"]', testUser.password);
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL(/\/dashboard/);
}
async function waitForChartLoad(page: Page, chartSelector: string) {
await page.waitForSelector(chartSelector);
await page.waitForFunction((selector) => {
const chart = document.querySelector(selector);
return chart && chart.children.length > 0;
}, chartSelector);
}
test.describe("Dashboard Navigation", () => {
test.beforeEach(async ({ page }) => {
await loginUser(page);
});
test.describe("Navigation Menu", () => {
test("should display main navigation menu", async ({ page }) => {
// Check navigation sidebar
const nav = page.locator('[data-testid="main-navigation"]');
await expect(nav).toBeVisible();
// Check navigation items
await expect(page.locator('[data-testid="nav-overview"]')).toBeVisible();
await expect(page.locator('[data-testid="nav-sessions"]')).toBeVisible();
await expect(page.locator('[data-testid="nav-users"]')).toBeVisible();
await expect(page.locator('[data-testid="nav-company"]')).toBeVisible();
});
test("should highlight active navigation item", async ({ page }) => {
// Overview should be active by default
await expect(page.locator('[data-testid="nav-overview"]')).toHaveClass(
/active/
);
// Navigate to sessions
await page.click('[data-testid="nav-sessions"]');
await expect(page.locator('[data-testid="nav-sessions"]')).toHaveClass(
/active/
);
await expect(
page.locator('[data-testid="nav-overview"]')
).not.toHaveClass(/active/);
});
test("should navigate between sections correctly", async ({ page }) => {
// Navigate to Sessions
await page.click('[data-testid="nav-sessions"]');
await expect(page).toHaveURL(/\/dashboard\/sessions/);
await expect(page.locator("h1")).toContainText("Sessions");
// Navigate to Users
await page.click('[data-testid="nav-users"]');
await expect(page).toHaveURL(/\/dashboard\/users/);
await expect(page.locator("h1")).toContainText("Users");
// Navigate to Company
await page.click('[data-testid="nav-company"]');
await expect(page).toHaveURL(/\/dashboard\/company/);
await expect(page.locator("h1")).toContainText("Company Settings");
// Navigate back to Overview
await page.click('[data-testid="nav-overview"]');
await expect(page).toHaveURL(/\/dashboard\/overview/);
await expect(page.locator("h1")).toContainText("Dashboard Overview");
});
test("should support breadcrumb navigation", async ({ page }) => {
// Navigate to sessions and then a specific session
await page.click('[data-testid="nav-sessions"]');
// Mock a session item click (assuming sessions exist)
const sessionItems = page.locator('[data-testid="session-item"]');
const sessionCount = await sessionItems.count();
if (sessionCount > 0) {
await sessionItems.first().click();
// Check breadcrumbs
await expect(page.locator('[data-testid="breadcrumb"]')).toBeVisible();
await expect(
page.locator('[data-testid="breadcrumb-home"]')
).toContainText("Dashboard");
await expect(
page.locator('[data-testid="breadcrumb-sessions"]')
).toContainText("Sessions");
await expect(
page.locator('[data-testid="breadcrumb-current"]')
).toContainText("Session Details");
}
});
});
test.describe("Header Navigation", () => {
test("should display user menu", async ({ page }) => {
// Check user menu trigger
const userMenu = page.locator('[data-testid="user-menu"]');
await expect(userMenu).toBeVisible();
// Open user menu
await userMenu.click();
// Check menu items
await expect(page.locator('[data-testid="user-profile"]')).toBeVisible();
await expect(page.locator('[data-testid="user-settings"]')).toBeVisible();
await expect(page.locator('[data-testid="logout-button"]')).toBeVisible();
});
test("should display notifications", async ({ page }) => {
const notifications = page.locator('[data-testid="notifications"]');
if (await notifications.isVisible()) {
await notifications.click();
await expect(
page.locator('[data-testid="notifications-dropdown"]')
).toBeVisible();
}
});
test("should display search functionality", async ({ page }) => {
const searchInput = page.locator('[data-testid="global-search"]');
if (await searchInput.isVisible()) {
await searchInput.fill("test search");
await expect(
page.locator('[data-testid="search-results"]')
).toBeVisible();
}
});
});
test.describe("Page Titles and Metadata", () => {
test("should update page title for each section", async ({ page }) => {
// Overview page
await page.goto("http://localhost:3000/dashboard/overview");
await expect(page).toHaveTitle(/Dashboard Overview/);
// Sessions page
await page.goto("http://localhost:3000/dashboard/sessions");
await expect(page).toHaveTitle(/Sessions/);
// Users page
await page.goto("http://localhost:3000/dashboard/users");
await expect(page).toHaveTitle(/Users/);
});
});
});
test.describe("Data Visualization", () => {
test.beforeEach(async ({ page }) => {
await loginUser(page);
await page.goto("http://localhost:3000/dashboard/overview");
});
test.describe("Overview Dashboard", () => {
test("should display key metrics cards", async ({ page }) => {
// Check metric cards
await expect(
page.locator('[data-testid="total-sessions-card"]')
).toBeVisible();
await expect(
page.locator('[data-testid="avg-sentiment-card"]')
).toBeVisible();
await expect(
page.locator('[data-testid="escalation-rate-card"]')
).toBeVisible();
await expect(
page.locator('[data-testid="avg-response-time-card"]')
).toBeVisible();
// Check that metrics have values
const totalSessions = page.locator(
'[data-testid="total-sessions-value"]'
);
await expect(totalSessions).toContainText(/\d+/); // Should contain numbers
});
test("should display sentiment distribution chart", async ({ page }) => {
const sentimentChart = page.locator('[data-testid="sentiment-chart"]');
await expect(sentimentChart).toBeVisible();
await waitForChartLoad(page, '[data-testid="sentiment-chart"]');
// Check chart has data
await expect(
page.locator('[data-testid="positive-sentiment"]')
).toBeVisible();
await expect(
page.locator('[data-testid="neutral-sentiment"]')
).toBeVisible();
await expect(
page.locator('[data-testid="negative-sentiment"]')
).toBeVisible();
});
test("should display category distribution chart", async ({ page }) => {
const categoryChart = page.locator('[data-testid="category-chart"]');
await expect(categoryChart).toBeVisible();
await waitForChartLoad(page, '[data-testid="category-chart"]');
// Should show category data
const categories = page.locator('[data-testid="category-item"]');
const count = await categories.count();
expect(count).toBeGreaterThan(0);
});
test("should display geographic distribution map", async ({ page }) => {
const geoMap = page.locator('[data-testid="geographic-map"]');
await expect(geoMap).toBeVisible();
// Wait for map to load - wait for map container or country data to be rendered
await page.waitForSelector('[data-testid="country-data"], .leaflet-container, .geo-map-loaded', {
timeout: 10000,
state: 'visible'
}).catch(() => {
// Fallback: wait for any map-related element to indicate map is loaded
return page.waitForSelector('.map, [class*="map"], [data-map]', { timeout: 5000 }).catch(() => null);
});
// Check if country data is displayed
const countryData = page.locator('[data-testid="country-data"]');
if (await countryData.isVisible()) {
expect(await countryData.count()).toBeGreaterThan(0);
}
});
test("should display top questions list", async ({ page }) => {
const topQuestions = page.locator('[data-testid="top-questions"]');
await expect(topQuestions).toBeVisible();
// Check if questions are displayed
const questionItems = page.locator('[data-testid="question-item"]');
const count = await questionItems.count();
if (count > 0) {
// Should show question text and count
const firstQuestion = questionItems.first();
await expect(
firstQuestion.locator('[data-testid="question-text"]')
).toBeVisible();
await expect(
firstQuestion.locator('[data-testid="question-count"]')
).toBeVisible();
}
});
test("should display time series chart", async ({ page }) => {
const timeChart = page.locator('[data-testid="time-series-chart"]');
if (await timeChart.isVisible()) {
await waitForChartLoad(page, '[data-testid="time-series-chart"]');
// Check chart axes
await expect(
page.locator('[data-testid="chart-x-axis"]')
).toBeVisible();
await expect(
page.locator('[data-testid="chart-y-axis"]')
).toBeVisible();
}
});
});
test.describe("Chart Interactions", () => {
test("should allow chart filtering interactions", async ({ page }) => {
const sentimentChart = page.locator('[data-testid="sentiment-chart"]');
if (await sentimentChart.isVisible()) {
// Click on positive sentiment section
const positiveSection = page.locator(
'[data-testid="positive-segment"]'
);
if (await positiveSection.isVisible()) {
await positiveSection.click();
// Should filter data or show details
await expect(
page.locator('[data-testid="chart-filter-active"]')
).toBeVisible();
}
}
});
test("should show chart tooltips on hover", async ({ page }) => {
const chart = page.locator('[data-testid="sentiment-chart"]');
if (await chart.isVisible()) {
await chart.hover();
// Check for tooltip
const tooltip = page.locator('[data-testid="chart-tooltip"]');
if (await tooltip.isVisible()) {
await expect(tooltip).toContainText(/\d+/); // Should show numeric data
}
}
});
test("should support chart zoom and pan", async ({ page }) => {
const timeChart = page.locator('[data-testid="time-series-chart"]');
if (await timeChart.isVisible()) {
// Test zoom (scroll)
await timeChart.hover();
await page.mouse.wheel(0, -100);
// Test pan (drag)
const box = await timeChart.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(
box.x + box.width / 2 + 50,
box.y + box.height / 2
);
await page.mouse.up();
}
}
});
});
test.describe("Data Filtering", () => {
test("should filter data by date range", async ({ page }) => {
// Open date picker
const dateFilter = page.locator('[data-testid="date-range-picker"]');
if (await dateFilter.isVisible()) {
await dateFilter.click();
// Select date range
await page.click('[data-testid="date-last-week"]');
// Wait for charts to update after date filter application
await page.waitForSelector('[data-testid="filter-applied"], [data-testid="charts-updated"], .loading:not(.visible)', {
timeout: 5000,
state: 'visible'
}).catch(() => {
// Fallback: wait for any indication that filtering is complete
return page.waitForFunction(() => !document.querySelector('.loading, [data-loading="true"]'), { timeout: 3000 }).catch(() => null);
});
// Check that data is filtered
await expect(
page.locator('[data-testid="filter-applied"]')
).toBeVisible();
}
});
test("should filter data by sentiment", async ({ page }) => {
const sentimentFilter = page.locator('[data-testid="sentiment-filter"]');
if (await sentimentFilter.isVisible()) {
await sentimentFilter.selectOption("POSITIVE");
// Wait for visualizations to update after sentiment filter
await page.waitForSelector('[data-testid="active-filters"], [data-testid="sentiment-applied"], .charts-container:not(.updating)', {
timeout: 5000,
state: 'visible'
}).catch(() => {
// Fallback: wait for filter processing to complete
return page.waitForFunction(() => !document.querySelector('.updating, [data-updating="true"], .filter-loading'), { timeout: 3000 }).catch(() => null);
});
// Check filter is applied
await expect(
page.locator('[data-testid="active-filters"]')
).toContainText("Sentiment: Positive");
}
});
test("should clear all filters", async ({ page }) => {
// Apply some filters first
const sentimentFilter = page.locator('[data-testid="sentiment-filter"]');
if (await sentimentFilter.isVisible()) {
await sentimentFilter.selectOption("POSITIVE");
}
// Clear filters
const clearButton = page.locator('[data-testid="clear-filters"]');
if (await clearButton.isVisible()) {
await clearButton.click();
// Should reset all data
await expect(
page.locator('[data-testid="active-filters"]')
).toHaveCount(0);
}
});
});
test.describe("Data Export", () => {
test("should export chart data as CSV", async ({ page }) => {
const exportButton = page.locator('[data-testid="export-csv"]');
if (await exportButton.isVisible()) {
// Start download
const downloadPromise = page.waitForEvent("download");
await exportButton.click();
const download = await downloadPromise;
// Verify download
expect(download.suggestedFilename()).toContain(".csv");
}
});
test("should export chart as image", async ({ page }) => {
const exportButton = page.locator('[data-testid="export-image"]');
if (await exportButton.isVisible()) {
const downloadPromise = page.waitForEvent("download");
await exportButton.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.(png|jpg|svg)$/);
}
});
});
});
test.describe("Responsive Design", () => {
test.beforeEach(async ({ page }) => {
await loginUser(page);
});
test.describe("Mobile Layout", () => {
test("should adapt navigation for mobile", async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("http://localhost:3000/dashboard/overview");
// Should show mobile menu button
const mobileMenu = page.locator('[data-testid="mobile-menu-toggle"]');
await expect(mobileMenu).toBeVisible();
// Open mobile menu
await mobileMenu.click();
await expect(
page.locator('[data-testid="mobile-navigation"]')
).toBeVisible();
// Check navigation items in mobile menu
await expect(
page.locator('[data-testid="mobile-nav-overview"]')
).toBeVisible();
await expect(
page.locator('[data-testid="mobile-nav-sessions"]')
).toBeVisible();
});
test("should stack charts vertically on mobile", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("http://localhost:3000/dashboard/overview");
// Charts should be stacked
const chartContainer = page.locator('[data-testid="charts-container"]');
await expect(chartContainer).toHaveCSS("flex-direction", "column");
});
test("should show simplified metrics on mobile", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("http://localhost:3000/dashboard/overview");
// Should show condensed metric cards
const metricCards = page.locator('[data-testid="metric-card"]');
const count = await metricCards.count();
// Should show fewer cards or smaller layout
for (let i = 0; i < count; i++) {
const card = metricCards.nth(i);
const box = await card.boundingBox();
if (box) {
expect(box.width).toBeLessThan(300); // Smaller cards on mobile
}
}
});
});
test.describe("Tablet Layout", () => {
test("should adapt layout for tablet", async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto("http://localhost:3000/dashboard/overview");
// Should show sidebar but possibly collapsed
const sidebar = page.locator('[data-testid="sidebar"]');
await expect(sidebar).toBeVisible();
// Charts should adapt to medium screen
const chartGrid = page.locator('[data-testid="chart-grid"]');
await expect(chartGrid).toHaveCSS("grid-template-columns", /repeat\(2,/);
});
});
});
test.describe("Accessibility", () => {
test.beforeEach(async ({ page }) => {
await loginUser(page);
});
test.describe("Keyboard Navigation", () => {
test("should support keyboard navigation in dashboard", async ({
page,
}) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Test tab navigation
await page.keyboard.press("Tab");
// Should focus on first interactive element
const focused = page.locator(":focus");
await expect(focused).toBeVisible();
// Navigate through elements
for (let i = 0; i < 5; i++) {
await page.keyboard.press("Tab");
const currentFocus = page.locator(":focus");
await expect(currentFocus).toBeVisible();
}
});
test("should support keyboard shortcuts", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Test keyboard shortcuts (if implemented)
await page.keyboard.press("Alt+1"); // Navigate to overview
await expect(page).toHaveURL(/\/dashboard\/overview/);
await page.keyboard.press("Alt+2"); // Navigate to sessions
await expect(page).toHaveURL(/\/dashboard\/sessions/);
});
});
test.describe("Screen Reader Support", () => {
test("should have proper ARIA labels", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Check main landmarks
await expect(page.locator("main")).toHaveAttribute("role", "main");
await expect(page.locator("nav")).toHaveAttribute("role", "navigation");
// Check chart accessibility
const sentimentChart = page.locator('[data-testid="sentiment-chart"]');
if (await sentimentChart.isVisible()) {
await expect(sentimentChart).toHaveAttribute("role", "img");
await expect(sentimentChart).toHaveAttribute("aria-label");
}
});
test("should provide alternative text for charts", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Check chart descriptions
const charts = page.locator('[role="img"]');
const count = await charts.count();
for (let i = 0; i < count; i++) {
const chart = charts.nth(i);
const ariaLabel = await chart.getAttribute("aria-label");
expect(ariaLabel).toBeTruthy();
expect(ariaLabel?.length).toBeGreaterThan(10); // Should be descriptive
}
});
test("should announce dynamic content changes", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Check for live regions
const liveRegions = page.locator("[aria-live]");
const count = await liveRegions.count();
if (count > 0) {
// Should have appropriate aria-live settings
for (let i = 0; i < count; i++) {
const region = liveRegions.nth(i);
const ariaLive = await region.getAttribute("aria-live");
expect(["polite", "assertive"]).toContain(ariaLive);
}
}
});
});
test.describe("Color and Contrast", () => {
test("should maintain accessibility in dark mode", async ({ page }) => {
await page.goto("http://localhost:3000/dashboard/overview");
// Toggle dark mode (if available)
const darkModeToggle = page.locator('[data-testid="theme-toggle"]');
if (await darkModeToggle.isVisible()) {
await darkModeToggle.click();
// Check that elements are still visible
await expect(
page.locator('[data-testid="total-sessions-card"]')
).toBeVisible();
await expect(
page.locator('[data-testid="sentiment-chart"]')
).toBeVisible();
}
});
test("should work without color", async ({ page }) => {
// Test with forced colors (simulates high contrast mode)
await page.emulateMedia({ colorScheme: "dark", forcedColors: "active" });
await page.goto("http://localhost:3000/dashboard/overview");
// Elements should still be distinguishable
await expect(
page.locator('[data-testid="total-sessions-card"]')
).toBeVisible();
await expect(
page.locator('[data-testid="sentiment-chart"]')
).toBeVisible();
});
});
});

View File

@ -1,468 +0,0 @@
/**
* E2E tests for complete user registration and login workflow
*
* Tests the full user journey:
* 1. Company registration
* 2. User login
* 3. Dashboard access
* 4. Authentication state management
* 5. Session persistence
* 6. Logout functionality
*/
import { test, expect, type Page } from "@playwright/test";
// Test data
const testCompany = {
name: "E2E Test Company",
csvUrl: "https://example.com/test.csv",
csvUsername: "testuser",
csvPassword: "testpass123",
adminEmail: "admin@e2etest.com",
adminName: "E2E Admin",
adminPassword: "E2ETestPassword123!",
};
const testUser = {
email: "user@e2etest.com",
password: "UserTestPassword123!",
name: "E2E Test User",
};
// Helper functions
async function fillRegistrationForm(page: Page) {
await page.fill('[data-testid="company-name"]', testCompany.name);
await page.fill('[data-testid="csv-url"]', testCompany.csvUrl);
await page.fill('[data-testid="csv-username"]', testCompany.csvUsername);
await page.fill('[data-testid="csv-password"]', testCompany.csvPassword);
await page.fill('[data-testid="admin-email"]', testCompany.adminEmail);
await page.fill('[data-testid="admin-name"]', testCompany.adminName);
await page.fill('[data-testid="admin-password"]', testCompany.adminPassword);
}
async function fillLoginForm(page: Page, email: string, password: string) {
await page.fill('[data-testid="email"]', email);
await page.fill('[data-testid="password"]', password);
}
async function waitForDashboard(page: Page) {
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator("h1")).toContainText("Dashboard");
}
test.describe("User Authentication Workflow", () => {
test.beforeEach(async ({ page }) => {
// Set base URL for local development
await page.goto("http://localhost:3000");
});
test.describe("Company Registration Flow", () => {
test("should allow new company registration with admin user", async ({
page,
}) => {
// Navigate to registration page
await page.click('[data-testid="register-link"]');
await expect(page).toHaveURL(/\/register/);
// Fill registration form
await fillRegistrationForm(page);
// Submit registration
await page.click('[data-testid="register-button"]');
// Should redirect to login page with success message
await expect(page).toHaveURL(/\/login/);
await expect(
page.locator('[data-testid="success-message"]')
).toContainText("Registration successful");
});
test("should validate registration form fields", async ({ page }) => {
await page.goto("http://localhost:3000/register");
// Try to submit empty form
await page.click('[data-testid="register-button"]');
// Should show validation errors
await expect(
page.locator('[data-testid="company-name-error"]')
).toContainText("Company name is required");
await expect(
page.locator('[data-testid="admin-email-error"]')
).toContainText("Email is required");
await expect(
page.locator('[data-testid="admin-password-error"]')
).toContainText("Password must be at least 12 characters");
});
test("should enforce password strength requirements", async ({ page }) => {
await page.goto("http://localhost:3000/register");
// Test weak password
await page.fill('[data-testid="admin-password"]', "weakpass");
await page.blur('[data-testid="admin-password"]');
await expect(
page.locator('[data-testid="admin-password-error"]')
).toContainText("Password must contain at least one uppercase letter");
// Test strong password
await page.fill('[data-testid="admin-password"]', "StrongPassword123!");
await page.blur('[data-testid="admin-password"]');
await expect(
page.locator('[data-testid="admin-password-error"]')
).toHaveCount(0);
});
});
test.describe("User Login Flow", () => {
test.beforeEach(async ({ page }) => {
// Assume company registration was completed in previous test
// Navigate directly to login page
await page.goto("http://localhost:3000/login");
});
test("should allow successful login with valid credentials", async ({
page,
}) => {
// Fill login form
await fillLoginForm(
page,
testCompany.adminEmail,
testCompany.adminPassword
);
// Submit login
await page.click('[data-testid="login-button"]');
// Should redirect to dashboard
await waitForDashboard(page);
// Verify user info is displayed
await expect(page.locator('[data-testid="user-name"]')).toContainText(
testCompany.adminName
);
});
test("should reject invalid credentials", async ({ page }) => {
// Fill login form with wrong password
await fillLoginForm(page, testCompany.adminEmail, "wrongpassword");
// Submit login
await page.click('[data-testid="login-button"]');
// Should show error message
await expect(page.locator('[data-testid="error-message"]')).toContainText(
"Invalid credentials"
);
// Should remain on login page
await expect(page).toHaveURL(/\/login/);
});
test("should validate login form fields", async ({ page }) => {
// Try to submit empty form
await page.click('[data-testid="login-button"]');
// Should show validation errors
await expect(page.locator('[data-testid="email-error"]')).toContainText(
"Email is required"
);
await expect(
page.locator('[data-testid="password-error"]')
).toContainText("Password is required");
});
test("should handle rate limiting", async ({ page }) => {
// Attempt multiple failed logins
for (let i = 0; i < 6; i++) {
await fillLoginForm(page, "invalid@email.com", "wrongpassword");
await page.click('[data-testid="login-button"]');
await page.waitForTimeout(100); // Small delay between attempts
}
// Should show rate limit error
await expect(page.locator('[data-testid="error-message"]')).toContainText(
"Too many login attempts"
);
});
});
test.describe("Dashboard Access and Navigation", () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto("http://localhost:3000/login");
await fillLoginForm(
page,
testCompany.adminEmail,
testCompany.adminPassword
);
await page.click('[data-testid="login-button"]');
await waitForDashboard(page);
});
test("should display dashboard overview correctly", async ({ page }) => {
// Check main dashboard elements
await expect(page.locator("h1")).toContainText("Dashboard Overview");
// Check metric cards
await expect(
page.locator('[data-testid="total-sessions-card"]')
).toBeVisible();
await expect(
page.locator('[data-testid="avg-sentiment-card"]')
).toBeVisible();
await expect(
page.locator('[data-testid="escalation-rate-card"]')
).toBeVisible();
// Check navigation sidebar
await expect(page.locator('[data-testid="nav-overview"]')).toBeVisible();
await expect(page.locator('[data-testid="nav-sessions"]')).toBeVisible();
await expect(page.locator('[data-testid="nav-users"]')).toBeVisible();
});
test("should navigate between dashboard sections", async ({ page }) => {
// Navigate to Sessions
await page.click('[data-testid="nav-sessions"]');
await expect(page).toHaveURL(/\/dashboard\/sessions/);
await expect(page.locator("h1")).toContainText("Sessions");
// Navigate to Users
await page.click('[data-testid="nav-users"]');
await expect(page).toHaveURL(/\/dashboard\/users/);
await expect(page.locator("h1")).toContainText("Users");
// Navigate back to Overview
await page.click('[data-testid="nav-overview"]');
await expect(page).toHaveURL(/\/dashboard\/overview/);
await expect(page.locator("h1")).toContainText("Dashboard Overview");
});
test("should handle unauthorized access attempts", async ({ page }) => {
// Try to access admin-only features as regular user
await page.goto("http://localhost:3000/dashboard/users");
// If user is not admin, should show appropriate message or redirect
const isAdmin = await page
.locator('[data-testid="admin-panel"]')
.isVisible();
if (!isAdmin) {
await expect(
page.locator('[data-testid="access-denied"]')
).toBeVisible();
}
});
});
test.describe("Session Management", () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto("http://localhost:3000/login");
await fillLoginForm(
page,
testCompany.adminEmail,
testCompany.adminPassword
);
await page.click('[data-testid="login-button"]');
await waitForDashboard(page);
});
test("should persist session across page refreshes", async ({ page }) => {
// Refresh the page
await page.reload();
// Should still be logged in
await waitForDashboard(page);
await expect(page.locator('[data-testid="user-name"]')).toContainText(
testCompany.adminName
);
});
test("should persist session across browser tabs", async ({ context }) => {
// Open new tab
const newTab = await context.newPage();
await newTab.goto("http://localhost:3000/dashboard");
// Should be automatically logged in
await waitForDashboard(newTab);
await expect(newTab.locator('[data-testid="user-name"]')).toContainText(
testCompany.adminName
);
await newTab.close();
});
test("should redirect to login when session expires", async ({ page }) => {
// Simulate session expiration by clearing localStorage/cookies
await page.evaluate(() => {
localStorage.clear();
document.cookie.split(";").forEach((c) => {
const eqPos = c.indexOf("=");
const name = eqPos > -1 ? c.substr(0, eqPos) : c;
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
});
});
// Try to navigate to protected page
await page.goto("http://localhost:3000/dashboard");
// Should redirect to login
await expect(page).toHaveURL(/\/login/);
});
});
test.describe("Logout Functionality", () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto("http://localhost:3000/login");
await fillLoginForm(
page,
testCompany.adminEmail,
testCompany.adminPassword
);
await page.click('[data-testid="login-button"]');
await waitForDashboard(page);
});
test("should successfully logout user", async ({ page }) => {
// Open user menu
await page.click('[data-testid="user-menu"]');
// Click logout
await page.click('[data-testid="logout-button"]');
// Should redirect to login page
await expect(page).toHaveURL(/\/login/);
// Should show logout success message
await expect(
page.locator('[data-testid="success-message"]')
).toContainText("Logged out successfully");
// Try to access protected page
await page.goto("http://localhost:3000/dashboard");
// Should redirect back to login
await expect(page).toHaveURL(/\/login/);
});
test("should clear session data on logout", async ({ page }) => {
// Check that session data exists
const sessionBefore = await page.evaluate(() =>
localStorage.getItem("session")
);
expect(sessionBefore).toBeTruthy();
// Logout
await page.click('[data-testid="user-menu"]');
await page.click('[data-testid="logout-button"]');
// Check that session data is cleared
const sessionAfter = await page.evaluate(() =>
localStorage.getItem("session")
);
expect(sessionAfter).toBeFalsy();
});
});
test.describe("Password Reset Flow", () => {
test("should allow password reset request", async ({ page }) => {
await page.goto("http://localhost:3000/login");
// Click forgot password link
await page.click('[data-testid="forgot-password-link"]');
await expect(page).toHaveURL(/\/forgot-password/);
// Enter email
await page.fill('[data-testid="email"]', testCompany.adminEmail);
await page.click('[data-testid="reset-button"]');
// Should show success message
await expect(
page.locator('[data-testid="success-message"]')
).toContainText("Password reset email sent");
});
test("should validate email format in password reset", async ({ page }) => {
await page.goto("http://localhost:3000/forgot-password");
// Enter invalid email
await page.fill('[data-testid="email"]', "invalid-email");
await page.click('[data-testid="reset-button"]');
// Should show validation error
await expect(page.locator('[data-testid="email-error"]')).toContainText(
"Invalid email format"
);
});
});
test.describe("Mobile Responsive Design", () => {
test("should work correctly on mobile devices", async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
// Test login flow on mobile
await page.goto("http://localhost:3000/login");
await fillLoginForm(
page,
testCompany.adminEmail,
testCompany.adminPassword
);
await page.click('[data-testid="login-button"]');
// Should work on mobile
await waitForDashboard(page);
// Check mobile navigation
const mobileMenu = page.locator('[data-testid="mobile-menu-toggle"]');
if (await mobileMenu.isVisible()) {
await mobileMenu.click();
await expect(page.locator('[data-testid="mobile-nav"]')).toBeVisible();
}
});
});
test.describe("Accessibility", () => {
test("should be accessible with keyboard navigation", async ({ page }) => {
await page.goto("http://localhost:3000/login");
// Test keyboard navigation
await page.keyboard.press("Tab");
await expect(page.locator('[data-testid="email"]')).toBeFocused();
await page.keyboard.press("Tab");
await expect(page.locator('[data-testid="password"]')).toBeFocused();
await page.keyboard.press("Tab");
await expect(page.locator('[data-testid="login-button"]')).toBeFocused();
// Test form submission with Enter key
await page.fill('[data-testid="email"]', testCompany.adminEmail);
await page.fill('[data-testid="password"]', testCompany.adminPassword);
await page.keyboard.press("Enter");
await waitForDashboard(page);
});
test("should have proper ARIA labels and roles", async ({ page }) => {
await page.goto("http://localhost:3000/login");
// Check form accessibility
await expect(page.locator('[data-testid="email"]')).toHaveAttribute(
"aria-label",
"Email address"
);
await expect(page.locator('[data-testid="password"]')).toHaveAttribute(
"aria-label",
"Password"
);
await expect(
page.locator('[data-testid="login-button"]')
).toHaveAttribute("role", "button");
});
});
});

View File

@ -1,390 +0,0 @@
/**
* Centralized Authorization System
*
* Provides role-based access control with granular permissions,
* company-level access control, and audit trail integration.
*/
import { AuthorizationError } from "./errors";
import type { APIContext } from "./handler";
/**
* System permissions enumeration
*/
export enum Permission {
// Audit & Security
READ_AUDIT_LOGS = "audit_logs:read",
EXPORT_AUDIT_LOGS = "audit_logs:export",
MANAGE_SECURITY = "security:manage",
// User Management
READ_USERS = "users:read",
MANAGE_USERS = "users:manage",
INVITE_USERS = "users:invite",
// Company Management
READ_COMPANIES = "companies:read",
MANAGE_COMPANIES = "companies:manage",
MANAGE_COMPANY_SETTINGS = "companies:settings",
// Dashboard & Analytics
READ_DASHBOARD = "dashboard:read",
READ_SESSIONS = "sessions:read",
MANAGE_SESSIONS = "sessions:manage",
// System Administration
PLATFORM_ADMIN = "platform:admin",
CACHE_MANAGE = "cache:manage",
SCHEDULER_MANAGE = "schedulers:manage",
// AI & Processing
MANAGE_AI_PROCESSING = "ai:manage",
READ_AI_METRICS = "ai:read",
// Import & Export
IMPORT_DATA = "data:import",
EXPORT_DATA = "data:export",
}
/**
* User roles with their associated permissions
*/
export const ROLE_PERMISSIONS: Record<string, Permission[]> = {
USER: [Permission.READ_DASHBOARD, Permission.READ_SESSIONS],
AUDITOR: [
Permission.READ_DASHBOARD,
Permission.READ_SESSIONS,
Permission.READ_AUDIT_LOGS,
Permission.EXPORT_AUDIT_LOGS,
Permission.READ_AI_METRICS,
],
ADMIN: [
// Inherit USER permissions
Permission.READ_DASHBOARD,
Permission.READ_SESSIONS,
Permission.MANAGE_SESSIONS,
// Inherit AUDITOR permissions
Permission.READ_AUDIT_LOGS,
Permission.EXPORT_AUDIT_LOGS,
Permission.READ_AI_METRICS,
// Admin-specific permissions
Permission.READ_USERS,
Permission.MANAGE_USERS,
Permission.INVITE_USERS,
Permission.MANAGE_COMPANY_SETTINGS,
Permission.MANAGE_SECURITY,
Permission.MANAGE_AI_PROCESSING,
Permission.IMPORT_DATA,
Permission.EXPORT_DATA,
Permission.CACHE_MANAGE,
],
PLATFORM_ADMIN: [
// Include all ADMIN permissions
Permission.READ_DASHBOARD,
Permission.READ_SESSIONS,
Permission.MANAGE_SESSIONS,
Permission.READ_AUDIT_LOGS,
Permission.EXPORT_AUDIT_LOGS,
Permission.READ_AI_METRICS,
Permission.READ_USERS,
Permission.MANAGE_USERS,
Permission.INVITE_USERS,
Permission.MANAGE_COMPANY_SETTINGS,
Permission.MANAGE_SECURITY,
Permission.MANAGE_AI_PROCESSING,
Permission.IMPORT_DATA,
Permission.EXPORT_DATA,
Permission.CACHE_MANAGE,
// Platform-specific permissions
Permission.PLATFORM_ADMIN,
Permission.READ_COMPANIES,
Permission.MANAGE_COMPANIES,
Permission.SCHEDULER_MANAGE,
],
};
/**
* Resource types for company-level access control
*/
export enum ResourceType {
AUDIT_LOG = "audit_log",
SESSION = "session",
USER = "user",
COMPANY = "company",
AI_REQUEST = "ai_request",
}
/**
* Company access validation result
*/
export interface CompanyAccessResult {
allowed: boolean;
reason?: string;
companyId?: string;
}
/**
* Check if a user has a specific permission
*/
export function hasPermission(
userRole: string,
permission: Permission
): boolean {
const rolePermissions = ROLE_PERMISSIONS[userRole];
return rolePermissions?.includes(permission) ?? false;
}
/**
* Check if a user has any of the specified permissions
*/
export function hasAnyPermission(
userRole: string,
permissions: Permission[]
): boolean {
return permissions.some((permission) => hasPermission(userRole, permission));
}
/**
* Check if a user has all of the specified permissions
*/
export function hasAllPermissions(
userRole: string,
permissions: Permission[]
): boolean {
return permissions.every((permission) => hasPermission(userRole, permission));
}
/**
* Get all permissions for a user role
*/
export function getUserPermissions(userRole: string): Permission[] {
return ROLE_PERMISSIONS[userRole] || [];
}
/**
* Validate permission access and throw if unauthorized
*/
export function requirePermission(permission: Permission) {
return (context: APIContext) => {
if (!context.user) {
throw new AuthorizationError("Authentication required");
}
if (!hasPermission(context.user.role, permission)) {
throw new AuthorizationError(`Permission required: ${permission}`);
}
};
}
/**
* Validate any of the specified permissions
*/
export function requireAnyPermission(permissions: Permission[]) {
return (context: APIContext) => {
if (!context.user) {
throw new AuthorizationError("Authentication required");
}
if (!hasAnyPermission(context.user.role, permissions)) {
throw new AuthorizationError(
`One of these permissions required: ${permissions.join(", ")}`
);
}
};
}
/**
* Validate all of the specified permissions
*/
export function requireAllPermissions(permissions: Permission[]) {
return (context: APIContext) => {
if (!context.user) {
throw new AuthorizationError("Authentication required");
}
if (!hasAllPermissions(context.user.role, permissions)) {
throw new AuthorizationError(
`All of these permissions required: ${permissions.join(", ")}`
);
}
};
}
/**
* Check if user can access resources from a specific company
*/
export function validateCompanyAccess(
context: APIContext,
targetCompanyId: string,
_resourceType?: ResourceType
): CompanyAccessResult {
if (!context.user) {
return {
allowed: false,
reason: "Authentication required",
};
}
// Platform admins can access all companies
if (context.user.role === "PLATFORM_ADMIN") {
return {
allowed: true,
companyId: targetCompanyId,
};
}
// Regular users can only access their own company's resources
if (context.user.companyId !== targetCompanyId) {
return {
allowed: false,
reason: `Access denied to company ${targetCompanyId}`,
companyId: context.user.companyId,
};
}
return {
allowed: true,
companyId: targetCompanyId,
};
}
/**
* Require company access validation
*/
export function requireCompanyAccess(
targetCompanyId: string,
resourceType?: ResourceType
) {
return (context: APIContext) => {
const accessResult = validateCompanyAccess(
context,
targetCompanyId,
resourceType
);
if (!accessResult.allowed) {
throw new AuthorizationError(accessResult.reason);
}
};
}
/**
* Extract company ID from request and validate access
*/
export function requireCompanyAccessFromRequest(
getCompanyId: (context: APIContext) => string | Promise<string>,
resourceType?: ResourceType
) {
return async (context: APIContext) => {
const companyId = await getCompanyId(context);
const accessResult = validateCompanyAccess(
context,
companyId,
resourceType
);
if (!accessResult.allowed) {
throw new AuthorizationError(accessResult.reason);
}
return companyId;
};
}
/**
* Role hierarchy helper - check if role A is higher than role B
*/
export function isRoleHigherThan(roleA: string, roleB: string): boolean {
const roleHierarchy = {
USER: 1,
AUDITOR: 2,
ADMIN: 3,
PLATFORM_ADMIN: 4,
};
const levelA = roleHierarchy[roleA as keyof typeof roleHierarchy] || 0;
const levelB = roleHierarchy[roleB as keyof typeof roleHierarchy] || 0;
return levelA > levelB;
}
/**
* Check if user can manage another user (role hierarchy)
*/
export function canManageUser(
managerRole: string,
targetUserRole: string
): boolean {
// Platform admins can manage anyone
if (managerRole === "PLATFORM_ADMIN") {
return true;
}
// Admins can manage users and auditors, but not other admins or platform admins
if (managerRole === "ADMIN") {
return ["USER", "AUDITOR"].includes(targetUserRole);
}
// Other roles cannot manage users
return false;
}
/**
* Require user management permission
*/
export function requireUserManagementPermission(targetUserRole: string) {
return (context: APIContext) => {
if (!context.user) {
throw new AuthorizationError("Authentication required");
}
if (!canManageUser(context.user.role, targetUserRole)) {
throw new AuthorizationError(
`Insufficient permissions to manage ${targetUserRole} users`
);
}
};
}
/**
* Create a permission checker function
*/
export function createPermissionChecker(context: APIContext) {
return {
has: (permission: Permission) =>
hasPermission(context.user?.role || "", permission),
hasAny: (permissions: Permission[]) =>
hasAnyPermission(context.user?.role || "", permissions),
hasAll: (permissions: Permission[]) =>
hasAllPermissions(context.user?.role || "", permissions),
require: (permission: Permission) => requirePermission(permission)(context),
requireAny: (permissions: Permission[]) =>
requireAnyPermission(permissions)(context),
requireAll: (permissions: Permission[]) =>
requireAllPermissions(permissions)(context),
canAccessCompany: (companyId: string, resourceType?: ResourceType) =>
validateCompanyAccess(context, companyId, resourceType),
requireCompanyAccess: (companyId: string, resourceType?: ResourceType) =>
requireCompanyAccess(companyId, resourceType)(context),
canManageUser: (targetUserRole: string) =>
canManageUser(context.user?.role || "", targetUserRole),
};
}
/**
* Middleware function to attach permission checker to context
*/
export function withPermissions<T extends APIContext>(
context: T
): T & { permissions: ReturnType<typeof createPermissionChecker> } {
return {
...context,
permissions: createPermissionChecker(context),
};
}

Some files were not shown because too many files have changed in this diff Show More