dolibarr user

This commit is contained in:
alma 2025-05-04 09:23:26 +02:00
parent df644fd72d
commit acc7a44165
8 changed files with 271 additions and 4 deletions

3
.env
View File

@ -9,6 +9,9 @@ KEYCLOAK_BASE_URL=https://connect.slm-lab.net
KEYCLOAK_ADMIN_USERNAME=alma
KEYCLOAK_ADMIN_PASSWORD=PW5jfqX00m
DOLIBARR_API_URL=https://mediations.slm-lab.net/api/index.php/
DOLIBARR_API_KEY=2znq976PzZz1q2JSe9DG2A3hmbNMGIh8
NEXTCLOUD_URL=https://espace.slm-lab.net
NEXTCLOUD_CLIENT_ID=espace.slm-lab.net
NEXTCLOUD_CLIENT_SECRET=YHLVMGpu0nGRaP7gMDpSjRr1ia6HiSr1

View File

@ -36,10 +36,17 @@ export async function GET() {
}, { status: 400 });
}
if (!realm) {
return NextResponse.json({
error: 'Missing Realm',
message: 'KEYCLOAK_REALM is required'
}, { status: 400 });
}
console.log('Environment variables check:', envVars);
// Try direct authentication
const url = `${keycloakUrl}/realms/master/protocol/openid-connect/token`;
// Try direct authentication using the application realm, not master
const url = `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`;
const formData = new URLSearchParams();
// Try client credentials if available
@ -47,6 +54,7 @@ export async function GET() {
formData.append('client_id', clientId);
formData.append('client_secret', clientSecret);
formData.append('grant_type', 'client_credentials');
console.log('Using client credentials flow');
}
// Fall back to password grant
else if (adminUsername && adminPassword) {
@ -54,6 +62,7 @@ export async function GET() {
formData.append('username', adminUsername);
formData.append('password', adminPassword);
formData.append('grant_type', 'password');
console.log('Using password grant flow');
} else {
return NextResponse.json({
error: 'Missing authentication credentials',

View File

@ -1,6 +1,7 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { NextResponse } from "next/server";
import { createDolibarrUser, checkDolibarrUserExists } from "@/lib/dolibarr-api";
export async function GET() {
const session = await getServerSession(authOptions);
@ -450,11 +451,48 @@ export async function POST(req: Request) {
// We just log the error and continue
}
// Check if the user has mediation or expression role and create in Dolibarr if needed
const hasMediationRole = validRoles.includes('mediation');
const hasExpressionRole = validRoles.includes('expression');
let dolibarrUserId = null;
if (hasMediationRole || hasExpressionRole) {
console.log(`User has special role (mediation: ${hasMediationRole}, expression: ${hasExpressionRole}), creating in Dolibarr`);
// First check if the user already exists in Dolibarr
const existingUser = await checkDolibarrUserExists(data.email);
if (existingUser.exists) {
console.log(`User already exists in Dolibarr with ID: ${existingUser.id}`);
dolibarrUserId = existingUser.id;
} else {
// Create user in Dolibarr
const dolibarrResult = await createDolibarrUser({
username: data.username,
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
password: data.password,
});
if (dolibarrResult.success) {
console.log(`User created in Dolibarr with ID: ${dolibarrResult.id}`);
dolibarrUserId = dolibarrResult.id;
} else {
console.error("Dolibarr user creation failed:", dolibarrResult.error);
// We don't return an error here since Keycloak user was created successfully
// We just log the error and continue
}
}
}
return NextResponse.json({
success: true,
user: {
...user,
roles: validRoles,
dolibarrId: dolibarrUserId, // Include the Dolibarr ID if created
},
});

38
app/types/dolibarr.ts Normal file
View File

@ -0,0 +1,38 @@
/**
* Dolibarr API response types
*/
export interface DolibarrErrorResponse {
error: {
code: string;
message: string;
};
}
export interface DolibarrThirdParty {
id: number;
name: string;
name_alias?: string;
email?: string;
phone?: string;
address?: string;
zip?: string;
town?: string;
status: number;
client: number;
code_client: string;
date_creation: string;
date_modification?: string;
}
export interface DolibarrUser {
id: number;
login: string;
lastname?: string;
firstname?: string;
email?: string;
status: number;
admin: number;
entity: number;
employee: number;
}

View File

@ -45,7 +45,7 @@ interface MenuItem {
}
export function Sidebar({ isOpen, onClose }: SidebarProps) {
const { data: session, status } = useSession();
const { data: session, status, update } = useSession();
const router = useRouter();
const pathname = usePathname();
@ -59,6 +59,13 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
pathname
});
// Function to manually refresh the session from the server
const refreshSession = async () => {
console.log('Manually refreshing session...');
await update(); // Force update the session
console.log('Session refreshed!');
};
// Show loading state while session is being checked
if (status === 'loading') {
return null;
@ -212,7 +219,7 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
icon: Building2,
href: "/mediation",
iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL,
requiredRole: ["mediation", "expression"],
requiredRole: "mediation",
},
];
@ -270,6 +277,14 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
height={16.5}
className="text-black"
/>
{/* Add session refresh button */}
<button
onClick={refreshSession}
className="absolute right-2 top-6 bg-blue-500 text-white rounded-full p-1 hover:bg-blue-600"
title="Refresh session data"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg>
</button>
</div>
{/* Menu Items */}
@ -296,6 +311,12 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
<div className="mt-1">
<p>User: {session?.user?.name}</p>
<p>Email: {session?.user?.email}</p>
<button
onClick={refreshSession}
className="mt-1 w-full py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Refresh Session Data
</button>
<details>
<summary className="cursor-pointer">User Roles</summary>
<pre className="mt-1 p-1 bg-white text-[10px] overflow-x-auto">

View File

@ -48,6 +48,7 @@ interface User {
createdTimestamp: number;
roles: string[];
enabled: boolean;
dolibarrId?: number;
}
interface Role {
@ -513,6 +514,7 @@ export function UsersTable({ userRole = [] }: UsersTableProps) {
<TableHead>Email</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Roles</TableHead>
<TableHead>Dolibarr</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
@ -535,6 +537,15 @@ export function UsersTable({ userRole = [] }: UsersTableProps) {
))}
</div>
</TableCell>
<TableCell>
{user.dolibarrId ? (
<span className="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
ID: {user.dolibarrId}
</span>
) : (
<span className="text-gray-400">-</span>
)}
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>

127
lib/dolibarr-api.ts Normal file
View File

@ -0,0 +1,127 @@
import { NextResponse } from "next/server";
import { DolibarrErrorResponse, DolibarrThirdParty } from "@/app/types/dolibarr";
/**
* Create a user in Dolibarr
* @param userData User data to create in Dolibarr
* @returns Response with success status and ID or error
*/
export async function createDolibarrUser(userData: {
username: string;
firstName: string;
lastName: string;
email: string;
password: string;
}): Promise<{ success: boolean; id?: number; error?: string }> {
try {
// Validate environment variables
if (!process.env.DOLIBARR_API_URL) {
console.error('Missing DOLIBARR_API_URL environment variable');
return { success: false, error: 'Dolibarr API URL not configured' };
}
if (!process.env.DOLIBARR_API_KEY) {
console.error('Missing DOLIBARR_API_KEY environment variable');
return { success: false, error: 'Dolibarr API key not configured' };
}
// Format the API URL properly
const apiUrl = process.env.DOLIBARR_API_URL.endsWith('/')
? process.env.DOLIBARR_API_URL
: `${process.env.DOLIBARR_API_URL}/`;
console.log(`Creating Dolibarr user for ${userData.email}`);
// Create the user in Dolibarr as a thirdparty/customer
const response = await fetch(`${apiUrl}thirdparties`, {
method: 'POST',
headers: {
'DOLAPIKEY': process.env.DOLIBARR_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: `${userData.firstName} ${userData.lastName}`,
name_alias: userData.username,
email: userData.email,
client: '1', // Mark as customer
code_client: 'auto', // Auto-generate client code
note_private: 'Created via API integration from platform',
status: '1', // Active
}),
});
// Handle non-OK responses
if (!response.ok) {
let errorMessage = `HTTP error ${response.status}`;
try {
const errorData = await response.json() as DolibarrErrorResponse;
errorMessage = errorData.error?.message || errorMessage;
console.error('Dolibarr API error response:', errorData);
} catch (jsonError) {
console.error('Failed to parse Dolibarr error response');
}
return {
success: false,
error: `Failed to create Dolibarr user: ${errorMessage}`
};
}
// Parse the successful response
const data = await response.json();
console.log('Dolibarr user created successfully with ID:', data);
return { success: true, id: data };
} catch (error) {
console.error('Error creating Dolibarr user:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Check if a user exists in Dolibarr by email
* @param email Email to search for
* @returns Boolean indicating if user exists and user ID if found
*/
export async function checkDolibarrUserExists(email: string): Promise<{ exists: boolean; id?: number }> {
try {
if (!process.env.DOLIBARR_API_URL || !process.env.DOLIBARR_API_KEY) {
console.error('Missing Dolibarr API configuration');
return { exists: false };
}
// Format the API URL
const apiUrl = process.env.DOLIBARR_API_URL.endsWith('/')
? process.env.DOLIBARR_API_URL
: `${process.env.DOLIBARR_API_URL}/`;
// Search for thirdparty/customer with matching email
const response = await fetch(`${apiUrl}thirdparties?sortfield=t.rowid&sortorder=ASC&limit=1&sqlfilters=(t.email:=:'${encodeURIComponent(email)}')`, {
method: 'GET',
headers: {
'DOLAPIKEY': process.env.DOLIBARR_API_KEY,
},
});
if (!response.ok) {
console.error(`Error checking if Dolibarr user exists: HTTP ${response.status}`);
return { exists: false };
}
const data = await response.json() as DolibarrThirdParty[];
// If we got results, user exists
if (Array.isArray(data) && data.length > 0) {
return { exists: true, id: data[0].id };
}
return { exists: false };
} catch (error) {
console.error('Error checking if Dolibarr user exists:', error);
return { exists: false };
}
}

20
types/env.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
// Existing env vars
KEYCLOAK_BASE_URL: string;
KEYCLOAK_REALM: string;
KEYCLOAK_CLIENT_ID: string;
KEYCLOAK_CLIENT_SECRET: string;
KEYCLOAK_ADMIN_USERNAME?: string;
KEYCLOAK_ADMIN_PASSWORD?: string;
LEANTIME_TOKEN?: string;
// Dolibarr API env vars
DOLIBARR_API_URL: string;
DOLIBARR_API_KEY: string;
}
}
}
export {};