initial push
This commit is contained in:
87
src/app/api/auth/login/route.ts
Normal file
87
src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isAdminIdentity } from "@/lib/auth/admin";
|
||||
import { createSessionToken, setSessionCookie } from "@/lib/auth/session";
|
||||
import { verifyPassword } from "@/lib/auth/password";
|
||||
import { issueEmailVerificationToken, sendEmailVerificationLink } from "@/lib/auth/verification";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { buildAppUrl } from "@/lib/http/url";
|
||||
|
||||
function redirectTo(request: Request, path: string, params: Record<string, string>) {
|
||||
return NextResponse.redirect(buildAppUrl(request, path, params));
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const formData = await request.formData();
|
||||
|
||||
const email = typeof formData.get("email") === "string" ? formData.get("email")!.toString().trim().toLowerCase() : "";
|
||||
const password = typeof formData.get("password") === "string" ? formData.get("password")!.toString() : "";
|
||||
|
||||
if (!email || !password) {
|
||||
return redirectTo(request, "/login", { error: "invalid_credentials" });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
if (!user) {
|
||||
return redirectTo(request, "/login", { error: "invalid_credentials" });
|
||||
}
|
||||
|
||||
const passwordMatches = await verifyPassword(password, user.passwordHash);
|
||||
|
||||
if (!passwordMatches) {
|
||||
return redirectTo(request, "/login", { error: "invalid_credentials" });
|
||||
}
|
||||
|
||||
if (!user.emailVerifiedAt) {
|
||||
const { token } = await issueEmailVerificationToken(user.id);
|
||||
const sendResult = await sendEmailVerificationLink(request, email, token);
|
||||
|
||||
const verifyParams: Record<string, string> = {
|
||||
email,
|
||||
sent: sendResult.sent ? "1" : "0",
|
||||
unverified: "1",
|
||||
};
|
||||
|
||||
if (!sendResult.sent) {
|
||||
verifyParams.error = "email_delivery_failed";
|
||||
}
|
||||
|
||||
return redirectTo(request, "/verify", verifyParams);
|
||||
}
|
||||
|
||||
let onboardingCompleted = false;
|
||||
|
||||
try {
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: { userId: user.id },
|
||||
select: { onboardingCompletedAt: true },
|
||||
});
|
||||
|
||||
onboardingCompleted = Boolean(organization?.onboardingCompletedAt);
|
||||
} catch {
|
||||
// Backward compatibility for databases that have not applied onboarding v2 migration yet.
|
||||
const legacyOrganization = await prisma.organization.findUnique({
|
||||
where: { userId: user.id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
onboardingCompleted = Boolean(legacyOrganization);
|
||||
}
|
||||
|
||||
const targetPath = isAdminIdentity(user.email, user.role)
|
||||
? "/admin"
|
||||
: onboardingCompleted
|
||||
? "/dashboard"
|
||||
: "/onboarding";
|
||||
|
||||
const response = NextResponse.redirect(buildAppUrl(request, targetPath));
|
||||
const token = createSessionToken(user.id, user.email);
|
||||
|
||||
setSessionCookie(response, token);
|
||||
|
||||
return response;
|
||||
} catch {
|
||||
return redirectTo(request, "/login", { error: "server_error" });
|
||||
}
|
||||
}
|
||||
11
src/app/api/auth/logout/route.ts
Normal file
11
src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { clearSessionCookie } from "@/lib/auth/session";
|
||||
import { buildAppUrl } from "@/lib/http/url";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const response = NextResponse.redirect(buildAppUrl(request, "/login", { logged_out: "1" }));
|
||||
|
||||
clearSessionCookie(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
75
src/app/api/auth/register/route.ts
Normal file
75
src/app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { UserRole } from "@prisma/client";
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConfiguredAdminEmail } from "@/lib/auth/admin";
|
||||
import { hashPassword } from "@/lib/auth/password";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { issueEmailVerificationToken, sendEmailVerificationLink } from "@/lib/auth/verification";
|
||||
import { buildAppUrl } from "@/lib/http/url";
|
||||
|
||||
function redirectTo(request: Request, path: string, params: Record<string, string>) {
|
||||
return NextResponse.redirect(buildAppUrl(request, path, params));
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const formData = await request.formData();
|
||||
|
||||
const name = typeof formData.get("name") === "string" ? formData.get("name")!.toString().trim() : "";
|
||||
const email = typeof formData.get("email") === "string" ? formData.get("email")!.toString().trim().toLowerCase() : "";
|
||||
const password = typeof formData.get("password") === "string" ? formData.get("password")!.toString() : "";
|
||||
|
||||
if (!email || !password || password.length < 8) {
|
||||
return redirectTo(request, "/register", { error: "invalid_input" });
|
||||
}
|
||||
|
||||
try {
|
||||
const existingUser = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
if (existingUser) {
|
||||
if (!existingUser.emailVerifiedAt) {
|
||||
const { token } = await issueEmailVerificationToken(existingUser.id);
|
||||
const sendResult = await sendEmailVerificationLink(request, email, token);
|
||||
|
||||
const verifyParams: Record<string, string> = {
|
||||
email,
|
||||
sent: sendResult.sent ? "1" : "0",
|
||||
unverified: "1",
|
||||
};
|
||||
|
||||
if (!sendResult.sent) {
|
||||
verifyParams.error = "email_delivery_failed";
|
||||
}
|
||||
|
||||
return redirectTo(request, "/verify", verifyParams);
|
||||
}
|
||||
|
||||
return redirectTo(request, "/register", { error: "email_in_use" });
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: name || null,
|
||||
email,
|
||||
passwordHash,
|
||||
role: isConfiguredAdminEmail(email) ? UserRole.ADMIN : UserRole.USER,
|
||||
},
|
||||
});
|
||||
|
||||
const { token } = await issueEmailVerificationToken(user.id);
|
||||
const sendResult = await sendEmailVerificationLink(request, email, token);
|
||||
|
||||
const verifyParams: Record<string, string> = {
|
||||
email,
|
||||
sent: sendResult.sent ? "1" : "0",
|
||||
};
|
||||
|
||||
if (!sendResult.sent) {
|
||||
verifyParams.error = "email_delivery_failed";
|
||||
}
|
||||
|
||||
return redirectTo(request, "/verify", verifyParams);
|
||||
} catch {
|
||||
return redirectTo(request, "/register", { error: "server_error" });
|
||||
}
|
||||
}
|
||||
41
src/app/api/auth/resend/route.ts
Normal file
41
src/app/api/auth/resend/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { issueEmailVerificationToken, sendEmailVerificationLink } from "@/lib/auth/verification";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { buildAppUrl } from "@/lib/http/url";
|
||||
|
||||
function redirectTo(request: Request, path: string, params: Record<string, string>) {
|
||||
return NextResponse.redirect(buildAppUrl(request, path, params));
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const formData = await request.formData();
|
||||
const email = typeof formData.get("email") === "string" ? formData.get("email")!.toString().trim().toLowerCase() : "";
|
||||
|
||||
if (!email) {
|
||||
return redirectTo(request, "/verify", { error: "missing_email" });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
if (user && !user.emailVerifiedAt) {
|
||||
const { token } = await issueEmailVerificationToken(user.id);
|
||||
const sendResult = await sendEmailVerificationLink(request, email, token);
|
||||
|
||||
const verifyParams: Record<string, string> = {
|
||||
email,
|
||||
sent: sendResult.sent ? "1" : "0",
|
||||
};
|
||||
|
||||
if (!sendResult.sent) {
|
||||
verifyParams.error = "email_delivery_failed";
|
||||
}
|
||||
|
||||
return redirectTo(request, "/verify", verifyParams);
|
||||
}
|
||||
|
||||
return redirectTo(request, "/verify", { email, sent: "1" });
|
||||
} catch {
|
||||
return redirectTo(request, "/verify", { email, error: "server_error" });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user