Compare commits

..

32 Commits
v1 ... main

Author SHA1 Message Date
5923e811b0 database wf 2025-04-17 12:10:02 +02:00
0158c6e696 Neah version calendar fix 3 debuger sec chance danger debug 10 2025-04-17 00:55:29 +02:00
0c24fd0c95 Neah version calendar fix 3 debuger sec chance danger debug 9 2025-04-17 00:53:09 +02:00
ae15fa7678 Neah version calendar fix 3 debuger sec chance danger debug 8 2025-04-17 00:51:57 +02:00
d767ee9510 Neah version calendar fix 3 debuger sec chance danger debug 7 2025-04-17 00:49:15 +02:00
f331080d39 Neah version calendar fix 3 debuger sec chance danger debug 66 2025-04-17 00:38:56 +02:00
f22f9490e4 Neah version calendar fix 3 debuger sec chance danger debug 5 2025-04-17 00:36:59 +02:00
16219fc250 Neah version calendar fix 3 debuger sec chance danger debug 4 2025-04-17 00:34:13 +02:00
bef5ed1946 Neah version calendar fix 3 debuger sec chance danger debug 3 2025-04-17 00:28:00 +02:00
b5855e5a84 Neah version calendar fix 3 debuger sec chance danger debug 2 2025-04-17 00:25:13 +02:00
9eef481490 Neah version calendar fix 3 debuger sec chance danger debug 2025-04-17 00:16:34 +02:00
7586e17524 Neah version calendar fix 3 debuger sec chance danger 2 2025-04-17 00:13:38 +02:00
f89c25d9a7 Neah version calendar fix 3 debuger sec chance danger 2025-04-17 00:07:17 +02:00
2b147c85a2 Neah version calendar fix 3 debuger sec chance 3 2025-04-17 00:02:15 +02:00
3bdc2745ca Neah version calendar fix 3 debuger sec chance 2 2025-04-17 00:00:17 +02:00
b2fce2ad74 Neah version calendar fix 3 debuger sec chance 2025-04-16 23:58:06 +02:00
0b1a496a98 Neah version calendar fix 3 debuger ???? ?????????????? 2025-04-16 23:23:11 +02:00
30f6b12960 Neah version calendar fix 3 debuger ???? ????????????? 2025-04-16 23:20:06 +02:00
8f9802f172 Neah version calendar fix 3 debuger ???? ???????????? 2025-04-16 23:10:52 +02:00
ba5ab5d0e9 Neah version calendar fix 3 debuger ???? ??????????? 2025-04-16 23:09:23 +02:00
bc7231efa3 Neah version calendar fix 3 debuger ???? ?????????? 2025-04-16 23:02:12 +02:00
a1e910e866 Neah version calendar fix 3 debuger ???? ????????? 2025-04-16 22:58:19 +02:00
97a0bbe31d Neah version calendar fix 3 debuger ???? ???????? 2025-04-16 22:55:20 +02:00
780cb2a4ed Neah version calendar fix 3 debuger ???? ??????? 2025-04-16 22:53:01 +02:00
a2ee2d08a0 Neah version calendar fix 3 debuger ???? ?????? 2025-04-16 22:50:47 +02:00
48c3ba470c Neah version calendar fix 3 debuger ???? ?????? 2025-04-16 22:46:10 +02:00
c9f7125f37 Neah version calendar fix 3 debuger ???? ????? 2025-04-16 22:42:18 +02:00
88bb8a7f0a Neah version calendar fix 3 debuger ???? ???? 2025-04-16 22:38:02 +02:00
e5d69d9645 Neah version calendar fix 3 debuger ???? ??? 2025-04-16 22:35:44 +02:00
f14bfc1203 Neah version calendar fix 3 debuger ???? ?? 2025-04-16 22:24:44 +02:00
8d91a350c8 Neah version calendar fix 3 debuger ???? ? 2025-04-16 22:07:28 +02:00
cb481a4f3f Neah version calendar fix 2025-04-16 21:03:39 +02:00
32 changed files with 1708 additions and 730 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,7 +0,0 @@
# SMTP Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-specific-password
SMTP_FROM=your-email@gmail.com

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

BIN
app/api/.DS_Store vendored Normal file

Binary file not shown.

BIN
app/api/auth/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,5 +1,15 @@
import NextAuth from "next-auth";
import { authOptions } from '@/lib/auth';
import NextAuth, { NextAuthOptions } from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";
interface KeycloakProfile {
sub: string;
email?: string;
name?: string;
roles?: string[];
preferred_username?: string;
given_name?: string;
family_name?: string;
}
declare module "next-auth" {
interface Session {
@ -17,6 +27,7 @@ declare module "next-auth" {
}
interface JWT {
sub?: string;
accessToken: string;
refreshToken: string;
accessTokenExpires: number;
@ -24,9 +35,135 @@ declare module "next-auth" {
username: string;
first_name: string;
last_name: string;
error?: string;
}
}
function getRequiredEnvVar(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
async function refreshAccessToken(token: JWT) {
try {
const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.KEYCLOAK_CLIENT_ID!,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
}),
method: "POST",
});
const refreshedTokens = await response.json();
if (!response.ok) {
throw refreshedTokens;
}
return {
...token,
accessToken: refreshedTokens.access_token,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
};
} catch (error) {
console.error("Error refreshing access token:", error);
return {
...token,
error: "RefreshAccessTokenError",
};
}
}
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) {
const keycloakProfile = profile as KeycloakProfile;
token.accessToken = account.access_token ?? '';
token.refreshToken = account.refresh_token ?? '';
token.accessTokenExpires = account.expires_at ?? 0;
token.sub = keycloakProfile.sub;
token.role = keycloakProfile.roles ?? [];
token.username = keycloakProfile.preferred_username ?? '';
token.first_name = keycloakProfile.given_name ?? '';
token.last_name = keycloakProfile.family_name ?? '';
}
if (Date.now() < (token.accessTokenExpires as number) * 1000) {
return token;
}
return refreshAccessToken(token);
},
async session({ session, token }) {
if (token.error) {
throw new Error(token.error);
}
session.user = {
id: token.sub ?? '',
email: token.email ?? null,
name: token.name ?? null,
image: null,
username: token.username ?? '',
first_name: token.first_name ?? '',
last_name: token.last_name ?? '',
role: token.role ?? [],
};
session.accessToken = token.accessToken;
return session;
}
},
pages: {
signIn: '/signin',
error: '/signin',
},
debug: process.env.NODE_ENV === 'development',
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
interface JWT {
accessToken: string;
refreshToken: string;
accessTokenExpires: number;
}
interface Profile {
sub?: string;
email?: string;
name?: string;
roles?: string[];
}

View File

@ -1,4 +1,4 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { PrismaClient } from "@prisma/client";

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { getServerSession } from "next-auth/next";
import { prisma } from "@/lib/prisma";
/**

View File

@ -26,8 +26,6 @@ async function getLeantimeUserId(email: string): Promise<number | null> {
}
try {
console.log('Fetching Leantime user with token:', process.env.LEANTIME_TOKEN ? 'Token present' : 'Token missing');
const response = await fetch('https://agilite.slm-lab.net/api/jsonrpc', {
method: 'POST',
headers: {
@ -45,18 +43,12 @@ async function getLeantimeUserId(email: string): Promise<number | null> {
});
if (!response.ok) {
console.error('Failed to fetch user from Leantime:', {
status: response.status,
statusText: response.statusText
});
throw new Error(`Failed to fetch user from Leantime: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log('Leantime user response:', data);
if (!data.result || data.result === false) {
console.log('User not found in Leantime');
return null;
}
@ -74,7 +66,6 @@ async function getLeantimeUserId(email: string): Promise<number | null> {
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
console.log('Session:', session ? 'Present' : 'Missing');
if (!session) {
return NextResponse.json(
@ -90,11 +81,8 @@ export async function GET(request: NextRequest) {
);
}
console.log('User email:', session.user.email);
// Get Leantime user ID
const leantimeUserId = await getLeantimeUserId(session.user.email);
console.log('Leantime user ID:', leantimeUserId);
if (!leantimeUserId) {
return NextResponse.json(
@ -104,7 +92,6 @@ export async function GET(request: NextRequest) {
}
// Get all tasks assigned to the user
console.log('Fetching tasks for user:', leantimeUserId);
const response = await fetch('https://agilite.slm-lab.net/api/jsonrpc', {
method: 'POST',
headers: {
@ -125,22 +112,16 @@ export async function GET(request: NextRequest) {
});
if (!response.ok) {
console.error('Failed to fetch tasks:', {
status: response.status,
statusText: response.statusText
});
throw new Error(`Failed to fetch tasks: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log('Tasks response:', data);
if (!data.result) {
return NextResponse.json({ projects: [] });
}
// Get project details to include project names
console.log('Fetching projects');
const projectsResponse = await fetch('https://agilite.slm-lab.net/api/jsonrpc', {
method: 'POST',
headers: {
@ -155,15 +136,10 @@ export async function GET(request: NextRequest) {
});
if (!projectsResponse.ok) {
console.error('Failed to fetch projects:', {
status: projectsResponse.status,
statusText: projectsResponse.statusText
});
throw new Error(`Failed to fetch projects: ${projectsResponse.status} ${projectsResponse.statusText}`);
}
const projectsData = await projectsResponse.json();
console.log('Projects response:', projectsData);
// Create a map of projects with their tasks grouped by status
const projectMap = new Map<string, Project>();
@ -221,7 +197,6 @@ export async function GET(request: NextRequest) {
// Convert the map to an array and sort projects by name
const projects = Array.from(projectMap.values()).sort((a, b) => a.name.localeCompare(b.name));
console.log('Final projects:', projects);
return NextResponse.json({ projects });
} catch (error) {

View File

@ -1,6 +1,10 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
// Cache for Leantime user IDs
const userCache = new Map<string, number>();
interface Task {
id: string;
@ -19,70 +23,45 @@ interface Task {
}
async function getLeantimeUserId(email: string): Promise<number | null> {
// Check cache first
if (userCache.has(email)) {
return userCache.get(email)!;
}
try {
if (!process.env.LEANTIME_TOKEN) {
console.error('LEANTIME_TOKEN is not set in environment variables');
return null;
}
console.log('Fetching Leantime users for email:', email);
console.log('API URL:', process.env.LEANTIME_API_URL);
console.log('Token length:', process.env.LEANTIME_TOKEN.length);
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-API-Key': process.env.LEANTIME_TOKEN
};
const response = await fetch(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, {
const response = await fetch('https://agilite.slm-lab.net/api/jsonrpc', {
method: 'POST',
headers,
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.LEANTIME_TOKEN || '',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'leantime.rpc.users.getAll',
id: 1
method: 'leantime.rpc.Users.Users.getUserByEmail',
id: 1,
params: {
email: email
}
}),
});
const responseText = await response.text();
console.log('Raw Leantime response:', responseText);
if (!response.ok) {
console.error('Failed to fetch Leantime users:', {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries())
});
throw new Error(`Failed to fetch user from Leantime: ${response.status} ${response.statusText}`);
}
const data = await response.json();
if (!data.result || data.result === false) {
return null;
}
let data;
try {
data = JSON.parse(responseText);
} catch (e) {
console.error('Failed to parse Leantime response:', e);
return null;
}
console.log('Leantime users response:', data);
if (!data.result || !Array.isArray(data.result)) {
console.error('Invalid response format from Leantime users API');
return null;
}
const users = data.result;
const user = users.find((u: any) => u.username === email);
if (user) {
console.log('Found Leantime user:', { id: user.id, username: user.username });
} else {
console.log('No Leantime user found for username:', email);
}
return user ? user.id : null;
// Cache the user ID
userCache.set(email, data.result.id);
// Clear cache after 5 minutes
setTimeout(() => userCache.delete(email), 5 * 60 * 1000);
return data.result.id;
} catch (error) {
console.error('Error fetching Leantime user ID:', error);
console.error('Error getting Leantime user ID:', error);
return null;
}
}
@ -90,116 +69,101 @@ async function getLeantimeUserId(email: string): Promise<number | null> {
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!session) {
return NextResponse.json(
{ error: "Unauthorized", message: "No session found. Please sign in." },
{ status: 401 }
);
}
console.log('Fetching tasks for user:', session.user.email);
const userId = await getLeantimeUserId(session.user.email);
if (!session.user?.email) {
return NextResponse.json(
{ error: "Unauthorized", message: "No email found in session. Please sign in again." },
{ status: 401 }
);
}
// Get Leantime user ID
const leantimeUserId = await getLeantimeUserId(session.user.email);
if (!userId) {
console.error('User not found in Leantime');
return NextResponse.json({ error: "User not found in Leantime" }, { status: 404 });
if (!leantimeUserId) {
return NextResponse.json(
{ error: "User not found", message: "Could not find user in Leantime. Please check your email." },
{ status: 404 }
);
}
console.log('Fetching tasks for Leantime user ID:', userId);
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-API-Key': process.env.LEANTIME_TOKEN!
};
const response = await fetch(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, {
// Get all tasks assigned to the user
const response = await fetch('https://agilite.slm-lab.net/api/jsonrpc', {
method: 'POST',
headers,
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.LEANTIME_TOKEN || '',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'leantime.rpc.tickets.getAll',
method: 'leantime.rpc.Tickets.Tickets.getAll',
id: 1,
params: {
userId: userId,
status: "all"
},
id: 1
}),
projectId: 0, // 0 means all projects
userId: leantimeUserId,
status: "all",
limit: 100
}
})
});
const responseText = await response.text();
console.log('Tasks API response status:', response.status);
if (!response.ok) {
console.error('Failed to fetch tasks from Leantime:', {
status: response.status,
statusText: response.statusText
});
throw new Error('Failed to fetch tasks from Leantime');
throw new Error(`Failed to fetch tasks: ${response.status} ${response.statusText}`);
}
let data;
try {
data = JSON.parse(responseText);
} catch (e) {
console.error('Failed to parse tasks response');
throw new Error('Invalid response format from Leantime');
const data = await response.json();
if (!data.result) {
return NextResponse.json({ tasks: [] });
}
if (!data.result || !Array.isArray(data.result)) {
console.error('Invalid response format from Leantime tasks API');
throw new Error('Invalid response format from Leantime');
}
// Filter out tasks that are not in progress or new
const filteredTasks = data.result.filter((task: any) => {
const status = task.status.toString();
return status === '1' || status === '2'; // 1 = new, 2 = in progress
});
// Log only the number of tasks and their IDs
console.log('Received tasks count:', data.result.length);
console.log('Task IDs:', data.result.map((task: any) => task.id));
const tasks = data.result
.filter((task: any) => {
// Log raw task data for debugging
console.log('Raw task data:', {
id: task.id,
headline: task.headline,
status: task.status,
type: task.type,
dependingTicketId: task.dependingTicketId
});
// Filter out any task (main or subtask) that has status Done (5)
if (task.status === 5) {
console.log(`Filtering out Done task ${task.id} (type: ${task.type || 'main'}, status: ${task.status})`);
return false;
}
// Convert both to strings for comparison to handle any type mismatches
const taskEditorId = String(task.editorId).trim();
const currentUserId = String(userId).trim();
// Only show tasks where the user is the editor
const isUserEditor = taskEditorId === currentUserId;
console.log(`Task ${task.id}: status=${task.status}, type=${task.type || 'main'}, parentId=${task.dependingTicketId || 'none'}, isUserEditor=${isUserEditor}`);
return isUserEditor;
// Get project details to include project names
const projectsResponse = await fetch('https://agilite.slm-lab.net/api/jsonrpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.LEANTIME_TOKEN || '',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'leantime.rpc.Projects.getAll',
id: 1
})
.map((task: any) => ({
id: task.id.toString(),
headline: task.headline,
projectName: task.projectName,
projectId: task.projectId,
status: task.status,
dateToFinish: task.dateToFinish || null,
milestone: task.type || null,
details: task.description || null,
createdOn: task.dateCreated,
editedOn: task.editedOn || null,
editorId: task.editorId,
editorFirstname: task.editorFirstname,
editorLastname: task.editorLastname,
type: task.type || null, // Added type field to identify subtasks
dependingTicketId: task.dependingTicketId || null // Added parent task reference
}));
});
console.log(`Found ${tasks.length} tasks assigned to user ${userId}`);
return NextResponse.json(tasks);
if (!projectsResponse.ok) {
throw new Error(`Failed to fetch projects: ${projectsResponse.status} ${projectsResponse.statusText}`);
}
const projectsData = await projectsResponse.json();
// Map tasks to include project names
const tasksWithProjects = filteredTasks.map((task: any) => {
const project = projectsData.result.find((p: any) => p.id === task.projectId);
return {
...task,
projectName: project ? project.name : `Project ${task.projectId}`
};
});
return NextResponse.json({ tasks: tasksWithProjects });
} catch (error) {
console.error('Error in tasks route:', error);
console.error('Error fetching tasks:', error);
return NextResponse.json(
{ error: "Failed to fetch tasks" },
{ error: "Failed to fetch tasks", message: error instanceof Error ? error.message : "Unknown error occurred" },
{ status: 500 }
);
}

View File

@ -2,14 +2,9 @@ import { NextResponse } from 'next/server';
import Imap from 'imap';
import nodemailer from 'nodemailer';
import { parseEmailHeaders, decodeEmailBody } from '@/lib/email-parser';
import { cookies } from 'next/headers';
interface StoredCredentials {
email: string;
password: string;
host: string;
port: number;
}
import { getServerSession } from 'next-auth/next';
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from '@/lib/prisma';
interface Email {
id: string;
@ -55,48 +50,21 @@ interface ImapConfig {
debug?: (info: string) => void;
}
function getStoredCredentials(): StoredCredentials | null {
const cookieStore = cookies();
const credentialsCookie = cookieStore.get('imap_credentials');
console.log('Retrieved credentials cookie:', credentialsCookie ? 'Found' : 'Not found');
export async function GET(request: Request) {
const session = await getServerSession(authOptions);
if (!credentialsCookie?.value) {
console.log('No credentials cookie found');
return null;
if (!session?.user?.id) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
const credentials = JSON.parse(credentialsCookie.value);
console.log('Parsed credentials:', {
...credentials,
password: '***'
const userCredentials = await prisma.emailCredentials.findUnique({
where: { userId: session.user.id }
});
// Validate required fields
if (!credentials.email || !credentials.password || !credentials.host || !credentials.port) {
console.error('Missing required credentials fields');
return null;
}
return {
email: credentials.email,
password: credentials.password,
host: credentials.host,
port: credentials.port
};
} catch (error) {
console.error('Error parsing credentials cookie:', error);
return null;
}
}
export async function GET(request: Request) {
try {
const credentials = getStoredCredentials();
if (!credentials) {
if (!userCredentials) {
return NextResponse.json(
{ error: 'No stored credentials found' },
{ error: 'No email credentials configured. Please set up your email account first.' },
{ status: 401 }
);
}
@ -110,10 +78,10 @@ export async function GET(request: Request) {
return new Promise((resolve) => {
const imap = new Imap({
user: credentials.email,
password: credentials.password,
host: credentials.host,
port: credentials.port,
user: userCredentials.email,
password: userCredentials.password,
host: userCredentials.host,
port: userCredentials.port,
tls: true,
tlsOptions: { rejectUnauthorized: false },
authTimeout: 30000,
@ -295,28 +263,27 @@ export async function GET(request: Request) {
}
export async function POST(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
const credentials = getStoredCredentials();
if (!credentials) {
const userCredentials = await prisma.emailCredentials.findUnique({
where: { userId: session.user.id }
});
if (!userCredentials) {
return NextResponse.json(
{ error: 'No stored credentials found' },
{ error: 'No email credentials configured. Please set up your email account first.' },
{ status: 401 }
);
}
let body;
try {
body = await request.json();
} catch (error) {
return NextResponse.json(
{ error: 'Invalid JSON in request body' },
{ status: 400 }
);
}
const { to, subject, body, attachments } = await request.json();
const { to, subject, body: emailBody, attachments } = body;
if (!to || !subject || !emailBody) {
if (!to || !subject || !body) {
return NextResponse.json(
{ error: 'Missing required fields: to, subject, or body' },
{ status: 400 }
@ -324,20 +291,20 @@ export async function POST(request: Request) {
}
const transporter = nodemailer.createTransport({
host: credentials.host,
port: credentials.port,
host: userCredentials.host,
port: userCredentials.port,
secure: true,
auth: {
user: credentials.email,
pass: credentials.password,
user: userCredentials.email,
pass: userCredentials.password,
},
});
const mailOptions = {
from: credentials.email,
from: userCredentials.email,
to,
subject,
text: emailBody,
html: body,
attachments: attachments || [],
};

View File

@ -1,4 +1,4 @@
import { getServerSession } from "next-auth";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { NextResponse } from "next/server";

View File

@ -110,6 +110,11 @@ export default async function CalendarPage() {
<CalendarClient
initialCalendars={calendars}
userId={session.user.id}
userProfile={{
name: session.user.name || '',
email: session.user.email || '',
avatar: session.user.image || undefined
}}
/>
</div>
);

View File

@ -6,6 +6,7 @@ import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { Providers } from "@/components/providers";
import { LayoutWrapper } from "@/components/layout/layout-wrapper";
import { Toaster } from "@/components/ui/toaster";
const inter = Inter({ subsets: ["latin"] });
@ -14,10 +15,12 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
const headersList = headers();
const pathname = headersList.get("x-pathname") || "";
const isSignInPage = pathname === "/signin";
// Only check session if not on signin page
const session = !isSignInPage ? await getServerSession(authOptions) : null;
return (
<html lang="fr">
@ -30,6 +33,7 @@ export default async function RootLayout({
{children}
</LayoutWrapper>
</Providers>
<Toaster />
</body>
</html>
);

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

@ -10,18 +10,24 @@ export function AuthCheck({ children }: { children: React.ReactNode }) {
const router = useRouter();
useEffect(() => {
// Only redirect if we're certain the user is not authenticated
// and we're not already on the signin page
if (status === "unauthenticated" && pathname !== "/signin") {
router.push("/signin");
router.replace("/signin");
}
}, [status, router, pathname]);
}, [status, pathname, router]);
// During loading, show the children to prevent flashing
// This works because server-side session check will handle protection
if (status === "loading") {
return <div>Chargement...</div>;
return <>{children}</>;
}
if (status === "unauthenticated" && pathname !== "/signin") {
return null;
// If we're on the signin page, or if we're authenticated, show the children
if (pathname === "/signin" || status === "authenticated") {
return <>{children}</>;
}
return <>{children}</>;
// Otherwise, render nothing while redirecting
return null;
}

View File

@ -1,17 +1,21 @@
"use client";
import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
export function SignInForm() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/";
return (
<div className="text-center">
<h1 className="text-4xl font-bold text-white mb-4">Bienvenue sur Enkun</h1>
<p className="text-white/80 mb-8">Connectez-vous pour accéder à votre espace</p>
<button
onClick={() => signIn("keycloak", { callbackUrl: "/" })}
onClick={() => signIn("keycloak", { callbackUrl })}
className="px-8 py-3 bg-[#0F172A] text-white rounded hover:bg-[#1E293B] transition-colors"
>
Commit
Se connecter
</button>
</div>
);

View File

@ -75,8 +75,8 @@ interface CalendarClientProps {
interface EventFormData {
title: string;
description: string | null;
start: string;
end: string;
start: Date | null;
end: Date | null;
allDay: boolean;
location: string | null;
calendarId?: string;
@ -448,8 +448,8 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
const [eventForm, setEventForm] = useState<EventFormData>({
title: "",
description: null,
start: "",
end: "",
start: null,
end: null,
allDay: false,
location: null,
calendarId: selectedCalendarId
@ -466,6 +466,8 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
const [visibleCalendarIds, setVisibleCalendarIds] = useState<string[]>([]);
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
// Update useEffect to initialize visible calendars and fetch events
useEffect(() => {
if (calendars.length > 0) {
@ -637,8 +639,8 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
setEventForm({
title: "",
description: null,
start: startDate.toISOString(),
end: endDate.toISOString(),
start: startDate,
end: endDate,
allDay: selectInfo.allDay,
location: null,
calendarId: firstCalendar.id
@ -647,8 +649,8 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
setEventForm({
title: "",
description: null,
start: startDate.toISOString(),
end: endDate.toISOString(),
start: startDate,
end: endDate,
allDay: selectInfo.allDay,
location: null,
calendarId: selectedCalendarId
@ -667,8 +669,8 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
setEventForm({
title: event.title,
description: event.extendedProps.description,
start: startDate.toISOString().slice(0, 16),
end: endDate.toISOString().slice(0, 16),
start: startDate,
end: endDate,
allDay: event.isAllDay,
location: event.extendedProps.location,
calendarId: event.extendedProps.calendarId,
@ -676,79 +678,47 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
setIsEventModalOpen(true);
};
const handleEventSubmit = async () => {
const handleEventSubmit = async (event: React.MouseEvent<HTMLButtonElement>) => {
try {
// Validate required fields including calendar
if (!eventForm.title || !eventForm.start || !eventForm.end || !eventForm.calendarId) {
console.log("Form validation failed:", {
title: eventForm.title,
start: eventForm.start,
end: eventForm.end,
calendarId: eventForm.calendarId
});
setError("Veuillez remplir tous les champs obligatoires et sélectionner un calendrier");
return;
setLoading(true);
if (!eventForm.start || !eventForm.end) {
throw new Error('Start and end dates are required');
}
setLoading(true);
const eventData = {
...eventForm,
start: new Date(eventForm.start).toISOString(),
end: new Date(eventForm.end).toISOString(),
start: eventForm.start.toISOString(),
end: eventForm.end.toISOString(),
userId,
...(selectedEvent ? { id: selectedEvent.id } : {}), // Include ID for updates
allDay: eventForm.allDay // Use allDay instead of isAllDay
};
console.log("Submitting event with data:", eventData);
const response = await fetch("/api/events", {
method: selectedEvent ? "PUT" : "POST",
const response = await fetch('/api/events', {
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify(eventData),
credentials: 'include'
});
const responseData = await response.json();
console.log("Response from server:", responseData);
if (!response.ok) {
console.error("Error response:", responseData);
throw new Error(responseData.error || "Failed to save event");
throw new Error('Failed to save event');
}
// Reset form and close modal first
setIsEventModalOpen(false);
setEventForm({
title: "",
description: null,
start: "",
end: "",
allDay: false,
location: null,
calendarId: selectedCalendarId
});
setSelectedEvent(null);
setError(null);
// Update calendars state with the new event
const updatedCalendars = calendars.map(cal => {
if (cal.id === eventData.calendarId) {
const newEvent = await response.json();
setCalendars(prev => prev.map(cal => {
if (cal.id === newEvent.calendarId) {
return {
...cal,
events: [...cal.events, responseData]
events: [...cal.events, newEvent],
};
}
return cal;
});
setCalendars(updatedCalendars);
// Fetch fresh data to ensure all calendars are up to date
await fetchCalendars();
} catch (error) {
console.error("Error saving event:", error);
setError(error instanceof Error ? error.message : "Failed to save event");
}));
setError(null);
} catch (err) {
setError('Failed to save event');
console.error('Error saving event:', err);
} finally {
setLoading(false);
}
@ -785,8 +755,8 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
setEventForm({
title: "",
description: null,
start: "",
end: "",
start: null,
end: null,
allDay: false,
location: null,
calendarId: selectedCalendarId
@ -867,42 +837,42 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
const handleStartDateChange = (date: Date | null) => {
if (!date) return;
const endDate = getDateFromString(eventForm.end);
const endDate = getDateFromString(eventForm.end?.toISOString() || "");
if (date > endDate) {
// If start date is after end date, set end date to start date + 1 hour
const newEndDate = new Date(date);
newEndDate.setHours(date.getHours() + 1);
setEventForm({
...eventForm,
start: date.toISOString(),
end: newEndDate.toISOString(),
});
setEventForm(prev => ({
...prev,
start: date,
end: newEndDate,
}));
} else {
setEventForm({
...eventForm,
start: date.toISOString(),
});
setEventForm(prev => ({
...prev,
start: date,
}));
}
};
const handleEndDateChange = (date: Date | null) => {
if (!date) return;
const startDate = getDateFromString(eventForm.start);
const startDate = getDateFromString(eventForm.start?.toISOString() || "");
if (date < startDate) {
// If end date is before start date, set start date to end date - 1 hour
const newStartDate = new Date(date);
newStartDate.setHours(date.getHours() - 1);
setEventForm({
...eventForm,
start: newStartDate.toISOString(),
end: date.toISOString(),
});
setEventForm(prev => ({
...prev,
start: newStartDate,
end: date,
}));
} else {
setEventForm({
...eventForm,
end: date.toISOString(),
});
setEventForm(prev => ({
...prev,
end: date,
}));
}
};
@ -1160,8 +1130,8 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
setEventForm({
title: "",
description: null,
start: "",
end: "",
start: null,
end: null,
allDay: false,
location: null,
calendarId: selectedCalendarId || calendars[0]?.id
@ -1236,8 +1206,13 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
<div className="flex gap-2">
<div className="flex-1">
<DatePicker
selected={getDateFromString(eventForm.start)}
onChange={handleStartDateChange}
selected={eventForm.start || undefined}
onChange={(date: Date | null) => {
setEventForm(prev => ({
...prev,
start: date
}));
}}
dateFormat="dd/MM/yyyy"
locale="fr"
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
@ -1246,8 +1221,13 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
/>
</div>
<DatePicker
selected={getDateFromString(eventForm.start)}
onChange={handleStartDateChange}
selected={eventForm.start || undefined}
onChange={(date: Date | null) => {
setEventForm(prev => ({
...prev,
start: date
}));
}}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}
@ -1264,19 +1244,29 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
<div className="flex gap-2">
<div className="flex-1">
<DatePicker
selected={getDateFromString(eventForm.end)}
onChange={handleEndDateChange}
selected={eventForm.end || undefined}
onChange={(date: Date | null) => {
setEventForm(prev => ({
...prev,
end: date
}));
}}
dateFormat="dd/MM/yyyy"
locale="fr"
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
placeholderText="Date"
customInput={<Input />}
minDate={getDateFromString(eventForm.start)}
minDate={eventForm.start || undefined}
/>
</div>
<DatePicker
selected={getDateFromString(eventForm.end)}
onChange={handleEndDateChange}
selected={eventForm.end || undefined}
onChange={(date: Date | null) => {
setEventForm(prev => ({
...prev,
end: date
}));
}}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}

View File

@ -27,50 +27,43 @@ export function CalendarWidget() {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
console.log("Calendar Widget - Session Status:", status);
console.log("Calendar Widget - Session Data:", session);
if (status === "loading") {
console.log("Calendar Widget - Session is loading");
return;
}
if (status !== "authenticated" || !session) {
console.log("Calendar Widget - Not authenticated, skipping fetch");
setLoading(false);
return;
}
const fetchUpcomingEvents = async () => {
try {
console.log("Calendar Widget - Starting to fetch events");
setLoading(true);
// Fetch calendars with events
console.log("Calendar Widget - Making API request to /api/calendars");
const response = await fetch('/api/calendars');
if (!response.ok) {
console.error("Calendar Widget - API response not OK:", response.status, response.statusText);
throw new Error("Impossible de charger les événements");
}
const calendarsData = await response.json();
console.log("Calendar Widget - Raw calendars data:", calendarsData);
if (!Array.isArray(calendarsData)) {
console.error("Calendar Widget - Calendars data is not an array:", calendarsData);
throw new Error("Format de données invalide");
}
// Get current date at the start of the day
const now = new Date();
now.setHours(0, 0, 0, 0);
// Extract all events and add calendar info
const allEvents = calendarsData.flatMap((calendar) => {
console.log("Calendar Widget - Processing calendar:", calendar.name, "Events:", calendar.events?.length || 0);
return (calendar.events || []).map((event) => {
return (calendar.events || []).map((event: {
id: string;
title: string;
start: string;
end: string;
isAllDay: boolean;
calendarId: string;
}) => {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
return {
@ -86,27 +79,22 @@ export function CalendarWidget() {
});
});
// Filter for upcoming events (today and future)
const upcomingEvents = allEvents
.filter(event => event.start >= now)
.sort((a, b) => a.start.getTime() - b.start.getTime())
.slice(0, 5);
console.log("Calendar Widget - Final upcoming events:", upcomingEvents);
setEvents(upcomingEvents);
setError(null);
} catch (err) {
console.error("Calendar Widget - Error in fetchUpcomingEvents:", err);
setError("Impossible de charger les événements à venir");
} finally {
setLoading(false);
}
};
// Initial fetch
fetchUpcomingEvents();
// Set up an interval to refresh events every 5 minutes
const intervalId = setInterval(fetchUpcomingEvents, 300000);
return () => clearInterval(intervalId);

View File

@ -98,10 +98,7 @@ export function Duties() {
}
const data = await response.json();
console.log('Raw API response:', data);
if (!Array.isArray(data)) {
console.warn('No tasks found in response', data as unknown);
setTasks([]);
return;
}
@ -110,13 +107,7 @@ export function Duties() {
const sortedTasks = data
.filter((task: Task) => {
// Filter out any task (main or subtask) that has status Done (5)
const isNotDone = task.status !== 5;
if (!isNotDone) {
console.log(`Filtering out Done task ${task.id} (type: ${task.type || 'main'}, status: ${task.status})`);
} else {
console.log(`Keeping task ${task.id}: status=${task.status} (${getStatusLabel(task.status)}), type=${task.type || 'main'}`);
}
return isNotDone;
return task.status !== 5;
})
.sort((a: Task, b: Task) => {
// First sort by dateToFinish (oldest first)
@ -144,15 +135,8 @@ export function Duties() {
return 0;
});
console.log('Sorted and filtered tasks:', sortedTasks.map(t => ({
id: t.id,
date: t.dateToFinish,
status: t.status,
type: t.type || 'main'
})));
setTasks(sortedTasks.slice(0, 7));
} catch (error) {
console.error('Error fetching tasks:', error);
setError(error instanceof Error ? error.message : 'Failed to fetch tasks');
} finally {
setLoading(false);

View File

@ -8,7 +8,7 @@ interface ProvidersProps {
export function Providers({ children }: ProvidersProps) {
return (
<SessionProvider>
<SessionProvider refetchInterval={30 * 60}>
{children}
</SessionProvider>
);

View File

@ -1,74 +1,105 @@
import { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { PrismaClient } from '@prisma/client';
import { NextAuthOptions, User as NextAuthUser, Account, Profile, JWT as NextAuthJWT } from 'next-auth';
import KeycloakProvider from 'next-auth/providers/keycloak';
const prisma = new PrismaClient();
interface User extends NextAuthUser {
id: string;
email: string;
name: string;
role: string[];
first_name: string;
last_name: string;
username: string;
}
declare module 'next-auth' {
interface User {
id: string;
email: string;
password: string;
}
interface Session {
user: User;
}
interface Session {
user: User;
accessToken: string;
}
interface JWT extends Omit<NextAuthJWT, 'accessToken' | 'refreshToken' | 'accessTokenExpires'> {
sub: string;
email: string | null;
name: string | null;
role: string[];
username: string;
first_name: string;
last_name: string;
accessToken: string;
refreshToken: string;
accessTokenExpires: number;
error?: string;
}
interface KeycloakProfile extends Profile {
sub: string;
email?: string;
name?: string;
roles?: string[];
preferred_username?: string;
given_name?: string;
family_name?: string;
}
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID!,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
issuer: process.env.KEYCLOAK_ISSUER!,
authorization: {
params: {
scope: 'openid email profile',
},
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
// Find user in database
const user = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user) {
return null;
}
// Here you would typically verify the password
// For now, we'll just return the user
return {
id: user.id,
email: user.email,
password: user.password,
};
}
})
}),
],
session: {
strategy: 'jwt',
},
pages: {
signIn: '/login',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.email = user.email;
token.password = user.password;
async jwt({ token, account, profile }) {
if (account && profile) {
const keycloakProfile = profile as KeycloakProfile;
token.accessToken = account.access_token ?? '';
token.refreshToken = account.refresh_token ?? '';
token.accessTokenExpires = account.expires_at ?? 0;
token.sub = keycloakProfile.sub;
token.role = keycloakProfile.roles ?? [];
token.username = keycloakProfile.preferred_username ?? '';
token.first_name = keycloakProfile.given_name ?? '';
token.last_name = keycloakProfile.family_name ?? '';
}
return token;
},
async session({ session, token }) {
if (token) {
session.user.id = token.id as string;
session.user.email = token.email as string;
session.user.password = token.password as string;
console.log('Session callback - token:', token);
console.log('Session callback - session before:', session);
if (token.error) {
throw new Error(token.error);
}
// Only update session if token has changed
if (session.user?.id !== token.sub) {
session.user = {
id: token.sub,
email: token.email ?? '',
name: token.name ?? '',
role: token.role,
first_name: token.first_name,
last_name: token.last_name,
username: token.username,
};
}
session.accessToken = token.accessToken;
console.log('Session callback - session after:', session);
return session;
}
}
},
},
pages: {
signIn: '/signin',
error: '/signin',
},
};

805
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -57,7 +57,7 @@
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"mailparser": "^3.7.2",
"next": "14.2.24",
"next": "^15.3.0",
"next-auth": "^4.24.11",
"next-themes": "^0.4.4",
"nodemailer": "^6.10.1",

View File

@ -11,16 +11,6 @@ datasource db {
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
calendars Calendar[]
events Event[]
}
model Calendar {
id String @id @default(uuid())
name String
@ -30,7 +20,6 @@ model Calendar {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
events Event[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
@ -48,8 +37,18 @@ model Event {
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([calendarId])
@@index([userId])
}
model EmailCredentials {
id String @id @default(cuid())
userId String @unique
email String
password String
host String
port Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

188
yarn.lock
View File

@ -110,6 +110,18 @@
resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz"
integrity sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==
"@img/sharp-darwin-arm64@0.34.1":
version "0.34.1"
resolved "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz"
integrity sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==
optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.1.0"
"@img/sharp-libvips-darwin-arm64@1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz"
integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
@ -154,15 +166,15 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@next/env@14.2.24":
version "14.2.24"
resolved "https://registry.npmjs.org/@next/env/-/env-14.2.24.tgz"
integrity sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==
"@next/env@15.3.0":
version "15.3.0"
resolved "https://registry.npmjs.org/@next/env/-/env-15.3.0.tgz"
integrity sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA==
"@next/swc-darwin-arm64@14.2.24":
version "14.2.24"
resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.24.tgz"
integrity sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==
"@next/swc-darwin-arm64@15.3.0":
version "15.3.0"
resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.0.tgz"
integrity sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@ -943,18 +955,17 @@
domhandler "^5.0.3"
selderee "^0.11.0"
"@swc/counter@^0.1.3":
"@swc/counter@0.1.3":
version "0.1.3"
resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@swc/helpers@0.5.5":
version "0.5.5"
resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz"
integrity sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==
"@swc/helpers@0.5.15":
version "0.5.15"
resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz"
integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==
dependencies:
"@swc/counter" "^0.1.3"
tslib "^2.4.0"
tslib "^2.8.0"
"@types/d3-array@^3.0.3":
version "3.2.1"
@ -1059,12 +1070,12 @@
"@types/react" "*"
date-fns "^3.3.1"
"@types/react-dom@^18":
"@types/react-dom@*", "@types/react-dom@^18":
version "18.3.5"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz"
integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==
"@types/react@*", "@types/react@^18":
"@types/react@*", "@types/react@^18", "@types/react@^18.0.0":
version "18.3.18"
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz"
integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==
@ -1160,7 +1171,7 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
browserslist@^4.23.3:
browserslist@^4.23.3, "browserslist@>= 4.21.0":
version "4.24.4"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
@ -1236,11 +1247,27 @@ color-convert@^2.0.1:
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
color-name@^1.0.0, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.9.0:
version "1.9.1"
resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz"
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
dependencies:
color-convert "^2.0.1"
color-string "^1.9.0"
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
@ -1358,7 +1385,7 @@ d3-timer@^3.0.1:
resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
date-fns@^3.3.1, date-fns@^3.6.0:
"date-fns@^2.28.0 || ^3.0.0", date-fns@^3.3.1, date-fns@^3.6.0:
version "3.6.0"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz"
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
@ -1385,6 +1412,11 @@ deepmerge@^4.3.1:
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
detect-libc@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz"
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
detect-node-es@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz"
@ -1642,11 +1674,6 @@ glob@^10.3.10:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
graceful-fs@^4.2.11:
version "4.2.11"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
@ -1733,6 +1760,11 @@ ip-address@^9.0.5:
jsbn "1.1.0"
sprintf-js "^1.1.3"
is-arrayish@^0.3.1:
version "0.3.2"
resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
@ -1977,35 +2009,35 @@ next-themes@^0.4.4:
resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz"
integrity sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==
next@14.2.24:
version "14.2.24"
resolved "https://registry.npmjs.org/next/-/next-14.2.24.tgz"
integrity sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==
"next@^12.2.5 || ^13 || ^14 || ^15", next@^15.3.0, next@>=15.0.0:
version "15.3.0"
resolved "https://registry.npmjs.org/next/-/next-15.3.0.tgz"
integrity sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==
dependencies:
"@next/env" "14.2.24"
"@swc/helpers" "0.5.5"
"@next/env" "15.3.0"
"@swc/counter" "0.1.3"
"@swc/helpers" "0.5.15"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
graceful-fs "^4.2.11"
postcss "8.4.31"
styled-jsx "5.1.1"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "14.2.24"
"@next/swc-darwin-x64" "14.2.24"
"@next/swc-linux-arm64-gnu" "14.2.24"
"@next/swc-linux-arm64-musl" "14.2.24"
"@next/swc-linux-x64-gnu" "14.2.24"
"@next/swc-linux-x64-musl" "14.2.24"
"@next/swc-win32-arm64-msvc" "14.2.24"
"@next/swc-win32-ia32-msvc" "14.2.24"
"@next/swc-win32-x64-msvc" "14.2.24"
"@next/swc-darwin-arm64" "15.3.0"
"@next/swc-darwin-x64" "15.3.0"
"@next/swc-linux-arm64-gnu" "15.3.0"
"@next/swc-linux-arm64-musl" "15.3.0"
"@next/swc-linux-x64-gnu" "15.3.0"
"@next/swc-linux-x64-musl" "15.3.0"
"@next/swc-win32-arm64-msvc" "15.3.0"
"@next/swc-win32-x64-msvc" "15.3.0"
sharp "^0.34.1"
node-releases@^2.0.19:
version "2.0.19"
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
nodemailer@^6.10.1:
nodemailer@^6.10.1, nodemailer@^6.6.5:
version "6.10.1"
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz"
integrity sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==
@ -2165,7 +2197,7 @@ pg-types@^4.0.1:
postgres-interval "^3.0.0"
postgres-range "^1.1.1"
pg@^8.14.1:
pg@^8.14.1, pg@>=8.0:
version "8.14.1"
resolved "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz"
integrity sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==
@ -2278,7 +2310,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8, postcss@^8.4.47:
postcss@^8, postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.47, postcss@>=8.0.9:
version "8.4.49"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz"
integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
@ -2352,7 +2384,7 @@ preact-render-to-string@^5.1.19:
dependencies:
pretty-format "^3.8.0"
preact@^10.6.3:
preact@^10.6.3, preact@>=10:
version "10.26.2"
resolved "https://registry.npmjs.org/preact/-/preact-10.26.2.tgz"
integrity sha512-0gNmv4qpS9HaN3+40CLBAnKe0ZfyE4ZWo5xKlC1rVrr0ckkEvJvAQqKaHANdFKsGstoxrY4AItZ7kZSGVoVjgg==
@ -2367,7 +2399,7 @@ pretty-format@^3.8.0:
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz"
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
prisma@^6.4.1:
prisma@*, prisma@^6.4.1:
version "6.4.1"
resolved "https://registry.npmjs.org/prisma/-/prisma-6.4.1.tgz"
integrity sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg==
@ -2421,7 +2453,7 @@ react-day-picker@8.10.1:
resolved "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz"
integrity sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==
react-dom@^18:
"react-dom@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.7.0 || ^17 || ^18 || ^19", "react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^17.0.2 || ^18 || ^19", react-dom@^18, "react-dom@^18 || ^19 || ^19.0.0-rc", "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react-dom@>=16.6.0, react-dom@>=16.8.0, react-dom@>=17.0.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
@ -2429,7 +2461,7 @@ react-dom@^18:
loose-envify "^1.1.0"
scheduler "^0.23.2"
react-hook-form@^7.54.1:
react-hook-form@^7.0.0, react-hook-form@^7.54.1:
version "7.54.2"
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz"
integrity sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==
@ -2500,7 +2532,7 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@^18:
react@*, "react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.7.0 || ^17 || ^18 || ^19", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^17.0.2 || ^18 || ^19", react@^18, "react@^18 || ^19 || ^19.0.0-rc", "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^18.3.1, "react@>= 16.8.0", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", react@>=16.6.0, react@>=16.8.0, react@>=17.0.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@ -2607,11 +2639,46 @@ selderee@^0.11.0:
dependencies:
parseley "^0.12.0"
semver@^7.7.1:
version "7.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
semver@~5.3.0:
version "5.3.0"
resolved "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz"
integrity sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==
sharp@^0.34.1:
version "0.34.1"
resolved "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz"
integrity sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==
dependencies:
color "^4.2.3"
detect-libc "^2.0.3"
semver "^7.7.1"
optionalDependencies:
"@img/sharp-darwin-arm64" "0.34.1"
"@img/sharp-darwin-x64" "0.34.1"
"@img/sharp-libvips-darwin-arm64" "1.1.0"
"@img/sharp-libvips-darwin-x64" "1.1.0"
"@img/sharp-libvips-linux-arm" "1.1.0"
"@img/sharp-libvips-linux-arm64" "1.1.0"
"@img/sharp-libvips-linux-ppc64" "1.1.0"
"@img/sharp-libvips-linux-s390x" "1.1.0"
"@img/sharp-libvips-linux-x64" "1.1.0"
"@img/sharp-libvips-linuxmusl-arm64" "1.1.0"
"@img/sharp-libvips-linuxmusl-x64" "1.1.0"
"@img/sharp-linux-arm" "0.34.1"
"@img/sharp-linux-arm64" "0.34.1"
"@img/sharp-linux-s390x" "0.34.1"
"@img/sharp-linux-x64" "0.34.1"
"@img/sharp-linuxmusl-arm64" "0.34.1"
"@img/sharp-linuxmusl-x64" "0.34.1"
"@img/sharp-wasm32" "0.34.1"
"@img/sharp-win32-ia32" "0.34.1"
"@img/sharp-win32-x64" "0.34.1"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
@ -2629,6 +2696,13 @@ signal-exit@^4.0.1:
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz"
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
dependencies:
is-arrayish "^0.3.1"
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz"
@ -2727,10 +2801,10 @@ strip-ansi@^7.0.1:
dependencies:
ansi-regex "^6.0.1"
styled-jsx@5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz"
integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==
styled-jsx@5.1.6:
version "5.1.6"
resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz"
integrity sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==
dependencies:
client-only "0.0.1"
@ -2767,7 +2841,7 @@ tailwindcss-animate@^1.0.7:
resolved "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz"
integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==
tailwindcss@^3.4.17:
tailwindcss@^3.4.17, "tailwindcss@>=3.0.0 || insiders":
version "3.4.17"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz"
integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==
@ -2838,12 +2912,12 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0:
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.8.0:
version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
typescript@^5:
typescript@^5, typescript@>=5.1.0:
version "5.7.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz"
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==