diff --git a/scripts/create-admin.js b/scripts/create-admin.js index 5da9828..a65d6b5 100644 --- a/scripts/create-admin.js +++ b/scripts/create-admin.js @@ -10,13 +10,13 @@ async function createInitialAdmin() { const adminUser = await createUser({ name: "Administrator", - email: "admin@localhost.com", + username: "admin", password: "admin123456", // Change this in production! role: "admin" }) console.log("✅ Initial admin user created successfully!") - console.log("📧 Email: admin@localhost.com") + console.log("� Username: admin") console.log("🔑 Password: admin123456") console.log("⚠️ Please change the password after first login!") console.log("👤 User ID:", adminUser.id) diff --git a/src/app/admin/users/[id]/edit/page.js b/src/app/admin/users/[id]/edit/page.js index 6a57ea0..1841782 100644 --- a/src/app/admin/users/[id]/edit/page.js +++ b/src/app/admin/users/[id]/edit/page.js @@ -15,7 +15,7 @@ export default function EditUserPage() { const [user, setUser] = useState(null); const [formData, setFormData] = useState({ name: "", - email: "", + username: "", role: "user", is_active: true, password: "" @@ -62,7 +62,7 @@ export default function EditUserPage() { setUser(userData); setFormData({ name: userData.name, - email: userData.email, + username: userData.username, role: userData.role, is_active: userData.is_active, password: "" // Never populate password field @@ -84,7 +84,7 @@ export default function EditUserPage() { // Prepare update data (exclude empty password) const updateData = { name: formData.name, - email: formData.email, + username: formData.username, role: formData.role, is_active: formData.is_active }; @@ -209,12 +209,12 @@ export default function EditUserPage() {
setFormData({ ...formData, email: e.target.value })} + type="text" + value={formData.username} + onChange={(e) => setFormData({ ...formData, username: e.target.value })} required />
diff --git a/src/app/admin/users/page.js b/src/app/admin/users/page.js index bcd9cd6..e805449 100644 --- a/src/app/admin/users/page.js +++ b/src/app/admin/users/page.js @@ -194,7 +194,7 @@ export default function UserManagementPage() {

{user.name}

-

{user.email}

+

{user.username}

@@ -284,7 +284,7 @@ export default function UserManagementPage() { function CreateUserModal({ onClose, onUserCreated }) { const [formData, setFormData] = useState({ name: "", - email: "", + username: "", password: "", role: "user", is_active: true @@ -353,12 +353,12 @@ function CreateUserModal({ onClose, onUserCreated }) {
setFormData({ ...formData, email: e.target.value })} + type="text" + value={formData.username} + onChange={(e) => setFormData({ ...formData, username: e.target.value })} required />
diff --git a/src/app/api/admin/users/[id]/route.js b/src/app/api/admin/users/[id]/route.js index b686f43..49a407c 100644 --- a/src/app/api/admin/users/[id]/route.js +++ b/src/app/api/admin/users/[id]/route.js @@ -78,7 +78,7 @@ async function updateUserHandler(req, { params }) { if (error.message.includes("already exists")) { return NextResponse.json( - { error: "A user with this email already exists" }, + { error: "A user with this username already exists" }, { status: 409 } ); } diff --git a/src/app/api/admin/users/route.js b/src/app/api/admin/users/route.js index 324162c..2ab09ee 100644 --- a/src/app/api/admin/users/route.js +++ b/src/app/api/admin/users/route.js @@ -27,9 +27,9 @@ async function createUserHandler(req) { const data = await req.json(); // Validate required fields - if (!data.name || !data.email || !data.password) { + if (!data.name || !data.username || !data.password) { return NextResponse.json( - { error: "Name, email, and password are required" }, + { error: "Name, username, and password are required" }, { status: 400 } ); } @@ -53,7 +53,7 @@ async function createUserHandler(req) { const newUser = await createUser({ name: data.name, - email: data.email, + username: data.username, password: data.password, role: data.role || "user", is_active: data.is_active !== undefined ? data.is_active : true @@ -68,7 +68,7 @@ async function createUserHandler(req) { if (error.message.includes("already exists")) { return NextResponse.json( - { error: "A user with this email already exists" }, + { error: "A user with this username already exists" }, { status: 409 } ); } diff --git a/src/app/auth/signin/page.js b/src/app/auth/signin/page.js index 6dab208..0176ce3 100644 --- a/src/app/auth/signin/page.js +++ b/src/app/auth/signin/page.js @@ -6,7 +6,7 @@ import { useRouter } from "next/navigation" import { useSearchParams } from "next/navigation" function SignInContent() { - const [email, setEmail] = useState("") + const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [error, setError] = useState("") const [isLoading, setIsLoading] = useState(false) @@ -21,13 +21,13 @@ function SignInContent() { try { const result = await signIn("credentials", { - email, + username, password, redirect: false, }) if (result?.error) { - setError("Invalid email or password") + setError("Invalid username or password") } else { // Successful login router.push(callbackUrl) @@ -45,10 +45,10 @@ function SignInContent() {

- Sign in to your account + Zaloguj się do swojego konta

- Access the Project Management Panel + Dostęp do panelu

@@ -60,24 +60,24 @@ function SignInContent() {
-
- Signing in... + Zaloguj... ) : ( "Sign in" diff --git a/src/components/ProjectForm.js b/src/components/ProjectForm.js index 492b7dd..788bdc9 100644 --- a/src/components/ProjectForm.js +++ b/src/components/ProjectForm.js @@ -178,7 +178,7 @@ export default function ProjectForm({ initialData = null }) { {users.map((user) => ( ))} diff --git a/src/lib/auth.js b/src/lib/auth.js index d906e92..35cb9fb 100644 --- a/src/lib/auth.js +++ b/src/lib/auth.js @@ -4,7 +4,7 @@ import bcrypt from "bcryptjs"; import { z } from "zod"; const loginSchema = z.object({ - email: z.string().email("Invalid email format"), + username: z.string().min(1, "Username is required"), password: z.string().min(6, "Password must be at least 6 characters"), }); @@ -13,7 +13,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ Credentials({ name: "credentials", credentials: { - email: { label: "Email", type: "email" }, + username: { label: "Username", type: "text" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { @@ -28,13 +28,13 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ const user = db .prepare( ` - SELECT id, email, name, password_hash, role, is_active, + SELECT id, username, name, password_hash, role, is_active, failed_login_attempts, locked_until FROM users - WHERE email = ? AND is_active = 1 + WHERE username = ? AND is_active = 1 ` ) - .get(validatedFields.email); + .get(validatedFields.username); if (!user) { throw new Error("Invalid credentials"); @@ -75,7 +75,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ userId: user.id, resourceType: RESOURCE_TYPES.SESSION, details: { - email: validatedFields.email, + username: validatedFields.username, reason: "invalid_password", failed_attempts: user.failed_login_attempts + 1, }, @@ -107,7 +107,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ userId: user.id, resourceType: RESOURCE_TYPES.SESSION, details: { - email: user.email, + username: user.username, role: user.role, }, }); @@ -117,7 +117,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ return { id: user.id, - email: user.email, + username: user.username, name: user.name, role: user.role, }; @@ -128,30 +128,29 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ }, }), ], - session: { - strategy: "jwt", - maxAge: 30 * 24 * 60 * 60, // 30 days - }, callbacks: { async jwt({ token, user }) { if (user) { token.role = user.role; - token.userId = user.id; + token.username = user.username; } return token; }, async session({ session, token }) { if (token) { - session.user.id = token.userId; + session.user.id = token.sub; session.user.role = token.role; + session.user.username = token.username; } return session; }, }, pages: { signIn: "/auth/signin", - signOut: "/auth/signout", - error: "/auth/error", }, - debug: process.env.NODE_ENV === "development", + session: { + strategy: "jwt", + maxAge: 24 * 60 * 60, // 24 hours + }, + secret: process.env.NEXTAUTH_SECRET, }); diff --git a/src/lib/auth_backup.js b/src/lib/auth_backup.js new file mode 100644 index 0000000..35cb9fb --- /dev/null +++ b/src/lib/auth_backup.js @@ -0,0 +1,156 @@ +import NextAuth from "next-auth"; +import Credentials from "next-auth/providers/credentials"; +import bcrypt from "bcryptjs"; +import { z } from "zod"; + +const loginSchema = z.object({ + username: z.string().min(1, "Username is required"), + password: z.string().min(6, "Password must be at least 6 characters"), +}); + +export const { handlers, auth, signIn, signOut } = NextAuth({ + providers: [ + Credentials({ + name: "credentials", + credentials: { + username: { label: "Username", type: "text" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + try { + // Import database here to avoid edge runtime issues + const { default: db } = await import("./db.js"); + + // Validate input + const validatedFields = loginSchema.parse(credentials); + + // Check if user exists and is active + const user = db + .prepare( + ` + SELECT id, username, name, password_hash, role, is_active, + failed_login_attempts, locked_until + FROM users + WHERE username = ? AND is_active = 1 + ` + ) + .get(validatedFields.username); + + if (!user) { + throw new Error("Invalid credentials"); + } + + // Check if account is locked + if (user.locked_until && new Date(user.locked_until) > new Date()) { + throw new Error("Account temporarily locked"); + } + + // Verify password + const isValidPassword = await bcrypt.compare( + validatedFields.password, + user.password_hash + ); + + if (!isValidPassword) { + // Increment failed attempts + db.prepare( + ` + UPDATE users + SET failed_login_attempts = failed_login_attempts + 1, + locked_until = CASE + WHEN failed_login_attempts >= 4 + THEN datetime('now', '+15 minutes') + ELSE locked_until + END + WHERE id = ? + ` + ).run(user.id); + + // Log failed login attempt (only in Node.js runtime) + try { + const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = + await import("./auditLogSafe.js"); + await logAuditEventSafe({ + action: AUDIT_ACTIONS.LOGIN_FAILED, + userId: user.id, + resourceType: RESOURCE_TYPES.SESSION, + details: { + username: validatedFields.username, + reason: "invalid_password", + failed_attempts: user.failed_login_attempts + 1, + }, + }); + } catch (auditError) { + console.error("Failed to log audit event:", auditError); + } + + throw new Error("Invalid credentials"); + } + + // Reset failed attempts and update last login + db.prepare( + ` + UPDATE users + SET failed_login_attempts = 0, + locked_until = NULL, + last_login = CURRENT_TIMESTAMP + WHERE id = ? + ` + ).run(user.id); + + // Log successful login (only in Node.js runtime) + try { + const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = + await import("./auditLogSafe.js"); + await logAuditEventSafe({ + action: AUDIT_ACTIONS.LOGIN, + userId: user.id, + resourceType: RESOURCE_TYPES.SESSION, + details: { + username: user.username, + role: user.role, + }, + }); + } catch (auditError) { + console.error("Failed to log audit event:", auditError); + } + + return { + id: user.id, + username: user.username, + name: user.name, + role: user.role, + }; + } catch (error) { + console.error("Login error:", error); + return null; + } + }, + }), + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.role = user.role; + token.username = user.username; + } + return token; + }, + async session({ session, token }) { + if (token) { + session.user.id = token.sub; + session.user.role = token.role; + session.user.username = token.username; + } + return session; + }, + }, + pages: { + signIn: "/auth/signin", + }, + session: { + strategy: "jwt", + maxAge: 24 * 60 * 60, // 24 hours + }, + secret: process.env.NEXTAUTH_SECRET, +}); diff --git a/src/lib/init-db.js b/src/lib/init-db.js index c8c87e0..d191011 100644 --- a/src/lib/init-db.js +++ b/src/lib/init-db.js @@ -273,7 +273,7 @@ export default function initializeDatabase() { CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), name TEXT NOT NULL, - email TEXT UNIQUE NOT NULL, + username 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, @@ -309,9 +309,36 @@ export default function initializeDatabase() { ); -- Create indexes for performance - CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(session_token); CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); CREATE INDEX IF NOT EXISTS idx_audit_user_timestamp ON audit_logs(user_id, timestamp); `); + + // Migration: Add username column and migrate from email if needed + try { + // Check if username column exists + const columns = db.prepare("PRAGMA table_info(users)").all(); + const hasUsername = columns.some(col => col.name === 'username'); + const hasEmail = columns.some(col => col.name === 'email'); + + if (!hasUsername && hasEmail) { + // Add username column + db.exec(`ALTER TABLE users ADD COLUMN username TEXT;`); + + // Migrate existing email data to username (for development/testing) + // In production, you might want to handle this differently + db.exec(`UPDATE users SET username = email WHERE username IS NULL;`); + + // Create unique index on username + db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username_unique ON users(username);`); + + console.log("✅ Migrated users table from email to username"); + } else if (!hasUsername) { + // If neither username nor email exists, something is wrong + console.warn("⚠️ Users table missing both username and email columns"); + } + } catch (e) { + console.warn("Migration warning:", e.message); + } } diff --git a/src/lib/queries/projects.js b/src/lib/queries/projects.js index 3839dc7..8206365 100644 --- a/src/lib/queries/projects.js +++ b/src/lib/queries/projects.js @@ -5,9 +5,9 @@ export function getAllProjects(contractId = null) { SELECT p.*, creator.name as created_by_name, - creator.email as created_by_email, + creator.username as created_by_username, assignee.name as assigned_to_name, - assignee.email as assigned_to_email + assignee.username as assigned_to_username FROM projects p LEFT JOIN users creator ON p.created_by = creator.id LEFT JOIN users assignee ON p.assigned_to = assignee.id @@ -30,9 +30,9 @@ export function getProjectById(id) { SELECT p.*, creator.name as created_by_name, - creator.email as created_by_email, + creator.username as created_by_username, assignee.name as assigned_to_name, - assignee.email as assigned_to_email + assignee.username as assigned_to_username FROM projects p LEFT JOIN users creator ON p.created_by = creator.id LEFT JOIN users assignee ON p.assigned_to = assignee.id @@ -136,7 +136,7 @@ export function getAllUsersForAssignment() { return db .prepare( ` - SELECT id, name, email, role + SELECT id, name, username, role FROM users WHERE is_active = 1 ORDER BY name @@ -153,9 +153,9 @@ export function getProjectsByAssignedUser(userId) { SELECT p.*, creator.name as created_by_name, - creator.email as created_by_email, + creator.username as created_by_username, assignee.name as assigned_to_name, - assignee.email as assigned_to_email + assignee.username as assigned_to_username FROM projects p LEFT JOIN users creator ON p.created_by = creator.id LEFT JOIN users assignee ON p.assigned_to = assignee.id @@ -174,9 +174,9 @@ export function getProjectsByCreator(userId) { SELECT p.*, creator.name as created_by_name, - creator.email as created_by_email, + creator.username as created_by_username, assignee.name as assigned_to_name, - assignee.email as assigned_to_email + assignee.username as assigned_to_username FROM projects p LEFT JOIN users creator ON p.created_by = creator.id LEFT JOIN users assignee ON p.assigned_to = assignee.id @@ -224,7 +224,7 @@ export function getNotesForProject(projectId) { ` SELECT n.*, u.name as created_by_name, - u.email as created_by_email + u.username as created_by_username FROM notes n LEFT JOIN users u ON n.created_by = u.id WHERE n.project_id = ? diff --git a/src/lib/queries/tasks.js b/src/lib/queries/tasks.js index c9d1abf..1364dda 100644 --- a/src/lib/queries/tasks.js +++ b/src/lib/queries/tasks.js @@ -29,9 +29,9 @@ export function getAllProjectTasks() { p.address, p.finish_date, creator.name as created_by_name, - creator.email as created_by_email, + creator.username as created_by_username, assignee.name as assigned_to_name, - assignee.email as assigned_to_email + assignee.username as assigned_to_username FROM project_tasks pt LEFT JOIN tasks t ON pt.task_template_id = t.task_id LEFT JOIN projects p ON pt.project_id = p.project_id @@ -58,9 +58,9 @@ export function getProjectTasks(projectId) { ELSE 'custom' END as task_type, creator.name as created_by_name, - creator.email as created_by_email, + creator.username as created_by_username, assignee.name as assigned_to_name, - assignee.email as assigned_to_email + assignee.username as assigned_to_username FROM project_tasks pt LEFT JOIN tasks t ON pt.task_template_id = t.task_id LEFT JOIN users creator ON pt.created_by = creator.id @@ -222,9 +222,9 @@ export function getProjectTasksByAssignedUser(userId) { p.address, p.finish_date, creator.name as created_by_name, - creator.email as created_by_email, + creator.username as created_by_username, assignee.name as assigned_to_name, - assignee.email as assigned_to_email + assignee.username as assigned_to_username FROM project_tasks pt LEFT JOIN tasks t ON pt.task_template_id = t.task_id LEFT JOIN projects p ON pt.project_id = p.project_id @@ -258,9 +258,9 @@ export function getProjectTasksByCreator(userId) { p.address, p.finish_date, creator.name as created_by_name, - creator.email as created_by_email, + creator.username as created_by_username, assignee.name as assigned_to_name, - assignee.email as assigned_to_email + assignee.username as assigned_to_username FROM project_tasks pt LEFT JOIN tasks t ON pt.task_template_id = t.task_id LEFT JOIN projects p ON pt.project_id = p.project_id @@ -288,7 +288,7 @@ export function getAllUsersForTaskAssignment() { return db .prepare( ` - SELECT id, name, email, role + SELECT id, name, username, role FROM users WHERE is_active = 1 ORDER BY name ASC diff --git a/src/lib/userManagement.js b/src/lib/userManagement.js index 8c139c1..04404a2 100644 --- a/src/lib/userManagement.js +++ b/src/lib/userManagement.js @@ -3,22 +3,22 @@ import bcrypt from "bcryptjs" import { randomBytes } from "crypto" // Create a new user -export async function createUser({ name, email, password, role = 'user', is_active = true }) { - const existingUser = db.prepare("SELECT id FROM users WHERE email = ?").get(email) +export async function createUser({ name, username, password, role = 'user', is_active = true }) { + const existingUser = db.prepare("SELECT id FROM users WHERE username = ?").get(username) if (existingUser) { - throw new Error("User with this email already exists") + throw new Error("User with this username already exists") } const passwordHash = await bcrypt.hash(password, 12) const userId = randomBytes(16).toString('hex') const result = db.prepare(` - INSERT INTO users (id, name, email, password_hash, role, is_active) + INSERT INTO users (id, name, username, password_hash, role, is_active) VALUES (?, ?, ?, ?, ?, ?) - `).run(userId, name, email, passwordHash, role, is_active ? 1 : 0) + `).run(userId, name, username, passwordHash, role, is_active ? 1 : 0) return db.prepare(` - SELECT id, name, email, role, created_at, updated_at, last_login, + SELECT id, name, username, role, created_at, updated_at, last_login, is_active, failed_login_attempts, locked_until FROM users WHERE id = ? `).get(userId) @@ -27,24 +27,24 @@ export async function createUser({ name, email, password, role = 'user', is_acti // Get user by ID export function getUserById(id) { return db.prepare(` - SELECT id, name, email, password_hash, role, created_at, updated_at, last_login, + SELECT id, name, username, password_hash, role, created_at, updated_at, last_login, is_active, failed_login_attempts, locked_until FROM users WHERE id = ? `).get(id) } -// Get user by email -export function getUserByEmail(email) { +// Get user by username +export function getUserByUsername(username) { return db.prepare(` - SELECT id, name, email, role, created_at, last_login, is_active - FROM users WHERE email = ? - `).get(email) + SELECT id, name, username, role, created_at, last_login, is_active + FROM users WHERE username = ? + `).get(username) } // Get all users (for admin) export function getAllUsers() { return db.prepare(` - SELECT id, name, email, password_hash, role, created_at, updated_at, last_login, is_active, + SELECT id, name, username, password_hash, role, created_at, updated_at, last_login, is_active, failed_login_attempts, locked_until FROM users ORDER BY created_at DESC @@ -136,11 +136,11 @@ export async function updateUser(userId, updates) { 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); + // Check if username is being changed and if it already exists + if (updates.username && updates.username !== user.username) { + const existingUser = db.prepare("SELECT id FROM users WHERE username = ? AND id != ?").get(updates.username, userId); if (existingUser) { - throw new Error("User with this email already exists"); + throw new Error("User with this username already exists"); } } @@ -153,9 +153,9 @@ export async function updateUser(userId, updates) { updateValues.push(updates.name); } - if (updates.email !== undefined) { - updateFields.push("email = ?"); - updateValues.push(updates.email); + if (updates.username !== undefined) { + updateFields.push("username = ?"); + updateValues.push(updates.username); } if (updates.role !== undefined) { @@ -198,7 +198,7 @@ export async function updateUser(userId, updates) { if (result.changes > 0) { return db.prepare(` - SELECT id, name, email, role, created_at, updated_at, last_login, + SELECT id, name, username, role, created_at, updated_at, last_login, is_active, failed_login_attempts, locked_until FROM users WHERE id = ? `).get(userId); diff --git a/update-queries.ps1 b/update-queries.ps1 new file mode 100644 index 0000000..dbb9512 --- /dev/null +++ b/update-queries.ps1 @@ -0,0 +1,20 @@ +$files = @( + "d:\panel\src\lib\queries\tasks.js", + "d:\panel\src\lib\userManagement.js" +) + +foreach ($file in $files) { + if (Test-Path $file) { + Write-Host "Updating $file..." + $content = Get-Content $file -Raw + $content = $content -replace "creator\.email as created_by_email", "creator.username as created_by_username" + $content = $content -replace "assignee\.email as assigned_to_email", "assignee.username as assigned_to_username" + $content = $content -replace "u\.email as created_by_email", "u.username as created_by_username" + $content = $content -replace "SELECT id, name, email, role", "SELECT id, name, username, role" + $content = $content -replace "name, email, role", "name, username, role" + Set-Content $file $content -NoNewline + Write-Host "Updated $file" + } +} + +Write-Host "All files updated!"