feat: Refactor user management to replace email with username across the application
This commit is contained in:
@@ -10,13 +10,13 @@ async function createInitialAdmin() {
|
|||||||
|
|
||||||
const adminUser = await createUser({
|
const adminUser = await createUser({
|
||||||
name: "Administrator",
|
name: "Administrator",
|
||||||
email: "admin@localhost.com",
|
username: "admin",
|
||||||
password: "admin123456", // Change this in production!
|
password: "admin123456", // Change this in production!
|
||||||
role: "admin"
|
role: "admin"
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("✅ Initial admin user created successfully!")
|
console.log("✅ Initial admin user created successfully!")
|
||||||
console.log("📧 Email: admin@localhost.com")
|
console.log("<EFBFBD> Username: admin")
|
||||||
console.log("🔑 Password: admin123456")
|
console.log("🔑 Password: admin123456")
|
||||||
console.log("⚠️ Please change the password after first login!")
|
console.log("⚠️ Please change the password after first login!")
|
||||||
console.log("👤 User ID:", adminUser.id)
|
console.log("👤 User ID:", adminUser.id)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function EditUserPage() {
|
|||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
username: "",
|
||||||
role: "user",
|
role: "user",
|
||||||
is_active: true,
|
is_active: true,
|
||||||
password: ""
|
password: ""
|
||||||
@@ -62,7 +62,7 @@ export default function EditUserPage() {
|
|||||||
setUser(userData);
|
setUser(userData);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: userData.name,
|
name: userData.name,
|
||||||
email: userData.email,
|
username: userData.username,
|
||||||
role: userData.role,
|
role: userData.role,
|
||||||
is_active: userData.is_active,
|
is_active: userData.is_active,
|
||||||
password: "" // Never populate password field
|
password: "" // Never populate password field
|
||||||
@@ -84,7 +84,7 @@ export default function EditUserPage() {
|
|||||||
// Prepare update data (exclude empty password)
|
// Prepare update data (exclude empty password)
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
email: formData.email,
|
username: formData.username,
|
||||||
role: formData.role,
|
role: formData.role,
|
||||||
is_active: formData.is_active
|
is_active: formData.is_active
|
||||||
};
|
};
|
||||||
@@ -209,12 +209,12 @@ export default function EditUserPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Email *
|
Username *
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="text"
|
||||||
value={formData.email}
|
value={formData.username}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export default function UserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-900">{user.name}</h3>
|
<h3 className="text-lg font-semibold text-gray-900">{user.name}</h3>
|
||||||
<p className="text-sm text-gray-500">{user.email}</p>
|
<p className="text-sm text-gray-500">{user.username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -284,7 +284,7 @@ export default function UserManagementPage() {
|
|||||||
function CreateUserModal({ onClose, onUserCreated }) {
|
function CreateUserModal({ onClose, onUserCreated }) {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
role: "user",
|
role: "user",
|
||||||
is_active: true
|
is_active: true
|
||||||
@@ -353,12 +353,12 @@ function CreateUserModal({ onClose, onUserCreated }) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Email
|
Username
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="text"
|
||||||
value={formData.email}
|
value={formData.username}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ async function updateUserHandler(req, { params }) {
|
|||||||
|
|
||||||
if (error.message.includes("already exists")) {
|
if (error.message.includes("already exists")) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "A user with this email already exists" },
|
{ error: "A user with this username already exists" },
|
||||||
{ status: 409 }
|
{ status: 409 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ async function createUserHandler(req) {
|
|||||||
const data = await req.json();
|
const data = await req.json();
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!data.name || !data.email || !data.password) {
|
if (!data.name || !data.username || !data.password) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Name, email, and password are required" },
|
{ error: "Name, username, and password are required" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ async function createUserHandler(req) {
|
|||||||
|
|
||||||
const newUser = await createUser({
|
const newUser = await createUser({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
email: data.email,
|
username: data.username,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
role: data.role || "user",
|
role: data.role || "user",
|
||||||
is_active: data.is_active !== undefined ? data.is_active : true
|
is_active: data.is_active !== undefined ? data.is_active : true
|
||||||
@@ -68,7 +68,7 @@ async function createUserHandler(req) {
|
|||||||
|
|
||||||
if (error.message.includes("already exists")) {
|
if (error.message.includes("already exists")) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "A user with this email already exists" },
|
{ error: "A user with this username already exists" },
|
||||||
{ status: 409 }
|
{ status: 409 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation"
|
|||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
|
|
||||||
function SignInContent() {
|
function SignInContent() {
|
||||||
const [email, setEmail] = useState("")
|
const [username, setUsername] = useState("")
|
||||||
const [password, setPassword] = useState("")
|
const [password, setPassword] = useState("")
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
@@ -21,13 +21,13 @@ function SignInContent() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await signIn("credentials", {
|
const result = await signIn("credentials", {
|
||||||
email,
|
username,
|
||||||
password,
|
password,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
setError("Invalid email or password")
|
setError("Invalid username or password")
|
||||||
} else {
|
} else {
|
||||||
// Successful login
|
// Successful login
|
||||||
router.push(callbackUrl)
|
router.push(callbackUrl)
|
||||||
@@ -45,10 +45,10 @@ function SignInContent() {
|
|||||||
<div className="max-w-md w-full space-y-8">
|
<div className="max-w-md w-full space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
Sign in to your account
|
Zaloguj się do swojego konta
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-center text-sm text-gray-600">
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||||||
Access the Project Management Panel
|
Dostęp do panelu
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||||
@@ -60,24 +60,24 @@ function SignInContent() {
|
|||||||
|
|
||||||
<div className="rounded-md shadow-sm -space-y-px">
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="sr-only">
|
<label htmlFor="username" className="sr-only">
|
||||||
Email address
|
Nazwa użytkownika
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="username"
|
||||||
name="email"
|
name="username"
|
||||||
type="email"
|
type="text"
|
||||||
autoComplete="email"
|
autoComplete="username"
|
||||||
required
|
required
|
||||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||||
placeholder="Email address"
|
placeholder="Nazwa użytkownika"
|
||||||
value={email}
|
value={username}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password" className="sr-only">
|
<label htmlFor="password" className="sr-only">
|
||||||
Password
|
Hasło
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
@@ -105,7 +105,7 @@ function SignInContent() {
|
|||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
Signing in...
|
Zaloguj...
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
"Sign in"
|
"Sign in"
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export default function ProjectForm({ initialData = null }) {
|
|||||||
<option value="">{t('projects.unassigned')}</option>
|
<option value="">{t('projects.unassigned')}</option>
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<option key={user.id} value={user.id}>
|
<option key={user.id} value={user.id}>
|
||||||
{user.name} ({user.email})
|
{user.name} ({user.username})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import bcrypt from "bcryptjs";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
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"),
|
password: z.string().min(6, "Password must be at least 6 characters"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
Credentials({
|
Credentials({
|
||||||
name: "credentials",
|
name: "credentials",
|
||||||
credentials: {
|
credentials: {
|
||||||
email: { label: "Email", type: "email" },
|
username: { label: "Username", type: "text" },
|
||||||
password: { label: "Password", type: "password" },
|
password: { label: "Password", type: "password" },
|
||||||
},
|
},
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
@@ -28,13 +28,13 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
const user = db
|
const user = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
SELECT id, email, name, password_hash, role, is_active,
|
SELECT id, username, name, password_hash, role, is_active,
|
||||||
failed_login_attempts, locked_until
|
failed_login_attempts, locked_until
|
||||||
FROM users
|
FROM users
|
||||||
WHERE email = ? AND is_active = 1
|
WHERE username = ? AND is_active = 1
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.get(validatedFields.email);
|
.get(validatedFields.username);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("Invalid credentials");
|
throw new Error("Invalid credentials");
|
||||||
@@ -75,7 +75,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
resourceType: RESOURCE_TYPES.SESSION,
|
||||||
details: {
|
details: {
|
||||||
email: validatedFields.email,
|
username: validatedFields.username,
|
||||||
reason: "invalid_password",
|
reason: "invalid_password",
|
||||||
failed_attempts: user.failed_login_attempts + 1,
|
failed_attempts: user.failed_login_attempts + 1,
|
||||||
},
|
},
|
||||||
@@ -107,7 +107,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
resourceType: RESOURCE_TYPES.SESSION,
|
resourceType: RESOURCE_TYPES.SESSION,
|
||||||
details: {
|
details: {
|
||||||
email: user.email,
|
username: user.username,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -117,7 +117,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
username: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: user.role,
|
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: {
|
callbacks: {
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.role = user.role;
|
token.role = user.role;
|
||||||
token.userId = user.id;
|
token.username = user.username;
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (token) {
|
if (token) {
|
||||||
session.user.id = token.userId;
|
session.user.id = token.sub;
|
||||||
session.user.role = token.role;
|
session.user.role = token.role;
|
||||||
|
session.user.username = token.username;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
signIn: "/auth/signin",
|
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,
|
||||||
});
|
});
|
||||||
|
|||||||
156
src/lib/auth_backup.js
Normal file
156
src/lib/auth_backup.js
Normal file
@@ -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,
|
||||||
|
});
|
||||||
@@ -273,7 +273,7 @@ export default function initializeDatabase() {
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
email TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
role TEXT CHECK(role IN ('admin', 'project_manager', 'user', 'read_only')) DEFAULT 'user',
|
role TEXT CHECK(role IN ('admin', 'project_manager', 'user', 'read_only')) DEFAULT 'user',
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -309,9 +309,36 @@ export default function initializeDatabase() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Create indexes for performance
|
-- 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_token ON sessions(session_token);
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ export function getAllProjects(contractId = null) {
|
|||||||
SELECT
|
SELECT
|
||||||
p.*,
|
p.*,
|
||||||
creator.name as created_by_name,
|
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.name as assigned_to_name,
|
||||||
assignee.email as assigned_to_email
|
assignee.username as assigned_to_username
|
||||||
FROM projects p
|
FROM projects p
|
||||||
LEFT JOIN users creator ON p.created_by = creator.id
|
LEFT JOIN users creator ON p.created_by = creator.id
|
||||||
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
||||||
@@ -30,9 +30,9 @@ export function getProjectById(id) {
|
|||||||
SELECT
|
SELECT
|
||||||
p.*,
|
p.*,
|
||||||
creator.name as created_by_name,
|
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.name as assigned_to_name,
|
||||||
assignee.email as assigned_to_email
|
assignee.username as assigned_to_username
|
||||||
FROM projects p
|
FROM projects p
|
||||||
LEFT JOIN users creator ON p.created_by = creator.id
|
LEFT JOIN users creator ON p.created_by = creator.id
|
||||||
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
||||||
@@ -136,7 +136,7 @@ export function getAllUsersForAssignment() {
|
|||||||
return db
|
return db
|
||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
SELECT id, name, email, role
|
SELECT id, name, username, role
|
||||||
FROM users
|
FROM users
|
||||||
WHERE is_active = 1
|
WHERE is_active = 1
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
@@ -153,9 +153,9 @@ export function getProjectsByAssignedUser(userId) {
|
|||||||
SELECT
|
SELECT
|
||||||
p.*,
|
p.*,
|
||||||
creator.name as created_by_name,
|
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.name as assigned_to_name,
|
||||||
assignee.email as assigned_to_email
|
assignee.username as assigned_to_username
|
||||||
FROM projects p
|
FROM projects p
|
||||||
LEFT JOIN users creator ON p.created_by = creator.id
|
LEFT JOIN users creator ON p.created_by = creator.id
|
||||||
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
||||||
@@ -174,9 +174,9 @@ export function getProjectsByCreator(userId) {
|
|||||||
SELECT
|
SELECT
|
||||||
p.*,
|
p.*,
|
||||||
creator.name as created_by_name,
|
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.name as assigned_to_name,
|
||||||
assignee.email as assigned_to_email
|
assignee.username as assigned_to_username
|
||||||
FROM projects p
|
FROM projects p
|
||||||
LEFT JOIN users creator ON p.created_by = creator.id
|
LEFT JOIN users creator ON p.created_by = creator.id
|
||||||
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
LEFT JOIN users assignee ON p.assigned_to = assignee.id
|
||||||
@@ -224,7 +224,7 @@ export function getNotesForProject(projectId) {
|
|||||||
`
|
`
|
||||||
SELECT n.*,
|
SELECT n.*,
|
||||||
u.name as created_by_name,
|
u.name as created_by_name,
|
||||||
u.email as created_by_email
|
u.username as created_by_username
|
||||||
FROM notes n
|
FROM notes n
|
||||||
LEFT JOIN users u ON n.created_by = u.id
|
LEFT JOIN users u ON n.created_by = u.id
|
||||||
WHERE n.project_id = ?
|
WHERE n.project_id = ?
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export function getAllProjectTasks() {
|
|||||||
p.address,
|
p.address,
|
||||||
p.finish_date,
|
p.finish_date,
|
||||||
creator.name as created_by_name,
|
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.name as assigned_to_name,
|
||||||
assignee.email as assigned_to_email
|
assignee.username as assigned_to_username
|
||||||
FROM project_tasks pt
|
FROM project_tasks pt
|
||||||
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
LEFT JOIN projects p ON pt.project_id = p.project_id
|
LEFT JOIN projects p ON pt.project_id = p.project_id
|
||||||
@@ -58,9 +58,9 @@ export function getProjectTasks(projectId) {
|
|||||||
ELSE 'custom'
|
ELSE 'custom'
|
||||||
END as task_type,
|
END as task_type,
|
||||||
creator.name as created_by_name,
|
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.name as assigned_to_name,
|
||||||
assignee.email as assigned_to_email
|
assignee.username as assigned_to_username
|
||||||
FROM project_tasks pt
|
FROM project_tasks pt
|
||||||
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
LEFT JOIN users creator ON pt.created_by = creator.id
|
LEFT JOIN users creator ON pt.created_by = creator.id
|
||||||
@@ -222,9 +222,9 @@ export function getProjectTasksByAssignedUser(userId) {
|
|||||||
p.address,
|
p.address,
|
||||||
p.finish_date,
|
p.finish_date,
|
||||||
creator.name as created_by_name,
|
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.name as assigned_to_name,
|
||||||
assignee.email as assigned_to_email
|
assignee.username as assigned_to_username
|
||||||
FROM project_tasks pt
|
FROM project_tasks pt
|
||||||
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
LEFT JOIN projects p ON pt.project_id = p.project_id
|
LEFT JOIN projects p ON pt.project_id = p.project_id
|
||||||
@@ -258,9 +258,9 @@ export function getProjectTasksByCreator(userId) {
|
|||||||
p.address,
|
p.address,
|
||||||
p.finish_date,
|
p.finish_date,
|
||||||
creator.name as created_by_name,
|
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.name as assigned_to_name,
|
||||||
assignee.email as assigned_to_email
|
assignee.username as assigned_to_username
|
||||||
FROM project_tasks pt
|
FROM project_tasks pt
|
||||||
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
LEFT JOIN projects p ON pt.project_id = p.project_id
|
LEFT JOIN projects p ON pt.project_id = p.project_id
|
||||||
@@ -288,7 +288,7 @@ export function getAllUsersForTaskAssignment() {
|
|||||||
return db
|
return db
|
||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
SELECT id, name, email, role
|
SELECT id, name, username, role
|
||||||
FROM users
|
FROM users
|
||||||
WHERE is_active = 1
|
WHERE is_active = 1
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ import bcrypt from "bcryptjs"
|
|||||||
import { randomBytes } from "crypto"
|
import { randomBytes } from "crypto"
|
||||||
|
|
||||||
// Create a new user
|
// Create a new user
|
||||||
export async function createUser({ name, email, password, role = 'user', is_active = true }) {
|
export async function createUser({ name, username, password, role = 'user', is_active = true }) {
|
||||||
const existingUser = db.prepare("SELECT id FROM users WHERE email = ?").get(email)
|
const existingUser = db.prepare("SELECT id FROM users WHERE username = ?").get(username)
|
||||||
if (existingUser) {
|
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 passwordHash = await bcrypt.hash(password, 12)
|
||||||
const userId = randomBytes(16).toString('hex')
|
const userId = randomBytes(16).toString('hex')
|
||||||
|
|
||||||
const result = db.prepare(`
|
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 (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`).run(userId, name, email, passwordHash, role, is_active ? 1 : 0)
|
`).run(userId, name, username, passwordHash, role, is_active ? 1 : 0)
|
||||||
|
|
||||||
return db.prepare(`
|
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
|
is_active, failed_login_attempts, locked_until
|
||||||
FROM users WHERE id = ?
|
FROM users WHERE id = ?
|
||||||
`).get(userId)
|
`).get(userId)
|
||||||
@@ -27,24 +27,24 @@ export async function createUser({ name, email, password, role = 'user', is_acti
|
|||||||
// Get user by ID
|
// Get user by ID
|
||||||
export function getUserById(id) {
|
export function getUserById(id) {
|
||||||
return db.prepare(`
|
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
|
is_active, failed_login_attempts, locked_until
|
||||||
FROM users WHERE id = ?
|
FROM users WHERE id = ?
|
||||||
`).get(id)
|
`).get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user by email
|
// Get user by username
|
||||||
export function getUserByEmail(email) {
|
export function getUserByUsername(username) {
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
SELECT id, name, email, role, created_at, last_login, is_active
|
SELECT id, name, username, role, created_at, last_login, is_active
|
||||||
FROM users WHERE email = ?
|
FROM users WHERE username = ?
|
||||||
`).get(email)
|
`).get(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all users (for admin)
|
// Get all users (for admin)
|
||||||
export function getAllUsers() {
|
export function getAllUsers() {
|
||||||
return db.prepare(`
|
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
|
failed_login_attempts, locked_until
|
||||||
FROM users
|
FROM users
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
@@ -136,11 +136,11 @@ export async function updateUser(userId, updates) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email is being changed and if it already exists
|
// Check if username is being changed and if it already exists
|
||||||
if (updates.email && updates.email !== user.email) {
|
if (updates.username && updates.username !== user.username) {
|
||||||
const existingUser = db.prepare("SELECT id FROM users WHERE email = ? AND id != ?").get(updates.email, userId);
|
const existingUser = db.prepare("SELECT id FROM users WHERE username = ? AND id != ?").get(updates.username, userId);
|
||||||
if (existingUser) {
|
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);
|
updateValues.push(updates.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updates.email !== undefined) {
|
if (updates.username !== undefined) {
|
||||||
updateFields.push("email = ?");
|
updateFields.push("username = ?");
|
||||||
updateValues.push(updates.email);
|
updateValues.push(updates.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updates.role !== undefined) {
|
if (updates.role !== undefined) {
|
||||||
@@ -198,7 +198,7 @@ export async function updateUser(userId, updates) {
|
|||||||
|
|
||||||
if (result.changes > 0) {
|
if (result.changes > 0) {
|
||||||
return db.prepare(`
|
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
|
is_active, failed_login_attempts, locked_until
|
||||||
FROM users WHERE id = ?
|
FROM users WHERE id = ?
|
||||||
`).get(userId);
|
`).get(userId);
|
||||||
|
|||||||
20
update-queries.ps1
Normal file
20
update-queries.ps1
Normal file
@@ -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!"
|
||||||
Reference in New Issue
Block a user