Add Keycloak group management for member portal access control
Build And Push Image / docker (push) Successful in 3m53s
Details
Build And Push Image / docker (push) Successful in 3m53s
Details
- Add portal access control section to EditMemberDialog for admins - Implement API endpoints for managing member Keycloak groups - Add group selection UI with user/board/admin access levels - Enhance admin config with reload functionality - Support real-time group synchronization and status feedback
This commit is contained in:
parent
5371ad4fa2
commit
4b1a77de90
|
|
@ -172,6 +172,52 @@
|
||||||
:error-messages="getFieldError('payment_due_date')"
|
:error-messages="getFieldError('payment_due_date')"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Portal Access Control Section (Admin Only) -->
|
||||||
|
<template v-if="isAdmin && member?.keycloak_id">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-divider class="my-4" />
|
||||||
|
<h3 class="text-h6 mb-4 text-primary">Portal Access Control</h3>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="form.portal_group"
|
||||||
|
:items="portalGroupOptions"
|
||||||
|
label="Portal Access Level"
|
||||||
|
variant="outlined"
|
||||||
|
hint="Controls user's access level in the portal"
|
||||||
|
persistent-hint
|
||||||
|
:loading="groupLoading"
|
||||||
|
:disabled="groupLoading"
|
||||||
|
:error="hasFieldError('portal_group')"
|
||||||
|
:error-messages="getFieldError('portal_group')"
|
||||||
|
>
|
||||||
|
<template #prepend-inner>
|
||||||
|
<v-icon color="primary">mdi-shield-account</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-alert
|
||||||
|
v-if="groupSyncStatus"
|
||||||
|
:type="groupSyncStatus.type"
|
||||||
|
:text="groupSyncStatus.message"
|
||||||
|
density="compact"
|
||||||
|
class="mb-0"
|
||||||
|
/>
|
||||||
|
<v-chip
|
||||||
|
v-else-if="member.keycloak_id"
|
||||||
|
color="success"
|
||||||
|
size="small"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<v-icon start size="small">mdi-check-circle</v-icon>
|
||||||
|
Portal Account Active
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
</template>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
@ -233,7 +279,8 @@ const form = ref({
|
||||||
member_since: '',
|
member_since: '',
|
||||||
current_year_dues_paid: 'false',
|
current_year_dues_paid: 'false',
|
||||||
membership_date_paid: '',
|
membership_date_paid: '',
|
||||||
payment_due_date: ''
|
payment_due_date: '',
|
||||||
|
portal_group: 'user'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Additional form state
|
// Additional form state
|
||||||
|
|
@ -243,6 +290,71 @@ const phoneData = ref(null);
|
||||||
// Error handling
|
// Error handling
|
||||||
const fieldErrors = ref<Record<string, string>>({});
|
const fieldErrors = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
// Auth state
|
||||||
|
const { user, isAdmin } = useAuth();
|
||||||
|
|
||||||
|
// Portal group management
|
||||||
|
const groupLoading = ref(false);
|
||||||
|
const groupSyncStatus = ref<{ type: 'success' | 'warning' | 'error'; message: string } | null>(null);
|
||||||
|
const originalPortalGroup = ref<string>('user');
|
||||||
|
|
||||||
|
const portalGroupOptions = [
|
||||||
|
{ title: 'User - Basic Access', value: 'user' },
|
||||||
|
{ title: 'Board Member - Extended Access', value: 'board' },
|
||||||
|
{ title: 'Administrator - Full Access', value: 'admin' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Watch for portal group changes and sync with Keycloak
|
||||||
|
watch(() => form.value.portal_group, async (newGroup, oldGroup) => {
|
||||||
|
if (!props.member?.keycloak_id || !isAdmin || newGroup === oldGroup || newGroup === originalPortalGroup.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[EditMemberDialog] Portal group changed:', oldGroup, '->', newGroup);
|
||||||
|
|
||||||
|
groupLoading.value = true;
|
||||||
|
groupSyncStatus.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[EditMemberDialog] Updating Keycloak groups for member:', props.member.Id);
|
||||||
|
|
||||||
|
const response = await $fetch(`/api/members/${props.member.Id}/keycloak-groups`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { newGroup }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
groupSyncStatus.value = {
|
||||||
|
type: 'success',
|
||||||
|
message: `Successfully changed access level to ${newGroup}`
|
||||||
|
};
|
||||||
|
originalPortalGroup.value = newGroup; // Update original to prevent re-trigger
|
||||||
|
console.log('[EditMemberDialog] Group change successful:', response.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to update access level');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[EditMemberDialog] Failed to update Keycloak groups:', error);
|
||||||
|
|
||||||
|
groupSyncStatus.value = {
|
||||||
|
type: 'error',
|
||||||
|
message: error.data?.message || error.message || 'Failed to update access level'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Revert the form value on error
|
||||||
|
form.value.portal_group = oldGroup || 'user';
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
groupLoading.value = false;
|
||||||
|
|
||||||
|
// Clear status after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
groupSyncStatus.value = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Watch dues paid switch
|
// Watch dues paid switch
|
||||||
watch(duesPaid, (newValue) => {
|
watch(duesPaid, (newValue) => {
|
||||||
form.value.current_year_dues_paid = newValue ? 'true' : 'false';
|
form.value.current_year_dues_paid = newValue ? 'true' : 'false';
|
||||||
|
|
@ -334,7 +446,8 @@ const populateForm = () => {
|
||||||
member_since: formatDateForInput(member.member_since || ''),
|
member_since: formatDateForInput(member.member_since || ''),
|
||||||
current_year_dues_paid: member.current_year_dues_paid || 'false',
|
current_year_dues_paid: member.current_year_dues_paid || 'false',
|
||||||
membership_date_paid: formatDateForInput(member.membership_date_paid || ''),
|
membership_date_paid: formatDateForInput(member.membership_date_paid || ''),
|
||||||
payment_due_date: formatDateForInput(member.payment_due_date || '')
|
payment_due_date: formatDateForInput(member.payment_due_date || ''),
|
||||||
|
portal_group: member.portal_group || 'user'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set dues paid switch based on the string value
|
// Set dues paid switch based on the string value
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,16 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-config.get] Admin access confirmed for:', session.user.email);
|
console.log('[api/admin/nocodb-config.get] Admin access confirmed for:', session.user.email);
|
||||||
|
|
||||||
// Get current configuration using the new admin config system
|
// Force reload and get current configuration using the new admin config system
|
||||||
const { getCurrentConfig } = await import('~/server/utils/admin-config');
|
const { getCurrentConfig, reloadAdminConfig } = await import('~/server/utils/admin-config');
|
||||||
|
|
||||||
|
// Force reload configuration to ensure we have the latest settings
|
||||||
|
try {
|
||||||
|
await reloadAdminConfig();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[api/admin/nocodb-config.get] Failed to reload config, using cached version:', error);
|
||||||
|
}
|
||||||
|
|
||||||
const settings = await getCurrentConfig();
|
const settings = await getCurrentConfig();
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-config.get] ✅ Settings retrieved successfully');
|
console.log('[api/admin/nocodb-config.get] ✅ Settings retrieved successfully');
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,16 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
console.log('[api/admin/recaptcha-config.get] Authorized admin:', session.user.email);
|
console.log('[api/admin/recaptcha-config.get] Authorized admin:', session.user.email);
|
||||||
|
|
||||||
// Get reCAPTCHA configuration
|
// Force reload and get reCAPTCHA configuration
|
||||||
const { getRecaptchaConfig } = await import('~/server/utils/admin-config');
|
const { getRecaptchaConfig, reloadAdminConfig } = await import('~/server/utils/admin-config');
|
||||||
|
|
||||||
|
// Force reload configuration to ensure we have the latest settings
|
||||||
|
try {
|
||||||
|
await reloadAdminConfig();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[api/admin/recaptcha-config.get] Failed to reload config, using cached version:', error);
|
||||||
|
}
|
||||||
|
|
||||||
const config = getRecaptchaConfig();
|
const config = getRecaptchaConfig();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,16 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
console.log('[api/admin/registration-config.get] Authorized admin:', session.user.email);
|
console.log('[api/admin/registration-config.get] Authorized admin:', session.user.email);
|
||||||
|
|
||||||
// Get registration configuration
|
// Force reload and get registration configuration
|
||||||
const { getRegistrationConfig } = await import('~/server/utils/admin-config');
|
const { getRegistrationConfig, reloadAdminConfig } = await import('~/server/utils/admin-config');
|
||||||
|
|
||||||
|
// Force reload configuration to ensure we have the latest settings
|
||||||
|
try {
|
||||||
|
await reloadAdminConfig();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[api/admin/registration-config.get] Failed to reload config, using cached version:', error);
|
||||||
|
}
|
||||||
|
|
||||||
const config = getRegistrationConfig();
|
const config = getRegistrationConfig();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,24 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
console.log('[api/admin/smtp-config.get] Authorized admin:', session.user.email);
|
console.log('[api/admin/smtp-config.get] Authorized admin:', session.user.email);
|
||||||
|
|
||||||
// Get SMTP configuration
|
// Force reload and get SMTP configuration
|
||||||
const { getSMTPConfig } = await import('~/server/utils/admin-config');
|
const { getSMTPConfig, reloadAdminConfig } = await import('~/server/utils/admin-config');
|
||||||
|
|
||||||
|
// Force reload configuration to ensure we have the latest settings
|
||||||
|
try {
|
||||||
|
await reloadAdminConfig();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[api/admin/smtp-config.get] Failed to reload config, using cached version:', error);
|
||||||
|
}
|
||||||
|
|
||||||
const config = getSMTPConfig();
|
const config = getSMTPConfig();
|
||||||
|
console.log('[api/admin/smtp-config.get] Current SMTP config status:', {
|
||||||
|
host: config.host || 'not set',
|
||||||
|
port: config.port || 'not set',
|
||||||
|
hasUsername: !!config.username,
|
||||||
|
hasPassword: !!config.password,
|
||||||
|
fromAddress: config.fromAddress || 'not set'
|
||||||
|
});
|
||||||
|
|
||||||
// Hide password for security
|
// Hide password for security
|
||||||
const safeConfig = {
|
const safeConfig = {
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,9 @@ export default defineEventHandler(async (event) => {
|
||||||
nocodbMemberId: memberId
|
nocodbMemberId: memberId
|
||||||
};
|
};
|
||||||
|
|
||||||
// 7. Create Keycloak user with role-based registration
|
// 7. Create Keycloak user with group-based registration
|
||||||
console.log('[api/members/[id]/create-portal-account.post] Creating Keycloak user with role-based system...');
|
console.log('[api/members/[id]/create-portal-account.post] Creating Keycloak user with group-based system...');
|
||||||
const keycloakId = await keycloakAdmin.createUserWithRoleRegistration({
|
const keycloakId = await keycloakAdmin.createUserWithGroupAssignment({
|
||||||
email: member.email,
|
email: member.email,
|
||||||
firstName: member.first_name,
|
firstName: member.first_name,
|
||||||
lastName: member.last_name,
|
lastName: member.last_name,
|
||||||
|
|
@ -192,16 +192,14 @@ export default defineEventHandler(async (event) => {
|
||||||
* This function analyzes member information to assign appropriate portal roles
|
* This function analyzes member information to assign appropriate portal roles
|
||||||
*/
|
*/
|
||||||
function determineMembershipTier(member: any): 'user' | 'board' | 'admin' {
|
function determineMembershipTier(member: any): 'user' | 'board' | 'admin' {
|
||||||
// Check for explicit tier indicators in member data
|
// Use stored portal_group value if available and valid
|
||||||
// This could be based on membership type, special flags, or other criteria
|
if (member.portal_group && ['user', 'board', 'admin'].includes(member.portal_group)) {
|
||||||
|
console.log(`[determineMembershipTier] Using stored portal_group: ${member.portal_group}`);
|
||||||
|
return member.portal_group as 'user' | 'board' | 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
// For now, default all members to 'user' tier
|
// Fallback logic for legacy data or when portal_group is not set
|
||||||
// In the future, you might want to check specific fields like:
|
console.log('[determineMembershipTier] No valid portal_group found, using fallback logic');
|
||||||
// - member.membership_type
|
|
||||||
// - member.is_board_member
|
|
||||||
// - member.is_admin
|
|
||||||
// - specific email domains for admins
|
|
||||||
// - etc.
|
|
||||||
|
|
||||||
// Example logic (uncomment and modify as needed):
|
// Example logic (uncomment and modify as needed):
|
||||||
/*
|
/*
|
||||||
|
|
@ -215,5 +213,6 @@ function determineMembershipTier(member: any): 'user' | 'board' | 'admin' {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Default to user tier for all members
|
// Default to user tier for all members
|
||||||
|
console.log('[determineMembershipTier] Defaulting to user tier');
|
||||||
return 'user';
|
return 'user';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] =========================');
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] GET /api/members/:id/keycloak-groups - Get member Keycloak groups');
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Authentication required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require admin privileges for group management
|
||||||
|
if (session.user.tier !== 'admin') {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Admin privileges required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] Authorized admin user:', session.user.email);
|
||||||
|
|
||||||
|
// Get member ID from route parameter
|
||||||
|
const memberId = getRouterParam(event, 'id');
|
||||||
|
if (!memberId) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Member ID is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] Processing member ID:', memberId);
|
||||||
|
|
||||||
|
// 1. Get member data
|
||||||
|
const { getMemberById } = await import('~/server/utils/nocodb');
|
||||||
|
const member = await getMemberById(memberId);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Member not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if member has portal account
|
||||||
|
if (!member.keycloak_id) {
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] Member has no portal account');
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Member has no portal account'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] Found member with Keycloak ID:', member.keycloak_id);
|
||||||
|
|
||||||
|
// 3. Get user's current groups from Keycloak
|
||||||
|
const { createKeycloakAdminClient } = await import('~/server/utils/keycloak-admin');
|
||||||
|
const keycloakAdmin = createKeycloakAdminClient();
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] Fetching user groups from Keycloak...');
|
||||||
|
const userGroups = await keycloakAdmin.getUserGroups(member.keycloak_id);
|
||||||
|
|
||||||
|
// 4. Determine primary group (user/board/admin)
|
||||||
|
const primaryGroups = userGroups.filter(g => ['user', 'board', 'admin'].includes(g.name || ''));
|
||||||
|
const primaryGroup = primaryGroups.length > 0 ? primaryGroups[0].name : 'user';
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] Primary group determined:', primaryGroup);
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] Total groups:', userGroups.length);
|
||||||
|
|
||||||
|
// 5. Compare with database portal_group field
|
||||||
|
const databaseGroup = member.portal_group || 'user';
|
||||||
|
const groupsInSync = primaryGroup === databaseGroup;
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.get] ✅ Successfully retrieved group information');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
member_id: memberId,
|
||||||
|
keycloak_id: member.keycloak_id,
|
||||||
|
primary_group: primaryGroup,
|
||||||
|
database_group: databaseGroup,
|
||||||
|
groups_in_sync: groupsInSync,
|
||||||
|
all_groups: userGroups.map(g => ({
|
||||||
|
id: g.id,
|
||||||
|
name: g.name,
|
||||||
|
path: g.path
|
||||||
|
})),
|
||||||
|
primary_groups: primaryGroups.map(g => ({
|
||||||
|
id: g.id,
|
||||||
|
name: g.name,
|
||||||
|
path: g.path
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[api/members/[id]/keycloak-groups.get] ❌ Failed to get member groups:', error);
|
||||||
|
|
||||||
|
// If it's already an HTTP error, re-throw it
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle specific Keycloak errors
|
||||||
|
if (error.message?.includes('Failed to get user groups')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to retrieve user groups from Keycloak. Check service account permissions.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, wrap it in a generic error
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: error.message || 'Failed to get member Keycloak groups'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] =========================');
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] PUT /api/members/:id/keycloak-groups - Update member Keycloak groups');
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Authentication required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require admin privileges for group management
|
||||||
|
if (session.user.tier !== 'admin') {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Admin privileges required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Authorized admin user:', session.user.email);
|
||||||
|
|
||||||
|
// Get member ID from route parameter
|
||||||
|
const memberId = getRouterParam(event, 'id');
|
||||||
|
if (!memberId) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Member ID is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
const body = await readBody(event);
|
||||||
|
const { newGroup } = body;
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Processing member ID:', memberId, 'new group:', newGroup);
|
||||||
|
|
||||||
|
// Validate new group
|
||||||
|
if (!['user', 'board', 'admin'].includes(newGroup)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Invalid group. Must be one of: user, board, admin'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if member has portal account
|
||||||
|
if (!member.keycloak_id) {
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Member has no portal account');
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Member has no portal account'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Found member with Keycloak ID:', member.keycloak_id);
|
||||||
|
|
||||||
|
// 3. Get current group status
|
||||||
|
const { createKeycloakAdminClient } = await import('~/server/utils/keycloak-admin');
|
||||||
|
const keycloakAdmin = createKeycloakAdminClient();
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Getting current groups...');
|
||||||
|
const currentGroups = await keycloakAdmin.getUserGroups(member.keycloak_id);
|
||||||
|
const currentPrimaryGroups = currentGroups.filter(g => ['user', 'board', 'admin'].includes(g.name || ''));
|
||||||
|
const currentPrimaryGroup = currentPrimaryGroups.length > 0 ? currentPrimaryGroups[0].name : 'user';
|
||||||
|
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Current primary group:', currentPrimaryGroup);
|
||||||
|
|
||||||
|
// 4. Check if change is needed
|
||||||
|
if (currentPrimaryGroup === newGroup) {
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] User already in target group, syncing database...');
|
||||||
|
|
||||||
|
// Still sync database to ensure consistency
|
||||||
|
await updateMember(memberId, { portal_group: newGroup });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `User was already in ${newGroup} group. Database synced.`,
|
||||||
|
data: {
|
||||||
|
member_id: memberId,
|
||||||
|
keycloak_id: member.keycloak_id,
|
||||||
|
old_group: currentPrimaryGroup,
|
||||||
|
new_group: newGroup,
|
||||||
|
changed: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Change user's primary group in Keycloak
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Changing user primary group in Keycloak...');
|
||||||
|
await keycloakAdmin.changeUserPrimaryGroup(member.keycloak_id, newGroup);
|
||||||
|
|
||||||
|
// 6. Update database portal_group field
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Updating database portal_group field...');
|
||||||
|
await updateMember(memberId, { portal_group: newGroup });
|
||||||
|
|
||||||
|
// 7. Verify the change
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] Verifying group change...');
|
||||||
|
const updatedGroups = await keycloakAdmin.getUserGroups(member.keycloak_id);
|
||||||
|
const newPrimaryGroups = updatedGroups.filter(g => ['user', 'board', 'admin'].includes(g.name || ''));
|
||||||
|
const verifiedNewGroup = newPrimaryGroups.length > 0 ? newPrimaryGroups[0].name : 'user';
|
||||||
|
|
||||||
|
const changeSuccessful = verifiedNewGroup === newGroup;
|
||||||
|
|
||||||
|
if (changeSuccessful) {
|
||||||
|
console.log('[api/members/[id]/keycloak-groups.put] ✅ Group change successful and verified');
|
||||||
|
} else {
|
||||||
|
console.error('[api/members/[id]/keycloak-groups.put] ❌ Group change verification failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: changeSuccessful,
|
||||||
|
message: changeSuccessful
|
||||||
|
? `Successfully changed user from ${currentPrimaryGroup} to ${newGroup} group`
|
||||||
|
: `Group change attempted but verification failed. Current group: ${verifiedNewGroup}`,
|
||||||
|
data: {
|
||||||
|
member_id: memberId,
|
||||||
|
keycloak_id: member.keycloak_id,
|
||||||
|
old_group: currentPrimaryGroup,
|
||||||
|
new_group: newGroup,
|
||||||
|
verified_group: verifiedNewGroup,
|
||||||
|
changed: changeSuccessful,
|
||||||
|
database_updated: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[api/members/[id]/keycloak-groups.put] ❌ Failed to update member groups:', error);
|
||||||
|
|
||||||
|
// If it's already an HTTP error, re-throw it
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle specific Keycloak errors
|
||||||
|
if (error.message?.includes('Failed to get user groups')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to retrieve user groups from Keycloak. Check service account permissions.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message?.includes('Invalid group name')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message?.includes('Group not found')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: `Keycloak group not found. Ensure the groups 'user', 'board', and 'admin' exist in Keycloak.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, wrap it in a generic error
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: error.message || 'Failed to update member Keycloak groups'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -566,6 +566,35 @@ export function getSMTPConfig(): SMTPConfig {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force reload configuration from file and update cache
|
||||||
|
*/
|
||||||
|
export async function reloadAdminConfig(): Promise<void> {
|
||||||
|
console.log('[admin-config] Force reloading configuration from file...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clear cache to force reload
|
||||||
|
configCache = null;
|
||||||
|
|
||||||
|
// Load fresh from file
|
||||||
|
await loadAdminConfig();
|
||||||
|
|
||||||
|
// Update global nocodb configuration
|
||||||
|
try {
|
||||||
|
const { setGlobalNocoDBConfig } = await import('./nocodb');
|
||||||
|
setGlobalNocoDBConfig(getEffectiveNocoDBConfig());
|
||||||
|
console.log('[admin-config] Global configuration updated after reload');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[admin-config] Failed to update global configuration after reload:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[admin-config] ✅ Configuration reloaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[admin-config] ❌ Failed to reload configuration:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize configuration system on server startup
|
* Initialize configuration system on server startup
|
||||||
*/
|
*/
|
||||||
|
|
@ -574,9 +603,59 @@ export async function initAdminConfig(): Promise<void> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureConfigDir();
|
await ensureConfigDir();
|
||||||
await loadAdminConfig();
|
|
||||||
|
// Load configuration and initialize cache
|
||||||
|
const config = await loadAdminConfig();
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
console.log('[admin-config] ✅ Configuration loaded from existing file');
|
||||||
|
console.log('[admin-config] Configuration summary:', {
|
||||||
|
nocodb: {
|
||||||
|
url: config.nocodb.url || 'not set',
|
||||||
|
hasApiKey: !!config.nocodb.apiKey,
|
||||||
|
baseId: config.nocodb.baseId || 'not set'
|
||||||
|
},
|
||||||
|
smtp: {
|
||||||
|
host: config.smtp?.host || 'not set',
|
||||||
|
port: config.smtp?.port || 'not set',
|
||||||
|
hasAuth: !!(config.smtp?.username && config.smtp?.password)
|
||||||
|
},
|
||||||
|
recaptcha: {
|
||||||
|
hasSiteKey: !!config.recaptcha?.siteKey,
|
||||||
|
hasSecretKey: !!config.recaptcha?.secretKey
|
||||||
|
},
|
||||||
|
registration: {
|
||||||
|
membershipFee: config.registration?.membershipFee || 'not set',
|
||||||
|
hasIban: !!config.registration?.iban
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[admin-config] ⚠️ No existing configuration file found, using defaults');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[admin-config] ✅ Admin configuration system initialized');
|
console.log('[admin-config] ✅ Admin configuration system initialized');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[admin-config] ❌ Failed to initialize admin configuration:', error);
|
console.error('[admin-config] ❌ Failed to initialize admin configuration:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration status for health checks
|
||||||
|
*/
|
||||||
|
export function getConfigStatus(): {
|
||||||
|
loaded: boolean;
|
||||||
|
nocodb: boolean;
|
||||||
|
smtp: boolean;
|
||||||
|
recaptcha: boolean;
|
||||||
|
registration: boolean;
|
||||||
|
} {
|
||||||
|
const config = configCache;
|
||||||
|
|
||||||
|
return {
|
||||||
|
loaded: !!config,
|
||||||
|
nocodb: !!(config?.nocodb?.url && config?.nocodb?.apiKey && config?.nocodb?.baseId),
|
||||||
|
smtp: !!(config?.smtp?.host && config?.smtp?.port),
|
||||||
|
recaptcha: !!(config?.recaptcha?.siteKey && config?.recaptcha?.secretKey),
|
||||||
|
registration: !!(config?.registration?.membershipFee && config?.registration?.iban)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -443,13 +443,31 @@ let emailServiceInstance: EmailService | null = null;
|
||||||
* Get or create EmailService instance with current SMTP config
|
* Get or create EmailService instance with current SMTP config
|
||||||
*/
|
*/
|
||||||
export async function getEmailService(): Promise<EmailService> {
|
export async function getEmailService(): Promise<EmailService> {
|
||||||
const { getSMTPConfig } = await import('./admin-config');
|
const { getSMTPConfig, reloadAdminConfig } = await import('./admin-config');
|
||||||
|
|
||||||
|
// Force reload configuration to ensure we have the latest settings
|
||||||
|
try {
|
||||||
|
await reloadAdminConfig();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[EmailService] Failed to reload admin config, using cached version:', error);
|
||||||
|
}
|
||||||
|
|
||||||
const config = getSMTPConfig();
|
const config = getSMTPConfig();
|
||||||
|
|
||||||
|
console.log('[EmailService] Current SMTP config:', {
|
||||||
|
host: config.host || 'not set',
|
||||||
|
port: config.port || 'not set',
|
||||||
|
secure: config.secure,
|
||||||
|
hasUsername: !!config.username,
|
||||||
|
hasPassword: !!config.password,
|
||||||
|
fromAddress: config.fromAddress || 'not set',
|
||||||
|
fromName: config.fromName || 'not set'
|
||||||
|
});
|
||||||
|
|
||||||
if (!emailServiceInstance) {
|
if (!emailServiceInstance) {
|
||||||
emailServiceInstance = new EmailService(config);
|
emailServiceInstance = new EmailService(config);
|
||||||
} else {
|
} else {
|
||||||
// Update config in case it changed
|
// Always update config in case it changed
|
||||||
emailServiceInstance.updateConfig(config);
|
emailServiceInstance.updateConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -225,32 +225,60 @@ export class KeycloakAdminClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign a realm role to a user
|
* Assign a realm role to a user (requires proper Keycloak admin permissions)
|
||||||
*/
|
*/
|
||||||
async assignRealmRoleToUser(userId: string, roleName: string): Promise<void> {
|
async assignRealmRoleToUser(userId: string, roleName: string): Promise<void> {
|
||||||
const adminToken = await this.getAdminToken();
|
const adminToken = await this.getAdminToken();
|
||||||
const adminBaseUrl = this.config.issuer.replace('/realms/', '/admin/realms/');
|
const adminBaseUrl = this.config.issuer.replace('/realms/', '/admin/realms/');
|
||||||
|
|
||||||
// First get the role
|
console.log(`[keycloak-admin] Assigning realm role ${roleName} to user ${userId}`);
|
||||||
const role = await this.getRealmRole(roleName, adminToken);
|
|
||||||
|
|
||||||
// Then assign it to user
|
try {
|
||||||
const response = await fetch(`${adminBaseUrl}/users/${userId}/role-mappings/realm`, {
|
// First try to get the role to ensure it exists
|
||||||
method: 'POST',
|
const role = await this.getRealmRole(roleName, adminToken);
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${adminToken}`,
|
// Then assign it to user
|
||||||
'Content-Type': 'application/json',
|
const response = await fetch(`${adminBaseUrl}/users/${userId}/role-mappings/realm`, {
|
||||||
'User-Agent': 'MonacoUSA-Portal/1.0'
|
method: 'POST',
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify([role])
|
'Authorization': `Bearer ${adminToken}`,
|
||||||
});
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'MonacoUSA-Portal/1.0'
|
||||||
|
},
|
||||||
|
body: JSON.stringify([role])
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text().catch(() => 'Unknown error');
|
const errorText = await response.text().catch(() => 'Unknown error');
|
||||||
throw new Error(`Failed to assign role to user: ${response.status} - ${errorText}`);
|
throw new Error(`Failed to assign role to user: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[keycloak-admin] ✅ Successfully assigned role ${roleName} to user ${userId}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Provide detailed error information for troubleshooting
|
||||||
|
if (error.message?.includes('403')) {
|
||||||
|
console.error(`[keycloak-admin] ❌ Permission denied when assigning role ${roleName} to user ${userId}:`);
|
||||||
|
console.error(`[keycloak-admin] The Keycloak service account needs the following permissions:`);
|
||||||
|
console.error(`[keycloak-admin] 1. 'view-realm' permission to read realm roles`);
|
||||||
|
console.error(`[keycloak-admin] 2. 'manage-users' permission to assign roles to users`);
|
||||||
|
console.error(`[keycloak-admin] 3. Access to the '${roleName}' realm role`);
|
||||||
|
console.warn(`[keycloak-admin] User ${userId} created successfully but role ${roleName} could not be assigned due to insufficient permissions.`);
|
||||||
|
console.warn(`[keycloak-admin] Please assign the role manually in Keycloak admin console.`);
|
||||||
|
// Don't throw - allow user creation to complete
|
||||||
|
return;
|
||||||
|
} else if (error.message?.includes('404')) {
|
||||||
|
console.error(`[keycloak-admin] ❌ Role ${roleName} does not exist in Keycloak realm.`);
|
||||||
|
console.error(`[keycloak-admin] Please create the '${roleName}' role in Keycloak admin console.`);
|
||||||
|
console.warn(`[keycloak-admin] User ${userId} created successfully but role ${roleName} does not exist.`);
|
||||||
|
// Don't throw - allow user creation to complete
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.error(`[keycloak-admin] ❌ Unexpected error assigning role ${roleName} to user ${userId}:`, error);
|
||||||
|
console.warn(`[keycloak-admin] User ${userId} created successfully but role assignment failed.`);
|
||||||
|
// Don't throw - allow user creation to complete
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[keycloak-admin] Assigned role ${roleName} to user ${userId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -393,9 +421,9 @@ export class KeycloakAdminClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create user with role-based registration (enhanced version)
|
* Create user with group-based registration (replaces role-based method)
|
||||||
*/
|
*/
|
||||||
async createUserWithRoleRegistration(userData: {
|
async createUserWithGroupAssignment(userData: {
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
|
@ -406,6 +434,8 @@ export class KeycloakAdminClient {
|
||||||
const adminToken = await this.getAdminToken();
|
const adminToken = await this.getAdminToken();
|
||||||
const adminBaseUrl = this.config.issuer.replace('/realms/', '/admin/realms/');
|
const adminBaseUrl = this.config.issuer.replace('/realms/', '/admin/realms/');
|
||||||
|
|
||||||
|
console.log(`[keycloak-admin] Creating user with group assignment: ${userData.email}, tier: ${userData.membershipTier || 'user'}`);
|
||||||
|
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
const existingUsers = await this.findUserByEmail(userData.email, adminToken);
|
const existingUsers = await this.findUserByEmail(userData.email, adminToken);
|
||||||
if (existingUsers.length > 0) {
|
if (existingUsers.length > 0) {
|
||||||
|
|
@ -461,19 +491,42 @@ export class KeycloakAdminClient {
|
||||||
throw new Error('Failed to extract user ID from response');
|
throw new Error('Failed to extract user ID from response');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign appropriate realm role
|
console.log(`[keycloak-admin] Created user ${userData.email} with ID: ${userId}`);
|
||||||
const roleName = `monaco-${userData.membershipTier || 'user'}`;
|
|
||||||
|
// Assign appropriate group instead of role
|
||||||
|
const groupName = userData.membershipTier || 'user';
|
||||||
|
console.log(`[keycloak-admin] Assigning user to group: ${groupName}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.assignRealmRoleToUser(userId, roleName);
|
const groupId = await this.getGroupByPath(groupName);
|
||||||
} catch (error) {
|
await this.assignUserToGroup(userId, groupId);
|
||||||
console.warn(`[keycloak-admin] Failed to assign role ${roleName} to user ${userId}:`, error);
|
console.log(`[keycloak-admin] ✅ Successfully assigned user ${userId} to group: ${groupName}`);
|
||||||
// Don't fail the entire operation if role assignment fails
|
} catch (error: any) {
|
||||||
|
console.warn(`[keycloak-admin] ⚠️ Failed to assign user ${userId} to group ${groupName}:`, error);
|
||||||
|
console.warn(`[keycloak-admin] User will receive default group assignment from Keycloak realm settings`);
|
||||||
|
// Don't fail the entire operation - user gets default group automatically
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[keycloak-admin] Created user ${userData.email} with ID: ${userId} and role: ${roleName}`);
|
console.log(`[keycloak-admin] ✅ User creation with group assignment completed: ${userData.email}`);
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use createUserWithGroupAssignment() instead
|
||||||
|
* This method has permission issues with role assignment
|
||||||
|
*/
|
||||||
|
async createUserWithRoleRegistration(userData: {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username?: string;
|
||||||
|
membershipTier?: 'user' | 'board' | 'admin';
|
||||||
|
membershipData?: MembershipProfileData;
|
||||||
|
}): Promise<string> {
|
||||||
|
console.warn('[keycloak-admin] createUserWithRoleRegistration is deprecated. Use createUserWithGroupAssignment instead.');
|
||||||
|
throw new Error('Method deprecated. Use createUserWithGroupAssignment() instead - it resolves permission issues.');
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// SESSION MANAGEMENT METHODS
|
// SESSION MANAGEMENT METHODS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -644,6 +697,99 @@ export class KeycloakAdminClient {
|
||||||
console.log(`[keycloak-admin] Assigned user ${userId} to group ${groupId}`);
|
console.log(`[keycloak-admin] Assigned user ${userId} to group ${groupId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all groups for a user
|
||||||
|
*/
|
||||||
|
async getUserGroups(userId: string): Promise<KeycloakGroupRepresentation[]> {
|
||||||
|
const adminToken = await this.getAdminToken();
|
||||||
|
const adminBaseUrl = this.config.issuer.replace('/realms/', '/admin/realms/');
|
||||||
|
|
||||||
|
console.log(`[keycloak-admin] Getting groups for user: ${userId}`);
|
||||||
|
|
||||||
|
const response = await fetch(`${adminBaseUrl}/users/${userId}/groups`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${adminToken}`,
|
||||||
|
'User-Agent': 'MonacoUSA-Portal/1.0'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text().catch(() => 'Unknown error');
|
||||||
|
throw new Error(`Failed to get user groups: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = await response.json();
|
||||||
|
console.log(`[keycloak-admin] ✅ Retrieved ${groups.length} groups for user ${userId}`);
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove user from a group
|
||||||
|
*/
|
||||||
|
async removeUserFromGroup(userId: string, groupId: string): Promise<void> {
|
||||||
|
const adminToken = await this.getAdminToken();
|
||||||
|
const adminBaseUrl = this.config.issuer.replace('/realms/', '/admin/realms/');
|
||||||
|
|
||||||
|
console.log(`[keycloak-admin] Removing user ${userId} from group ${groupId}`);
|
||||||
|
|
||||||
|
const response = await fetch(`${adminBaseUrl}/users/${userId}/groups/${groupId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${adminToken}`,
|
||||||
|
'User-Agent': 'MonacoUSA-Portal/1.0'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text().catch(() => 'Unknown error');
|
||||||
|
throw new Error(`Failed to remove user from group: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[keycloak-admin] ✅ Removed user ${userId} from group ${groupId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change user's primary group (remove from old primary groups, add to new)
|
||||||
|
*/
|
||||||
|
async changeUserPrimaryGroup(userId: string, newGroupName: string): Promise<void> {
|
||||||
|
console.log(`[keycloak-admin] Changing user ${userId} primary group to: ${newGroupName}`);
|
||||||
|
|
||||||
|
if (!['user', 'board', 'admin'].includes(newGroupName)) {
|
||||||
|
throw new Error(`Invalid group name: ${newGroupName}. Must be one of: user, board, admin`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current user groups
|
||||||
|
const currentGroups = await this.getUserGroups(userId);
|
||||||
|
const primaryGroups = currentGroups.filter(g => ['user', 'board', 'admin'].includes(g.name || ''));
|
||||||
|
|
||||||
|
console.log(`[keycloak-admin] User currently in ${primaryGroups.length} primary groups: ${primaryGroups.map(g => g.name).join(', ')}`);
|
||||||
|
|
||||||
|
// Remove from old primary groups
|
||||||
|
for (const group of primaryGroups) {
|
||||||
|
if (group.id && group.name !== newGroupName) {
|
||||||
|
console.log(`[keycloak-admin] Removing user from old group: ${group.name}`);
|
||||||
|
await this.removeUserFromGroup(userId, group.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to new group (if not already in it)
|
||||||
|
const alreadyInNewGroup = primaryGroups.some(g => g.name === newGroupName);
|
||||||
|
if (!alreadyInNewGroup) {
|
||||||
|
console.log(`[keycloak-admin] Adding user to new group: ${newGroupName}`);
|
||||||
|
const newGroupId = await this.getGroupByPath(newGroupName);
|
||||||
|
await this.assignUserToGroup(userId, newGroupId);
|
||||||
|
} else {
|
||||||
|
console.log(`[keycloak-admin] User already in target group: ${newGroupName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[keycloak-admin] ✅ Successfully changed user ${userId} primary group to: ${newGroupName}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[keycloak-admin] ❌ Error changing user primary group:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ADVANCED EMAIL WORKFLOWS
|
// ADVANCED EMAIL WORKFLOWS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ export interface Member {
|
||||||
member_since: string;
|
member_since: string;
|
||||||
keycloak_id?: string; // New field for linking to Keycloak user
|
keycloak_id?: string; // New field for linking to Keycloak user
|
||||||
registration_date?: string; // New field for tracking registration date
|
registration_date?: string; // New field for tracking registration date
|
||||||
|
portal_group?: 'user' | 'board' | 'admin'; // Portal access level group
|
||||||
|
|
||||||
// Computed fields (added by processing)
|
// Computed fields (added by processing)
|
||||||
FullName?: string;
|
FullName?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue