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/lib/middleware/auth.js b/src/lib/middleware/auth.js
index 70169c2..1d01a78 100644
--- a/src/lib/middleware/auth.js
+++ b/src/lib/middleware/auth.js
@@ -10,7 +10,7 @@ const ROLE_HIERARCHY = {
}
export function withAuth(handler, options = {}) {
- return auth(async (req) => {
+ return auth(async (req, context) => {
try {
// Check if user is authenticated
if (!req.auth?.user) {
@@ -39,8 +39,8 @@ export function withAuth(handler, options = {}) {
role: req.auth.user.role
}
- // Call the original handler
- return await handler(req)
+ // Call the original handler with both req and context
+ return await handler(req, context)
} catch (error) {
console.error("Auth middleware error:", error)
return NextResponse.json(
diff --git a/src/lib/userManagement.js b/src/lib/userManagement.js
index d95bad3..8c139c1 100644
--- a/src/lib/userManagement.js
+++ b/src/lib/userManagement.js
@@ -3,7 +3,7 @@ import bcrypt from "bcryptjs"
import { randomBytes } from "crypto"
// Create a new user
-export async function createUser({ name, email, password, role = 'user' }) {
+export async function createUser({ name, email, password, role = 'user', is_active = true }) {
const existingUser = db.prepare("SELECT id FROM users WHERE email = ?").get(email)
if (existingUser) {
throw new Error("User with this email already exists")
@@ -13,17 +13,22 @@ export async function createUser({ name, email, password, role = 'user' }) {
const userId = randomBytes(16).toString('hex')
const result = db.prepare(`
- INSERT INTO users (id, name, email, password_hash, role)
- VALUES (?, ?, ?, ?, ?)
- `).run(userId, name, email, passwordHash, role)
+ INSERT INTO users (id, name, email, password_hash, role, is_active)
+ VALUES (?, ?, ?, ?, ?, ?)
+ `).run(userId, name, email, passwordHash, role, is_active ? 1 : 0)
- return { id: userId, name, email, role }
+ return db.prepare(`
+ SELECT id, name, email, role, created_at, updated_at, last_login,
+ is_active, failed_login_attempts, locked_until
+ FROM users WHERE id = ?
+ `).get(userId)
}
// Get user by ID
export function getUserById(id) {
return db.prepare(`
- SELECT id, name, email, role, created_at, last_login, is_active
+ SELECT id, name, email, password_hash, role, created_at, updated_at, last_login,
+ is_active, failed_login_attempts, locked_until
FROM users WHERE id = ?
`).get(id)
}
@@ -39,7 +44,7 @@ export function getUserByEmail(email) {
// Get all users (for admin)
export function getAllUsers() {
return db.prepare(`
- SELECT id, name, email, role, created_at, last_login, is_active,
+ SELECT id, name, email, password_hash, role, created_at, updated_at, last_login, is_active,
failed_login_attempts, locked_until
FROM users
ORDER BY created_at DESC
@@ -123,3 +128,140 @@ export function getUserAuditLogs(userId, limit = 50) {
LIMIT ?
`).all(userId, limit)
}
+
+// Update user (comprehensive update function)
+export async function updateUser(userId, updates) {
+ const user = getUserById(userId);
+ if (!user) {
+ return null;
+ }
+
+ // Check if email is being changed and if it already exists
+ if (updates.email && updates.email !== user.email) {
+ const existingUser = db.prepare("SELECT id FROM users WHERE email = ? AND id != ?").get(updates.email, userId);
+ if (existingUser) {
+ throw new Error("User with this email already exists");
+ }
+ }
+
+ // Prepare update fields
+ const updateFields = [];
+ const updateValues = [];
+
+ if (updates.name !== undefined) {
+ updateFields.push("name = ?");
+ updateValues.push(updates.name);
+ }
+
+ if (updates.email !== undefined) {
+ updateFields.push("email = ?");
+ updateValues.push(updates.email);
+ }
+
+ if (updates.role !== undefined) {
+ const validRoles = ['admin', 'project_manager', 'user', 'read_only'];
+ if (!validRoles.includes(updates.role)) {
+ throw new Error("Invalid role");
+ }
+ updateFields.push("role = ?");
+ updateValues.push(updates.role);
+ }
+
+ if (updates.is_active !== undefined) {
+ updateFields.push("is_active = ?");
+ updateValues.push(updates.is_active ? 1 : 0);
+ }
+
+ if (updates.password !== undefined) {
+ const passwordHash = await bcrypt.hash(updates.password, 12);
+ updateFields.push("password_hash = ?");
+ updateValues.push(passwordHash);
+ // Reset failed login attempts when password is changed
+ updateFields.push("failed_login_attempts = 0");
+ updateFields.push("locked_until = NULL");
+ }
+
+ if (updateFields.length === 0) {
+ return getUserById(userId); // Return existing user if no updates
+ }
+
+ updateFields.push("updated_at = CURRENT_TIMESTAMP");
+ updateValues.push(userId);
+
+ const query = `
+ UPDATE users
+ SET ${updateFields.join(", ")}
+ WHERE id = ?
+ `;
+
+ const result = db.prepare(query).run(...updateValues);
+
+ if (result.changes > 0) {
+ return db.prepare(`
+ SELECT id, name, email, role, created_at, updated_at, last_login,
+ is_active, failed_login_attempts, locked_until
+ FROM users WHERE id = ?
+ `).get(userId);
+ }
+
+ return null;
+}
+
+// Delete user
+export function deleteUser(userId) {
+ // First, delete related data (sessions, audit logs, etc.)
+ db.prepare("DELETE FROM sessions WHERE user_id = ?").run(userId);
+ db.prepare("DELETE FROM audit_logs WHERE user_id = ?").run(userId);
+
+ // Then delete the user
+ const result = db.prepare("DELETE FROM users WHERE id = ?").run(userId);
+
+ return result.changes > 0;
+}
+
+// Reset user password (admin function)
+export async function resetUserPassword(userId, newPassword) {
+ const passwordHash = await bcrypt.hash(newPassword, 12);
+
+ const result = db.prepare(`
+ UPDATE users
+ SET password_hash = ?,
+ failed_login_attempts = 0,
+ locked_until = NULL,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ `).run(passwordHash, userId);
+
+ return result.changes > 0;
+}
+
+// Unlock user account
+export function unlockUserAccount(userId) {
+ const result = db.prepare(`
+ UPDATE users
+ SET failed_login_attempts = 0,
+ locked_until = NULL,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ `).run(userId);
+
+ return result.changes > 0;
+}
+
+// Get user statistics
+export function getUserStats() {
+ const stats = db.prepare(`
+ SELECT
+ COUNT(*) as total_users,
+ COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_users,
+ COUNT(CASE WHEN is_active = 0 THEN 1 END) as inactive_users,
+ COUNT(CASE WHEN role = 'admin' THEN 1 END) as admin_users,
+ COUNT(CASE WHEN role = 'project_manager' THEN 1 END) as manager_users,
+ COUNT(CASE WHEN role = 'user' THEN 1 END) as regular_users,
+ COUNT(CASE WHEN role = 'read_only' THEN 1 END) as readonly_users,
+ COUNT(CASE WHEN last_login IS NOT NULL THEN 1 END) as users_with_login
+ FROM users
+ `).get();
+
+ return stats;
+}