My-Weekly-ToDo-List/src/lib/auth.ts
mARTin 3657fb284f fix: reverse FAB menu order + persist iOS PWA session
- FAB menu items reordered so Task is closest to button (top),
  Search is furthest (bottom)
- auth.ts: session maxAge=30d + updateAge=1d so the JWT cookie is
  written as a persistent cookie (Max-Age header), preventing iOS
  PWA from losing the session when the app is closed/backgrounded

v1.81.5
2026-04-01 20:25:23 +02:00

153 lines
4.5 KiB
TypeScript

import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import AzureADProvider from "next-auth/providers/azure-ad";
import AppleProvider from "next-auth/providers/apple";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import CredentialsProvider from "next-auth/providers/credentials";
import { compare } from "bcryptjs";
import { prisma } from "@/lib/prisma";
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
allowDangerousEmailAccountLinking: true,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
scope: "openid profile email https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/tasks"
}
}
}),
AppleProvider({
clientId: process.env.APPLE_ID || "",
clientSecret: process.env.APPLE_SECRET || "",
allowDangerousEmailAccountLinking: true,
}),
AzureADProvider({
clientId: process.env.MICROSOFT_CLIENT_ID || "",
clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
tenantId: "common",
allowDangerousEmailAccountLinking: true,
authorization: {
params: {
prompt: "consent",
scope: "openid profile email offline_access user.read Calendars.ReadWrite Tasks.ReadWrite"
}
},
checks: ["none"],
}),
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error("Invalid credentials");
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email
}
});
if (!user || !user.passwordHash) {
throw new Error("Invalid credentials");
}
// Block login if email is not verified
if (!user.emailVerified) {
throw new Error("email_not_verified");
}
const isPasswordValid = await compare(
credentials.password,
user.passwordHash
);
if (!isPasswordValid) {
throw new Error("Invalid credentials");
}
return {
id: user.id,
email: user.email,
name: user.name || null,
image: user.image || null,
};
}
})
],
debug: true,
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // refresh cookie on each daily visit (keeps iOS PWA alive)
},
pages: {
signIn: "/auth/login",
signOut: "/auth/login",
error: "/auth/login",
},
callbacks: {
async signIn({ user, account }) {
// For OAuth providers: ensure user ID is set on the token
// allowDangerousEmailAccountLinking handles auto-linking
if (account?.provider !== "credentials" && user.email) {
const existingUser = await prisma.user.findUnique({
where: { email: user.email },
});
if (existingUser) {
// Ensure the JWT gets the correct DB user ID
user.id = existingUser.id;
}
}
return true;
},
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
// Refresh user ID from DB periodically (not on every request)
if (token.email && !token.id) {
try {
const dbUser = await prisma.user.findUnique({
where: { email: token.email as string },
select: { id: true }
});
if (dbUser) {
token.id = dbUser.id;
}
} catch (e) {
console.error("[AUTH] JWT callback DB lookup failed:", e);
}
}
return token;
},
async session({ session, token }) {
if (session.user) {
(session.user as any).id = token.id;
}
return session;
}
},
events: {
async createUser({ user }) {
// New OAuth users should see the onboarding wizard
if (user.id) {
await prisma.user.update({
where: { id: user.id },
data: { hasCompletedOnboarding: false },
});
}
},
},
secret: process.env.NEXTAUTH_SECRET
};