From 358e9c0ad14ccce5ed318ce456d41718827a95d1 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 9 Aug 2025 18:22:34 +0200 Subject: [PATCH] Add Keycloak integration support - Update domain configuration to portal subdomain with HTTPS - Add keycloak_id field to member creation and update operations - Add API endpoint for linking Keycloak accounts to existing members --- .env.example | 2 +- .../api/admin/link-keycloak-account.post.ts | 82 +++++++++++++++++++ server/utils/nocodb.ts | 6 +- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 server/api/admin/link-keycloak-account.post.ts diff --git a/.env.example b/.env.example index 036ee1b..1923ae5 100644 --- a/.env.example +++ b/.env.example @@ -35,5 +35,5 @@ NUXT_SESSION_SECRET=your-48-character-session-secret-key-here NUXT_ENCRYPTION_KEY=your-32-character-encryption-key-here # Public Configuration -NUXT_PUBLIC_DOMAIN=monacousa.org +NUXT_PUBLIC_DOMAIN=https://portal.monacousa.org # diff --git a/server/api/admin/link-keycloak-account.post.ts b/server/api/admin/link-keycloak-account.post.ts new file mode 100644 index 0000000..9297018 --- /dev/null +++ b/server/api/admin/link-keycloak-account.post.ts @@ -0,0 +1,82 @@ +export default defineEventHandler(async (event) => { + console.log('[api/admin/link-keycloak-account.post] Manual Keycloak account linking'); + + try { + // Validate session and require admin privileges + const sessionManager = createSessionManager(); + const cookieHeader = getCookie(event, 'monacousa-session') ? getHeader(event, 'cookie') : undefined; + const session = sessionManager.getSession(cookieHeader); + + if (!session?.user || session.user.tier !== 'admin') { + throw createError({ + statusCode: 403, + statusMessage: 'Admin privileges required' + }); + } + + const body = await readBody(event); + const { memberId, keycloakId, keycloakEmail } = body; + + if (!memberId || !keycloakId) { + throw createError({ + statusCode: 400, + statusMessage: 'Member ID and Keycloak ID are required' + }); + } + + // Get member data + const { getMemberById, updateMember } = await import('~/server/utils/nocodb'); + const member = await getMemberById(memberId); + + if (!member) { + throw createError({ + statusCode: 404, + statusMessage: 'Member not found' + }); + } + + // Verify the Keycloak user exists + const { createKeycloakAdminClient } = await import('~/server/utils/keycloak-admin'); + const keycloakAdmin = createKeycloakAdminClient(); + + try { + const keycloakUser = await keycloakAdmin.getUserById(keycloakId); + console.log('[link-keycloak-account] Found Keycloak user:', keycloakUser.email); + } catch (error) { + throw createError({ + statusCode: 404, + statusMessage: 'Keycloak user not found' + }); + } + + // Update member record with keycloak_id + console.log('[link-keycloak-account] Linking member', memberId, 'to Keycloak user', keycloakId); + await updateMember(memberId, { keycloak_id: keycloakId }); + + console.log('[link-keycloak-account] ✅ Successfully linked accounts'); + + return { + success: true, + message: 'Keycloak account successfully linked to member', + data: { + member_id: memberId, + keycloak_id: keycloakId, + member_email: member.email, + keycloak_email: keycloakEmail, + name: `${member.first_name} ${member.last_name}` + } + }; + + } catch (error: any) { + console.error('[link-keycloak-account] ❌ Linking failed:', error); + + if (error.statusCode) { + throw error; + } + + throw createError({ + statusCode: 500, + statusMessage: error.message || 'Failed to link Keycloak account' + }); + } +}); diff --git a/server/utils/nocodb.ts b/server/utils/nocodb.ts index a33fb8a..2bd86ed 100644 --- a/server/utils/nocodb.ts +++ b/server/utils/nocodb.ts @@ -310,7 +310,8 @@ export const createMember = async (data: Partial): Promise => { "payment_due_date", "membership_status", "address", - "member_since" + "member_since", + "keycloak_id" ]; // Filter the data to only include allowed fields @@ -392,7 +393,8 @@ export const updateMember = async (id: string, data: Partial, retryCount "payment_due_date", "membership_status", "address", - "member_since" + "member_since", + "keycloak_id" ]; // Filter the data to only include allowed fields