Initial commit
This commit is contained in:
17
app/api/auth/[...route]/route.ts
Normal file
17
app/api/auth/[...route]/route.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getAuth } from "@/lib/auth";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
async function handler(req: NextRequest) {
|
||||
const auth = await getAuth();
|
||||
return auth.handler(req);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return handler(req);
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return handler(req);
|
||||
}
|
||||
16
app/api/auth/captcha/route.ts
Normal file
16
app/api/auth/captcha/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { generateCaptcha } from "../../../../lib/captcha";
|
||||
import { ok } from "../../../../lib/http";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { id, code } = generateCaptcha(Date.now().toString());
|
||||
|
||||
// Log the code to console for dev/demo purposes
|
||||
console.log(`CAPTCHA Code: ${code}`);
|
||||
|
||||
return ok({
|
||||
captchaId: id,
|
||||
captchaCode: code, // Return the code to display to user
|
||||
message: "CAPTCHA code generated. Enter it below to continue.",
|
||||
});
|
||||
}
|
||||
64
app/api/auth/change-password/route.ts
Normal file
64
app/api/auth/change-password/route.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getSession } from "../../../../lib/auth/session";
|
||||
import { getPrisma } from "../../../../lib/db";
|
||||
import { hashPassword, verifyPassword } from "../../../../lib/auth/password";
|
||||
import { isStrongPassword } from "../../../../lib/auth/validation";
|
||||
import { ok, fail } from "../../../../lib/http";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const Body = z.object({
|
||||
currentPassword: z.string().min(1),
|
||||
newPassword: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await getSession();
|
||||
if (!session) return fail(new Error("Unauthorized"), { status: 401 });
|
||||
|
||||
const prisma = await getPrisma();
|
||||
if (!prisma) return fail(new Error("Database not configured"), { status: 503, isAdmin: session.role === "ADMIN" });
|
||||
|
||||
const parsed = Body.safeParse(await req.json().catch(() => ({})));
|
||||
if (!parsed.success) return fail(new Error("Invalid input"));
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.sub },
|
||||
include: { credential: true }
|
||||
});
|
||||
if (!user || !user.credential) return fail(new Error("Invalid user or no password set"));
|
||||
|
||||
if (parsed.data.newPassword !== parsed.data.confirmPassword) {
|
||||
return fail(new Error("Passwords do not match"));
|
||||
}
|
||||
if (!isStrongPassword(parsed.data.newPassword)) {
|
||||
return fail(new Error("Password is not strong enough"));
|
||||
}
|
||||
|
||||
// 🔥 Only verify current password if NOT forced reset
|
||||
if (!user.forcePasswordReset) {
|
||||
const okPw = await verifyPassword(
|
||||
parsed.data.currentPassword,
|
||||
user.credential.password
|
||||
);
|
||||
if (!okPw) return fail(new Error("Invalid password"));
|
||||
}
|
||||
|
||||
const newHash = await hashPassword(parsed.data.newPassword);
|
||||
await prisma.credential.update({
|
||||
where: { userId: user.id },
|
||||
data: { password: newHash },
|
||||
});
|
||||
|
||||
// Clear force password reset flag if it was set
|
||||
if (user.forcePasswordReset) {
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { forcePasswordReset: false },
|
||||
});
|
||||
}
|
||||
|
||||
return ok({ message: "Password updated" });
|
||||
}
|
||||
108
app/api/auth/login/route.ts
Normal file
108
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getPrisma } from "../../../../lib/db";
|
||||
import { verifyPassword } from "../../../../lib/auth/password";
|
||||
import { signSession } from "../../../../lib/auth/jwt";
|
||||
import { verifyCaptcha } from "../../../../lib/captcha";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const Body = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
captchaId: z.string().optional(),
|
||||
captchaCode: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const prisma = await getPrisma();
|
||||
const body = Body.safeParse(await req.json().catch(() => ({})));
|
||||
|
||||
if (!body.success) {
|
||||
return NextResponse.json({ ok: false, message: "Invalid input" }, { status: 400 });
|
||||
}
|
||||
if (!prisma) {
|
||||
return NextResponse.json({ ok: false, message: "Database not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
// Validate CAPTCHA if provided
|
||||
if (body.data.captchaId && body.data.captchaCode) {
|
||||
const captchaResult = verifyCaptcha(body.data.captchaId, body.data.captchaCode);
|
||||
if (!captchaResult.success) {
|
||||
return NextResponse.json({ ok: false, message: captchaResult.error || "CAPTCHA verification failed. Please try again." }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const email = body.data.email.toLowerCase();
|
||||
const password = body.data.password;
|
||||
|
||||
console.log("[LOGIN] Login attempt for:", email);
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: { credential: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.log("[LOGIN] User not found:", email);
|
||||
return NextResponse.json({ ok: false, message: "Invalid email or password" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!user.credential || !user.credential.password) {
|
||||
console.log("[LOGIN] No password credential for user:", email);
|
||||
return NextResponse.json({ ok: false, message: "Please use Google sign-in for this account" }, { status: 401 });
|
||||
}
|
||||
|
||||
const valid = await verifyPassword(password, user.credential.password);
|
||||
|
||||
if (!valid) {
|
||||
console.log("[LOGIN] Invalid password for:", email);
|
||||
return NextResponse.json({ ok: false, message: "Invalid email or password" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
console.log("[LOGIN] Inactive account:", email);
|
||||
return NextResponse.json({ ok: false, message: "Account is inactive. Please contact support." }, { status: 403 });
|
||||
}
|
||||
|
||||
// Create session token
|
||||
const token = await signSession({
|
||||
sub: user.id,
|
||||
role: user.role as "ADMIN" | "USER",
|
||||
email: user.email,
|
||||
forcePasswordReset: user.forcePasswordReset || false,
|
||||
});
|
||||
|
||||
console.log("[LOGIN] Session created for:", email, "with role:", user.role);
|
||||
|
||||
// Set cookie
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set("ep_session", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: "/",
|
||||
});
|
||||
|
||||
console.log("[LOGIN] Login successful for:", email);
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
emailVerified: user.emailVerified,
|
||||
forcePasswordReset: user.forcePasswordReset,
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.error("[LOGIN] Error:", e);
|
||||
return NextResponse.json({ ok: false, message: "Server error: Unable to process request" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
23
app/api/auth/logout/route.ts
Normal file
23
app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { SESSION_COOKIE } from "../../../../lib/auth/session";
|
||||
import { ok } from "../../../../lib/http";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
console.log("[LOGOUT] User logout");
|
||||
|
||||
const res = ok({ message: "Logged out successfully" });
|
||||
|
||||
// Clear the session cookie
|
||||
res.cookies.set(SESSION_COOKIE, "", {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
console.log("[LOGOUT] Session cleared");
|
||||
return res;
|
||||
}
|
||||
27
app/api/auth/me/route.ts
Normal file
27
app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ok } from "../../../../lib/http";
|
||||
import { getSession } from "../../../../lib/auth/session";
|
||||
import { getPrisma } from "../../../../lib/db";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSession();
|
||||
if (!session) return ok({ session: null });
|
||||
|
||||
const prisma = await getPrisma();
|
||||
if (!prisma) return ok({ session });
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.sub } });
|
||||
return ok({
|
||||
session,
|
||||
user: user
|
||||
? {
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
avatarUrl: user.image,
|
||||
role: user.role,
|
||||
email: user.email,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
124
app/api/auth/register/route.ts
Normal file
124
app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getPrisma } from "../../../../lib/db";
|
||||
import { hashPassword } from "../../../../lib/auth/password";
|
||||
import { isStrongPassword, sanitizeText } from "../../../../lib/auth/validation";
|
||||
import { ok, fail } from "../../../../lib/http";
|
||||
import { loadSystemConfig } from "../../../../lib/system-config";
|
||||
import { sendEmail } from "../../../../lib/email";
|
||||
import { verifyCaptcha } from "../../../../lib/captcha";
|
||||
import crypto from "crypto";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const Body = z.object({
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
gender: z.string().optional(),
|
||||
dob: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
captchaId: z.string().optional(),
|
||||
captchaCode: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const cfg = await loadSystemConfig();
|
||||
const prisma = await getPrisma();
|
||||
const body = Body.safeParse(await req.json().catch(() => ({})));
|
||||
|
||||
if (!body.success) return fail(new Error("Invalid input"));
|
||||
|
||||
if (!prisma) return fail(new Error("Database not configured"), { status: 503 });
|
||||
|
||||
// Validate CAPTCHA if provided
|
||||
if (body.data.captchaId && body.data.captchaCode) {
|
||||
const captchaResult = verifyCaptcha(body.data.captchaId, body.data.captchaCode);
|
||||
if (!captchaResult.success) {
|
||||
return fail(new Error(captchaResult.error || "CAPTCHA verification failed. Please try again."));
|
||||
}
|
||||
}
|
||||
|
||||
const email = body.data.email.toLowerCase();
|
||||
const password = body.data.password;
|
||||
const confirmPassword = body.data.confirmPassword;
|
||||
|
||||
if (password !== confirmPassword) return fail(new Error("Passwords do not match"));
|
||||
if (!isStrongPassword(password)) return fail(new Error("Password is not strong enough"));
|
||||
|
||||
// Validate birth date if provided
|
||||
if (body.data.dob) {
|
||||
const birthDate = new Date(body.data.dob);
|
||||
const today = new Date();
|
||||
const minDate = new Date(today.getFullYear() - 100, today.getMonth(), today.getDate());
|
||||
const maxDate = new Date(today.getFullYear() - 18, today.getMonth(), today.getDate());
|
||||
if (birthDate < minDate || birthDate > maxDate) {
|
||||
return fail(new Error("Birth date must be between 18 and 100 years old"));
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
try {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
role: "USER",
|
||||
name: `${body.data.firstName} ${body.data.lastName}`,
|
||||
firstName: sanitizeText(body.data.firstName),
|
||||
lastName: sanitizeText(body.data.lastName),
|
||||
gender: body.data.gender ? sanitizeText(body.data.gender) : null,
|
||||
dob: body.data.dob ? new Date(body.data.dob) : null,
|
||||
address: body.data.address ? sanitizeText(body.data.address) : null,
|
||||
// If email is enabled, require verification; otherwise mark as verified
|
||||
emailVerified: !cfg.email?.enabled,
|
||||
forcePasswordReset: false,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create credential for email/password authentication
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
password: passwordHash,
|
||||
},
|
||||
});
|
||||
|
||||
// Only create verification token and send email if email is enabled
|
||||
if (cfg.email?.enabled) {
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
await prisma.verification.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
identifier: email,
|
||||
value: token,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
const baseUrl = process.env.APP_BASE_URL || "http://localhost:3000";
|
||||
const link = `${baseUrl}/auth/verify?token=${token}`;
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: "Verify your email",
|
||||
html: `<p>Welcome! Please verify your email:</p><p><a href="${link}">Verify Email</a></p>`,
|
||||
});
|
||||
return ok({ message: "Account created. Please check your email to verify your account." });
|
||||
} else {
|
||||
return ok({ message: "Account created successfully. You can now log in." });
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Check if the error is due to unique constraint on email
|
||||
if (e.code === 'P2002' && e.meta?.target?.includes('email')) {
|
||||
return fail(new Error("This email is already taken"));
|
||||
}
|
||||
console.error("Registration error:", e);
|
||||
return fail(new Error("Account creation failed. Please try again or contact support."));
|
||||
}
|
||||
|
||||
return ok({ message: "Account created. Please check your email to verify your account." });
|
||||
}
|
||||
Reference in New Issue
Block a user