diff --git a/lib/keycloak.ts b/lib/keycloak.ts new file mode 100644 index 00000000..027c72cb --- /dev/null +++ b/lib/keycloak.ts @@ -0,0 +1,61 @@ +import KcAdminClient from '@keycloak/keycloak-admin-client'; +import { Credentials } from '@keycloak/keycloak-admin-client/lib/utils/auth'; + +// Cache the KC admin client to avoid multiple initializations +let kcAdminClient: KcAdminClient | null = null; + +/** + * Returns a configured Keycloak admin client instance + */ +export async function getKeycloakAdminClient(): Promise { + // If we already have a client initialized, return it + if (kcAdminClient) { + try { + // Try to refresh the token before returning + await kcAdminClient.auth({ + grantType: 'refresh_token', + refreshToken: kcAdminClient.refreshToken + }); + return kcAdminClient; + } catch (error) { + // Token refresh failed, initialize a new client + console.log('Token refresh failed, initializing new Keycloak admin client'); + kcAdminClient = null; + } + } + + // Initialize a new client + kcAdminClient = new KcAdminClient({ + baseUrl: process.env.KEYCLOAK_URL || process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER?.replace('/realms/cercle', '') || '', + realmName: process.env.KEYCLOAK_REALM || 'cercle', + }); + + // Authenticate the client + await kcAdminClient.auth({ + grantType: 'client_credentials', + clientId: process.env.KEYCLOAK_ADMIN_CLIENT_ID || process.env.KEYCLOAK_CLIENT_ID || '', + clientSecret: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET || process.env.KEYCLOAK_CLIENT_SECRET || '', + } as Credentials); + + return kcAdminClient; +} + +/** + * Get a list of users from Keycloak with optional search parameters + */ +export async function getKeycloakUsers(search?: string, first?: number, max?: number) { + const client = await getKeycloakAdminClient(); + return client.users.find({ + search, + first, + max, + }); +} + +/** + * Get a specific user from Keycloak by ID + */ +export async function getKeycloakUser(userId: string) { + const client = await getKeycloakAdminClient(); + return client.users.findOne({ id: userId }); +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index edc9c235..1c5b8b78 100644 --- a/next.config.js +++ b/next.config.js @@ -43,10 +43,6 @@ const nextConfig = { compress: true, // Enable production source maps for better performance debugging productionBrowserSourceMaps: false, - // Optimize fonts - optimizeFonts: true, - // Enable React strict mode for better development experience - reactStrictMode: true, // Reduce build output size poweredByHeader: false, // Add cache headers to immutable assets diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 68510c81..4d0e4d3d 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -426,6 +426,20 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keycloak/keycloak-admin-client": { + "version": "26.2.2", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-26.2.2.tgz", + "integrity": "sha512-H0U3jjkXRHR0zU9xVcv5+GzWpDCAEab4NHKCbilVZSjrSLzqbGLMTEiGAo81NpHilseTiFpzEkz2qFm6/Hm0BA==", + "license": "Apache-2.0", + "dependencies": { + "camelize-ts": "^3.0.0", + "url-join": "^5.0.0", + "url-template": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@next/bundle-analyzer": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.3.1.tgz", @@ -2952,6 +2966,15 @@ "node": ">= 6" } }, + "node_modules/camelize-ts": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz", + "integrity": "sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001692", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", @@ -7106,6 +7129,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/url-template": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", + "integrity": "sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA==", + "license": "BSD-3-Clause", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", diff --git a/package-lock.json b/package-lock.json index 9008044c..a8214950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@fullcalendar/react": "^6.1.15", "@hookform/resolvers": "^3.9.1", + "@keycloak/keycloak-admin-client": "^26.2.2", "@nextcloud/files": "^2.1.0", "@prisma/client": "^6.4.1", "@radix-ui/react-accordion": "^1.2.2", @@ -1292,6 +1293,20 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keycloak/keycloak-admin-client": { + "version": "26.2.2", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-26.2.2.tgz", + "integrity": "sha512-H0U3jjkXRHR0zU9xVcv5+GzWpDCAEab4NHKCbilVZSjrSLzqbGLMTEiGAo81NpHilseTiFpzEkz2qFm6/Hm0BA==", + "license": "Apache-2.0", + "dependencies": { + "camelize-ts": "^3.0.0", + "url-join": "^5.0.0", + "url-template": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@next/bundle-analyzer": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.3.1.tgz", @@ -3930,6 +3945,15 @@ "node": ">= 6" } }, + "node_modules/camelize-ts": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz", + "integrity": "sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001692", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", @@ -8084,6 +8108,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/url-template": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", + "integrity": "sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA==", + "license": "BSD-3-Clause", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", diff --git a/package.json b/package.json index 313eae01..782710cb 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@fullcalendar/react": "^6.1.15", "@hookform/resolvers": "^3.9.1", + "@keycloak/keycloak-admin-client": "^26.2.2", "@nextcloud/files": "^2.1.0", "@prisma/client": "^6.4.1", "@radix-ui/react-accordion": "^1.2.2", diff --git a/yarn.lock b/yarn.lock index 12775409..751ed85b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -222,6 +222,15 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@keycloak/keycloak-admin-client@^26.2.2": + version "26.2.2" + resolved "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-26.2.2.tgz" + integrity sha512-H0U3jjkXRHR0zU9xVcv5+GzWpDCAEab4NHKCbilVZSjrSLzqbGLMTEiGAo81NpHilseTiFpzEkz2qFm6/Hm0BA== + dependencies: + camelize-ts "^3.0.0" + url-join "^5.0.0" + url-template "^3.1.1" + "@next/bundle-analyzer@^15.3.1": version "15.3.1" resolved "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.3.1.tgz" @@ -1432,6 +1441,11 @@ camelcase-css@^2.0.1: resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== +camelize-ts@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz" + integrity sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ== + caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: version "1.0.30001692" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz" @@ -3914,6 +3928,11 @@ url-parse@^1.5.10: querystringify "^2.1.1" requires-port "^1.0.0" +url-template@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz" + integrity sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA== + use-callback-ref@^1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz"