next-auth v5 beta.30 cannot reliably pass the /hp-prod-tracker prefix through OAuth redirect_uri — redirectProxyUrl is silently ignored. Instead: AUTH_URL=https://…/api/auth (matches basePath exactly), Auth.js sends consistent redirect_uri in both authorization and token exchange, Apache proxies /api/auth → :3001 before the OliVAS /api/ rule. Azure must have https://optical-dev.oliver.solutions/api/auth/callback/microsoft-entra-id registered. Server .env: AUTH_URL=https://optical-dev.oliver.solutions/api/auth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
import NextAuth from "next-auth";
|
|
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id";
|
|
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
import { prisma } from "@/lib/prisma";
|
|
import type { Role } from "@/generated/prisma/client";
|
|
|
|
// AUTH_URL = https://optical-dev.oliver.solutions/api/auth
|
|
//
|
|
// We intentionally use the /api/auth path WITHOUT the Next.js /hp-prod-tracker
|
|
// basePath. next-auth v5 beta cannot reliably pass the app basePath through the
|
|
// OAuth redirect_uri — redirectProxyUrl is silently ignored in beta.30.
|
|
//
|
|
// Instead: AUTH_URL matches basePath exactly (/api/auth) so there is no
|
|
// env-url-basepath-mismatch, Auth.js sends a consistent redirect_uri in both
|
|
// the authorization and token exchange requests, and Apache proxies /api/auth
|
|
// directly to the container (see apache/hp-prod-tracker.conf).
|
|
|
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
basePath: "/api/auth",
|
|
adapter: PrismaAdapter(prisma),
|
|
providers: [
|
|
MicrosoftEntraID({
|
|
clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID,
|
|
clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET,
|
|
issuer: `https://login.microsoftonline.com/${process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID}/v2.0`,
|
|
// Safe for Entra ID — Microsoft verifies organizational emails.
|
|
// Required to link SSO accounts to pre-seeded User records by email match.
|
|
allowDangerousEmailAccountLinking: true,
|
|
}),
|
|
],
|
|
session: {
|
|
strategy: "database",
|
|
},
|
|
events: {
|
|
async signIn({ user }) {
|
|
if (!user.id || !user.email) return;
|
|
|
|
// Auto-assign organization by email domain match
|
|
const dbUser = await prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
select: { organizationId: true, email: true },
|
|
});
|
|
|
|
if (dbUser?.organizationId || !dbUser?.email) return;
|
|
|
|
const domain = dbUser.email.split("@")[1];
|
|
if (!domain) return;
|
|
|
|
const org = await prisma.organization.findFirst({
|
|
where: { domain },
|
|
});
|
|
|
|
if (org) {
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { organizationId: org.id },
|
|
});
|
|
}
|
|
},
|
|
},
|
|
callbacks: {
|
|
async session({ session, user }) {
|
|
// Fetch user with role and org from database
|
|
const dbUser = await prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
select: { role: true, organizationId: true },
|
|
});
|
|
|
|
if (dbUser) {
|
|
session.user.id = user.id;
|
|
session.user.role = dbUser.role;
|
|
session.user.organizationId = dbUser.organizationId;
|
|
}
|
|
|
|
return session;
|
|
},
|
|
},
|
|
pages: {
|
|
signIn: "/hp-prod-tracker/login",
|
|
},
|
|
});
|