Add Keycloak group management for member portal access control
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:
2025-08-13 16:31:54 +02:00
parent 5371ad4fa2
commit 4b1a77de90
12 changed files with 749 additions and 52 deletions

View File

@@ -172,6 +172,52 @@
:error-messages="getFieldError('payment_due_date')"
/>
</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-form>
</v-card-text>
@@ -233,7 +279,8 @@ const form = ref({
member_since: '',
current_year_dues_paid: 'false',
membership_date_paid: '',
payment_due_date: ''
payment_due_date: '',
portal_group: 'user'
});
// Additional form state
@@ -243,6 +290,71 @@ const phoneData = ref(null);
// Error handling
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(duesPaid, (newValue) => {
form.value.current_year_dues_paid = newValue ? 'true' : 'false';
@@ -334,7 +446,8 @@ const populateForm = () => {
member_since: formatDateForInput(member.member_since || ''),
current_year_dues_paid: member.current_year_dues_paid || 'false',
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