diff --git a/src/app/api/auth/change-password/route.js b/src/app/api/auth/change-password/route.js
new file mode 100644
index 0000000..8bbf0b6
--- /dev/null
+++ b/src/app/api/auth/change-password/route.js
@@ -0,0 +1,80 @@
+import { NextResponse } from "next/server";
+import { auth } from "@/lib/auth";
+import bcrypt from "bcryptjs";
+import { z } from "zod";
+
+const changePasswordSchema = z.object({
+ currentPassword: z.string().min(1, "Current password is required"),
+ newPassword: z.string().min(6, "New password must be at least 6 characters"),
+});
+
+export async function POST(request) {
+ try {
+ const session = await auth();
+
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401 }
+ );
+ }
+
+ const body = await request.json();
+ const { currentPassword, newPassword } = changePasswordSchema.parse(body);
+
+ // Import database here to avoid edge runtime issues
+ const { default: db } = await import("@/lib/db.js");
+
+ // Get current user password hash
+ const user = db
+ .prepare("SELECT password_hash FROM users WHERE id = ?")
+ .get(session.user.id);
+
+ if (!user) {
+ return NextResponse.json(
+ { error: "User not found" },
+ { status: 404 }
+ );
+ }
+
+ // Verify current password
+ const isValidPassword = await bcrypt.compare(currentPassword, user.password_hash);
+
+ if (!isValidPassword) {
+ return NextResponse.json(
+ { error: "Current password is incorrect" },
+ { status: 400 }
+ );
+ }
+
+ // Hash the new password
+ const hashedNewPassword = await bcrypt.hash(newPassword, 12);
+
+ // Update password
+ db.prepare("UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
+ .run(hashedNewPassword, session.user.id);
+
+ // Log audit event
+ try {
+ const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = await import("@/lib/auditLogSafe.js");
+ await logAuditEventSafe({
+ action: AUDIT_ACTIONS.USER_UPDATE,
+ userId: session.user.id,
+ resourceType: RESOURCE_TYPES.USER,
+ details: { field: "password", username: session.user.username },
+ });
+ } catch (auditError) {
+ console.error("Failed to log audit event:", auditError);
+ }
+
+ return NextResponse.json({
+ message: "Password changed successfully",
+ });
+ } catch (error) {
+ console.error("Change password error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/password-reset/request/route.js b/src/app/api/auth/password-reset/request/route.js
new file mode 100644
index 0000000..ca7c550
--- /dev/null
+++ b/src/app/api/auth/password-reset/request/route.js
@@ -0,0 +1,70 @@
+import { NextResponse } from "next/server";
+import crypto from "crypto";
+import { z } from "zod";
+
+const requestSchema = z.object({
+ username: z.string().min(1, "Username is required"),
+});
+
+export async function POST(request) {
+ try {
+ const body = await request.json();
+ const { username } = requestSchema.parse(body);
+
+ // Import database here to avoid edge runtime issues
+ const { default: db } = await import("@/lib/db.js");
+
+ // Check if user exists and is active
+ const user = db
+ .prepare("SELECT id, username, name FROM users WHERE username = ? AND is_active = 1")
+ .get(username);
+
+ if (!user) {
+ // Don't reveal if user exists or not for security
+ return NextResponse.json(
+ { message: "If the username exists, a password reset link has been sent." },
+ { status: 200 }
+ );
+ }
+
+ // Generate reset token
+ const token = crypto.randomBytes(32).toString("hex");
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour
+
+ // Delete any existing tokens for this user
+ db.prepare("DELETE FROM password_reset_tokens WHERE user_id = ?").run(user.id);
+
+ // Insert new token
+ db.prepare(
+ "INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)"
+ ).run(user.id, token, expiresAt);
+
+ // TODO: Send email with reset link
+ // For now, return the token for testing purposes
+ console.log(`Password reset token for ${username}: ${token}`);
+
+ // Log audit event
+ try {
+ const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = await import("@/lib/auditLogSafe.js");
+ await logAuditEventSafe({
+ action: AUDIT_ACTIONS.PASSWORD_RESET_REQUEST,
+ userId: user.id,
+ resourceType: RESOURCE_TYPES.USER,
+ details: { username: user.username },
+ });
+ } catch (auditError) {
+ console.error("Failed to log audit event:", auditError);
+ }
+
+ return NextResponse.json(
+ { message: "If the username exists, a password reset link has been sent." },
+ { status: 200 }
+ );
+ } catch (error) {
+ console.error("Password reset request error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/password-reset/reset/route.js b/src/app/api/auth/password-reset/reset/route.js
new file mode 100644
index 0000000..b525b2e
--- /dev/null
+++ b/src/app/api/auth/password-reset/reset/route.js
@@ -0,0 +1,71 @@
+import { NextResponse } from "next/server";
+import bcrypt from "bcryptjs";
+import { z } from "zod";
+
+const resetSchema = z.object({
+ token: z.string().min(1, "Token is required"),
+ password: z.string().min(6, "Password must be at least 6 characters"),
+});
+
+export async function POST(request) {
+ try {
+ const body = await request.json();
+ const { token, password } = resetSchema.parse(body);
+
+ // Import database here to avoid edge runtime issues
+ const { default: db } = await import("@/lib/db.js");
+
+ // Check if token exists and is valid
+ const resetToken = db
+ .prepare(
+ `
+ SELECT prt.*, u.username, u.name
+ FROM password_reset_tokens prt
+ JOIN users u ON prt.user_id = u.id
+ WHERE prt.token = ? AND prt.used = 0 AND prt.expires_at > datetime('now')
+ `
+ )
+ .get(token);
+
+ if (!resetToken) {
+ return NextResponse.json(
+ { error: "Invalid or expired token" },
+ { status: 400 }
+ );
+ }
+
+ // Hash the new password
+ const hashedPassword = await bcrypt.hash(password, 12);
+
+ // Update user password
+ db.prepare("UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
+ .run(hashedPassword, resetToken.user_id);
+
+ // Mark token as used
+ db.prepare("UPDATE password_reset_tokens SET used = 1 WHERE id = ?")
+ .run(resetToken.id);
+
+ // Log audit event
+ try {
+ const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } = await import("@/lib/auditLogSafe.js");
+ await logAuditEventSafe({
+ action: AUDIT_ACTIONS.PASSWORD_RESET,
+ userId: resetToken.user_id,
+ resourceType: RESOURCE_TYPES.USER,
+ details: { username: resetToken.username },
+ });
+ } catch (auditError) {
+ console.error("Failed to log audit event:", auditError);
+ }
+
+ return NextResponse.json({
+ message: "Password has been reset successfully",
+ });
+ } catch (error) {
+ console.error("Password reset error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/password-reset/verify/route.js b/src/app/api/auth/password-reset/verify/route.js
new file mode 100644
index 0000000..ec7f8b5
--- /dev/null
+++ b/src/app/api/auth/password-reset/verify/route.js
@@ -0,0 +1,47 @@
+import { NextResponse } from "next/server";
+import { z } from "zod";
+
+const verifySchema = z.object({
+ token: z.string().min(1, "Token is required"),
+});
+
+export async function POST(request) {
+ try {
+ const body = await request.json();
+ const { token } = verifySchema.parse(body);
+
+ // Import database here to avoid edge runtime issues
+ const { default: db } = await import("@/lib/db.js");
+
+ // Check if token exists and is valid
+ const resetToken = db
+ .prepare(
+ `
+ SELECT prt.*, u.username, u.name
+ FROM password_reset_tokens prt
+ JOIN users u ON prt.user_id = u.id
+ WHERE prt.token = ? AND prt.used = 0 AND prt.expires_at > datetime('now')
+ `
+ )
+ .get(token);
+
+ if (!resetToken) {
+ return NextResponse.json(
+ { error: "Invalid or expired token" },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({
+ valid: true,
+ username: resetToken.username,
+ name: resetToken.name,
+ });
+ } catch (error) {
+ console.error("Token verification error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/settings/page.js b/src/app/settings/page.js
index e9c4196..936ba1a 100644
--- a/src/app/settings/page.js
+++ b/src/app/settings/page.js
@@ -6,6 +6,7 @@ import PageHeader from "@/components/ui/PageHeader";
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import ThemeToggle from "@/components/ui/ThemeToggle";
import LanguageSwitcher from "@/components/ui/LanguageSwitcher";
+import PasswordReset from "@/components/settings/PasswordReset";
export default function SettingsPage() {
const { t } = useTranslation();
@@ -58,6 +59,18 @@ export default function SettingsPage() {
+
+ {/* Password Settings */}
+
+ {t('settings.password') || 'Password'}
+
+
+ {t('settings.passwordDescription') || 'Change your account password'} +
+ + +