Neah version calendar fix 3 debuger sec chance danger debug 7
This commit is contained in:
parent
f331080d39
commit
d767ee9510
BIN
backup oldversion for help/.DS_Store
vendored
Normal file
BIN
backup oldversion for help/.DS_Store
vendored
Normal file
Binary file not shown.
166
backup oldversion for help/api-calendar/route.ts
Normal file
166
backup oldversion for help/api-calendar/route.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getServerSession } from "next-auth/next";
|
||||||
|
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// GET events
|
||||||
|
export async function GET(req: Request) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const start = searchParams.get("start");
|
||||||
|
const end = searchParams.get("end");
|
||||||
|
|
||||||
|
// First get all calendars for the user
|
||||||
|
const calendars = await prisma.calendar.findMany({
|
||||||
|
where: {
|
||||||
|
userId: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then get events with calendar information
|
||||||
|
const events = await prisma.event.findMany({
|
||||||
|
where: {
|
||||||
|
calendarId: {
|
||||||
|
in: calendars.map(cal => cal.id)
|
||||||
|
},
|
||||||
|
...(start && end
|
||||||
|
? {
|
||||||
|
start: {
|
||||||
|
gte: new Date(start),
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
lte: new Date(end),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
calendar: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
start: "asc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map the events to include calendar color and name
|
||||||
|
const eventsWithCalendarInfo = events.map(event => ({
|
||||||
|
...event,
|
||||||
|
calendarColor: event.calendar.color,
|
||||||
|
calendarName: event.calendar.name,
|
||||||
|
calendar: undefined, // Remove the full calendar object
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json(eventsWithCalendarInfo);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching events:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Erreur lors du chargement des événements" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST new event
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Non autorisé" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await req.json();
|
||||||
|
const { title, description, start, end, location, calendarId } = data;
|
||||||
|
|
||||||
|
const event = await prisma.event.create({
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
start: new Date(start),
|
||||||
|
end: new Date(end),
|
||||||
|
isAllDay: data.allDay || false,
|
||||||
|
location: location || null,
|
||||||
|
calendarId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating event:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Erreur lors de la création de l'événement" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT update event
|
||||||
|
export async function PUT(req: Request) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const { id, ...data } = body;
|
||||||
|
|
||||||
|
const event = await prisma.event.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating event:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Error updating event" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE event
|
||||||
|
export async function DELETE(req: Request) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const id = searchParams.get("id");
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Event ID is required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.event.delete({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting event:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Error deleting event" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
backup oldversion for help/flow.tsx
Normal file
129
backup oldversion for help/flow.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface Task {
|
||||||
|
id: string;
|
||||||
|
headline: string;
|
||||||
|
projectName: string;
|
||||||
|
projectId: number;
|
||||||
|
status: number;
|
||||||
|
dueDate: string | null;
|
||||||
|
milestone: string | null;
|
||||||
|
details: string | null;
|
||||||
|
createdOn: string;
|
||||||
|
editedOn: string | null;
|
||||||
|
assignedTo: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Flow() {
|
||||||
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const getStatusLabel = (status: number): string => {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return 'New';
|
||||||
|
case 2:
|
||||||
|
return 'In Progress';
|
||||||
|
case 3:
|
||||||
|
return 'Done';
|
||||||
|
case 4:
|
||||||
|
return 'In Progress';
|
||||||
|
case 5:
|
||||||
|
return 'Done';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: number): string => {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
|
||||||
|
case 2:
|
||||||
|
case 4:
|
||||||
|
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
|
||||||
|
case 3:
|
||||||
|
case 5:
|
||||||
|
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTasks = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/leantime/tasks');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch tasks');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.tasks && Array.isArray(data.tasks)) {
|
||||||
|
// Sort tasks by creation date (oldest first)
|
||||||
|
const sortedTasks = data.tasks.sort((a: Task, b: Task) => {
|
||||||
|
const dateA = new Date(a.createdOn).getTime();
|
||||||
|
const dateB = new Date(b.createdOn).getTime();
|
||||||
|
return dateA - dateB;
|
||||||
|
});
|
||||||
|
setTasks(sortedTasks);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid tasks data format:', data);
|
||||||
|
setError('Invalid tasks data format');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching tasks:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTasks();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error: {error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.length === 0) {
|
||||||
|
return <div>No tasks found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{task.headline}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{task.projectName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(
|
||||||
|
task.status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{getStatusLabel(task.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
backup oldversion for help/prisma-news.ts
Normal file
13
backup oldversion for help/prisma-news.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client/news'
|
||||||
|
|
||||||
|
const globalForPrisma = globalThis as unknown as {
|
||||||
|
prismaNews: PrismaClient | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prismaNews =
|
||||||
|
globalForPrisma.prismaNews ||
|
||||||
|
new PrismaClient({
|
||||||
|
log: ['query'],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prismaNews = prismaNews
|
||||||
14
backup oldversion for help/prisma.ts
Normal file
14
backup oldversion for help/prisma.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// front/lib/prisma.ts
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
const globalForPrisma = globalThis as unknown as {
|
||||||
|
prisma: PrismaClient | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prisma =
|
||||||
|
globalForPrisma.prisma ||
|
||||||
|
new PrismaClient({
|
||||||
|
log: ['query'],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||||
144
backup oldversion for help/route.ts
Normal file
144
backup oldversion for help/route.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||||
|
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
image?: string | null;
|
||||||
|
username: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
role: string[];
|
||||||
|
};
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JWT {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
accessTokenExpires: number;
|
||||||
|
role: string[];
|
||||||
|
username: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequiredEnvVar(name: string): string {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing required environment variable: ${name}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
providers: [
|
||||||
|
KeycloakProvider({
|
||||||
|
clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"),
|
||||||
|
clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"),
|
||||||
|
issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"),
|
||||||
|
profile(profile) {
|
||||||
|
return {
|
||||||
|
id: profile.sub,
|
||||||
|
name: profile.name ?? profile.preferred_username,
|
||||||
|
email: profile.email,
|
||||||
|
first_name: profile.given_name ?? '',
|
||||||
|
last_name: profile.family_name ?? '',
|
||||||
|
username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '',
|
||||||
|
role: profile.groups ?? [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, account, profile }) {
|
||||||
|
if (account && profile) {
|
||||||
|
token.accessToken = account.access_token;
|
||||||
|
token.refreshToken = account.refresh_token;
|
||||||
|
token.accessTokenExpires = account.expires_at! * 1000;
|
||||||
|
token.role = (profile as any).groups ?? [];
|
||||||
|
token.username = (profile as any).preferred_username ?? profile.email?.split('@')[0] ?? '';
|
||||||
|
token.first_name = (profile as any).given_name ?? '';
|
||||||
|
token.last_name = (profile as any).family_name ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return previous token if not expired
|
||||||
|
if (Date.now() < (token.accessTokenExpires as number)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clientId = getRequiredEnvVar("KEYCLOAK_CLIENT_ID");
|
||||||
|
const clientSecret = getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
refresh_token: token.refreshToken as string,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokens = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("RefreshAccessTokenError");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
accessToken: tokens.access_token,
|
||||||
|
refreshToken: tokens.refresh_token ?? token.refreshToken,
|
||||||
|
accessTokenExpires: Date.now() + tokens.expires_in * 1000,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
error: "RefreshAccessTokenError",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
if (token.error) {
|
||||||
|
throw new Error("RefreshAccessTokenError");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.accessToken = token.accessToken;
|
||||||
|
session.user = {
|
||||||
|
...session.user,
|
||||||
|
id: token.sub as string,
|
||||||
|
first_name: token.first_name ?? '',
|
||||||
|
last_name: token.last_name ?? '',
|
||||||
|
username: token.username ?? '',
|
||||||
|
role: token.role ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: '/signin',
|
||||||
|
error: '/signin',
|
||||||
|
},
|
||||||
|
debug: process.env.NODE_ENV === 'development',
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = NextAuth(authOptions);
|
||||||
|
export { handler as GET, handler as POST };
|
||||||
|
|
||||||
20
backup oldversion for help/utils.ts
Normal file
20
backup oldversion for help/utils.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(dateString: string): string {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return new Intl.DateTimeFormat('en-US', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
}).format(date);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
148
lib/auth.ts
148
lib/auth.ts
@ -1,123 +1,96 @@
|
|||||||
import { NextAuthOptions } from 'next-auth';
|
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||||
import KeycloakProvider from 'next-auth/providers/keycloak';
|
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||||
|
|
||||||
declare module 'next-auth' {
|
declare module "next-auth" {
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name?: string | null;
|
|
||||||
role: string[];
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
interface Session {
|
interface Session {
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
role: string[];
|
email?: string | null;
|
||||||
|
image?: string | null;
|
||||||
|
username: string;
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
username: string;
|
role: string[];
|
||||||
};
|
};
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
|
||||||
}
|
}
|
||||||
interface Profile {
|
|
||||||
sub?: string;
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
roles?: string[];
|
|
||||||
given_name?: string;
|
|
||||||
family_name?: string;
|
|
||||||
preferred_username?: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'next-auth/jwt' {
|
|
||||||
interface JWT {
|
interface JWT {
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name?: string;
|
|
||||||
role: string[];
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
username: string;
|
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
accessTokenExpires: number;
|
accessTokenExpires: number;
|
||||||
error?: string;
|
role: string[];
|
||||||
|
username: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRequiredEnvVar(name: string): string {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing required environment variable: ${name}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export const authOptions: NextAuthOptions = {
|
export const authOptions: NextAuthOptions = {
|
||||||
providers: [
|
providers: [
|
||||||
KeycloakProvider({
|
KeycloakProvider({
|
||||||
clientId: process.env.KEYCLOAK_CLIENT_ID!,
|
clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"),
|
||||||
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"),
|
||||||
issuer: process.env.KEYCLOAK_ISSUER,
|
issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"),
|
||||||
authorization: {
|
profile(profile) {
|
||||||
params: {
|
return {
|
||||||
scope: 'openid email profile',
|
id: profile.sub,
|
||||||
response_type: 'code',
|
name: profile.name ?? profile.preferred_username,
|
||||||
|
email: profile.email,
|
||||||
|
first_name: profile.given_name ?? '',
|
||||||
|
last_name: profile.family_name ?? '',
|
||||||
|
username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '',
|
||||||
|
role: profile.groups ?? [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
debug: false,
|
|
||||||
session: {
|
session: {
|
||||||
strategy: 'jwt',
|
strategy: "jwt",
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
},
|
},
|
||||||
pages: {
|
|
||||||
signIn: '/signin',
|
|
||||||
error: '/signin',
|
|
||||||
},
|
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account, profile }) {
|
async jwt({ token, account, profile }) {
|
||||||
if (account && profile) {
|
if (account && profile) {
|
||||||
if (!profile.sub) {
|
token.accessToken = account.access_token!;
|
||||||
throw new Error('No user ID (sub) provided by Keycloak');
|
token.refreshToken = account.refresh_token!;
|
||||||
}
|
token.accessTokenExpires = account.expires_at! * 1000;
|
||||||
if (!account.access_token || !account.refresh_token || !account.expires_at) {
|
token.role = (profile as any).groups ?? [];
|
||||||
throw new Error('Missing required token fields from Keycloak');
|
token.username = (profile as any).preferred_username ?? profile.email?.split('@')[0] ?? '';
|
||||||
}
|
token.first_name = (profile as any).given_name ?? '';
|
||||||
token.id = profile.sub;
|
token.last_name = (profile as any).family_name ?? '';
|
||||||
token.email = profile.email || '';
|
|
||||||
token.name = profile.name;
|
|
||||||
token.role = profile.roles || ['user'];
|
|
||||||
token.first_name = profile.given_name || '';
|
|
||||||
token.last_name = profile.family_name || '';
|
|
||||||
token.username = profile.preferred_username || '';
|
|
||||||
token.accessToken = account.access_token;
|
|
||||||
token.refreshToken = account.refresh_token;
|
|
||||||
token.accessTokenExpires = account.expires_at * 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return previous token if not expired
|
// Return previous token if not expired
|
||||||
if (token.accessTokenExpires && Date.now() < token.accessTokenExpires) {
|
if (Date.now() < (token.accessTokenExpires as number)) {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token expired, try to refresh
|
|
||||||
if (!token.refreshToken) {
|
|
||||||
throw new Error('No refresh token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const clientId = getRequiredEnvVar("KEYCLOAK_CLIENT_ID");
|
||||||
|
const clientSecret = getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET");
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
|
`${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
grant_type: 'refresh_token',
|
grant_type: "refresh_token",
|
||||||
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
client_id: clientId,
|
||||||
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
client_secret: clientSecret,
|
||||||
refresh_token: token.refreshToken,
|
refresh_token: token.refreshToken as string,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -125,7 +98,7 @@ export const authOptions: NextAuthOptions = {
|
|||||||
const tokens = await response.json();
|
const tokens = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('RefreshAccessTokenError');
|
throw new Error("RefreshAccessTokenError");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -137,26 +110,31 @@ export const authOptions: NextAuthOptions = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...token,
|
...token,
|
||||||
error: 'RefreshAccessTokenError',
|
error: "RefreshAccessTokenError",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (token.error) {
|
if (token.error) {
|
||||||
throw new Error('RefreshAccessTokenError');
|
throw new Error("RefreshAccessTokenError");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.accessToken = token.accessToken;
|
||||||
session.user = {
|
session.user = {
|
||||||
id: token.sub ?? token.id ?? '',
|
...session.user,
|
||||||
email: token.email ?? '',
|
id: token.sub as string,
|
||||||
name: token.name ?? '',
|
|
||||||
role: token.role ?? ['user'],
|
|
||||||
first_name: token.first_name ?? '',
|
first_name: token.first_name ?? '',
|
||||||
last_name: token.last_name ?? '',
|
last_name: token.last_name ?? '',
|
||||||
username: token.username ?? ''
|
username: token.username ?? '',
|
||||||
|
role: token.role ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: '/signin',
|
||||||
|
error: '/signin',
|
||||||
|
},
|
||||||
|
debug: process.env.NODE_ENV === 'development',
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user