Add Keycloak group management for member portal access control
All checks were successful
Build And Push Image / docker (push) Successful in 3m53s
All checks were successful
Build And Push Image / docker (push) Successful in 3m53s
- 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:
@@ -28,8 +28,16 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
console.log('[api/admin/nocodb-config.get] Admin access confirmed for:', session.user.email);
|
||||
|
||||
// Get current configuration using the new admin config system
|
||||
const { getCurrentConfig } = await import('~/server/utils/admin-config');
|
||||
// Force reload and get current configuration using the new admin config system
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
// Get reCAPTCHA configuration
|
||||
const { getRecaptchaConfig } = await import('~/server/utils/admin-config');
|
||||
// Force reload and get reCAPTCHA configuration
|
||||
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();
|
||||
|
||||
return {
|
||||
|
||||
@@ -24,8 +24,16 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
console.log('[api/admin/registration-config.get] Authorized admin:', session.user.email);
|
||||
|
||||
// Get registration configuration
|
||||
const { getRegistrationConfig } = await import('~/server/utils/admin-config');
|
||||
// Force reload and get registration configuration
|
||||
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();
|
||||
|
||||
return {
|
||||
|
||||
@@ -24,9 +24,24 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
console.log('[api/admin/smtp-config.get] Authorized admin:', session.user.email);
|
||||
|
||||
// Get SMTP configuration
|
||||
const { getSMTPConfig } = await import('~/server/utils/admin-config');
|
||||
// Force reload and get SMTP configuration
|
||||
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();
|
||||
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
|
||||
const safeConfig = {
|
||||
|
||||
@@ -99,9 +99,9 @@ export default defineEventHandler(async (event) => {
|
||||
nocodbMemberId: memberId
|
||||
};
|
||||
|
||||
// 7. Create Keycloak user with role-based registration
|
||||
console.log('[api/members/[id]/create-portal-account.post] Creating Keycloak user with role-based system...');
|
||||
const keycloakId = await keycloakAdmin.createUserWithRoleRegistration({
|
||||
// 7. Create Keycloak user with group-based registration
|
||||
console.log('[api/members/[id]/create-portal-account.post] Creating Keycloak user with group-based system...');
|
||||
const keycloakId = await keycloakAdmin.createUserWithGroupAssignment({
|
||||
email: member.email,
|
||||
firstName: member.first_name,
|
||||
lastName: member.last_name,
|
||||
@@ -192,16 +192,14 @@ export default defineEventHandler(async (event) => {
|
||||
* This function analyzes member information to assign appropriate portal roles
|
||||
*/
|
||||
function determineMembershipTier(member: any): 'user' | 'board' | 'admin' {
|
||||
// Check for explicit tier indicators in member data
|
||||
// This could be based on membership type, special flags, or other criteria
|
||||
// Use stored portal_group value if available and valid
|
||||
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
|
||||
// In the future, you might want to check specific fields like:
|
||||
// - member.membership_type
|
||||
// - member.is_board_member
|
||||
// - member.is_admin
|
||||
// - specific email domains for admins
|
||||
// - etc.
|
||||
// Fallback logic for legacy data or when portal_group is not set
|
||||
console.log('[determineMembershipTier] No valid portal_group found, using fallback logic');
|
||||
|
||||
// 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
|
||||
console.log('[determineMembershipTier] Defaulting to user tier');
|
||||
return 'user';
|
||||
}
|
||||
|
||||
124
server/api/members/[id]/keycloak-groups.get.ts
Normal file
124
server/api/members/[id]/keycloak-groups.get.ts
Normal file
@@ -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'
|
||||
});
|
||||
}
|
||||
});
|
||||
178
server/api/members/[id]/keycloak-groups.put.ts
Normal file
178
server/api/members/[id]/keycloak-groups.put.ts
Normal file
@@ -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'
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user