diff --git a/AUDIT_LOGGING_IMPLEMENTATION.md b/AUDIT_LOGGING_IMPLEMENTATION.md new file mode 100644 index 0000000..e86f219 --- /dev/null +++ b/AUDIT_LOGGING_IMPLEMENTATION.md @@ -0,0 +1,379 @@ +# 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 diff --git a/AUTHORIZATION_IMPLEMENTATION.md b/AUTHORIZATION_IMPLEMENTATION.md index 4424c2e..7607ecf 100644 --- a/AUTHORIZATION_IMPLEMENTATION.md +++ b/AUTHORIZATION_IMPLEMENTATION.md @@ -4,28 +4,52 @@ 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 +## Current State Analysis (Updated: June 25, 2025) + +### โœ… What We Have Implemented -### โœ… What We Have - **Framework**: Next.js 15 with App Router - **Database**: SQLite with better-sqlite3 -- **API Routes**: Multiple unprotected endpoints (projects, contracts, tasks, notes) -- **UI**: Basic navigation and CRUD interfaces -- **Security**: โš ๏ธ **NONE** - All endpoints are publicly accessible +- **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 Missing -- User authentication system -- Session management -- Role-based access control -- API route protection -- Input validation & sanitization -- Security middleware +### โœ… 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) @@ -36,181 +60,195 @@ This document outlines the implementation strategy for adding authentication and **Proposed User Roles:** -| Role | Permissions | Use Case | -|------|-------------|----------| -| **Admin** | Full system access, user management | System administrators | +| 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 | +| **User** | View/edit assigned projects/tasks | Regular employees | +| **Read-only** | View-only access to data | Clients, stakeholders | -## Implementation Plan +## Implementation Status -### Phase 1: Foundation Setup +### โœ… Phase 1: Foundation Setup - COMPLETED -#### 1.1 Install Dependencies +#### 1.1 Dependencies - โœ… INSTALLED -```bash -npm install next-auth@beta @auth/better-sqlite3-adapter -npm install bcryptjs zod -npm install @types/bcryptjs # if using TypeScript -``` +- NextAuth.js v5 (beta) +- bcryptjs for password hashing +- Zod for validation +- Better-sqlite3 adapter compatibility -#### 1.2 Environment Configuration +#### 1.2 Environment Configuration - โœ… COMPLETED -Create `.env.local`: -```env -# NextAuth.js Configuration -NEXTAUTH_SECRET=your-super-secret-key-here-minimum-32-characters -NEXTAUTH_URL=http://localhost:3000 +- `.env.local` configured with NEXTAUTH_SECRET and NEXTAUTH_URL +- Database URL configuration +- Development environment setup -# Database -DATABASE_URL=./data/database.sqlite +#### 1.3 Database Schema - โœ… IMPLEMENTED -# Optional: Email configuration for password reset -EMAIL_SERVER_HOST=smtp.gmail.com -EMAIL_SERVER_PORT=587 -EMAIL_SERVER_USER=your-email@gmail.com -EMAIL_SERVER_PASSWORD=your-app-password -EMAIL_FROM=noreply@yourapp.com -``` +- Users table with roles and security features +- Sessions table for NextAuth.js +- Audit logs table for security tracking +- Proper indexes for performance -#### 1.3 Database Schema Extension +#### 1.4 Initial Admin User - โœ… COMPLETED -Add to `src/lib/init-db.js`: +- `scripts/create-admin.js` script available +- Default admin user: admin@localhost.com / admin123456 -```sql --- Users table -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), - name TEXT NOT NULL, - email TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - role TEXT CHECK(role IN ('admin', '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 -); +### โœ… Phase 2: Authentication Core - COMPLETED --- NextAuth.js required tables -CREATE TABLE IF NOT EXISTS accounts ( - id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), - userId TEXT NOT NULL, - type TEXT NOT NULL, - provider TEXT NOT NULL, - providerAccountId TEXT NOT NULL, - refresh_token TEXT, - access_token TEXT, - expires_at INTEGER, - token_type TEXT, - scope TEXT, - id_token TEXT, - session_state TEXT, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE -); +#### 2.1 NextAuth.js Configuration - โœ… IMPLEMENTED -CREATE TABLE IF NOT EXISTS sessions ( - id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), - sessionToken TEXT UNIQUE NOT NULL, - userId TEXT NOT NULL, - expires TEXT NOT NULL, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE -); +- **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 -CREATE TABLE IF NOT EXISTS verification_tokens ( - identifier TEXT NOT NULL, - token TEXT UNIQUE NOT NULL, - expires TEXT NOT NULL, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (identifier, token) -); +#### 2.2 API Route Handlers - โœ… IMPLEMENTED --- Audit log table for security tracking -CREATE TABLE IF NOT EXISTS audit_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT, - action TEXT NOT NULL, - resource_type TEXT, - resource_id TEXT, - ip_address TEXT, - user_agent TEXT, - timestamp TEXT DEFAULT CURRENT_TIMESTAMP, - details TEXT, - FOREIGN KEY (user_id) REFERENCES users(id) -); +- **File**: `src/app/api/auth/[...nextauth]/route.js` +- NextAuth.js handlers properly configured --- Create indexes for performance -CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); -CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(sessionToken); -CREATE INDEX IF NOT EXISTS idx_accounts_user ON accounts(userId); -CREATE INDEX IF NOT EXISTS idx_audit_user_timestamp ON audit_logs(user_id, timestamp); -``` +#### 2.3 User Management System - โœ… IMPLEMENTED -### Phase 2: Authentication Core +- **File**: `src/lib/userManagement.js` +- Complete CRUD operations for users +- Password hashing and validation +- Role management functions +- User lookup by ID and email -#### 2.1 NextAuth.js Configuration +### โœ… Phase 3: Authorization Middleware - COMPLETED -Create `src/lib/auth.js`: +#### 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" +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") -}) + 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(` + 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(` + ` + ) + .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 @@ -219,87 +257,92 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ ELSE locked_until END WHERE id = ? - `).run(user.id) - - throw new Error("Invalid credentials") - } - - // Reset failed attempts and update last login - db.prepare(` + ` + ).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) - } - } - } -}) + ` + ).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(` + 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) - } + ` + ).run( + userId, + action, + resourceType, + resourceId, + req?.ip || "unknown", + req?.headers?.["user-agent"] || "unknown" + ); + } catch (error) { + console.error("Audit log error:", error); + } } ``` @@ -308,9 +351,9 @@ function logAuditEvent(userId, action, resourceType, resourceId, req = null) { Create `src/app/api/auth/[...nextauth]/route.js`: ```javascript -import { handlers } from "@/lib/auth" +import { handlers } from "@/lib/auth"; -export const { GET, POST } = handlers +export const { GET, POST } = handlers; ``` ### Phase 3: Authorization Middleware @@ -320,111 +363,129 @@ export const { GET, POST } = handlers Create `src/lib/middleware/auth.js`: ```javascript -import { auth } from "@/lib/auth" -import { NextResponse } from "next/server" -import { z } from "zod" +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 -} + 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 } - ) - } - } + 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] + return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole]; } // Helper for read-only operations export function withReadAuth(handler) { - return withAuth(handler, { requiredRole: 'read_only' }) + return withAuth(handler, { requiredRole: "read_only" }); } // Helper for user-level operations export function withUserAuth(handler) { - return withAuth(handler, { requiredRole: 'user' }) + return withAuth(handler, { requiredRole: "user" }); } // Helper for project manager operations export function withManagerAuth(handler) { - return withAuth(handler, { requiredRole: 'project_manager' }) + return withAuth(handler, { requiredRole: "project_manager" }); } // Helper for admin operations export function withAdminAuth(handler) { - return withAuth(handler, { requiredRole: 'admin' }) + return withAuth(handler, { requiredRole: "admin" }); } ``` @@ -433,54 +494,60 @@ export function withAdminAuth(handler) { Create `src/components/auth/ProtectedRoute.js`: ```javascript -"use client" +"use client"; -import { useSession } from "next-auth/react" -import { useRouter } from "next/navigation" -import { useEffect } from "react" +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
Loading...
- } - - if (!session) { - return fallback ||
Redirecting to login...
- } - - if (requiredRole && !hasPermission(session.user.role, requiredRole)) { - return fallback ||
Access denied
- } - - return children +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 ( +
Loading...
+ ); + } + + if (!session) { + return fallback ||
Redirecting to login...
; + } + + if (requiredRole && !hasPermission(session.user.role, requiredRole)) { + return fallback ||
Access denied
; + } + + return children; } function hasPermission(userRole, requiredRole) { - const roleHierarchy = { - 'admin': 4, - 'project_manager': 3, - 'user': 2, - 'read_only': 1 - } - - return roleHierarchy[userRole] >= roleHierarchy[requiredRole] + const roleHierarchy = { + admin: 4, + project_manager: 3, + user: 2, + read_only: 1, + }; + + return roleHierarchy[userRole] >= roleHierarchy[requiredRole]; } ``` @@ -489,6 +556,7 @@ function hasPermission(userRole, requiredRole) { #### 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 @@ -497,6 +565,7 @@ Pages to create: #### 4.2 Navigation Updates Update `src/components/ui/Navigation.js` to include: + - Login/logout buttons - User info display - Role-based menu items @@ -504,6 +573,7 @@ Update `src/components/ui/Navigation.js` to include: #### 4.3 User Management Interface For admin users: + - User listing and management - Role assignment - Account activation/deactivation @@ -516,17 +586,17 @@ Create `src/lib/schemas/` with Zod schemas for all API endpoints: ```javascript // src/lib/schemas/project.js -import { z } from "zod" +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 -}) + 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() +export const updateProjectSchema = createProjectSchema.partial(); ``` #### 5.2 Rate Limiting @@ -535,162 +605,367 @@ Implement rate limiting for sensitive endpoints: ```javascript // src/lib/middleware/rateLimit.js -const attempts = new Map() +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) - } +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 +## Implementation Checklist (Updated Status) -### Phase 1: Foundation -- [ ] Install dependencies -- [ ] Create environment configuration -- [ ] Extend database schema -- [ ] Create initial admin user script +### โœ… Phase 1: Foundation - COMPLETED -### Phase 2: Authentication -- [ ] Configure NextAuth.js -- [ ] Create API route handlers -- [ ] Test login/logout functionality +- [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 3: Authorization -- [ ] Implement API middleware -- [ ] Protect existing API routes -- [ ] Create client-side route protection +### โœ… Phase 2: Authentication - COMPLETED -### Phase 4: User Interface -- [ ] Create authentication pages -- [ ] Update navigation component -- [ ] Build user management interface +- [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 5: Security -- [ ] Add input validation to all endpoints -- [ ] Implement rate limiting -- [ ] Add audit logging +### โœ… 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 -## Security Best Practices +## Current Working Features -### 1. Password Security -- Minimum 8 characters -- Require special characters, numbers -- Hash with bcrypt (cost factor 12+) -- Implement password history +### ๐Ÿ” Authentication System -### 2. Session Security -- Secure cookies -- Session rotation -- Timeout handling -- Device tracking +- **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) -### 3. API Security -- Input validation on all endpoints -- SQL injection prevention (prepared statements) -- XSS protection -- CSRF tokens +### ๐Ÿ›ก๏ธ Authorization System -### 4. Audit & Monitoring -- Log all authentication events -- Monitor failed login attempts -- Track permission changes -- Alert on suspicious activity +- **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 -## Testing Strategy +### ๐Ÿ“ฑ User Interface -### 1. Authentication Tests -- Valid/invalid login attempts -- Password reset functionality -- Session expiration -- Account lockout +- **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 -### 2. Authorization Tests -- Role-based access control -- API endpoint protection -- Resource-level permissions -- Privilege escalation attempts +### ๐Ÿ—„๏ธ Database Security -### 3. Security Tests -- SQL injection attempts -- XSS attacks -- CSRF attacks -- Rate limiting +- **User Management**: Complete CRUD with proper validation +- **Audit Schema**: Ready for comprehensive logging +- **Indexes**: Optimized for performance +- **Constraints**: Role validation and data integrity -## Deployment Considerations +## Next Priority Tasks -### 1. Environment Variables -- Use strong, random secrets -- Different keys per environment -- Secure secret management +1. **Complete Auth UI** (High Priority) -### 2. Database Security -- Regular backups -- Encryption at rest -- Network security -- Access logging + - Sign-out confirmation page + - Unauthorized access page + - Enhanced error handling -### 3. Application Security -- HTTPS enforcement -- Security headers -- Content Security Policy -- Regular security updates +2. **Admin User Management** (High Priority) -## Migration Strategy + - User listing interface + - Create/edit user forms + - Role assignment controls -### 1. Development Phase -- Implement on development branch -- Test thoroughly with sample data -- Document all changes +3. **Security Enhancements** (Medium Priority) -### 2. Staging Deployment -- Deploy to staging environment -- Performance testing -- Security testing -- User acceptance testing + - Input validation schemas + - Rate limiting middleware + - Comprehensive audit logging -### 3. Production Deployment -- Database backup before migration -- Gradual rollout -- Monitor for issues -- Rollback plan ready +4. **Password Management** (Medium Priority) + - Password reset functionality + - Password strength requirements + - Password change interface -## Resources and Documentation +## User Tracking in Projects - NEW FEATURE โœ… -### NextAuth.js -- [Official Documentation](https://next-auth.js.org/) -- [Better SQLite3 Adapter](https://authjs.dev/reference/adapter/better-sqlite3) +### ๐Ÿ“Š Project User Management Implementation -### Security Libraries -- [Zod Validation](https://zod.dev/) -- [bcryptjs](https://www.npmjs.com/package/bcryptjs) +We've successfully implemented comprehensive user tracking for projects: -### Best Practices -- [OWASP Top 10](https://owasp.org/www-project-top-ten/) -- [Next.js Security Guidelines](https://nextjs.org/docs/advanced-features/security-headers) +#### 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 -**Next Steps**: Choose which phase to implement first and create detailed implementation tickets for development. +#### 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 diff --git a/EDGE_RUNTIME_FIX.md b/EDGE_RUNTIME_FIX.md new file mode 100644 index 0000000..3dac76e --- /dev/null +++ b/EDGE_RUNTIME_FIX.md @@ -0,0 +1,176 @@ +# 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! ๐ŸŽ‰ diff --git a/EDGE_RUNTIME_FIX_FINAL.md b/EDGE_RUNTIME_FIX_FINAL.md new file mode 100644 index 0000000..689b60c --- /dev/null +++ b/EDGE_RUNTIME_FIX_FINAL.md @@ -0,0 +1,161 @@ +# 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! ๐ŸŽŠ diff --git a/MERGE_PREPARATION_SUMMARY.md b/MERGE_PREPARATION_SUMMARY.md new file mode 100644 index 0000000..a23b049 --- /dev/null +++ b/MERGE_PREPARATION_SUMMARY.md @@ -0,0 +1,90 @@ +# 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. diff --git a/README.md b/README.md index 3d4a5dc..b6c2ba4 100644 --- a/README.md +++ b/README.md @@ -100,29 +100,42 @@ The application uses SQLite database which will be automatically initialized on ``` src/ โ”œโ”€โ”€ app/ # Next.js app router pages +โ”‚ โ”œโ”€โ”€ admin/ # Admin dashboard and user management โ”‚ โ”œโ”€โ”€ api/ # API routes +โ”‚ โ”‚ โ”œโ”€โ”€ admin/ # Admin-related endpoints (e.g., user management) โ”‚ โ”‚ โ”œโ”€โ”€ all-project-tasks/ # Get all project tasks endpoint +โ”‚ โ”‚ โ”œโ”€โ”€ audit-logs/ # Audit log endpoints +โ”‚ โ”‚ โ”œโ”€โ”€ auth/ # Authentication endpoints โ”‚ โ”‚ โ”œโ”€โ”€ contracts/ # Contract management endpoints โ”‚ โ”‚ โ”œโ”€โ”€ notes/ # Notes management endpoints โ”‚ โ”‚ โ”œโ”€โ”€ projects/ # Project management endpoints โ”‚ โ”‚ โ”œโ”€โ”€ project-tasks/ # Task management endpoints +โ”‚ โ”‚ โ”œโ”€โ”€ task-notes/ # Task-specific notes endpoints โ”‚ โ”‚ โ””โ”€โ”€ tasks/ # Task template endpoints +โ”‚ โ”œโ”€โ”€ auth/ # Authentication pages (login, etc.) โ”‚ โ”œโ”€โ”€ contracts/ # Contract pages โ”‚ โ”œโ”€โ”€ projects/ # Project pages +โ”‚ โ”œโ”€โ”€ project-tasks/ # Project-specific task pages โ”‚ โ””โ”€โ”€ tasks/ # Task management pages โ”œโ”€โ”€ components/ # Reusable React components -โ”‚ โ”œโ”€โ”€ ui/ # UI components (Button, Card, etc.) -โ”‚ โ”œโ”€โ”€ ContractForm.js # Contract form component -โ”‚ โ”œโ”€โ”€ NoteForm.js # Note form component -โ”‚ โ”œโ”€โ”€ ProjectForm.js # Project form component +โ”‚ โ”œโ”€โ”€ auth/ # Authentication-related components +โ”‚ โ”œโ”€โ”€ ui/ # UI components (Button, Card, etc.) +โ”‚ โ”œโ”€โ”€ AuditLogViewer.js # Component to view audit logs +โ”‚ โ”œโ”€โ”€ ContractForm.js # Contract form component +โ”‚ โ”œโ”€โ”€ NoteForm.js # Note form component +โ”‚ โ”œโ”€โ”€ ProjectForm.js # Project form component โ”‚ โ”œโ”€โ”€ ProjectTaskForm.js # Project task form component โ”‚ โ”œโ”€โ”€ ProjectTasksSection.js # Project tasks section component -โ”‚ โ”œโ”€โ”€ TaskForm.js # Task form component +โ”‚ โ”œโ”€โ”€ TaskForm.js # Task form component โ”‚ โ””โ”€โ”€ TaskTemplateForm.js # Task template form component -โ””โ”€โ”€ lib/ # Utility functions - โ”œโ”€โ”€ queries/ # Database query functions - โ”œโ”€โ”€ db.js # Database connection - โ””โ”€โ”€ init-db.js # Database initialization +โ”œโ”€โ”€ lib/ # Utility functions +โ”‚ โ”œโ”€โ”€ queries/ # Database query functions +โ”‚ โ”œโ”€โ”€ auditLog.js # Audit logging utilities +โ”‚ โ”œโ”€โ”€ auth.js # Authentication helpers +โ”‚ โ”œโ”€โ”€ db.js # Database connection +โ”‚ โ”œโ”€โ”€ init-db.js # Database initialization +โ”‚ โ””โ”€โ”€ userManagement.js # User management functions +โ””โ”€โ”€ middleware.js # Next.js middleware for auth and routing ``` ## Available Scripts @@ -147,6 +160,9 @@ The application uses the following main tables: - **tasks** - Task templates - **project_tasks** - Tasks assigned to specific projects - **notes** - Project notes and updates +- **users** - User accounts and roles for authentication +- **sessions** - User session management +- **audit_logs** - Detailed logs for security and tracking ## API Endpoints @@ -188,6 +204,19 @@ The application uses the following main tables: - `POST /api/notes` - Create new note - `DELETE /api/notes` - Delete note +### Audit Logs + +- `GET /api/audit-logs` - Get all audit logs +- `POST /api/audit-logs/log` - Create a new audit log entry +- `GET /api/audit-logs/stats` - Get audit log statistics + +### Admin + +- `GET /api/admin/users` - Get all users +- `POST /api/admin/users` - Create a new user +- `PUT /api/admin/users/[id]` - Update a user +- `DELETE /api/admin/users/[id]` - Delete a user + ## Advanced Map Features This project includes a powerful map system for project locations, supporting multiple dynamic base layers: diff --git a/check-audit-db.mjs b/check-audit-db.mjs new file mode 100644 index 0000000..316d113 --- /dev/null +++ b/check-audit-db.mjs @@ -0,0 +1,56 @@ +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(); diff --git a/check-columns.mjs b/check-columns.mjs new file mode 100644 index 0000000..5a98918 --- /dev/null +++ b/check-columns.mjs @@ -0,0 +1,13 @@ +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); diff --git a/check-projects-table.mjs b/check-projects-table.mjs new file mode 100644 index 0000000..25dfc17 --- /dev/null +++ b/check-projects-table.mjs @@ -0,0 +1,5 @@ +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)); diff --git a/check-projects.mjs b/check-projects.mjs new file mode 100644 index 0000000..10823c7 --- /dev/null +++ b/check-projects.mjs @@ -0,0 +1,32 @@ +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(); diff --git a/check-task-schema.mjs b/check-task-schema.mjs new file mode 100644 index 0000000..626abe4 --- /dev/null +++ b/check-task-schema.mjs @@ -0,0 +1,25 @@ +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(); diff --git a/src/app/comprehensive-polish-map/page.js b/debug-disabled/comprehensive-polish-map/page.js similarity index 98% rename from src/app/comprehensive-polish-map/page.js rename to debug-disabled/comprehensive-polish-map/page.js index 3f096e6..130c7c2 100644 --- a/src/app/comprehensive-polish-map/page.js +++ b/debug-disabled/comprehensive-polish-map/page.js @@ -1,7 +1,15 @@ "use client"; import { useState } from 'react'; -import ComprehensivePolishMap from '../../components/ui/ComprehensivePolishMap'; +import dynamic from 'next/dynamic'; + +const ComprehensivePolishMap = dynamic( + () => import('../../components/ui/ComprehensivePolishMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); export default function ComprehensivePolishMapPage() { const [selectedLocation, setSelectedLocation] = useState('krakow'); diff --git a/debug-disabled/debug-polish-orthophoto/layout.disabled.js b/debug-disabled/debug-polish-orthophoto/layout.disabled.js new file mode 100644 index 0000000..6ddd998 --- /dev/null +++ b/debug-disabled/debug-polish-orthophoto/layout.disabled.js @@ -0,0 +1,9 @@ +// 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'; diff --git a/src/app/debug-polish-orthophoto/page.js b/debug-disabled/debug-polish-orthophoto/page.js similarity index 93% rename from src/app/debug-polish-orthophoto/page.js rename to debug-disabled/debug-polish-orthophoto/page.js index 2722048..71aef92 100644 --- a/src/app/debug-polish-orthophoto/page.js +++ b/debug-disabled/debug-polish-orthophoto/page.js @@ -1,6 +1,16 @@ "use client"; -import DebugPolishOrthophotoMap from '../../components/ui/DebugPolishOrthophotoMap'; +import dynamic from 'next/dynamic'; + +const DebugPolishOrthophotoMap = dynamic( + () => import('../../components/ui/DebugPolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); + +export const dynamicParams = true; export default function DebugPolishOrthophotoPage() { // Test marker in Poland @@ -100,4 +110,4 @@ export default function DebugPolishOrthophotoPage() { ); -} +} \ No newline at end of file diff --git a/src/app/test-improved-wmts/page.js b/debug-disabled/test-improved-wmts/page.js similarity index 93% rename from src/app/test-improved-wmts/page.js rename to debug-disabled/test-improved-wmts/page.js index 83e3efe..fe336ca 100644 --- a/src/app/test-improved-wmts/page.js +++ b/debug-disabled/test-improved-wmts/page.js @@ -1,6 +1,14 @@ "use client"; -import ImprovedPolishOrthophotoMap from '../../components/ui/ImprovedPolishOrthophotoMap'; +import dynamic from 'next/dynamic'; + +const ImprovedPolishOrthophotoMap = dynamic( + () => import('../../components/ui/ImprovedPolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); export default function ImprovedPolishOrthophotoPage() { const testMarkers = [ diff --git a/src/app/test-polish-map/page.js b/debug-disabled/test-polish-map/page.js similarity index 94% rename from src/app/test-polish-map/page.js rename to debug-disabled/test-polish-map/page.js index 70956e4..df033d9 100644 --- a/src/app/test-polish-map/page.js +++ b/debug-disabled/test-polish-map/page.js @@ -1,8 +1,23 @@ "use client"; import { useState } from 'react'; -import PolishOrthophotoMap from '../../components/ui/PolishOrthophotoMap'; -import AdvancedPolishOrthophotoMap from '../../components/ui/AdvancedPolishOrthophotoMap'; +import dynamic from 'next/dynamic'; + +const PolishOrthophotoMap = dynamic( + () => import('../../components/ui/PolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); + +const AdvancedPolishOrthophotoMap = dynamic( + () => import('../../components/ui/AdvancedPolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); export default function PolishOrthophotoTestPage() { const [activeMap, setActiveMap] = useState('basic'); diff --git a/src/app/test-polish-orthophoto/page.js b/debug-disabled/test-polish-orthophoto/page.js similarity index 93% rename from src/app/test-polish-orthophoto/page.js rename to debug-disabled/test-polish-orthophoto/page.js index 640b0ff..fecc41a 100644 --- a/src/app/test-polish-orthophoto/page.js +++ b/debug-disabled/test-polish-orthophoto/page.js @@ -1,6 +1,14 @@ "use client"; -import PolishOrthophotoMap from '../../components/ui/PolishOrthophotoMap'; +import dynamic from 'next/dynamic'; + +const PolishOrthophotoMap = dynamic( + () => import('../../components/ui/PolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); export default function TestPolishOrthophotoPage() { // Test markers - various locations in Poland diff --git a/debug-task-insert.mjs b/debug-task-insert.mjs new file mode 100644 index 0000000..edc8666 --- /dev/null +++ b/debug-task-insert.mjs @@ -0,0 +1,49 @@ +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(); diff --git a/fix-notes-columns.mjs b/fix-notes-columns.mjs new file mode 100644 index 0000000..284c8ac --- /dev/null +++ b/fix-notes-columns.mjs @@ -0,0 +1,60 @@ +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(); diff --git a/fix-task-columns.mjs b/fix-task-columns.mjs new file mode 100644 index 0000000..cbb4c7a --- /dev/null +++ b/fix-task-columns.mjs @@ -0,0 +1,37 @@ +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(); diff --git a/package-lock.json b/package-lock.json index 8a6fd5e..e734519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,20 @@ "name": "panel", "version": "0.1.0", "dependencies": { + "bcryptjs": "^3.0.2", "better-sqlite3": "^11.10.0", "date-fns": "^4.1.0", "leaflet": "^1.9.4", "next": "15.1.8", + "next-auth": "^5.0.0-beta.29", + "node-fetch": "^3.3.2", "proj4": "^2.19.3", "proj4leaflet": "^1.0.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-leaflet": "^5.0.0", - "recharts": "^2.15.3" + "recharts": "^2.15.3", + "zod": "^3.25.67" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -68,6 +72,35 @@ "node": ">=6.0.0" } }, + "node_modules/@auth/core": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz", + "integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1912,6 +1945,15 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@petamoriken/float16": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz", @@ -3396,6 +3438,14 @@ } ] }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/better-sqlite3": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", @@ -4114,6 +4164,14 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -5262,6 +5320,28 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5374,6 +5454,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -7176,6 +7267,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7720,6 +7820,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.29", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.29.tgz", + "integrity": "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.40.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0-0", + "nodemailer": "^6.6.5", + "react": "^18.2.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -7758,6 +7885,42 @@ "node": ">=10" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7801,6 +7964,15 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth4webapi": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.3.tgz", + "integrity": "sha512-2bnHosmBLAQpXNBLOvaJMyMkr4Yya5ohE5Q9jqyxiN+aa7GFCzvDN1RRRMrp0NkfqRR2MTaQNkcSUCCjILD9oQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8404,6 +8576,25 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -10382,6 +10573,14 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/web-worker": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", @@ -10826,6 +11025,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zstddec": { "version": "0.2.0-alpha.3", "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0-alpha.3.tgz", @@ -10857,6 +11064,18 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "@auth/core": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz", + "integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==", + "requires": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + } + }, "@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -12031,6 +12250,11 @@ "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", "dev": true }, + "@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==" + }, "@petamoriken/float16": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz", @@ -13077,6 +13301,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==" + }, "better-sqlite3": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", @@ -13577,6 +13806,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, "data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -14419,6 +14653,15 @@ "bser": "2.1.1" } }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -14500,6 +14743,14 @@ "mime-types": "^2.1.12" } }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -15738,6 +15989,11 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true }, + "jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16128,6 +16384,14 @@ } } }, + "next-auth": { + "version": "5.0.0-beta.29", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.29.tgz", + "integrity": "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==", + "requires": { + "@auth/core": "0.40.0" + } + }, "node-abi": { "version": "3.75.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", @@ -16136,6 +16400,21 @@ "semver": "^7.3.5" } }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, + "node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -16169,6 +16448,11 @@ "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", "dev": true }, + "oauth4webapi": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.3.tgz", + "integrity": "sha512-2bnHosmBLAQpXNBLOvaJMyMkr4Yya5ohE5Q9jqyxiN+aa7GFCzvDN1RRRMrp0NkfqRR2MTaQNkcSUCCjILD9oQ==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -16559,6 +16843,17 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==" + }, + "preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "requires": {} + }, "prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -17931,6 +18226,11 @@ "makeerror": "1.0.12" } }, + "web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" + }, "web-worker": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", @@ -18240,6 +18540,11 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, + "zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==" + }, "zstddec": { "version": "0.2.0-alpha.3", "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0-alpha.3.tgz", diff --git a/package.json b/package.json index 0051bae..e12c617 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "panel", "version": "0.1.0", "private": true, + "type": "module", "scripts": { "dev": "next dev", "build": "next build", @@ -14,16 +15,20 @@ "test:e2e:ui": "playwright test --ui" }, "dependencies": { + "bcryptjs": "^3.0.2", "better-sqlite3": "^11.10.0", "date-fns": "^4.1.0", "leaflet": "^1.9.4", "next": "15.1.8", + "next-auth": "^5.0.0-beta.29", + "node-fetch": "^3.3.2", "proj4": "^2.19.3", "proj4leaflet": "^1.0.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-leaflet": "^5.0.0", - "recharts": "^2.15.3" + "recharts": "^2.15.3", + "zod": "^3.25.67" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/public/test-auth.html b/public/test-auth.html new file mode 100644 index 0000000..a12361d --- /dev/null +++ b/public/test-auth.html @@ -0,0 +1,142 @@ + + + + + + Authentication Test Page + + + +

Authentication & API Test Page

+ +
+

Authentication Status

+ +
+
+ +
+

API Endpoint Tests

+ +
+
+ +
+

Manual Login Instructions

+
+

Test Credentials:

+

Email: admin@localhost.com

+

Password: admin123456

+

Open Sign-in Page

+
+
+ + + + diff --git a/scripts/create-admin.js b/scripts/create-admin.js new file mode 100644 index 0000000..5da9828 --- /dev/null +++ b/scripts/create-admin.js @@ -0,0 +1,34 @@ +import { createUser } from "../src/lib/userManagement.js" +import initializeDatabase from "../src/lib/init-db.js" + +async function createInitialAdmin() { + try { + // Initialize database first + initializeDatabase() + + console.log("Creating initial admin user...") + + const adminUser = await createUser({ + name: "Administrator", + email: "admin@localhost.com", + password: "admin123456", // Change this in production! + role: "admin" + }) + + console.log("โœ… Initial admin user created successfully!") + console.log("๐Ÿ“ง Email: admin@localhost.com") + console.log("๐Ÿ”‘ Password: admin123456") + console.log("โš ๏ธ Please change the password after first login!") + console.log("๐Ÿ‘ค User ID:", adminUser.id) + + } catch (error) { + if (error.message.includes("already exists")) { + console.log("โ„น๏ธ Admin user already exists. Skipping creation.") + } else { + console.error("โŒ Error creating admin user:", error.message) + process.exit(1) + } + } +} + +createInitialAdmin() diff --git a/src/app/admin/audit-logs/page.js b/src/app/admin/audit-logs/page.js new file mode 100644 index 0000000..7731969 --- /dev/null +++ b/src/app/admin/audit-logs/page.js @@ -0,0 +1,55 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import AuditLogViewer from "@/components/AuditLogViewer"; + +export default function AuditLogsPage() { + const { data: session, status } = useSession(); + const router = useRouter(); + + useEffect(() => { + if (status === "loading") return; // Still loading + + if (!session) { + router.push("/auth/signin"); + return; + } + + // Only allow admins and project managers to view audit logs + if (!["admin", "project_manager"].includes(session.user.role)) { + router.push("/"); + return; + } + }, [session, status, router]); + + if (status === "loading") { + return ( +
+
+
+ ); + } + + if (!session || !["admin", "project_manager"].includes(session.user.role)) { + return ( +
+
+

+ Access Denied +

+

+ You don't have permission to view this page. +

+
+
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/src/app/admin/users/[id]/edit/page.js b/src/app/admin/users/[id]/edit/page.js new file mode 100644 index 0000000..6a57ea0 --- /dev/null +++ b/src/app/admin/users/[id]/edit/page.js @@ -0,0 +1,336 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { useRouter, useParams } from "next/navigation"; +import Link from "next/link"; +import { Card, CardHeader, CardContent } from "@/components/ui/Card"; +import Button from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; +import PageContainer from "@/components/ui/PageContainer"; +import PageHeader from "@/components/ui/PageHeader"; +import { LoadingState } from "@/components/ui/States"; + +export default function EditUserPage() { + const [user, setUser] = useState(null); + const [formData, setFormData] = useState({ + name: "", + email: "", + role: "user", + is_active: true, + password: "" + }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + const { data: session, status } = useSession(); + const router = useRouter(); + const params = useParams(); + + // Check if user is admin + useEffect(() => { + if (status === "loading") return; + if (!session || session.user.role !== "admin") { + router.push("/"); + return; + } + }, [session, status, router]); + + // Fetch user data + useEffect(() => { + if (session?.user?.role === "admin" && params.id) { + fetchUser(); + } + }, [session, params.id]); + + const fetchUser = async () => { + try { + setLoading(true); + const response = await fetch(`/api/admin/users/${params.id}`); + + if (!response.ok) { + if (response.status === 404) { + setError("User not found"); + return; + } + throw new Error("Failed to fetch user"); + } + + const userData = await response.json(); + setUser(userData); + setFormData({ + name: userData.name, + email: userData.email, + role: userData.role, + is_active: userData.is_active, + password: "" // Never populate password field + }); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setSaving(true); + setError(""); + setSuccess(""); + + try { + // Prepare update data (exclude empty password) + const updateData = { + name: formData.name, + email: formData.email, + role: formData.role, + is_active: formData.is_active + }; + + // Only include password if it's provided + if (formData.password.trim()) { + if (formData.password.length < 6) { + throw new Error("Password must be at least 6 characters long"); + } + updateData.password = formData.password; + } + + const response = await fetch(`/api/admin/users/${params.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updateData), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to update user"); + } + + const updatedUser = await response.json(); + setUser(updatedUser); + setSuccess("User updated successfully"); + + // Clear password field after successful update + setFormData(prev => ({ ...prev, password: "" })); + + } catch (err) { + setError(err.message); + } finally { + setSaving(false); + } + }; + + if (status === "loading" || !session) { + return ; + } + + if (session.user.role !== "admin") { + return ( + +
+

Access Denied

+

You need admin privileges to access this page.

+ + + +
+
+ ); + } + + if (loading) { + return ; + } + + if (error && !user) { + return ( + +
+

Error

+

{error}

+ + + +
+
+ ); + } + + return ( + + + + + + + + {error && ( +
+

{error}

+
+ )} + + {success && ( +
+

{success}

+
+ )} + + + +

User Information

+
+ +
+
+
+ + setFormData({ ...formData, name: e.target.value })} + required + /> +
+ +
+ + setFormData({ ...formData, email: e.target.value })} + required + /> +
+ +
+ + +
+ +
+ + setFormData({ ...formData, password: e.target.value })} + placeholder="Leave blank to keep current password" + minLength={6} + /> +

+ Leave blank to keep the current password +

+
+
+ +
+ setFormData({ ...formData, is_active: e.target.checked })} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + disabled={user?.id === session?.user?.id} + /> + +
+ +
+ + + + +
+
+
+
+ + {/* User Details Card */} + {user && ( + + +

Account Details

+
+ +
+
+

Created

+

{new Date(user.created_at).toLocaleDateString()}

+
+
+

Last Updated

+

+ {user.updated_at ? new Date(user.updated_at).toLocaleDateString() : "Never"} +

+
+
+

Last Login

+

+ {user.last_login ? new Date(user.last_login).toLocaleDateString() : "Never"} +

+
+
+

Failed Login Attempts

+

{user.failed_login_attempts || 0}

+
+
+

Account Status

+

+ {user.is_active ? "Active" : "Inactive"} +

+
+
+

Account Locked

+

+ {user.locked_until && new Date(user.locked_until) > new Date() + ? `Until ${new Date(user.locked_until).toLocaleDateString()}` + : "No" + } +

+
+
+
+
+ )} +
+ ); +} diff --git a/src/app/admin/users/page.js b/src/app/admin/users/page.js new file mode 100644 index 0000000..b2b1727 --- /dev/null +++ b/src/app/admin/users/page.js @@ -0,0 +1,418 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Card, CardHeader, CardContent } from "@/components/ui/Card"; +import Button from "@/components/ui/Button"; +import Badge from "@/components/ui/Badge"; +import { Input } from "@/components/ui/Input"; +import PageContainer from "@/components/ui/PageContainer"; +import PageHeader from "@/components/ui/PageHeader"; +import { LoadingState } from "@/components/ui/States"; +import { formatDate } from "@/lib/utils"; + +export default function UserManagementPage() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [showCreateForm, setShowCreateForm] = useState(false); + const { data: session, status } = useSession(); + const router = useRouter(); + + // Check if user is admin + useEffect(() => { + if (status === "loading") return; + if (!session || session.user.role !== "admin") { + router.push("/"); + return; + } + }, [session, status, router]); + + // Fetch users + useEffect(() => { + if (session?.user?.role === "admin") { + fetchUsers(); + } + }, [session]); + + const fetchUsers = async () => { + try { + setLoading(true); + const response = await fetch("/api/admin/users"); + if (!response.ok) { + throw new Error("Failed to fetch users"); + } + const data = await response.json(); + setUsers(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleDeleteUser = async (userId) => { + if (!confirm("Are you sure you want to delete this user?")) return; + + try { + const response = await fetch(`/api/admin/users/${userId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Failed to delete user"); + } + + setUsers(users.filter(user => user.id !== userId)); + } catch (err) { + setError(err.message); + } + }; + + const handleToggleUser = async (userId, isActive) => { + try { + const response = await fetch(`/api/admin/users/${userId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ is_active: !isActive }), + }); + + if (!response.ok) { + throw new Error("Failed to update user"); + } + + setUsers(users.map(user => + user.id === userId + ? { ...user, is_active: !isActive } + : user + )); + } catch (err) { + setError(err.message); + } + }; + + const getRoleColor = (role) => { + switch (role) { + case "admin": + return "red"; + case "project_manager": + return "blue"; + case "user": + return "green"; + case "read_only": + return "gray"; + default: + return "gray"; + } + }; + + const getRoleDisplay = (role) => { + switch (role) { + case "project_manager": + return "Project Manager"; + case "read_only": + return "Read Only"; + default: + return role.charAt(0).toUpperCase() + role.slice(1); + } + }; + + if (status === "loading" || !session) { + return ; + } + + if (session.user.role !== "admin") { + return ( + +
+

Access Denied

+

You need admin privileges to access this page.

+ + + +
+
+ ); + } + + return ( + + + + + + {error && ( +
+

{error}

+
+ )} + + {loading ? ( + + ) : ( +
+ {/* Users List */} +
+ {users.length === 0 ? ( + + +
+ + + +

No Users Found

+

Start by creating your first user.

+
+
+
+ ) : ( + users.map((user) => ( + + +
+
+
+
+ + + +
+
+
+

{user.name}

+

{user.email}

+
+
+
+ + {getRoleDisplay(user.role)} + + + {user.is_active ? "Active" : "Inactive"} + +
+
+
+ +
+
+

Created

+

{formatDate(user.created_at)}

+
+
+

Last Login

+

+ {user.last_login ? formatDate(user.last_login) : "Never"} +

+
+
+

Failed Attempts

+

{user.failed_login_attempts || 0}

+
+
+ + {user.locked_until && new Date(user.locked_until) > new Date() && ( +
+

+ Account locked until {formatDate(user.locked_until)} +

+
+ )} + +
+
+ + + + +
+ +
+
+
+ )) + )} +
+
+ )} + + {/* Create User Modal/Form */} + {showCreateForm && ( + setShowCreateForm(false)} + onUserCreated={(newUser) => { + setUsers([...users, newUser]); + setShowCreateForm(false); + }} + /> + )} +
+ ); +} + +// Create User Modal Component +function CreateUserModal({ onClose, onUserCreated }) { + const [formData, setFormData] = useState({ + name: "", + email: "", + password: "", + role: "user", + is_active: true + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(""); + + try { + const response = await fetch("/api/admin/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to create user"); + } + + const newUser = await response.json(); + onUserCreated(newUser); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

Create New User

+ +
+ + {error && ( +
+

{error}

+
+ )} + +
+
+ + setFormData({ ...formData, name: e.target.value })} + required + /> +
+ +
+ + setFormData({ ...formData, email: e.target.value })} + required + /> +
+ +
+ + setFormData({ ...formData, password: e.target.value })} + required + minLength={6} + /> +
+ +
+ + +
+ +
+ setFormData({ ...formData, is_active: e.target.checked })} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> + +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/app/api/admin/users/[id]/route.js b/src/app/api/admin/users/[id]/route.js new file mode 100644 index 0000000..b686f43 --- /dev/null +++ b/src/app/api/admin/users/[id]/route.js @@ -0,0 +1,129 @@ +import { getUserById, updateUser, deleteUser } from "@/lib/userManagement.js"; +import { NextResponse } from "next/server"; +import { withAdminAuth } from "@/lib/middleware/auth"; + +// GET: Get user by ID (admin only) +async function getUserHandler(req, { params }) { + try { + const user = getUserById(params.id); + + if (!user) { + return NextResponse.json( + { error: "User not found" }, + { status: 404 } + ); + } + + // Remove password hash from response + const { password_hash, ...safeUser } = user; + return NextResponse.json(safeUser); + + } catch (error) { + console.error("Error fetching user:", error); + return NextResponse.json( + { error: "Failed to fetch user" }, + { status: 500 } + ); + } +} + +// PUT: Update user (admin only) +async function updateUserHandler(req, { params }) { + try { + const data = await req.json(); + const userId = params.id; + + // Prevent admin from deactivating themselves + if (data.is_active === false && userId === req.user.id) { + return NextResponse.json( + { error: "You cannot deactivate your own account" }, + { status: 400 } + ); + } + + // Validate role if provided + if (data.role) { + const validRoles = ["read_only", "user", "project_manager", "admin"]; + if (!validRoles.includes(data.role)) { + return NextResponse.json( + { error: "Invalid role specified" }, + { status: 400 } + ); + } + } + + // Validate password length if provided + if (data.password && data.password.length < 6) { + return NextResponse.json( + { error: "Password must be at least 6 characters long" }, + { status: 400 } + ); + } + + const updatedUser = await updateUser(userId, data); + + if (!updatedUser) { + return NextResponse.json( + { error: "User not found" }, + { status: 404 } + ); + } + + // Remove password hash from response + const { password_hash, ...safeUser } = updatedUser; + return NextResponse.json(safeUser); + + } catch (error) { + console.error("Error updating user:", error); + + if (error.message.includes("already exists")) { + return NextResponse.json( + { error: "A user with this email already exists" }, + { status: 409 } + ); + } + + return NextResponse.json( + { error: "Failed to update user" }, + { status: 500 } + ); + } +} + +// DELETE: Delete user (admin only) +async function deleteUserHandler(req, { params }) { + try { + const userId = params.id; + + // Prevent admin from deleting themselves + if (userId === req.user.id) { + return NextResponse.json( + { error: "You cannot delete your own account" }, + { status: 400 } + ); + } + + const success = await deleteUser(userId); + + if (!success) { + return NextResponse.json( + { error: "User not found" }, + { status: 404 } + ); + } + + return NextResponse.json({ message: "User deleted successfully" }); + + } catch (error) { + console.error("Error deleting user:", error); + return NextResponse.json( + { error: "Failed to delete user" }, + { status: 500 } + ); + } +} + +// Protected routes - require admin authentication +export const GET = withAdminAuth(getUserHandler); +export const PUT = withAdminAuth(updateUserHandler); +export const DELETE = withAdminAuth(deleteUserHandler); diff --git a/src/app/api/admin/users/route.js b/src/app/api/admin/users/route.js new file mode 100644 index 0000000..324162c --- /dev/null +++ b/src/app/api/admin/users/route.js @@ -0,0 +1,85 @@ +import { getAllUsers, createUser } from "@/lib/userManagement.js"; +import { NextResponse } from "next/server"; +import { withAdminAuth } from "@/lib/middleware/auth"; + +// GET: Get all users (admin only) +async function getUsersHandler(req) { + try { + const users = getAllUsers(); + // Remove password hashes from response + const safeUsers = users.map(user => { + const { password_hash, ...safeUser } = user; + return safeUser; + }); + return NextResponse.json(safeUsers); + } catch (error) { + console.error("Error fetching users:", error); + return NextResponse.json( + { error: "Failed to fetch users" }, + { status: 500 } + ); + } +} + +// POST: Create new user (admin only) +async function createUserHandler(req) { + try { + const data = await req.json(); + + // Validate required fields + if (!data.name || !data.email || !data.password) { + return NextResponse.json( + { error: "Name, email, and password are required" }, + { status: 400 } + ); + } + + // Validate password length + if (data.password.length < 6) { + return NextResponse.json( + { error: "Password must be at least 6 characters long" }, + { status: 400 } + ); + } + + // Validate role + const validRoles = ["read_only", "user", "project_manager", "admin"]; + if (data.role && !validRoles.includes(data.role)) { + return NextResponse.json( + { error: "Invalid role specified" }, + { status: 400 } + ); + } + + const newUser = await createUser({ + name: data.name, + email: data.email, + password: data.password, + role: data.role || "user", + is_active: data.is_active !== undefined ? data.is_active : true + }); + + // Remove password hash from response + const { password_hash, ...safeUser } = newUser; + return NextResponse.json(safeUser, { status: 201 }); + + } catch (error) { + console.error("Error creating user:", error); + + if (error.message.includes("already exists")) { + return NextResponse.json( + { error: "A user with this email already exists" }, + { status: 409 } + ); + } + + return NextResponse.json( + { error: "Failed to create user" }, + { status: 500 } + ); + } +} + +// Protected routes - require admin authentication +export const GET = withAdminAuth(getUsersHandler); +export const POST = withAdminAuth(createUserHandler); diff --git a/src/app/api/all-project-tasks/route.js b/src/app/api/all-project-tasks/route.js index 0ed991d..5d645b1 100644 --- a/src/app/api/all-project-tasks/route.js +++ b/src/app/api/all-project-tasks/route.js @@ -1,8 +1,9 @@ import { getAllProjectTasks } from "@/lib/queries/tasks"; import { NextResponse } from "next/server"; +import { withReadAuth } from "@/lib/middleware/auth"; // GET: Get all project tasks across all projects -export async function GET() { +async function getAllProjectTasksHandler() { try { const tasks = getAllProjectTasks(); return NextResponse.json(tasks); @@ -13,3 +14,6 @@ export async function GET() { ); } } + +// Protected routes - require authentication +export const GET = withReadAuth(getAllProjectTasksHandler); diff --git a/src/app/api/audit-logs/log/route.js b/src/app/api/audit-logs/log/route.js new file mode 100644 index 0000000..6f7a218 --- /dev/null +++ b/src/app/api/audit-logs/log/route.js @@ -0,0 +1,49 @@ +// Force this API route to use Node.js runtime for database access +export const runtime = "nodejs"; + +import { NextResponse } from "next/server"; +import { logAuditEvent } from "@/lib/auditLog"; + +export async function POST(request) { + try { + const data = await request.json(); + + const { + action, + userId, + resourceType, + resourceId, + ipAddress, + userAgent, + details, + timestamp, + } = data; + + if (!action) { + return NextResponse.json( + { error: "Action is required" }, + { status: 400 } + ); + } + + // Log the audit event + await logAuditEvent({ + action, + userId, + resourceType, + resourceId, + ipAddress, + userAgent, + details, + timestamp, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Audit log API error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/audit-logs/route.js b/src/app/api/audit-logs/route.js new file mode 100644 index 0000000..66df429 --- /dev/null +++ b/src/app/api/audit-logs/route.js @@ -0,0 +1,67 @@ +// Force this API route to use Node.js runtime +export const runtime = "nodejs"; + +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { getAuditLogs, getAuditLogStats } from "@/lib/auditLog"; + +export async function GET(request) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Only admins and project managers can view audit logs + if (!["admin", "project_manager"].includes(session.user.role)) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { searchParams } = new URL(request.url); + + // Parse query parameters + const filters = { + userId: searchParams.get("userId") || null, + action: searchParams.get("action") || null, + resourceType: searchParams.get("resourceType") || null, + resourceId: searchParams.get("resourceId") || null, + startDate: searchParams.get("startDate") || null, + endDate: searchParams.get("endDate") || null, + limit: parseInt(searchParams.get("limit")) || 100, + offset: parseInt(searchParams.get("offset")) || 0, + orderBy: searchParams.get("orderBy") || "timestamp", + orderDirection: searchParams.get("orderDirection") || "DESC", + }; + + // Get audit logs + const logs = await getAuditLogs(filters); + + // Get statistics if requested + const includeStats = searchParams.get("includeStats") === "true"; + let stats = null; + + if (includeStats) { + stats = await getAuditLogStats({ + startDate: filters.startDate, + endDate: filters.endDate, + }); + } + + return NextResponse.json({ + success: true, + data: logs, + stats, + filters: { + ...filters, + total: logs.length, + }, + }); + } catch (error) { + console.error("Audit logs API error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/audit-logs/stats/route.js b/src/app/api/audit-logs/stats/route.js new file mode 100644 index 0000000..cbe4606 --- /dev/null +++ b/src/app/api/audit-logs/stats/route.js @@ -0,0 +1,41 @@ +// Force this API route to use Node.js runtime +export const runtime = "nodejs"; + +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { getAuditLogStats } from "@/lib/auditLog"; + +export async function GET(request) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Only admins and project managers can view audit log statistics + if (!["admin", "project_manager"].includes(session.user.role)) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { searchParams } = new URL(request.url); + + const filters = { + startDate: searchParams.get("startDate") || null, + endDate: searchParams.get("endDate") || null, + }; + + const stats = await getAuditLogStats(filters); + + return NextResponse.json({ + success: true, + data: stats, + }); + } catch (error) { + console.error("Audit log stats API error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/auth/[...nextauth]/route.js b/src/app/api/auth/[...nextauth]/route.js new file mode 100644 index 0000000..866b2be --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.js @@ -0,0 +1,3 @@ +import { handlers } from "@/lib/auth" + +export const { GET, POST } = handlers diff --git a/src/app/api/contracts/[id]/route.js b/src/app/api/contracts/[id]/route.js index eae30af..0dd9410 100644 --- a/src/app/api/contracts/[id]/route.js +++ b/src/app/api/contracts/[id]/route.js @@ -1,7 +1,8 @@ import db from "@/lib/db"; import { NextResponse } from "next/server"; +import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; -export async function GET(req, { params }) { +async function getContractHandler(req, { params }) { const { id } = await params; const contract = db @@ -20,7 +21,7 @@ export async function GET(req, { params }) { return NextResponse.json(contract); } -export async function DELETE(req, { params }) { +async function deleteContractHandler(req, { params }) { const { id } = params; try { @@ -57,3 +58,7 @@ export async function DELETE(req, { params }) { ); } } + +// Protected routes - require authentication +export const GET = withReadAuth(getContractHandler); +export const DELETE = withUserAuth(deleteContractHandler); diff --git a/src/app/api/contracts/route.js b/src/app/api/contracts/route.js index 4867ccb..796ac4e 100644 --- a/src/app/api/contracts/route.js +++ b/src/app/api/contracts/route.js @@ -1,7 +1,8 @@ import db from "@/lib/db"; import { NextResponse } from "next/server"; +import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; -export async function GET() { +async function getContractsHandler() { const contracts = db .prepare( ` @@ -21,7 +22,7 @@ export async function GET() { return NextResponse.json(contracts); } -export async function POST(req) { +async function createContractHandler(req) { const data = await req.json(); db.prepare( ` @@ -46,3 +47,7 @@ export async function POST(req) { ); return NextResponse.json({ success: true }); } + +// Protected routes - require authentication +export const GET = withReadAuth(getContractsHandler); +export const POST = withUserAuth(createContractHandler); diff --git a/src/app/api/debug-auth/route.js b/src/app/api/debug-auth/route.js new file mode 100644 index 0000000..e63f04e --- /dev/null +++ b/src/app/api/debug-auth/route.js @@ -0,0 +1,37 @@ +import { auth } from "@/lib/auth" +import { NextResponse } from "next/server" + +export const GET = auth(async (req) => { + try { + console.log("=== DEBUG AUTH ENDPOINT ===") + console.log("Request URL:", req.url) + console.log("Auth object:", req.auth) + + if (!req.auth?.user) { + return NextResponse.json({ + error: "No session found", + debug: { + hasAuth: !!req.auth, + authKeys: req.auth ? Object.keys(req.auth) : [], + } + }, { status: 401 }) + } + + return NextResponse.json({ + message: "Authenticated", + user: req.auth.user, + debug: { + authKeys: Object.keys(req.auth), + userKeys: Object.keys(req.auth.user) + } + }) + + } catch (error) { + console.error("Auth debug error:", error) + return NextResponse.json({ + error: "Auth error", + message: error.message, + stack: error.stack + }, { status: 500 }) + } +}) diff --git a/src/app/api/notes/route.js b/src/app/api/notes/route.js index d728731..cc6bf79 100644 --- a/src/app/api/notes/route.js +++ b/src/app/api/notes/route.js @@ -1,32 +1,82 @@ +// Force this API route to use Node.js runtime for database access +export const runtime = "nodejs"; + import db from "@/lib/db"; import { NextResponse } from "next/server"; +import { withUserAuth } from "@/lib/middleware/auth"; +import { + logApiActionSafe, + AUDIT_ACTIONS, + RESOURCE_TYPES, +} from "@/lib/auditLogSafe.js"; -export async function POST(req) { +async function createNoteHandler(req) { const { project_id, task_id, note } = await req.json(); if (!note || (!project_id && !task_id)) { return NextResponse.json({ error: "Missing fields" }, { status: 400 }); } - db.prepare( + try { + const result = db + .prepare( + ` + INSERT INTO notes (project_id, task_id, note, created_by, note_date) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) ` - INSERT INTO notes (project_id, task_id, note) - VALUES (?, ?, ?) - ` - ).run(project_id || null, task_id || null, note); + ) + .run(project_id || null, task_id || null, note, req.user?.id || null); - return NextResponse.json({ success: true }); + // Log note creation + await logApiActionSafe( + req, + AUDIT_ACTIONS.NOTE_CREATE, + RESOURCE_TYPES.NOTE, + result.lastInsertRowid.toString(), + req.auth, // Use req.auth instead of req.session + { + noteData: { project_id, task_id, note_length: note.length }, + } + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error creating note:", error); + return NextResponse.json( + { error: "Failed to create note", details: error.message }, + { status: 500 } + ); + } } -export async function DELETE(_, { params }) { +async function deleteNoteHandler(req, { params }) { const { id } = params; + // Get note data before deletion for audit log + const note = db.prepare("SELECT * FROM notes WHERE note_id = ?").get(id); + db.prepare("DELETE FROM notes WHERE note_id = ?").run(id); + // Log note deletion + await logApiActionSafe( + req, + AUDIT_ACTIONS.NOTE_DELETE, + RESOURCE_TYPES.NOTE, + id, + req.auth, // Use req.auth instead of req.session + { + deletedNote: { + project_id: note?.project_id, + task_id: note?.task_id, + note_length: note?.note?.length || 0, + }, + } + ); + return NextResponse.json({ success: true }); } -export async function PUT(req, { params }) { +async function updateNoteHandler(req, { params }) { const noteId = params.id; const { note } = await req.json(); @@ -34,11 +84,40 @@ export async function PUT(req, { params }) { return NextResponse.json({ error: "Missing note or ID" }, { status: 400 }); } + // Get original note for audit log + const originalNote = db + .prepare("SELECT * FROM notes WHERE note_id = ?") + .get(noteId); + db.prepare( ` UPDATE notes SET note = ? WHERE note_id = ? ` ).run(note, noteId); + // Log note update + await logApiActionSafe( + req, + AUDIT_ACTIONS.NOTE_UPDATE, + RESOURCE_TYPES.NOTE, + noteId, + req.auth, // Use req.auth instead of req.session + { + originalNote: { + note_length: originalNote?.note?.length || 0, + project_id: originalNote?.project_id, + task_id: originalNote?.task_id, + }, + updatedNote: { + note_length: note.length, + }, + } + ); + return NextResponse.json({ success: true }); } + +// Protected routes - require authentication +export const POST = withUserAuth(createNoteHandler); +export const DELETE = withUserAuth(deleteNoteHandler); +export const PUT = withUserAuth(updateNoteHandler); diff --git a/src/app/api/project-tasks/[id]/route.js b/src/app/api/project-tasks/[id]/route.js index a9d7665..ce96dd8 100644 --- a/src/app/api/project-tasks/[id]/route.js +++ b/src/app/api/project-tasks/[id]/route.js @@ -3,9 +3,10 @@ import { deleteProjectTask, } from "@/lib/queries/tasks"; import { NextResponse } from "next/server"; +import { withUserAuth } from "@/lib/middleware/auth"; // PATCH: Update project task status -export async function PATCH(req, { params }) { +async function updateProjectTaskHandler(req, { params }) { try { const { status } = await req.json(); @@ -16,18 +17,19 @@ export async function PATCH(req, { params }) { ); } - updateProjectTaskStatus(params.id, status); + updateProjectTaskStatus(params.id, status, req.user?.id || null); return NextResponse.json({ success: true }); } catch (error) { + console.error("Error updating task status:", error); return NextResponse.json( - { error: "Failed to update project task" }, + { error: "Failed to update project task", details: error.message }, { status: 500 } ); } } // DELETE: Delete a project task -export async function DELETE(req, { params }) { +async function deleteProjectTaskHandler(req, { params }) { try { deleteProjectTask(params.id); return NextResponse.json({ success: true }); @@ -38,3 +40,7 @@ export async function DELETE(req, { params }) { ); } } + +// Protected routes - require authentication +export const PATCH = withUserAuth(updateProjectTaskHandler); +export const DELETE = withUserAuth(deleteProjectTaskHandler); diff --git a/src/app/api/project-tasks/route.js b/src/app/api/project-tasks/route.js index fd3c93b..9429832 100644 --- a/src/app/api/project-tasks/route.js +++ b/src/app/api/project-tasks/route.js @@ -5,9 +5,10 @@ import { } from "@/lib/queries/tasks"; import { NextResponse } from "next/server"; import db from "@/lib/db"; +import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; // GET: Get all project tasks or task templates based on query params -export async function GET(req) { +async function getProjectTasksHandler(req) { const { searchParams } = new URL(req.url); const projectId = searchParams.get("project_id"); @@ -23,7 +24,7 @@ export async function GET(req) { } // POST: Create a new project task -export async function POST(req) { +async function createProjectTaskHandler(req) { try { const data = await req.json(); @@ -42,11 +43,20 @@ export async function POST(req) { ); } - const result = createProjectTask(data); + // Add user tracking information from authenticated session + const taskData = { + ...data, + created_by: req.user?.id || null, + // If no assigned_to is specified, default to the creator + assigned_to: data.assigned_to || req.user?.id || null, + }; + + const result = createProjectTask(taskData); return NextResponse.json({ success: true, id: result.lastInsertRowid }); } catch (error) { + console.error("Error creating project task:", error); return NextResponse.json( - { error: "Failed to create project task" }, + { error: "Failed to create project task", details: error.message }, { status: 500 } ); } @@ -113,3 +123,7 @@ export async function PATCH(req) { ); } } + +// Protected routes - require authentication +export const GET = withReadAuth(getProjectTasksHandler); +export const POST = withUserAuth(createProjectTaskHandler); diff --git a/src/app/api/project-tasks/users/route.js b/src/app/api/project-tasks/users/route.js new file mode 100644 index 0000000..45f2f5a --- /dev/null +++ b/src/app/api/project-tasks/users/route.js @@ -0,0 +1,50 @@ +import { + updateProjectTaskAssignment, + getAllUsersForTaskAssignment, +} from "@/lib/queries/tasks"; +import { NextResponse } from "next/server"; +import { withUserAuth, withReadAuth } from "@/lib/middleware/auth"; + +// GET: Get all users for task assignment +async function getUsersForTaskAssignmentHandler(req) { + try { + const users = getAllUsersForTaskAssignment(); + return NextResponse.json(users); + } catch (error) { + return NextResponse.json( + { error: "Failed to fetch users" }, + { status: 500 } + ); + } +} + +// POST: Update task assignment +async function updateTaskAssignmentHandler(req) { + try { + const { taskId, assignedToUserId } = await req.json(); + + if (!taskId) { + return NextResponse.json( + { error: "taskId is required" }, + { status: 400 } + ); + } + + const result = updateProjectTaskAssignment(taskId, assignedToUserId); + + if (result.changes === 0) { + return NextResponse.json({ error: "Task not found" }, { status: 404 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json( + { error: "Failed to update task assignment" }, + { status: 500 } + ); + } +} + +// Protected routes +export const GET = withReadAuth(getUsersForTaskAssignmentHandler); +export const POST = withUserAuth(updateTaskAssignmentHandler); diff --git a/src/app/api/projects/[id]/route.js b/src/app/api/projects/[id]/route.js index 40803f5..ae5f59f 100644 --- a/src/app/api/projects/[id]/route.js +++ b/src/app/api/projects/[id]/route.js @@ -1,22 +1,103 @@ +// Force this API route to use Node.js runtime for database access +export const runtime = "nodejs"; + import { getProjectById, updateProject, deleteProject, } from "@/lib/queries/projects"; +import initializeDatabase from "@/lib/init-db"; import { NextResponse } from "next/server"; +import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; +import { + logApiActionSafe, + AUDIT_ACTIONS, + RESOURCE_TYPES, +} from "@/lib/auditLogSafe.js"; + +// Make sure the DB is initialized before queries run +initializeDatabase(); + +async function getProjectHandler(req, { params }) { + const { id } = await params; + const project = getProjectById(parseInt(id)); + + if (!project) { + return NextResponse.json({ error: "Project not found" }, { status: 404 }); + } + + // Log project view + await logApiActionSafe( + req, + AUDIT_ACTIONS.PROJECT_VIEW, + RESOURCE_TYPES.PROJECT, + id, + req.auth, // Use req.auth instead of req.session + { project_name: project.project_name } + ); -export async function GET(_, { params }) { - const project = getProjectById(params.id); return NextResponse.json(project); } -export async function PUT(req, { params }) { +async function updateProjectHandler(req, { params }) { + const { id } = await params; const data = await req.json(); - updateProject(params.id, data); + + // Get user ID from authenticated request + const userId = req.user?.id; + + // Get original project data for audit log + const originalProject = getProjectById(parseInt(id)); + + updateProject(parseInt(id), data, userId); + + // Get updated project + const updatedProject = getProjectById(parseInt(id)); + + // Log project update + await logApiActionSafe( + req, + AUDIT_ACTIONS.PROJECT_UPDATE, + RESOURCE_TYPES.PROJECT, + id, + req.auth, // Use req.auth instead of req.session + { + originalData: originalProject, + updatedData: data, + changedFields: Object.keys(data), + } + ); + + return NextResponse.json(updatedProject); +} + +async function deleteProjectHandler(req, { params }) { + const { id } = await params; + + // Get project data before deletion for audit log + const project = getProjectById(parseInt(id)); + + deleteProject(parseInt(id)); + + // Log project deletion + await logApiActionSafe( + req, + AUDIT_ACTIONS.PROJECT_DELETE, + RESOURCE_TYPES.PROJECT, + id, + req.auth, // Use req.auth instead of req.session + { + deletedProject: { + project_name: project?.project_name, + project_number: project?.project_number, + }, + } + ); + return NextResponse.json({ success: true }); } -export async function DELETE(_, { params }) { - deleteProject(params.id); - return NextResponse.json({ success: true }); -} +// Protected routes - require authentication +export const GET = withReadAuth(getProjectHandler); +export const PUT = withUserAuth(updateProjectHandler); +export const DELETE = withUserAuth(deleteProjectHandler); diff --git a/src/app/api/projects/route.js b/src/app/api/projects/route.js index 10ebd54..8439150 100644 --- a/src/app/api/projects/route.js +++ b/src/app/api/projects/route.js @@ -1,20 +1,90 @@ -import { getAllProjects, createProject } from "@/lib/queries/projects"; +// Force this API route to use Node.js runtime for database access +export const runtime = "nodejs"; + +import { + getAllProjects, + createProject, + getAllUsersForAssignment, +} from "@/lib/queries/projects"; import initializeDatabase from "@/lib/init-db"; import { NextResponse } from "next/server"; +import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; +import { + logApiActionSafe, + AUDIT_ACTIONS, + RESOURCE_TYPES, +} from "@/lib/auditLogSafe.js"; // Make sure the DB is initialized before queries run initializeDatabase(); -export async function GET(req) { +async function getProjectsHandler(req) { const { searchParams } = new URL(req.url); const contractId = searchParams.get("contract_id"); + const assignedTo = searchParams.get("assigned_to"); + const createdBy = searchParams.get("created_by"); + + let projects; + + if (assignedTo) { + const { getProjectsByAssignedUser } = await import( + "@/lib/queries/projects" + ); + projects = getProjectsByAssignedUser(assignedTo); + } else if (createdBy) { + const { getProjectsByCreator } = await import("@/lib/queries/projects"); + projects = getProjectsByCreator(createdBy); + } else { + projects = getAllProjects(contractId); + } + + // Log project list access + await logApiActionSafe( + req, + AUDIT_ACTIONS.PROJECT_VIEW, + RESOURCE_TYPES.PROJECT, + null, // No specific project ID for list view + req.auth, // Use req.auth instead of req.session + { + filters: { contractId, assignedTo, createdBy }, + resultCount: projects.length, + } + ); - const projects = getAllProjects(contractId); return NextResponse.json(projects); } -export async function POST(req) { +async function createProjectHandler(req) { const data = await req.json(); - createProject(data); - return NextResponse.json({ success: true }); + + // Get user ID from authenticated request + const userId = req.user?.id; + + const result = createProject(data, userId); + const projectId = result.lastInsertRowid; + + // Log project creation + await logApiActionSafe( + req, + AUDIT_ACTIONS.PROJECT_CREATE, + RESOURCE_TYPES.PROJECT, + projectId.toString(), + req.auth, // Use req.auth instead of req.session + { + projectData: { + project_name: data.project_name, + project_number: data.project_number, + contract_id: data.contract_id, + }, + } + ); + + return NextResponse.json({ + success: true, + projectId: projectId, + }); } + +// Protected routes - require authentication +export const GET = withReadAuth(getProjectsHandler); +export const POST = withUserAuth(createProjectHandler); diff --git a/src/app/api/projects/users/route.js b/src/app/api/projects/users/route.js new file mode 100644 index 0000000..32cedf3 --- /dev/null +++ b/src/app/api/projects/users/route.js @@ -0,0 +1,33 @@ +import { + getAllUsersForAssignment, + updateProjectAssignment, +} from "@/lib/queries/projects"; +import initializeDatabase from "@/lib/init-db"; +import { NextResponse } from "next/server"; +import { withUserAuth } from "@/lib/middleware/auth"; + +// Make sure the DB is initialized before queries run +initializeDatabase(); + +async function getUsersHandler(req) { + const users = getAllUsersForAssignment(); + return NextResponse.json(users); +} + +async function updateAssignmentHandler(req) { + const { projectId, assignedToUserId } = await req.json(); + + if (!projectId) { + return NextResponse.json( + { error: "Project ID is required" }, + { status: 400 } + ); + } + + updateProjectAssignment(projectId, assignedToUserId); + return NextResponse.json({ success: true }); +} + +// Protected routes - require authentication +export const GET = withUserAuth(getUsersHandler); +export const POST = withUserAuth(updateAssignmentHandler); diff --git a/src/app/api/task-notes/route.js b/src/app/api/task-notes/route.js index 28652ac..ba6bc69 100644 --- a/src/app/api/task-notes/route.js +++ b/src/app/api/task-notes/route.js @@ -4,9 +4,10 @@ import { deleteNote, } from "@/lib/queries/notes"; import { NextResponse } from "next/server"; +import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; // GET: Get notes for a specific task -export async function GET(req) { +async function getTaskNotesHandler(req) { const { searchParams } = new URL(req.url); const taskId = searchParams.get("task_id"); @@ -26,7 +27,7 @@ export async function GET(req) { } // POST: Add a note to a task -export async function POST(req) { +async function addTaskNoteHandler(req) { try { const { task_id, note, is_system } = await req.json(); @@ -37,7 +38,7 @@ export async function POST(req) { ); } - addNoteToTask(task_id, note, is_system); + addNoteToTask(task_id, note, is_system, req.user?.id || null); return NextResponse.json({ success: true }); } catch (error) { console.error("Error adding task note:", error); @@ -49,7 +50,7 @@ export async function POST(req) { } // DELETE: Delete a note -export async function DELETE(req) { +async function deleteTaskNoteHandler(req) { try { const { searchParams } = new URL(req.url); const noteId = searchParams.get("note_id"); @@ -71,3 +72,8 @@ export async function DELETE(req) { ); } } + +// Protected routes - require authentication +export const GET = withReadAuth(getTaskNotesHandler); +export const POST = withUserAuth(addTaskNoteHandler); +export const DELETE = withUserAuth(deleteTaskNoteHandler); diff --git a/src/app/api/tasks/[id]/route.js b/src/app/api/tasks/[id]/route.js index fd14899..5e792af 100644 --- a/src/app/api/tasks/[id]/route.js +++ b/src/app/api/tasks/[id]/route.js @@ -1,8 +1,9 @@ import db from "@/lib/db"; import { NextResponse } from "next/server"; +import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; // GET: Get a specific task template -export async function GET(req, { params }) { +async function getTaskHandler(req, { params }) { try { const template = db .prepare("SELECT * FROM tasks WHERE task_id = ? AND is_standard = 1") @@ -25,7 +26,7 @@ export async function GET(req, { params }) { } // PUT: Update a task template -export async function PUT(req, { params }) { +async function updateTaskHandler(req, { params }) { try { const { name, max_wait_days, description } = await req.json(); @@ -58,7 +59,7 @@ export async function PUT(req, { params }) { } // DELETE: Delete a task template -export async function DELETE(req, { params }) { +async function deleteTaskHandler(req, { params }) { try { const result = db .prepare("DELETE FROM tasks WHERE task_id = ? AND is_standard = 1") @@ -79,3 +80,8 @@ export async function DELETE(req, { params }) { ); } } + +// Protected routes - require authentication +export const GET = withReadAuth(getTaskHandler); +export const PUT = withUserAuth(updateTaskHandler); +export const DELETE = withUserAuth(deleteTaskHandler); diff --git a/src/app/api/tasks/route.js b/src/app/api/tasks/route.js index ce0cd22..24d63bc 100644 --- a/src/app/api/tasks/route.js +++ b/src/app/api/tasks/route.js @@ -1,8 +1,10 @@ import db from "@/lib/db"; import { NextResponse } from "next/server"; +import { withUserAuth, withReadAuth } from "@/lib/middleware/auth"; +import { getAllTaskTemplates } from "@/lib/queries/tasks"; // POST: create new template -export async function POST(req) { +async function createTaskHandler(req) { const { name, max_wait_days, description } = await req.json(); if (!name) { @@ -18,3 +20,13 @@ export async function POST(req) { return NextResponse.json({ success: true }); } + +// GET: Get all task templates +async function getTasksHandler(req) { + const templates = getAllTaskTemplates(); + return NextResponse.json(templates); +} + +// Protected routes - require authentication +export const GET = withReadAuth(getTasksHandler); +export const POST = withUserAuth(createTaskHandler); diff --git a/src/app/api/tasks/templates/route.js b/src/app/api/tasks/templates/route.js index 7c7387e..0f6b7ca 100644 --- a/src/app/api/tasks/templates/route.js +++ b/src/app/api/tasks/templates/route.js @@ -1,8 +1,12 @@ import { getAllTaskTemplates } from "@/lib/queries/tasks"; import { NextResponse } from "next/server"; +import { withReadAuth } from "@/lib/middleware/auth"; // GET: Get all task templates -export async function GET() { +async function getTaskTemplatesHandler() { const templates = getAllTaskTemplates(); return NextResponse.json(templates); } + +// Protected routes - require authentication +export const GET = withReadAuth(getTaskTemplatesHandler); diff --git a/src/app/auth/error/page.js b/src/app/auth/error/page.js new file mode 100644 index 0000000..757a2b0 --- /dev/null +++ b/src/app/auth/error/page.js @@ -0,0 +1,65 @@ +'use client' + +import { useSearchParams } from 'next/navigation' +import { Suspense } from 'react' + +function AuthErrorContent() { + const searchParams = useSearchParams() + const error = searchParams.get('error') + + const getErrorMessage = (error) => { + switch (error) { + case 'CredentialsSignin': + return 'Invalid email or password. Please check your credentials and try again.' + case 'AccessDenied': + return 'Access denied. You do not have permission to sign in.' + case 'Verification': + return 'The verification token has expired or has already been used.' + default: + return 'An unexpected error occurred during authentication. Please try again.' + } + } + + return ( +
+
+
+

+ Authentication Error +

+

+ {getErrorMessage(error)} +

+ {error && ( +

+ Error code: {error} +

+ )} + +
+
+
+ ) +} + +export default function AuthError() { + return ( + +
+
+

Loading...

+
+ + }> + +
+ ) +} diff --git a/src/app/auth/signin/page.js b/src/app/auth/signin/page.js new file mode 100644 index 0000000..bbc45ac --- /dev/null +++ b/src/app/auth/signin/page.js @@ -0,0 +1,142 @@ +"use client" + +import { useState, Suspense } from "react" +import { signIn, getSession } from "next-auth/react" +import { useRouter } from "next/navigation" +import { useSearchParams } from "next/navigation" + +function SignInContent() { + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [error, setError] = useState("") + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + const searchParams = useSearchParams() + const callbackUrl = searchParams.get("callbackUrl") || "/" + + const handleSubmit = async (e) => { + e.preventDefault() + setIsLoading(true) + setError("") + + try { + const result = await signIn("credentials", { + email, + password, + redirect: false, + }) + + if (result?.error) { + setError("Invalid email or password") + } else { + // Successful login + router.push(callbackUrl) + router.refresh() + } + } catch (error) { + setError("An error occurred. Please try again.") + } finally { + setIsLoading(false) + } + } + + return ( +
+
+
+

+ Sign in to your account +

+

+ Access the Project Management Panel +

+
+
+ {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+ +
+ +
+
+

Default Admin Account:

+

Email: admin@localhost

+

Password: admin123456

+
+
+
+
+
+ ) +} + +export default function SignIn() { + return ( + +
+
+

Loading...

+
+ + }> + +
+ ) +} \ No newline at end of file diff --git a/src/app/layout.js b/src/app/layout.js index f90d3dd..e2fb948 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -1,6 +1,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import Navigation from "@/components/ui/Navigation"; +import { AuthProvider } from "@/components/auth/AuthProvider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -23,8 +24,10 @@ export default function RootLayout({ children }) { - -
{children}
+ + +
{children}
+
); diff --git a/src/app/page.js b/src/app/page.js index 52ae231..22372d2 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; import Link from "next/link"; import { Card, CardHeader, CardContent } from "@/components/ui/Card"; import Button from "@/components/ui/Button"; @@ -24,6 +25,7 @@ import { formatDate } from "@/lib/utils"; import TaskStatusChart from "@/components/ui/TaskStatusChart"; export default function Home() { + const { data: session, status } = useSession(); const [stats, setStats] = useState({ totalProjects: 0, activeProjects: 0, @@ -47,6 +49,12 @@ export default function Home() { const [loading, setLoading] = useState(true); useEffect(() => { + // Only fetch data if user is authenticated + if (!session) { + setLoading(false); + return; + } + const fetchDashboardData = async () => { try { // Fetch all data concurrently @@ -210,7 +218,7 @@ export default function Home() { }; fetchDashboardData(); - }, []); + }, [session]); const getProjectStatusColor = (status) => { switch (status) { @@ -257,10 +265,38 @@ export default function Home() { ); } + + // Show loading state while session is being fetched + if (status === "loading") { + return ; + } + + // Show sign-in prompt if not authenticated + if (!session) { + return ( + +
+

+ Welcome to Project Management Panel +

+

+ Please sign in to access the project management system. +

+ + Sign In + +
+
+ ); + } + return (
diff --git a/src/app/projects/[id]/edit/page.js b/src/app/projects/[id]/edit/page.js index aae2915..5857a1d 100644 --- a/src/app/projects/[id]/edit/page.js +++ b/src/app/projects/[id]/edit/page.js @@ -1,17 +1,52 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; import ProjectForm from "@/components/ProjectForm"; import PageContainer from "@/components/ui/PageContainer"; import PageHeader from "@/components/ui/PageHeader"; import Button from "@/components/ui/Button"; import Link from "next/link"; +import { LoadingState } from "@/components/ui/States"; -export default async function EditProjectPage({ params }) { - const { id } = await params; - const res = await fetch(`http://localhost:3000/api/projects/${id}`, { - cache: "no-store", - }); - const project = await res.json(); +export default function EditProjectPage() { + const params = useParams(); + const id = params.id; + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - if (!project) { + useEffect(() => { + const fetchProject = async () => { + try { + const res = await fetch(`/api/projects/${id}`); + if (res.ok) { + const projectData = await res.json(); + setProject(projectData); + } else { + setError("Project not found"); + } + } catch (err) { + setError("Failed to load project"); + } finally { + setLoading(false); + } + }; + + if (id) { + fetchProject(); + } + }, [id]); + + if (loading) { + return ( + + + + ); + } + + if (error || !project) { return (
diff --git a/src/app/projects/[id]/page.js b/src/app/projects/[id]/page.js index a58f6fb..e01dea4 100644 --- a/src/app/projects/[id]/page.js +++ b/src/app/projects/[id]/page.js @@ -13,12 +13,12 @@ import { formatDate } from "@/lib/utils"; import PageContainer from "@/components/ui/PageContainer"; import PageHeader from "@/components/ui/PageHeader"; import ProjectStatusDropdown from "@/components/ProjectStatusDropdown"; -import ProjectMap from "@/components/ui/ProjectMap"; +import ClientProjectMap from "@/components/ui/ClientProjectMap"; export default async function ProjectViewPage({ params }) { const { id } = await params; - const project = getProjectWithContract(id); - const notes = getNotesForProject(id); + const project = await getProjectWithContract(id); + const notes = await getNotesForProject(id); if (!project) { return ( @@ -400,12 +400,20 @@ export default async function ProjectViewPage({ params }) {
{" "} -
+ + {" "} +

Project Location

{project.coordinates && ( - +
diff --git a/src/app/projects/map/page-old.js b/src/app/projects/map/page-old.js new file mode 100644 index 0000000..33e6751 --- /dev/null +++ b/src/app/projects/map/page-old.js @@ -0,0 +1,928 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Link from "next/link"; +import dynamic from "next/dynamic"; +import { useSearchParams, useRouter } from "next/navigation"; +import Button from "@/components/ui/Button"; +import { mapLayers } from "@/components/ui/mapLayers"; + +// Dynamically import the map component to avoid SSR issues +const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), { + ssr: false, + loading: () => ( +
+ Loading map... +
+ ), +}); + +export default function ProjectsMapPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [mapCenter, setMapCenter] = useState([50.0614, 19.9366]); // Default to Krakow, Poland + const [mapZoom, setMapZoom] = useState(10); // Default zoom level + const [statusFilters, setStatusFilters] = useState({ + registered: true, + in_progress_design: true, + in_progress_construction: true, + fulfilled: true, + }); + const [activeBaseLayer, setActiveBaseLayer] = useState("OpenStreetMap"); + const [activeOverlays, setActiveOverlays] = useState([]); + const [showLayerPanel, setShowLayerPanel] = useState(true); + const [currentTool, setCurrentTool] = useState("move"); // Current map tool + + // Status configuration with colors and labels + const statusConfig = { + registered: { + color: "#6B7280", + label: "Registered", + shortLabel: "Zarejestr.", + }, + in_progress_design: { + color: "#3B82F6", + label: "In Progress (Design)", + shortLabel: "W real. (P)", + }, + in_progress_construction: { + color: "#F59E0B", + label: "In Progress (Construction)", + shortLabel: "W real. (R)", + }, + fulfilled: { + color: "#10B981", + label: "Completed", + shortLabel: "Zakoล„czony", + }, + }; + + // Toggle all status filters + const toggleAllFilters = () => { + const allActive = Object.values(statusFilters).every((value) => value); + const newState = allActive + ? Object.keys(statusFilters).reduce( + (acc, key) => ({ ...acc, [key]: false }), + {} + ) + : Object.keys(statusFilters).reduce( + (acc, key) => ({ ...acc, [key]: true }), + {} + ); + setStatusFilters(newState); + }; + + // Toggle status filter + const toggleStatusFilter = (status) => { + setStatusFilters((prev) => ({ + ...prev, + [status]: !prev[status], + })); + }; + + // Layer control functions + const handleBaseLayerChange = (layerName) => { + setActiveBaseLayer(layerName); + }; + + const toggleOverlay = (layerName) => { + setActiveOverlays((prev) => { + if (prev.includes(layerName)) { + return prev.filter((name) => name !== layerName); + } else { + return [...prev, layerName]; + } + }); + }; + + const toggleLayerPanel = () => { + setShowLayerPanel(!showLayerPanel); + }; + + // Update URL with current map state (debounced to avoid too many updates) + const updateURL = (center, zoom) => { + const params = new URLSearchParams(); + params.set("lat", center[0].toFixed(6)); + params.set("lng", center[1].toFixed(6)); + params.set("zoom", zoom.toString()); + + // Use replace to avoid cluttering browser history + router.replace(`/projects/map?${params.toString()}`, { scroll: false }); + }; + + // Handle map view changes with debouncing + const handleMapViewChange = (center, zoom) => { + setMapCenter(center); + setMapZoom(zoom); + + // Debounce URL updates to avoid too many history entries + clearTimeout(window.mapUpdateTimeout); + window.mapUpdateTimeout = setTimeout(() => { + updateURL(center, zoom); + }, 500); // Wait 500ms after the last move to update URL + }; + + // Hide navigation and ensure full-screen layout + useEffect(() => { + // Check for URL parameters for coordinates and zoom + const lat = searchParams.get("lat"); + const lng = searchParams.get("lng"); + const zoom = searchParams.get("zoom"); + + if (lat && lng) { + const latitude = parseFloat(lat); + const longitude = parseFloat(lng); + if (!isNaN(latitude) && !isNaN(longitude)) { + setMapCenter([latitude, longitude]); + } + } + + if (zoom) { + const zoomLevel = parseInt(zoom); + if (!isNaN(zoomLevel) && zoomLevel >= 1 && zoomLevel <= 20) { + setMapZoom(zoomLevel); + } + } + + // Hide navigation bar for full-screen experience + const nav = document.querySelector("nav"); + if (nav) { + nav.style.display = "none"; + } + + // Prevent scrolling on body + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + + // Cleanup when leaving page + return () => { + if (nav) { + nav.style.display = ""; + } + document.body.style.overflow = ""; + document.documentElement.style.overflow = ""; + + // Clear any pending URL updates + if (window.mapUpdateTimeout) { + clearTimeout(window.mapUpdateTimeout); + } + }; + }, [searchParams]); + + useEffect(() => { + fetch("/api/projects") + .then((res) => res.json()) + .then((data) => { + setProjects(data); + + // Only calculate center based on projects if no URL parameters are provided + const lat = searchParams.get("lat"); + const lng = searchParams.get("lng"); + + if (!lat || !lng) { + // Calculate center based on projects with coordinates + const projectsWithCoords = data.filter((p) => p.coordinates); + if (projectsWithCoords.length > 0) { + const avgLat = + projectsWithCoords.reduce((sum, p) => { + const [lat] = p.coordinates + .split(",") + .map((coord) => parseFloat(coord.trim())); + return sum + lat; + }, 0) / projectsWithCoords.length; + + const avgLng = + projectsWithCoords.reduce((sum, p) => { + const [, lng] = p.coordinates + .split(",") + .map((coord) => parseFloat(coord.trim())); + return sum + lng; + }, 0) / projectsWithCoords.length; + + setMapCenter([avgLat, avgLng]); + } + } + + setLoading(false); + }) + .catch((error) => { + console.error("Error fetching projects:", error); + setLoading(false); + }); + }, [searchParams]); + + // Convert projects to map markers with filtering + const markers = projects + .filter((project) => project.coordinates) + .filter((project) => statusFilters[project.project_status] !== false) + .map((project) => { + const [lat, lng] = project.coordinates + .split(",") + .map((coord) => parseFloat(coord.trim())); + if (isNaN(lat) || isNaN(lng)) { + return null; + } + + const statusInfo = + statusConfig[project.project_status] || statusConfig.registered; + + return { + position: [lat, lng], + color: statusInfo.color, + popup: ( +
+
+

+ {project.project_name} +

+ {project.project_number && ( +
+ {project.project_number} +
+ )} +
+ +
+ {project.address && ( +
+ + + + +
+ + {project.address} + + {project.city && ( + , {project.city} + )} +
+
+ )} +
+ {project.wp && ( +
+ WP:{" "} + {project.wp} +
+ )} + {project.plot && ( +
+ Plot:{" "} + {project.plot} +
+ )} +
+ {project.project_status && ( +
+ Status: + + {statusInfo.shortLabel} + +
+ )} +
+ +
+ + + +
+
+ ), + }; + }) + .filter((marker) => marker !== null); + + if (loading) { + return ( +
+
+
+

Loading projects map...

+

+ Preparing your full-screen map experience +

+
+
+ ); + } + return ( +
+ {/* Floating Header - Left Side */} +
+ {/* Title Box */} +
+
+

+ Projects Map +

+
+ {markers.length} of {projects.length} projects with coordinates +
+
{" "} +
+
+ {/* Zoom Controls - Below Title */} +
+
+ + {" "} +
+
{" "} + {/* Tool Panel - Below Zoom Controls */} +
+ {" "} +
+ {" "} + {/* Move Tool */} + + {/* Select Tool */} + + {/* Measure Tool */} + + {/* Draw Tool */} + + {/* Pin/Marker Tool */} + + {/* Area Tool */} + +
+
+ {/* Layer Control Panel - Right Side */} +
+ {/* Action Buttons */} +
+ + + + + + +
+ + {/* Layer Control Panel */} +
+ {/* Layer Control Header */} +
+ +
{" "} + {/* Layer Control Content */} +
+
+ {/* Base Layers Section */} +
+

+ + + + Base Maps +

+
+ {mapLayers.base.map((layer, index) => ( + + ))} +
+
+ + {/* Overlay Layers Section */} + {mapLayers.overlays && mapLayers.overlays.length > 0 && ( +
+

+ + + + Overlay Layers +

{" "} +
+ {mapLayers.overlays.map((layer, index) => ( + + ))} +
+
+ )} +
+
{" "} +
+
+ {/* Status Filter Panel - Bottom Left */} +
+
+
+ + Filters: + + {/* Toggle All Button */} + + {/* Individual Status Filters */} + {Object.entries(statusConfig).map(([status, config]) => { + const isActive = statusFilters[status]; + const projectCount = projects.filter( + (p) => p.project_status === status && p.coordinates + ).length; + + return ( + + ); + })}{" "} +
+
+
{" "} + {/* Status Panel - Bottom Left */} + {markers.length > 0 && ( +
+
+ + Filters: + + + {/* Toggle All Button */} + + + {/* Individual Status Filters */} + {Object.entries(statusConfig).map(([status, config]) => { + const isActive = statusFilters[status]; + const projectCount = projects.filter( + (p) => p.project_status === status && p.coordinates + ).length; + + return ( + + ); + })} +
+
+ )}{" "} + {/* Full Screen Map */} + {markers.length === 0 ? ( +
+
+
+ + + +
+

+ No projects with coordinates +

+

+ Projects need coordinates to appear on the map. Add coordinates + when creating or editing projects. +

+
+ + + + + + +
+
+
+ ) : ( +
+ +
+ )}{" "} +
+ ); +} diff --git a/src/app/projects/map/page.js b/src/app/projects/map/page.js index bbd0916..23026dd 100644 --- a/src/app/projects/map/page.js +++ b/src/app/projects/map/page.js @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, Suspense } from "react"; import Link from "next/link"; import dynamic from "next/dynamic"; import { useSearchParams, useRouter } from "next/navigation"; @@ -17,7 +17,7 @@ const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), { ), }); -export default function ProjectsMapPage() { +function ProjectsMapPageContent() { const searchParams = useSearchParams(); const router = useRouter(); const [projects, setProjects] = useState([]); @@ -541,7 +541,7 @@ export default function ProjectsMapPage() { {/* Layer Control Panel - Right Side */}
{/* Action Buttons */} -
+
+ }> + + + ); +} diff --git a/src/app/projects/page.js b/src/app/projects/page.js index 7555ae3..9f9c9aa 100644 --- a/src/app/projects/page.js +++ b/src/app/projects/page.js @@ -195,7 +195,13 @@ export default function ProjectListPage() { Status - {" "} + + + Created By + + + Assigned To + Actions @@ -275,6 +281,18 @@ export default function ProjectListPage() { ? "Zakoล„czony" : "-"} + + {project.created_by_name || "Unknown"} + + + {project.assigned_to_name || "Unassigned"} + + +
+
+ + {/* Statistics */} + {stats && ( +
+
+

Total Events

+

{stats.total}

+
+
+

Top Action

+

+ {stats.actionBreakdown[0]?.action || "N/A"} +

+

+ {stats.actionBreakdown[0]?.count || 0} +

+
+
+

Active Users

+

+ {stats.userBreakdown.length} +

+
+
+

Resource Types

+

+ {stats.resourceBreakdown.length} +

+
+
+ )} + + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Audit Logs Table */} +
+
+ + + + + + + + + + + + + {logs.map((log) => ( + + + + + + + + + ))} + +
+ Timestamp + + User + + Action + + Resource + + IP Address + + Details +
+ {formatTimestamp(log.timestamp)} + +
+
+ {log.user_name || "Anonymous"} +
+
{log.user_email}
+
+
+ + {log.action.replace(/_/g, " ").toUpperCase()} + + +
+
+ {log.resource_type || "N/A"} +
+
+ ID: {log.resource_id || "N/A"} +
+
+
+ {log.ip_address || "Unknown"} + + {log.details && ( +
+ + View Details + +
+													{JSON.stringify(log.details, null, 2)}
+												
+
+ )} +
+
+ + {logs.length === 0 && !loading && ( +
+ No audit logs found matching your criteria. +
+ )} + + {logs.length > 0 && ( +
+
+
+ Showing {filters.offset + 1} to {filters.offset + logs.length}{" "} + results +
+ +
+
+ )} +
+
+ ); +} diff --git a/src/components/ProjectForm.js b/src/components/ProjectForm.js index 4034b5e..da43eab 100644 --- a/src/components/ProjectForm.js +++ b/src/components/ProjectForm.js @@ -22,22 +22,59 @@ export default function ProjectForm({ initialData = null }) { contact: "", notes: "", coordinates: "", - project_type: initialData?.project_type || "design", - // project_status is not included in the form for creation or editing - ...initialData, + project_type: "design", + assigned_to: "", }); const [contracts, setContracts] = useState([]); + const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const router = useRouter(); const isEdit = !!initialData; useEffect(() => { + // Fetch contracts fetch("/api/contracts") .then((res) => res.json()) .then(setContracts); + + // Fetch users for assignment + fetch("/api/projects/users") + .then((res) => res.json()) + .then(setUsers); }, []); + // Update form state when initialData changes (for edit mode) + useEffect(() => { + if (initialData) { + setForm({ + contract_id: "", + project_name: "", + address: "", + plot: "", + district: "", + unit: "", + city: "", + investment_number: "", + finish_date: "", + wp: "", + contact: "", + notes: "", + coordinates: "", + project_type: "design", + assigned_to: "", + ...initialData, + // Ensure these defaults are preserved if not in initialData + project_type: initialData.project_type || "design", + assigned_to: initialData.assigned_to || "", + // Format finish_date for input if it exists + finish_date: initialData.finish_date + ? formatDateForInput(initialData.finish_date) + : "", + }); + } + }, [initialData]); + function handleChange(e) { setForm({ ...form, [e.target.name]: e.target.value }); } @@ -83,7 +120,7 @@ export default function ProjectForm({ initialData = null }) {
{/* Contract and Project Type Section */} -
+
+ +
+ + +
{/* Basic Information Section */} diff --git a/src/components/ProjectTaskForm.js b/src/components/ProjectTaskForm.js index 434ea2a..a43ba95 100644 --- a/src/components/ProjectTaskForm.js +++ b/src/components/ProjectTaskForm.js @@ -6,12 +6,14 @@ import Badge from "./ui/Badge"; export default function ProjectTaskForm({ projectId, onTaskAdded }) { const [taskTemplates, setTaskTemplates] = useState([]); + const [users, setUsers] = useState([]); const [taskType, setTaskType] = useState("template"); // "template" or "custom" const [selectedTemplate, setSelectedTemplate] = useState(""); const [customTaskName, setCustomTaskName] = useState(""); const [customMaxWaitDays, setCustomMaxWaitDays] = useState(""); const [customDescription, setCustomDescription] = useState(""); const [priority, setPriority] = useState("normal"); + const [assignedTo, setAssignedTo] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { @@ -19,6 +21,11 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) { fetch("/api/tasks/templates") .then((res) => res.json()) .then(setTaskTemplates); + + // Fetch users for assignment + fetch("/api/project-tasks/users") + .then((res) => res.json()) + .then(setUsers); }, []); async function handleSubmit(e) { @@ -34,6 +41,7 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) { const requestData = { project_id: parseInt(projectId), priority, + assigned_to: assignedTo || null, }; if (taskType === "template") { @@ -56,6 +64,7 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) { setCustomMaxWaitDays(""); setCustomDescription(""); setPriority("normal"); + setAssignedTo(""); if (onTaskAdded) onTaskAdded(); } else { alert("Failed to add task to project."); @@ -158,6 +167,24 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
)} +
+ + +
+
diff --git a/src/lib/auditLog.js b/src/lib/auditLog.js new file mode 100644 index 0000000..5db20ed --- /dev/null +++ b/src/lib/auditLog.js @@ -0,0 +1,424 @@ +/** + * Audit log actions - standardized action types + */ +export const AUDIT_ACTIONS = { + // Authentication + LOGIN: "login", + LOGOUT: "logout", + LOGIN_FAILED: "login_failed", + + // Projects + PROJECT_CREATE: "project_create", + PROJECT_UPDATE: "project_update", + PROJECT_DELETE: "project_delete", + PROJECT_VIEW: "project_view", + + // Tasks + TASK_CREATE: "task_create", + TASK_UPDATE: "task_update", + TASK_DELETE: "task_delete", + TASK_STATUS_CHANGE: "task_status_change", + + // Project Tasks + PROJECT_TASK_CREATE: "project_task_create", + PROJECT_TASK_UPDATE: "project_task_update", + PROJECT_TASK_DELETE: "project_task_delete", + PROJECT_TASK_STATUS_CHANGE: "project_task_status_change", + + // Contracts + CONTRACT_CREATE: "contract_create", + CONTRACT_UPDATE: "contract_update", + CONTRACT_DELETE: "contract_delete", + + // Notes + NOTE_CREATE: "note_create", + NOTE_UPDATE: "note_update", + NOTE_DELETE: "note_delete", + + // Admin actions + USER_CREATE: "user_create", + USER_UPDATE: "user_update", + USER_DELETE: "user_delete", + USER_ROLE_CHANGE: "user_role_change", + + // System actions + DATA_EXPORT: "data_export", + BULK_OPERATION: "bulk_operation", +}; + +/** + * Resource types for audit logging + */ +export const RESOURCE_TYPES = { + PROJECT: "project", + TASK: "task", + PROJECT_TASK: "project_task", + CONTRACT: "contract", + NOTE: "note", + USER: "user", + SESSION: "session", + SYSTEM: "system", +}; + +/** + * Log an audit event + * @param {Object} params - Audit log parameters + * @param {string} params.action - Action performed (use AUDIT_ACTIONS constants) + * @param {string} [params.userId] - ID of user performing the action + * @param {string} [params.resourceType] - Type of resource affected (use RESOURCE_TYPES constants) + * @param {string} [params.resourceId] - ID of the affected resource + * @param {string} [params.ipAddress] - IP address of the user + * @param {string} [params.userAgent] - User agent string + * @param {Object} [params.details] - Additional details about the action + * @param {string} [params.timestamp] - Custom timestamp (defaults to current time) + */ +export async function logAuditEvent({ + action, + userId = null, + resourceType = null, + resourceId = null, + ipAddress = null, + userAgent = null, + details = null, + timestamp = null, +}) { + try { + // Check if we're in Edge Runtime - if so, skip database operations + if ( + typeof EdgeRuntime !== "undefined" || + process.env.NEXT_RUNTIME === "edge" + ) { + console.log( + `[Audit Log - Edge Runtime] ${action} by user ${ + userId || "anonymous" + } on ${resourceType}:${resourceId}` + ); + return; + } + + // Dynamic import to avoid Edge Runtime issues + const { default: db } = await import("./db.js"); + + const auditTimestamp = timestamp || new Date().toISOString(); + const detailsJson = details ? JSON.stringify(details) : null; + + const stmt = db.prepare(` + INSERT INTO audit_logs ( + user_id, action, resource_type, resource_id, + ip_address, user_agent, timestamp, details + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + + stmt.run( + userId, + action, + resourceType, + resourceId, + ipAddress, + userAgent, + auditTimestamp, + detailsJson + ); + + console.log( + `Audit log: ${action} by user ${ + userId || "anonymous" + } on ${resourceType}:${resourceId}` + ); + } catch (error) { + console.error("Failed to log audit event:", error); + // Don't throw error to avoid breaking the main application flow + } +} + +/** + * Get audit logs with filtering and pagination + * @param {Object} options - Query options + * @param {string} [options.userId] - Filter by user ID + * @param {string} [options.action] - Filter by action + * @param {string} [options.resourceType] - Filter by resource type + * @param {string} [options.resourceId] - Filter by resource ID + * @param {string} [options.startDate] - Filter from this date (ISO string) + * @param {string} [options.endDate] - Filter until this date (ISO string) + * @param {number} [options.limit] - Maximum number of records to return + * @param {number} [options.offset] - Number of records to skip + * @param {string} [options.orderBy] - Order by field (default: timestamp) + * @param {string} [options.orderDirection] - Order direction (ASC/DESC, default: DESC) + * @returns {Array} Array of audit log entries + */ +export async function getAuditLogs({ + userId = null, + action = null, + resourceType = null, + resourceId = null, + startDate = null, + endDate = null, + limit = 100, + offset = 0, + orderBy = "timestamp", + orderDirection = "DESC", +} = {}) { + try { + // Check if we're in Edge Runtime - if so, return empty array + if ( + typeof EdgeRuntime !== "undefined" || + process.env.NEXT_RUNTIME === "edge" + ) { + console.log( + "[Audit Log - Edge Runtime] Cannot query audit logs in Edge Runtime" + ); + return []; + } + + // Dynamic import to avoid Edge Runtime issues + const { default: db } = await import("./db.js"); + + let query = ` + SELECT + al.*, + u.name as user_name, + u.email as user_email + FROM audit_logs al + LEFT JOIN users u ON al.user_id = u.id + WHERE 1=1 + `; + + const params = []; + + if (userId) { + query += " AND al.user_id = ?"; + params.push(userId); + } + + if (action) { + query += " AND al.action = ?"; + params.push(action); + } + + if (resourceType) { + query += " AND al.resource_type = ?"; + params.push(resourceType); + } + + if (resourceId) { + query += " AND al.resource_id = ?"; + params.push(resourceId); + } + + if (startDate) { + query += " AND al.timestamp >= ?"; + params.push(startDate); + } + + if (endDate) { + query += " AND al.timestamp <= ?"; + params.push(endDate); + } + + // Validate order direction + const validOrderDirection = ["ASC", "DESC"].includes( + orderDirection.toUpperCase() + ) + ? orderDirection.toUpperCase() + : "DESC"; + + // Validate order by field + const validOrderFields = [ + "timestamp", + "action", + "user_id", + "resource_type", + "resource_id", + ]; + const validOrderBy = validOrderFields.includes(orderBy) + ? orderBy + : "timestamp"; + + query += ` ORDER BY al.${validOrderBy} ${validOrderDirection}`; + + if (limit) { + query += " LIMIT ?"; + params.push(limit); + } + + if (offset) { + query += " OFFSET ?"; + params.push(offset); + } + + const stmt = db.prepare(query); + const results = stmt.all(...params); + + // Parse details JSON for each result + return results.map((log) => ({ + ...log, + details: log.details ? JSON.parse(log.details) : null, + })); + } catch (error) { + console.error("Failed to get audit logs:", error); + return []; + } +} + +/** + * Get audit log statistics + * @param {Object} options - Query options + * @param {string} [options.startDate] - Filter from this date (ISO string) + * @param {string} [options.endDate] - Filter until this date (ISO string) + * @returns {Object} Statistics object + */ +export async function getAuditLogStats({ + startDate = null, + endDate = null, +} = {}) { + try { + // Check if we're in Edge Runtime - if so, return empty stats + if ( + typeof EdgeRuntime !== "undefined" || + process.env.NEXT_RUNTIME === "edge" + ) { + console.log( + "[Audit Log - Edge Runtime] Cannot query audit log stats in Edge Runtime" + ); + return { + total: 0, + actionBreakdown: [], + userBreakdown: [], + resourceBreakdown: [], + }; + } + + // Dynamic import to avoid Edge Runtime issues + const { default: db } = await import("./db.js"); + + let baseQuery = "FROM audit_logs WHERE 1=1"; + const params = []; + + if (startDate) { + baseQuery += " AND timestamp >= ?"; + params.push(startDate); + } + + if (endDate) { + baseQuery += " AND timestamp <= ?"; + params.push(endDate); + } + + // Total count + const totalStmt = db.prepare(`SELECT COUNT(*) as total ${baseQuery}`); + const totalResult = totalStmt.get(...params); + + // Actions breakdown + const actionsStmt = db.prepare(` + SELECT action, COUNT(*) as count + ${baseQuery} + GROUP BY action + ORDER BY count DESC + `); + const actionsResult = actionsStmt.all(...params); + + // Users breakdown + const usersStmt = db.prepare(` + SELECT + al.user_id, + u.name as user_name, + u.email as user_email, + COUNT(*) as count + ${baseQuery} + LEFT JOIN users u ON al.user_id = u.id + GROUP BY al.user_id, u.name, u.email + ORDER BY count DESC + LIMIT 10 + `); + const usersResult = usersStmt.all(...params); + + // Resource types breakdown + const resourcesStmt = db.prepare(` + SELECT resource_type, COUNT(*) as count + ${baseQuery} + WHERE resource_type IS NOT NULL + GROUP BY resource_type + ORDER BY count DESC + `); + const resourcesResult = resourcesStmt.all(...params); + + return { + total: totalResult.total, + actionBreakdown: actionsResult, + userBreakdown: usersResult, + resourceBreakdown: resourcesResult, + }; + } catch (error) { + console.error("Failed to get audit log statistics:", error); + return { + total: 0, + actionBreakdown: [], + userBreakdown: [], + resourceBreakdown: [], + }; + } +} + +/** + * Helper function to extract client information from request + * @param {Request} req - The request object + * @returns {Object} Object containing IP address and user agent + */ +export function getClientInfo(req) { + const ipAddress = + req.headers.get("x-forwarded-for") || + req.headers.get("x-real-ip") || + req.headers.get("cf-connecting-ip") || + req.ip || + "unknown"; + + const userAgent = req.headers.get("user-agent") || "unknown"; + + return { ipAddress, userAgent }; +} + +/** + * Middleware helper to log API actions + * @param {Request} req - The request object + * @param {string} action - The action being performed + * @param {string} resourceType - The type of resource + * @param {string} resourceId - The ID of the resource + * @param {Object} session - The user session + * @param {Object} additionalDetails - Additional details to log + */ +export async function logApiAction( + req, + action, + resourceType, + resourceId, + session, + additionalDetails = {} +) { + const { ipAddress, userAgent } = getClientInfo(req); + + await logAuditEvent({ + action, + userId: session?.user?.id || null, + resourceType, + resourceId, + ipAddress, + userAgent, + details: { + method: req.method, + url: req.url, + ...additionalDetails, + }, + }); +} + +const auditLog = { + logAuditEvent, + getAuditLogs, + getAuditLogStats, + getClientInfo, + logApiAction, + AUDIT_ACTIONS, + RESOURCE_TYPES, +}; + +export default auditLog; diff --git a/src/lib/auditLogEdge.js b/src/lib/auditLogEdge.js new file mode 100644 index 0000000..938f552 --- /dev/null +++ b/src/lib/auditLogEdge.js @@ -0,0 +1,129 @@ +/** + * Edge-compatible audit logging utility + * This version avoids direct database imports and can be used in Edge Runtime + */ + +import { AUDIT_ACTIONS, RESOURCE_TYPES } from "./auditLog.js"; + +/** + * Log an audit event in Edge Runtime compatible way + * @param {Object} params - Audit log parameters + */ +export async function logAuditEventAsync({ + action, + userId = null, + resourceType = null, + resourceId = null, + ipAddress = null, + userAgent = null, + details = null, + timestamp = null, +}) { + try { + // In Edge Runtime or when database is not available, log to console + if ( + typeof EdgeRuntime !== "undefined" || + process.env.NEXT_RUNTIME === "edge" + ) { + console.log( + `[Audit Log - Edge] ${action} by user ${ + userId || "anonymous" + } on ${resourceType}:${resourceId}`, + { + details, + ipAddress, + userAgent, + timestamp: timestamp || new Date().toISOString(), + } + ); + return; + } + + // Try to make an API call to log the event + try { + const response = await fetch("/api/audit-logs/log", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + action, + userId, + resourceType, + resourceId, + ipAddress, + userAgent, + details, + timestamp, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + } catch (fetchError) { + // Fallback to console logging if API call fails + console.log( + `[Audit Log - Fallback] ${action} by user ${ + userId || "anonymous" + } on ${resourceType}:${resourceId}`, + { + details, + ipAddress, + userAgent, + timestamp: timestamp || new Date().toISOString(), + error: fetchError.message, + } + ); + } + } catch (error) { + console.error("Failed to log audit event:", error); + } +} + +/** + * Helper function to extract client information from request (Edge compatible) + * @param {Request} req - The request object + * @returns {Object} Object containing IP address and user agent + */ +export function getClientInfoEdgeCompatible(req) { + const ipAddress = + req.headers.get("x-forwarded-for") || + req.headers.get("x-real-ip") || + req.headers.get("cf-connecting-ip") || + "unknown"; + + const userAgent = req.headers.get("user-agent") || "unknown"; + + return { ipAddress, userAgent }; +} + +/** + * Middleware helper to log API actions (Edge compatible) + */ +export async function logApiActionAsync( + req, + action, + resourceType, + resourceId, + session, + additionalDetails = {} +) { + const { ipAddress, userAgent } = getClientInfoEdgeCompatible(req); + + await logAuditEventAsync({ + action, + userId: session?.user?.id || null, + resourceType, + resourceId, + ipAddress, + userAgent, + details: { + method: req.method, + url: req.url, + ...additionalDetails, + }, + }); +} + +export { AUDIT_ACTIONS, RESOURCE_TYPES }; diff --git a/src/lib/auditLogSafe.js b/src/lib/auditLogSafe.js new file mode 100644 index 0000000..84234e9 --- /dev/null +++ b/src/lib/auditLogSafe.js @@ -0,0 +1,159 @@ +/** + * Safe audit logging that doesn't cause Edge Runtime issues + * This module can be safely imported anywhere without causing database issues + */ + +// Constants that can be safely exported +export const AUDIT_ACTIONS = { + // Authentication + LOGIN: "login", + LOGOUT: "logout", + LOGIN_FAILED: "login_failed", + + // Projects + PROJECT_CREATE: "project_create", + PROJECT_UPDATE: "project_update", + PROJECT_DELETE: "project_delete", + PROJECT_VIEW: "project_view", + + // Tasks + TASK_CREATE: "task_create", + TASK_UPDATE: "task_update", + TASK_DELETE: "task_delete", + TASK_STATUS_CHANGE: "task_status_change", + + // Project Tasks + PROJECT_TASK_CREATE: "project_task_create", + PROJECT_TASK_UPDATE: "project_task_update", + PROJECT_TASK_DELETE: "project_task_delete", + PROJECT_TASK_STATUS_CHANGE: "project_task_status_change", + + // Contracts + CONTRACT_CREATE: "contract_create", + CONTRACT_UPDATE: "contract_update", + CONTRACT_DELETE: "contract_delete", + + // Notes + NOTE_CREATE: "note_create", + NOTE_UPDATE: "note_update", + NOTE_DELETE: "note_delete", + + // Admin actions + USER_CREATE: "user_create", + USER_UPDATE: "user_update", + USER_DELETE: "user_delete", + USER_ROLE_CHANGE: "user_role_change", + + // System actions + DATA_EXPORT: "data_export", + BULK_OPERATION: "bulk_operation", +}; + +export const RESOURCE_TYPES = { + PROJECT: "project", + TASK: "task", + PROJECT_TASK: "project_task", + CONTRACT: "contract", + NOTE: "note", + USER: "user", + SESSION: "session", + SYSTEM: "system", +}; + +/** + * Safe audit logging function that works in any runtime + */ +export async function logAuditEventSafe({ + action, + userId = null, + resourceType = null, + resourceId = null, + ipAddress = null, + userAgent = null, + details = null, + timestamp = null, +}) { + try { + // Always log to console first + console.log( + `[Audit] ${action} by user ${ + userId || "anonymous" + } on ${resourceType}:${resourceId}` + ); + + // Check if we're in Edge Runtime + if ( + typeof EdgeRuntime !== "undefined" || + process.env.NEXT_RUNTIME === "edge" + ) { + console.log("[Audit] Edge Runtime detected - console logging only"); + return; + } + + // Try to get the database-enabled audit function + try { + const auditModule = await import("./auditLog.js"); + await auditModule.logAuditEvent({ + action, + userId, + resourceType, + resourceId, + ipAddress, + userAgent, + details, + timestamp, + }); + } catch (dbError) { + console.log( + "[Audit] Database logging failed, using console fallback:", + dbError.message + ); + } + } catch (error) { + console.error("[Audit] Failed to log audit event:", error); + } +} + +/** + * Helper function to extract client information from request + */ +export function getClientInfo(req) { + const ipAddress = + req.headers?.get?.("x-forwarded-for") || + req.headers?.get?.("x-real-ip") || + req.headers?.get?.("cf-connecting-ip") || + req.ip || + "unknown"; + + const userAgent = req.headers?.get?.("user-agent") || "unknown"; + + return { ipAddress, userAgent }; +} + +/** + * Safe API action logging + */ +export async function logApiActionSafe( + req, + action, + resourceType, + resourceId, + session, + additionalDetails = {} +) { + const { ipAddress, userAgent } = getClientInfo(req); + + await logAuditEventSafe({ + action, + userId: session?.user?.id || null, + resourceType, + resourceId, + ipAddress, + userAgent, + details: { + method: req.method, + url: req.url, + ...additionalDetails, + }, + }); +} diff --git a/src/lib/auth.js b/src/lib/auth.js new file mode 100644 index 0000000..d906e92 --- /dev/null +++ b/src/lib/auth.js @@ -0,0 +1,157 @@ +import NextAuth from "next-auth"; +import Credentials from "next-auth/providers/credentials"; +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({ + providers: [ + Credentials({ + name: "credentials", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + try { + // Import database here to avoid edge runtime issues + const { default: db } = await import("./db.js"); + + // 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); + + // Log failed login attempt (only in Node.js runtime) + try { + const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = + await import("./auditLogSafe.js"); + await logAuditEventSafe({ + action: AUDIT_ACTIONS.LOGIN_FAILED, + userId: user.id, + resourceType: RESOURCE_TYPES.SESSION, + details: { + email: validatedFields.email, + reason: "invalid_password", + failed_attempts: user.failed_login_attempts + 1, + }, + }); + } catch (auditError) { + console.error("Failed to log audit event:", auditError); + } + + 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 (only in Node.js runtime) + try { + const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = + await import("./auditLogSafe.js"); + await logAuditEventSafe({ + action: AUDIT_ACTIONS.LOGIN, + userId: user.id, + resourceType: RESOURCE_TYPES.SESSION, + details: { + email: user.email, + role: user.role, + }, + }); + } catch (auditError) { + console.error("Failed to log audit event:", auditError); + } + + return { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + }; + } catch (error) { + console.error("Login error:", error); + return null; + } + }, + }), + ], + session: { + strategy: "jwt", + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + callbacks: { + async jwt({ token, user }) { + if (user) { + token.role = user.role; + token.userId = user.id; + } + return token; + }, + async session({ session, token }) { + if (token) { + session.user.id = token.userId; + session.user.role = token.role; + } + return session; + }, + }, + pages: { + signIn: "/auth/signin", + signOut: "/auth/signout", + error: "/auth/error", + }, + debug: process.env.NODE_ENV === "development", +}); diff --git a/src/lib/init-db.js b/src/lib/init-db.js index a72788b..c8c87e0 100644 --- a/src/lib/init-db.js +++ b/src/lib/init-db.js @@ -162,4 +162,156 @@ export default function initializeDatabase() { } catch (e) { // Column already exists, ignore error } + + // Migration: Add user tracking columns to projects table + try { + db.exec(` + ALTER TABLE projects ADD COLUMN created_by TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + try { + db.exec(` + ALTER TABLE projects ADD COLUMN assigned_to TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + try { + db.exec(` + ALTER TABLE projects ADD COLUMN created_at TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + try { + db.exec(` + ALTER TABLE projects ADD COLUMN updated_at TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + // Migration: Add user tracking columns to project_tasks table + try { + db.exec(` + ALTER TABLE project_tasks ADD COLUMN created_by TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + try { + db.exec(` + ALTER TABLE project_tasks ADD COLUMN assigned_to TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + try { + db.exec(` + ALTER TABLE project_tasks ADD COLUMN created_at TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + try { + db.exec(` + ALTER TABLE project_tasks ADD COLUMN updated_at TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + // Create indexes for project_tasks user tracking + try { + db.exec(` + CREATE INDEX IF NOT EXISTS idx_project_tasks_created_by ON project_tasks(created_by); + CREATE INDEX IF NOT EXISTS idx_project_tasks_assigned_to ON project_tasks(assigned_to); + `); + } catch (e) { + // Index already exists, ignore error + } + + // Migration: Add user tracking columns to notes table + try { + db.exec(` + ALTER TABLE notes ADD COLUMN created_by TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } + + try { + db.exec(` + ALTER TABLE notes ADD COLUMN is_system INTEGER DEFAULT 0; + `); + } catch (e) { + // Column already exists, ignore error + } + + // Create indexes for notes user tracking + try { + db.exec(` + CREATE INDEX IF NOT EXISTS idx_notes_created_by ON notes(created_by); + CREATE INDEX IF NOT EXISTS idx_notes_project_id ON notes(project_id); + CREATE INDEX IF NOT EXISTS idx_notes_task_id ON notes(task_id); + `); + } catch (e) { + // Index already exists, ignore error + } + + // Authorization tables + db.exec(` + -- Users table + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT CHECK(role IN ('admin', '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 + ); + + -- NextAuth.js sessions table (simplified for custom implementation) + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + session_token TEXT UNIQUE NOT NULL, + user_id TEXT NOT NULL, + expires TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + + -- Audit log table for security tracking + CREATE TABLE IF NOT EXISTS audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT, + action TEXT NOT NULL, + resource_type TEXT, + resource_id TEXT, + ip_address TEXT, + user_agent TEXT, + timestamp TEXT DEFAULT CURRENT_TIMESTAMP, + details TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) + ); + + -- Create indexes for performance + CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(session_token); + CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); + CREATE INDEX IF NOT EXISTS idx_audit_user_timestamp ON audit_logs(user_id, timestamp); + `); } diff --git a/src/lib/middleware/auditLog.js b/src/lib/middleware/auditLog.js new file mode 100644 index 0000000..a707830 --- /dev/null +++ b/src/lib/middleware/auditLog.js @@ -0,0 +1,235 @@ +import { logApiAction, AUDIT_ACTIONS, RESOURCE_TYPES } from "@/lib/auditLog.js"; + +/** + * Higher-order function to add audit logging to API routes + * @param {Function} handler - The original API route handler + * @param {Object} auditConfig - Audit logging configuration + * @param {string} auditConfig.action - The audit action to log + * @param {string} auditConfig.resourceType - The resource type being accessed + * @param {Function} [auditConfig.getResourceId] - Function to extract resource ID from request/params + * @param {Function} [auditConfig.getAdditionalDetails] - Function to get additional details to log + * @returns {Function} Wrapped handler with audit logging + */ +export function withAuditLog(handler, auditConfig) { + return async (request, context) => { + try { + // Execute the original handler first + const response = await handler(request, context); + + // Extract resource ID if function provided + let resourceId = null; + if (auditConfig.getResourceId) { + resourceId = auditConfig.getResourceId(request, context, response); + } else if (context?.params?.id) { + resourceId = context.params.id; + } + + // Get additional details if function provided + let additionalDetails = {}; + if (auditConfig.getAdditionalDetails) { + additionalDetails = auditConfig.getAdditionalDetails( + request, + context, + response + ); + } + + // Log the action + logApiAction( + request, + auditConfig.action, + auditConfig.resourceType, + resourceId, + request.session, + additionalDetails + ); + + return response; + } catch (error) { + // Log failed actions + const resourceId = auditConfig.getResourceId + ? auditConfig.getResourceId(request, context, null) + : context?.params?.id || null; + + logApiAction( + request, + `${auditConfig.action}_failed`, + auditConfig.resourceType, + resourceId, + request.session, + { + error: error.message, + ...(auditConfig.getAdditionalDetails + ? auditConfig.getAdditionalDetails(request, context, null) + : {}), + } + ); + + // Re-throw the error + throw error; + } + }; +} + +/** + * Predefined audit configurations for common actions + */ +export const AUDIT_CONFIGS = { + // Project actions + PROJECT_VIEW: { + action: AUDIT_ACTIONS.PROJECT_VIEW, + resourceType: RESOURCE_TYPES.PROJECT, + }, + PROJECT_CREATE: { + action: AUDIT_ACTIONS.PROJECT_CREATE, + resourceType: RESOURCE_TYPES.PROJECT, + getResourceId: (req, ctx, res) => res?.json?.projectId?.toString(), + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { projectData: data }; + }, + }, + PROJECT_UPDATE: { + action: AUDIT_ACTIONS.PROJECT_UPDATE, + resourceType: RESOURCE_TYPES.PROJECT, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { updatedData: data }; + }, + }, + PROJECT_DELETE: { + action: AUDIT_ACTIONS.PROJECT_DELETE, + resourceType: RESOURCE_TYPES.PROJECT, + }, + + // Task actions + TASK_VIEW: { + action: AUDIT_ACTIONS.TASK_VIEW, + resourceType: RESOURCE_TYPES.TASK, + }, + TASK_CREATE: { + action: AUDIT_ACTIONS.TASK_CREATE, + resourceType: RESOURCE_TYPES.TASK, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { taskData: data }; + }, + }, + TASK_UPDATE: { + action: AUDIT_ACTIONS.TASK_UPDATE, + resourceType: RESOURCE_TYPES.TASK, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { updatedData: data }; + }, + }, + TASK_DELETE: { + action: AUDIT_ACTIONS.TASK_DELETE, + resourceType: RESOURCE_TYPES.TASK, + }, + + // Project Task actions + PROJECT_TASK_VIEW: { + action: AUDIT_ACTIONS.PROJECT_TASK_VIEW, + resourceType: RESOURCE_TYPES.PROJECT_TASK, + }, + PROJECT_TASK_CREATE: { + action: AUDIT_ACTIONS.PROJECT_TASK_CREATE, + resourceType: RESOURCE_TYPES.PROJECT_TASK, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { taskData: data }; + }, + }, + PROJECT_TASK_UPDATE: { + action: AUDIT_ACTIONS.PROJECT_TASK_UPDATE, + resourceType: RESOURCE_TYPES.PROJECT_TASK, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { updatedData: data }; + }, + }, + PROJECT_TASK_DELETE: { + action: AUDIT_ACTIONS.PROJECT_TASK_DELETE, + resourceType: RESOURCE_TYPES.PROJECT_TASK, + }, + + // Contract actions + CONTRACT_VIEW: { + action: AUDIT_ACTIONS.CONTRACT_VIEW, + resourceType: RESOURCE_TYPES.CONTRACT, + }, + CONTRACT_CREATE: { + action: AUDIT_ACTIONS.CONTRACT_CREATE, + resourceType: RESOURCE_TYPES.CONTRACT, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { contractData: data }; + }, + }, + CONTRACT_UPDATE: { + action: AUDIT_ACTIONS.CONTRACT_UPDATE, + resourceType: RESOURCE_TYPES.CONTRACT, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { updatedData: data }; + }, + }, + CONTRACT_DELETE: { + action: AUDIT_ACTIONS.CONTRACT_DELETE, + resourceType: RESOURCE_TYPES.CONTRACT, + }, + + // Note actions + NOTE_VIEW: { + action: AUDIT_ACTIONS.NOTE_VIEW, + resourceType: RESOURCE_TYPES.NOTE, + }, + NOTE_CREATE: { + action: AUDIT_ACTIONS.NOTE_CREATE, + resourceType: RESOURCE_TYPES.NOTE, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { noteData: data }; + }, + }, + NOTE_UPDATE: { + action: AUDIT_ACTIONS.NOTE_UPDATE, + resourceType: RESOURCE_TYPES.NOTE, + getAdditionalDetails: async (req) => { + const data = await req.json(); + return { updatedData: data }; + }, + }, + NOTE_DELETE: { + action: AUDIT_ACTIONS.NOTE_DELETE, + resourceType: RESOURCE_TYPES.NOTE, + }, +}; + +/** + * Utility function to create audit-logged API handlers + * @param {Object} handlers - Object with HTTP method handlers + * @param {Object} auditConfig - Audit configuration for this route + * @returns {Object} Object with audit-logged handlers + */ +export function createAuditedHandlers(handlers, auditConfig) { + const auditedHandlers = {}; + + Object.entries(handlers).forEach(([method, handler]) => { + // Get method-specific audit config or use default + const config = auditConfig[method] || auditConfig.default || auditConfig; + + auditedHandlers[method] = withAuditLog(handler, config); + }); + + return auditedHandlers; +} + +const auditLogMiddleware = { + withAuditLog, + AUDIT_CONFIGS, + createAuditedHandlers, +}; + +export default auditLogMiddleware; diff --git a/src/lib/middleware/auth.js b/src/lib/middleware/auth.js new file mode 100644 index 0000000..1d01a78 --- /dev/null +++ b/src/lib/middleware/auth.js @@ -0,0 +1,76 @@ +import { auth } from "@/lib/auth" +import { NextResponse } from "next/server" + +// Role hierarchy for permission checking +const ROLE_HIERARCHY = { + 'admin': 4, + 'project_manager': 3, + 'user': 2, + 'read_only': 1 +} + +export function withAuth(handler, options = {}) { + return auth(async (req, context) => { + try { + // Check if user is authenticated + if (!req.auth?.user) { + console.log("No session found for request to:", req.url) + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 } + ) + } + + console.log("Session found for user:", req.auth.user.email) + + // Check role-based permissions (without database access) + if (options.requiredRole && !hasPermission(req.auth.user.role, options.requiredRole)) { + return NextResponse.json( + { error: "Insufficient permissions" }, + { status: 403 } + ) + } + + // Add user info to request + req.user = { + id: req.auth.user.id, + email: req.auth.user.email, + name: req.auth.user.name, + role: req.auth.user.role + } + + // Call the original handler with both req and context + 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 admin-level operations +export function withAdminAuth(handler) { + return withAuth(handler, { requiredRole: 'admin' }) +} + +// Helper for project manager operations +export function withManagerAuth(handler) { + return withAuth(handler, { requiredRole: 'project_manager' }) +} diff --git a/src/lib/queries/notes.js b/src/lib/queries/notes.js index ca896fa..0bd7e67 100644 --- a/src/lib/queries/notes.js +++ b/src/lib/queries/notes.js @@ -2,29 +2,100 @@ import db from "../db.js"; export function getNotesByProjectId(project_id) { return db - .prepare(`SELECT * FROM notes WHERE project_id = ? ORDER BY note_date DESC`) + .prepare( + ` + SELECT n.*, + u.name as created_by_name, + u.email as created_by_email + FROM notes n + LEFT JOIN users u ON n.created_by = u.id + WHERE n.project_id = ? + ORDER BY n.note_date DESC + ` + ) .all(project_id); } -export function addNoteToProject(project_id, note) { - db.prepare(`INSERT INTO notes (project_id, note) VALUES (?, ?)`).run( - project_id, - note - ); +export function addNoteToProject(project_id, note, created_by = null) { + db.prepare( + ` + INSERT INTO notes (project_id, note, created_by, note_date) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + ` + ).run(project_id, note, created_by); } export function getNotesByTaskId(task_id) { return db - .prepare(`SELECT * FROM notes WHERE task_id = ? ORDER BY note_date DESC`) + .prepare( + ` + SELECT n.*, + u.name as created_by_name, + u.email as created_by_email + FROM notes n + LEFT JOIN users u ON n.created_by = u.id + WHERE n.task_id = ? + ORDER BY n.note_date DESC + ` + ) .all(task_id); } -export function addNoteToTask(task_id, note, is_system = false) { +export function addNoteToTask( + task_id, + note, + is_system = false, + created_by = null +) { db.prepare( - `INSERT INTO notes (task_id, note, is_system) VALUES (?, ?, ?)` - ).run(task_id, note, is_system ? 1 : 0); + `INSERT INTO notes (task_id, note, is_system, created_by, note_date) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)` + ).run(task_id, note, is_system ? 1 : 0, created_by); } export function deleteNote(note_id) { db.prepare(`DELETE FROM notes WHERE note_id = ?`).run(note_id); } + +// Get all notes with user information (for admin/reporting purposes) +export function getAllNotesWithUsers() { + return db + .prepare( + ` + SELECT n.*, + u.name as created_by_name, + u.email as created_by_email, + p.project_name, + COALESCE(pt.custom_task_name, t.name) as task_name + FROM notes n + LEFT JOIN users u ON n.created_by = u.id + LEFT JOIN projects p ON n.project_id = p.project_id + LEFT JOIN project_tasks pt ON n.task_id = pt.id + LEFT JOIN tasks t ON pt.task_template_id = t.task_id + ORDER BY n.note_date DESC + ` + ) + .all(); +} + +// Get notes created by a specific user +export function getNotesByCreator(userId) { + return db + .prepare( + ` + SELECT n.*, + u.name as created_by_name, + u.email as created_by_email, + p.project_name, + COALESCE(pt.custom_task_name, t.name) as task_name + FROM notes n + LEFT JOIN users u ON n.created_by = u.id + LEFT JOIN projects p ON n.project_id = p.project_id + LEFT JOIN project_tasks pt ON n.task_id = pt.id + LEFT JOIN tasks t ON pt.task_template_id = t.task_id + WHERE n.created_by = ? + ORDER BY n.note_date DESC + ` + ) + .all(userId); +} diff --git a/src/lib/queries/projects.js b/src/lib/queries/projects.js index 6714e5a..3839dc7 100644 --- a/src/lib/queries/projects.js +++ b/src/lib/queries/projects.js @@ -1,21 +1,48 @@ import db from "../db.js"; export function getAllProjects(contractId = null) { + const baseQuery = ` + SELECT + p.*, + creator.name as created_by_name, + creator.email as created_by_email, + assignee.name as assigned_to_name, + assignee.email as assigned_to_email + FROM projects p + LEFT JOIN users creator ON p.created_by = creator.id + LEFT JOIN users assignee ON p.assigned_to = assignee.id + `; + if (contractId) { return db .prepare( - "SELECT * FROM projects WHERE contract_id = ? ORDER BY finish_date DESC" + baseQuery + " WHERE p.contract_id = ? ORDER BY p.finish_date DESC" ) .all(contractId); } - return db.prepare("SELECT * FROM projects ORDER BY finish_date DESC").all(); + return db.prepare(baseQuery + " ORDER BY p.finish_date DESC").all(); } export function getProjectById(id) { - return db.prepare("SELECT * FROM projects WHERE project_id = ?").get(id); + return db + .prepare( + ` + SELECT + p.*, + creator.name as created_by_name, + creator.email as created_by_email, + assignee.name as assigned_to_name, + assignee.email as assigned_to_email + FROM projects p + LEFT JOIN users creator ON p.created_by = creator.id + LEFT JOIN users assignee ON p.assigned_to = assignee.id + WHERE p.project_id = ? + ` + ) + .get(id); } -export function createProject(data) { +export function createProject(data, userId = null) { // 1. Get the contract number and count existing projects const contractInfo = db .prepare( @@ -37,12 +64,16 @@ export function createProject(data) { // 2. Generate sequential number and project number const sequentialNumber = (contractInfo.project_count || 0) + 1; - const projectNumber = `${sequentialNumber}/${contractInfo.contract_number}`; const stmt = db.prepare(` + const projectNumber = `${sequentialNumber}/${contractInfo.contract_number}`; + + const stmt = db.prepare(` INSERT INTO projects ( contract_id, project_name, project_number, address, plot, district, unit, city, investment_number, finish_date, - wp, contact, notes, project_type, project_status, coordinates - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `);stmt.run( + wp, contact, notes, project_type, project_status, coordinates, created_by, assigned_to, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `); + + const result = stmt.run( data.contract_id, data.project_name, projectNumber, @@ -55,16 +86,23 @@ export function createProject(data) { data.finish_date, data.wp, data.contact, - data.notes, data.project_type || "design", + data.notes, + data.project_type || "design", data.project_status || "registered", - data.coordinates || null + data.coordinates || null, + userId, + data.assigned_to || null ); + + return result; } -export function updateProject(id, data) { const stmt = db.prepare(` +export function updateProject(id, data, userId = null) { + const stmt = db.prepare(` UPDATE projects SET contract_id = ?, project_name = ?, project_number = ?, address = ?, plot = ?, district = ?, unit = ?, city = ?, - investment_number = ?, finish_date = ?, wp = ?, contact = ?, notes = ?, project_type = ?, project_status = ?, coordinates = ? + investment_number = ?, finish_date = ?, wp = ?, contact = ?, notes = ?, project_type = ?, project_status = ?, + coordinates = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP WHERE project_id = ? `); stmt.run( @@ -80,9 +118,11 @@ export function updateProject(id, data) { const stmt = db.prepare(` data.finish_date, data.wp, data.contact, - data.notes, data.project_type || "design", + data.notes, + data.project_type || "design", data.project_status || "registered", data.coordinates || null, + data.assigned_to || null, id ); } @@ -91,6 +131,75 @@ export function deleteProject(id) { db.prepare("DELETE FROM projects WHERE project_id = ?").run(id); } +// Get all users for assignment dropdown +export function getAllUsersForAssignment() { + return db + .prepare( + ` + SELECT id, name, email, role + FROM users + WHERE is_active = 1 + ORDER BY name + ` + ) + .all(); +} + +// Get projects assigned to a specific user +export function getProjectsByAssignedUser(userId) { + return db + .prepare( + ` + SELECT + p.*, + creator.name as created_by_name, + creator.email as created_by_email, + assignee.name as assigned_to_name, + assignee.email as assigned_to_email + FROM projects p + LEFT JOIN users creator ON p.created_by = creator.id + LEFT JOIN users assignee ON p.assigned_to = assignee.id + WHERE p.assigned_to = ? + ORDER BY p.finish_date DESC + ` + ) + .all(userId); +} + +// Get projects created by a specific user +export function getProjectsByCreator(userId) { + return db + .prepare( + ` + SELECT + p.*, + creator.name as created_by_name, + creator.email as created_by_email, + assignee.name as assigned_to_name, + assignee.email as assigned_to_email + FROM projects p + LEFT JOIN users creator ON p.created_by = creator.id + LEFT JOIN users assignee ON p.assigned_to = assignee.id + WHERE p.created_by = ? + ORDER BY p.finish_date DESC + ` + ) + .all(userId); +} + +// Update project assignment +export function updateProjectAssignment(projectId, assignedToUserId) { + return db + .prepare( + ` + UPDATE projects + SET assigned_to = ?, updated_at = CURRENT_TIMESTAMP + WHERE project_id = ? + ` + ) + .run(assignedToUserId, projectId); +} + export function getProjectWithContract(id) { return db .prepare( @@ -113,9 +222,13 @@ export function getNotesForProject(projectId) { return db .prepare( ` - SELECT * FROM notes - WHERE project_id = ? - ORDER BY note_date DESC + SELECT n.*, + u.name as created_by_name, + u.email as created_by_email + FROM notes n + LEFT JOIN users u ON n.created_by = u.id + WHERE n.project_id = ? + ORDER BY n.note_date DESC ` ) .all(projectId); diff --git a/src/lib/queries/tasks.js b/src/lib/queries/tasks.js index 437025a..05f185a 100644 --- a/src/lib/queries/tasks.js +++ b/src/lib/queries/tasks.js @@ -27,10 +27,16 @@ export function getAllProjectTasks() { p.plot, p.city, p.address, - p.finish_date + p.finish_date, + creator.name as created_by_name, + creator.email as created_by_email, + assignee.name as assigned_to_name, + assignee.email as assigned_to_email FROM project_tasks pt LEFT JOIN tasks t ON pt.task_template_id = t.task_id LEFT JOIN projects p ON pt.project_id = p.project_id + LEFT JOIN users creator ON pt.created_by = creator.id + LEFT JOIN users assignee ON pt.assigned_to = assignee.id ORDER BY pt.date_added DESC ` ) @@ -50,9 +56,15 @@ export function getProjectTasks(projectId) { CASE WHEN pt.task_template_id IS NOT NULL THEN 'template' ELSE 'custom' - END as task_type + END as task_type, + creator.name as created_by_name, + creator.email as created_by_email, + assignee.name as assigned_to_name, + assignee.email as assigned_to_email FROM project_tasks pt LEFT JOIN tasks t ON pt.task_template_id = t.task_id + LEFT JOIN users creator ON pt.created_by = creator.id + LEFT JOIN users assignee ON pt.assigned_to = assignee.id WHERE pt.project_id = ? ORDER BY pt.date_added DESC ` @@ -68,14 +80,19 @@ export function createProjectTask(data) { if (data.task_template_id) { // Creating from template - explicitly set custom_max_wait_days to NULL so COALESCE uses template value const stmt = db.prepare(` - INSERT INTO project_tasks (project_id, task_template_id, custom_max_wait_days, status, priority) - VALUES (?, ?, NULL, ?, ?) + INSERT INTO project_tasks ( + project_id, task_template_id, custom_max_wait_days, status, priority, + created_by, assigned_to, created_at, updated_at + ) + VALUES (?, ?, NULL, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `); result = stmt.run( data.project_id, data.task_template_id, data.status || "pending", - data.priority || "normal" + data.priority || "normal", + data.created_by || null, + data.assigned_to || null ); // Get the template name for the log @@ -85,8 +102,11 @@ export function createProjectTask(data) { } else { // Creating custom task const stmt = db.prepare(` - INSERT INTO project_tasks (project_id, custom_task_name, custom_max_wait_days, custom_description, status, priority) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO project_tasks ( + project_id, custom_task_name, custom_max_wait_days, custom_description, + status, priority, created_by, assigned_to, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `); result = stmt.run( data.project_id, @@ -94,7 +114,9 @@ export function createProjectTask(data) { data.custom_max_wait_days || 0, data.custom_description || "", data.status || "pending", - data.priority || "normal" + data.priority || "normal", + data.created_by || null, + data.assigned_to || null ); taskName = data.custom_task_name; @@ -105,14 +127,14 @@ export function createProjectTask(data) { const priority = data.priority || "normal"; const status = data.status || "pending"; const logMessage = `Task "${taskName}" created with priority: ${priority}, status: ${status}`; - addNoteToTask(result.lastInsertRowid, logMessage, true); + addNoteToTask(result.lastInsertRowid, logMessage, true, data.created_by); } return result; } // Update project task status -export function updateProjectTaskStatus(taskId, status) { +export function updateProjectTaskStatus(taskId, status, userId = null) { // First get the current task details for logging const getCurrentTask = db.prepare(` SELECT @@ -136,7 +158,7 @@ export function updateProjectTaskStatus(taskId, status) { // Starting a task - set date_started stmt = db.prepare(` UPDATE project_tasks - SET status = ?, date_started = CURRENT_TIMESTAMP + SET status = ?, date_started = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); result = stmt.run(status, taskId); @@ -144,7 +166,7 @@ export function updateProjectTaskStatus(taskId, status) { // Completing a task - set date_completed stmt = db.prepare(` UPDATE project_tasks - SET status = ?, date_completed = CURRENT_TIMESTAMP + SET status = ?, date_completed = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); result = stmt.run(status, taskId); @@ -152,7 +174,7 @@ export function updateProjectTaskStatus(taskId, status) { // Just updating status without changing timestamps stmt = db.prepare(` UPDATE project_tasks - SET status = ? + SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); result = stmt.run(status, taskId); @@ -162,7 +184,7 @@ export function updateProjectTaskStatus(taskId, status) { if (result.changes > 0 && oldStatus !== status) { const taskName = currentTask.task_name || "Unknown task"; const logMessage = `Status changed from "${oldStatus}" to "${status}"`; - addNoteToTask(taskId, logMessage, true); + addNoteToTask(taskId, logMessage, true, userId); } return result; @@ -173,3 +195,99 @@ export function deleteProjectTask(taskId) { const stmt = db.prepare("DELETE FROM project_tasks WHERE id = ?"); return stmt.run(taskId); } + +// Get project tasks assigned to a specific user +export function getProjectTasksByAssignedUser(userId) { + return db + .prepare( + ` + SELECT + pt.*, + COALESCE(pt.custom_task_name, t.name) as task_name, + COALESCE(pt.custom_max_wait_days, t.max_wait_days) as max_wait_days, + COALESCE(pt.custom_description, t.description) as description, + CASE + WHEN pt.task_template_id IS NOT NULL THEN 'template' + ELSE 'custom' + END as task_type, + p.project_name, + p.wp, + p.plot, + p.city, + p.address, + p.finish_date, + creator.name as created_by_name, + creator.email as created_by_email, + assignee.name as assigned_to_name, + assignee.email as assigned_to_email + FROM project_tasks pt + LEFT JOIN tasks t ON pt.task_template_id = t.task_id + LEFT JOIN projects p ON pt.project_id = p.project_id + LEFT JOIN users creator ON pt.created_by = creator.id + LEFT JOIN users assignee ON pt.assigned_to = assignee.id + WHERE pt.assigned_to = ? + ORDER BY pt.date_added DESC + ` + ) + .all(userId); +} + +// Get project tasks created by a specific user +export function getProjectTasksByCreator(userId) { + return db + .prepare( + ` + SELECT + pt.*, + COALESCE(pt.custom_task_name, t.name) as task_name, + COALESCE(pt.custom_max_wait_days, t.max_wait_days) as max_wait_days, + COALESCE(pt.custom_description, t.description) as description, + CASE + WHEN pt.task_template_id IS NOT NULL THEN 'template' + ELSE 'custom' + END as task_type, + p.project_name, + p.wp, + p.plot, + p.city, + p.address, + p.finish_date, + creator.name as created_by_name, + creator.email as created_by_email, + assignee.name as assigned_to_name, + assignee.email as assigned_to_email + FROM project_tasks pt + LEFT JOIN tasks t ON pt.task_template_id = t.task_id + LEFT JOIN projects p ON pt.project_id = p.project_id + LEFT JOIN users creator ON pt.created_by = creator.id + LEFT JOIN users assignee ON pt.assigned_to = assignee.id + WHERE pt.created_by = ? + ORDER BY pt.date_added DESC + ` + ) + .all(userId); +} + +// Update project task assignment +export function updateProjectTaskAssignment(taskId, assignedToUserId) { + const stmt = db.prepare(` + UPDATE project_tasks + SET assigned_to = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `); + return stmt.run(assignedToUserId, taskId); +} + +// Get active users for task assignment (same as projects) +export function getAllUsersForTaskAssignment() { + return db + .prepare( + ` + SELECT id, name, email, role + FROM users + WHERE is_active = 1 + ORDER BY name ASC + ` + ) + .all(); +} diff --git a/src/lib/userManagement.js b/src/lib/userManagement.js new file mode 100644 index 0000000..8c139c1 --- /dev/null +++ b/src/lib/userManagement.js @@ -0,0 +1,267 @@ +import db from "./db.js" +import bcrypt from "bcryptjs" +import { randomBytes } from "crypto" + +// Create a new user +export async function createUser({ name, email, password, role = 'user', is_active = true }) { + const existingUser = db.prepare("SELECT id FROM users WHERE email = ?").get(email) + if (existingUser) { + throw new Error("User with this email already exists") + } + + const passwordHash = await bcrypt.hash(password, 12) + const userId = randomBytes(16).toString('hex') + + const result = db.prepare(` + INSERT INTO users (id, name, email, password_hash, role, is_active) + VALUES (?, ?, ?, ?, ?, ?) + `).run(userId, name, email, passwordHash, role, is_active ? 1 : 0) + + return db.prepare(` + SELECT id, name, email, role, created_at, updated_at, last_login, + is_active, failed_login_attempts, locked_until + FROM users WHERE id = ? + `).get(userId) +} + +// Get user by ID +export function getUserById(id) { + return db.prepare(` + SELECT id, name, email, password_hash, role, created_at, updated_at, last_login, + is_active, failed_login_attempts, locked_until + FROM users WHERE id = ? + `).get(id) +} + +// Get user by email +export function getUserByEmail(email) { + return db.prepare(` + SELECT id, name, email, role, created_at, last_login, is_active + FROM users WHERE email = ? + `).get(email) +} + +// Get all users (for admin) +export function getAllUsers() { + return db.prepare(` + SELECT id, name, email, password_hash, role, created_at, updated_at, last_login, is_active, + failed_login_attempts, locked_until + FROM users + ORDER BY created_at DESC + `).all() +} + +// Update user role +export function updateUserRole(userId, role) { + const validRoles = ['admin', 'project_manager', 'user', 'read_only'] + if (!validRoles.includes(role)) { + throw new Error("Invalid role") + } + + const result = db.prepare(` + UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `).run(role, userId) + + return result.changes > 0 +} + +// Activate/deactivate user +export function setUserActive(userId, isActive) { + const result = db.prepare(` + UPDATE users SET is_active = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `).run(isActive ? 1 : 0, userId) + + return result.changes > 0 +} + +// Change user password +export async function changeUserPassword(userId, newPassword) { + const passwordHash = await bcrypt.hash(newPassword, 12) + + const result = db.prepare(` + UPDATE users + SET password_hash = ?, updated_at = CURRENT_TIMESTAMP, + failed_login_attempts = 0, locked_until = NULL + WHERE id = ? + `).run(passwordHash, userId) + + return result.changes > 0 +} + +// Clean up expired sessions +export function cleanupExpiredSessions() { + const result = db.prepare(` + DELETE FROM sessions WHERE expires < datetime('now') + `).run() + + return result.changes +} + +// Get user sessions +export function getUserSessions(userId) { + return db.prepare(` + SELECT id, session_token, expires, created_at + FROM sessions + WHERE user_id = ? AND expires > datetime('now') + ORDER BY created_at DESC + `).all(userId) +} + +// Revoke user session +export function revokeSession(sessionToken) { + const result = db.prepare(` + DELETE FROM sessions WHERE session_token = ? + `).run(sessionToken) + + return result.changes > 0 +} + +// Get audit logs for user +export function getUserAuditLogs(userId, limit = 50) { + return db.prepare(` + SELECT action, resource_type, resource_id, ip_address, timestamp, details + FROM audit_logs + WHERE user_id = ? + ORDER BY timestamp DESC + LIMIT ? + `).all(userId, limit) +} + +// Update user (comprehensive update function) +export async function updateUser(userId, updates) { + const user = getUserById(userId); + if (!user) { + return null; + } + + // Check if email is being changed and if it already exists + if (updates.email && updates.email !== user.email) { + const existingUser = db.prepare("SELECT id FROM users WHERE email = ? AND id != ?").get(updates.email, userId); + if (existingUser) { + throw new Error("User with this email already exists"); + } + } + + // Prepare update fields + const updateFields = []; + const updateValues = []; + + if (updates.name !== undefined) { + updateFields.push("name = ?"); + updateValues.push(updates.name); + } + + if (updates.email !== undefined) { + updateFields.push("email = ?"); + updateValues.push(updates.email); + } + + if (updates.role !== undefined) { + const validRoles = ['admin', 'project_manager', 'user', 'read_only']; + if (!validRoles.includes(updates.role)) { + throw new Error("Invalid role"); + } + updateFields.push("role = ?"); + updateValues.push(updates.role); + } + + if (updates.is_active !== undefined) { + updateFields.push("is_active = ?"); + updateValues.push(updates.is_active ? 1 : 0); + } + + if (updates.password !== undefined) { + const passwordHash = await bcrypt.hash(updates.password, 12); + updateFields.push("password_hash = ?"); + updateValues.push(passwordHash); + // Reset failed login attempts when password is changed + updateFields.push("failed_login_attempts = 0"); + updateFields.push("locked_until = NULL"); + } + + if (updateFields.length === 0) { + return getUserById(userId); // Return existing user if no updates + } + + updateFields.push("updated_at = CURRENT_TIMESTAMP"); + updateValues.push(userId); + + const query = ` + UPDATE users + SET ${updateFields.join(", ")} + WHERE id = ? + `; + + const result = db.prepare(query).run(...updateValues); + + if (result.changes > 0) { + return db.prepare(` + SELECT id, name, email, role, created_at, updated_at, last_login, + is_active, failed_login_attempts, locked_until + FROM users WHERE id = ? + `).get(userId); + } + + return null; +} + +// Delete user +export function deleteUser(userId) { + // First, delete related data (sessions, audit logs, etc.) + db.prepare("DELETE FROM sessions WHERE user_id = ?").run(userId); + db.prepare("DELETE FROM audit_logs WHERE user_id = ?").run(userId); + + // Then delete the user + const result = db.prepare("DELETE FROM users WHERE id = ?").run(userId); + + return result.changes > 0; +} + +// Reset user password (admin function) +export async function resetUserPassword(userId, newPassword) { + const passwordHash = await bcrypt.hash(newPassword, 12); + + const result = db.prepare(` + UPDATE users + SET password_hash = ?, + failed_login_attempts = 0, + locked_until = NULL, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `).run(passwordHash, userId); + + return result.changes > 0; +} + +// Unlock user account +export function unlockUserAccount(userId) { + const result = db.prepare(` + UPDATE users + SET failed_login_attempts = 0, + locked_until = NULL, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `).run(userId); + + return result.changes > 0; +} + +// Get user statistics +export function getUserStats() { + const stats = db.prepare(` + SELECT + COUNT(*) as total_users, + COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_users, + COUNT(CASE WHEN is_active = 0 THEN 1 END) as inactive_users, + COUNT(CASE WHEN role = 'admin' THEN 1 END) as admin_users, + COUNT(CASE WHEN role = 'project_manager' THEN 1 END) as manager_users, + COUNT(CASE WHEN role = 'user' THEN 1 END) as regular_users, + COUNT(CASE WHEN role = 'read_only' THEN 1 END) as readonly_users, + COUNT(CASE WHEN last_login IS NOT NULL THEN 1 END) as users_with_login + FROM users + `).get(); + + return stats; +} diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..e791824 --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,43 @@ +import { auth } from "@/lib/auth"; + +export default auth((req) => { + const { pathname } = req.nextUrl; + + // Allow access to auth pages + if (pathname.startsWith("/auth/")) { + return; + } + + // Allow access to API routes (they handle their own auth) + if (pathname.startsWith("/api/")) { + return; + } + + // Require authentication for all other pages + if (!req.auth) { + const url = new URL("/auth/signin", req.url); + url.searchParams.set("callbackUrl", req.nextUrl.pathname); + return Response.redirect(url); + } + + // Check admin routes (role check only, no database access) + if (pathname.startsWith("/admin/")) { + if (!["admin", "project_manager"].includes(req.auth.user.role)) { + return Response.redirect(new URL("/", req.url)); + } + } +}); + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (all API routes handle their own auth) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - auth pages (auth pages should be accessible) + */ + "/((?!api|_next/static|_next/image|favicon.ico|auth).*)", + ], +}; diff --git a/test-audit-fix-direct.mjs b/test-audit-fix-direct.mjs new file mode 100644 index 0000000..24bc802 --- /dev/null +++ b/test-audit-fix-direct.mjs @@ -0,0 +1,97 @@ +// 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(); diff --git a/test-audit-logging.mjs b/test-audit-logging.mjs new file mode 100644 index 0000000..19fc5fb --- /dev/null +++ b/test-audit-logging.mjs @@ -0,0 +1,138 @@ +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"); diff --git a/test-auth-api.mjs b/test-auth-api.mjs new file mode 100644 index 0000000..a88b3f4 --- /dev/null +++ b/test-auth-api.mjs @@ -0,0 +1,109 @@ +// 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(); diff --git a/test-auth-detailed.mjs b/test-auth-detailed.mjs new file mode 100644 index 0000000..bce59fa --- /dev/null +++ b/test-auth-detailed.mjs @@ -0,0 +1,40 @@ +// 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); diff --git a/test-auth-pages.mjs b/test-auth-pages.mjs new file mode 100644 index 0000000..8320616 --- /dev/null +++ b/test-auth-pages.mjs @@ -0,0 +1,127 @@ +// 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); diff --git a/test-auth-session.mjs b/test-auth-session.mjs new file mode 100644 index 0000000..568f803 --- /dev/null +++ b/test-auth-session.mjs @@ -0,0 +1,37 @@ +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(); diff --git a/test-auth.mjs b/test-auth.mjs new file mode 100644 index 0000000..be08343 --- /dev/null +++ b/test-auth.mjs @@ -0,0 +1,49 @@ +// 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); diff --git a/test-complete-auth.mjs b/test-complete-auth.mjs new file mode 100644 index 0000000..4d3d653 --- /dev/null +++ b/test-complete-auth.mjs @@ -0,0 +1,115 @@ +// 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); diff --git a/test-create-function.mjs b/test-create-function.mjs new file mode 100644 index 0000000..c214482 --- /dev/null +++ b/test-create-function.mjs @@ -0,0 +1,40 @@ +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); +} diff --git a/test-current-audit-logs.mjs b/test-current-audit-logs.mjs new file mode 100644 index 0000000..6adae6d --- /dev/null +++ b/test-current-audit-logs.mjs @@ -0,0 +1,124 @@ +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(); diff --git a/test-edge-compatibility.mjs b/test-edge-compatibility.mjs new file mode 100644 index 0000000..0f85fbc --- /dev/null +++ b/test-edge-compatibility.mjs @@ -0,0 +1,83 @@ +/** + * 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"); diff --git a/test-logged-in-flow.mjs b/test-logged-in-flow.mjs new file mode 100644 index 0000000..4e0b70d --- /dev/null +++ b/test-logged-in-flow.mjs @@ -0,0 +1,206 @@ +// 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(); diff --git a/test-nextauth.mjs b/test-nextauth.mjs new file mode 100644 index 0000000..b60efe2 --- /dev/null +++ b/test-nextauth.mjs @@ -0,0 +1,47 @@ +// 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); diff --git a/test-project-api.mjs b/test-project-api.mjs new file mode 100644 index 0000000..d17dc88 --- /dev/null +++ b/test-project-api.mjs @@ -0,0 +1,27 @@ +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(); diff --git a/test-project-creation.mjs b/test-project-creation.mjs new file mode 100644 index 0000000..14fc51c --- /dev/null +++ b/test-project-creation.mjs @@ -0,0 +1,43 @@ +// 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(); diff --git a/test-safe-audit-logging.mjs b/test-safe-audit-logging.mjs new file mode 100644 index 0000000..b9dbd02 --- /dev/null +++ b/test-safe-audit-logging.mjs @@ -0,0 +1,82 @@ +/** + * 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!"); diff --git a/test-task-api.mjs b/test-task-api.mjs new file mode 100644 index 0000000..8412a48 --- /dev/null +++ b/test-task-api.mjs @@ -0,0 +1,44 @@ +// 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(); diff --git a/test-user-tracking.mjs b/test-user-tracking.mjs new file mode 100644 index 0000000..319afdc --- /dev/null +++ b/test-user-tracking.mjs @@ -0,0 +1,27 @@ +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!"); diff --git a/verify-audit-fix.mjs b/verify-audit-fix.mjs new file mode 100644 index 0000000..a8cc2b1 --- /dev/null +++ b/verify-audit-fix.mjs @@ -0,0 +1,101 @@ +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(); diff --git a/verify-project.mjs b/verify-project.mjs new file mode 100644 index 0000000..c064555 --- /dev/null +++ b/verify-project.mjs @@ -0,0 +1,7 @@ +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));