Remove obsolete test scripts and update admin username
- Deleted various test scripts related to due date reminders, edge compatibility, logged-in flow, logging, mobile view, NextAuth, notifications API, notifications working, project API, project creation, Radicale sync configuration, safe audit logging, task API, task sets, user tracking, and verification scripts. - Removed the script for updating admin username from email to a simple "admin". - Cleaned up unused PowerShell script for updating queries.
This commit is contained in:
@@ -1,379 +0,0 @@
|
|||||||
# Audit Logging Implementation
|
|
||||||
|
|
||||||
This document describes the audit logging system implemented for the panel application. The system provides comprehensive tracking of user actions and system events for security, compliance, and monitoring purposes.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Comprehensive Action Tracking**: Logs all CRUD operations on projects, tasks, contracts, notes, and user management
|
|
||||||
- **Authentication Events**: Tracks login attempts, successes, and failures
|
|
||||||
- **Detailed Context**: Captures IP addresses, user agents, and request details
|
|
||||||
- **Flexible Filtering**: Query logs by user, action, resource type, date range, and more
|
|
||||||
- **Statistics Dashboard**: Provides insights into system usage patterns
|
|
||||||
- **Role-based Access**: Only admins and project managers can view audit logs
|
|
||||||
- **Performance Optimized**: Uses database indexes for efficient querying
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
1. **Audit Log Utility** (`src/lib/auditLog.js`)
|
|
||||||
|
|
||||||
- Core logging functions
|
|
||||||
- Query and statistics functions
|
|
||||||
- Action and resource type constants
|
|
||||||
|
|
||||||
2. **API Endpoints** (`src/app/api/audit-logs/`)
|
|
||||||
|
|
||||||
- `/api/audit-logs` - Query audit logs with filtering
|
|
||||||
- `/api/audit-logs/stats` - Get audit log statistics
|
|
||||||
|
|
||||||
3. **UI Components** (`src/components/AuditLogViewer.js`)
|
|
||||||
|
|
||||||
- Interactive audit log viewer
|
|
||||||
- Advanced filtering interface
|
|
||||||
- Statistics dashboard
|
|
||||||
|
|
||||||
4. **Admin Pages** (`src/app/admin/audit-logs/`)
|
|
||||||
- Admin interface for viewing audit logs
|
|
||||||
- Role-based access control
|
|
||||||
|
|
||||||
### Database Schema
|
|
||||||
|
|
||||||
The audit logs are stored in the `audit_logs` table:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE audit_logs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id TEXT, -- User who performed the action
|
|
||||||
action TEXT NOT NULL, -- Action performed (see AUDIT_ACTIONS)
|
|
||||||
resource_type TEXT, -- Type of resource affected
|
|
||||||
resource_id TEXT, -- ID of the affected resource
|
|
||||||
ip_address TEXT, -- IP address of the user
|
|
||||||
user_agent TEXT, -- Browser/client information
|
|
||||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
details TEXT, -- Additional details (JSON)
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Logging
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { logAuditEvent, AUDIT_ACTIONS, RESOURCE_TYPES } from "@/lib/auditLog";
|
|
||||||
|
|
||||||
// Log a simple action
|
|
||||||
logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_CREATE,
|
|
||||||
userId: "user123",
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "proj-456",
|
|
||||||
ipAddress: req.ip,
|
|
||||||
userAgent: req.headers["user-agent"],
|
|
||||||
details: {
|
|
||||||
project_name: "New Project",
|
|
||||||
project_number: "NP-001",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Route Integration
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { logApiAction, AUDIT_ACTIONS, RESOURCE_TYPES } from "@/lib/auditLog";
|
|
||||||
|
|
||||||
export async function POST(req) {
|
|
||||||
const data = await req.json();
|
|
||||||
|
|
||||||
// Perform the operation
|
|
||||||
const result = createProject(data);
|
|
||||||
|
|
||||||
// Log the action
|
|
||||||
logApiAction(
|
|
||||||
req,
|
|
||||||
AUDIT_ACTIONS.PROJECT_CREATE,
|
|
||||||
RESOURCE_TYPES.PROJECT,
|
|
||||||
result.id.toString(),
|
|
||||||
req.session,
|
|
||||||
{ projectData: data }
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true, id: result.id });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Querying Audit Logs
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { getAuditLogs, getAuditLogStats } from "@/lib/auditLog";
|
|
||||||
|
|
||||||
// Get recent logs
|
|
||||||
const recentLogs = getAuditLogs({
|
|
||||||
limit: 50,
|
|
||||||
orderBy: "timestamp",
|
|
||||||
orderDirection: "DESC",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get logs for a specific user
|
|
||||||
const userLogs = getAuditLogs({
|
|
||||||
userId: "user123",
|
|
||||||
startDate: "2025-01-01T00:00:00Z",
|
|
||||||
endDate: "2025-12-31T23:59:59Z",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get statistics
|
|
||||||
const stats = getAuditLogStats({
|
|
||||||
startDate: "2025-01-01T00:00:00Z",
|
|
||||||
endDate: "2025-12-31T23:59:59Z",
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Actions
|
|
||||||
|
|
||||||
### Authentication Actions
|
|
||||||
|
|
||||||
- `login` - Successful user login
|
|
||||||
- `logout` - User logout
|
|
||||||
- `login_failed` - Failed login attempt
|
|
||||||
|
|
||||||
### Project Actions
|
|
||||||
|
|
||||||
- `project_create` - Project creation
|
|
||||||
- `project_update` - Project modification
|
|
||||||
- `project_delete` - Project deletion
|
|
||||||
- `project_view` - Project viewing
|
|
||||||
|
|
||||||
### Task Actions
|
|
||||||
|
|
||||||
- `task_create` - Task creation
|
|
||||||
- `task_update` - Task modification
|
|
||||||
- `task_delete` - Task deletion
|
|
||||||
- `task_status_change` - Task status modification
|
|
||||||
|
|
||||||
### Project Task Actions
|
|
||||||
|
|
||||||
- `project_task_create` - Project task assignment
|
|
||||||
- `project_task_update` - Project task modification
|
|
||||||
- `project_task_delete` - Project task removal
|
|
||||||
- `project_task_status_change` - Project task status change
|
|
||||||
|
|
||||||
### Contract Actions
|
|
||||||
|
|
||||||
- `contract_create` - Contract creation
|
|
||||||
- `contract_update` - Contract modification
|
|
||||||
- `contract_delete` - Contract deletion
|
|
||||||
|
|
||||||
### Note Actions
|
|
||||||
|
|
||||||
- `note_create` - Note creation
|
|
||||||
- `note_update` - Note modification
|
|
||||||
- `note_delete` - Note deletion
|
|
||||||
|
|
||||||
### Admin Actions
|
|
||||||
|
|
||||||
- `user_create` - User account creation
|
|
||||||
- `user_update` - User account modification
|
|
||||||
- `user_delete` - User account deletion
|
|
||||||
- `user_role_change` - User role modification
|
|
||||||
|
|
||||||
### System Actions
|
|
||||||
|
|
||||||
- `data_export` - Data export operations
|
|
||||||
- `bulk_operation` - Bulk data operations
|
|
||||||
|
|
||||||
## Resource Types
|
|
||||||
|
|
||||||
- `project` - Project resources
|
|
||||||
- `task` - Task templates
|
|
||||||
- `project_task` - Project-specific tasks
|
|
||||||
- `contract` - Contracts
|
|
||||||
- `note` - Notes and comments
|
|
||||||
- `user` - User accounts
|
|
||||||
- `session` - Authentication sessions
|
|
||||||
- `system` - System-level operations
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### GET /api/audit-logs
|
|
||||||
|
|
||||||
Query audit logs with optional filtering.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
|
|
||||||
- `userId` - Filter by user ID
|
|
||||||
- `action` - Filter by action type
|
|
||||||
- `resourceType` - Filter by resource type
|
|
||||||
- `resourceId` - Filter by resource ID
|
|
||||||
- `startDate` - Filter from date (ISO string)
|
|
||||||
- `endDate` - Filter to date (ISO string)
|
|
||||||
- `limit` - Maximum results (default: 100)
|
|
||||||
- `offset` - Results offset (default: 0)
|
|
||||||
- `orderBy` - Order by field (default: timestamp)
|
|
||||||
- `orderDirection` - ASC or DESC (default: DESC)
|
|
||||||
- `includeStats` - Include statistics (true/false)
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"user_id": "user123",
|
|
||||||
"user_name": "John Doe",
|
|
||||||
"user_email": "john@example.com",
|
|
||||||
"action": "project_create",
|
|
||||||
"resource_type": "project",
|
|
||||||
"resource_id": "proj-456",
|
|
||||||
"ip_address": "192.168.1.100",
|
|
||||||
"user_agent": "Mozilla/5.0...",
|
|
||||||
"timestamp": "2025-07-09T10:30:00Z",
|
|
||||||
"details": {
|
|
||||||
"project_name": "New Project",
|
|
||||||
"project_number": "NP-001"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stats": {
|
|
||||||
"total": 150,
|
|
||||||
"actionBreakdown": [...],
|
|
||||||
"userBreakdown": [...],
|
|
||||||
"resourceBreakdown": [...]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GET /api/audit-logs/stats
|
|
||||||
|
|
||||||
Get audit log statistics.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
|
|
||||||
- `startDate` - Filter from date (ISO string)
|
|
||||||
- `endDate` - Filter to date (ISO string)
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"total": 150,
|
|
||||||
"actionBreakdown": [
|
|
||||||
{ "action": "project_view", "count": 45 },
|
|
||||||
{ "action": "login", "count": 23 }
|
|
||||||
],
|
|
||||||
"userBreakdown": [
|
|
||||||
{ "user_id": "user123", "user_name": "John Doe", "count": 67 }
|
|
||||||
],
|
|
||||||
"resourceBreakdown": [{ "resource_type": "project", "count": 89 }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Access Control
|
|
||||||
|
|
||||||
Audit logs are restricted to users with the following roles:
|
|
||||||
|
|
||||||
- `admin` - Full access to all audit logs
|
|
||||||
- `project_manager` - Full access to all audit logs
|
|
||||||
|
|
||||||
Other users cannot access audit logs.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Run the audit logging test script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node test-audit-logging.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
This will:
|
|
||||||
|
|
||||||
1. Create sample audit events
|
|
||||||
2. Test querying and filtering
|
|
||||||
3. Verify statistics generation
|
|
||||||
4. Test date range filtering
|
|
||||||
|
|
||||||
## Integration Status
|
|
||||||
|
|
||||||
The audit logging system has been integrated into the following API routes:
|
|
||||||
|
|
||||||
✅ **Authentication** (`src/lib/auth.js`)
|
|
||||||
|
|
||||||
- Login success/failure tracking
|
|
||||||
- Account lockout logging
|
|
||||||
|
|
||||||
✅ **Projects** (`src/app/api/projects/`)
|
|
||||||
|
|
||||||
- Project CRUD operations
|
|
||||||
- List view access
|
|
||||||
|
|
||||||
✅ **Notes** (`src/app/api/notes/`)
|
|
||||||
|
|
||||||
- Note creation, updates, and deletion
|
|
||||||
|
|
||||||
🔄 **Pending Integration:**
|
|
||||||
|
|
||||||
- Tasks API
|
|
||||||
- Project Tasks API
|
|
||||||
- Contracts API
|
|
||||||
- User management API
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
- Database indexes are created on frequently queried fields
|
|
||||||
- Large result sets are paginated
|
|
||||||
- Statistics queries are optimized for common use cases
|
|
||||||
- Failed operations are logged to prevent data loss
|
|
||||||
|
|
||||||
## Security Features
|
|
||||||
|
|
||||||
- IP address tracking for forensic analysis
|
|
||||||
- User agent logging for client identification
|
|
||||||
- Failed authentication attempt tracking
|
|
||||||
- Detailed change logging for sensitive operations
|
|
||||||
- Role-based access control for audit log viewing
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
### Log Retention
|
|
||||||
|
|
||||||
Consider implementing log retention policies:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Delete audit logs older than 1 year
|
|
||||||
DELETE FROM audit_logs
|
|
||||||
WHERE timestamp < datetime('now', '-1 year');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monitoring
|
|
||||||
|
|
||||||
Monitor audit log growth and performance:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Check audit log table size
|
|
||||||
SELECT COUNT(*) as total_logs,
|
|
||||||
MIN(timestamp) as oldest_log,
|
|
||||||
MAX(timestamp) as newest_log
|
|
||||||
FROM audit_logs;
|
|
||||||
|
|
||||||
-- Check most active users
|
|
||||||
SELECT user_id, COUNT(*) as activity_count
|
|
||||||
FROM audit_logs
|
|
||||||
WHERE timestamp > datetime('now', '-30 days')
|
|
||||||
GROUP BY user_id
|
|
||||||
ORDER BY activity_count DESC
|
|
||||||
LIMIT 10;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- Real-time audit log streaming
|
|
||||||
- Advanced analytics and reporting
|
|
||||||
- Integration with external SIEM systems
|
|
||||||
- Automatic anomaly detection
|
|
||||||
- Compliance reporting templates
|
|
||||||
- Log export functionality
|
|
||||||
@@ -1,971 +0,0 @@
|
|||||||
# Authorization Implementation Guide
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
This document outlines the implementation strategy for adding authentication and authorization to the Project Management Panel - a Next.js 15 application with SQLite database.
|
|
||||||
|
|
||||||
## Current State Analysis (Updated: June 25, 2025)
|
|
||||||
|
|
||||||
### ✅ What We Have Implemented
|
|
||||||
|
|
||||||
- **Framework**: Next.js 15 with App Router
|
|
||||||
- **Database**: SQLite with better-sqlite3
|
|
||||||
- **Authentication**: NextAuth.js v5 with credentials provider
|
|
||||||
- **User Management**: Complete user CRUD operations with bcrypt password hashing
|
|
||||||
- **Database Schema**: Users table with roles, audit logs, sessions
|
|
||||||
- **API Protection**: Middleware system with role-based access control
|
|
||||||
- **Session Management**: JWT-based sessions with 30-day expiration
|
|
||||||
- **Security Features**: Account lockout, failed login tracking, password validation
|
|
||||||
- **UI Components**: Authentication provider, navigation with user context
|
|
||||||
- **Auth Pages**: Sign-in page implemented
|
|
||||||
|
|
||||||
### ✅ What's Protected
|
|
||||||
|
|
||||||
- **API Routes**: All major endpoints (projects, contracts, tasks, notes) are protected
|
|
||||||
- **Role Hierarchy**: admin > project_manager > user > read_only
|
|
||||||
- **Navigation**: Role-based menu items (admin sees user management)
|
|
||||||
- **Session Security**: Automatic session management and validation
|
|
||||||
|
|
||||||
### 🔄 Partially Implemented
|
|
||||||
|
|
||||||
- **Auth Pages**: Sign-in exists, missing sign-out and error pages
|
|
||||||
- **User Interface**: Basic auth integration, could use more polish
|
|
||||||
- **Admin Features**: User management backend exists, UI needs completion
|
|
||||||
- **Audit Logging**: Database schema exists, not fully integrated
|
|
||||||
|
|
||||||
### ❌ Still Missing
|
|
||||||
|
|
||||||
- Complete user management UI for admins
|
|
||||||
- Password reset functionality
|
|
||||||
- Rate limiting implementation
|
|
||||||
- Enhanced input validation schemas
|
|
||||||
- CSRF protection
|
|
||||||
- Security headers middleware
|
|
||||||
- Comprehensive error handling
|
|
||||||
- Email notifications
|
|
||||||
|
|
||||||
## Recommended Implementation Strategy
|
|
||||||
|
|
||||||
### 1. Authentication Solution: NextAuth.js
|
|
||||||
|
|
||||||
**Why NextAuth.js?**
|
|
||||||
|
|
||||||
- ✅ Native Next.js 15 App Router support
|
|
||||||
- ✅ Database session management
|
|
||||||
- ✅ Built-in security features (CSRF, JWT handling)
|
|
||||||
- ✅ Flexible provider system
|
|
||||||
- ✅ SQLite adapter available
|
|
||||||
|
|
||||||
### 2. Role-Based Access Control (RBAC)
|
|
||||||
|
|
||||||
**Proposed User Roles:**
|
|
||||||
|
|
||||||
| Role | Permissions | Use Case |
|
|
||||||
| ------------------- | --------------------------------------- | ----------------------- |
|
|
||||||
| **Admin** | Full system access, user management | System administrators |
|
|
||||||
| **Project Manager** | Manage all projects/tasks, view reports | Team leads, supervisors |
|
|
||||||
| **User** | View/edit assigned projects/tasks | Regular employees |
|
|
||||||
| **Read-only** | View-only access to data | Clients, stakeholders |
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
### ✅ Phase 1: Foundation Setup - COMPLETED
|
|
||||||
|
|
||||||
#### 1.1 Dependencies - ✅ INSTALLED
|
|
||||||
|
|
||||||
- NextAuth.js v5 (beta)
|
|
||||||
- bcryptjs for password hashing
|
|
||||||
- Zod for validation
|
|
||||||
- Better-sqlite3 adapter compatibility
|
|
||||||
|
|
||||||
#### 1.2 Environment Configuration - ✅ COMPLETED
|
|
||||||
|
|
||||||
- `.env.local` configured with NEXTAUTH_SECRET and NEXTAUTH_URL
|
|
||||||
- Database URL configuration
|
|
||||||
- Development environment setup
|
|
||||||
|
|
||||||
#### 1.3 Database Schema - ✅ IMPLEMENTED
|
|
||||||
|
|
||||||
- Users table with roles and security features
|
|
||||||
- Sessions table for NextAuth.js
|
|
||||||
- Audit logs table for security tracking
|
|
||||||
- Proper indexes for performance
|
|
||||||
|
|
||||||
#### 1.4 Initial Admin User - ✅ COMPLETED
|
|
||||||
|
|
||||||
- `scripts/create-admin.js` script available
|
|
||||||
- Default admin user: admin@localhost.com / admin123456
|
|
||||||
|
|
||||||
### ✅ Phase 2: Authentication Core - COMPLETED
|
|
||||||
|
|
||||||
#### 2.1 NextAuth.js Configuration - ✅ IMPLEMENTED
|
|
||||||
|
|
||||||
- **File**: `src/lib/auth.js`
|
|
||||||
- Credentials provider with email/password
|
|
||||||
- JWT session strategy with 30-day expiration
|
|
||||||
- Account lockout after 5 failed attempts (15-minute lockout)
|
|
||||||
- Password verification with bcrypt
|
|
||||||
- Failed login attempt tracking
|
|
||||||
- Session callbacks for role management
|
|
||||||
|
|
||||||
#### 2.2 API Route Handlers - ✅ IMPLEMENTED
|
|
||||||
|
|
||||||
- **File**: `src/app/api/auth/[...nextauth]/route.js`
|
|
||||||
- NextAuth.js handlers properly configured
|
|
||||||
|
|
||||||
#### 2.3 User Management System - ✅ IMPLEMENTED
|
|
||||||
|
|
||||||
- **File**: `src/lib/userManagement.js`
|
|
||||||
- Complete CRUD operations for users
|
|
||||||
- Password hashing and validation
|
|
||||||
- Role management functions
|
|
||||||
- User lookup by ID and email
|
|
||||||
|
|
||||||
### ✅ Phase 3: Authorization Middleware - COMPLETED
|
|
||||||
|
|
||||||
#### 3.1 API Protection Middleware - ✅ IMPLEMENTED
|
|
||||||
|
|
||||||
- **File**: `src/lib/middleware/auth.js`
|
|
||||||
- `withAuth()` function for protecting routes
|
|
||||||
- Role hierarchy enforcement (admin=4, project_manager=3, user=2, read_only=1)
|
|
||||||
- Helper functions: `withReadAuth`, `withUserAuth`, `withAdminAuth`, `withManagerAuth`
|
|
||||||
- Proper error handling and status codes
|
|
||||||
|
|
||||||
#### 3.2 Protected API Routes - ✅ IMPLEMENTED
|
|
||||||
|
|
||||||
Example in `src/app/api/projects/route.js`:
|
|
||||||
|
|
||||||
- GET requests require read_only access
|
|
||||||
- POST requests require user access
|
|
||||||
- All major API endpoints are protected
|
|
||||||
|
|
||||||
#### 3.3 Session Provider - ✅ IMPLEMENTED
|
|
||||||
|
|
||||||
- **File**: `src/components/auth/AuthProvider.js`
|
|
||||||
- NextAuth SessionProvider wrapper
|
|
||||||
- Integrated into root layout
|
|
||||||
|
|
||||||
### 🔄 Phase 4: User Interface - PARTIALLY COMPLETED
|
|
||||||
|
|
||||||
#### 4.1 Authentication Pages - 🔄 PARTIAL
|
|
||||||
|
|
||||||
- ✅ **Sign-in page**: `src/app/auth/signin/page.js` - Complete with form validation
|
|
||||||
- ❌ **Sign-out page**: Missing
|
|
||||||
- 🔄 **Error page**: `src/app/auth/error/page.js` - Basic implementation
|
|
||||||
- ❌ **Unauthorized page**: Missing
|
|
||||||
|
|
||||||
#### 4.2 Navigation Updates - ✅ COMPLETED
|
|
||||||
|
|
||||||
- **File**: `src/components/ui/Navigation.js`
|
|
||||||
- User session integration with useSession
|
|
||||||
- Role-based menu items (admin sees user management)
|
|
||||||
- Sign-out functionality
|
|
||||||
- Conditional rendering based on auth status
|
|
||||||
|
|
||||||
#### 4.3 User Management Interface - ❌ MISSING
|
|
||||||
|
|
||||||
- Backend exists in userManagement.js
|
|
||||||
- Admin UI for user CRUD operations needed
|
|
||||||
- Role assignment interface needed
|
|
||||||
|
|
||||||
### ❌ Phase 5: Security Enhancements - NOT STARTED
|
|
||||||
|
|
||||||
#### 5.1 Input Validation Schemas - ❌ MISSING
|
|
||||||
|
|
||||||
- Zod schemas for API endpoints
|
|
||||||
- Request validation middleware
|
|
||||||
|
|
||||||
#### 5.2 Rate Limiting - ❌ MISSING
|
|
||||||
|
|
||||||
- Rate limiting middleware
|
|
||||||
- IP-based request tracking
|
|
||||||
|
|
||||||
#### 5.3 Security Headers - ❌ MISSING
|
|
||||||
|
|
||||||
- CSRF protection
|
|
||||||
- Security headers middleware
|
|
||||||
- Content Security Policy
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import NextAuth from "next-auth";
|
|
||||||
import CredentialsProvider from "next-auth/providers/credentials";
|
|
||||||
import { BetterSQLite3Adapter } from "@auth/better-sqlite3-adapter";
|
|
||||||
import db from "./db.js";
|
|
||||||
import bcrypt from "bcryptjs";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const loginSchema = z.object({
|
|
||||||
email: z.string().email("Invalid email format"),
|
|
||||||
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
||||||
adapter: BetterSQLite3Adapter(db),
|
|
||||||
session: {
|
|
||||||
strategy: "database",
|
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
||||||
updateAge: 24 * 60 * 60, // 24 hours
|
|
||||||
},
|
|
||||||
providers: [
|
|
||||||
CredentialsProvider({
|
|
||||||
name: "credentials",
|
|
||||||
credentials: {
|
|
||||||
email: { label: "Email", type: "email" },
|
|
||||||
password: { label: "Password", type: "password" },
|
|
||||||
},
|
|
||||||
async authorize(credentials, req) {
|
|
||||||
try {
|
|
||||||
// Validate input
|
|
||||||
const validatedFields = loginSchema.parse(credentials);
|
|
||||||
|
|
||||||
// Check if user exists and is active
|
|
||||||
const user = db
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT id, email, name, password_hash, role, is_active,
|
|
||||||
failed_login_attempts, locked_until
|
|
||||||
FROM users
|
|
||||||
WHERE email = ? AND is_active = 1
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.get(validatedFields.email);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("Invalid credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if account is locked
|
|
||||||
if (user.locked_until && new Date(user.locked_until) > new Date()) {
|
|
||||||
throw new Error("Account temporarily locked");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify password
|
|
||||||
const isValidPassword = await bcrypt.compare(
|
|
||||||
validatedFields.password,
|
|
||||||
user.password_hash
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isValidPassword) {
|
|
||||||
// Increment failed attempts
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE users
|
|
||||||
SET failed_login_attempts = failed_login_attempts + 1,
|
|
||||||
locked_until = CASE
|
|
||||||
WHEN failed_login_attempts >= 4
|
|
||||||
THEN datetime('now', '+15 minutes')
|
|
||||||
ELSE locked_until
|
|
||||||
END
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
).run(user.id);
|
|
||||||
|
|
||||||
throw new Error("Invalid credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset failed attempts and update last login
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE users
|
|
||||||
SET failed_login_attempts = 0,
|
|
||||||
locked_until = NULL,
|
|
||||||
last_login = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
).run(user.id);
|
|
||||||
|
|
||||||
// Log successful login
|
|
||||||
logAuditEvent(user.id, "LOGIN_SUCCESS", "user", user.id, req);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
name: user.name,
|
|
||||||
role: user.role,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Login error:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
callbacks: {
|
|
||||||
async jwt({ token, user, account }) {
|
|
||||||
if (user) {
|
|
||||||
token.role = user.role;
|
|
||||||
token.userId = user.id;
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
},
|
|
||||||
async session({ session, token, user }) {
|
|
||||||
if (token) {
|
|
||||||
session.user.id = token.userId || token.sub;
|
|
||||||
session.user.role = token.role || user?.role;
|
|
||||||
}
|
|
||||||
return session;
|
|
||||||
},
|
|
||||||
async signIn({ user, account, profile, email, credentials }) {
|
|
||||||
// Additional sign-in logic if needed
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pages: {
|
|
||||||
signIn: "/auth/signin",
|
|
||||||
signOut: "/auth/signout",
|
|
||||||
error: "/auth/error",
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
async signOut({ session, token }) {
|
|
||||||
if (session?.user?.id) {
|
|
||||||
logAuditEvent(session.user.id, "LOGOUT", "user", session.user.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Audit logging helper
|
|
||||||
function logAuditEvent(userId, action, resourceType, resourceId, req = null) {
|
|
||||||
try {
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, ip_address, user_agent)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
`
|
|
||||||
).run(
|
|
||||||
userId,
|
|
||||||
action,
|
|
||||||
resourceType,
|
|
||||||
resourceId,
|
|
||||||
req?.ip || "unknown",
|
|
||||||
req?.headers?.["user-agent"] || "unknown"
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Audit log error:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 API Route Handlers
|
|
||||||
|
|
||||||
Create `src/app/api/auth/[...nextauth]/route.js`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { handlers } from "@/lib/auth";
|
|
||||||
|
|
||||||
export const { GET, POST } = handlers;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Authorization Middleware
|
|
||||||
|
|
||||||
#### 3.1 API Protection Middleware
|
|
||||||
|
|
||||||
Create `src/lib/middleware/auth.js`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { auth } from "@/lib/auth";
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
// Role hierarchy for permission checking
|
|
||||||
const ROLE_HIERARCHY = {
|
|
||||||
admin: 4,
|
|
||||||
project_manager: 3,
|
|
||||||
user: 2,
|
|
||||||
read_only: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function withAuth(handler, options = {}) {
|
|
||||||
return async (req, context) => {
|
|
||||||
try {
|
|
||||||
const session = await auth();
|
|
||||||
|
|
||||||
// Check if user is authenticated
|
|
||||||
if (!session?.user) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Authentication required" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user account is active
|
|
||||||
const user = db
|
|
||||||
.prepare("SELECT is_active FROM users WHERE id = ?")
|
|
||||||
.get(session.user.id);
|
|
||||||
if (!user?.is_active) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Account deactivated" },
|
|
||||||
{ status: 403 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check role-based permissions
|
|
||||||
if (
|
|
||||||
options.requiredRole &&
|
|
||||||
!hasPermission(session.user.role, options.requiredRole)
|
|
||||||
) {
|
|
||||||
logAuditEvent(
|
|
||||||
session.user.id,
|
|
||||||
"ACCESS_DENIED",
|
|
||||||
options.resource || "api",
|
|
||||||
req.url
|
|
||||||
);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Insufficient permissions" },
|
|
||||||
{ status: 403 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check resource-specific permissions
|
|
||||||
if (options.checkResourceAccess) {
|
|
||||||
const hasAccess = await options.checkResourceAccess(
|
|
||||||
session.user,
|
|
||||||
context.params
|
|
||||||
);
|
|
||||||
if (!hasAccess) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Access denied to this resource" },
|
|
||||||
{ status: 403 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate request body if schema provided
|
|
||||||
if (
|
|
||||||
options.bodySchema &&
|
|
||||||
(req.method === "POST" ||
|
|
||||||
req.method === "PUT" ||
|
|
||||||
req.method === "PATCH")
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const body = await req.json();
|
|
||||||
options.bodySchema.parse(body);
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Invalid request data", details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add user info to request
|
|
||||||
req.user = session.user;
|
|
||||||
req.session = session;
|
|
||||||
|
|
||||||
// Call the original handler
|
|
||||||
return await handler(req, context);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Auth middleware error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal server error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasPermission(userRole, requiredRole) {
|
|
||||||
return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for read-only operations
|
|
||||||
export function withReadAuth(handler) {
|
|
||||||
return withAuth(handler, { requiredRole: "read_only" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for user-level operations
|
|
||||||
export function withUserAuth(handler) {
|
|
||||||
return withAuth(handler, { requiredRole: "user" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for project manager operations
|
|
||||||
export function withManagerAuth(handler) {
|
|
||||||
return withAuth(handler, { requiredRole: "project_manager" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for admin operations
|
|
||||||
export function withAdminAuth(handler) {
|
|
||||||
return withAuth(handler, { requiredRole: "admin" });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3.2 Client-Side Route Protection
|
|
||||||
|
|
||||||
Create `src/components/auth/ProtectedRoute.js`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export function ProtectedRoute({
|
|
||||||
children,
|
|
||||||
requiredRole = null,
|
|
||||||
fallback = null,
|
|
||||||
}) {
|
|
||||||
const { data: session, status } = useSession();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === "loading") return; // Still loading
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
router.push("/auth/signin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiredRole && !hasPermission(session.user.role, requiredRole)) {
|
|
||||||
router.push("/unauthorized");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [session, status, router, requiredRole]);
|
|
||||||
|
|
||||||
if (status === "loading") {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center items-center h-64">Loading...</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return fallback || <div>Redirecting to login...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiredRole && !hasPermission(session.user.role, requiredRole)) {
|
|
||||||
return fallback || <div>Access denied</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasPermission(userRole, requiredRole) {
|
|
||||||
const roleHierarchy = {
|
|
||||||
admin: 4,
|
|
||||||
project_manager: 3,
|
|
||||||
user: 2,
|
|
||||||
read_only: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 4: User Interface Components
|
|
||||||
|
|
||||||
#### 4.1 Authentication Pages
|
|
||||||
|
|
||||||
Pages to create:
|
|
||||||
|
|
||||||
- `src/app/auth/signin/page.js` - Login form
|
|
||||||
- `src/app/auth/signout/page.js` - Logout confirmation
|
|
||||||
- `src/app/auth/error/page.js` - Error handling
|
|
||||||
- `src/app/unauthorized/page.js` - Access denied page
|
|
||||||
|
|
||||||
#### 4.2 Navigation Updates
|
|
||||||
|
|
||||||
Update `src/components/ui/Navigation.js` to include:
|
|
||||||
|
|
||||||
- Login/logout buttons
|
|
||||||
- User info display
|
|
||||||
- Role-based menu items
|
|
||||||
|
|
||||||
#### 4.3 User Management Interface
|
|
||||||
|
|
||||||
For admin users:
|
|
||||||
|
|
||||||
- User listing and management
|
|
||||||
- Role assignment
|
|
||||||
- Account activation/deactivation
|
|
||||||
|
|
||||||
### Phase 5: Security Enhancements
|
|
||||||
|
|
||||||
#### 5.1 Input Validation Schemas
|
|
||||||
|
|
||||||
Create `src/lib/schemas/` with Zod schemas for all API endpoints:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// src/lib/schemas/project.js
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const createProjectSchema = z.object({
|
|
||||||
contract_id: z.number().int().positive(),
|
|
||||||
project_name: z.string().min(1).max(255),
|
|
||||||
project_number: z.string().min(1).max(50),
|
|
||||||
address: z.string().optional(),
|
|
||||||
// ... other fields
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateProjectSchema = createProjectSchema.partial();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5.2 Rate Limiting
|
|
||||||
|
|
||||||
Implement rate limiting for sensitive endpoints:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// src/lib/middleware/rateLimit.js
|
|
||||||
const attempts = new Map();
|
|
||||||
|
|
||||||
export function withRateLimit(
|
|
||||||
handler,
|
|
||||||
options = { maxAttempts: 5, windowMs: 15 * 60 * 1000 }
|
|
||||||
) {
|
|
||||||
return async (req, context) => {
|
|
||||||
const key = req.ip || "unknown";
|
|
||||||
const now = Date.now();
|
|
||||||
const window = attempts.get(key) || {
|
|
||||||
count: 0,
|
|
||||||
resetTime: now + options.windowMs,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (now > window.resetTime) {
|
|
||||||
window.count = 1;
|
|
||||||
window.resetTime = now + options.windowMs;
|
|
||||||
} else {
|
|
||||||
window.count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts.set(key, window);
|
|
||||||
|
|
||||||
if (window.count > options.maxAttempts) {
|
|
||||||
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler(req, context);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Checklist (Updated Status)
|
|
||||||
|
|
||||||
### ✅ Phase 1: Foundation - COMPLETED
|
|
||||||
|
|
||||||
- [x] Install dependencies (NextAuth.js v5, bcryptjs, zod)
|
|
||||||
- [x] Create environment configuration (.env.local)
|
|
||||||
- [x] Extend database schema (users, sessions, audit_logs)
|
|
||||||
- [x] Create initial admin user script
|
|
||||||
|
|
||||||
### ✅ Phase 2: Authentication - COMPLETED
|
|
||||||
|
|
||||||
- [x] Configure NextAuth.js with credentials provider
|
|
||||||
- [x] Create API route handlers (/api/auth/[...nextauth])
|
|
||||||
- [x] Implement user management system
|
|
||||||
- [x] Test login/logout functionality
|
|
||||||
|
|
||||||
### ✅ Phase 3: Authorization - COMPLETED
|
|
||||||
|
|
||||||
- [x] Implement API middleware (withAuth, role hierarchy)
|
|
||||||
- [x] Protect existing API routes (projects, contracts, tasks, notes)
|
|
||||||
- [x] Create role-based helper functions
|
|
||||||
- [x] Integrate session provider in app layout
|
|
||||||
|
|
||||||
### 🔄 Phase 4: User Interface - IN PROGRESS
|
|
||||||
|
|
||||||
- [x] Create sign-in page with form validation
|
|
||||||
- [x] Update navigation component with auth integration
|
|
||||||
- [x] Add role-based menu items
|
|
||||||
- [ ] Create sign-out confirmation page
|
|
||||||
- [ ] Create error handling page
|
|
||||||
- [ ] Create unauthorized access page
|
|
||||||
- [ ] Build admin user management interface
|
|
||||||
|
|
||||||
### ❌ Phase 5: Security Enhancements - NOT STARTED
|
|
||||||
|
|
||||||
- [ ] Add input validation schemas to all endpoints
|
|
||||||
- [ ] Implement rate limiting for sensitive operations
|
|
||||||
- [ ] Add comprehensive audit logging
|
|
||||||
- [ ] Create security headers middleware
|
|
||||||
- [ ] Implement CSRF protection
|
|
||||||
- [ ] Add password reset functionality
|
|
||||||
|
|
||||||
## Current Working Features
|
|
||||||
|
|
||||||
### 🔐 Authentication System
|
|
||||||
|
|
||||||
- **Login/Logout**: Fully functional with NextAuth.js
|
|
||||||
- **Session Management**: JWT-based with 30-day expiration
|
|
||||||
- **Password Security**: bcrypt hashing with salt rounds
|
|
||||||
- **Account Lockout**: 5 failed attempts = 15-minute lockout
|
|
||||||
- **Role System**: 4-tier hierarchy (admin, project_manager, user, read_only)
|
|
||||||
|
|
||||||
### 🛡️ Authorization System
|
|
||||||
|
|
||||||
- **API Protection**: All major endpoints require authentication
|
|
||||||
- **Role-Based Access**: Different permission levels per endpoint
|
|
||||||
- **Middleware**: Clean abstraction with helper functions
|
|
||||||
- **Session Validation**: Automatic session verification
|
|
||||||
|
|
||||||
### 📱 User Interface
|
|
||||||
|
|
||||||
- **Navigation**: Context-aware with user info and sign-out
|
|
||||||
- **Auth Pages**: Professional sign-in form with error handling
|
|
||||||
- **Role Integration**: Admin users see additional menu items
|
|
||||||
- **Responsive**: Works across device sizes
|
|
||||||
|
|
||||||
### 🗄️ Database Security
|
|
||||||
|
|
||||||
- **User Management**: Complete CRUD with proper validation
|
|
||||||
- **Audit Schema**: Ready for comprehensive logging
|
|
||||||
- **Indexes**: Optimized for performance
|
|
||||||
- **Constraints**: Role validation and data integrity
|
|
||||||
|
|
||||||
## Next Priority Tasks
|
|
||||||
|
|
||||||
1. **Complete Auth UI** (High Priority)
|
|
||||||
|
|
||||||
- Sign-out confirmation page
|
|
||||||
- Unauthorized access page
|
|
||||||
- Enhanced error handling
|
|
||||||
|
|
||||||
2. **Admin User Management** (High Priority)
|
|
||||||
|
|
||||||
- User listing interface
|
|
||||||
- Create/edit user forms
|
|
||||||
- Role assignment controls
|
|
||||||
|
|
||||||
3. **Security Enhancements** (Medium Priority)
|
|
||||||
|
|
||||||
- Input validation schemas
|
|
||||||
- Rate limiting middleware
|
|
||||||
- Comprehensive audit logging
|
|
||||||
|
|
||||||
4. **Password Management** (Medium Priority)
|
|
||||||
- Password reset functionality
|
|
||||||
- Password strength requirements
|
|
||||||
- Password change interface
|
|
||||||
|
|
||||||
## User Tracking in Projects - NEW FEATURE ✅
|
|
||||||
|
|
||||||
### 📊 Project User Management Implementation
|
|
||||||
|
|
||||||
We've successfully implemented comprehensive user tracking for projects:
|
|
||||||
|
|
||||||
#### Database Schema Updates ✅
|
|
||||||
|
|
||||||
- **created_by**: Tracks who created the project (user ID)
|
|
||||||
- **assigned_to**: Tracks who is assigned to work on the project (user ID)
|
|
||||||
- **created_at**: Timestamp when project was created
|
|
||||||
- **updated_at**: Timestamp when project was last modified
|
|
||||||
- **Indexes**: Performance optimized with proper foreign key indexes
|
|
||||||
|
|
||||||
#### API Enhancements ✅
|
|
||||||
|
|
||||||
- **Enhanced Queries**: Projects now include user names and emails via JOIN operations
|
|
||||||
- **User Assignment**: New `/api/projects/users` endpoint for user management
|
|
||||||
- **Query Filters**: Support for filtering projects by assigned user or creator
|
|
||||||
- **User Context**: Create/update operations automatically capture authenticated user ID
|
|
||||||
|
|
||||||
#### UI Components ✅
|
|
||||||
|
|
||||||
- **Project Form**: User assignment dropdown in create/edit forms
|
|
||||||
- **Project Listing**: "Created By" and "Assigned To" columns in project table
|
|
||||||
- **User Selection**: Dropdown populated with active users for assignment
|
|
||||||
|
|
||||||
#### New Query Functions ✅
|
|
||||||
|
|
||||||
- `getAllUsersForAssignment()`: Get active users for assignment dropdown
|
|
||||||
- `getProjectsByAssignedUser(userId)`: Filter projects by assignee
|
|
||||||
- `getProjectsByCreator(userId)`: Filter projects by creator
|
|
||||||
- `updateProjectAssignment(projectId, userId)`: Update project assignment
|
|
||||||
|
|
||||||
#### Security Integration ✅
|
|
||||||
|
|
||||||
- **Authentication Required**: All user operations require valid session
|
|
||||||
- **Role-Based Access**: User assignment respects role hierarchy
|
|
||||||
- **Audit Ready**: Infrastructure prepared for comprehensive user action logging
|
|
||||||
|
|
||||||
### Usage Examples
|
|
||||||
|
|
||||||
#### Creating Projects with User Tracking
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Projects are automatically assigned to the authenticated user as creator
|
|
||||||
POST /api/projects
|
|
||||||
{
|
|
||||||
"project_name": "New Project",
|
|
||||||
"assigned_to": "user-id-here", // Optional assignment
|
|
||||||
// ... other project data
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Filtering Projects by User
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Get projects assigned to specific user
|
|
||||||
GET /api/projects?assigned_to=user-id
|
|
||||||
|
|
||||||
// Get projects created by specific user
|
|
||||||
GET /api/projects?created_by=user-id
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Updating Project Assignment
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
POST /api/projects/users
|
|
||||||
{
|
|
||||||
"projectId": 123,
|
|
||||||
"assignedToUserId": "new-user-id"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Tasks User Tracking - NEW FEATURE ✅
|
|
||||||
|
|
||||||
### 📋 Task User Management Implementation
|
|
||||||
|
|
||||||
We've also implemented comprehensive user tracking for project tasks:
|
|
||||||
|
|
||||||
#### Database Schema Updates ✅
|
|
||||||
|
|
||||||
- **created_by**: Tracks who created the task (user ID)
|
|
||||||
- **assigned_to**: Tracks who is assigned to work on the task (user ID)
|
|
||||||
- **created_at**: Timestamp when task was created
|
|
||||||
- **updated_at**: Timestamp when task was last modified
|
|
||||||
- **Indexes**: Performance optimized with proper foreign key indexes
|
|
||||||
|
|
||||||
#### API Enhancements ✅
|
|
||||||
|
|
||||||
- **Enhanced Queries**: Tasks now include user names and emails via JOIN operations
|
|
||||||
- **User Assignment**: New `/api/project-tasks/users` endpoint for user management
|
|
||||||
- **Query Filters**: Support for filtering tasks by assigned user or creator
|
|
||||||
- **User Context**: Create/update operations automatically capture authenticated user ID
|
|
||||||
|
|
||||||
#### UI Components ✅
|
|
||||||
|
|
||||||
- **Task Form**: User assignment dropdown in create task forms
|
|
||||||
- **Task Listing**: "Created By" and "Assigned To" columns in task table
|
|
||||||
- **User Selection**: Dropdown populated with active users for assignment
|
|
||||||
|
|
||||||
#### New Task Query Functions ✅
|
|
||||||
|
|
||||||
- `getAllUsersForTaskAssignment()`: Get active users for assignment dropdown
|
|
||||||
- `getProjectTasksByAssignedUser(userId)`: Filter tasks by assignee
|
|
||||||
- `getProjectTasksByCreator(userId)`: Filter tasks by creator
|
|
||||||
- `updateProjectTaskAssignment(taskId, userId)`: Update task assignment
|
|
||||||
|
|
||||||
#### Task Creation Behavior ✅
|
|
||||||
|
|
||||||
- **Auto-assignment**: Tasks are automatically assigned to the authenticated user as creator
|
|
||||||
- **Optional Assignment**: Users can assign tasks to other team members during creation
|
|
||||||
- **Creator Tracking**: All tasks track who created them for accountability
|
|
||||||
|
|
||||||
### Task Usage Examples
|
|
||||||
|
|
||||||
#### Creating Tasks with User Tracking
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Tasks are automatically assigned to the authenticated user as creator
|
|
||||||
POST /api/project-tasks
|
|
||||||
{
|
|
||||||
"project_id": 123,
|
|
||||||
"task_template_id": 1, // or custom_task_name for custom tasks
|
|
||||||
"assigned_to": "user-id-here", // Optional, defaults to creator
|
|
||||||
"priority": "high"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Filtering Tasks by User
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Get tasks assigned to specific user
|
|
||||||
GET /api/project-tasks?assigned_to=user-id
|
|
||||||
|
|
||||||
// Get tasks created by specific user
|
|
||||||
GET /api/project-tasks?created_by=user-id
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Updating Task Assignment
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
POST /api/project-tasks/users
|
|
||||||
{
|
|
||||||
"taskId": 456,
|
|
||||||
"assignedToUserId": "new-user-id"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Next Enhancements
|
|
||||||
|
|
||||||
1. **Dashboard Views** (Recommended)
|
|
||||||
|
|
||||||
- "My Projects" dashboard showing assigned projects
|
|
||||||
- Project creation history per user
|
|
||||||
- Workload distribution reports
|
|
||||||
|
|
||||||
2. **Advanced Filtering** (Future)
|
|
||||||
|
|
||||||
- Multi-user assignment support
|
|
||||||
- Team-based project assignments
|
|
||||||
- Role-based project visibility
|
|
||||||
|
|
||||||
3. **Notifications** (Future)
|
|
||||||
- Email alerts on project assignment
|
|
||||||
- Deadline reminders for assigned users
|
|
||||||
- Status change notifications
|
|
||||||
|
|
||||||
## Notes User Tracking - NEW FEATURE ✅
|
|
||||||
|
|
||||||
### 📝 Notes User Management Implementation
|
|
||||||
|
|
||||||
We've also implemented comprehensive user tracking for all notes (both project notes and task notes):
|
|
||||||
|
|
||||||
#### Database Schema Updates ✅
|
|
||||||
|
|
||||||
- **created_by**: Tracks who created the note (user ID)
|
|
||||||
- **is_system**: Distinguishes between user notes and system-generated notes
|
|
||||||
- **Enhanced queries**: Notes now include user names and emails via JOIN operations
|
|
||||||
- **Indexes**: Performance optimized with proper indexes for user lookups
|
|
||||||
|
|
||||||
#### API Enhancements ✅
|
|
||||||
|
|
||||||
- **User Context**: All note creation operations automatically capture authenticated user ID
|
|
||||||
- **System Notes**: Automatic system notes (task status changes) track who made the change
|
|
||||||
- **User Information**: Note retrieval includes creator name and email for display
|
|
||||||
|
|
||||||
#### UI Components ✅
|
|
||||||
|
|
||||||
- **Project Notes**: Display creator name and email in project note listings
|
|
||||||
- **Task Notes**: Show who added each note with user badges and timestamps
|
|
||||||
- **System Notes**: Distinguished from user notes with special styling and "System" badge
|
|
||||||
- **User Attribution**: Clear indication of who created each note and when
|
|
||||||
|
|
||||||
#### New Note Query Functions ✅
|
|
||||||
|
|
||||||
- `getAllNotesWithUsers()`: Get all notes with user and project/task context
|
|
||||||
- `getNotesByCreator(userId)`: Filter notes by creator for user activity tracking
|
|
||||||
- Enhanced `getNotesByProjectId()` and `getNotesByTaskId()` with user information
|
|
||||||
|
|
||||||
#### Automatic User Tracking ✅
|
|
||||||
|
|
||||||
- **Note Creation**: All new notes automatically record who created them
|
|
||||||
- **System Notes**: Task status changes generate system notes attributed to the user who made the change
|
|
||||||
- **Audit Trail**: Complete history of who added what notes and when
|
|
||||||
|
|
||||||
### Notes Usage Examples
|
|
||||||
|
|
||||||
#### Project Notes with User Tracking
|
|
||||||
|
|
||||||
- Notes display creator name in a blue badge next to the timestamp
|
|
||||||
- Form automatically associates notes with the authenticated user
|
|
||||||
- Clear visual distinction between different note authors
|
|
||||||
|
|
||||||
#### Task Notes with User Tracking
|
|
||||||
|
|
||||||
- User notes show creator name in a gray badge
|
|
||||||
- System notes show "System" badge but also track the user who triggered the action
|
|
||||||
- Full audit trail of task status changes and who made them
|
|
||||||
|
|
||||||
#### System Note Generation
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// When a user changes a task status, a system note is automatically created:
|
|
||||||
// "Status changed from 'pending' to 'in_progress'" - attributed to the user who made the change
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
|
|
||||||
1. **Accountability**: Full audit trail of who added what notes
|
|
||||||
2. **Context**: Know who to contact for clarification on specific notes
|
|
||||||
3. **History**: Track communication and decisions made by team members
|
|
||||||
4. **System Integration**: Automatic notes for system actions still maintain user attribution
|
|
||||||
5. **User Experience**: Clear visual indicators of note authors improve team collaboration
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
# Quick Deployment Guide - Timezone Fix
|
|
||||||
|
|
||||||
## For Production Server
|
|
||||||
|
|
||||||
1. **SSH into your server** where Docker is running
|
|
||||||
|
|
||||||
2. **Navigate to project directory**
|
|
||||||
```bash
|
|
||||||
cd /path/to/panel
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Pull latest code** (includes timezone fixes)
|
|
||||||
```bash
|
|
||||||
git pull origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Stop running containers**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Rebuild Docker images** (this is critical - it bakes in the timezone configuration)
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml build --no-cache
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Start containers**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
7. **Verify timezone is correct**
|
|
||||||
```bash
|
|
||||||
# Check container timezone
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app date
|
|
||||||
# Should show Polish time with CEST/CET timezone
|
|
||||||
|
|
||||||
# Example output:
|
|
||||||
# Sat Oct 4 19:45:00 CEST 2025
|
|
||||||
```
|
|
||||||
|
|
||||||
8. **Test the fix**
|
|
||||||
- Post a new note at a known time (e.g., 19:45)
|
|
||||||
- Verify it displays the same time (19:45)
|
|
||||||
- Test both project notes and task notes
|
|
||||||
|
|
||||||
## What Changed
|
|
||||||
|
|
||||||
### Code Changes
|
|
||||||
- ✅ Fixed `datetime('now', 'localtime')` in all database queries
|
|
||||||
- ✅ Updated display formatters to use Europe/Warsaw timezone
|
|
||||||
- ✅ Fixed note display in components
|
|
||||||
|
|
||||||
### Docker Changes (Critical!)
|
|
||||||
- ✅ Set `ENV TZ=Europe/Warsaw` in Dockerfile
|
|
||||||
- ✅ Configured system timezone in containers
|
|
||||||
- ✅ Added TZ environment variable to docker-compose files
|
|
||||||
|
|
||||||
## Why Rebuild is Necessary
|
|
||||||
|
|
||||||
The timezone configuration is **baked into the Docker image** during build time:
|
|
||||||
- `ENV TZ=Europe/Warsaw` - Set during image build
|
|
||||||
- `RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime` - Executed during image build
|
|
||||||
|
|
||||||
Just restarting containers (`docker-compose restart`) will **NOT** apply these changes!
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### If times are still wrong after deployment:
|
|
||||||
|
|
||||||
1. **Verify you rebuilt the images**
|
|
||||||
```bash
|
|
||||||
docker images | grep panel
|
|
||||||
# Check the "CREATED" timestamp - should be recent
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Check if container has correct timezone**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app date
|
|
||||||
```
|
|
||||||
Should show Polish time, not UTC!
|
|
||||||
|
|
||||||
3. **Check SQLite is using correct time**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app node -e "const db = require('better-sqlite3')('./data/database.sqlite'); console.log(db.prepare(\"SELECT datetime('now', 'localtime') as time\").get());"
|
|
||||||
```
|
|
||||||
Should show current Polish time
|
|
||||||
|
|
||||||
4. **Force rebuild if needed**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml down
|
|
||||||
docker system prune -f
|
|
||||||
docker-compose -f docker-compose.prod.yml build --no-cache
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Expected Behavior After Fix
|
|
||||||
|
|
||||||
### Before Fix (Docker in UTC):
|
|
||||||
```
|
|
||||||
User posts note at 10:30 Poland time
|
|
||||||
→ Docker sees 08:30 UTC as "local time"
|
|
||||||
→ SQLite stores: 08:30
|
|
||||||
→ Display shows: 08:30 ❌ (2 hours off!)
|
|
||||||
```
|
|
||||||
|
|
||||||
### After Fix (Docker in Europe/Warsaw):
|
|
||||||
```
|
|
||||||
User posts note at 10:30 Poland time
|
|
||||||
→ Docker sees 10:30 Poland time as "local time"
|
|
||||||
→ SQLite stores: 10:30
|
|
||||||
→ Display shows: 10:30 ✅ (correct!)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
1. **Old notes**: Notes created before this fix may still show incorrect times (they were stored in UTC)
|
|
||||||
2. **New notes**: All new notes after deployment will show correct times
|
|
||||||
3. **Audit logs**: Continue to work correctly (they always used ISO format)
|
|
||||||
4. **Zero downtime**: Can't achieve - need to stop/rebuild/start containers
|
|
||||||
|
|
||||||
## Quick Check Command
|
|
||||||
|
|
||||||
After deployment, run this one-liner to verify everything:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app sh -c 'date && node -e "console.log(new Date().toLocaleString(\"pl-PL\"))"'
|
|
||||||
```
|
|
||||||
|
|
||||||
Both outputs should show the same Polish time!
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# Docker Timezone Configuration Fix
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
Even after fixing the SQLite `datetime('now', 'localtime')` calls, notes posted at 10:00 still showed as 08:00 when running in Docker.
|
|
||||||
|
|
||||||
## Root Cause
|
|
||||||
**Docker containers run in UTC timezone by default!**
|
|
||||||
|
|
||||||
When using `datetime('now', 'localtime')` in SQLite:
|
|
||||||
- On local Windows machine: Uses Windows timezone (Europe/Warsaw) → ✅ Correct
|
|
||||||
- In Docker container: Uses container timezone (UTC) → ❌ Wrong by 2 hours
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```
|
|
||||||
User posts at 10:00 Poland time (UTC+2)
|
|
||||||
↓
|
|
||||||
Docker container thinks local time is 08:00 UTC
|
|
||||||
↓
|
|
||||||
SQLite datetime('now', 'localtime') stores: 08:00
|
|
||||||
↓
|
|
||||||
Display shows: 08:00 (wrong!)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Solution
|
|
||||||
Set the Docker container timezone to Europe/Warsaw
|
|
||||||
|
|
||||||
### 1. Updated Dockerfile (Production)
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Use Node.js 22.11.0 as the base image
|
|
||||||
FROM node:22.11.0
|
|
||||||
|
|
||||||
# Set timezone to Europe/Warsaw (Polish timezone)
|
|
||||||
ENV TZ=Europe/Warsaw
|
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
|
||||||
|
|
||||||
# ... rest of Dockerfile
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Updated Dockerfile.dev (Development)
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Use Node.js 22.11.0 as the base image
|
|
||||||
FROM node:22.11.0
|
|
||||||
|
|
||||||
# Set timezone to Europe/Warsaw (Polish timezone)
|
|
||||||
ENV TZ=Europe/Warsaw
|
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
|
||||||
|
|
||||||
# ... rest of Dockerfile
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Updated docker-compose.yml (Development)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- TZ=Europe/Warsaw
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Updated docker-compose.prod.yml (Production)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
- TZ=Europe/Warsaw
|
|
||||||
- NEXTAUTH_SECRET=...
|
|
||||||
- NEXTAUTH_URL=...
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to Apply
|
|
||||||
|
|
||||||
### Option 1: Rebuild Docker Images
|
|
||||||
```bash
|
|
||||||
# Stop containers
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Rebuild images
|
|
||||||
docker-compose build --no-cache
|
|
||||||
|
|
||||||
# Start containers
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: For Production Deployment
|
|
||||||
```bash
|
|
||||||
# Pull latest code with fixes
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# Rebuild production image
|
|
||||||
docker-compose -f docker-compose.prod.yml build --no-cache
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
After rebuilding and restarting, verify timezone inside container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check timezone
|
|
||||||
docker exec -it <container_name> date
|
|
||||||
# Should show: Sat Oct 4 19:00:00 CEST 2025
|
|
||||||
|
|
||||||
# Check Node.js sees correct timezone
|
|
||||||
docker exec -it <container_name> node -e "console.log(new Date().toLocaleString('pl-PL', {timeZone: 'Europe/Warsaw'}))"
|
|
||||||
# Should show current Polish time
|
|
||||||
|
|
||||||
# Check SQLite sees correct timezone
|
|
||||||
docker exec -it <container_name> node -e "const db = require('better-sqlite3')('./data/database.sqlite'); console.log(db.prepare(\"SELECT datetime('now', 'localtime')\").get());"
|
|
||||||
# Should show current Polish time
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why This Works
|
|
||||||
|
|
||||||
1. **TZ Environment Variable**: Tells all processes (including Node.js and SQLite) what timezone to use
|
|
||||||
2. **Symlink /etc/localtime**: Updates system timezone for the entire container
|
|
||||||
3. **echo TZ > /etc/timezone**: Ensures the timezone persists
|
|
||||||
|
|
||||||
Now when SQLite uses `datetime('now', 'localtime')`:
|
|
||||||
- Container local time is 10:00 Poland time
|
|
||||||
- SQLite stores: 10:00
|
|
||||||
- Display shows: 10:00 ✅
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
1. **Must rebuild images**: Just restarting containers is not enough - the timezone configuration is baked into the image
|
|
||||||
2. **All existing data**: Old notes will still show incorrect times (they were stored in UTC)
|
|
||||||
3. **New notes**: Will now display correctly
|
|
||||||
4. **DST handling**: Europe/Warsaw automatically handles Daylight Saving Time transitions
|
|
||||||
|
|
||||||
## Alternative Approach (Not Recommended)
|
|
||||||
|
|
||||||
Instead of changing container timezone, you could:
|
|
||||||
1. Store everything in UTC (like audit logs do with ISO format)
|
|
||||||
2. Always convert on display
|
|
||||||
|
|
||||||
But this requires more code changes and the current approach is simpler and more maintainable.
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
1. `Dockerfile` - Added TZ configuration
|
|
||||||
2. `Dockerfile.dev` - Added TZ configuration
|
|
||||||
3. `docker-compose.yml` - Added TZ environment variable
|
|
||||||
4. `docker-compose.prod.yml` - Added TZ environment variable
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
After deployment:
|
|
||||||
- [ ] Container shows correct date/time with `docker exec <container> date`
|
|
||||||
- [ ] Post a new note at known time (e.g., 10:30)
|
|
||||||
- [ ] Verify note displays the same time (10:30)
|
|
||||||
- [ ] Check both project notes and task notes
|
|
||||||
- [ ] Verify audit logs still work correctly
|
|
||||||
- [ ] Check task timestamps (date_started, date_completed)
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
# ✅ Dropdown Consolidation - COMPLETED
|
|
||||||
|
|
||||||
## Summary of Changes
|
|
||||||
|
|
||||||
The project management interface has been successfully updated to eliminate redundant status displays by consolidating status badges and dropdowns into unified interactive components.
|
|
||||||
|
|
||||||
## ✅ Components Successfully Updated
|
|
||||||
|
|
||||||
### Task Status Dropdowns:
|
|
||||||
|
|
||||||
- **ProjectTasksSection.js** → TaskStatusDropdownSimple ✅
|
|
||||||
- **Tasks page** (`/tasks`) → TaskStatusDropdownSimple ✅
|
|
||||||
- **ProjectTasksDashboard.js** → TaskStatusDropdownSimple ✅
|
|
||||||
- **Main Dashboard** (`/`) → TaskStatusDropdownSimple ✅ (read-only mode)
|
|
||||||
|
|
||||||
### Status Configurations:
|
|
||||||
|
|
||||||
#### Task Statuses:
|
|
||||||
|
|
||||||
- `pending` → Warning (yellow)
|
|
||||||
- `in_progress` → Primary (blue)
|
|
||||||
- `completed` → Success (green)
|
|
||||||
- `cancelled` → Danger (red)
|
|
||||||
|
|
||||||
#### Project Statuses:
|
|
||||||
|
|
||||||
- `registered` → Secondary (gray)
|
|
||||||
- `in_progress_design` → Primary (blue)
|
|
||||||
- `in_progress_construction` → Primary (blue)
|
|
||||||
- `fulfilled` → Success (green)
|
|
||||||
|
|
||||||
## 🎯 Key Features Implemented
|
|
||||||
|
|
||||||
### Unified Interface:
|
|
||||||
|
|
||||||
- Single component serves as both status display and edit interface
|
|
||||||
- Click to expand dropdown with available status options
|
|
||||||
- Visual feedback with arrow rotation and hover effects
|
|
||||||
- Loading states during API updates
|
|
||||||
|
|
||||||
### Debug Features (Current):
|
|
||||||
|
|
||||||
- Red borders around dropdowns for visibility testing
|
|
||||||
- Yellow debug headers showing component type
|
|
||||||
- Console logging for click events and API calls
|
|
||||||
- Semi-transparent backdrop for easy identification
|
|
||||||
|
|
||||||
### Z-Index Solution:
|
|
||||||
|
|
||||||
- Dropdown: `z-[9999]` (maximum priority)
|
|
||||||
- Backdrop: `z-[9998]` (behind dropdown)
|
|
||||||
|
|
||||||
## 🧪 Testing Instructions
|
|
||||||
|
|
||||||
### 1. Access Test Pages:
|
|
||||||
|
|
||||||
```
|
|
||||||
http://localhost:3000/test-dropdowns # Isolated component testing
|
|
||||||
http://localhost:3000/projects # Project list with status dropdowns
|
|
||||||
http://localhost:3000/tasks # Task list with status dropdowns
|
|
||||||
http://localhost:3000/ # Main dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Standalone HTML Tests:
|
|
||||||
|
|
||||||
```
|
|
||||||
test-dropdown-comprehensive.html # Complete functionality test
|
|
||||||
test-dropdown.html # Basic dropdown structure test
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Checklist:
|
|
||||||
|
|
||||||
- [ ] Dropdowns appear immediately when clicked
|
|
||||||
- [ ] Red borders and debug headers are visible
|
|
||||||
- [ ] Dropdowns appear above all other elements
|
|
||||||
- [ ] Clicking outside closes dropdowns
|
|
||||||
- [ ] Dropdowns work properly in table contexts
|
|
||||||
- [ ] API calls update status correctly
|
|
||||||
- [ ] Loading states show during updates
|
|
||||||
- [ ] Error handling reverts status on failure
|
|
||||||
|
|
||||||
## 📁 Files Created/Modified
|
|
||||||
|
|
||||||
### New Components:
|
|
||||||
|
|
||||||
- `src/components/TaskStatusDropdownSimple.js` ✅
|
|
||||||
- `src/components/ProjectStatusDropdownSimple.js` ✅
|
|
||||||
- `src/app/test-dropdowns/page.js` ✅
|
|
||||||
|
|
||||||
### Updated Components:
|
|
||||||
|
|
||||||
- `src/components/ProjectTasksSection.js` ✅
|
|
||||||
- `src/app/tasks/page.js` ✅
|
|
||||||
- `src/components/ProjectTasksDashboard.js` ✅
|
|
||||||
- `src/app/page.js` ✅
|
|
||||||
|
|
||||||
### Test Files:
|
|
||||||
|
|
||||||
- `test-dropdown-comprehensive.html` ✅
|
|
||||||
- `test-dropdown.html` ✅
|
|
||||||
|
|
||||||
### Documentation:
|
|
||||||
|
|
||||||
- `DROPDOWN_IMPLEMENTATION_SUMMARY.md` ✅
|
|
||||||
- `DROPDOWN_COMPLETION_STATUS.md` ✅ (this file)
|
|
||||||
|
|
||||||
## 🚀 Next Steps (Production Polish)
|
|
||||||
|
|
||||||
### 1. Remove Debug Features:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Remove these debug elements:
|
|
||||||
- Red borders (border-2 border-red-500)
|
|
||||||
- Yellow debug headers
|
|
||||||
- Console.log statements
|
|
||||||
- Semi-transparent backdrop styling
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Final Styling:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Replace debug styles with:
|
|
||||||
border border-gray-200 // Subtle borders
|
|
||||||
shadow-lg // Professional shadows
|
|
||||||
Clean backdrop (transparent)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Performance Optimization:
|
|
||||||
|
|
||||||
- Consider portal-based positioning for complex table layouts
|
|
||||||
- Add keyboard navigation (Enter/Escape keys)
|
|
||||||
- Implement click-outside using refs instead of global listeners
|
|
||||||
|
|
||||||
### 4. Code Cleanup:
|
|
||||||
|
|
||||||
- Remove original TaskStatusDropdown.js and ProjectStatusDropdown.js
|
|
||||||
- Rename Simple components to drop "Simple" suffix
|
|
||||||
- Update import statements across application
|
|
||||||
|
|
||||||
## ✅ Success Criteria Met
|
|
||||||
|
|
||||||
1. **Redundant UI Eliminated**: ✅ Single component replaces badge + dropdown pairs
|
|
||||||
2. **Z-Index Issues Resolved**: ✅ Dropdowns appear above all elements
|
|
||||||
3. **Table Compatibility**: ✅ Works properly in table/overflow contexts
|
|
||||||
4. **API Integration**: ✅ Status updates via PATCH/PUT requests
|
|
||||||
5. **Error Handling**: ✅ Reverts status on API failures
|
|
||||||
6. **Loading States**: ✅ Shows "Updating..." during API calls
|
|
||||||
7. **Consistent Styling**: ✅ Unified design patterns across components
|
|
||||||
|
|
||||||
## 🎉 Project Status: READY FOR TESTING
|
|
||||||
|
|
||||||
The dropdown consolidation is complete and ready for user testing. All components have been updated to use the simplified, working versions with debug features enabled for validation.
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# Dropdown Consolidation - Implementation Summary
|
|
||||||
|
|
||||||
## Problem Identified
|
|
||||||
|
|
||||||
The project management interface had redundant status displays where both a status badge and a dropdown showing the same status information were displayed together. Additionally, there was a z-index issue where dropdowns appeared behind other elements.
|
|
||||||
|
|
||||||
## Solution Implemented
|
|
||||||
|
|
||||||
### 1. Created Unified Dropdown Components
|
|
||||||
|
|
||||||
#### TaskStatusDropdown Components:
|
|
||||||
|
|
||||||
- **TaskStatusDropdown.js** - Original enhanced component with portal positioning (currently has complexity issues)
|
|
||||||
- **TaskStatusDropdownSimple.js** - ✅ Simplified working version for testing
|
|
||||||
|
|
||||||
#### ProjectStatusDropdown Components:
|
|
||||||
|
|
||||||
- **ProjectStatusDropdown.js** - Original enhanced component with portal positioning (currently has complexity issues)
|
|
||||||
- **ProjectStatusDropdownSimple.js** - ✅ Simplified working version for testing
|
|
||||||
|
|
||||||
### 2. Key Features of Unified Components
|
|
||||||
|
|
||||||
#### Interactive Status Display:
|
|
||||||
|
|
||||||
- Single component serves as both status badge and dropdown
|
|
||||||
- Click to expand dropdown with status options
|
|
||||||
- Visual feedback (arrow rotation, hover effects)
|
|
||||||
- Loading states during API calls
|
|
||||||
|
|
||||||
#### Debugging Features (Current Implementation):
|
|
||||||
|
|
||||||
- Console logging for click events
|
|
||||||
- Visible red border around dropdown for testing
|
|
||||||
- Yellow debug header showing dropdown is visible
|
|
||||||
- Semi-transparent backdrop for easy identification
|
|
||||||
|
|
||||||
#### API Integration:
|
|
||||||
|
|
||||||
- TaskStatusDropdown: PATCH `/api/project-tasks/{id}`
|
|
||||||
- ProjectStatusDropdown: PUT `/api/projects/{id}`
|
|
||||||
- Callback support for parent component refresh
|
|
||||||
- Error handling with status reversion
|
|
||||||
|
|
||||||
### 3. Updated Components
|
|
||||||
|
|
||||||
#### Currently Using Simplified Version:
|
|
||||||
|
|
||||||
- ✅ **ProjectTasksSection.js** - Task table uses TaskStatusDropdownSimple
|
|
||||||
- ✅ **Test page created** - `/test-dropdowns` for isolated testing
|
|
||||||
|
|
||||||
#### Still Using Original (Need to Update):
|
|
||||||
|
|
||||||
- **ProjectTasksPage** (`/tasks`) - Uses TaskStatusDropdown
|
|
||||||
- **ProjectTasksDashboard** - Uses TaskStatusDropdown
|
|
||||||
- **Main Dashboard** (`/`) - Uses TaskStatusDropdown (read-only mode)
|
|
||||||
- **Project Detail Pages** - Uses ProjectStatusDropdown
|
|
||||||
|
|
||||||
### 4. Configuration
|
|
||||||
|
|
||||||
#### Task Status Options:
|
|
||||||
|
|
||||||
- `pending` → Warning variant (yellow)
|
|
||||||
- `in_progress` → Primary variant (blue)
|
|
||||||
- `completed` → Success variant (green)
|
|
||||||
- `cancelled` → Danger variant (red)
|
|
||||||
|
|
||||||
#### Project Status Options:
|
|
||||||
|
|
||||||
- `registered` → Secondary variant (gray)
|
|
||||||
- `in_progress_design` → Primary variant (blue)
|
|
||||||
- `in_progress_construction` → Primary variant (blue)
|
|
||||||
- `fulfilled` → Success variant (green)
|
|
||||||
|
|
||||||
### 5. Z-Index Solution
|
|
||||||
|
|
||||||
- Dropdown: `z-[9999]` (maximum visibility)
|
|
||||||
- Backdrop: `z-[9998]` (behind dropdown)
|
|
||||||
|
|
||||||
## Current Status
|
|
||||||
|
|
||||||
### ✅ Working:
|
|
||||||
|
|
||||||
- Simplified dropdown components compile without errors
|
|
||||||
- Basic dropdown structure and styling
|
|
||||||
- Debug features for testing
|
|
||||||
- Test page available at `/test-dropdowns`
|
|
||||||
|
|
||||||
### 🚧 In Progress:
|
|
||||||
|
|
||||||
- Testing dropdown visibility in browser
|
|
||||||
- Development server startup (terminal access issues)
|
|
||||||
|
|
||||||
### 📋 Next Steps:
|
|
||||||
|
|
||||||
1. **Test Simplified Components**
|
|
||||||
|
|
||||||
- Verify dropdowns appear correctly
|
|
||||||
- Test click interactions
|
|
||||||
- Confirm API calls work
|
|
||||||
|
|
||||||
2. **Replace Original Components**
|
|
||||||
|
|
||||||
- Update remaining pages to use simplified versions
|
|
||||||
- Remove complex portal/positioning code if simple version works
|
|
||||||
|
|
||||||
3. **Production Polish**
|
|
||||||
|
|
||||||
- Remove debug features (red borders, console logs)
|
|
||||||
- Fine-tune styling and positioning
|
|
||||||
- Add portal-based positioning if needed for table overflow
|
|
||||||
|
|
||||||
4. **Code Cleanup**
|
|
||||||
- Remove unused original components
|
|
||||||
- Clean up imports across all files
|
|
||||||
|
|
||||||
## Testing Instructions
|
|
||||||
|
|
||||||
1. **Access Test Page**: Navigate to `/test-dropdowns`
|
|
||||||
2. **Check Console**: Open browser dev tools (F12) → Console tab
|
|
||||||
3. **Test Interactions**: Click dropdowns to see debug messages
|
|
||||||
4. **Verify Visibility**: Look for red-bordered dropdowns with yellow debug headers
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
### New Components:
|
|
||||||
|
|
||||||
- `src/components/TaskStatusDropdownSimple.js`
|
|
||||||
- `src/components/ProjectStatusDropdownSimple.js`
|
|
||||||
- `src/app/test-dropdowns/page.js`
|
|
||||||
|
|
||||||
### Updated Components:
|
|
||||||
|
|
||||||
- `src/components/ProjectTasksSection.js` (using simple version)
|
|
||||||
- `src/components/TaskStatusDropdown.js` (enhanced but problematic)
|
|
||||||
- `src/components/ProjectStatusDropdown.js` (enhanced but problematic)
|
|
||||||
|
|
||||||
### Test Files:
|
|
||||||
|
|
||||||
- `test-dropdown.html` (standalone HTML test)
|
|
||||||
- `start-dev.bat` (development server script)
|
|
||||||
|
|
||||||
The consolidation successfully eliminates duplicate status displays and provides a unified interface for status management across the application.
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
# Edge Runtime Compatibility Fix - Final Solution
|
|
||||||
|
|
||||||
## Problem Resolved
|
|
||||||
|
|
||||||
The audit logging system was causing "Edge runtime does not support Node.js 'fs' module" errors because the `better-sqlite3` database module was being loaded in Edge Runtime contexts through static imports.
|
|
||||||
|
|
||||||
## Root Cause
|
|
||||||
|
|
||||||
The middleware imports `auth.js` → which imported `auditLog.js` → which had a static import of `db.js` → which imports `better-sqlite3`. This caused the entire SQLite module to be loaded even in Edge Runtime where it's not supported.
|
|
||||||
|
|
||||||
## Final Solution
|
|
||||||
|
|
||||||
### 1. Created Safe Audit Logging Module
|
|
||||||
|
|
||||||
**File: `src/lib/auditLogSafe.js`**
|
|
||||||
|
|
||||||
This module provides:
|
|
||||||
|
|
||||||
- ✅ **No static database imports** - completely safe for Edge Runtime
|
|
||||||
- ✅ **Runtime detection** - automatically detects Edge vs Node.js
|
|
||||||
- ✅ **Graceful fallbacks** - console logging in Edge, database in Node.js
|
|
||||||
- ✅ **Constants always available** - `AUDIT_ACTIONS` and `RESOURCE_TYPES`
|
|
||||||
- ✅ **Async/await support** - works with modern API patterns
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Safe import that never causes Edge Runtime errors
|
|
||||||
import {
|
|
||||||
logAuditEventSafe,
|
|
||||||
AUDIT_ACTIONS,
|
|
||||||
RESOURCE_TYPES,
|
|
||||||
} from "./auditLogSafe.js";
|
|
||||||
|
|
||||||
// Works in any runtime
|
|
||||||
await logAuditEventSafe({
|
|
||||||
action: AUDIT_ACTIONS.LOGIN,
|
|
||||||
userId: "user123",
|
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Updated All Imports
|
|
||||||
|
|
||||||
**Files Updated:**
|
|
||||||
|
|
||||||
- `src/lib/auth.js` - Authentication logging
|
|
||||||
- `src/app/api/projects/route.js` - Project operations
|
|
||||||
- `src/app/api/projects/[id]/route.js` - Individual project operations
|
|
||||||
- `src/app/api/notes/route.js` - Note operations
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { logApiAction, AUDIT_ACTIONS } from "@/lib/auditLog.js"; // ❌ Causes Edge Runtime errors
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { logApiActionSafe, AUDIT_ACTIONS } from "@/lib/auditLogSafe.js"; // ✅ Edge Runtime safe
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Runtime Behavior
|
|
||||||
|
|
||||||
#### Edge Runtime
|
|
||||||
|
|
||||||
- **Detection**: Automatic via `typeof EdgeRuntime !== 'undefined'`
|
|
||||||
- **Logging**: Console output only
|
|
||||||
- **Performance**: Zero database overhead
|
|
||||||
- **Errors**: None - completely safe
|
|
||||||
|
|
||||||
#### Node.js Runtime
|
|
||||||
|
|
||||||
- **Detection**: Automatic fallback when Edge Runtime not detected
|
|
||||||
- **Logging**: Full database functionality via dynamic import
|
|
||||||
- **Performance**: Full audit trail with database persistence
|
|
||||||
- **Errors**: Graceful handling with console fallback
|
|
||||||
|
|
||||||
### 4. Migration Pattern
|
|
||||||
|
|
||||||
The safe module uses a smart delegation pattern:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// In Edge Runtime: Console logging only
|
|
||||||
console.log(`[Audit] ${action} by user ${userId}`);
|
|
||||||
|
|
||||||
// In Node.js Runtime: Try database, fallback to console
|
|
||||||
try {
|
|
||||||
const auditModule = await import("./auditLog.js");
|
|
||||||
auditModule.logAuditEvent({ ...params });
|
|
||||||
} catch (dbError) {
|
|
||||||
console.log("[Audit] Database logging failed, using console fallback");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/lib/
|
|
||||||
├── auditLog.js # Original - Node.js only (database operations)
|
|
||||||
├── auditLogSafe.js # New - Universal (Edge + Node.js compatible)
|
|
||||||
├── auditLogEdge.js # Alternative - Edge-specific with API calls
|
|
||||||
└── auth.js # Updated to use safe imports
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Run the compatibility test:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node test-safe-audit-logging.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Output:**
|
|
||||||
|
|
||||||
```
|
|
||||||
✅ Safe module imported successfully
|
|
||||||
✅ Edge Runtime logging successful (console only)
|
|
||||||
✅ Node.js Runtime logging successful (database + console)
|
|
||||||
✅ Constants accessible
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
✅ **No more Edge Runtime errors**
|
|
||||||
✅ **Middleware works without database dependencies**
|
|
||||||
✅ **Authentication logging works in all contexts**
|
|
||||||
✅ **API routes maintain full audit functionality**
|
|
||||||
✅ **Constants available everywhere**
|
|
||||||
✅ **Graceful degradation in Edge Runtime**
|
|
||||||
✅ **Full functionality in Node.js Runtime**
|
|
||||||
|
|
||||||
## Performance Impact
|
|
||||||
|
|
||||||
- **Edge Runtime**: Minimal - only console logging
|
|
||||||
- **Node.js Runtime**: Same as before - full database operations
|
|
||||||
- **Import cost**: Near zero - no static database imports
|
|
||||||
- **Memory usage**: Significantly reduced in Edge Runtime
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
To update existing code:
|
|
||||||
|
|
||||||
1. **Replace imports:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Old
|
|
||||||
import { logApiAction } from "@/lib/auditLog.js";
|
|
||||||
|
|
||||||
// New
|
|
||||||
import { logApiActionSafe } from "@/lib/auditLogSafe.js";
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update function calls:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Old
|
|
||||||
logApiAction(req, action, type, id, session, details);
|
|
||||||
|
|
||||||
// New
|
|
||||||
await logApiActionSafe(req, action, type, id, session, details);
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Add runtime exports** (for API routes):
|
|
||||||
```javascript
|
|
||||||
export const runtime = "nodejs"; // For database-heavy routes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices Applied
|
|
||||||
|
|
||||||
1. **Separation of Concerns**: Safe module for universal use, full module for Node.js
|
|
||||||
2. **Dynamic Imports**: Database modules loaded only when needed
|
|
||||||
3. **Runtime Detection**: Automatic environment detection
|
|
||||||
4. **Graceful Degradation**: Meaningful fallbacks in constrained environments
|
|
||||||
5. **Error Isolation**: Audit failures don't break main application flow
|
|
||||||
|
|
||||||
The application now handles both Edge and Node.js runtimes seamlessly with zero Edge Runtime errors! 🎉
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
# Final Edge Runtime Fix - Audit Logging System
|
|
||||||
|
|
||||||
## ✅ **Issue Resolved**
|
|
||||||
|
|
||||||
The Edge Runtime error has been completely fixed! The audit logging system now works seamlessly across all Next.js runtime environments.
|
|
||||||
|
|
||||||
## 🔧 **Final Implementation**
|
|
||||||
|
|
||||||
### **Problem Summary**
|
|
||||||
|
|
||||||
- Edge Runtime was trying to load `better-sqlite3` (Node.js fs module)
|
|
||||||
- Static imports in middleware caused the entire dependency chain to load
|
|
||||||
- `middleware.js` → `auth.js` → `auditLog.js` → `db.js` → `better-sqlite3`
|
|
||||||
|
|
||||||
### **Solution Implemented**
|
|
||||||
|
|
||||||
#### 1. **Made All Functions Async**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Before: Synchronous with require()
|
|
||||||
export function logAuditEvent() {
|
|
||||||
const { default: db } = require("./db.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
// After: Async with dynamic import
|
|
||||||
export async function logAuditEvent() {
|
|
||||||
const { default: db } = await import("./db.js");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. **Runtime Detection & Graceful Fallbacks**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export async function logAuditEvent(params) {
|
|
||||||
try {
|
|
||||||
// Edge Runtime detection
|
|
||||||
if (
|
|
||||||
typeof EdgeRuntime !== "undefined" ||
|
|
||||||
process.env.NEXT_RUNTIME === "edge"
|
|
||||||
) {
|
|
||||||
console.log(`[Audit Log - Edge Runtime] ${action} by user ${userId}`);
|
|
||||||
return; // Graceful exit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node.js Runtime: Full database functionality
|
|
||||||
const { default: db } = await import("./db.js");
|
|
||||||
// ... database operations
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to log audit event:", error);
|
|
||||||
// Non-breaking error handling
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. **Safe Wrapper Module (`auditLogSafe.js`)**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export async function logAuditEventSafe(params) {
|
|
||||||
console.log(`[Audit] ${action} by user ${userId}`); // Always log to console
|
|
||||||
|
|
||||||
if (typeof EdgeRuntime !== "undefined") {
|
|
||||||
return; // Edge Runtime: Console only
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const auditModule = await import("./auditLog.js");
|
|
||||||
await auditModule.logAuditEvent(params); // Node.js: Database + console
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[Audit] Database logging failed, using console fallback");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 **Runtime Behavior**
|
|
||||||
|
|
||||||
| Runtime | Behavior | Database | Console | Errors |
|
|
||||||
| ----------- | ------------------------ | -------- | ------- | ---------------------- |
|
|
||||||
| **Edge** | Console logging only | ❌ | ✅ | ❌ Zero errors |
|
|
||||||
| **Node.js** | Full audit functionality | ✅ | ✅ | ❌ Full error handling |
|
|
||||||
|
|
||||||
## ✅ **Test Results**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ node test-safe-audit-logging.mjs
|
|
||||||
|
|
||||||
Testing Safe Audit Logging...
|
|
||||||
|
|
||||||
1. Testing safe module import...
|
|
||||||
✅ Safe module imported successfully
|
|
||||||
Available actions: 27
|
|
||||||
Available resource types: 8
|
|
||||||
|
|
||||||
2. Testing in simulated Edge Runtime...
|
|
||||||
[Audit] project_view by user anonymous on project:test-123
|
|
||||||
[Audit] Edge Runtime detected - console logging only
|
|
||||||
✅ Edge Runtime logging successful (console only)
|
|
||||||
|
|
||||||
3. Testing in simulated Node.js Runtime...
|
|
||||||
[Audit] project_create by user anonymous on project:test-456
|
|
||||||
Audit log: project_create by user anonymous on project:test-456
|
|
||||||
✅ Node.js Runtime logging successful (database + console)
|
|
||||||
|
|
||||||
4. Testing constants accessibility...
|
|
||||||
✅ Constants accessible:
|
|
||||||
LOGIN action: login
|
|
||||||
PROJECT resource: project
|
|
||||||
NOTE_CREATE action: note_create
|
|
||||||
|
|
||||||
✅ Safe Audit Logging test completed!
|
|
||||||
|
|
||||||
Key features verified:
|
|
||||||
- ✅ No static database imports
|
|
||||||
- ✅ Edge Runtime compatibility
|
|
||||||
- ✅ Graceful fallbacks
|
|
||||||
- ✅ Constants always available
|
|
||||||
- ✅ Async/await support
|
|
||||||
|
|
||||||
The middleware should now work without Edge Runtime errors!
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 **Files Updated**
|
|
||||||
|
|
||||||
### **Core Audit System**
|
|
||||||
|
|
||||||
- ✅ `src/lib/auditLog.js` - Made all functions async, removed static imports
|
|
||||||
- ✅ `src/lib/auditLogSafe.js` - New Edge-compatible wrapper module
|
|
||||||
|
|
||||||
### **Authentication**
|
|
||||||
|
|
||||||
- ✅ `src/lib/auth.js` - Updated to use safe audit logging
|
|
||||||
|
|
||||||
### **API Routes**
|
|
||||||
|
|
||||||
- ✅ `src/app/api/audit-logs/route.js` - Updated for async functions
|
|
||||||
- ✅ `src/app/api/audit-logs/stats/route.js` - Updated for async functions
|
|
||||||
- ✅ `src/app/api/audit-logs/log/route.js` - Updated for async functions
|
|
||||||
- ✅ `src/app/api/projects/route.js` - Using safe audit logging
|
|
||||||
- ✅ `src/app/api/projects/[id]/route.js` - Using safe audit logging
|
|
||||||
- ✅ `src/app/api/notes/route.js` - Using safe audit logging
|
|
||||||
|
|
||||||
## 🚀 **Benefits Achieved**
|
|
||||||
|
|
||||||
1. **✅ Zero Edge Runtime Errors** - No more fs module conflicts
|
|
||||||
2. **✅ Universal Compatibility** - Works in any Next.js runtime environment
|
|
||||||
3. **✅ No Functionality Loss** - Full audit trail in production (Node.js runtime)
|
|
||||||
4. **✅ Graceful Degradation** - Meaningful console logging in Edge Runtime
|
|
||||||
5. **✅ Performance Optimized** - No unnecessary database loads in Edge Runtime
|
|
||||||
6. **✅ Developer Friendly** - Clear logging shows what's happening in each runtime
|
|
||||||
|
|
||||||
## 🎉 **Final Status**
|
|
||||||
|
|
||||||
**The audit logging system is now production-ready and Edge Runtime compatible!**
|
|
||||||
|
|
||||||
- **Middleware**: ✅ Works without errors
|
|
||||||
- **Authentication**: ✅ Logs login/logout events
|
|
||||||
- **API Routes**: ✅ Full audit trail for CRUD operations
|
|
||||||
- **Admin Interface**: ✅ View audit logs at `/admin/audit-logs`
|
|
||||||
- **Edge Runtime**: ✅ Zero errors, console fallbacks
|
|
||||||
- **Node.js Runtime**: ✅ Full database functionality
|
|
||||||
|
|
||||||
Your application should now run perfectly without any Edge Runtime errors while maintaining comprehensive audit logging! 🎊
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# Polish Geospatial Layers Integration - COMPLETE SUCCESS! 🎉
|
|
||||||
|
|
||||||
## ✅ Mission Accomplished
|
|
||||||
|
|
||||||
All Polish geospatial layers including Google Maps have been successfully integrated into the main project's mapping system. The integration maintains proper transparency handling and provides a comprehensive mapping solution.
|
|
||||||
|
|
||||||
## 🚀 What Was Implemented
|
|
||||||
|
|
||||||
### 1. Enhanced Layer Configuration (`mapLayers.js`)
|
|
||||||
**Before**: Only basic OpenStreetMap + simple Polish orthophoto
|
|
||||||
**After**: 8 base layers + 6 overlay layers with full transparency support
|
|
||||||
|
|
||||||
### 2. Updated Main Map Components
|
|
||||||
- **`LeafletMap.js`** - Main project map component ✅
|
|
||||||
- **`EnhancedLeafletMap.js`** - Enhanced map variant ✅
|
|
||||||
- Added `WMSTileLayer` import and proper overlay handling
|
|
||||||
|
|
||||||
### 3. Comprehensive Layer Selection
|
|
||||||
|
|
||||||
#### Base Layers (8 total)
|
|
||||||
1. **OpenStreetMap** (default)
|
|
||||||
2. **🇵🇱 Polish Orthophoto (Standard)** - WMTS format
|
|
||||||
3. **🇵🇱 Polish Orthophoto (High Resolution)** - WMTS format
|
|
||||||
4. **🌍 Google Satellite** - Global satellite imagery
|
|
||||||
5. **🌍 Google Hybrid** - Satellite + roads
|
|
||||||
6. **🌍 Google Roads** - Road map
|
|
||||||
7. **Satellite (Esri)** - Alternative satellite
|
|
||||||
8. **Topographic** - CartoDB topographic
|
|
||||||
|
|
||||||
#### Overlay Layers (6 total with transparency)
|
|
||||||
1. **📋 Polish Cadastral Data** (WMS, 80% opacity)
|
|
||||||
2. **🏗️ Polish Spatial Planning** (WMS, 70% opacity)
|
|
||||||
3. **🛣️ LP-Portal Roads** (WMS, 90% opacity)
|
|
||||||
4. **🏷️ LP-Portal Street Names** (WMS, 100% opacity)
|
|
||||||
5. **📐 LP-Portal Parcels** (WMS, 60% opacity)
|
|
||||||
6. **📍 LP-Portal Survey Markers** (WMS, 80% opacity)
|
|
||||||
|
|
||||||
## 🎯 Key Features Implemented
|
|
||||||
|
|
||||||
### Layer Control Interface
|
|
||||||
- **📚 Layer Control Button** in top-right corner
|
|
||||||
- **Radio buttons** for base layers (mutually exclusive)
|
|
||||||
- **Checkboxes** for overlays (can combine multiple)
|
|
||||||
- **Emoji icons** for easy layer identification
|
|
||||||
|
|
||||||
### Transparency System
|
|
||||||
- **Base layers**: Fully opaque backgrounds
|
|
||||||
- **Overlay layers**: Each with optimized transparency:
|
|
||||||
- Property boundaries: Semi-transparent for visibility
|
|
||||||
- Planning zones: Semi-transparent for context
|
|
||||||
- Roads: Mostly opaque for navigation
|
|
||||||
- Text labels: Fully opaque for readability
|
|
||||||
- Survey data: Semi-transparent for reference
|
|
||||||
|
|
||||||
### Technical Excellence
|
|
||||||
- **WMTS Integration**: Proper KVP format for Polish orthophoto
|
|
||||||
- **WMS Integration**: Transparent PNG overlays with correct parameters
|
|
||||||
- **Performance**: Efficient tile loading and layer switching
|
|
||||||
- **Compatibility**: Works with existing project structure
|
|
||||||
- **SSR Safe**: Proper dynamic imports for Next.js
|
|
||||||
|
|
||||||
## 🌍 Geographic Coverage
|
|
||||||
|
|
||||||
### Poland-Specific Layers
|
|
||||||
- **Polish Orthophoto**: Complete national coverage at high resolution
|
|
||||||
- **Cadastral Data**: Official property boundaries nationwide
|
|
||||||
- **Spatial Planning**: Zoning data where available
|
|
||||||
- **LP-Portal**: Municipal data for specific regions
|
|
||||||
|
|
||||||
### Global Layers
|
|
||||||
- **Google Services**: Worldwide satellite and road data
|
|
||||||
- **Esri Satellite**: Global high-resolution imagery
|
|
||||||
- **OpenStreetMap**: Community-driven global mapping
|
|
||||||
|
|
||||||
## 📱 Where It's Available
|
|
||||||
|
|
||||||
### Main Project Maps
|
|
||||||
- **`/projects/map`** - Projects overview map ✅
|
|
||||||
- **Individual project cards** - Project location maps ✅
|
|
||||||
- **All existing map components** - Enhanced with new layers ✅
|
|
||||||
|
|
||||||
### Demo/Test Pages (Still Available)
|
|
||||||
- **`/comprehensive-polish-map`** - Full-featured demo
|
|
||||||
- **`/test-polish-map`** - Layer comparison
|
|
||||||
- **`/debug-polish-orthophoto`** - Technical testing
|
|
||||||
|
|
||||||
## 🔧 Code Changes Summary
|
|
||||||
|
|
||||||
### Layer Configuration (`mapLayers.js`)
|
|
||||||
```javascript
|
|
||||||
// Added 6 new base layers including Polish orthophoto + Google
|
|
||||||
// Added 6 overlay layers with WMS configuration
|
|
||||||
// Proper transparency and opacity settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Map Components (`LeafletMap.js`, `EnhancedLeafletMap.js`)
|
|
||||||
```javascript
|
|
||||||
// Added WMSTileLayer import
|
|
||||||
// Added Overlay component support
|
|
||||||
// Layer control with both BaseLayer and Overlay
|
|
||||||
// Transparency parameter handling
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 User Experience
|
|
||||||
|
|
||||||
### Easy Layer Selection
|
|
||||||
1. Click **📚** layer control button
|
|
||||||
2. Select base layer (aerial photos, satellite, roads, etc.)
|
|
||||||
3. Check/uncheck overlays (property boundaries, planning, etc.)
|
|
||||||
4. Layers update instantly
|
|
||||||
|
|
||||||
### Visual Clarity
|
|
||||||
- **Emojis** make layer types instantly recognizable
|
|
||||||
- **Proper transparency** prevents overlays from obscuring base maps
|
|
||||||
- **Performance** optimized for smooth switching
|
|
||||||
|
|
||||||
## 🚀 Ready for Production
|
|
||||||
|
|
||||||
✅ **Integration Complete**: All layers working in main project maps
|
|
||||||
✅ **Transparency Handled**: Overlays properly configured with opacity
|
|
||||||
✅ **Performance Optimized**: Efficient loading and switching
|
|
||||||
✅ **User-Friendly**: Clear interface with emoji identifiers
|
|
||||||
✅ **Tested**: Development server running successfully
|
|
||||||
✅ **Documented**: Comprehensive guides available
|
|
||||||
|
|
||||||
## 🎉 Final Result
|
|
||||||
|
|
||||||
The project now has **enterprise-grade Polish geospatial capabilities** integrated directly into the main mapping system. Users can access:
|
|
||||||
|
|
||||||
- **High-resolution Polish orthophoto** from official government sources
|
|
||||||
- **Official cadastral data** for property boundaries
|
|
||||||
- **Spatial planning information** for zoning
|
|
||||||
- **Municipal data** from LP-Portal
|
|
||||||
- **Global satellite imagery** from Google and Esri
|
|
||||||
- **Full transparency control** for overlay combinations
|
|
||||||
|
|
||||||
**Mission: ACCOMPLISHED!** 🚀🗺️🇵🇱
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# Polish Geospatial Layers Integration - Project Maps Complete! 🎉
|
|
||||||
|
|
||||||
## ✅ Successfully Integrated Into Main Project Maps
|
|
||||||
|
|
||||||
All Polish geospatial layers and Google layers have been successfully integrated into the main project's mapping system.
|
|
||||||
|
|
||||||
## 🗺️ Available Layers in Project Maps
|
|
||||||
|
|
||||||
### Base Layers (Mutually Exclusive)
|
|
||||||
1. **OpenStreetMap** - Default layer
|
|
||||||
2. **🇵🇱 Polish Orthophoto (Standard)** - High-quality aerial imagery
|
|
||||||
3. **🇵🇱 Polish Orthophoto (High Resolution)** - Ultra-high resolution aerial imagery
|
|
||||||
4. **🌍 Google Satellite** - Google satellite imagery
|
|
||||||
5. **🌍 Google Hybrid** - Google satellite with roads overlay
|
|
||||||
6. **🌍 Google Roads** - Google road map
|
|
||||||
7. **Satellite (Esri)** - Esri world imagery
|
|
||||||
8. **Topographic** - CartoDB Voyager topographic map
|
|
||||||
|
|
||||||
### Overlay Layers (Can be Combined with Transparency)
|
|
||||||
1. **📋 Polish Cadastral Data** - Property boundaries and parcel information (80% opacity)
|
|
||||||
2. **🏗️ Polish Spatial Planning** - Zoning and urban planning data (70% opacity)
|
|
||||||
3. **🛣️ LP-Portal Roads** - Detailed road network (90% opacity)
|
|
||||||
4. **🏷️ LP-Portal Street Names** - Street names and descriptions (100% opacity)
|
|
||||||
5. **📐 LP-Portal Parcels** - Municipal property parcels (60% opacity)
|
|
||||||
6. **📍 LP-Portal Survey Markers** - Survey markers and reference points (80% opacity)
|
|
||||||
|
|
||||||
## 📁 Updated Files
|
|
||||||
|
|
||||||
### Core Map Components
|
|
||||||
- **`src/components/ui/LeafletMap.js`** - Main project map component ✅
|
|
||||||
- **`src/components/ui/EnhancedLeafletMap.js`** - Enhanced map component ✅
|
|
||||||
- **`src/components/ui/mapLayers.js`** - Layer configuration ✅
|
|
||||||
|
|
||||||
### Map Usage in Project
|
|
||||||
- **`src/app/projects/map/page.js`** - Projects map page (uses LeafletMap)
|
|
||||||
- **`src/components/ui/ProjectMap.js`** - Individual project maps (uses LeafletMap)
|
|
||||||
|
|
||||||
## 🚀 How It Works
|
|
||||||
|
|
||||||
### Layer Control
|
|
||||||
- **Layer Control Button** (📚) appears in top-right corner of maps
|
|
||||||
- **Base Layers** - Radio buttons (only one can be selected)
|
|
||||||
- **Overlay Layers** - Checkboxes (multiple can be selected)
|
|
||||||
|
|
||||||
### Transparency Handling
|
|
||||||
- **Base layers** are fully opaque (no transparency)
|
|
||||||
- **Overlay layers** have appropriate transparency levels:
|
|
||||||
- Cadastral data: Semi-transparent for property boundaries
|
|
||||||
- Planning data: Semi-transparent for zoning information
|
|
||||||
- Roads: Mostly opaque for visibility
|
|
||||||
- Street names: Fully opaque for text readability
|
|
||||||
- Parcels: Semi-transparent for boundary visualization
|
|
||||||
- Survey markers: Semi-transparent for reference points
|
|
||||||
|
|
||||||
### Automatic Integration
|
|
||||||
All existing project maps now have access to:
|
|
||||||
- Polish orthophoto layers
|
|
||||||
- Google satellite/road layers
|
|
||||||
- Polish government WMS overlays
|
|
||||||
- LP-Portal municipal data overlays
|
|
||||||
|
|
||||||
## 🎯 Benefits
|
|
||||||
|
|
||||||
1. **Enhanced Mapping Capabilities**: Rich selection of base layers for different use cases
|
|
||||||
2. **Polish-Specific Data**: Access to official Polish cadastral and planning data
|
|
||||||
3. **Transparency Support**: Overlays work correctly with transparency
|
|
||||||
4. **Maintained Performance**: Layers load efficiently and switch smoothly
|
|
||||||
5. **User-Friendly**: Clear naming with emojis for easy identification
|
|
||||||
|
|
||||||
## 🌍 Geographic Coverage
|
|
||||||
|
|
||||||
- **Polish Orthophoto**: Complete coverage of Poland
|
|
||||||
- **Polish Cadastral**: Official property boundaries across Poland
|
|
||||||
- **Polish Planning**: Zoning data where available
|
|
||||||
- **LP-Portal**: Municipal data (specific regions)
|
|
||||||
- **Google Layers**: Global coverage
|
|
||||||
- **Esri Satellite**: Global coverage
|
|
||||||
|
|
||||||
## 📱 Test Locations
|
|
||||||
|
|
||||||
Perfect locations to test all layers:
|
|
||||||
- **Kraków**: [50.0647, 19.9450] - Historic center with detailed cadastral data
|
|
||||||
- **Warszawa**: [52.2297, 21.0122] - Capital city with planning data
|
|
||||||
- **Gdańsk**: [54.3520, 18.6466] - Port city with orthophoto coverage
|
|
||||||
- **Wrocław**: [51.1079, 17.0385] - University city
|
|
||||||
- **Poznań**: [52.4064, 16.9252] - Industrial center
|
|
||||||
|
|
||||||
## 🔧 Technical Implementation
|
|
||||||
|
|
||||||
### WMTS Integration
|
|
||||||
- Polish orthophoto uses proper WMTS KVP format
|
|
||||||
- EPSG:3857 coordinate system for Leaflet compatibility
|
|
||||||
- Standard 256x256 tile size for optimal performance
|
|
||||||
|
|
||||||
### WMS Overlay Integration
|
|
||||||
- Transparent PNG format for overlays
|
|
||||||
- Proper parameter configuration for each service
|
|
||||||
- Optimized opacity levels for each overlay type
|
|
||||||
- Tiled requests for better performance
|
|
||||||
|
|
||||||
### React/Leaflet Architecture
|
|
||||||
- Uses `react-leaflet` components: `TileLayer` and `WMSTileLayer`
|
|
||||||
- Proper layer control with `BaseLayer` and `Overlay` components
|
|
||||||
- Icon fixes for marker display
|
|
||||||
- SSR-safe dynamic imports
|
|
||||||
|
|
||||||
## 🎉 Status: COMPLETE
|
|
||||||
|
|
||||||
✅ All Polish geospatial layers integrated
|
|
||||||
✅ Google layers integrated
|
|
||||||
✅ Transparency properly handled
|
|
||||||
✅ Layer control working
|
|
||||||
✅ Project maps updated
|
|
||||||
✅ Documentation complete
|
|
||||||
|
|
||||||
The main project maps now have comprehensive Polish geospatial capabilities with proper transparency support! 🚀
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Merge Complete - auth2 to main
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Successfully merged the `auth2` branch into the `main` branch on **2024-12-29**.
|
|
||||||
|
|
||||||
## What was merged
|
|
||||||
The `auth2` branch contained extensive authentication and authorization features:
|
|
||||||
|
|
||||||
### Core Features Added:
|
|
||||||
1. **Authentication System** - Complete NextAuth.js implementation with database sessions
|
|
||||||
2. **Authorization & Access Control** - Role-based permissions (admin, user, guest)
|
|
||||||
3. **User Management** - Admin interface for user creation, editing, and role management
|
|
||||||
4. **Audit Logging** - Comprehensive logging of all user actions and system events
|
|
||||||
5. **Edge Runtime Compatibility** - Fixed SSR and build issues for production deployment
|
|
||||||
|
|
||||||
### Technical Improvements:
|
|
||||||
- **SSR Fixes** - Resolved all Server-Side Rendering issues with map components and auth pages
|
|
||||||
- **Build Optimization** - Project now builds cleanly without errors
|
|
||||||
- **UI/UX Preservation** - Maintained all original functionality, especially the projects map view
|
|
||||||
- **Security Enhancements** - Added middleware for route protection and audit logging
|
|
||||||
|
|
||||||
## Files Modified/Added:
|
|
||||||
- **98 files changed** with 9,544 additions and 658 deletions
|
|
||||||
- Major additions include authentication pages, admin interfaces, API routes, and middleware
|
|
||||||
- All test/debug pages moved to `debug-disabled/` folder to keep them out of production builds
|
|
||||||
|
|
||||||
## Verification:
|
|
||||||
✅ Build completed successfully (`npm run build`)
|
|
||||||
✅ Development server starts without errors (`npm run dev`)
|
|
||||||
✅ All pages load correctly, including `/projects/map`
|
|
||||||
✅ Original UI/UX functionality preserved
|
|
||||||
✅ Authentication and authorization working as expected
|
|
||||||
|
|
||||||
## Post-Merge Status:
|
|
||||||
- **Current branch**: `main`
|
|
||||||
- **Remote status**: All changes pushed to origin/main
|
|
||||||
- **Ready for production**: Yes, all SSR issues resolved
|
|
||||||
- **Authentication**: Fully functional with admin panel at `/admin/users`
|
|
||||||
|
|
||||||
## Next Steps:
|
|
||||||
1. Test the production deployment
|
|
||||||
2. Create initial admin user using `node scripts/create-admin.js`
|
|
||||||
3. Monitor audit logs for any issues
|
|
||||||
4. Consider cleaning up old test files in future iterations
|
|
||||||
|
|
||||||
---
|
|
||||||
*Merge completed by GitHub Copilot on 2024-12-29*
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# Branch Merge Preparation Summary
|
|
||||||
|
|
||||||
## ✅ Completed Tasks
|
|
||||||
|
|
||||||
### 1. Build Issues Fixed
|
|
||||||
- **SSR Issues**: Fixed server-side rendering issues with Leaflet map components
|
|
||||||
- **useSearchParams**: Added Suspense boundaries to all pages using useSearchParams
|
|
||||||
- **Dynamic Imports**: Implemented proper dynamic imports for map components
|
|
||||||
- **Build Success**: Project now builds successfully without errors
|
|
||||||
|
|
||||||
### 2. Code Quality Improvements
|
|
||||||
- **README Updated**: Comprehensive documentation reflecting current project state
|
|
||||||
- **Project Structure**: Updated project structure documentation
|
|
||||||
- **API Documentation**: Added complete API endpoint documentation
|
|
||||||
- **Clean Build**: All pages compile and build correctly
|
|
||||||
|
|
||||||
### 3. Debug Pages Management
|
|
||||||
- **Temporary Relocation**: Moved debug/test pages to `debug-disabled/` folder
|
|
||||||
- **Build Optimization**: Removed non-production pages from build process
|
|
||||||
- **Development Tools**: Preserved debug functionality for future development
|
|
||||||
|
|
||||||
### 4. Authentication & Authorization
|
|
||||||
- **Auth Pages Fixed**: All authentication pages now build correctly
|
|
||||||
- **Suspense Boundaries**: Proper error boundaries for auth components
|
|
||||||
- **Session Management**: Maintained existing auth functionality
|
|
||||||
|
|
||||||
## 🔍 Current State
|
|
||||||
|
|
||||||
### Build Status
|
|
||||||
- ✅ **npm run build**: Successful
|
|
||||||
- ✅ **34 pages**: All pages compile
|
|
||||||
- ✅ **Static Generation**: Working correctly
|
|
||||||
- ⚠️ **ESLint Warning**: Parser serialization issue (non-blocking)
|
|
||||||
|
|
||||||
### Branch Status
|
|
||||||
- **Branch**: `auth2`
|
|
||||||
- **Status**: Ready for merge to main
|
|
||||||
- **Commit**: `faeb1ca` - "Prepare branch for merge to main"
|
|
||||||
- **Files Changed**: 13 files modified/moved
|
|
||||||
|
|
||||||
## 🚀 Next Steps for Merge
|
|
||||||
|
|
||||||
### 1. Pre-merge Checklist
|
|
||||||
- [x] All build errors resolved
|
|
||||||
- [x] Documentation updated
|
|
||||||
- [x] Non-production code moved
|
|
||||||
- [x] Changes committed
|
|
||||||
- [ ] Final testing (recommended)
|
|
||||||
- [ ] Merge to main branch
|
|
||||||
|
|
||||||
### 2. Post-merge Tasks
|
|
||||||
- [ ] Re-enable debug pages if needed (move back from `debug-disabled/`)
|
|
||||||
- [ ] Fix ESLint parser configuration
|
|
||||||
- [ ] Add integration tests
|
|
||||||
- [ ] Deploy to production
|
|
||||||
|
|
||||||
### 3. Optional Improvements
|
|
||||||
- [ ] Fix ESLint configuration for better linting
|
|
||||||
- [ ] Add more comprehensive error handling
|
|
||||||
- [ ] Optimize bundle size
|
|
||||||
- [ ] Add more unit tests
|
|
||||||
|
|
||||||
## 📝 Files Modified
|
|
||||||
|
|
||||||
### Core Changes
|
|
||||||
- `README.md` - Updated comprehensive documentation
|
|
||||||
- `src/app/auth/error/page.js` - Added Suspense boundary
|
|
||||||
- `src/app/auth/signin/page.js` - Added Suspense boundary
|
|
||||||
- `src/app/projects/[id]/page.js` - Fixed dynamic import
|
|
||||||
- `src/app/projects/map/page.js` - Added Suspense boundary
|
|
||||||
- `src/components/ui/ClientProjectMap.js` - New client component wrapper
|
|
||||||
|
|
||||||
### Debug Pages (Temporarily Moved)
|
|
||||||
- `debug-disabled/debug-polish-orthophoto/` - Polish orthophoto debug
|
|
||||||
- `debug-disabled/test-polish-orthophoto/` - Polish orthophoto test
|
|
||||||
- `debug-disabled/test-polish-map/` - Polish map test
|
|
||||||
- `debug-disabled/test-improved-wmts/` - WMTS test
|
|
||||||
- `debug-disabled/comprehensive-polish-map/` - Comprehensive map test
|
|
||||||
|
|
||||||
## 🎯 Recommendation
|
|
||||||
|
|
||||||
**The branch is now ready for merge to main.** All critical build issues have been resolved, and the project builds successfully. The debug pages have been temporarily moved to prevent build issues while preserving their functionality for future development.
|
|
||||||
|
|
||||||
To proceed with the merge:
|
|
||||||
1. Switch to main branch: `git checkout main`
|
|
||||||
2. Merge auth2 branch: `git merge auth2`
|
|
||||||
3. Push to origin: `git push origin main`
|
|
||||||
4. Deploy if needed
|
|
||||||
|
|
||||||
The project is now in a stable state with comprehensive authentication, project management, and mapping functionality.
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
# Polish Geospatial Layers Integration Guide
|
|
||||||
|
|
||||||
## 🎯 All 4+ Polish Layers Successfully Implemented!
|
|
||||||
|
|
||||||
This document shows how to use the comprehensive Polish geospatial layers that have been converted from your OpenLayers implementation to work with Leaflet/React.
|
|
||||||
|
|
||||||
## 📦 Available Components
|
|
||||||
|
|
||||||
### Complete Map Components
|
|
||||||
- `ComprehensivePolishMap.js` - Full-featured map with all layers
|
|
||||||
- `AdvancedPolishOrthophotoMap.js` - Advanced map with overlays
|
|
||||||
- `PolishOrthophotoMap.js` - Basic map with Polish orthophoto
|
|
||||||
|
|
||||||
### Individual Layer Components
|
|
||||||
- `PolishGeoLayers.js` - Individual layer components for custom integration
|
|
||||||
|
|
||||||
## 🗺️ Implemented Layers
|
|
||||||
|
|
||||||
### Base Layers (WMTS)
|
|
||||||
1. **Polish Orthophoto Standard Resolution** ✅
|
|
||||||
- URL: `https://mapy.geoportal.gov.pl/wss/service/PZGIK/ORTO/WMTS/StandardResolution`
|
|
||||||
- Format: JPEG, Max Zoom: 19
|
|
||||||
|
|
||||||
2. **Polish Orthophoto High Resolution** ✅
|
|
||||||
- URL: `https://mapy.geoportal.gov.pl/wss/service/PZGIK/ORTO/WMTS/HighResolution`
|
|
||||||
- Format: JPEG, Max Zoom: 19
|
|
||||||
|
|
||||||
### Overlay Layers (WMS)
|
|
||||||
|
|
||||||
3. **Polish Cadastral Data (Działki)** ✅
|
|
||||||
- Service: GUGiK Krajowa Integracja Ewidencji Gruntów
|
|
||||||
- Layers: Property boundaries, parcels, buildings
|
|
||||||
- Format: PNG (transparent)
|
|
||||||
|
|
||||||
4. **Polish Spatial Planning (MPZT)** ✅
|
|
||||||
- Service: Geoportal Spatial Planning Integration
|
|
||||||
- Layers: Zoning, planning boundaries, land use
|
|
||||||
- Format: PNG (transparent)
|
|
||||||
|
|
||||||
### Additional LP-Portal Layers
|
|
||||||
5. **LP-Portal Roads** ✅
|
|
||||||
6. **LP-Portal Street Names** ✅
|
|
||||||
7. **LP-Portal Property Parcels** ✅
|
|
||||||
8. **LP-Portal Survey Markers** ✅
|
|
||||||
|
|
||||||
## 🚀 How to Use
|
|
||||||
|
|
||||||
### Option 1: Use Complete Component
|
|
||||||
```jsx
|
|
||||||
import ComprehensivePolishMap from '../components/ui/ComprehensivePolishMap';
|
|
||||||
|
|
||||||
export default function MyPage() {
|
|
||||||
return (
|
|
||||||
<div style={{ height: '500px' }}>
|
|
||||||
<ComprehensivePolishMap
|
|
||||||
center={[50.0647, 19.9450]} // Krakow
|
|
||||||
zoom={14}
|
|
||||||
markers={[]}
|
|
||||||
showLayerControl={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Use Individual Layers
|
|
||||||
```jsx
|
|
||||||
import { MapContainer, LayersControl } from 'react-leaflet';
|
|
||||||
import {
|
|
||||||
PolishOrthophotoStandard,
|
|
||||||
PolishCadastralData,
|
|
||||||
LPPortalRoads
|
|
||||||
} from '../components/ui/PolishGeoLayers';
|
|
||||||
|
|
||||||
export default function CustomMap() {
|
|
||||||
const { BaseLayer, Overlay } = LayersControl;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MapContainer center={[50.0647, 19.9450]} zoom={14}>
|
|
||||||
<LayersControl>
|
|
||||||
<BaseLayer checked name="Polish Orthophoto">
|
|
||||||
<PolishOrthophotoStandard />
|
|
||||||
</BaseLayer>
|
|
||||||
|
|
||||||
<Overlay name="Property Boundaries">
|
|
||||||
<PolishCadastralData />
|
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
<Overlay name="Roads">
|
|
||||||
<LPPortalRoads />
|
|
||||||
</Overlay>
|
|
||||||
</LayersControl>
|
|
||||||
</MapContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📍 Test Locations
|
|
||||||
|
|
||||||
Good locations to test the layers:
|
|
||||||
- **Kraków**: [50.0647, 19.9450] - Historic center
|
|
||||||
- **Warszawa**: [52.2297, 21.0122] - Capital city
|
|
||||||
- **Gdańsk**: [54.3520, 18.6466] - Port city
|
|
||||||
- **Wrocław**: [51.1079, 17.0385] - University city
|
|
||||||
- **Poznań**: [52.4064, 16.9252] - Industrial center
|
|
||||||
|
|
||||||
## ⚙️ Technical Details
|
|
||||||
|
|
||||||
### WMTS Implementation
|
|
||||||
- Uses proper KVP (Key-Value Pair) URL format
|
|
||||||
- EPSG:3857 coordinate system for Leaflet compatibility
|
|
||||||
- Standard tile size (256x256)
|
|
||||||
|
|
||||||
### WMS Implementation
|
|
||||||
- Transparent PNG overlays
|
|
||||||
- Proper parameter configuration
|
|
||||||
- Tiled requests for better performance
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
- All layers use standard web projections
|
|
||||||
- Optimized for React/Leaflet
|
|
||||||
- Minimal additional dependencies (only proj4 for future enhancements)
|
|
||||||
|
|
||||||
## 🎉 Success!
|
|
||||||
|
|
||||||
All layers from your OpenLayers implementation are now working in your Leaflet-based React/Next.js project:
|
|
||||||
|
|
||||||
✅ Polish Orthophoto (Standard & High-Res)
|
|
||||||
✅ Polish Cadastral Data (Property boundaries)
|
|
||||||
✅ Polish Spatial Planning (Zoning data)
|
|
||||||
✅ LP-Portal Municipal Data (Roads, names, parcels, surveys)
|
|
||||||
|
|
||||||
The implementation maintains the same functionality as your original OpenLayers code while being fully compatible with your existing React/Leaflet architecture.
|
|
||||||
|
|
||||||
## 📱 Test Pages Available
|
|
||||||
|
|
||||||
- `/comprehensive-polish-map` - Full featured map
|
|
||||||
- `/test-polish-map` - Basic comparison
|
|
||||||
- `/test-improved-wmts` - Technical testing
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
|
|
||||||
console.log("Adding can_be_assigned column to users table...");
|
|
||||||
|
|
||||||
// Add the new column
|
|
||||||
db.prepare(`
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN can_be_assigned INTEGER DEFAULT 1
|
|
||||||
`).run();
|
|
||||||
|
|
||||||
// Set admin users to not be assignable by default
|
|
||||||
db.prepare(`
|
|
||||||
UPDATE users
|
|
||||||
SET can_be_assigned = 0
|
|
||||||
WHERE role = 'admin'
|
|
||||||
`).run();
|
|
||||||
|
|
||||||
console.log("Migration completed. Admin users are now not assignable by default.");
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { readFileSync } from "fs";
|
|
||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
// Check database directly
|
|
||||||
const dbPath = "./data/database.sqlite";
|
|
||||||
const db = new Database(dbPath);
|
|
||||||
|
|
||||||
console.log("Checking audit logs table...\n");
|
|
||||||
|
|
||||||
// Check table schema
|
|
||||||
const schema = db
|
|
||||||
.prepare(
|
|
||||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_logs'"
|
|
||||||
)
|
|
||||||
.get();
|
|
||||||
console.log("Table schema:");
|
|
||||||
console.log(schema?.sql || "Table not found");
|
|
||||||
|
|
||||||
console.log("\n" + "=".repeat(50) + "\n");
|
|
||||||
|
|
||||||
// Get some audit logs
|
|
||||||
const logs = db
|
|
||||||
.prepare("SELECT * FROM audit_logs ORDER BY timestamp DESC LIMIT 5")
|
|
||||||
.all();
|
|
||||||
console.log(`Found ${logs.length} audit log entries:`);
|
|
||||||
|
|
||||||
logs.forEach((log, index) => {
|
|
||||||
console.log(`\n${index + 1}. ID: ${log.id}`);
|
|
||||||
console.log(` Timestamp: ${log.timestamp}`);
|
|
||||||
console.log(` User ID: ${log.user_id || "NULL"}`);
|
|
||||||
console.log(` Action: ${log.action}`);
|
|
||||||
console.log(` Resource Type: ${log.resource_type}`);
|
|
||||||
console.log(` Resource ID: ${log.resource_id || "N/A"}`);
|
|
||||||
console.log(` IP Address: ${log.ip_address || "N/A"}`);
|
|
||||||
console.log(` User Agent: ${log.user_agent || "N/A"}`);
|
|
||||||
console.log(` Details: ${log.details || "NULL"}`);
|
|
||||||
console.log(` Details type: ${typeof log.details}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Count null user_ids
|
|
||||||
const nullUserCount = db
|
|
||||||
.prepare("SELECT COUNT(*) as count FROM audit_logs WHERE user_id IS NULL")
|
|
||||||
.get();
|
|
||||||
const totalCount = db.prepare("SELECT COUNT(*) as count FROM audit_logs").get();
|
|
||||||
|
|
||||||
console.log(`\n${"=".repeat(50)}`);
|
|
||||||
console.log(`Total audit logs: ${totalCount.count}`);
|
|
||||||
console.log(`Logs with NULL user_id: ${nullUserCount.count}`);
|
|
||||||
console.log(
|
|
||||||
`Percentage with NULL user_id: ${(
|
|
||||||
(nullUserCount.count / totalCount.count) *
|
|
||||||
100
|
|
||||||
).toFixed(2)}%`
|
|
||||||
);
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
|
|
||||||
console.log("Checking projects table structure:");
|
|
||||||
const tableInfo = db.prepare("PRAGMA table_info(projects)").all();
|
|
||||||
console.log(JSON.stringify(tableInfo, null, 2));
|
|
||||||
|
|
||||||
// Check if created_at and updated_at columns exist
|
|
||||||
const hasCreatedAt = tableInfo.some((col) => col.name === "created_at");
|
|
||||||
const hasUpdatedAt = tableInfo.some((col) => col.name === "updated_at");
|
|
||||||
|
|
||||||
console.log("\nColumn existence check:");
|
|
||||||
console.log("created_at exists:", hasCreatedAt);
|
|
||||||
console.log("updated_at exists:", hasUpdatedAt);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import db from './src/lib/db.js';
|
|
||||||
|
|
||||||
console.log('Checking contacts in database...\n');
|
|
||||||
|
|
||||||
const contacts = db.prepare('SELECT contact_id, name, phone, email, is_active, contact_type FROM contacts LIMIT 10').all();
|
|
||||||
|
|
||||||
console.log(`Total contacts found: ${contacts.length}\n`);
|
|
||||||
|
|
||||||
if (contacts.length > 0) {
|
|
||||||
console.log('Sample contacts:');
|
|
||||||
contacts.forEach(c => {
|
|
||||||
console.log(` ID: ${c.contact_id}, Name: ${c.name}, Phone: ${c.phone || 'N/A'}, Email: ${c.email || 'N/A'}, Active: ${c.is_active}, Type: ${c.contact_type}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('No contacts found in database!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeCount = db.prepare('SELECT COUNT(*) as count FROM contacts WHERE is_active = 1').get();
|
|
||||||
console.log(`\nActive contacts: ${activeCount.count}`);
|
|
||||||
|
|
||||||
const totalCount = db.prepare('SELECT COUNT(*) as count FROM contacts').get();
|
|
||||||
console.log(`Total contacts: ${totalCount.count}`);
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import db from "./src/lib/db.js";
|
|
||||||
import { parseISO, isAfter, startOfDay, addDays } from "date-fns";
|
|
||||||
|
|
||||||
const today = startOfDay(new Date());
|
|
||||||
const threeDaysFromNow = addDays(today, 3);
|
|
||||||
const oneDayFromNow = addDays(today, 1);
|
|
||||||
|
|
||||||
console.log(`Today: ${today.toISOString().split('T')[0]}`);
|
|
||||||
console.log(`3 days from now: ${threeDaysFromNow.toISOString().split('T')[0]}`);
|
|
||||||
console.log(`1 day from now: ${oneDayFromNow.toISOString().split('T')[0]}`);
|
|
||||||
|
|
||||||
const projects = db.prepare(`
|
|
||||||
SELECT project_name, finish_date, project_status
|
|
||||||
FROM projects
|
|
||||||
WHERE finish_date IS NOT NULL
|
|
||||||
AND project_status != 'fulfilled'
|
|
||||||
AND project_status != 'cancelled'
|
|
||||||
ORDER BY finish_date ASC
|
|
||||||
`).all();
|
|
||||||
|
|
||||||
console.log(`\nFound ${projects.length} active projects with due dates:`);
|
|
||||||
|
|
||||||
projects.forEach(project => {
|
|
||||||
try {
|
|
||||||
const finishDate = parseISO(project.finish_date);
|
|
||||||
const finishDateStart = startOfDay(finishDate);
|
|
||||||
|
|
||||||
const isDueIn3Days = finishDateStart.getTime() === threeDaysFromNow.getTime();
|
|
||||||
const isDueIn1Day = finishDateStart.getTime() === oneDayFromNow.getTime();
|
|
||||||
const isOverdue = isAfter(today, finishDateStart);
|
|
||||||
|
|
||||||
let status = '';
|
|
||||||
if (isDueIn3Days) status = '⚠️ DUE IN 3 DAYS';
|
|
||||||
else if (isDueIn1Day) status = '🚨 DUE IN 1 DAY';
|
|
||||||
else if (isOverdue) status = '❌ OVERDUE';
|
|
||||||
else status = '📅 Future';
|
|
||||||
|
|
||||||
console.log(`${status} - ${project.project_name}: ${project.finish_date.split('T')[0]} (${project.project_status})`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ Error parsing date for ${project.project_name}: ${project.finish_date}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
|
|
||||||
console.log("Current projects table structure:");
|
|
||||||
const tableInfo = db.prepare("PRAGMA table_info(projects)").all();
|
|
||||||
console.log(JSON.stringify(tableInfo, null, 2));
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
const db = new Database("./data/database.sqlite");
|
|
||||||
|
|
||||||
// Check table structures first
|
|
||||||
console.log("Users table structure:");
|
|
||||||
const usersSchema = db.prepare("PRAGMA table_info(users)").all();
|
|
||||||
console.log(usersSchema);
|
|
||||||
|
|
||||||
console.log("\nProjects table structure:");
|
|
||||||
const projectsSchema = db.prepare("PRAGMA table_info(projects)").all();
|
|
||||||
console.log(projectsSchema);
|
|
||||||
|
|
||||||
// Check if there are any projects
|
|
||||||
const projects = db
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT p.*,
|
|
||||||
creator.name as created_by_name,
|
|
||||||
assignee.name as assigned_to_name
|
|
||||||
FROM projects p
|
|
||||||
LEFT JOIN users creator ON p.created_by = creator.id
|
|
||||||
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
|
||||||
LIMIT 5
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.all();
|
|
||||||
|
|
||||||
console.log("\nProjects in database:");
|
|
||||||
console.log(JSON.stringify(projects, null, 2));
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
|
|
||||||
console.log("Database schema for notes table:");
|
|
||||||
console.log(db.prepare("PRAGMA table_info(notes)").all());
|
|
||||||
|
|
||||||
console.log("\nDatabase schema for project_tasks table:");
|
|
||||||
console.log(db.prepare("PRAGMA table_info(project_tasks)").all());
|
|
||||||
|
|
||||||
console.log("\nSample notes to check is_system column:");
|
|
||||||
console.log(db.prepare("SELECT * FROM notes LIMIT 5").all());
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
const db = new Database("./data/database.sqlite");
|
|
||||||
|
|
||||||
console.log("Project Tasks table structure:");
|
|
||||||
const projectTasksSchema = db.prepare("PRAGMA table_info(project_tasks)").all();
|
|
||||||
console.table(projectTasksSchema);
|
|
||||||
|
|
||||||
console.log("\nSample project tasks with user tracking:");
|
|
||||||
const tasks = db
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT pt.*,
|
|
||||||
creator.name as created_by_name,
|
|
||||||
assignee.name as assigned_to_name
|
|
||||||
FROM project_tasks pt
|
|
||||||
LEFT JOIN users creator ON pt.created_by = creator.id
|
|
||||||
LEFT JOIN users assignee ON pt.assigned_to = assignee.id
|
|
||||||
LIMIT 3
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.all();
|
|
||||||
console.table(tasks);
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const ComprehensivePolishMap = dynamic(
|
|
||||||
() => import('../../components/ui/ComprehensivePolishMap'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div className="flex items-center justify-center h-96">Loading map...</div>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function ComprehensivePolishMapPage() {
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState('krakow');
|
|
||||||
|
|
||||||
// Different locations to test the layers
|
|
||||||
const locations = {
|
|
||||||
krakow: {
|
|
||||||
center: [50.0647, 19.9450],
|
|
||||||
zoom: 14,
|
|
||||||
name: "Kraków",
|
|
||||||
description: "Historic city center with good cadastral data coverage"
|
|
||||||
},
|
|
||||||
warsaw: {
|
|
||||||
center: [52.2297, 21.0122],
|
|
||||||
zoom: 14,
|
|
||||||
name: "Warszawa",
|
|
||||||
description: "Capital city with extensive planning data"
|
|
||||||
},
|
|
||||||
gdansk: {
|
|
||||||
center: [54.3520, 18.6466],
|
|
||||||
zoom: 14,
|
|
||||||
name: "Gdańsk",
|
|
||||||
description: "Port city with detailed property boundaries"
|
|
||||||
},
|
|
||||||
wroclaw: {
|
|
||||||
center: [51.1079, 17.0385],
|
|
||||||
zoom: 14,
|
|
||||||
name: "Wrocław",
|
|
||||||
description: "University city with good orthophoto coverage"
|
|
||||||
},
|
|
||||||
poznan: {
|
|
||||||
center: [52.4064, 16.9252],
|
|
||||||
zoom: 14,
|
|
||||||
name: "Poznań",
|
|
||||||
description: "Industrial center with road network data"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentLocation = locations[selectedLocation];
|
|
||||||
|
|
||||||
// Test markers for selected location
|
|
||||||
const testMarkers = [
|
|
||||||
{
|
|
||||||
position: currentLocation.center,
|
|
||||||
popup: `${currentLocation.name} - ${currentLocation.description}`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-100">
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<h1 className="text-4xl font-bold text-gray-800 mb-6">
|
|
||||||
🇵🇱 Comprehensive Polish Geospatial Data Platform
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-green-800 mb-3">
|
|
||||||
All Polish Layers Implementation Complete! 🎉
|
|
||||||
</h2>
|
|
||||||
<p className="text-green-700 mb-4">
|
|
||||||
This comprehensive map includes all layers from your OpenLayers implementation,
|
|
||||||
converted to work seamlessly with your Leaflet-based React/Next.js project.
|
|
||||||
</p>
|
|
||||||
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<strong className="text-green-800">Base Layers:</strong>
|
|
||||||
<ul className="mt-1 text-green-700">
|
|
||||||
<li>• Polish Orthophoto (Standard & High Resolution)</li>
|
|
||||||
<li>• OpenStreetMap, Google Maps, Esri Satellite</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong className="text-green-800">Overlay Layers:</strong>
|
|
||||||
<ul className="mt-1 text-green-700">
|
|
||||||
<li>• Cadastral Data, Spatial Planning</li>
|
|
||||||
<li>• LP-Portal Roads, Street Names, Parcels, Surveys</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Location Selector */}
|
|
||||||
<div className="bg-white rounded-lg shadow-lg p-4 mb-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-3">
|
|
||||||
🎯 Select Test Location:
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
|
||||||
{Object.entries(locations).map(([key, location]) => (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
onClick={() => setSelectedLocation(key)}
|
|
||||||
className={`px-3 py-2 rounded-lg text-sm transition-colors ${
|
|
||||||
selectedLocation === key
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{location.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600 mt-2">
|
|
||||||
<strong>Current:</strong> {currentLocation.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Map Container */}
|
|
||||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
|
||||||
<div className="p-4 bg-blue-600 text-white">
|
|
||||||
<h2 className="text-xl font-semibold">
|
|
||||||
Interactive Map: {currentLocation.name}
|
|
||||||
</h2>
|
|
||||||
<p className="text-blue-100 mt-2">
|
|
||||||
Use the layer control (top-right) to toggle between base layers and enable overlay layers.
|
|
||||||
Combine orthophoto with cadastral data for detailed property analysis.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-96 md:h-[700px]">
|
|
||||||
<ComprehensivePolishMap
|
|
||||||
key={selectedLocation} // Force re-render when location changes
|
|
||||||
center={currentLocation.center}
|
|
||||||
zoom={currentLocation.zoom}
|
|
||||||
markers={testMarkers}
|
|
||||||
showLayerControl={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Layer Information */}
|
|
||||||
<div className="mt-8 grid md:grid-cols-2 gap-6">
|
|
||||||
{/* Base Layers */}
|
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
|
||||||
🗺️ Base Layers
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-3 text-sm">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-green-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>Polish Orthophoto (Standard)</strong>
|
|
||||||
<p className="text-gray-600 mt-1">High-quality aerial imagery from Polish Geoportal</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-emerald-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>Polish Orthophoto (High Resolution)</strong>
|
|
||||||
<p className="text-gray-600 mt-1">Ultra-high resolution aerial imagery for detailed analysis</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-blue-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>OpenStreetMap</strong>
|
|
||||||
<p className="text-gray-600 mt-1">Community-driven map data</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-red-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>Google Maps</strong>
|
|
||||||
<p className="text-gray-600 mt-1">Satellite imagery and road overlay</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Overlay Layers */}
|
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
|
||||||
📊 Overlay Layers
|
|
||||||
</h3> <div className="space-y-3 text-sm">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-orange-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>📋 Polish Cadastral Data</strong>
|
|
||||||
<p className="text-gray-600 mt-1">Property boundaries, parcels, and building outlines</p>
|
|
||||||
<p className="text-xs text-gray-500">Opacity: 80% - Semi-transparent overlay</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-purple-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>🏗️ Polish Spatial Planning</strong>
|
|
||||||
<p className="text-gray-600 mt-1">Zoning data and urban planning information</p>
|
|
||||||
<p className="text-xs text-gray-500">Opacity: 70% - Semi-transparent overlay</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-teal-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>🛣️ LP-Portal Roads</strong>
|
|
||||||
<p className="text-gray-600 mt-1">Detailed road network data</p>
|
|
||||||
<p className="text-xs text-gray-500">Opacity: 90% - Mostly opaque for visibility</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-indigo-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>🏷️ LP-Portal Street Names</strong>
|
|
||||||
<p className="text-gray-600 mt-1">Street names and road descriptions</p>
|
|
||||||
<p className="text-xs text-gray-500">Opacity: 100% - Fully opaque for readability</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<span className="w-4 h-4 bg-pink-500 rounded-full mr-3 mt-0.5 flex-shrink-0"></span>
|
|
||||||
<div>
|
|
||||||
<strong>📐 LP-Portal Parcels & Surveys</strong>
|
|
||||||
<p className="text-gray-600 mt-1">Property parcels and survey markers</p>
|
|
||||||
<p className="text-xs text-gray-500">Opacity: 60-80% - Variable transparency</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Transparency Information */}
|
|
||||||
<div className="mt-8 bg-green-50 border border-green-200 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-green-800 mb-4">
|
|
||||||
🎨 Layer Transparency Handling
|
|
||||||
</h3>
|
|
||||||
<div className="grid md:grid-cols-2 gap-6 text-sm">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-green-700 mb-3">Base Layers (Opaque):</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Polish Orthophoto</span>
|
|
||||||
<span className="bg-green-200 px-2 py-1 rounded text-xs">100% Opaque</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Google Satellite/Roads</span>
|
|
||||||
<span className="bg-green-200 px-2 py-1 rounded text-xs">100% Opaque</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-green-700 mb-3">Overlay Layers (Transparent):</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>📋 Cadastral Data</span>
|
|
||||||
<span className="bg-yellow-200 px-2 py-1 rounded text-xs">80% Opacity</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>🏗️ Spatial Planning</span>
|
|
||||||
<span className="bg-yellow-200 px-2 py-1 rounded text-xs">70% Opacity</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>🛣️ Roads</span>
|
|
||||||
<span className="bg-blue-200 px-2 py-1 rounded text-xs">90% Opacity</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>🏷️ Street Names</span>
|
|
||||||
<span className="bg-green-200 px-2 py-1 rounded text-xs">100% Opacity</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>📐 Parcels</span>
|
|
||||||
<span className="bg-orange-200 px-2 py-1 rounded text-xs">60% Opacity</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>📍 Survey Markers</span>
|
|
||||||
<span className="bg-yellow-200 px-2 py-1 rounded text-xs">80% Opacity</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 p-3 bg-green-100 rounded">
|
|
||||||
<p className="text-green-800 text-sm">
|
|
||||||
<strong>Smart Transparency:</strong> Each overlay layer has been optimized with appropriate transparency levels.
|
|
||||||
Property boundaries are semi-transparent (60-80%) so you can see the underlying imagery,
|
|
||||||
while text labels are fully opaque (100%) for maximum readability.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Usage Guide */}
|
|
||||||
<div className="mt-8 bg-blue-50 border border-blue-200 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-blue-800 mb-4">
|
|
||||||
📋 How to Use This Comprehensive Map
|
|
||||||
</h3>
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-blue-700 mb-2">Basic Navigation:</h4>
|
|
||||||
<ul className="text-blue-600 space-y-1 text-sm">
|
|
||||||
<li>• Use mouse wheel to zoom in/out</li>
|
|
||||||
<li>• Click and drag to pan around</li>
|
|
||||||
<li>• Use layer control (top-right) to switch layers</li>
|
|
||||||
<li>• Select different Polish cities above to test</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-blue-700 mb-2">Advanced Features:</h4>
|
|
||||||
<ul className="text-blue-600 space-y-1 text-sm">
|
|
||||||
<li>• Combine orthophoto with cadastral overlay</li>
|
|
||||||
<li>• Enable multiple overlays simultaneously</li>
|
|
||||||
<li>• Use high-resolution orthophoto for detail work</li>
|
|
||||||
<li>• Compare with Google/OSM base layers</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Technical Implementation */}
|
|
||||||
<div className="mt-8 bg-gray-50 border border-gray-200 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
⚙️ Technical Implementation Details
|
|
||||||
</h3>
|
|
||||||
<div className="grid md:grid-cols-3 gap-6 text-sm">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-700 mb-2">WMTS Integration:</h4>
|
|
||||||
<ul className="text-gray-600 space-y-1">
|
|
||||||
<li>• Proper KVP URL construction</li>
|
|
||||||
<li>• EPSG:3857 coordinate system</li>
|
|
||||||
<li>• Standard and high-res orthophoto</li>
|
|
||||||
<li>• Multiple format support (JPEG/PNG)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-700 mb-2">WMS Overlays:</h4>
|
|
||||||
<ul className="text-gray-600 space-y-1">
|
|
||||||
<li>• Polish government services</li>
|
|
||||||
<li>• LP-Portal municipal data</li>
|
|
||||||
<li>• Transparent overlay support</li>
|
|
||||||
<li>• Multiple layer combinations</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-700 mb-2">React/Leaflet:</h4>
|
|
||||||
<ul className="text-gray-600 space-y-1">
|
|
||||||
<li>• React-Leaflet component integration</li>
|
|
||||||
<li>• Dynamic layer switching</li>
|
|
||||||
<li>• Responsive design</li>
|
|
||||||
<li>• Performance optimized</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// Temporarily disabled debug pages during build
|
|
||||||
// These pages are for development/testing purposes only
|
|
||||||
// To re-enable, rename this file to layout.js
|
|
||||||
|
|
||||||
export default function DebugLayout({ children }) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const DebugPolishOrthophotoMap = dynamic(
|
|
||||||
() => import('../../components/ui/DebugPolishOrthophotoMap'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div className="flex items-center justify-center h-96">Loading map...</div>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const dynamicParams = true;
|
|
||||||
|
|
||||||
export default function DebugPolishOrthophotoPage() {
|
|
||||||
// Test marker in Poland
|
|
||||||
const testMarkers = [
|
|
||||||
{
|
|
||||||
position: [50.0647, 19.9450], // Krakow
|
|
||||||
popup: "Kraków - Test Location"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-100">
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-6">
|
|
||||||
Debug Polish Geoportal Orthophoto
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
||||||
<h2 className="text-lg font-semibold text-red-800 mb-2">
|
|
||||||
Debug Mode Active
|
|
||||||
</h2>
|
|
||||||
<p className="text-red-700">
|
|
||||||
This page tests multiple URL formats for Polish Geoportal orthophoto tiles.
|
|
||||||
Check the browser console and the debug panel on the map for network request information.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
|
||||||
<div className="p-4 bg-blue-600 text-white">
|
|
||||||
<h2 className="text-xl font-semibold">Debug Map with Multiple Orthophoto Options</h2>
|
|
||||||
<p className="text-blue-100 mt-2">
|
|
||||||
Try switching between different Polish orthophoto options using the layer control.
|
|
||||||
Google layers are included as working references.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-96 md:h-[600px]">
|
|
||||||
<DebugPolishOrthophotoMap
|
|
||||||
center={[50.0647, 19.9450]} // Centered on Krakow
|
|
||||||
zoom={12}
|
|
||||||
markers={testMarkers}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
URL Formats Being Tested:
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4 text-sm"> <div className="bg-gray-50 p-3 rounded">
|
|
||||||
<strong>Option 1 (WMTS KVP EPSG:3857):</strong>
|
|
||||||
<code className="block mt-1 text-xs">
|
|
||||||
?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTO&STYLE=default&TILEMATRIXSET=EPSG:3857&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=image/jpeg
|
|
||||||
</code>
|
|
||||||
<span className="text-gray-600">Standard Web Mercator projection</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
|
||||||
<strong>Option 2 (WMTS KVP EPSG:2180):</strong>
|
|
||||||
<code className="block mt-1 text-xs">
|
|
||||||
?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTO&STYLE=default&TILEMATRIXSET=EPSG:2180&TILEMATRIX=EPSG:2180:{z}&TILEROW={y}&TILECOL={x}&FORMAT=image/jpeg
|
|
||||||
</code>
|
|
||||||
<span className="text-gray-600">Polish coordinate system</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
|
||||||
<strong>Option 3 (Alternative TILEMATRIXSET):</strong>
|
|
||||||
<code className="block mt-1 text-xs">
|
|
||||||
?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTO&STYLE=default&TILEMATRIXSET=GoogleMapsCompatible&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=image/jpeg
|
|
||||||
</code>
|
|
||||||
<span className="text-gray-600">Google Maps compatible matrix</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
|
||||||
<strong>Option 4 (PNG format):</strong>
|
|
||||||
<code className="block mt-1 text-xs">
|
|
||||||
?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTO&STYLE=default&TILEMATRIXSET=EPSG:3857&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=image/png
|
|
||||||
</code>
|
|
||||||
<span className="text-gray-600">PNG format instead of JPEG</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-yellow-800 mb-2">
|
|
||||||
Debug Instructions:
|
|
||||||
</h3>
|
|
||||||
<ol className="text-yellow-700 space-y-2">
|
|
||||||
<li><strong>1.</strong> Open browser Developer Tools (F12) and go to Network tab</li>
|
|
||||||
<li><strong>2.</strong> Switch between different Polish orthophoto options in the layer control</li>
|
|
||||||
<li><strong>3.</strong> Look for requests to geoportal.gov.pl in the Network tab</li>
|
|
||||||
<li><strong>4.</strong> Check the debug panel on the map for request/response info</li>
|
|
||||||
<li><strong>5.</strong> Note which options return 200 OK vs 404/403 errors</li>
|
|
||||||
<li><strong>6.</strong> Compare with working Google layers</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const ImprovedPolishOrthophotoMap = dynamic(
|
|
||||||
() => import('../../components/ui/ImprovedPolishOrthophotoMap'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div className="flex items-center justify-center h-96">Loading map...</div>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function ImprovedPolishOrthophotoPage() {
|
|
||||||
const testMarkers = [
|
|
||||||
{
|
|
||||||
position: [50.0647, 19.9450], // Krakow
|
|
||||||
popup: "Kraków - Testing WMTS"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-100">
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-6">
|
|
||||||
Improved Polish WMTS Implementation
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
|
||||||
<h2 className="text-lg font-semibold text-green-800 mb-2">
|
|
||||||
Custom WMTS Layer Implementation
|
|
||||||
</h2>
|
|
||||||
<p className="text-green-700">
|
|
||||||
This version uses a custom WMTS layer that properly constructs KVP URLs based on the GetCapabilities response.
|
|
||||||
Check the debug panel on the map to see the actual requests being made.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
|
||||||
<div className="p-4 bg-blue-600 text-white">
|
|
||||||
<h2 className="text-xl font-semibold">Custom WMTS Layer with Proper KVP URLs</h2>
|
|
||||||
<p className="text-blue-100 mt-2">
|
|
||||||
This implementation builds proper WMTS GetTile requests with all required parameters.
|
|
||||||
Monitor the debug panel and browser network tab for request details.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-96 md:h-[600px]">
|
|
||||||
<ImprovedPolishOrthophotoMap
|
|
||||||
center={[50.0647, 19.9450]}
|
|
||||||
zoom={12}
|
|
||||||
markers={testMarkers}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
WMTS Parameters Being Tested:
|
|
||||||
</h3>
|
|
||||||
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
|
||||||
<strong>Tile Matrix Sets Available:</strong>
|
|
||||||
<ul className="mt-2 space-y-1">
|
|
||||||
<li>• EPSG:3857 (Web Mercator)</li>
|
|
||||||
<li>• EPSG:4326 (WGS84)</li>
|
|
||||||
<li>• EPSG:2180 (Polish National Grid)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
|
||||||
<strong>Formats Available:</strong>
|
|
||||||
<ul className="mt-2 space-y-1">
|
|
||||||
<li>• image/jpeg (default)</li>
|
|
||||||
<li>• image/png</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-blue-800 mb-2">
|
|
||||||
Testing Instructions:
|
|
||||||
</h3>
|
|
||||||
<ol className="text-blue-700 space-y-2">
|
|
||||||
<li><strong>1.</strong> Open Browser Developer Tools (F12) → Network tab</li>
|
|
||||||
<li><strong>2.</strong> Filter by "geoportal.gov.pl" to see WMTS requests</li>
|
|
||||||
<li><strong>3.</strong> Switch between different Polish WMTS options</li>
|
|
||||||
<li><strong>4.</strong> Check if requests return 200 OK or error codes</li>
|
|
||||||
<li><strong>5.</strong> Compare with Google Satellite (known working)</li>
|
|
||||||
<li><strong>6.</strong> Monitor the debug panel for request URLs</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-yellow-800 mb-2">
|
|
||||||
Expected Behavior:
|
|
||||||
</h3>
|
|
||||||
<p className="text-yellow-700">
|
|
||||||
If the Polish orthophoto tiles appear, you should see aerial imagery of Poland.
|
|
||||||
If they don't load, check the network requests - they should show proper WMTS GetTile URLs
|
|
||||||
with all required parameters (SERVICE, REQUEST, LAYER, TILEMATRIXSET, etc.).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const PolishOrthophotoMap = dynamic(
|
|
||||||
() => import('../../components/ui/PolishOrthophotoMap'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div className="flex items-center justify-center h-96">Loading map...</div>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const AdvancedPolishOrthophotoMap = dynamic(
|
|
||||||
() => import('../../components/ui/AdvancedPolishOrthophotoMap'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div className="flex items-center justify-center h-96">Loading map...</div>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function PolishOrthophotoTestPage() {
|
|
||||||
const [activeMap, setActiveMap] = useState('basic');
|
|
||||||
|
|
||||||
// Test markers - various locations in Poland
|
|
||||||
const testMarkers = [
|
|
||||||
{
|
|
||||||
position: [50.0647, 19.9450], // Krakow
|
|
||||||
popup: "Kraków - Main Market Square"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: [52.2297, 21.0122], // Warsaw
|
|
||||||
popup: "Warszawa - Palace of Culture and Science"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: [54.3520, 18.6466], // Gdansk
|
|
||||||
popup: "Gdańsk - Old Town"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-100">
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-6">
|
|
||||||
Polish Geoportal Orthophoto Integration
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Map Type Selector */}
|
|
||||||
<div className="mb-6 bg-white rounded-lg shadow-lg p-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">
|
|
||||||
Choose Map Implementation:
|
|
||||||
</h2>
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveMap('basic')}
|
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
|
||||||
activeMap === 'basic'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Basic Polish Orthophoto
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveMap('advanced')}
|
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
|
||||||
activeMap === 'advanced'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Advanced with WMS Overlays
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Map Container */}
|
|
||||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
|
||||||
<div className="p-4 bg-blue-600 text-white">
|
|
||||||
<h2 className="text-xl font-semibold">
|
|
||||||
{activeMap === 'basic'
|
|
||||||
? 'Basic Polish Orthophoto Map'
|
|
||||||
: 'Advanced Polish Orthophoto with WMS Overlays'
|
|
||||||
}
|
|
||||||
</h2>
|
|
||||||
<p className="text-blue-100 mt-2">
|
|
||||||
{activeMap === 'basic'
|
|
||||||
? 'Demonstrates working Polish Geoportal orthophoto tiles with multiple base layer options.'
|
|
||||||
: 'Advanced version includes Polish cadastral data (działki) and spatial planning (MPZT) as overlay layers.'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-96 md:h-[600px]">
|
|
||||||
{activeMap === 'basic' ? (
|
|
||||||
<PolishOrthophotoMap
|
|
||||||
center={[50.0647, 19.9450]} // Centered on Krakow
|
|
||||||
zoom={12}
|
|
||||||
markers={testMarkers}
|
|
||||||
showLayerControl={true}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AdvancedPolishOrthophotoMap
|
|
||||||
center={[50.0647, 19.9450]} // Centered on Krakow
|
|
||||||
zoom={12}
|
|
||||||
markers={testMarkers}
|
|
||||||
showLayerControl={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Features Overview */}
|
|
||||||
<div className="mt-8 grid md:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
Basic Map Features:
|
|
||||||
</h3>
|
|
||||||
<ul className="space-y-2 text-gray-600">
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
|
|
||||||
Polish Geoportal Orthophoto (Working)
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
|
|
||||||
OpenStreetMap base layer
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-red-500 rounded-full mr-3"></span>
|
|
||||||
Google Satellite imagery
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-yellow-500 rounded-full mr-3"></span>
|
|
||||||
Google Roads overlay
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-purple-500 rounded-full mr-3"></span>
|
|
||||||
Esri World Imagery
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
Advanced Map Features:
|
|
||||||
</h3>
|
|
||||||
<ul className="space-y-2 text-gray-600">
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
|
|
||||||
Standard & High Resolution Orthophoto
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-orange-500 rounded-full mr-3"></span>
|
|
||||||
Polish Cadastral Data (WMS)
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-teal-500 rounded-full mr-3"></span>
|
|
||||||
Spatial Planning Data (MPZT)
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-indigo-500 rounded-full mr-3"></span>
|
|
||||||
Overlay layer support
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-pink-500 rounded-full mr-3"></span>
|
|
||||||
Multiple base layers
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Technical Implementation Details */}
|
|
||||||
<div className="mt-8 bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
Technical Implementation:
|
|
||||||
</h3>
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-700 mb-2">Key Improvements:</h4>
|
|
||||||
<ul className="text-sm text-gray-600 space-y-1">
|
|
||||||
<li>• Uses REST tile service instead of WMTS for better compatibility</li>
|
|
||||||
<li>• Proper tile size (512px) with zoomOffset=-1</li>
|
|
||||||
<li>• proj4 integration for EPSG:2180 coordinate system</li>
|
|
||||||
<li>• Multiple fallback layers for reliability</li>
|
|
||||||
<li>• WMS overlay support for cadastral data</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-700 mb-2">Based on OpenLayers Code:</h4>
|
|
||||||
<ul className="text-sm text-gray-600 space-y-1">
|
|
||||||
<li>• Converted from OpenLayers to Leaflet implementation</li>
|
|
||||||
<li>• Maintains same layer structure and URLs</li>
|
|
||||||
<li>• Includes Polish projection definitions</li>
|
|
||||||
<li>• Compatible with existing React/Next.js setup</li>
|
|
||||||
<li>• Extensible for additional WMS services</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Usage Instructions */}
|
|
||||||
<div className="mt-8 bg-blue-50 border border-blue-200 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-blue-800 mb-2">
|
|
||||||
How to Use:
|
|
||||||
</h3>
|
|
||||||
<div className="text-blue-700 space-y-2">
|
|
||||||
<p><strong>1.</strong> Use the layer control (top-right) to switch between base layers</p>
|
|
||||||
<p><strong>2.</strong> In advanced mode, enable overlay layers for cadastral/planning data</p>
|
|
||||||
<p><strong>3.</strong> Click on markers to see location information</p>
|
|
||||||
<p><strong>4.</strong> Zoom in to see high-resolution orthophoto details</p>
|
|
||||||
<p><strong>5.</strong> Combine orthophoto with cadastral overlay for property boundaries</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const PolishOrthophotoMap = dynamic(
|
|
||||||
() => import('../../components/ui/PolishOrthophotoMap'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div className="flex items-center justify-center h-96">Loading map...</div>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function TestPolishOrthophotoPage() {
|
|
||||||
// Test markers - various locations in Poland
|
|
||||||
const testMarkers = [
|
|
||||||
{
|
|
||||||
position: [50.0647, 19.9450], // Krakow
|
|
||||||
popup: "Kraków - Main Market Square"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: [52.2297, 21.0122], // Warsaw
|
|
||||||
popup: "Warszawa - Palace of Culture and Science"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: [54.3520, 18.6466], // Gdansk
|
|
||||||
popup: "Gdańsk - Old Town"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-100">
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-6">
|
|
||||||
Polish Geoportal Orthophoto Map Test
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
|
||||||
<div className="p-4 bg-blue-600 text-white">
|
|
||||||
<h2 className="text-xl font-semibold">Interactive Map with Polish Orthophoto</h2>
|
|
||||||
<p className="text-blue-100 mt-2">
|
|
||||||
This map demonstrates working Polish Geoportal orthophoto tiles.
|
|
||||||
Use the layer control (top-right) to switch between different map layers.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-96 md:h-[600px]">
|
|
||||||
<PolishOrthophotoMap
|
|
||||||
center={[50.0647, 19.9450]} // Centered on Krakow
|
|
||||||
zoom={12}
|
|
||||||
markers={testMarkers}
|
|
||||||
showLayerControl={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
Map Layers Available:
|
|
||||||
</h3>
|
|
||||||
<ul className="space-y-2 text-gray-600">
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
|
|
||||||
<strong>Polish Geoportal Orthophoto:</strong> High-resolution aerial imagery from Polish Geoportal
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
|
|
||||||
<strong>OpenStreetMap:</strong> Standard OpenStreetMap tiles
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-red-500 rounded-full mr-3"></span>
|
|
||||||
<strong>Google Satellite:</strong> Google satellite imagery
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-yellow-500 rounded-full mr-3"></span>
|
|
||||||
<strong>Google Roads:</strong> Google road overlay
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span className="w-3 h-3 bg-purple-500 rounded-full mr-3"></span>
|
|
||||||
<strong>Esri Satellite:</strong> Esri world imagery
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-yellow-800 mb-2">
|
|
||||||
Implementation Notes:
|
|
||||||
</h3>
|
|
||||||
<div className="text-yellow-700 space-y-2">
|
|
||||||
<p>
|
|
||||||
• The Polish Geoportal orthophoto uses REST tile service instead of WMTS for better compatibility
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
• Tile size is set to 512px with zoomOffset=-1 for proper tile alignment
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
• proj4 library is included for coordinate system transformations (EPSG:2180)
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
• Multiple fallback layers are provided for comparison and reliability
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Debug file to test dropdown functionality
|
|
||||||
console.log("Testing dropdown components...");
|
|
||||||
|
|
||||||
// Simple test to check if components are rendering
|
|
||||||
const testTask = {
|
|
||||||
id: 1,
|
|
||||||
status: "pending",
|
|
||||||
task_name: "Test Task",
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Test task:", testTask);
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
const db = new Database("./data/database.sqlite");
|
|
||||||
|
|
||||||
console.log("Project Tasks table columns:");
|
|
||||||
const projectTasksSchema = db.prepare("PRAGMA table_info(project_tasks)").all();
|
|
||||||
projectTasksSchema.forEach((col) => {
|
|
||||||
console.log(
|
|
||||||
`${col.name}: ${col.type} (${col.notnull ? "NOT NULL" : "NULL"})`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("\nChecking if created_at and updated_at columns exist...");
|
|
||||||
const hasCreatedAt = projectTasksSchema.some(
|
|
||||||
(col) => col.name === "created_at"
|
|
||||||
);
|
|
||||||
const hasUpdatedAt = projectTasksSchema.some(
|
|
||||||
(col) => col.name === "updated_at"
|
|
||||||
);
|
|
||||||
console.log("created_at exists:", hasCreatedAt);
|
|
||||||
console.log("updated_at exists:", hasUpdatedAt);
|
|
||||||
|
|
||||||
// Let's try a simple insert to see what happens
|
|
||||||
console.log("\nTesting manual insert...");
|
|
||||||
try {
|
|
||||||
const result = db
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
INSERT INTO project_tasks (
|
|
||||||
project_id, task_template_id, status, priority,
|
|
||||||
created_by, assigned_to, created_at, updated_at
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.run(1, 1, "pending", "normal", "test-user", "test-user");
|
|
||||||
|
|
||||||
console.log("Insert successful, ID:", result.lastInsertRowid);
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
db.prepare("DELETE FROM project_tasks WHERE id = ?").run(
|
|
||||||
result.lastInsertRowid
|
|
||||||
);
|
|
||||||
console.log("Test record cleaned up");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Insert failed:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
const db = new Database("./data/database.sqlite");
|
|
||||||
|
|
||||||
console.log("Adding user tracking columns to notes table...\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Adding created_by column...");
|
|
||||||
db.exec(`ALTER TABLE notes ADD COLUMN created_by TEXT;`);
|
|
||||||
console.log("✓ created_by column added");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("created_by column already exists or error:", e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Adding is_system column...");
|
|
||||||
db.exec(`ALTER TABLE notes ADD COLUMN is_system INTEGER DEFAULT 0;`);
|
|
||||||
console.log("✓ is_system column added");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("is_system column already exists or error:", e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\nVerifying columns were added...");
|
|
||||||
const schema = db.prepare("PRAGMA table_info(notes)").all();
|
|
||||||
const hasCreatedBy = schema.some((col) => col.name === "created_by");
|
|
||||||
const hasIsSystem = schema.some((col) => col.name === "is_system");
|
|
||||||
|
|
||||||
console.log("created_by exists:", hasCreatedBy);
|
|
||||||
console.log("is_system exists:", hasIsSystem);
|
|
||||||
|
|
||||||
if (hasCreatedBy && hasIsSystem) {
|
|
||||||
console.log("\n✅ All columns are now present!");
|
|
||||||
|
|
||||||
// Test a manual insert
|
|
||||||
console.log("\nTesting manual note insert...");
|
|
||||||
try {
|
|
||||||
const result = db
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
INSERT INTO notes (project_id, note, created_by, is_system, note_date)
|
|
||||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.run(1, "Test note with user tracking", "test-user-id", 0);
|
|
||||||
|
|
||||||
console.log("Insert successful, ID:", result.lastInsertRowid);
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
db.prepare("DELETE FROM notes WHERE note_id = ?").run(
|
|
||||||
result.lastInsertRowid
|
|
||||||
);
|
|
||||||
console.log("Test record cleaned up");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Insert failed:", error.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("\n❌ Some columns are still missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
const db = new Database("./data/database.sqlite");
|
|
||||||
|
|
||||||
console.log("Adding missing columns to project_tasks table...\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Adding created_at column...");
|
|
||||||
db.exec(`ALTER TABLE project_tasks ADD COLUMN created_at TEXT;`);
|
|
||||||
console.log("✓ created_at column added");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("created_at column already exists or error:", e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Adding updated_at column...");
|
|
||||||
db.exec(`ALTER TABLE project_tasks ADD COLUMN updated_at TEXT;`);
|
|
||||||
console.log("✓ updated_at column added");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("updated_at column already exists or error:", e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\nVerifying columns were added...");
|
|
||||||
const schema = db.prepare("PRAGMA table_info(project_tasks)").all();
|
|
||||||
const hasCreatedAt = schema.some((col) => col.name === "created_at");
|
|
||||||
const hasUpdatedAt = schema.some((col) => col.name === "updated_at");
|
|
||||||
|
|
||||||
console.log("created_at exists:", hasCreatedAt);
|
|
||||||
console.log("updated_at exists:", hasUpdatedAt);
|
|
||||||
|
|
||||||
if (hasCreatedAt && hasUpdatedAt) {
|
|
||||||
console.log("\n✅ All columns are now present!");
|
|
||||||
} else {
|
|
||||||
console.log("\n❌ Some columns are still missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
|
|
||||||
export default function migrateAddCompletionDate() {
|
|
||||||
try {
|
|
||||||
// First, check if actual_completion_date exists and rename it to completion_date
|
|
||||||
const columns = db.prepare("PRAGMA table_info(projects)").all();
|
|
||||||
const hasActualCompletionDate = columns.some(col => col.name === 'actual_completion_date');
|
|
||||||
const hasCompletionDate = columns.some(col => col.name === 'completion_date');
|
|
||||||
|
|
||||||
if (hasActualCompletionDate && !hasCompletionDate) {
|
|
||||||
// Rename the column
|
|
||||||
db.exec(`
|
|
||||||
ALTER TABLE projects RENAME COLUMN actual_completion_date TO completion_date;
|
|
||||||
`);
|
|
||||||
console.log("Migration completed: Renamed actual_completion_date to completion_date");
|
|
||||||
} else if (!hasActualCompletionDate && !hasCompletionDate) {
|
|
||||||
// Add the column if it doesn't exist
|
|
||||||
db.exec(`
|
|
||||||
ALTER TABLE projects ADD COLUMN completion_date TEXT;
|
|
||||||
`);
|
|
||||||
console.log("Migration completed: Added completion_date column to projects table");
|
|
||||||
} else if (hasCompletionDate) {
|
|
||||||
console.log("Migration skipped: completion_date column already exists");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Migration failed:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
|
|
||||||
// Migration to add docx_templates table
|
|
||||||
const migration = () => {
|
|
||||||
console.log("Running migration: add-docx-templates-table");
|
|
||||||
|
|
||||||
try {
|
|
||||||
db.exec(`
|
|
||||||
-- Table: docx_templates
|
|
||||||
CREATE TABLE IF NOT EXISTS docx_templates (
|
|
||||||
template_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
template_name TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
original_filename TEXT NOT NULL,
|
|
||||||
stored_filename TEXT NOT NULL,
|
|
||||||
file_path TEXT NOT NULL,
|
|
||||||
file_size INTEGER NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL,
|
|
||||||
is_active INTEGER DEFAULT 1,
|
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_by TEXT,
|
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indexes for templates
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_docx_templates_active ON docx_templates(is_active);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_docx_templates_created_by ON docx_templates(created_by);
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log("Migration completed successfully");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Migration failed:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
migration();
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
|
|
||||||
export default function migrateAddEditedAtToNotes() {
|
|
||||||
try {
|
|
||||||
// Check if edited_at column already exists
|
|
||||||
const columns = db.prepare("PRAGMA table_info(notes)").all();
|
|
||||||
const hasEditedAt = columns.some(col => col.name === 'edited_at');
|
|
||||||
|
|
||||||
if (!hasEditedAt) {
|
|
||||||
// Add the edited_at column
|
|
||||||
db.exec(`
|
|
||||||
ALTER TABLE notes ADD COLUMN edited_at TEXT;
|
|
||||||
`);
|
|
||||||
console.log("Migration completed: Added edited_at column to notes table");
|
|
||||||
} else {
|
|
||||||
console.log("Migration skipped: edited_at column already exists");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Migration failed:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the migration if this file is executed directly
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
||||||
migrateAddEditedAtToNotes();
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
// Migration script to add 'initial' column to users table
|
|
||||||
// Run this on your live server to apply the database changes
|
|
||||||
|
|
||||||
const dbPath = process.argv[2] || "./data/database.sqlite"; // Allow custom path via command line
|
|
||||||
|
|
||||||
console.log(`Applying migration to database: ${dbPath}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const db = new Database(dbPath);
|
|
||||||
|
|
||||||
// Check if initial column already exists
|
|
||||||
const schema = db.prepare("PRAGMA table_info(users)").all();
|
|
||||||
const hasInitialColumn = schema.some(column => column.name === 'initial');
|
|
||||||
|
|
||||||
if (hasInitialColumn) {
|
|
||||||
console.log("✅ Initial column already exists in users table");
|
|
||||||
} else {
|
|
||||||
// Add the initial column
|
|
||||||
db.prepare("ALTER TABLE users ADD COLUMN initial TEXT").run();
|
|
||||||
console.log("✅ Added 'initial' column to users table");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the column was added
|
|
||||||
const updatedSchema = db.prepare("PRAGMA table_info(users)").all();
|
|
||||||
const initialColumn = updatedSchema.find(column => column.name === 'initial');
|
|
||||||
|
|
||||||
if (initialColumn) {
|
|
||||||
console.log("✅ Migration completed successfully");
|
|
||||||
console.log(`Column details: ${JSON.stringify(initialColumn, null, 2)}`);
|
|
||||||
} else {
|
|
||||||
console.error("❌ Migration failed - initial column not found");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
console.log("Database connection closed");
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Migration failed:", error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
|
|
||||||
console.log("Adding settings table...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_by TEXT,
|
|
||||||
FOREIGN KEY (updated_by) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
db.exec(`
|
|
||||||
INSERT OR IGNORE INTO settings (key, value, description) VALUES
|
|
||||||
('backup_notification_user_id', '', 'User ID to receive backup completion notifications');
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log("✅ Settings table created successfully");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating settings table:", error);
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import db from './src/lib/db.js';
|
|
||||||
|
|
||||||
console.log('Starting migration to add team_lead role to users table constraint...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Disable foreign key constraints temporarily
|
|
||||||
db.pragma('foreign_keys = OFF');
|
|
||||||
console.log('Disabled foreign key constraints');
|
|
||||||
|
|
||||||
// Since SQLite doesn't support modifying CHECK constraints directly,
|
|
||||||
// we need to recreate the table with the new constraint
|
|
||||||
|
|
||||||
// First, create a backup table with current data
|
|
||||||
db.exec('CREATE TABLE users_backup AS SELECT * FROM users');
|
|
||||||
console.log('Created backup table');
|
|
||||||
|
|
||||||
// Drop the original table
|
|
||||||
db.exec('DROP TABLE users');
|
|
||||||
console.log('Dropped original table');
|
|
||||||
|
|
||||||
// Recreate the table with the updated constraint
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE users (
|
|
||||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
username TEXT UNIQUE NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
role TEXT CHECK(role IN ('admin', 'team_lead', 'project_manager', 'user', 'read_only')) DEFAULT 'user',
|
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
is_active INTEGER DEFAULT 1,
|
|
||||||
last_login TEXT,
|
|
||||||
failed_login_attempts INTEGER DEFAULT 0,
|
|
||||||
locked_until TEXT,
|
|
||||||
can_be_assigned INTEGER DEFAULT 1,
|
|
||||||
initial TEXT
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
console.log('Created new table with updated constraint');
|
|
||||||
|
|
||||||
// Copy data back from backup
|
|
||||||
db.exec(`
|
|
||||||
INSERT INTO users (
|
|
||||||
id, name, username, password_hash, role, created_at, updated_at,
|
|
||||||
is_active, last_login, failed_login_attempts, locked_until,
|
|
||||||
can_be_assigned, initial
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
id, name, username, password_hash, role, created_at, updated_at,
|
|
||||||
is_active, last_login, failed_login_attempts, locked_until,
|
|
||||||
can_be_assigned, initial
|
|
||||||
FROM users_backup
|
|
||||||
`);
|
|
||||||
console.log('Copied data back from backup');
|
|
||||||
|
|
||||||
// Drop the backup table
|
|
||||||
db.exec('DROP TABLE users_backup');
|
|
||||||
console.log('Dropped backup table');
|
|
||||||
|
|
||||||
// Re-enable foreign key constraints
|
|
||||||
db.pragma('foreign_keys = ON');
|
|
||||||
console.log('Re-enabled foreign key constraints');
|
|
||||||
|
|
||||||
// Verify the migration
|
|
||||||
const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
|
||||||
console.log(`✅ Migration completed successfully! Users table now has ${userCount.count} records`);
|
|
||||||
|
|
||||||
// Verify the constraint allows the new role
|
|
||||||
console.log('✅ CHECK constraint now includes: admin, team_lead, project_manager, user, read_only');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Migration failed:', error.message);
|
|
||||||
console.error('You may need to restore from backup manually');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import db from './src/lib/db.js';
|
|
||||||
|
|
||||||
console.log('Starting migration to add wartosc_zlecenia field to projects table...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if wartosc_zlecenia column already exists
|
|
||||||
const schema = db.prepare("PRAGMA table_info(projects)").all();
|
|
||||||
const hasWartoscZleceniaColumn = schema.some(column => column.name === 'wartosc_zlecenia');
|
|
||||||
|
|
||||||
if (hasWartoscZleceniaColumn) {
|
|
||||||
console.log("✅ wartosc_zlecenia column already exists in projects table");
|
|
||||||
} else {
|
|
||||||
// Add the wartosc_zlecenia column
|
|
||||||
db.prepare("ALTER TABLE projects ADD COLUMN wartosc_zlecenia REAL").run();
|
|
||||||
console.log("✅ Added 'wartosc_zlecenia' column to projects table");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the column was added
|
|
||||||
const updatedSchema = db.prepare("PRAGMA table_info(projects)").all();
|
|
||||||
const wartoscZleceniaColumn = updatedSchema.find(column => column.name === 'wartosc_zlecenia');
|
|
||||||
|
|
||||||
if (wartoscZleceniaColumn) {
|
|
||||||
console.log("✅ Migration completed successfully");
|
|
||||||
console.log(`Column details: ${JSON.stringify(wartoscZleceniaColumn, null, 2)}`);
|
|
||||||
} else {
|
|
||||||
console.error("❌ Migration failed - wartosc_zlecenia column not found");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
console.log("Database connection closed");
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Migration failed:", error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import db from './src/lib/db.js';
|
|
||||||
import initializeDatabase from './src/lib/init-db.js';
|
|
||||||
|
|
||||||
console.log('🚀 Initializing contacts tables...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Run database initialization which will create the new contacts tables
|
|
||||||
initializeDatabase();
|
|
||||||
|
|
||||||
console.log('✅ Contacts tables created successfully!\n');
|
|
||||||
|
|
||||||
// Check if there are projects with contact data in the old text field
|
|
||||||
const projectsWithContacts = db.prepare(`
|
|
||||||
SELECT project_id, project_name, contact
|
|
||||||
FROM projects
|
|
||||||
WHERE contact IS NOT NULL AND contact != ''
|
|
||||||
`).all();
|
|
||||||
|
|
||||||
if (projectsWithContacts.length > 0) {
|
|
||||||
console.log(`📋 Found ${projectsWithContacts.length} projects with contact information in the old text field.\n`);
|
|
||||||
console.log('Sample contacts that could be migrated:');
|
|
||||||
projectsWithContacts.slice(0, 5).forEach(p => {
|
|
||||||
console.log(` - ${p.project_name}: "${p.contact}"`);
|
|
||||||
});
|
|
||||||
console.log('\nℹ️ You can manually create contacts from the /contacts page and link them to projects.');
|
|
||||||
console.log(' The old contact field will remain in the database for reference.\n');
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ No existing contact data found in projects.\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show table statistics
|
|
||||||
const contactsCount = db.prepare('SELECT COUNT(*) as count FROM contacts').get();
|
|
||||||
const projectContactsCount = db.prepare('SELECT COUNT(*) as count FROM project_contacts').get();
|
|
||||||
|
|
||||||
console.log('📊 Database Statistics:');
|
|
||||||
console.log(` - Contacts: ${contactsCount.count}`);
|
|
||||||
console.log(` - Project-Contact Links: ${projectContactsCount.count}`);
|
|
||||||
console.log('\n✨ Migration complete! You can now:');
|
|
||||||
console.log(' 1. Visit /contacts to manage your contacts');
|
|
||||||
console.log(' 2. Add/edit projects to link contacts');
|
|
||||||
console.log(' 3. View linked contacts in project details\n');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error during migration:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import db from './src/lib/db.js';
|
|
||||||
import initializeDatabase from './src/lib/init-db.js';
|
|
||||||
|
|
||||||
console.log('🚀 Migrating contact data from projects...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Run database initialization to ensure tables exist
|
|
||||||
initializeDatabase();
|
|
||||||
|
|
||||||
console.log('✅ Database tables verified\n');
|
|
||||||
|
|
||||||
// Get all projects with contact data
|
|
||||||
const projectsWithContacts = db.prepare(`
|
|
||||||
SELECT project_id, project_name, contact
|
|
||||||
FROM projects
|
|
||||||
WHERE contact IS NOT NULL AND contact != '' AND TRIM(contact) != ''
|
|
||||||
`).all();
|
|
||||||
|
|
||||||
if (projectsWithContacts.length === 0) {
|
|
||||||
console.log('ℹ️ No contact data found in projects to migrate.\n');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📋 Found ${projectsWithContacts.length} projects with contact information\n`);
|
|
||||||
|
|
||||||
let created = 0;
|
|
||||||
let linked = 0;
|
|
||||||
let skipped = 0;
|
|
||||||
|
|
||||||
const createContact = db.prepare(`
|
|
||||||
INSERT INTO contacts (name, phone, email, contact_type, notes, is_active)
|
|
||||||
VALUES (?, ?, ?, 'project', ?, 1)
|
|
||||||
`);
|
|
||||||
|
|
||||||
const linkContact = db.prepare(`
|
|
||||||
INSERT OR IGNORE INTO project_contacts (project_id, contact_id, is_primary, relationship_type)
|
|
||||||
VALUES (?, ?, 1, 'general')
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Process each project
|
|
||||||
for (const project of projectsWithContacts) {
|
|
||||||
try {
|
|
||||||
const contactText = project.contact.trim();
|
|
||||||
|
|
||||||
// Parse contact information - common formats:
|
|
||||||
// "Jan Kowalski, tel. 123-456-789"
|
|
||||||
// "Jan Kowalski 123-456-789"
|
|
||||||
// "123-456-789"
|
|
||||||
// "Jan Kowalski"
|
|
||||||
|
|
||||||
let name = '';
|
|
||||||
let phone = '';
|
|
||||||
let email = '';
|
|
||||||
let notes = '';
|
|
||||||
|
|
||||||
// Try to extract email
|
|
||||||
const emailPattern = /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/;
|
|
||||||
const emailMatch = contactText.match(emailPattern);
|
|
||||||
if (emailMatch) {
|
|
||||||
email = emailMatch[1].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to extract phone number (various formats)
|
|
||||||
const phonePatterns = [
|
|
||||||
/(?:\+?48)?[\s-]?(\d{3}[\s-]?\d{3}[\s-]?\d{3})/, // Polish: 123-456-789, 123 456 789, +48 123456789
|
|
||||||
/(?:\+?48)?[\s-]?(\d{9})/, // 9 digits
|
|
||||||
/tel\.?\s*[:.]?\s*([+\d\s-]+)/i, // tel. 123-456-789
|
|
||||||
/phone\s*[:.]?\s*([+\d\s-]+)/i, // phone: 123-456-789
|
|
||||||
/(\d{3}[-\s]?\d{3}[-\s]?\d{3})/, // Generic phone pattern
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const pattern of phonePatterns) {
|
|
||||||
const match = contactText.match(pattern);
|
|
||||||
if (match) {
|
|
||||||
phone = match[1] || match[0];
|
|
||||||
phone = phone.replace(/\s+/g, ' ').trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract name (text before phone/email or comma)
|
|
||||||
let textForName = contactText;
|
|
||||||
|
|
||||||
if (phone) {
|
|
||||||
// Remove phone from text to get name
|
|
||||||
textForName = textForName.replace(phone, '');
|
|
||||||
}
|
|
||||||
if (email) {
|
|
||||||
// Remove email from text to get name
|
|
||||||
textForName = textForName.replace(email, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove common prefixes like "tel.", "phone:", "email:", commas, etc.
|
|
||||||
name = textForName.replace(/tel\.?|phone:?|email:?|e-mail:?|,/gi, '').trim();
|
|
||||||
|
|
||||||
// Clean up name
|
|
||||||
name = name.replace(/^[,\s-]+|[,\s-]+$/g, '').trim();
|
|
||||||
|
|
||||||
// If we couldn't extract structured data, use project name and put original text in notes
|
|
||||||
if (!phone && !email) {
|
|
||||||
// No structured contact info found, put everything in notes
|
|
||||||
notes = `${contactText}`;
|
|
||||||
name = project.project_name;
|
|
||||||
} else if (!name) {
|
|
||||||
// We have phone/email but no clear name
|
|
||||||
name = project.project_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this contact already exists (by name, phone, or email)
|
|
||||||
let existingContact = null;
|
|
||||||
if (phone) {
|
|
||||||
existingContact = db.prepare(`
|
|
||||||
SELECT contact_id FROM contacts
|
|
||||||
WHERE phone LIKE ? OR phone LIKE ?
|
|
||||||
`).get(`%${phone}%`, `%${phone.replace(/\s/g, '')}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingContact && email) {
|
|
||||||
existingContact = db.prepare(`
|
|
||||||
SELECT contact_id FROM contacts
|
|
||||||
WHERE LOWER(email) = LOWER(?)
|
|
||||||
`).get(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingContact && name && name !== project.project_name) {
|
|
||||||
existingContact = db.prepare(`
|
|
||||||
SELECT contact_id FROM contacts
|
|
||||||
WHERE LOWER(name) = LOWER(?)
|
|
||||||
`).get(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let contactId;
|
|
||||||
|
|
||||||
if (existingContact) {
|
|
||||||
contactId = existingContact.contact_id;
|
|
||||||
console.log(` ♻️ Using existing contact "${name}" for project "${project.project_name}"`);
|
|
||||||
} else {
|
|
||||||
// Create new contact
|
|
||||||
const result = createContact.run(
|
|
||||||
name,
|
|
||||||
phone || null,
|
|
||||||
email || null,
|
|
||||||
notes || `Przeniesiono z projektu: ${project.project_name}`
|
|
||||||
);
|
|
||||||
contactId = result.lastInsertRowid;
|
|
||||||
created++;
|
|
||||||
|
|
||||||
const contactInfo = [];
|
|
||||||
if (phone) contactInfo.push(`📞 ${phone}`);
|
|
||||||
if (email) contactInfo.push(`📧 ${email}`);
|
|
||||||
const infoStr = contactInfo.length > 0 ? ` (${contactInfo.join(', ')})` : '';
|
|
||||||
|
|
||||||
console.log(` ✨ Created contact "${name}"${infoStr} for project "${project.project_name}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Link contact to project
|
|
||||||
linkContact.run(project.project_id, contactId);
|
|
||||||
linked++;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(` ❌ Error processing project "${project.project_name}":`, error.message);
|
|
||||||
skipped++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n📊 Migration Summary:');
|
|
||||||
console.log(` - Contacts created: ${created}`);
|
|
||||||
console.log(` - Project-contact links created: ${linked}`);
|
|
||||||
console.log(` - Projects skipped: ${skipped}`);
|
|
||||||
console.log(` - Total projects processed: ${projectsWithContacts.length}`);
|
|
||||||
|
|
||||||
// Show final statistics
|
|
||||||
const contactsCount = db.prepare('SELECT COUNT(*) as count FROM contacts').get();
|
|
||||||
const projectContactsCount = db.prepare('SELECT COUNT(*) as count FROM project_contacts').get();
|
|
||||||
|
|
||||||
console.log('\n📈 Current Database Statistics:');
|
|
||||||
console.log(` - Total contacts: ${contactsCount.count}`);
|
|
||||||
console.log(` - Total project-contact links: ${projectContactsCount.count}`);
|
|
||||||
|
|
||||||
console.log('\n✨ Migration complete!');
|
|
||||||
console.log(' - Visit /contacts to view and manage your contacts');
|
|
||||||
console.log(' - Edit projects to see linked contacts');
|
|
||||||
console.log(' - The old contact text field is preserved for reference\n');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error during migration:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import db from './src/lib/db.js';
|
|
||||||
|
|
||||||
console.log('Starting migration to add cancelled status to project_status constraint...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Disable foreign key constraints temporarily
|
|
||||||
db.pragma('foreign_keys = OFF');
|
|
||||||
console.log('Disabled foreign key constraints');
|
|
||||||
|
|
||||||
// Since SQLite doesn't support modifying CHECK constraints directly,
|
|
||||||
// we need to recreate the table with the new constraint
|
|
||||||
|
|
||||||
// First, create a backup table with current data
|
|
||||||
db.exec('CREATE TABLE projects_backup AS SELECT * FROM projects');
|
|
||||||
console.log('Created backup table');
|
|
||||||
|
|
||||||
// Drop the original table
|
|
||||||
db.exec('DROP TABLE projects');
|
|
||||||
console.log('Dropped original table');
|
|
||||||
|
|
||||||
// Recreate the table with the updated constraint
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE projects (
|
|
||||||
project_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
contract_id INTEGER,
|
|
||||||
project_name TEXT NOT NULL,
|
|
||||||
project_number TEXT NOT NULL,
|
|
||||||
address TEXT,
|
|
||||||
plot TEXT,
|
|
||||||
district TEXT,
|
|
||||||
unit TEXT,
|
|
||||||
city TEXT,
|
|
||||||
investment_number TEXT,
|
|
||||||
finish_date TEXT,
|
|
||||||
wp TEXT,
|
|
||||||
contact TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
project_type TEXT CHECK(project_type IN ('design', 'construction', 'design+construction')) DEFAULT 'design',
|
|
||||||
project_status TEXT CHECK(project_status IN ('registered', 'in_progress_design', 'in_progress_construction', 'fulfilled', 'cancelled')) DEFAULT 'registered',
|
|
||||||
coordinates TEXT,
|
|
||||||
created_by TEXT,
|
|
||||||
assigned_to TEXT,
|
|
||||||
created_at TEXT,
|
|
||||||
updated_at TEXT,
|
|
||||||
FOREIGN KEY (contract_id) REFERENCES contracts(contract_id)
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
console.log('Created new table with updated constraint');
|
|
||||||
|
|
||||||
// Copy data back
|
|
||||||
db.exec('INSERT INTO projects SELECT * FROM projects_backup');
|
|
||||||
console.log('Restored data from backup');
|
|
||||||
|
|
||||||
// Drop backup table
|
|
||||||
db.exec('DROP TABLE projects_backup');
|
|
||||||
console.log('Cleaned up backup table');
|
|
||||||
|
|
||||||
// Re-enable foreign key constraints
|
|
||||||
db.pragma('foreign_keys = ON');
|
|
||||||
console.log('Re-enabled foreign key constraints');
|
|
||||||
|
|
||||||
// Verify the new constraint
|
|
||||||
const schema = db.prepare('SELECT sql FROM sqlite_master WHERE type=\'table\' AND name=\'projects\'').get();
|
|
||||||
console.log('New table definition:');
|
|
||||||
console.log(schema.sql);
|
|
||||||
|
|
||||||
console.log('Migration completed successfully!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Migration failed:', error);
|
|
||||||
|
|
||||||
// Re-enable foreign keys in case of error
|
|
||||||
try {
|
|
||||||
db.pragma('foreign_keys = ON');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to re-enable foreign keys:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to restore from backup if it exists
|
|
||||||
try {
|
|
||||||
db.exec('DROP TABLE IF EXISTS projects');
|
|
||||||
db.exec('ALTER TABLE projects_backup RENAME TO projects');
|
|
||||||
console.log('Restored from backup due to error');
|
|
||||||
} catch (restoreError) {
|
|
||||||
console.error('Failed to restore from backup:', restoreError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
const db = new Database("./data/database.sqlite");
|
|
||||||
|
|
||||||
console.log("🔄 Migrating database to username-based authentication...\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check current table structure
|
|
||||||
const tableInfo = db.prepare("PRAGMA table_info(users)").all();
|
|
||||||
console.log("Current users table columns:");
|
|
||||||
tableInfo.forEach(col => console.log(` - ${col.name}: ${col.type}`));
|
|
||||||
|
|
||||||
const hasUsername = tableInfo.some(col => col.name === 'username');
|
|
||||||
const hasEmail = tableInfo.some(col => col.name === 'email');
|
|
||||||
|
|
||||||
if (hasUsername) {
|
|
||||||
console.log("✅ Username column already exists!");
|
|
||||||
} else if (hasEmail) {
|
|
||||||
console.log("\n📝 Adding username column...");
|
|
||||||
|
|
||||||
// Add username column
|
|
||||||
db.exec(`ALTER TABLE users ADD COLUMN username TEXT;`);
|
|
||||||
console.log("✅ Username column added");
|
|
||||||
|
|
||||||
// Copy email data to username for existing users
|
|
||||||
console.log("📋 Migrating existing email data to username...");
|
|
||||||
const result = db.exec(`UPDATE users SET username = email WHERE username IS NULL;`);
|
|
||||||
console.log("✅ Data migrated");
|
|
||||||
|
|
||||||
// Create unique index on username
|
|
||||||
console.log("🔍 Creating unique index on username...");
|
|
||||||
try {
|
|
||||||
db.exec(`CREATE UNIQUE INDEX idx_users_username_unique ON users(username);`);
|
|
||||||
console.log("✅ Unique index created");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("ℹ️ Index already exists or couldn't be created:", e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify migration
|
|
||||||
console.log("\n🔍 Verifying migration...");
|
|
||||||
const users = db.prepare("SELECT id, name, username, email FROM users LIMIT 3").all();
|
|
||||||
console.log("Sample users after migration:");
|
|
||||||
users.forEach(user => {
|
|
||||||
console.log(` - ${user.name}: username="${user.username}", email="${user.email || 'NULL'}"`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("\n✅ Migration completed successfully!");
|
|
||||||
console.log("ℹ️ You can now log in using usernames instead of emails");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.log("❌ Neither username nor email column found. Database may be corrupted.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Migration failed:", error.message);
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Database migration runner for deployment
|
|
||||||
# This script runs all pending migrations in order
|
|
||||||
|
|
||||||
echo "🔄 Running database migrations..."
|
|
||||||
|
|
||||||
# List of migration scripts to run (in order)
|
|
||||||
MIGRATIONS=(
|
|
||||||
"migrate-add-team-lead-role.mjs"
|
|
||||||
"migrate-add-wartosc-zlecenia.mjs"
|
|
||||||
)
|
|
||||||
|
|
||||||
for migration in "${MIGRATIONS[@]}"; do
|
|
||||||
if [ -f "$migration" ]; then
|
|
||||||
echo "Running migration: $migration"
|
|
||||||
if node "$migration"; then
|
|
||||||
echo "✅ Migration $migration completed successfully"
|
|
||||||
# Optionally move completed migration to a completed folder
|
|
||||||
# mkdir -p migrations/completed
|
|
||||||
# mv "$migration" "migrations/completed/"
|
|
||||||
else
|
|
||||||
echo "❌ Migration $migration failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Migration file $migration not found, skipping..."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "✅ All migrations completed"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@echo off
|
|
||||||
cd /d "x:\projekty\panel"
|
|
||||||
echo Clearing Next.js cache...
|
|
||||||
if exist .next rmdir /s /q .next
|
|
||||||
echo Starting development server...
|
|
||||||
npm run dev
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
// Test script to verify audit logging after our fixes
|
|
||||||
// This test shows what happens when API calls are made with proper authentication
|
|
||||||
|
|
||||||
console.log("=== TESTING AUDIT LOGGING FIX ===\n");
|
|
||||||
|
|
||||||
// Simulate the flow that would happen in a real authenticated API call
|
|
||||||
async function testAuditLogging() {
|
|
||||||
try {
|
|
||||||
// Import the logging function
|
|
||||||
const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = await import(
|
|
||||||
"./src/lib/auditLogSafe.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("1. Testing audit logging with proper user session...");
|
|
||||||
|
|
||||||
// Simulate an authenticated session (like what req.auth would contain)
|
|
||||||
const mockAuthenticatedSession = {
|
|
||||||
user: {
|
|
||||||
id: "e42a4b036074ff7233942a0728557141", // Real user ID from our logs
|
|
||||||
email: "admin@localhost.com",
|
|
||||||
name: "Administrator",
|
|
||||||
role: "admin",
|
|
||||||
},
|
|
||||||
expires: "2025-08-08T21:18:07.949Z",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Simulate a null/undefined session (like unauthenticated requests)
|
|
||||||
const mockUnauthenticatedSession = null;
|
|
||||||
|
|
||||||
// Test 1: Authenticated user logging
|
|
||||||
console.log("\n2. Testing with authenticated session:");
|
|
||||||
await logAuditEventSafe({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_VIEW,
|
|
||||||
userId: mockAuthenticatedSession?.user?.id || null,
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "test-project-123",
|
|
||||||
ipAddress: "127.0.0.1",
|
|
||||||
userAgent: "Test Browser",
|
|
||||||
details: {
|
|
||||||
test: "authenticated_user_test",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: Unauthenticated user logging (should result in null userId)
|
|
||||||
console.log("\n3. Testing with unauthenticated session:");
|
|
||||||
await logAuditEventSafe({
|
|
||||||
action: AUDIT_ACTIONS.LOGIN_FAILED,
|
|
||||||
userId: mockUnauthenticatedSession?.user?.id || null,
|
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
|
||||||
resourceId: null,
|
|
||||||
ipAddress: "127.0.0.1",
|
|
||||||
userAgent: "Test Browser",
|
|
||||||
details: {
|
|
||||||
test: "unauthenticated_user_test",
|
|
||||||
email: "hacker@test.com",
|
|
||||||
reason: "invalid_credentials",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Check what we just logged
|
|
||||||
console.log("\n4. Checking the audit events we just created...");
|
|
||||||
const { getAuditLogs } = await import("./src/lib/auditLog.js");
|
|
||||||
const latestLogs = await getAuditLogs({ limit: 2 });
|
|
||||||
|
|
||||||
console.log("Latest 2 audit events:");
|
|
||||||
latestLogs.forEach((log, index) => {
|
|
||||||
const userDisplay = log.user_id ? `user ${log.user_id}` : "NULL USER ID";
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${log.timestamp} - ${log.action} by ${userDisplay} on ${
|
|
||||||
log.resource_type
|
|
||||||
}:${log.resource_id || "N/A"}`
|
|
||||||
);
|
|
||||||
if (log.details) {
|
|
||||||
const details =
|
|
||||||
typeof log.details === "string"
|
|
||||||
? JSON.parse(log.details)
|
|
||||||
: log.details;
|
|
||||||
console.log(` Details: ${JSON.stringify(details, null, 4)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("\n5. CONCLUSION:");
|
|
||||||
console.log("✅ The audit logging system is working correctly!");
|
|
||||||
console.log("✅ Authenticated users get proper user IDs logged");
|
|
||||||
console.log(
|
|
||||||
"✅ Unauthenticated requests get NULL user IDs (which is expected)"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"✅ The logApiActionSafe function will extract userId from session?.user?.id correctly"
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Test failed:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testAuditLogging();
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import {
|
|
||||||
logAuditEvent,
|
|
||||||
getAuditLogs,
|
|
||||||
getAuditLogStats,
|
|
||||||
AUDIT_ACTIONS,
|
|
||||||
RESOURCE_TYPES,
|
|
||||||
} from "./src/lib/auditLog.js";
|
|
||||||
|
|
||||||
// Test audit logging functionality
|
|
||||||
console.log("Testing Audit Logging System...\n");
|
|
||||||
|
|
||||||
// Test 1: Log some sample events
|
|
||||||
console.log("1. Creating sample audit events...");
|
|
||||||
|
|
||||||
logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.LOGIN,
|
|
||||||
userId: "user123",
|
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
|
||||||
ipAddress: "192.168.1.100",
|
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
||||||
details: {
|
|
||||||
email: "test@example.com",
|
|
||||||
role: "user",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_CREATE,
|
|
||||||
userId: "user123",
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "proj-456",
|
|
||||||
ipAddress: "192.168.1.100",
|
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
||||||
details: {
|
|
||||||
project_name: "Test Project",
|
|
||||||
project_number: "TP-001",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_UPDATE,
|
|
||||||
userId: "user456",
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "proj-456",
|
|
||||||
ipAddress: "192.168.1.101",
|
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
||||||
details: {
|
|
||||||
updatedFields: ["project_name", "address"],
|
|
||||||
oldValues: { project_name: "Test Project" },
|
|
||||||
newValues: { project_name: "Updated Test Project" },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.LOGIN_FAILED,
|
|
||||||
userId: null,
|
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
|
||||||
ipAddress: "192.168.1.102",
|
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
||||||
details: {
|
|
||||||
email: "hacker@evil.com",
|
|
||||||
reason: "invalid_password",
|
|
||||||
failed_attempts: 3,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Sample events created!\n");
|
|
||||||
|
|
||||||
// Test 2: Retrieve audit logs
|
|
||||||
console.log("2. Retrieving audit logs...");
|
|
||||||
|
|
||||||
const allLogs = getAuditLogs();
|
|
||||||
console.log(`Found ${allLogs.length} total audit events`);
|
|
||||||
|
|
||||||
// Show the latest 3 events
|
|
||||||
console.log("\nLatest audit events:");
|
|
||||||
allLogs.slice(0, 3).forEach((log, index) => {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${log.timestamp} - ${log.action} by user ${
|
|
||||||
log.user_id || "anonymous"
|
|
||||||
} on ${log.resource_type}:${log.resource_id || "N/A"}`
|
|
||||||
);
|
|
||||||
if (log.details) {
|
|
||||||
console.log(` Details: ${JSON.stringify(log.details, null, 2)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Filtered queries
|
|
||||||
console.log("\n3. Testing filtered queries...");
|
|
||||||
|
|
||||||
const loginEvents = getAuditLogs({ action: AUDIT_ACTIONS.LOGIN });
|
|
||||||
console.log(`Found ${loginEvents.length} login events`);
|
|
||||||
|
|
||||||
const projectEvents = getAuditLogs({ resourceType: RESOURCE_TYPES.PROJECT });
|
|
||||||
console.log(`Found ${projectEvents.length} project-related events`);
|
|
||||||
|
|
||||||
const user123Events = getAuditLogs({ userId: "user123" });
|
|
||||||
console.log(`Found ${user123Events.length} events by user123`);
|
|
||||||
|
|
||||||
// Test 4: Statistics
|
|
||||||
console.log("\n4. Getting audit statistics...");
|
|
||||||
|
|
||||||
const stats = getAuditLogStats();
|
|
||||||
console.log("Overall statistics:");
|
|
||||||
console.log(`- Total events: ${stats.total}`);
|
|
||||||
console.log("- Action breakdown:");
|
|
||||||
stats.actionBreakdown.forEach((action) => {
|
|
||||||
console.log(` - ${action.action}: ${action.count}`);
|
|
||||||
});
|
|
||||||
console.log("- User breakdown:");
|
|
||||||
stats.userBreakdown.forEach((user) => {
|
|
||||||
console.log(
|
|
||||||
` - ${user.user_name || user.user_id || "Anonymous"}: ${user.count}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
console.log("- Resource breakdown:");
|
|
||||||
stats.resourceBreakdown.forEach((resource) => {
|
|
||||||
console.log(` - ${resource.resource_type}: ${resource.count}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 5: Date range filtering
|
|
||||||
console.log("\n5. Testing date range filtering...");
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
||||||
|
|
||||||
const recentLogs = getAuditLogs({
|
|
||||||
startDate: oneHourAgo.toISOString(),
|
|
||||||
endDate: now.toISOString(),
|
|
||||||
});
|
|
||||||
console.log(`Found ${recentLogs.length} events in the last hour`);
|
|
||||||
|
|
||||||
console.log("\nAudit logging test completed successfully! ✅");
|
|
||||||
console.log("\nTo view audit logs in the application:");
|
|
||||||
console.log("1. Start your Next.js application");
|
|
||||||
console.log("2. Login as an admin or project manager");
|
|
||||||
console.log("3. Navigate to /admin/audit-logs");
|
|
||||||
console.log("4. Use the filters to explore the audit trail");
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// Test authenticated API access using NextAuth.js client-side approach
|
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:3000';
|
|
||||||
|
|
||||||
async function testAuthenticatedAPI() {
|
|
||||||
console.log('🔐 Testing Authenticated API Access\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Check if server is running
|
|
||||||
console.log('1️⃣ Checking server status...');
|
|
||||||
const healthResponse = await fetch(`${BASE_URL}/api/auth/session`);
|
|
||||||
console.log(`Server status: ${healthResponse.status}`);
|
|
||||||
|
|
||||||
if (!healthResponse.ok) {
|
|
||||||
console.log('❌ Server not responding properly');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: Test unauthenticated access to protected endpoints
|
|
||||||
console.log('\n2️⃣ Testing unauthenticated access...');
|
|
||||||
const protectedEndpoints = [
|
|
||||||
'/api/projects',
|
|
||||||
'/api/contracts',
|
|
||||||
'/api/tasks',
|
|
||||||
'/api/project-tasks'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const endpoint of protectedEndpoints) {
|
|
||||||
const response = await fetch(`${BASE_URL}${endpoint}`);
|
|
||||||
console.log(`${endpoint}: ${response.status} ${response.status === 401 ? '✅ (properly protected)' : '❌ (not protected)'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 3: Check protected pages
|
|
||||||
console.log('\n3️⃣ Testing protected pages...');
|
|
||||||
const protectedPages = ['/projects', '/contracts', '/tasks'];
|
|
||||||
|
|
||||||
for (const page of protectedPages) {
|
|
||||||
const response = await fetch(`${BASE_URL}${page}`, {
|
|
||||||
redirect: 'manual'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 302) {
|
|
||||||
const location = response.headers.get('location');
|
|
||||||
if (location && location.includes('/auth/signin')) {
|
|
||||||
console.log(`${page}: ✅ Properly redirects to sign-in`);
|
|
||||||
} else {
|
|
||||||
console.log(`${page}: ⚠️ Redirects to: ${location}`);
|
|
||||||
}
|
|
||||||
} else if (response.status === 200) {
|
|
||||||
console.log(`${page}: ❌ Accessible without authentication`);
|
|
||||||
} else {
|
|
||||||
console.log(`${page}: ❓ Status ${response.status}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 4: Test sign-in page accessibility
|
|
||||||
console.log('\n4️⃣ Testing sign-in page...');
|
|
||||||
const signinResponse = await fetch(`${BASE_URL}/auth/signin`);
|
|
||||||
if (signinResponse.ok) {
|
|
||||||
console.log('✅ Sign-in page accessible');
|
|
||||||
const content = await signinResponse.text();
|
|
||||||
const hasEmailField = content.includes('name="email"') || content.includes('id="email"');
|
|
||||||
const hasPasswordField = content.includes('name="password"') || content.includes('id="password"');
|
|
||||||
console.log(` Email field: ${hasEmailField ? '✅' : '❌'}`);
|
|
||||||
console.log(` Password field: ${hasPasswordField ? '✅' : '❌'}`);
|
|
||||||
} else {
|
|
||||||
console.log('❌ Sign-in page not accessible');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 5: Check NextAuth.js providers endpoint
|
|
||||||
console.log('\n5️⃣ Testing NextAuth.js configuration...');
|
|
||||||
const providersResponse = await fetch(`${BASE_URL}/api/auth/providers`);
|
|
||||||
if (providersResponse.ok) {
|
|
||||||
const providers = await providersResponse.json();
|
|
||||||
console.log('✅ NextAuth.js providers endpoint accessible');
|
|
||||||
console.log('Available providers:', Object.keys(providers));
|
|
||||||
} else {
|
|
||||||
console.log('❌ NextAuth.js providers endpoint failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 6: Check CSRF token endpoint
|
|
||||||
console.log('\n6️⃣ Testing CSRF token...');
|
|
||||||
const csrfResponse = await fetch(`${BASE_URL}/api/auth/csrf`);
|
|
||||||
if (csrfResponse.ok) {
|
|
||||||
const csrf = await csrfResponse.json();
|
|
||||||
console.log('✅ CSRF token endpoint accessible');
|
|
||||||
console.log('CSRF token available:', !!csrf.csrfToken);
|
|
||||||
} else {
|
|
||||||
console.log('❌ CSRF token endpoint failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🎯 Manual Testing Instructions:');
|
|
||||||
console.log('1. Open browser to: http://localhost:3000/auth/signin');
|
|
||||||
console.log('2. Use credentials:');
|
|
||||||
console.log(' Email: admin@localhost.com');
|
|
||||||
console.log(' Password: admin123456');
|
|
||||||
console.log('3. After login, test these pages:');
|
|
||||||
protectedPages.forEach(page => {
|
|
||||||
console.log(` - http://localhost:3000${page}`);
|
|
||||||
});
|
|
||||||
console.log('4. Test API endpoints with browser dev tools or Postman');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Test failed with error:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testAuthenticatedAPI();
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Test script to verify API route protection with better error handling
|
|
||||||
const BASE_URL = 'http://localhost:3000';
|
|
||||||
|
|
||||||
// Test unauthenticated access to protected routes
|
|
||||||
async function testProtectedRoutes() {
|
|
||||||
console.log('🔐 Testing Authorization Setup\n');
|
|
||||||
|
|
||||||
const protectedRoutes = [
|
|
||||||
'/api/projects',
|
|
||||||
'/api/contracts'
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Testing unauthenticated access to protected routes...\n');
|
|
||||||
|
|
||||||
for (const route of protectedRoutes) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BASE_URL}${route}`);
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
|
|
||||||
console.log(`Route: ${route}`);
|
|
||||||
console.log(`Status: ${response.status}`);
|
|
||||||
console.log(`Content-Type: ${contentType}`);
|
|
||||||
|
|
||||||
if (contentType && contentType.includes('application/json')) {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log(`Response: ${JSON.stringify(data)}`);
|
|
||||||
} else {
|
|
||||||
const text = await response.text();
|
|
||||||
console.log(`Response (first 200 chars): ${text.substring(0, 200)}...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('---\n');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ ${route} - ERROR: ${error.message}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testProtectedRoutes().catch(console.error);
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
// Test authenticated access to pages and API endpoints
|
|
||||||
const BASE_URL = 'http://localhost:3000';
|
|
||||||
|
|
||||||
// Helper to extract cookies from response headers
|
|
||||||
function extractCookies(response) {
|
|
||||||
const cookies = [];
|
|
||||||
const setCookieHeaders = response.headers.get('set-cookie');
|
|
||||||
if (setCookieHeaders) {
|
|
||||||
cookies.push(setCookieHeaders);
|
|
||||||
}
|
|
||||||
return cookies.join('; ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test authenticated access
|
|
||||||
async function testAuthenticatedAccess() {
|
|
||||||
console.log('🔐 Testing Authenticated Access\n');
|
|
||||||
|
|
||||||
// Step 1: Get the sign-in page to check if it loads
|
|
||||||
console.log('1️⃣ Testing sign-in page access...');
|
|
||||||
try {
|
|
||||||
const signInResponse = await fetch(`${BASE_URL}/auth/signin`);
|
|
||||||
console.log(`✅ Sign-in page: ${signInResponse.status} ${signInResponse.statusText}`);
|
|
||||||
|
|
||||||
if (signInResponse.status === 200) {
|
|
||||||
const pageContent = await signInResponse.text();
|
|
||||||
const hasForm = pageContent.includes('Sign in to your account');
|
|
||||||
console.log(` Form present: ${hasForm ? '✅ Yes' : '❌ No'}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ Sign-in page error: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n2️⃣ Testing authentication endpoint...');
|
|
||||||
|
|
||||||
// Step 2: Test the authentication API endpoint
|
|
||||||
try {
|
|
||||||
const sessionResponse = await fetch(`${BASE_URL}/api/auth/session`);
|
|
||||||
console.log(`✅ Session endpoint: ${sessionResponse.status} ${sessionResponse.statusText}`);
|
|
||||||
|
|
||||||
if (sessionResponse.status === 200) {
|
|
||||||
const sessionData = await sessionResponse.json();
|
|
||||||
console.log(` Session data: ${JSON.stringify(sessionData)}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ Session endpoint error: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n3️⃣ Testing CSRF token endpoint...');
|
|
||||||
|
|
||||||
// Step 3: Get CSRF token
|
|
||||||
try {
|
|
||||||
const csrfResponse = await fetch(`${BASE_URL}/api/auth/csrf`);
|
|
||||||
console.log(`✅ CSRF endpoint: ${csrfResponse.status} ${csrfResponse.statusText}`);
|
|
||||||
|
|
||||||
if (csrfResponse.status === 200) {
|
|
||||||
const csrfData = await csrfResponse.json();
|
|
||||||
console.log(` CSRF token: ${csrfData.csrfToken ? '✅ Present' : '❌ Missing'}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ CSRF endpoint error: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n4️⃣ Testing main dashboard page (unauthenticated)...');
|
|
||||||
|
|
||||||
// Step 4: Test main page redirect
|
|
||||||
try {
|
|
||||||
const mainPageResponse = await fetch(`${BASE_URL}/`, {
|
|
||||||
redirect: 'manual' // Don't follow redirects automatically
|
|
||||||
});
|
|
||||||
console.log(`✅ Main page: ${mainPageResponse.status} ${mainPageResponse.statusText}`);
|
|
||||||
|
|
||||||
if (mainPageResponse.status === 307 || mainPageResponse.status === 302) {
|
|
||||||
const location = mainPageResponse.headers.get('location');
|
|
||||||
console.log(` Redirects to: ${location}`);
|
|
||||||
console.log(` Correct redirect: ${location && location.includes('/auth/signin') ? '✅ Yes' : '❌ No'}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ Main page error: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n5️⃣ Testing projects page (unauthenticated)...');
|
|
||||||
|
|
||||||
// Step 5: Test projects page redirect
|
|
||||||
try {
|
|
||||||
const projectsPageResponse = await fetch(`${BASE_URL}/projects`, {
|
|
||||||
redirect: 'manual'
|
|
||||||
});
|
|
||||||
console.log(`✅ Projects page: ${projectsPageResponse.status} ${projectsPageResponse.statusText}`);
|
|
||||||
|
|
||||||
if (projectsPageResponse.status === 307 || projectsPageResponse.status === 302) {
|
|
||||||
const location = projectsPageResponse.headers.get('location');
|
|
||||||
console.log(` Redirects to: ${location}`);
|
|
||||||
console.log(` Correct redirect: ${location && location.includes('/auth/signin') ? '✅ Yes' : '❌ No'}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ Projects page error: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n6️⃣ Testing API endpoints (unauthenticated)...');
|
|
||||||
|
|
||||||
// Step 6: Test API endpoints
|
|
||||||
const apiEndpoints = ['/api/projects', '/api/contracts', '/api/tasks/templates'];
|
|
||||||
|
|
||||||
for (const endpoint of apiEndpoints) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BASE_URL}${endpoint}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
console.log(`✅ ${endpoint}: Protected (401) - ${data.error}`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${endpoint}: Not protected (${response.status})`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ ${endpoint}: Error - ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n📋 Summary:');
|
|
||||||
console.log('- Sign-in page should be accessible');
|
|
||||||
console.log('- Protected pages should redirect to /auth/signin');
|
|
||||||
console.log('- Protected API endpoints should return 401 with JSON error');
|
|
||||||
console.log('- Auth endpoints (/api/auth/*) should be accessible');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testAuthenticatedAccess().catch(console.error);
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { auth } from "@/lib/auth";
|
|
||||||
|
|
||||||
// Test what the auth session looks like
|
|
||||||
console.log("Testing authentication session structure...\n");
|
|
||||||
|
|
||||||
async function testAuth() {
|
|
||||||
try {
|
|
||||||
// Create a mock request
|
|
||||||
const mockReq = {
|
|
||||||
url: "http://localhost:3000/api/projects",
|
|
||||||
method: "GET",
|
|
||||||
headers: new Map([
|
|
||||||
["cookie", ""], // Add any cookies if needed
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is how the auth middleware would wrap a handler
|
|
||||||
const testHandler = auth(async (req) => {
|
|
||||||
console.log("=== Authentication Session Debug ===");
|
|
||||||
console.log("req.auth:", JSON.stringify(req.auth, null, 2));
|
|
||||||
console.log("req.auth?.user:", JSON.stringify(req.auth?.user, null, 2));
|
|
||||||
console.log("req.auth?.user?.id:", req.auth?.user?.id);
|
|
||||||
console.log("req.user:", JSON.stringify(req.user, null, 2));
|
|
||||||
console.log("req.user?.id:", req.user?.id);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
// This would normally be called by Next.js
|
|
||||||
const result = await testHandler(mockReq);
|
|
||||||
console.log("Handler result:", result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Auth test failed:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testAuth();
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Test script to verify API route protection
|
|
||||||
const BASE_URL = 'http://localhost:3000';
|
|
||||||
|
|
||||||
// Test unauthenticated access to protected routes
|
|
||||||
async function testProtectedRoutes() {
|
|
||||||
console.log('🔐 Testing Authorization Setup\n');
|
|
||||||
|
|
||||||
const protectedRoutes = [
|
|
||||||
'/api/projects',
|
|
||||||
'/api/contracts',
|
|
||||||
'/api/tasks/templates',
|
|
||||||
'/api/project-tasks',
|
|
||||||
'/api/notes',
|
|
||||||
'/api/all-project-tasks'
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Testing unauthenticated access to protected routes...\n');
|
|
||||||
|
|
||||||
for (const route of protectedRoutes) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BASE_URL}${route}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
console.log(`✅ ${route} - PROTECTED (401 Unauthorized)`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${route} - NOT PROTECTED (${response.status})`);
|
|
||||||
console.log(` Response: ${JSON.stringify(data).substring(0, 100)}...`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ ${route} - ERROR: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🔍 Testing authentication endpoint...\n');
|
|
||||||
|
|
||||||
// Test NextAuth endpoint
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BASE_URL}/api/auth/session`);
|
|
||||||
const data = await response.json();
|
|
||||||
console.log(`✅ /api/auth/session - Available (${response.status})`);
|
|
||||||
console.log(` Response: ${JSON.stringify(data)}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ /api/auth/session - ERROR: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testProtectedRoutes().catch(console.error);
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// Complete authentication flow test
|
|
||||||
const BASE_URL = 'http://localhost:3000';
|
|
||||||
|
|
||||||
async function testCompleteAuthFlow() {
|
|
||||||
console.log('🔐 Testing Complete Authentication Flow\n');
|
|
||||||
|
|
||||||
// Test 1: Verify unauthenticated access is properly blocked
|
|
||||||
console.log('1️⃣ Testing unauthenticated access protection...');
|
|
||||||
|
|
||||||
const protectedRoutes = [
|
|
||||||
{ path: '/', name: 'Dashboard' },
|
|
||||||
{ path: '/projects', name: 'Projects Page' },
|
|
||||||
{ path: '/tasks/templates', name: 'Tasks Page' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const route of protectedRoutes) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BASE_URL}${route.path}`, {
|
|
||||||
redirect: 'manual'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 302 || response.status === 307) {
|
|
||||||
const location = response.headers.get('location');
|
|
||||||
if (location && location.includes('/auth/signin')) {
|
|
||||||
console.log(` ✅ ${route.name}: Properly redirects to sign-in`);
|
|
||||||
} else {
|
|
||||||
console.log(` ❌ ${route.name}: Redirects to wrong location: ${location}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(` ❌ ${route.name}: Not protected (${response.status})`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ ${route.name}: Error - ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: Verify API protection
|
|
||||||
console.log('\n2️⃣ Testing API protection...');
|
|
||||||
|
|
||||||
const apiRoutes = ['/api/projects', '/api/contracts', '/api/tasks/templates'];
|
|
||||||
|
|
||||||
for (const route of apiRoutes) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BASE_URL}${route}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.status === 401 && data.error === 'Authentication required') {
|
|
||||||
console.log(` ✅ ${route}: Properly protected`);
|
|
||||||
} else {
|
|
||||||
console.log(` ❌ ${route}: Not protected (${response.status}) - ${JSON.stringify(data)}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ ${route}: Error - ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 3: Verify auth endpoints work
|
|
||||||
console.log('\n3️⃣ Testing NextAuth endpoints...');
|
|
||||||
|
|
||||||
const authEndpoints = [
|
|
||||||
{ path: '/api/auth/session', name: 'Session' },
|
|
||||||
{ path: '/api/auth/providers', name: 'Providers' },
|
|
||||||
{ path: '/api/auth/csrf', name: 'CSRF' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const endpoint of authEndpoints) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BASE_URL}${endpoint.path}`);
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
console.log(` ✅ ${endpoint.name}: Working (200)`);
|
|
||||||
} else {
|
|
||||||
console.log(` ❌ ${endpoint.name}: Error (${response.status})`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ ${endpoint.name}: Error - ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 4: Verify sign-in page accessibility
|
|
||||||
console.log('\n4️⃣ Testing sign-in page...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BASE_URL}/auth/signin`);
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
const html = await response.text();
|
|
||||||
const hasForm = html.includes('Sign in to your account');
|
|
||||||
const hasEmailField = html.includes('email');
|
|
||||||
const hasPasswordField = html.includes('password');
|
|
||||||
|
|
||||||
console.log(` ✅ Sign-in page: Accessible (200)`);
|
|
||||||
console.log(` ✅ Form present: ${hasForm ? 'Yes' : 'No'}`);
|
|
||||||
console.log(` ✅ Email field: ${hasEmailField ? 'Yes' : 'No'}`);
|
|
||||||
console.log(` ✅ Password field: ${hasPasswordField ? 'Yes' : 'No'}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ❌ Sign-in page: Error (${response.status})`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ Sign-in page: Error - ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n📋 Summary:');
|
|
||||||
console.log('✅ All protected pages redirect to sign-in');
|
|
||||||
console.log('✅ All API endpoints require authentication');
|
|
||||||
console.log('✅ NextAuth endpoints are functional');
|
|
||||||
console.log('✅ Sign-in page is accessible and complete');
|
|
||||||
console.log('\n🎉 Authentication system is fully functional!');
|
|
||||||
console.log('\n📝 Next steps:');
|
|
||||||
console.log(' • Visit http://localhost:3000/auth/signin');
|
|
||||||
console.log(' • Login with: admin@localhost / admin123456');
|
|
||||||
console.log(' • Access the protected application!');
|
|
||||||
}
|
|
||||||
|
|
||||||
testCompleteAuthFlow().catch(console.error);
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import db from './src/lib/db.js';
|
|
||||||
|
|
||||||
console.log('Testing contacts query...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Basic query
|
|
||||||
console.log('Test 1: Basic contact query');
|
|
||||||
const basic = db.prepare('SELECT contact_id, name FROM contacts WHERE is_active = 1 LIMIT 3').all();
|
|
||||||
console.log('✓ Basic query works:', basic.length, 'contacts\n');
|
|
||||||
|
|
||||||
// Test 2: With LEFT JOIN
|
|
||||||
console.log('Test 2: With project_contacts join');
|
|
||||||
const withJoin = db.prepare(`
|
|
||||||
SELECT c.contact_id, c.name, COUNT(pc.project_id) as count
|
|
||||||
FROM contacts c
|
|
||||||
LEFT JOIN project_contacts pc ON c.contact_id = pc.contact_id
|
|
||||||
WHERE c.is_active = 1
|
|
||||||
GROUP BY c.contact_id
|
|
||||||
LIMIT 3
|
|
||||||
`).all();
|
|
||||||
console.log('✓ Join query works:', withJoin.length, 'contacts\n');
|
|
||||||
|
|
||||||
// Test 3: With both joins
|
|
||||||
console.log('Test 3: With both joins (no CASE)');
|
|
||||||
const bothJoins = db.prepare(`
|
|
||||||
SELECT c.contact_id, c.name, COUNT(p.project_id) as count
|
|
||||||
FROM contacts c
|
|
||||||
LEFT JOIN project_contacts pc ON c.contact_id = pc.contact_id
|
|
||||||
LEFT JOIN projects p ON pc.project_id = p.project_id
|
|
||||||
WHERE c.is_active = 1
|
|
||||||
GROUP BY c.contact_id
|
|
||||||
LIMIT 3
|
|
||||||
`).all();
|
|
||||||
console.log('✓ Both joins work:', bothJoins.length, 'contacts\n');
|
|
||||||
|
|
||||||
// Test 4: With CASE statement
|
|
||||||
console.log('Test 4: With CASE statement');
|
|
||||||
const withCase = db.prepare(`
|
|
||||||
SELECT c.contact_id, c.name,
|
|
||||||
COUNT(DISTINCT CASE WHEN p.is_deleted = 0 THEN p.project_id ELSE NULL END) as count
|
|
||||||
FROM contacts c
|
|
||||||
LEFT JOIN project_contacts pc ON c.contact_id = pc.contact_id
|
|
||||||
LEFT JOIN projects p ON pc.project_id = p.project_id
|
|
||||||
WHERE c.is_active = 1
|
|
||||||
GROUP BY c.contact_id
|
|
||||||
LIMIT 3
|
|
||||||
`).all();
|
|
||||||
console.log('✓ CASE query works:', withCase.length, 'contacts');
|
|
||||||
withCase.forEach(c => console.log(` ${c.name}: ${c.count} active projects`));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Query failed:', error.message);
|
|
||||||
console.error(error.stack);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { createProject } from "./src/lib/queries/projects.js";
|
|
||||||
import initializeDatabase from "./src/lib/init-db.js";
|
|
||||||
|
|
||||||
// Initialize database
|
|
||||||
initializeDatabase();
|
|
||||||
|
|
||||||
console.log("Testing createProject function...\n");
|
|
||||||
|
|
||||||
const testProjectData = {
|
|
||||||
contract_id: 1, // Assuming contract 1 exists
|
|
||||||
project_name: "Test Project - User Tracking",
|
|
||||||
address: "Test Address 123",
|
|
||||||
plot: "123/456",
|
|
||||||
district: "Test District",
|
|
||||||
unit: "Test Unit",
|
|
||||||
city: "Test City",
|
|
||||||
investment_number: "TEST-2025-001",
|
|
||||||
finish_date: "2025-12-31",
|
|
||||||
wp: "TEST/2025/001",
|
|
||||||
contact: "test@example.com",
|
|
||||||
notes: "Test project with user tracking",
|
|
||||||
project_type: "design",
|
|
||||||
project_status: "registered",
|
|
||||||
coordinates: "50.0,20.0",
|
|
||||||
assigned_to: "e42a4b036074ff7233942a0728557141", // admin user ID
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Creating test project with admin user as creator...");
|
|
||||||
const result = createProject(
|
|
||||||
testProjectData,
|
|
||||||
"e42a4b036074ff7233942a0728557141"
|
|
||||||
);
|
|
||||||
console.log("✅ Project created successfully!");
|
|
||||||
console.log("Result:", result);
|
|
||||||
console.log("Project ID:", result.lastInsertRowid);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error creating project:", error.message);
|
|
||||||
console.error("Stack:", error.stack);
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import {
|
|
||||||
logAuditEvent,
|
|
||||||
getAuditLogs,
|
|
||||||
getAuditLogStats,
|
|
||||||
AUDIT_ACTIONS,
|
|
||||||
RESOURCE_TYPES,
|
|
||||||
} from "./src/lib/auditLog.js";
|
|
||||||
|
|
||||||
// Test audit logging functionality
|
|
||||||
console.log("Testing Audit Logging System...\n");
|
|
||||||
|
|
||||||
async function testAuditLogging() {
|
|
||||||
try {
|
|
||||||
// Test 1: Check existing audit logs
|
|
||||||
console.log("1. Checking existing audit logs...");
|
|
||||||
const existingLogs = await getAuditLogs({ limit: 10 });
|
|
||||||
console.log(`Found ${existingLogs.length} existing audit events`);
|
|
||||||
|
|
||||||
if (existingLogs.length > 0) {
|
|
||||||
console.log("\nLatest audit events:");
|
|
||||||
existingLogs.slice(0, 5).forEach((log, index) => {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${log.timestamp} - ${log.action} by user ${
|
|
||||||
log.user_id || "NULL"
|
|
||||||
} on ${log.resource_type}:${log.resource_id || "N/A"}`
|
|
||||||
);
|
|
||||||
if (log.details) {
|
|
||||||
console.log(
|
|
||||||
` Details: ${JSON.stringify(JSON.parse(log.details), null, 2)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for null userIds
|
|
||||||
const nullUserIdLogs = await getAuditLogs();
|
|
||||||
const nullUserCount = nullUserIdLogs.filter(
|
|
||||||
(log) => log.user_id === null
|
|
||||||
).length;
|
|
||||||
console.log(
|
|
||||||
`\nFound ${nullUserCount} audit events with NULL user_id out of ${nullUserIdLogs.length} total`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: Log some sample events with different user scenarios
|
|
||||||
console.log("\n2. Creating sample audit events...");
|
|
||||||
|
|
||||||
await logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.LOGIN,
|
|
||||||
userId: "user123",
|
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
|
||||||
ipAddress: "192.168.1.100",
|
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
||||||
details: {
|
|
||||||
email: "test@example.com",
|
|
||||||
role: "user",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_CREATE,
|
|
||||||
userId: "user123",
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "proj-456",
|
|
||||||
ipAddress: "192.168.1.100",
|
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
||||||
details: {
|
|
||||||
project_name: "Test Project",
|
|
||||||
project_number: "TP-001",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test null userId scenario
|
|
||||||
await logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.LOGIN_FAILED,
|
|
||||||
userId: null,
|
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
|
||||||
ipAddress: "192.168.1.102",
|
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
||||||
details: {
|
|
||||||
email: "hacker@evil.com",
|
|
||||||
reason: "invalid_password",
|
|
||||||
failed_attempts: 3,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Sample events created!\n");
|
|
||||||
|
|
||||||
// Test 3: Check new logs
|
|
||||||
console.log("3. Checking audit logs after test events...");
|
|
||||||
const newLogs = await getAuditLogs({ limit: 5 });
|
|
||||||
console.log(`Latest 5 audit events:`);
|
|
||||||
newLogs.forEach((log, index) => {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${log.timestamp} - ${log.action} by user ${
|
|
||||||
log.user_id || "NULL"
|
|
||||||
} on ${log.resource_type}:${log.resource_id || "N/A"}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 4: Statistics
|
|
||||||
console.log("\n4. Getting audit log statistics...");
|
|
||||||
const stats = await getAuditLogStats();
|
|
||||||
console.log(`Total events: ${stats.total}`);
|
|
||||||
|
|
||||||
console.log("\nAction breakdown:");
|
|
||||||
stats.actionBreakdown.forEach((item) => {
|
|
||||||
console.log(` ${item.action}: ${item.count}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("\nUser breakdown:");
|
|
||||||
stats.userBreakdown.slice(0, 5).forEach((item) => {
|
|
||||||
console.log(
|
|
||||||
` ${item.user_id || "NULL"} (${item.user_name || "Unknown"}): ${
|
|
||||||
item.count
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Test failed:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testAuditLogging();
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// Test script to verify date formatting
|
|
||||||
import { formatDate, formatDateForInput } from "./src/lib/utils.js";
|
|
||||||
|
|
||||||
console.log("Testing Date Formatting Functions...\n");
|
|
||||||
|
|
||||||
// Test cases
|
|
||||||
const testDates = [
|
|
||||||
"2024-01-15",
|
|
||||||
"2024-12-25T14:30:00",
|
|
||||||
"2024-06-01",
|
|
||||||
new Date("2024-03-10"),
|
|
||||||
new Date("2024-09-22T09:15:30"),
|
|
||||||
null,
|
|
||||||
undefined,
|
|
||||||
"invalid-date",
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log("formatDate() tests (DD.MM.YYYY format):");
|
|
||||||
testDates.forEach((date, index) => {
|
|
||||||
try {
|
|
||||||
const result = formatDate(date);
|
|
||||||
console.log(`${index + 1}. ${JSON.stringify(date)} -> "${result}"`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${JSON.stringify(date)} -> ERROR: ${error.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("\nformatDate() with time tests (DD.MM.YYYY HH:MM format):");
|
|
||||||
testDates.forEach((date, index) => {
|
|
||||||
try {
|
|
||||||
const result = formatDate(date, { includeTime: true });
|
|
||||||
console.log(`${index + 1}. ${JSON.stringify(date)} -> "${result}"`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${JSON.stringify(date)} -> ERROR: ${error.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"\nformatDateForInput() tests (YYYY-MM-DD format for HTML inputs):"
|
|
||||||
);
|
|
||||||
testDates.forEach((date, index) => {
|
|
||||||
try {
|
|
||||||
const result = formatDateForInput(date);
|
|
||||||
console.log(`${index + 1}. ${JSON.stringify(date)} -> "${result}"`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${JSON.stringify(date)} -> ERROR: ${error.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("\nDate formatting verification complete!");
|
|
||||||
@@ -1,417 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Comprehensive Dropdown Test</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<style>
|
|
||||||
.test-container {
|
|
||||||
border: 2px solid #10b981;
|
|
||||||
background: #f0fdf4;
|
|
||||||
}
|
|
||||||
.dropdown-test {
|
|
||||||
border: 1px solid #6b7280;
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.status-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.status-warning {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
.status-primary {
|
|
||||||
background: #dbeafe;
|
|
||||||
color: #1e40af;
|
|
||||||
}
|
|
||||||
.status-success {
|
|
||||||
background: #d1fae5;
|
|
||||||
color: #065f46;
|
|
||||||
}
|
|
||||||
.status-danger {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #991b1b;
|
|
||||||
}
|
|
||||||
.status-secondary {
|
|
||||||
background: #f3f4f6;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="p-8 bg-gray-50">
|
|
||||||
<div class="max-w-4xl mx-auto">
|
|
||||||
<h1 class="text-3xl font-bold mb-6 text-gray-900">
|
|
||||||
Dropdown Component Validation
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Test 1: Basic Dropdown Functionality -->
|
|
||||||
<div class="test-container p-6 mb-6 rounded-lg">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-green-800">
|
|
||||||
✅ Test 1: Basic Dropdown Structure
|
|
||||||
</h2>
|
|
||||||
<div class="dropdown-test">
|
|
||||||
<label class="block text-sm font-medium mb-2"
|
|
||||||
>Task Status Dropdown Simulation:</label
|
|
||||||
>
|
|
||||||
<div class="relative inline-block">
|
|
||||||
<button id="task-status-btn" class="status-badge status-warning">
|
|
||||||
Pending
|
|
||||||
<svg
|
|
||||||
class="w-3 h-3 ml-1"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
id="task-status-dropdown"
|
|
||||||
class="hidden absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[120px]"
|
|
||||||
>
|
|
||||||
<div class="bg-yellow-100 p-2 text-xs text-center border-b">
|
|
||||||
DEBUG: Task Status Visible
|
|
||||||
</div>
|
|
||||||
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">
|
|
||||||
<span class="status-badge status-warning">Pending</span>
|
|
||||||
</button>
|
|
||||||
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">
|
|
||||||
<span class="status-badge status-primary">In Progress</span>
|
|
||||||
</button>
|
|
||||||
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">
|
|
||||||
<span class="status-badge status-success">Completed</span>
|
|
||||||
</button>
|
|
||||||
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">
|
|
||||||
<span class="status-badge status-danger">Cancelled</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dropdown-test">
|
|
||||||
<label class="block text-sm font-medium mb-2"
|
|
||||||
>Project Status Dropdown Simulation:</label
|
|
||||||
>
|
|
||||||
<div class="relative inline-block">
|
|
||||||
<button
|
|
||||||
id="project-status-btn"
|
|
||||||
class="status-badge status-secondary"
|
|
||||||
>
|
|
||||||
Registered
|
|
||||||
<svg
|
|
||||||
class="w-3 h-3 ml-1"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
id="project-status-dropdown"
|
|
||||||
class="hidden absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[140px]"
|
|
||||||
>
|
|
||||||
<div class="bg-yellow-100 p-2 text-xs text-center border-b">
|
|
||||||
DEBUG: Project Status Visible
|
|
||||||
</div>
|
|
||||||
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">
|
|
||||||
<span class="status-badge status-secondary">Registered</span>
|
|
||||||
</button>
|
|
||||||
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">
|
|
||||||
<span class="status-badge status-primary"
|
|
||||||
>In Progress (Design)</span
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">
|
|
||||||
<span class="status-badge status-primary"
|
|
||||||
>In Progress (Construction)</span
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">
|
|
||||||
<span class="status-badge status-success">Completed</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Test 2: Table Context -->
|
|
||||||
<div class="test-container p-6 mb-6 rounded-lg">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-green-800">
|
|
||||||
✅ Test 2: Dropdown in Table Context
|
|
||||||
</h2>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="min-w-full bg-white border border-gray-200 rounded-lg">
|
|
||||||
<thead class="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase"
|
|
||||||
>
|
|
||||||
Task
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase"
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase"
|
|
||||||
>
|
|
||||||
Project Status
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td class="px-4 py-4 text-sm text-gray-900">Sample Task 1</td>
|
|
||||||
<td class="px-4 py-4">
|
|
||||||
<div class="relative inline-block">
|
|
||||||
<button
|
|
||||||
id="table-task-btn"
|
|
||||||
class="status-badge status-primary"
|
|
||||||
>
|
|
||||||
In Progress
|
|
||||||
<svg
|
|
||||||
class="w-3 h-3 ml-1"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
id="table-task-dropdown"
|
|
||||||
class="hidden absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[120px]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-yellow-100 p-2 text-xs text-center border-b"
|
|
||||||
>
|
|
||||||
DEBUG: Table Task Visible
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-3 py-2 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<span class="status-badge status-warning">Pending</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-3 py-2 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<span class="status-badge status-primary"
|
|
||||||
>In Progress</span
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-3 py-2 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<span class="status-badge status-success"
|
|
||||||
>Completed</span
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-4 py-4">
|
|
||||||
<div class="relative inline-block">
|
|
||||||
<button
|
|
||||||
id="table-project-btn"
|
|
||||||
class="status-badge status-primary"
|
|
||||||
>
|
|
||||||
In Progress (Design)
|
|
||||||
<svg
|
|
||||||
class="w-3 h-3 ml-1"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
id="table-project-dropdown"
|
|
||||||
class="hidden absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[140px]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-yellow-100 p-2 text-xs text-center border-b"
|
|
||||||
>
|
|
||||||
DEBUG: Table Project Visible
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-3 py-2 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<span class="status-badge status-secondary"
|
|
||||||
>Registered</span
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-3 py-2 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<span class="status-badge status-primary"
|
|
||||||
>In Progress (Design)</span
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-3 py-2 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<span class="status-badge status-success"
|
|
||||||
>Completed</span
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Test Results -->
|
|
||||||
<div class="bg-blue-50 border-2 border-blue-200 p-6 rounded-lg">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-blue-800">
|
|
||||||
🧪 Test Results
|
|
||||||
</h2>
|
|
||||||
<div id="test-results" class="space-y-2 text-sm">
|
|
||||||
<p>⏳ Click the dropdown buttons above to test functionality...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 p-4 bg-white rounded border">
|
|
||||||
<h3 class="font-medium mb-2">Expected Behavior:</h3>
|
|
||||||
<ul class="text-sm space-y-1 text-gray-700">
|
|
||||||
<li>✅ Dropdowns should appear immediately when clicked</li>
|
|
||||||
<li>✅ Red border and yellow debug header should be visible</li>
|
|
||||||
<li>
|
|
||||||
✅ Dropdown should appear above all other elements (z-index test)
|
|
||||||
</li>
|
|
||||||
<li>✅ Clicking outside should close the dropdown</li>
|
|
||||||
<li>✅ Dropdown should not be clipped by table overflow</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const dropdowns = [
|
|
||||||
{
|
|
||||||
btn: "task-status-btn",
|
|
||||||
dropdown: "task-status-dropdown",
|
|
||||||
name: "Task Status",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
btn: "project-status-btn",
|
|
||||||
dropdown: "project-status-dropdown",
|
|
||||||
name: "Project Status",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
btn: "table-task-btn",
|
|
||||||
dropdown: "table-task-dropdown",
|
|
||||||
name: "Table Task Status",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
btn: "table-project-btn",
|
|
||||||
dropdown: "table-project-dropdown",
|
|
||||||
name: "Table Project Status",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = document.getElementById("test-results");
|
|
||||||
let testCount = 0;
|
|
||||||
|
|
||||||
function addResult(message, type = "info") {
|
|
||||||
testCount++;
|
|
||||||
const colors = {
|
|
||||||
success: "text-green-700",
|
|
||||||
error: "text-red-700",
|
|
||||||
info: "text-blue-700",
|
|
||||||
};
|
|
||||||
results.innerHTML += `<p class="${colors[type]}">${testCount}. ${message}</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
dropdowns.forEach(({ btn, dropdown, name }) => {
|
|
||||||
const button = document.getElementById(btn);
|
|
||||||
const dropdownEl = document.getElementById(dropdown);
|
|
||||||
|
|
||||||
button.addEventListener("click", function (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Close all other dropdowns
|
|
||||||
dropdowns.forEach(({ dropdown: otherId }) => {
|
|
||||||
if (otherId !== dropdown) {
|
|
||||||
document.getElementById(otherId).classList.add("hidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Toggle current dropdown
|
|
||||||
const isHidden = dropdownEl.classList.contains("hidden");
|
|
||||||
dropdownEl.classList.toggle("hidden");
|
|
||||||
|
|
||||||
if (isHidden) {
|
|
||||||
addResult(`${name} dropdown opened successfully`, "success");
|
|
||||||
|
|
||||||
// Test visibility
|
|
||||||
const rect = dropdownEl.getBoundingClientRect();
|
|
||||||
if (rect.width > 0 && rect.height > 0) {
|
|
||||||
addResult(
|
|
||||||
`${name} dropdown is visible (${rect.width}x${rect.height}px)`,
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
addResult(`${name} dropdown has zero dimensions!`, "error");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test z-index
|
|
||||||
const computedStyle = window.getComputedStyle(dropdownEl);
|
|
||||||
addResult(
|
|
||||||
`${name} dropdown z-index: ${computedStyle.zIndex}`,
|
|
||||||
"info"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
addResult(`${name} dropdown closed`, "info");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdowns when clicking outside
|
|
||||||
document.addEventListener("click", function () {
|
|
||||||
dropdowns.forEach(({ dropdown }) => {
|
|
||||||
document.getElementById(dropdown).classList.add("hidden");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial test message
|
|
||||||
setTimeout(() => {
|
|
||||||
if (testCount === 0) {
|
|
||||||
addResult("Waiting for user interaction...", "info");
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Dropdown Test</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<style>
|
|
||||||
.debug-border {
|
|
||||||
border: 2px solid red !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="p-8 bg-gray-100">
|
|
||||||
<h1 class="text-2xl mb-4">Dropdown Visibility Test</h1>
|
|
||||||
|
|
||||||
<!-- Test basic dropdown structure -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<h2 class="text-lg mb-2">Basic Dropdown Test</h2>
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
|
||||||
id="test-btn"
|
|
||||||
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Click me
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
id="test-dropdown"
|
|
||||||
class="hidden absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-[9999] min-w-[120px]"
|
|
||||||
>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50">Option 1</div>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50">Option 2</div>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50">Option 3</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Test with high z-index -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<h2 class="text-lg mb-2">High Z-Index Test</h2>
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
|
||||||
id="test-btn-2"
|
|
||||||
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
|
|
||||||
>
|
|
||||||
Click me (z-index 9999)
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
id="test-dropdown-2"
|
|
||||||
class="hidden absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[140px]"
|
|
||||||
>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50 bg-yellow-100">
|
|
||||||
High Z Option 1
|
|
||||||
</div>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50 bg-yellow-100">
|
|
||||||
High Z Option 2
|
|
||||||
</div>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50 bg-yellow-100">
|
|
||||||
High Z Option 3
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Test in table container -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<h2 class="text-lg mb-2">Table Container Test</h2>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="min-w-full bg-white border border-gray-200">
|
|
||||||
<thead class="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 text-left">Name</th>
|
|
||||||
<th class="px-6 py-3 text-left">Status</th>
|
|
||||||
<th class="px-6 py-3 text-left">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr class="border-t">
|
|
||||||
<td class="px-6 py-4">Test Task</td>
|
|
||||||
<td class="px-6 py-4">
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
|
||||||
id="table-btn"
|
|
||||||
class="bg-purple-500 text-white px-3 py-1 rounded text-sm"
|
|
||||||
>
|
|
||||||
Status Dropdown
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
id="table-dropdown"
|
|
||||||
class="hidden absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-[9999] min-w-[120px]"
|
|
||||||
>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50 bg-blue-100">
|
|
||||||
Pending
|
|
||||||
</div>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50 bg-green-100">
|
|
||||||
In Progress
|
|
||||||
</div>
|
|
||||||
<div class="px-3 py-2 hover:bg-gray-50 bg-red-100">
|
|
||||||
Completed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4">Edit</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Add click handlers
|
|
||||||
document
|
|
||||||
.getElementById("test-btn")
|
|
||||||
.addEventListener("click", function () {
|
|
||||||
const dropdown = document.getElementById("test-dropdown");
|
|
||||||
dropdown.classList.toggle("hidden");
|
|
||||||
console.log(
|
|
||||||
"Dropdown 1 toggled, hidden:",
|
|
||||||
dropdown.classList.contains("hidden")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById("test-btn-2")
|
|
||||||
.addEventListener("click", function () {
|
|
||||||
const dropdown = document.getElementById("test-dropdown-2");
|
|
||||||
dropdown.classList.toggle("hidden");
|
|
||||||
console.log(
|
|
||||||
"Dropdown 2 toggled, hidden:",
|
|
||||||
dropdown.classList.contains("hidden")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById("table-btn")
|
|
||||||
.addEventListener("click", function () {
|
|
||||||
const dropdown = document.getElementById("table-dropdown");
|
|
||||||
dropdown.classList.toggle("hidden");
|
|
||||||
console.log(
|
|
||||||
"Table dropdown toggled, hidden:",
|
|
||||||
dropdown.classList.contains("hidden")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdowns when clicking outside
|
|
||||||
document.addEventListener("click", function (e) {
|
|
||||||
if (!e.target.closest(".relative")) {
|
|
||||||
document.querySelectorAll('[id$="-dropdown"]').forEach((dropdown) => {
|
|
||||||
dropdown.classList.add("hidden");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test script to simulate due date reminders
|
|
||||||
* Creates a test project due in 3 days and runs the reminder script
|
|
||||||
*/
|
|
||||||
|
|
||||||
import db from "./src/lib/db.js";
|
|
||||||
import { addDays, format } from "date-fns";
|
|
||||||
|
|
||||||
async function createTestProject() {
|
|
||||||
try {
|
|
||||||
console.log("🧪 Creating test project due in 3 days...");
|
|
||||||
|
|
||||||
// Create a test contract first
|
|
||||||
const contractResult = db.prepare(`
|
|
||||||
INSERT INTO contracts (contract_number, contract_name, customer, date_signed)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
`).run('TEST-001', 'Test Contract', 'Test Customer', new Date().toISOString());
|
|
||||||
|
|
||||||
const contractId = contractResult.lastInsertRowid;
|
|
||||||
|
|
||||||
// Create a test project due in 3 days
|
|
||||||
const dueDate = addDays(new Date(), 3);
|
|
||||||
const projectResult = db.prepare(`
|
|
||||||
INSERT INTO projects (
|
|
||||||
contract_id, project_name, project_number, address,
|
|
||||||
finish_date, project_status
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
`).run(
|
|
||||||
contractId,
|
|
||||||
'Test Project - Due Soon',
|
|
||||||
'1/TEST-001',
|
|
||||||
'Test Address 123',
|
|
||||||
dueDate.toISOString(),
|
|
||||||
'in_progress_design'
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Created test project due on ${format(dueDate, 'yyyy-MM-dd')}`);
|
|
||||||
console.log("🔄 Running due date reminders script...");
|
|
||||||
|
|
||||||
// Run the reminders script
|
|
||||||
const { execSync } = await import('child_process');
|
|
||||||
execSync('node send-due-date-reminders.mjs', { stdio: 'inherit' });
|
|
||||||
|
|
||||||
// Check if notifications were created
|
|
||||||
const notifications = db.prepare(`
|
|
||||||
SELECT * FROM notifications
|
|
||||||
WHERE type = 'due_date_reminder'
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 5
|
|
||||||
`).all();
|
|
||||||
|
|
||||||
console.log(`📢 Found ${notifications.length} due date reminder notifications:`);
|
|
||||||
notifications.forEach(notif => {
|
|
||||||
console.log(` - ${notif.title}: ${notif.message.substring(0, 100)}...`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up test data
|
|
||||||
console.log("🧹 Cleaning up test data...");
|
|
||||||
db.prepare('DELETE FROM projects WHERE project_name = ?').run('Test Project - Due Soon');
|
|
||||||
db.prepare('DELETE FROM contracts WHERE contract_number = ?').run('TEST-001');
|
|
||||||
db.prepare('DELETE FROM notifications WHERE type = ? AND title LIKE ?').run('due_date_reminder', 'Projekt kończy się za%');
|
|
||||||
|
|
||||||
console.log("✅ Test completed successfully!");
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Test failed:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createTestProject();
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test Edge Runtime compatibility for audit logging
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Test Edge Runtime detection
|
|
||||||
console.log("Testing Edge Runtime compatibility...\n");
|
|
||||||
|
|
||||||
// Simulate Edge Runtime environment
|
|
||||||
const originalEdgeRuntime = global.EdgeRuntime;
|
|
||||||
const originalNextRuntime = process.env.NEXT_RUNTIME;
|
|
||||||
|
|
||||||
console.log("1. Testing in simulated Edge Runtime environment...");
|
|
||||||
global.EdgeRuntime = "edge";
|
|
||||||
process.env.NEXT_RUNTIME = "edge";
|
|
||||||
|
|
||||||
// Import the audit logging functions
|
|
||||||
const { logAuditEvent, getAuditLogs, AUDIT_ACTIONS, RESOURCE_TYPES } =
|
|
||||||
await import("./src/lib/auditLog.js");
|
|
||||||
|
|
||||||
// Test logging in Edge Runtime
|
|
||||||
logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_VIEW,
|
|
||||||
userId: "test-user",
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "test-project",
|
|
||||||
details: { test: "edge runtime test" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test querying in Edge Runtime
|
|
||||||
const logs = getAuditLogs({ limit: 10 });
|
|
||||||
console.log(`Queried logs in Edge Runtime: ${logs.length} results`);
|
|
||||||
|
|
||||||
console.log("2. Testing in simulated Node.js Runtime environment...");
|
|
||||||
// Restore Node.js environment
|
|
||||||
delete global.EdgeRuntime;
|
|
||||||
delete process.env.NEXT_RUNTIME;
|
|
||||||
|
|
||||||
// Test logging in Node.js Runtime
|
|
||||||
try {
|
|
||||||
logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_CREATE,
|
|
||||||
userId: "test-user",
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "test-project-2",
|
|
||||||
details: { test: "nodejs runtime test" },
|
|
||||||
});
|
|
||||||
console.log("Node.js runtime logging: ✅ Success");
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Node.js runtime logging: ❌ Error:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test querying in Node.js Runtime
|
|
||||||
try {
|
|
||||||
const nodeLogs = getAuditLogs({ limit: 10 });
|
|
||||||
console.log(
|
|
||||||
`Node.js runtime querying: ✅ Success (${nodeLogs.length} results)`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Node.js runtime querying: ❌ Error:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore original environment
|
|
||||||
if (originalEdgeRuntime !== undefined) {
|
|
||||||
global.EdgeRuntime = originalEdgeRuntime;
|
|
||||||
} else {
|
|
||||||
delete global.EdgeRuntime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalNextRuntime !== undefined) {
|
|
||||||
process.env.NEXT_RUNTIME = originalNextRuntime;
|
|
||||||
} else {
|
|
||||||
delete process.env.NEXT_RUNTIME;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n✅ Edge Runtime compatibility test completed!");
|
|
||||||
console.log("\nKey points:");
|
|
||||||
console.log(
|
|
||||||
"- Edge Runtime: Logs to console, returns empty arrays for queries"
|
|
||||||
);
|
|
||||||
console.log("- Node.js Runtime: Full database functionality");
|
|
||||||
console.log('- API routes are configured with runtime: "nodejs"');
|
|
||||||
console.log("- Middleware avoids database operations");
|
|
||||||
console.log("- Error handling prevents runtime crashes");
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
// Test authenticated flow without external dependencies
|
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:3000';
|
|
||||||
|
|
||||||
// Test data
|
|
||||||
const TEST_CREDENTIALS = {
|
|
||||||
email: 'admin@localhost.com',
|
|
||||||
password: 'admin123456'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to extract cookies from response
|
|
||||||
function extractCookies(response) {
|
|
||||||
const cookies = response.headers.raw()['set-cookie'];
|
|
||||||
if (!cookies) return '';
|
|
||||||
|
|
||||||
return cookies
|
|
||||||
.map(cookie => cookie.split(';')[0])
|
|
||||||
.join('; ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to make authenticated requests
|
|
||||||
async function makeAuthenticatedRequest(url, options = {}, cookies = '') {
|
|
||||||
return fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Cookie': cookies,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testCompleteAuthenticatedFlow() {
|
|
||||||
console.log('🔐 Testing Complete Authenticated Flow\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: Get CSRF token from sign-in page
|
|
||||||
console.log('1️⃣ Getting CSRF token...');
|
|
||||||
const signinResponse = await fetch(`${BASE_URL}/auth/signin`);
|
|
||||||
const signinHtml = await signinResponse.text();
|
|
||||||
|
|
||||||
// Extract CSRF token (NextAuth.js typically includes it in the form)
|
|
||||||
const csrfMatch = signinHtml.match(/name="csrfToken" value="([^"]+)"/);
|
|
||||||
const csrfToken = csrfMatch ? csrfMatch[1] : null;
|
|
||||||
|
|
||||||
if (!csrfToken) {
|
|
||||||
console.log('❌ Could not extract CSRF token');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ CSRF token extracted');
|
|
||||||
const initialCookies = extractCookies(signinResponse);
|
|
||||||
|
|
||||||
// Step 2: Attempt login
|
|
||||||
console.log('\n2️⃣ Attempting login...');
|
|
||||||
const loginResponse = await fetch(`${BASE_URL}/api/auth/callback/credentials`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Cookie': initialCookies
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({
|
|
||||||
csrfToken,
|
|
||||||
email: TEST_CREDENTIALS.email,
|
|
||||||
password: TEST_CREDENTIALS.password,
|
|
||||||
callbackUrl: `${BASE_URL}/projects`,
|
|
||||||
json: 'true'
|
|
||||||
}),
|
|
||||||
redirect: 'manual'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Login response status: ${loginResponse.status}`);
|
|
||||||
|
|
||||||
if (loginResponse.status === 200) {
|
|
||||||
const loginResult = await loginResponse.json();
|
|
||||||
console.log('Login result:', loginResult);
|
|
||||||
|
|
||||||
if (loginResult.url) {
|
|
||||||
console.log('✅ Login successful, redirecting to:', loginResult.url);
|
|
||||||
} else if (loginResult.error) {
|
|
||||||
console.log('❌ Login failed:', loginResult.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (loginResponse.status === 302) {
|
|
||||||
console.log('✅ Login successful (redirect)');
|
|
||||||
} else {
|
|
||||||
console.log('❌ Login failed with status:', loginResponse.status);
|
|
||||||
const errorText = await loginResponse.text();
|
|
||||||
console.log('Error response:', errorText.substring(0, 500));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session cookies
|
|
||||||
const sessionCookies = extractCookies(loginResponse) || initialCookies;
|
|
||||||
console.log('Session cookies:', sessionCookies ? 'Present' : 'Missing');
|
|
||||||
|
|
||||||
// Step 3: Test session endpoint
|
|
||||||
console.log('\n3️⃣ Testing session endpoint...');
|
|
||||||
const sessionResponse = await makeAuthenticatedRequest(
|
|
||||||
`${BASE_URL}/api/auth/session`,
|
|
||||||
{},
|
|
||||||
sessionCookies
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sessionResponse.ok) {
|
|
||||||
const session = await sessionResponse.json();
|
|
||||||
console.log('✅ Session data:', JSON.stringify(session, null, 2));
|
|
||||||
} else {
|
|
||||||
console.log('❌ Session check failed:', sessionResponse.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Test protected pages
|
|
||||||
console.log('\n4️⃣ Testing protected pages...');
|
|
||||||
const protectedPages = ['/projects', '/contracts', '/tasks'];
|
|
||||||
|
|
||||||
for (const page of protectedPages) {
|
|
||||||
const pageResponse = await makeAuthenticatedRequest(
|
|
||||||
`${BASE_URL}${page}`,
|
|
||||||
{},
|
|
||||||
sessionCookies
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pageResponse.ok) {
|
|
||||||
console.log(`✅ ${page} - accessible`);
|
|
||||||
} else if (pageResponse.status === 302) {
|
|
||||||
console.log(`⚠️ ${page} - redirected (status: 302)`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${page} - failed (status: ${pageResponse.status})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Test API endpoints
|
|
||||||
console.log('\n5️⃣ Testing API endpoints...');
|
|
||||||
const apiEndpoints = [
|
|
||||||
{ url: '/api/projects', method: 'GET' },
|
|
||||||
{ url: '/api/contracts', method: 'GET' },
|
|
||||||
{ url: '/api/tasks', method: 'GET' },
|
|
||||||
{ url: '/api/tasks/templates', method: 'GET' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const endpoint of apiEndpoints) {
|
|
||||||
const apiResponse = await makeAuthenticatedRequest(
|
|
||||||
`${BASE_URL}${endpoint.url}`,
|
|
||||||
{ method: endpoint.method },
|
|
||||||
sessionCookies
|
|
||||||
);
|
|
||||||
|
|
||||||
if (apiResponse.ok) {
|
|
||||||
const data = await apiResponse.json();
|
|
||||||
console.log(`✅ ${endpoint.method} ${endpoint.url} - success (${Array.isArray(data) ? data.length : 'object'} items)`);
|
|
||||||
} else if (apiResponse.status === 401) {
|
|
||||||
console.log(`❌ ${endpoint.method} ${endpoint.url} - unauthorized (status: 401)`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${endpoint.method} ${endpoint.url} - failed (status: ${apiResponse.status})`);
|
|
||||||
const errorText = await apiResponse.text();
|
|
||||||
console.log(` Error: ${errorText.substring(0, 200)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 6: Test creating data
|
|
||||||
console.log('\n6️⃣ Testing data creation...');
|
|
||||||
|
|
||||||
// Test creating a project
|
|
||||||
const projectData = {
|
|
||||||
name: 'Test Project Auth',
|
|
||||||
description: 'Testing authentication flow',
|
|
||||||
deadline: '2025-12-31',
|
|
||||||
status: 'active'
|
|
||||||
};
|
|
||||||
|
|
||||||
const createProjectResponse = await makeAuthenticatedRequest(
|
|
||||||
`${BASE_URL}/api/projects`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(projectData)
|
|
||||||
},
|
|
||||||
sessionCookies
|
|
||||||
);
|
|
||||||
|
|
||||||
if (createProjectResponse.ok) {
|
|
||||||
const newProject = await createProjectResponse.json();
|
|
||||||
console.log('✅ Project creation successful:', newProject.name);
|
|
||||||
|
|
||||||
// Clean up - delete the test project
|
|
||||||
const deleteResponse = await makeAuthenticatedRequest(
|
|
||||||
`${BASE_URL}/api/projects/${newProject.id}`,
|
|
||||||
{ method: 'DELETE' },
|
|
||||||
sessionCookies
|
|
||||||
);
|
|
||||||
|
|
||||||
if (deleteResponse.ok) {
|
|
||||||
console.log('✅ Test project cleaned up');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('❌ Project creation failed:', createProjectResponse.status);
|
|
||||||
const errorText = await createProjectResponse.text();
|
|
||||||
console.log(' Error:', errorText.substring(0, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Test failed with error:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testCompleteAuthenticatedFlow();
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import db from "./src/lib/db.js";
|
|
||||||
import {
|
|
||||||
createProjectTask,
|
|
||||||
updateProjectTaskStatus,
|
|
||||||
} from "./src/lib/queries/tasks.js";
|
|
||||||
import { getNotesByTaskId } from "./src/lib/queries/notes.js";
|
|
||||||
|
|
||||||
console.log("Testing automatic logging system...\n");
|
|
||||||
|
|
||||||
// Test 1: Create a new task and check if system note is created
|
|
||||||
console.log("Test 1: Creating a new task...");
|
|
||||||
try {
|
|
||||||
const result = createProjectTask({
|
|
||||||
project_id: 1, // Assuming project ID 1 exists
|
|
||||||
custom_task_name: "Test Task for Logging",
|
|
||||||
custom_description: "Testing automatic note creation",
|
|
||||||
custom_max_wait_days: 7,
|
|
||||||
priority: "high",
|
|
||||||
status: "pending",
|
|
||||||
});
|
|
||||||
|
|
||||||
const taskId = result.lastInsertRowid;
|
|
||||||
console.log(`✓ Task created with ID: ${taskId}`);
|
|
||||||
|
|
||||||
// Check if system note was created
|
|
||||||
const notes = getNotesByTaskId(taskId);
|
|
||||||
console.log(`✓ Notes found: ${notes.length}`);
|
|
||||||
console.log("Notes:", notes);
|
|
||||||
|
|
||||||
// Test 2: Update task status and check if system note is created
|
|
||||||
console.log("\nTest 2: Updating task status...");
|
|
||||||
updateProjectTaskStatus(taskId, "in_progress");
|
|
||||||
console.log("✓ Task status updated to in_progress");
|
|
||||||
|
|
||||||
// Check if new system note was created
|
|
||||||
const updatedNotes = getNotesByTaskId(taskId);
|
|
||||||
console.log(`✓ Notes after status update: ${updatedNotes.length}`);
|
|
||||||
console.log("Updated notes:", updatedNotes);
|
|
||||||
|
|
||||||
// Clean up - delete test task
|
|
||||||
console.log("\nCleaning up test data...");
|
|
||||||
db.prepare("DELETE FROM notes WHERE task_id = ?").run(taskId);
|
|
||||||
db.prepare("DELETE FROM project_tasks WHERE id = ?").run(taskId);
|
|
||||||
console.log("✓ Test data cleaned up");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error during testing:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\nTest completed.");
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Map Test - Mobile View</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 600px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
padding: 10px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="info">
|
|
||||||
<strong>Mobile View Test (400px width)</strong><br />
|
|
||||||
Testing responsive behavior of the projects map
|
|
||||||
</div>
|
|
||||||
<iframe src="http://localhost:3000/projects/map"></iframe>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// Simple test for NextAuth endpoints
|
|
||||||
const BASE_URL = 'http://localhost:3000';
|
|
||||||
|
|
||||||
async function testNextAuthEndpoints() {
|
|
||||||
console.log('🔐 Testing NextAuth Endpoints\n');
|
|
||||||
|
|
||||||
// Test session endpoint
|
|
||||||
try {
|
|
||||||
const sessionResponse = await fetch(`${BASE_URL}/api/auth/session`);
|
|
||||||
console.log(`Session endpoint: ${sessionResponse.status} ${sessionResponse.statusText}`);
|
|
||||||
|
|
||||||
if (sessionResponse.ok) {
|
|
||||||
const sessionData = await sessionResponse.json();
|
|
||||||
console.log(`Session data: ${JSON.stringify(sessionData)}\n`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Session endpoint error: ${error.message}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test providers endpoint
|
|
||||||
try {
|
|
||||||
const providersResponse = await fetch(`${BASE_URL}/api/auth/providers`);
|
|
||||||
console.log(`Providers endpoint: ${providersResponse.status} ${providersResponse.statusText}`);
|
|
||||||
|
|
||||||
if (providersResponse.ok) {
|
|
||||||
const providersData = await providersResponse.json();
|
|
||||||
console.log(`Providers: ${JSON.stringify(providersData, null, 2)}\n`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Providers endpoint error: ${error.message}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test CSRF endpoint
|
|
||||||
try {
|
|
||||||
const csrfResponse = await fetch(`${BASE_URL}/api/auth/csrf`);
|
|
||||||
console.log(`CSRF endpoint: ${csrfResponse.status} ${csrfResponse.statusText}`);
|
|
||||||
|
|
||||||
if (csrfResponse.ok) {
|
|
||||||
const csrfData = await csrfResponse.json();
|
|
||||||
console.log(`CSRF token present: ${csrfData.csrfToken ? 'Yes' : 'No'}\n`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`CSRF endpoint error: ${error.message}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testNextAuthEndpoints().catch(console.error);
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test script to verify notifications API
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function testNotificationsAPI() {
|
|
||||||
try {
|
|
||||||
console.log("Testing notifications API...");
|
|
||||||
|
|
||||||
// Test unread count endpoint
|
|
||||||
const unreadResponse = await fetch('http://localhost:3001/api/notifications/unread-count');
|
|
||||||
if (unreadResponse.ok) {
|
|
||||||
const unreadData = await unreadResponse.json();
|
|
||||||
console.log("✅ Unread count:", unreadData.unreadCount);
|
|
||||||
} else {
|
|
||||||
console.log("❌ Unread count endpoint failed:", unreadResponse.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test notifications list endpoint
|
|
||||||
const notificationsResponse = await fetch('http://localhost:3001/api/notifications');
|
|
||||||
if (notificationsResponse.ok) {
|
|
||||||
const notificationsData = await notificationsResponse.json();
|
|
||||||
console.log("✅ Notifications fetched:", notificationsData.notifications.length);
|
|
||||||
console.log("Sample notification:", notificationsData.notifications[0]);
|
|
||||||
} else {
|
|
||||||
console.log("❌ Notifications endpoint failed:", notificationsResponse.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error testing API:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testNotificationsAPI();
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test script to verify notifications are working
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function testNotifications() {
|
|
||||||
try {
|
|
||||||
console.log("Testing notifications system...");
|
|
||||||
|
|
||||||
// Test unread count endpoint (this should work without auth for now)
|
|
||||||
console.log("1. Testing unread count endpoint...");
|
|
||||||
const unreadResponse = await fetch('http://localhost:3001/api/notifications/unread-count');
|
|
||||||
|
|
||||||
if (unreadResponse.status === 401) {
|
|
||||||
console.log("✅ Unread count endpoint requires auth (expected)");
|
|
||||||
} else if (unreadResponse.ok) {
|
|
||||||
const data = await unreadResponse.json();
|
|
||||||
console.log("✅ Unread count:", data.unreadCount);
|
|
||||||
} else {
|
|
||||||
console.log("❌ Unread count endpoint failed:", unreadResponse.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test notifications endpoint
|
|
||||||
console.log("2. Testing notifications endpoint...");
|
|
||||||
const notificationsResponse = await fetch('http://localhost:3001/api/notifications');
|
|
||||||
|
|
||||||
if (notificationsResponse.status === 401) {
|
|
||||||
console.log("✅ Notifications endpoint requires auth (expected)");
|
|
||||||
} else if (notificationsResponse.ok) {
|
|
||||||
const data = await notificationsResponse.json();
|
|
||||||
console.log("✅ Notifications fetched:", data.notifications?.length || 0);
|
|
||||||
} else {
|
|
||||||
console.log("❌ Notifications endpoint failed:", notificationsResponse.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n🎉 Notification system test completed!");
|
|
||||||
console.log("Note: API endpoints require authentication, so 401 responses are expected.");
|
|
||||||
console.log("Test the UI by logging into the application and checking the notification dropdown.");
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error testing notifications:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testNotifications();
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
async function testProjectAPI() {
|
|
||||||
const baseURL = "http://localhost:3000";
|
|
||||||
|
|
||||||
console.log("Testing project API endpoints...\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test fetching project 1
|
|
||||||
console.log("1. Fetching project 1:");
|
|
||||||
const response = await fetch(`${baseURL}/api/projects/1`);
|
|
||||||
console.log("Status:", response.status);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const project = await response.json();
|
|
||||||
console.log("Project data received:");
|
|
||||||
console.log(JSON.stringify(project, null, 2));
|
|
||||||
} else {
|
|
||||||
const error = await response.text();
|
|
||||||
console.log("Error:", error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error testing API:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testProjectAPI();
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// Test project creation
|
|
||||||
const BASE_URL = "http://localhost:3001";
|
|
||||||
|
|
||||||
async function testProjectCreation() {
|
|
||||||
console.log("🧪 Testing project creation...\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First, login to get session
|
|
||||||
console.log("1. Logging in...");
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${BASE_URL}/api/auth/signin/credentials`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: "admin@localhost.com",
|
|
||||||
password: "admin123456",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Login response status:", loginResponse.status);
|
|
||||||
const loginResult = await loginResponse.text();
|
|
||||||
console.log("Login result:", loginResult.substring(0, 200));
|
|
||||||
|
|
||||||
// Try a simple API call to see the auth system
|
|
||||||
console.log("\n2. Testing projects API...");
|
|
||||||
const projectsResponse = await fetch(`${BASE_URL}/api/projects`);
|
|
||||||
console.log("Projects API status:", projectsResponse.status);
|
|
||||||
|
|
||||||
if (projectsResponse.status === 401) {
|
|
||||||
console.log("❌ Authentication required (expected for this test)");
|
|
||||||
} else {
|
|
||||||
const projectsData = await projectsResponse.json();
|
|
||||||
console.log("✅ Projects API accessible");
|
|
||||||
console.log("Number of projects:", projectsData.length);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Test failed:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testProjectCreation();
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Radicale sync configuration
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getRadicaleConfig, isRadicaleEnabled, generateVCard } from './src/lib/radicale-sync.js';
|
|
||||||
|
|
||||||
console.log('🧪 Testing Radicale Sync Configuration\n');
|
|
||||||
|
|
||||||
// Check if enabled
|
|
||||||
if (isRadicaleEnabled()) {
|
|
||||||
const config = getRadicaleConfig();
|
|
||||||
console.log('✅ Radicale sync is ENABLED');
|
|
||||||
console.log(` URL: ${config.url}`);
|
|
||||||
console.log(` Username: ${config.username}`);
|
|
||||||
console.log(` Password: ${config.password ? '***' + config.password.slice(-3) : 'not set'}`);
|
|
||||||
} else {
|
|
||||||
console.log('❌ Radicale sync is DISABLED');
|
|
||||||
console.log(' Set RADICALE_URL, RADICALE_USERNAME, and RADICALE_PASSWORD in .env.local to enable');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n📝 Testing VCARD Generation\n');
|
|
||||||
|
|
||||||
// Test VCARD generation
|
|
||||||
const testContact = {
|
|
||||||
contact_id: 999,
|
|
||||||
name: 'Jan Kowalski',
|
|
||||||
phone: '["123-456-789", "987-654-321"]',
|
|
||||||
email: 'jan.kowalski@example.com',
|
|
||||||
company: 'Test Company',
|
|
||||||
position: 'Manager',
|
|
||||||
contact_type: 'project',
|
|
||||||
notes: 'Test contact for VCARD generation',
|
|
||||||
is_active: 1,
|
|
||||||
created_at: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
const vcard = generateVCard(testContact);
|
|
||||||
console.log('Generated VCARD:');
|
|
||||||
console.log('─'.repeat(60));
|
|
||||||
console.log(vcard);
|
|
||||||
console.log('─'.repeat(60));
|
|
||||||
|
|
||||||
console.log('\n✅ Test complete!');
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test the safe audit logging in different runtime environments
|
|
||||||
*/
|
|
||||||
|
|
||||||
console.log("Testing Safe Audit Logging...\n");
|
|
||||||
|
|
||||||
// Test 1: Import the safe module (should work in any runtime)
|
|
||||||
console.log("1. Testing safe module import...");
|
|
||||||
try {
|
|
||||||
const { AUDIT_ACTIONS, RESOURCE_TYPES, logAuditEventSafe } = await import(
|
|
||||||
"./src/lib/auditLogSafe.js"
|
|
||||||
);
|
|
||||||
console.log("✅ Safe module imported successfully");
|
|
||||||
console.log(` Available actions: ${Object.keys(AUDIT_ACTIONS).length}`);
|
|
||||||
console.log(
|
|
||||||
` Available resource types: ${Object.keys(RESOURCE_TYPES).length}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("❌ Failed to import safe module:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: Test in simulated Edge Runtime
|
|
||||||
console.log("\n2. Testing in simulated Edge Runtime...");
|
|
||||||
global.EdgeRuntime = "edge";
|
|
||||||
try {
|
|
||||||
const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = await import(
|
|
||||||
"./src/lib/auditLogSafe.js"
|
|
||||||
);
|
|
||||||
await logAuditEventSafe({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_VIEW,
|
|
||||||
userId: null, // Use null to avoid foreign key constraint
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "test-123",
|
|
||||||
details: { test: "edge runtime" },
|
|
||||||
});
|
|
||||||
console.log("✅ Edge Runtime logging successful (console only)");
|
|
||||||
} catch (error) {
|
|
||||||
console.log("❌ Edge Runtime logging failed:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 3: Test in simulated Node.js Runtime
|
|
||||||
console.log("\n3. Testing in simulated Node.js Runtime...");
|
|
||||||
delete global.EdgeRuntime;
|
|
||||||
try {
|
|
||||||
const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = await import(
|
|
||||||
"./src/lib/auditLogSafe.js"
|
|
||||||
);
|
|
||||||
await logAuditEventSafe({
|
|
||||||
action: AUDIT_ACTIONS.PROJECT_CREATE,
|
|
||||||
userId: null, // Use null to avoid foreign key constraint
|
|
||||||
resourceType: RESOURCE_TYPES.PROJECT,
|
|
||||||
resourceId: "test-456",
|
|
||||||
details: { test: "nodejs runtime" },
|
|
||||||
});
|
|
||||||
console.log("✅ Node.js Runtime logging successful (database + console)");
|
|
||||||
} catch (error) {
|
|
||||||
console.log("❌ Node.js Runtime logging failed:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 4: Test constants accessibility
|
|
||||||
console.log("\n4. Testing constants accessibility...");
|
|
||||||
try {
|
|
||||||
const { AUDIT_ACTIONS, RESOURCE_TYPES } = await import(
|
|
||||||
"./src/lib/auditLogSafe.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Constants accessible:");
|
|
||||||
console.log(` LOGIN action: ${AUDIT_ACTIONS.LOGIN}`);
|
|
||||||
console.log(` PROJECT resource: ${RESOURCE_TYPES.PROJECT}`);
|
|
||||||
console.log(` NOTE_CREATE action: ${AUDIT_ACTIONS.NOTE_CREATE}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("❌ Constants not accessible:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n✅ Safe Audit Logging test completed!");
|
|
||||||
console.log("\nKey features verified:");
|
|
||||||
console.log("- ✅ No static database imports");
|
|
||||||
console.log("- ✅ Edge Runtime compatibility");
|
|
||||||
console.log("- ✅ Graceful fallbacks");
|
|
||||||
console.log("- ✅ Constants always available");
|
|
||||||
console.log("- ✅ Async/await support");
|
|
||||||
console.log("\nThe middleware should now work without Edge Runtime errors!");
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// Test the project-tasks API endpoints
|
|
||||||
|
|
||||||
async function testAPI() {
|
|
||||||
const baseURL = "http://localhost:3000";
|
|
||||||
|
|
||||||
console.log("Testing project-tasks API endpoints...\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Check if users endpoint exists
|
|
||||||
console.log("1. Testing /api/project-tasks/users:");
|
|
||||||
const usersResponse = await fetch(`${baseURL}/api/project-tasks/users`);
|
|
||||||
console.log("Status:", usersResponse.status);
|
|
||||||
if (usersResponse.ok) {
|
|
||||||
const users = await usersResponse.json();
|
|
||||||
console.log("Users found:", users.length);
|
|
||||||
console.log("First user:", users[0]);
|
|
||||||
} else {
|
|
||||||
const error = await usersResponse.text();
|
|
||||||
console.log("Error:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: Try to create a task (this will fail without auth, but let's see the response)
|
|
||||||
console.log("\n2. Testing POST /api/project-tasks:");
|
|
||||||
const taskData = {
|
|
||||||
project_id: 1,
|
|
||||||
task_template_id: 1,
|
|
||||||
priority: "normal",
|
|
||||||
};
|
|
||||||
|
|
||||||
const createResponse = await fetch(`${baseURL}/api/project-tasks`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(taskData),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Status:", createResponse.status);
|
|
||||||
const responseText = await createResponse.text();
|
|
||||||
console.log("Response:", responseText);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error testing API:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testAPI();
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Test script to verify task sets functionality
|
|
||||||
import { getAllTaskSets, createTaskSet } from './src/lib/queries/tasks.js';
|
|
||||||
import initializeDatabase from './src/lib/init-db.js';
|
|
||||||
|
|
||||||
async function testTaskSets() {
|
|
||||||
console.log('Testing Task Sets Database Functions...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initialize database
|
|
||||||
initializeDatabase();
|
|
||||||
|
|
||||||
// Test 1: Get all task sets
|
|
||||||
console.log('1. Getting all task sets...');
|
|
||||||
const taskSets = getAllTaskSets();
|
|
||||||
console.log(`Found ${taskSets.length} task sets:`);
|
|
||||||
taskSets.forEach(set => {
|
|
||||||
console.log(` - ${set.name} (${set.task_category}):`, JSON.stringify(set));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: Create a new task set (design)
|
|
||||||
console.log('\n2. Creating design task set...');
|
|
||||||
const designSetId = createTaskSet({
|
|
||||||
name: 'Test Design Set',
|
|
||||||
description: 'Test task set for design tasks',
|
|
||||||
task_category: 'design',
|
|
||||||
templates: []
|
|
||||||
});
|
|
||||||
console.log(`Created task set with ID: ${designSetId}`);
|
|
||||||
|
|
||||||
// Test 3: Create a construction task set
|
|
||||||
console.log('\n3. Creating construction task set...');
|
|
||||||
const constructionSetId = createTaskSet({
|
|
||||||
name: 'Test Construction Set',
|
|
||||||
description: 'Test task set for construction tasks',
|
|
||||||
task_category: 'construction',
|
|
||||||
templates: []
|
|
||||||
});
|
|
||||||
console.log(`Created task set with ID: ${constructionSetId}`);
|
|
||||||
|
|
||||||
// Test 4: Try to create invalid task set (should fail)
|
|
||||||
console.log('\n4. Testing invalid task category (should fail)...');
|
|
||||||
try {
|
|
||||||
const invalidSetId = createTaskSet({
|
|
||||||
name: 'Invalid Set',
|
|
||||||
description: 'This should fail',
|
|
||||||
task_category: 'design+construction',
|
|
||||||
templates: []
|
|
||||||
});
|
|
||||||
console.log('✗ Should have failed to create invalid task set');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✓ Correctly rejected invalid task category:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 5: Get all task sets again
|
|
||||||
console.log('\n5. Getting all task sets after creation...');
|
|
||||||
const updatedTaskSets = getAllTaskSets();
|
|
||||||
console.log(`Found ${updatedTaskSets.length} task sets:`);
|
|
||||||
updatedTaskSets.forEach(set => {
|
|
||||||
console.log(` - ${set.name} (${set.task_category})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n✅ All tests passed! Task sets functionality is working correctly.');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test failed:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testTaskSets();
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {
|
|
||||||
getAllProjects,
|
|
||||||
getAllUsersForAssignment,
|
|
||||||
} from "./src/lib/queries/projects.js";
|
|
||||||
import initializeDatabase from "./src/lib/init-db.js";
|
|
||||||
|
|
||||||
// Initialize database
|
|
||||||
initializeDatabase();
|
|
||||||
|
|
||||||
console.log("Testing user tracking in projects...\n");
|
|
||||||
|
|
||||||
console.log("1. Available users for assignment:");
|
|
||||||
const users = getAllUsersForAssignment();
|
|
||||||
console.log(JSON.stringify(users, null, 2));
|
|
||||||
|
|
||||||
console.log("\n2. Current projects with user information:");
|
|
||||||
const projects = getAllProjects();
|
|
||||||
console.log("Total projects:", projects.length);
|
|
||||||
|
|
||||||
if (projects.length > 0) {
|
|
||||||
console.log("\nFirst project details:");
|
|
||||||
console.log(JSON.stringify(projects[0], null, 2));
|
|
||||||
} else {
|
|
||||||
console.log("No projects found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n✅ User tracking implementation test completed!");
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
|
|
||||||
const db = new Database("./data/database.sqlite");
|
|
||||||
|
|
||||||
console.log("🔄 Updating admin username...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Update admin username from email to simple "admin"
|
|
||||||
const result = db.prepare('UPDATE users SET username = ? WHERE username = ?').run('admin', 'admin@localhost.com');
|
|
||||||
|
|
||||||
if (result.changes > 0) {
|
|
||||||
console.log('✅ Admin username updated to "admin"');
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ No admin user found with email "admin@localhost.com"');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show current users
|
|
||||||
const users = db.prepare("SELECT name, username, role FROM users").all();
|
|
||||||
console.log("\nCurrent users:");
|
|
||||||
users.forEach(user => {
|
|
||||||
console.log(` - ${user.name} (${user.role}): username="${user.username}"`);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error:", error.message);
|
|
||||||
} finally {
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
$files = @(
|
|
||||||
"d:\panel\src\lib\queries\tasks.js",
|
|
||||||
"d:\panel\src\lib\userManagement.js"
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach ($file in $files) {
|
|
||||||
if (Test-Path $file) {
|
|
||||||
Write-Host "Updating $file..."
|
|
||||||
$content = Get-Content $file -Raw
|
|
||||||
$content = $content -replace "creator\.email as created_by_email", "creator.username as created_by_username"
|
|
||||||
$content = $content -replace "assignee\.email as assigned_to_email", "assignee.username as assigned_to_username"
|
|
||||||
$content = $content -replace "u\.email as created_by_email", "u.username as created_by_username"
|
|
||||||
$content = $content -replace "SELECT id, name, email, role", "SELECT id, name, username, role"
|
|
||||||
$content = $content -replace "name, email, role", "name, username, role"
|
|
||||||
Set-Content $file $content -NoNewline
|
|
||||||
Write-Host "Updated $file"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "All files updated!"
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import {
|
|
||||||
logAuditEvent,
|
|
||||||
getAuditLogs,
|
|
||||||
AUDIT_ACTIONS,
|
|
||||||
RESOURCE_TYPES,
|
|
||||||
} from "./src/lib/auditLog.js";
|
|
||||||
|
|
||||||
console.log("=== FINAL AUDIT LOGGING VERIFICATION ===\n");
|
|
||||||
|
|
||||||
async function verifyAuditLogging() {
|
|
||||||
try {
|
|
||||||
// 1. Check recent audit logs
|
|
||||||
console.log("1. Checking recent audit logs for user ID issues...");
|
|
||||||
const recentLogs = await getAuditLogs({ limit: 10 });
|
|
||||||
|
|
||||||
console.log(`Found ${recentLogs.length} recent audit events:`);
|
|
||||||
recentLogs.forEach((log, index) => {
|
|
||||||
const userDisplay = log.user_id ? `user ${log.user_id}` : "NULL USER ID";
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${log.timestamp} - ${log.action} by ${userDisplay} on ${
|
|
||||||
log.resource_type
|
|
||||||
}:${log.resource_id || "N/A"}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Count null user IDs
|
|
||||||
const allLogs = await getAuditLogs();
|
|
||||||
const nullUserCount = allLogs.filter((log) => log.user_id === null).length;
|
|
||||||
const totalCount = allLogs.length;
|
|
||||||
const nullPercentage = ((nullUserCount / totalCount) * 100).toFixed(2);
|
|
||||||
|
|
||||||
console.log(`\n2. Audit Log Statistics:`);
|
|
||||||
console.log(` Total audit logs: ${totalCount}`);
|
|
||||||
console.log(` Logs with NULL user_id: ${nullUserCount}`);
|
|
||||||
console.log(` Percentage with NULL user_id: ${nullPercentage}%`);
|
|
||||||
|
|
||||||
// 3. Check distribution by action type
|
|
||||||
console.log(`\n3. Action distribution for NULL user_id logs:`);
|
|
||||||
const nullUserLogs = allLogs.filter((log) => log.user_id === null);
|
|
||||||
const actionCounts = {};
|
|
||||||
nullUserLogs.forEach((log) => {
|
|
||||||
actionCounts[log.action] = (actionCounts[log.action] || 0) + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.entries(actionCounts).forEach(([action, count]) => {
|
|
||||||
console.log(` ${action}: ${count} events`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Test new audit event with valid user ID
|
|
||||||
console.log(`\n4. Testing new audit event with valid user ID...`);
|
|
||||||
await logAuditEvent({
|
|
||||||
action: AUDIT_ACTIONS.LOGIN,
|
|
||||||
userId: "test-user-123",
|
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
|
||||||
ipAddress: "127.0.0.1",
|
|
||||||
userAgent: "Test Agent",
|
|
||||||
details: {
|
|
||||||
test: "verification",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify the new event was logged correctly
|
|
||||||
const verificationLogs = await getAuditLogs({ limit: 1 });
|
|
||||||
const latestLog = verificationLogs[0];
|
|
||||||
|
|
||||||
if (latestLog && latestLog.user_id === "test-user-123") {
|
|
||||||
console.log("✅ SUCCESS: New audit event logged with correct user ID");
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"❌ FAILED: New audit event has incorrect user ID:",
|
|
||||||
latestLog?.user_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Summary
|
|
||||||
console.log(`\n5. SUMMARY:`);
|
|
||||||
if (nullPercentage < 10) {
|
|
||||||
console.log("✅ EXCELLENT: Very few NULL user IDs detected");
|
|
||||||
} else if (nullPercentage < 30) {
|
|
||||||
console.log("⚠️ GOOD: Some NULL user IDs but manageable");
|
|
||||||
} else {
|
|
||||||
console.log("❌ NEEDS ATTENTION: High percentage of NULL user IDs");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n6. RECOMMENDATIONS:`);
|
|
||||||
if (nullUserCount > 0) {
|
|
||||||
console.log(
|
|
||||||
" - The NULL user IDs are likely from before the fix was applied"
|
|
||||||
);
|
|
||||||
console.log(" - New audit events should now log user IDs correctly");
|
|
||||||
console.log(" - Monitor future logs to ensure the fix is working");
|
|
||||||
} else {
|
|
||||||
console.log(" - All audit events have valid user IDs!");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Verification failed:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyAuditLogging();
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { getProjectById } from "./src/lib/queries/projects.js";
|
|
||||||
|
|
||||||
console.log("Checking the created project with user tracking...\n");
|
|
||||||
|
|
||||||
const project = getProjectById(17);
|
|
||||||
console.log("Project details:");
|
|
||||||
console.log(JSON.stringify(project, null, 2));
|
|
||||||
Reference in New Issue
Block a user