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 && (
+
+ )}
+
+ {success && (
+
+ )}
+
+
+
+ User Information
+
+
+
+
+
+
+ {/* 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 && (
+
+ )}
+
+ {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 && (
+
+ )}
+
+
+
+
+ );
+}
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 (
+
+
+
+ }>
+
+
+ )
+}
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
+
+
+
+
+
+ )
+}
+
+export default function SignIn() {
+ return (
+
+
+
+ }>
+
+
+ )
+}
\ 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}
+