Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5923e811b0 | |||
| 0158c6e696 | |||
| 0c24fd0c95 | |||
| ae15fa7678 | |||
| d767ee9510 | |||
| f331080d39 | |||
| f22f9490e4 | |||
| 16219fc250 | |||
| bef5ed1946 | |||
| b5855e5a84 | |||
| 9eef481490 | |||
| 7586e17524 | |||
| f89c25d9a7 | |||
| 2b147c85a2 | |||
| 3bdc2745ca | |||
| b2fce2ad74 | |||
| 0b1a496a98 | |||
| 30f6b12960 | |||
| 8f9802f172 | |||
| ba5ab5d0e9 | |||
| bc7231efa3 | |||
| a1e910e866 | |||
| 97a0bbe31d | |||
| 780cb2a4ed | |||
| a2ee2d08a0 | |||
| 48c3ba470c | |||
| c9f7125f37 | |||
| 88bb8a7f0a | |||
| e5d69d9645 | |||
| f14bfc1203 | |||
| 8d91a350c8 | |||
| cb481a4f3f |
@ -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
BIN
app/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
app/api/.DS_Store
vendored
Normal file
BIN
app/api/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
app/api/auth/.DS_Store
vendored
Normal file
BIN
app/api/auth/.DS_Store
vendored
Normal file
Binary file not shown.
@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 || [],
|
||||
};
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -8,7 +8,7 @@ interface ProvidersProps {
|
||||
|
||||
export function Providers({ children }: ProvidersProps) {
|
||||
return (
|
||||
<SessionProvider>
|
||||
<SessionProvider refetchInterval={30 * 60}>
|
||||
{children}
|
||||
</SessionProvider>
|
||||
);
|
||||
|
||||
141
lib/auth.ts
141
lib/auth.ts
@ -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
805
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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
188
yarn.lock
@ -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==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user