hp-prod-tracker/src/lib/auth.ts
Vadym Samoilenko bf0bee9c28 Fix SSO: use /api/auth (no basePath) as OAuth redirect_uri
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>
2026-04-15 15:54:37 +01:00

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