initial push

This commit is contained in:
Marcelo Dares
2026-03-15 15:03:56 +01:00
parent d48b9d5352
commit 65aaf9275e
146 changed files with 70245 additions and 100 deletions

View 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" });
}
}

View 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;
}

View 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" });
}
}

View 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" });
}
}