203 lines
5.1 KiB
TypeScript
203 lines
5.1 KiB
TypeScript
'use server';
|
|
|
|
import { redirect } from "next/navigation";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { hashPassword, verifyPassword, generateToken } from "@/lib/crypto";
|
|
import {
|
|
createSession,
|
|
deleteSession,
|
|
requireCurrentUser,
|
|
} from "@/lib/auth";
|
|
import { Resend } from "resend";
|
|
import type { FormState } from "@/lib/types";
|
|
|
|
export async function login(
|
|
prevState: FormState | undefined,
|
|
formData: FormData,
|
|
): Promise<FormState> {
|
|
const email = formData.get("email") as string;
|
|
const password = formData.get("password") as string;
|
|
|
|
if (!email || !password) {
|
|
return { message: "Email and password are required" };
|
|
}
|
|
|
|
const user = await prisma.user.findUnique({ where: { email } });
|
|
|
|
if (!user) {
|
|
return { message: "Invalid email or password" };
|
|
}
|
|
|
|
const valid = await verifyPassword(password, user.passwordHash);
|
|
|
|
if (!valid) {
|
|
return { message: "Invalid email or password" };
|
|
}
|
|
|
|
await createSession(user.id);
|
|
|
|
redirect("/");
|
|
}
|
|
|
|
export async function logout(): Promise<never> {
|
|
await deleteSession();
|
|
|
|
redirect("/login");
|
|
}
|
|
|
|
export async function changePassword(
|
|
prevState: FormState | undefined,
|
|
formData: FormData,
|
|
): Promise<FormState> {
|
|
const currentUser = await requireCurrentUser();
|
|
|
|
const currentPassword = formData.get("currentPassword") as string;
|
|
const newPassword = formData.get("newPassword") as string;
|
|
const confirmPassword = formData.get("confirmPassword") as string;
|
|
|
|
if (!currentPassword || !newPassword || !confirmPassword) {
|
|
return { message: "All fields are required" };
|
|
}
|
|
|
|
if (newPassword.length < 8) {
|
|
return {
|
|
errors: { newPassword: ["Password must be at least 8 characters"] },
|
|
message: "Password must be at least 8 characters",
|
|
};
|
|
}
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
return {
|
|
errors: { confirmPassword: ["Passwords do not match"] },
|
|
message: "Passwords do not match",
|
|
};
|
|
}
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: currentUser.id },
|
|
});
|
|
|
|
if (!user) {
|
|
return { message: "User not found" };
|
|
}
|
|
|
|
const valid = await verifyPassword(currentPassword, user.passwordHash);
|
|
|
|
if (!valid) {
|
|
return {
|
|
errors: { currentPassword: ["Current password is incorrect"] },
|
|
message: "Current password is incorrect",
|
|
};
|
|
}
|
|
|
|
const newHash = await hashPassword(newPassword);
|
|
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { passwordHash: newHash },
|
|
});
|
|
|
|
return { message: "Password changed successfully", success: true };
|
|
}
|
|
|
|
export async function forgotPassword(
|
|
prevState: FormState | undefined,
|
|
formData: FormData,
|
|
): Promise<FormState> {
|
|
const email = formData.get("email") as string;
|
|
|
|
if (!email) {
|
|
return { message: "Email is required" };
|
|
}
|
|
|
|
const user = await prisma.user.findUnique({ where: { email } });
|
|
|
|
if (user) {
|
|
const token = generateToken();
|
|
|
|
await prisma.passwordResetToken.create({
|
|
data: {
|
|
token,
|
|
userId: user.id,
|
|
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
|
},
|
|
});
|
|
|
|
const resetLink = `${process.env.NEXT_PUBLIC_APP_URL}/reset-password?token=${token}`;
|
|
|
|
if (process.env.RESEND_API_KEY) {
|
|
try {
|
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
await resend.emails.send({
|
|
from: "Order Loop <onboarding@resend.dev>",
|
|
to: email,
|
|
subject: "Reset your password",
|
|
html: `<p>Click <a href="${resetLink}">here</a> to reset your password. This link expires in 1 hour.</p>`,
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to send reset email:", error);
|
|
console.log("Password reset link:", resetLink);
|
|
}
|
|
} else {
|
|
console.log("Password reset link:", resetLink);
|
|
}
|
|
}
|
|
|
|
return {
|
|
message: "If an account exists, a reset link has been sent",
|
|
success: true,
|
|
};
|
|
}
|
|
|
|
export async function resetPassword(
|
|
prevState: FormState | undefined,
|
|
formData: FormData,
|
|
): Promise<FormState> {
|
|
const token = formData.get("token") as string;
|
|
const password = formData.get("password") as string;
|
|
const confirmPassword = formData.get("confirmPassword") as string;
|
|
|
|
if (!token || !password || !confirmPassword) {
|
|
return { message: "All fields are required" };
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
return {
|
|
errors: { password: ["Password must be at least 8 characters"] },
|
|
message: "Password must be at least 8 characters",
|
|
};
|
|
}
|
|
|
|
if (password !== confirmPassword) {
|
|
return {
|
|
errors: { confirmPassword: ["Passwords do not match"] },
|
|
message: "Passwords do not match",
|
|
};
|
|
}
|
|
|
|
const resetToken = await prisma.passwordResetToken.findUnique({
|
|
where: { token },
|
|
});
|
|
|
|
if (!resetToken || resetToken.used || resetToken.expiresAt < new Date()) {
|
|
return { message: "Invalid or expired reset token" };
|
|
}
|
|
|
|
const newHash = await hashPassword(password);
|
|
|
|
await prisma.user.update({
|
|
where: { id: resetToken.userId },
|
|
data: { passwordHash: newHash },
|
|
});
|
|
|
|
await prisma.passwordResetToken.update({
|
|
where: { id: resetToken.id },
|
|
data: { used: true },
|
|
});
|
|
|
|
return {
|
|
message: "Password reset successfully. You can now log in.",
|
|
success: true,
|
|
};
|
|
}
|