equipes keycloak flow
This commit is contained in:
parent
5bd2cb33bf
commit
31703ac95f
@ -2,18 +2,14 @@ import { NextResponse } from "next/server";
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||||
import { getKeycloakAdminClient } from "@/lib/keycloak";
|
import { getKeycloakAdminClient } from "@/lib/keycloak";
|
||||||
|
import { RoleRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
||||||
|
|
||||||
// Fix for Next.js "params should be awaited" error - temporarily disabled
|
// Fix for Next.js "params should be awaited" error
|
||||||
// export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
// @ts-ignore - disable dynamic route params warning
|
|
||||||
export const config = {
|
|
||||||
runtime: 'nodejs'
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { userId: string } }
|
{ params }: { params: { userId?: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
@ -21,7 +17,37 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId } = params;
|
// Safely extract userId from params
|
||||||
|
const rawUserId = params?.userId;
|
||||||
|
const userId = typeof rawUserId === 'string' ? rawUserId : '';
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check for required environment variables before attempting to connect
|
||||||
|
const missingVars = [];
|
||||||
|
if (!process.env.KEYCLOAK_BASE_URL && !process.env.KEYCLOAK_ISSUER && !process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
|
||||||
|
missingVars.push('KEYCLOAK_BASE_URL or KEYCLOAK_ISSUER');
|
||||||
|
}
|
||||||
|
if (!process.env.KEYCLOAK_ADMIN_CLIENT_ID) missingVars.push('KEYCLOAK_ADMIN_CLIENT_ID');
|
||||||
|
if (!process.env.KEYCLOAK_ADMIN_USERNAME) missingVars.push('KEYCLOAK_ADMIN_USERNAME');
|
||||||
|
if (!process.env.KEYCLOAK_ADMIN_PASSWORD) missingVars.push('KEYCLOAK_ADMIN_PASSWORD');
|
||||||
|
if (!process.env.KEYCLOAK_REALM) missingVars.push('KEYCLOAK_REALM');
|
||||||
|
|
||||||
|
if (missingVars.length > 0) {
|
||||||
|
console.error(`Missing Keycloak environment variables: ${missingVars.join(', ')}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Keycloak configuration incomplete",
|
||||||
|
message: "Role management is currently unavailable due to missing configuration.",
|
||||||
|
details: `Missing: ${missingVars.join(', ')}`
|
||||||
|
},
|
||||||
|
{ status: 503 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const kcAdminClient = await getKeycloakAdminClient();
|
const kcAdminClient = await getKeycloakAdminClient();
|
||||||
|
|
||||||
// Get all available roles
|
// Get all available roles
|
||||||
@ -36,6 +62,13 @@ export async function GET(
|
|||||||
availableRoles,
|
availableRoles,
|
||||||
userRoles,
|
userRoles,
|
||||||
});
|
});
|
||||||
|
} catch (keycloakError) {
|
||||||
|
console.error("Error connecting to Keycloak:", keycloakError);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to connect to Keycloak service", details: String(keycloakError) },
|
||||||
|
{ status: 503 }
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching roles:", error);
|
console.error("Error fetching roles:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@ -47,7 +80,7 @@ export async function GET(
|
|||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { userId: string } }
|
{ params }: { params: { userId?: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
@ -55,7 +88,37 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId } = params;
|
// Safely extract userId from params
|
||||||
|
const rawUserId = params?.userId;
|
||||||
|
const userId = typeof rawUserId === 'string' ? rawUserId : '';
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check for required environment variables before attempting to connect
|
||||||
|
const missingVars = [];
|
||||||
|
if (!process.env.KEYCLOAK_BASE_URL && !process.env.KEYCLOAK_ISSUER && !process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
|
||||||
|
missingVars.push('KEYCLOAK_BASE_URL or KEYCLOAK_ISSUER');
|
||||||
|
}
|
||||||
|
if (!process.env.KEYCLOAK_ADMIN_CLIENT_ID) missingVars.push('KEYCLOAK_ADMIN_CLIENT_ID');
|
||||||
|
if (!process.env.KEYCLOAK_ADMIN_USERNAME) missingVars.push('KEYCLOAK_ADMIN_USERNAME');
|
||||||
|
if (!process.env.KEYCLOAK_ADMIN_PASSWORD) missingVars.push('KEYCLOAK_ADMIN_PASSWORD');
|
||||||
|
if (!process.env.KEYCLOAK_REALM) missingVars.push('KEYCLOAK_REALM');
|
||||||
|
|
||||||
|
if (missingVars.length > 0) {
|
||||||
|
console.error(`Missing Keycloak environment variables: ${missingVars.join(', ')}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Keycloak configuration incomplete",
|
||||||
|
message: "Role management is currently unavailable due to missing configuration.",
|
||||||
|
details: `Missing: ${missingVars.join(', ')}`
|
||||||
|
},
|
||||||
|
{ status: 503 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { roles } = await request.json();
|
const { roles } = await request.json();
|
||||||
const kcAdminClient = await getKeycloakAdminClient();
|
const kcAdminClient = await getKeycloakAdminClient();
|
||||||
|
|
||||||
@ -95,6 +158,13 @@ export async function PUT(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (keycloakError) {
|
||||||
|
console.error("Error connecting to Keycloak:", keycloakError);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to connect to Keycloak service", details: String(keycloakError) },
|
||||||
|
{ status: 503 }
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating roles:", error);
|
console.error("Error updating roles:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@ -71,46 +71,66 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|||||||
|
|
||||||
// Function to check if user has a specific role
|
// Function to check if user has a specific role
|
||||||
const hasRole = (requiredRole: string | string[] | undefined) => {
|
const hasRole = (requiredRole: string | string[] | undefined) => {
|
||||||
if (!requiredRole || !session?.user?.role) {
|
// If no role is required, allow access
|
||||||
console.log('No required role or user roles found', {
|
if (!requiredRole) {
|
||||||
requiredRole,
|
return true;
|
||||||
userRoles: session?.user?.role
|
}
|
||||||
});
|
|
||||||
|
// If no session or user roles, deny access
|
||||||
|
if (!session?.user?.role) {
|
||||||
|
console.log('No user roles found in session');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get user roles and normalize them properly
|
||||||
const userRoles = Array.isArray(session.user.role) ? session.user.role : [session.user.role];
|
const userRoles = Array.isArray(session.user.role) ? session.user.role : [session.user.role];
|
||||||
const cleanUserRoles = userRoles.map(role => role.toLowerCase());
|
|
||||||
|
|
||||||
console.log('Debug roles:', {
|
// Filter out technical/system roles that shouldn't count for permissions
|
||||||
rawUserRoles: session.user.role,
|
const ignoredRoles = ['offline_access', 'uma_authorization', 'default-roles-cercle'];
|
||||||
processedUserRoles: cleanUserRoles,
|
|
||||||
requiredRole,
|
|
||||||
pathname
|
|
||||||
});
|
|
||||||
|
|
||||||
// If requiredRole is an array, check if user has any of the roles
|
const cleanUserRoles = userRoles
|
||||||
if (Array.isArray(requiredRole)) {
|
.filter(Boolean) // Remove any null/undefined values
|
||||||
const cleanRequiredRoles = requiredRole.map(role => role.toLowerCase());
|
.filter(role => !ignoredRoles.includes(String(role))) // Filter out system roles
|
||||||
console.log('Checking multiple roles:', {
|
.map(role => {
|
||||||
requiredRoles: requiredRole,
|
if (typeof role !== 'string') return '';
|
||||||
cleanRequiredRoles,
|
return role
|
||||||
userRoles: cleanUserRoles,
|
.replace(/^\//, '') // Remove leading slash
|
||||||
hasAnyRole: cleanRequiredRoles.some(role => cleanUserRoles.includes(role)),
|
.replace(/^ROLE_/i, '') // Remove ROLE_ prefix, case insensitive
|
||||||
matchingRoles: cleanRequiredRoles.filter(role => cleanUserRoles.includes(role))
|
.replace(/^default-roles-[^/]*\//i, '') // Remove realm prefix like default-roles-cercle/
|
||||||
|
.toLowerCase();
|
||||||
|
})
|
||||||
|
.filter(role => role !== ''); // Remove empty strings
|
||||||
|
|
||||||
|
// For debugging only
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(`Role check for: ${JSON.stringify(requiredRole)}`, {
|
||||||
|
userRoles,
|
||||||
|
ignoredRoles,
|
||||||
|
cleanUserRoles,
|
||||||
});
|
});
|
||||||
return cleanRequiredRoles.some(role => cleanUserRoles.includes(role));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For single role requirement
|
// Check against array of required roles
|
||||||
|
if (Array.isArray(requiredRole)) {
|
||||||
|
const cleanRequiredRoles = requiredRole
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(role => typeof role === 'string' ? role.toLowerCase() : '')
|
||||||
|
.filter(role => role !== '');
|
||||||
|
|
||||||
|
const hasRequiredRole = cleanRequiredRoles.some(role => cleanUserRoles.includes(role));
|
||||||
|
console.log(`Array role check: Required ${JSON.stringify(cleanRequiredRoles)}, Has any: ${hasRequiredRole}`);
|
||||||
|
return hasRequiredRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against single required role
|
||||||
|
if (typeof requiredRole === 'string') {
|
||||||
const cleanRequiredRole = requiredRole.toLowerCase();
|
const cleanRequiredRole = requiredRole.toLowerCase();
|
||||||
console.log('Checking single role:', {
|
const hasRequiredRole = cleanUserRoles.includes(cleanRequiredRole);
|
||||||
requiredRole,
|
console.log(`Single role check: Required "${cleanRequiredRole}", Has: ${hasRequiredRole}`);
|
||||||
cleanRequiredRole,
|
return hasRequiredRole;
|
||||||
userRoles: cleanUserRoles,
|
}
|
||||||
hasRole: cleanUserRoles.includes(cleanRequiredRole)
|
|
||||||
});
|
return false;
|
||||||
return cleanUserRoles.includes(cleanRequiredRole);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Base menu items (available for everyone)
|
// Base menu items (available for everyone)
|
||||||
@ -171,35 +191,39 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|||||||
icon: Palette,
|
icon: Palette,
|
||||||
href: "/design",
|
href: "/design",
|
||||||
iframe: process.env.NEXT_PUBLIC_IFRAME_ARTLAB_URL,
|
iframe: process.env.NEXT_PUBLIC_IFRAME_ARTLAB_URL,
|
||||||
requiredRole: "Expression",
|
requiredRole: "expression",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Gite",
|
title: "Gite",
|
||||||
icon: GitFork,
|
icon: GitFork,
|
||||||
href: "/gite",
|
href: "/gite",
|
||||||
iframe: process.env.NEXT_PUBLIC_IFRAME_GITE_URL,
|
iframe: process.env.NEXT_PUBLIC_IFRAME_GITE_URL,
|
||||||
requiredRole: ["Coding", "DataIntelligence"],
|
requiredRole: ["coding", "dataintelligence"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Calcul",
|
title: "Calcul",
|
||||||
icon: Calculator,
|
icon: Calculator,
|
||||||
href: "/calcul",
|
href: "/calcul",
|
||||||
iframe: process.env.NEXT_PUBLIC_IFRAME_CALCULATION_URL,
|
iframe: process.env.NEXT_PUBLIC_IFRAME_CALCULATION_URL,
|
||||||
requiredRole: "DataIntelligence",
|
requiredRole: "dataintelligence",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Médiation",
|
title: "Médiation",
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
href: "/mediation",
|
href: "/mediation",
|
||||||
iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL,
|
iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL,
|
||||||
requiredRole: ["Mediation", "Expression"],
|
requiredRole: ["mediation", "expression"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Combine base items with role-specific items based on user roles
|
// Combine base items with role-specific items based on user roles
|
||||||
const visibleMenuItems = [
|
const visibleMenuItems = [
|
||||||
...baseMenuItems,
|
...baseMenuItems,
|
||||||
...roleSpecificItems.filter(item => hasRole(item.requiredRole))
|
...roleSpecificItems.filter(item => {
|
||||||
|
const isVisible = hasRole(item.requiredRole);
|
||||||
|
console.log(`Item ${item.title} with requiredRole ${JSON.stringify(item.requiredRole)} is ${isVisible ? 'visible' : 'hidden'}`);
|
||||||
|
return isVisible;
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleNavigation = (href: string, external?: boolean) => {
|
const handleNavigation = (href: string, external?: boolean) => {
|
||||||
@ -264,6 +288,57 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Debug display only in development */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<div className="p-2 mt-4 border border-gray-200 rounded bg-blue-50 text-xs">
|
||||||
|
<p className="font-bold">Debug Info:</p>
|
||||||
|
<div className="mt-1">
|
||||||
|
<p>User: {session?.user?.name}</p>
|
||||||
|
<p>Email: {session?.user?.email}</p>
|
||||||
|
<details>
|
||||||
|
<summary className="cursor-pointer">User Roles</summary>
|
||||||
|
<pre className="mt-1 p-1 bg-white text-[10px] overflow-x-auto">
|
||||||
|
{JSON.stringify(session?.user?.role, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary className="cursor-pointer">Normalized Roles</summary>
|
||||||
|
<pre className="mt-1 p-1 bg-white text-[10px] overflow-x-auto">
|
||||||
|
{JSON.stringify(
|
||||||
|
Array.isArray(session?.user?.role)
|
||||||
|
? session.user.role
|
||||||
|
.filter(role => typeof role === 'string')
|
||||||
|
.filter(role => !['offline_access', 'uma_authorization', 'default-roles-cercle'].includes(role))
|
||||||
|
.map(role =>
|
||||||
|
role
|
||||||
|
.replace(/^\//, '')
|
||||||
|
.replace(/^ROLE_/i, '')
|
||||||
|
.replace(/^default-roles-[^/]*\//i, '')
|
||||||
|
.toLowerCase()
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary className="cursor-pointer">Role-Based Items</summary>
|
||||||
|
<div className="mt-1 p-1 bg-white text-[10px] overflow-x-auto">
|
||||||
|
{roleSpecificItems.map(item => {
|
||||||
|
const isVisible = hasRole(item.requiredRole);
|
||||||
|
return (
|
||||||
|
<div key={item.title} className="mb-1 pb-1 border-b">
|
||||||
|
<p><strong>Item:</strong> {item.title}</p>
|
||||||
|
<p><strong>Required Role:</strong> {JSON.stringify(item.requiredRole)}</p>
|
||||||
|
<p><strong>Visible:</strong> {isVisible ? '✅' : '❌'}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -28,6 +28,23 @@ export async function getKeycloakAdminClient(): Promise<KcAdminClient> {
|
|||||||
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
|
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
|
||||||
const realmName = process.env.KEYCLOAK_REALM;
|
const realmName = process.env.KEYCLOAK_REALM;
|
||||||
|
|
||||||
|
// Validate required environment variables
|
||||||
|
if (!keycloakUrl) {
|
||||||
|
console.error('Missing Keycloak URL. Please add one of these to your .env file: KEYCLOAK_BASE_URL, KEYCLOAK_ISSUER, or NEXT_PUBLIC_KEYCLOAK_ISSUER');
|
||||||
|
throw new Error('Missing Keycloak URL configuration');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adminClientId || !adminUsername || !adminPassword || !realmName) {
|
||||||
|
const missing = [];
|
||||||
|
if (!adminClientId) missing.push('KEYCLOAK_ADMIN_CLIENT_ID');
|
||||||
|
if (!adminUsername) missing.push('KEYCLOAK_ADMIN_USERNAME');
|
||||||
|
if (!adminPassword) missing.push('KEYCLOAK_ADMIN_PASSWORD');
|
||||||
|
if (!realmName) missing.push('KEYCLOAK_REALM');
|
||||||
|
|
||||||
|
console.error(`Missing Keycloak admin credentials in .env: ${missing.join(', ')}`);
|
||||||
|
throw new Error('Missing Keycloak admin credentials');
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Connecting to Keycloak at ${keycloakUrl}, realm: ${realmName}`);
|
console.log(`Connecting to Keycloak at ${keycloakUrl}, realm: ${realmName}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user