From 294d8343d337d5a11635c4782053a58bb6d8ff47 Mon Sep 17 00:00:00 2001 From: Chop <28534054+RChopin@users.noreply.github.com> Date: Wed, 25 Jun 2025 23:08:15 +0200 Subject: [PATCH] feat: Implement user tracking in projects - Added user tracking features to the projects module, including: - Database schema updates to track project creator and assignee. - API enhancements for user management and project filtering by user. - UI components for user assignment in project forms and listings. - New query functions for retrieving users and filtering projects. - Security integration with role-based access and authentication requirements. chore: Create utility scripts for database checks and project testing - Added scripts to check the structure of the projects table. - Created tests for project creation and user tracking functionality. - Implemented API tests to verify project retrieval and user assignment. fix: Update project creation and update functions to include user tracking - Modified createProject and updateProject functions to handle user IDs for creator and assignee. - Ensured that project updates reflect the correct user assignments and timestamps. --- AUTHORIZATION_IMPLEMENTATION.md | 93 +++++++++++++++++++ check-columns.mjs | 13 +++ check-projects-table.mjs | 5 ++ check-projects.mjs | 32 +++++++ package-lock.json | 128 ++++++++++++++++++++++++++ package.json | 1 + src/app/api/projects/[id]/route.js | 31 +++++-- src/app/api/projects/route.js | 34 ++++++- src/app/api/projects/users/route.js | 33 +++++++ src/app/projects/[id]/edit/page.js | 49 ++++++++-- src/app/projects/page.js | 20 ++++- src/components/ProjectForm.js | 64 ++++++++++++- src/lib/init-db.js | 43 +++++++++ src/lib/queries/projects.js | 135 +++++++++++++++++++++++++--- test-create-function.mjs | 40 +++++++++ test-project-api.mjs | 27 ++++++ test-project-creation.mjs | 43 +++++++++ test-user-tracking.mjs | 27 ++++++ verify-project.mjs | 7 ++ 19 files changed, 790 insertions(+), 35 deletions(-) create mode 100644 check-columns.mjs create mode 100644 check-projects-table.mjs create mode 100644 check-projects.mjs create mode 100644 src/app/api/projects/users/route.js create mode 100644 test-create-function.mjs create mode 100644 test-project-api.mjs create mode 100644 test-project-creation.mjs create mode 100644 test-user-tracking.mjs create mode 100644 verify-project.mjs diff --git a/AUTHORIZATION_IMPLEMENTATION.md b/AUTHORIZATION_IMPLEMENTATION.md index b2911fc..ac27373 100644 --- a/AUTHORIZATION_IMPLEMENTATION.md +++ b/AUTHORIZATION_IMPLEMENTATION.md @@ -735,6 +735,99 @@ export function withRateLimit( - Password strength requirements - Password change interface +## User Tracking in Projects - NEW FEATURE βœ… + +### πŸ“Š Project User Management Implementation + +We've successfully implemented comprehensive user tracking for projects: + +#### Database Schema Updates βœ… + +- **created_by**: Tracks who created the project (user ID) +- **assigned_to**: Tracks who is assigned to work on the project (user ID) +- **created_at**: Timestamp when project was created +- **updated_at**: Timestamp when project was last modified +- **Indexes**: Performance optimized with proper foreign key indexes + +#### API Enhancements βœ… + +- **Enhanced Queries**: Projects now include user names and emails via JOIN operations +- **User Assignment**: New `/api/projects/users` endpoint for user management +- **Query Filters**: Support for filtering projects by assigned user or creator +- **User Context**: Create/update operations automatically capture authenticated user ID + +#### UI Components βœ… + +- **Project Form**: User assignment dropdown in create/edit forms +- **Project Listing**: "Created By" and "Assigned To" columns in project table +- **User Selection**: Dropdown populated with active users for assignment + +#### New Query Functions βœ… + +- `getAllUsersForAssignment()`: Get active users for assignment dropdown +- `getProjectsByAssignedUser(userId)`: Filter projects by assignee +- `getProjectsByCreator(userId)`: Filter projects by creator +- `updateProjectAssignment(projectId, userId)`: Update project assignment + +#### Security Integration βœ… + +- **Authentication Required**: All user operations require valid session +- **Role-Based Access**: User assignment respects role hierarchy +- **Audit Ready**: Infrastructure prepared for comprehensive user action logging + +### Usage Examples + +#### Creating Projects with User Tracking + +```javascript +// Projects are automatically assigned to the authenticated user as creator +POST /api/projects +{ + "project_name": "New Project", + "assigned_to": "user-id-here", // Optional assignment + // ... other project data +} +``` + +#### Filtering Projects by User + +```javascript +// Get projects assigned to specific user +GET /api/projects?assigned_to=user-id + +// Get projects created by specific user +GET /api/projects?created_by=user-id +``` + +#### Updating Project Assignment + +```javascript +POST /api/projects/users +{ + "projectId": 123, + "assignedToUserId": "new-user-id" +} +``` + +### 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 + ## Security Best Practices ### 1. Password Security 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/package-lock.json b/package-lock.json index e7b4651..e734519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "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", @@ -4163,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", @@ -5311,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", @@ -5423,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", @@ -7843,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", @@ -10495,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", @@ -13720,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", @@ -14562,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", @@ -14643,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", @@ -16292,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", @@ -18103,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", diff --git a/package.json b/package.json index 16cf266..e12c617 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "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", diff --git a/src/app/api/projects/[id]/route.js b/src/app/api/projects/[id]/route.js index f8ec179..825607f 100644 --- a/src/app/api/projects/[id]/route.js +++ b/src/app/api/projects/[id]/route.js @@ -3,22 +3,41 @@ import { updateProject, deleteProject, } from "@/lib/queries/projects"; +import initializeDatabase from "@/lib/init-db"; import { NextResponse } from "next/server"; import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; -async function getProjectHandler(_, { params }) { - const project = getProjectById(params.id); +// 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 }); + } + return NextResponse.json(project); } async function updateProjectHandler(req, { params }) { + const { id } = await params; const data = await req.json(); - updateProject(params.id, data); - return NextResponse.json({ success: true }); + + // Get user ID from authenticated request + const userId = req.user?.id; + + updateProject(parseInt(id), data, userId); + + // Return the updated project + const updatedProject = getProjectById(parseInt(id)); + return NextResponse.json(updatedProject); } -async function deleteProjectHandler(_, { params }) { - deleteProject(params.id); +async function deleteProjectHandler(req, { params }) { + const { id } = await params; + deleteProject(parseInt(id)); return NextResponse.json({ success: true }); } diff --git a/src/app/api/projects/route.js b/src/app/api/projects/route.js index 857c391..b532ccc 100644 --- a/src/app/api/projects/route.js +++ b/src/app/api/projects/route.js @@ -1,4 +1,8 @@ -import { getAllProjects, createProject } from "@/lib/queries/projects"; +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"; @@ -9,15 +13,37 @@ initializeDatabase(); 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); + } - const projects = getAllProjects(contractId); return NextResponse.json(projects); } 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); + return NextResponse.json({ + success: true, + projectId: result.lastInsertRowid, + }); } // Protected routes - require authentication 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/projects/[id]/edit/page.js b/src/app/projects/[id]/edit/page.js index aae2915..5857a1d 100644 --- a/src/app/projects/[id]/edit/page.js +++ b/src/app/projects/[id]/edit/page.js @@ -1,17 +1,52 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; import ProjectForm from "@/components/ProjectForm"; import PageContainer from "@/components/ui/PageContainer"; import PageHeader from "@/components/ui/PageHeader"; import Button from "@/components/ui/Button"; import Link from "next/link"; +import { LoadingState } from "@/components/ui/States"; -export default async function EditProjectPage({ params }) { - const { id } = await params; - const res = await fetch(`http://localhost:3000/api/projects/${id}`, { - cache: "no-store", - }); - const project = await res.json(); +export default function EditProjectPage() { + const params = useParams(); + const id = params.id; + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - if (!project) { + useEffect(() => { + const fetchProject = async () => { + try { + const res = await fetch(`/api/projects/${id}`); + if (res.ok) { + const projectData = await res.json(); + setProject(projectData); + } else { + setError("Project not found"); + } + } catch (err) { + setError("Failed to load project"); + } finally { + setLoading(false); + } + }; + + if (id) { + fetchProject(); + } + }, [id]); + + if (loading) { + return ( + + + + ); + } + + if (error || !project) { return (
diff --git a/src/app/projects/page.js b/src/app/projects/page.js index e576d1d..7c9118a 100644 --- a/src/app/projects/page.js +++ b/src/app/projects/page.js @@ -195,7 +195,13 @@ export default function ProjectListPage() { Status - {" "} + + + Created By + + + Assigned To + Actions @@ -275,6 +281,18 @@ export default function ProjectListPage() { ? "ZakoΕ„czony" : "-"} + + {project.created_by_name || "Unknown"} + + + {project.assigned_to_name || "Unassigned"} +