Neah version calendar fix 3 debuger sec chance danger debug 7

This commit is contained in:
alma 2025-04-17 00:49:15 +02:00
parent f331080d39
commit d767ee9510
9 changed files with 550 additions and 86 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
backup oldversion for help/.DS_Store vendored Normal file

Binary file not shown.

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

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

View 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

View 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

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

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

View File

@ -1,123 +1,96 @@
import { NextAuthOptions } from 'next-auth';
import KeycloakProvider from 'next-auth/providers/keycloak';
import NextAuth, { NextAuthOptions } from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";
declare module 'next-auth' {
interface User {
id: string;
email: string;
name?: string | null;
role: string[];
first_name: string;
last_name: string;
username: string;
}
declare module "next-auth" {
interface Session {
user: {
id: string;
email: string;
name?: string | null;
role: string[];
email?: string | null;
image?: string | null;
username: string;
first_name: string;
last_name: string;
username: string;
role: 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 {
id: string;
email: string;
name?: string;
role: string[];
first_name: string;
last_name: string;
username: string;
accessToken: string;
refreshToken: string;
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 = {
providers: [
KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID!,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
issuer: process.env.KEYCLOAK_ISSUER,
authorization: {
params: {
scope: 'openid email profile',
response_type: 'code',
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 ?? [],
}
},
}),
],
debug: false,
session: {
strategy: 'jwt',
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
pages: {
signIn: '/signin',
error: '/signin',
},
callbacks: {
async jwt({ token, account, profile }) {
if (account && profile) {
if (!profile.sub) {
throw new Error('No user ID (sub) provided by Keycloak');
}
if (!account.access_token || !account.refresh_token || !account.expires_at) {
throw new Error('Missing required token fields from Keycloak');
}
token.id = profile.sub;
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;
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 (token.accessTokenExpires && Date.now() < token.accessTokenExpires) {
if (Date.now() < (token.accessTokenExpires as number)) {
return token;
}
// Token expired, try to refresh
if (!token.refreshToken) {
throw new Error('No refresh token available');
}
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',
method: "POST",
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.KEYCLOAK_CLIENT_ID!,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
refresh_token: token.refreshToken,
grant_type: "refresh_token",
client_id: clientId,
client_secret: clientSecret,
refresh_token: token.refreshToken as string,
}),
}
);
@ -125,7 +98,7 @@ export const authOptions: NextAuthOptions = {
const tokens = await response.json();
if (!response.ok) {
throw new Error('RefreshAccessTokenError');
throw new Error("RefreshAccessTokenError");
}
return {
@ -137,26 +110,31 @@ export const authOptions: NextAuthOptions = {
} catch (error) {
return {
...token,
error: 'RefreshAccessTokenError',
error: "RefreshAccessTokenError",
};
}
},
async session({ session, token }) {
if (token.error) {
throw new Error('RefreshAccessTokenError');
throw new Error("RefreshAccessTokenError");
}
session.accessToken = token.accessToken;
session.user = {
id: token.sub ?? token.id ?? '',
email: token.email ?? '',
name: token.name ?? '',
role: token.role ?? ['user'],
...session.user,
id: token.sub as string,
first_name: token.first_name ?? '',
last_name: token.last_name ?? '',
username: token.username ?? ''
username: token.username ?? '',
role: token.role ?? [],
};
return session;
},
}
},
pages: {
signIn: '/signin',
error: '/signin',
},
debug: process.env.NODE_ENV === 'development',
};