-
-
Notifications
You must be signed in to change notification settings - Fork 15
Add user and group management UI #376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
implement pagination for user list in GroupDetail
group detail improvment
new components for group and user detail sections
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (1)
frontend/src/common/backend.ts (1)
114-116: Property name mismatch with backend API persists.The backend's
MemberDto.javaserializes this field as"role"(via@JsonProperty("role")as shown in the relevant code snippet), but the frontend type usesvaultRole. This will causevaultRoleto beundefinedat runtime.
🧹 Nitpick comments (3)
frontend/src/components/authority/UserEditCreate.vue (1)
108-111: Consider usingtype="button"explicitly for role removal chips.The role removal buttons inside the ListboxButton work correctly with
@click.stop, but adding explicittype="button"would be more defensive against any future form submission behavior.🔎 Suggested improvement
- <button v-for="role in selectedRoles" :key="role" class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20" @click.stop="removeRole(role)"> + <button v-for="role in selectedRoles" :key="role" type="button" class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20" @click.stop="removeRole(role)">frontend/src/common/backend.ts (2)
97-101: Consider removing redundantdevicesproperty fromUserDtoWithDetails.
UserDtoalready hasdevices: DeviceDto[](line 83). Redeclaring it inUserDtoWithDetailsis redundant. If the intent is to ensure devices are always present (not optional), the baseUserDtoalready makes it non-optional.🔎 Suggested simplification
export type UserDtoWithDetails = UserDto & { groups: GroupDto[]; - devices: DeviceDto[]; legacyDevices: DeviceDto[]; }
135-137: Remove outdated TODO comment -VaultDtoWithRoleis in use.The TODO comment
// TODO: unused??is incorrect.VaultDtoWithRoleis used in:
UserDto.accessibleVaults(line 84)GroupDtoWithDetails.vaults(line 141)🔎 Proposed fix
-export type VaultDtoWithRole = VaultDto & { // TODO: unused?? +export type VaultDtoWithRole = VaultDto & { role: VaultRole; }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
frontend/src/common/backend.ts(6 hunks)frontend/src/components/authority/GroupList.vue(1 hunks)frontend/src/components/authority/UserEditCreate.vue(1 hunks)frontend/src/components/authority/UserList.vue(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/components/authority/UserList.vue
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-07-27T13:14:40.963Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 282
File: frontend/src/common/userdata.ts:82-88
Timestamp: 2024-07-27T13:14:40.963Z
Learning: The error handling suggestion for `createBrowserKeys` in `frontend/src/common/userdata.ts` was discussed and resolved in PR #282, and should not be repeated in future reviews.
Applied to files:
frontend/src/components/authority/UserEditCreate.vuefrontend/src/common/backend.ts
📚 Learning: 2025-12-18T08:32:28.253Z
Learnt from: iammajid
Repo: cryptomator/hub PR: 376
File: frontend/src/components/authority/GroupInfo.vue:5-5
Timestamp: 2025-12-18T08:32:28.253Z
Learning: In the frontend codebase, ensure that authority DTOs for users and groups always include a pictureUrl by having AuthorityService.fillInMissingPicture() run at the service layer (as in backend.ts). As a result, components rendering users or groups (e.g., GroupInfo.vue and related components) can safely rely on pictureUrl being present and should not implement their own fallback logic for missing images. When reviewing, verify that the service layer populates pictureUrl for all user/group DTOs and that UI components do not duplicate fallback handling.
Applied to files:
frontend/src/components/authority/UserEditCreate.vuefrontend/src/common/backend.tsfrontend/src/components/authority/GroupList.vue
🧬 Code graph analysis (1)
frontend/src/common/backend.ts (3)
backend/src/main/java/org/cryptomator/hub/api/GroupDto.java (1)
GroupDto(10-46)backend/src/main/java/org/cryptomator/hub/api/AuthorityDto.java (1)
AuthorityDto(8-45)backend/src/main/java/org/cryptomator/hub/api/MemberDto.java (1)
MemberDto(8-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (10)
frontend/src/components/authority/UserEditCreate.vue (3)
1-30: LGTM! Template structure and profile picture handling are well-implemented.The loading state, breadcrumb navigation for edit/create modes, and profile picture preview with Jdenticon fallback are correctly structured. The conditional rendering chain (
v-if/v-else-if/v-else) for image display handles all cases appropriately.
279-280: Verify thatisEditModeremains correct after user creation.
userIdis declared withletand reassigned on line 473 after creating a user. SinceisEditModeis a computed property checking!!userId, it would becometrueafter assignment. However, since the component navigates away immediately after (line 482), this shouldn't cause issues in practice.
484-498: LGTM! Error handling appropriately distinguishes conflict types.The 409 conflict handling correctly differentiates between email and username conflicts, displaying field-specific error messages. The fallback to
submitErrorfor other errors is appropriate.frontend/src/components/authority/GroupList.vue (3)
17-51: LGTM! Mobile card layout is well-structured.The mobile layout correctly displays group information with a responsive card design, action menu, and member/vault counts. The click handler on the card navigates to the group detail page while the menu button stops propagation appropriately.
114-142: LGTM! Empty state and pagination logic is correct.When
sortedGroups.length === 0,showPaginationisfalse(since 0 is not > pageSize), so pagination won't display. The empty states are correctly chained withv-if/v-else-ifand mutually exclusive from the content sections which checkpaginatedGroups.length > 0.
202-211: LGTM! Error state handling is properly implemented.The
onFetchErrorref is now correctly cleared at the start offetchData()(line 203), ensuring that a successful retry clears any previous error state. This addresses the previously flagged issue.frontend/src/common/backend.ts (4)
66-67: LGTM! RealmRole type correctly defined.The
RealmRoletype aligns with the Keycloak realm roles used in the backend for admin and vault creation permissions.
302-353: LGTM! GroupService implementation follows established patterns.The new
GroupServiceclass:
- Properly calls
AuthorityService.fillInMissingPicture()for all group and member DTOs, consistent with the learned pattern that UI components rely onpictureUrlbeing present.- Uses
rethrowAndConvertIfExpectedfor proper error handling.- Returns
Promise<void>for delete operations with.then(() => {})pattern.Based on learnings, the service layer correctly populates
pictureUrlfor all DTOs.
369-393: LGTM! UserService updates correctly implemented.The new and updated methods:
removeUsernow correctly returnsPromise<void>(past issue addressed).- All methods apply
fillInMissingPicture()consistently.- Error handling follows established patterns.
524-526: LGTM! GroupService properly registered in services export.The new
GroupServiceis correctly instantiated and exposed via the services object, making it accessible asbackend.groups.*.
| <div v-if="paginatedGroups.length > 0" class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8 hidden md:block"> | ||
| <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"> | ||
| <div class="shadow-sm overflow-hidden border-b border-gray-200 sm:rounded-lg"> | ||
| <table class="min-w-full divide-y divide-gray-200" aria-describedby="groupListTitle"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing element for aria-describedby reference.
The table has aria-describedby="groupListTitle" but there's no element with id="groupListTitle" in the template. Either add the referenced element or remove the attribute.
🔎 Proposed fix - Option A: Add the referenced element
<h2 class="text-2xl font-bold leading-9 text-gray-900 sm:text-3xl sm:truncate mb-4">
{{ t('groups.title') }}
</h2>
+ <p id="groupListTitle" class="sr-only">{{ t('groupList.tableDescription') }}</p>🔎 Proposed fix - Option B: Remove the invalid reference
- <table class="min-w-full divide-y divide-gray-200" aria-describedby="groupListTitle">
+ <table class="min-w-full divide-y divide-gray-200">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <table class="min-w-full divide-y divide-gray-200" aria-describedby="groupListTitle"> | |
| <table class="min-w-full divide-y divide-gray-200"> |
🤖 Prompt for AI Agents
In frontend/src/components/authority/GroupList.vue around line 57, the table
includes aria-describedby="groupListTitle" but there is no element with
id="groupListTitle"; either add a corresponding element (for example a <caption>
or visually-hidden heading with id="groupListTitle" placed immediately before or
inside the table that conveys the table's title/description) and ensure it is
accessible (visible or screen-reader-only), or remove the aria-describedby
attribute if no separate descriptive element is needed.
| <div class="mt-1 md:mt-0 md:col-span-2 lg:col-span-1"> | ||
| <div class="relative"> | ||
| <input id="passwordConfirm" v-model="passwordConfirm" :type="passwordInputType" :class="[errors.passwordConfirm ? 'border-red-300 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:ring-primary focus:border-primary', 'block w-full max-w-md shadow-sm sm:text-sm rounded-md pr-10']"/> | ||
| <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('userEditCreate.showPassword') : t('userEditCreate.hidePassword')" @click="togglePasswordVisibility"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent i18n key namespace for password visibility aria-label.
Line 158 uses t('common.showPassword') / t('common.hidePassword'), but line 189 uses t('userEditCreate.showPassword') / t('userEditCreate.hidePassword'). These should be consistent.
🔎 Proposed fix
- <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('userEditCreate.showPassword') : t('userEditCreate.hidePassword')" @click="togglePasswordVisibility">
+ <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('common.showPassword') : t('common.hidePassword')" @click="togglePasswordVisibility">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('userEditCreate.showPassword') : t('userEditCreate.hidePassword')" @click="togglePasswordVisibility"> | |
| <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('common.showPassword') : t('common.hidePassword')" @click="togglePasswordVisibility"> |
🤖 Prompt for AI Agents
In frontend/src/components/authority/UserEditCreate.vue around line 189, the
aria-label for the password visibility toggle uses the userEditCreate namespace
while other occurrences use the common namespace; change the keys to
t('common.showPassword') and t('common.hidePassword') to match line 158 and keep
i18n usage consistent across the component (update both the true and false
branches of the conditional and scan for any other mismatched uses in this file
to align them to the common namespace).
| initialUserData.value = { | ||
| firstName: firstName.value, | ||
| lastName: lastName.value, | ||
| username: username.value, | ||
| email: email.value, | ||
| roles: [...selectedRoles.value], | ||
| previewUrl: pictureUrl.value | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optimistic state update may cause UX confusion on API failure.
initialUserData is updated before the API call succeeds. If the API call fails, userDataHasUnsavedChanges will return false (since initial matches current), hiding the "unsaved changes" warning even though data wasn't actually saved. Consider moving this update after the successful API response.
🔎 Proposed fix
processing.value = true;
firstName.value = firstName.value.trim();
lastName.value = lastName.value.trim();
username.value = username.value.trim();
email.value = email.value.trim();
pictureUrl.value = pictureUrl.value.trim();
- initialUserData.value = {
- firstName: firstName.value,
- lastName: lastName.value,
- username: username.value,
- email: email.value,
- roles: [...selectedRoles.value],
- previewUrl: pictureUrl.value
- };
-
try {
if (isEditMode.value) {
await backend.users.updateUser(userId, {
// ...
});
} else {
const createdUser = await backend.users.createUser({
// ...
});
userId = createdUser.id;
}
+ initialUserData.value = {
+ firstName: firstName.value,
+ lastName: lastName.value,
+ username: username.value,
+ email: email.value,
+ roles: [...selectedRoles.value],
+ previewUrl: pictureUrl.value
+ };
+
userSaved.value = true;
processing.value = false;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In frontend/src/components/authority/UserEditCreate.vue around lines 444 to 451,
initialUserData is being set optimistically before the save API call completes
which can clear the "unsaved changes" state on API failure; move the assignment
of initialUserData into the success branch of the API response (after confirming
the server saved changes) so initialUserData is only updated on success, and
leave it unchanged (or revert UI state) on error to preserve the unsaved-changes
detection and show appropriate error handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (1)
frontend/src/components/authority/GroupMemberList.vue (1)
32-32: Missing alt attribute on member avatar image.The avatar image lacks an
altattribute, which is required for accessibility compliance. Screen readers need meaningful text to convey the image content to users.🔎 Proposed fix
- <img :src="member.pictureUrl" class="w-8 h-8 rounded-full object-cover border border-gray-300" /> + <img :src="member.pictureUrl" :alt="member.name || member.email || t('common.member')" class="w-8 h-8 rounded-full object-cover border border-gray-300" />
🧹 Nitpick comments (5)
frontend/src/components/authority/UserInfo.vue (1)
5-5: Add alt attribute for accessibility.The avatar image is missing an
altattribute, which is important for screen readers and accessibility compliance.Proposed fix
- <img :src="user.pictureUrl" class="w-48 h-48 rounded-full object-cover border border-gray-300 mb-4" /> + <img :src="user.pictureUrl" :alt="user.name" class="w-48 h-48 rounded-full object-cover border border-gray-300 mb-4" />frontend/src/components/authority/UserDetail.vue (2)
22-22: Localize screen reader text.The "Open options menu" string is hardcoded in English. Use i18n for consistency and accessibility across locales.
Proposed fix
- <span class="sr-only">Open options menu</span> + <span class="sr-only">{{ t('common.openOptionsMenu') }}</span>Add the corresponding translation key to your i18n files.
128-139: Consider showing user-facing feedback on fetch failure.When
backend.users.getUserfails, the error is only logged to the console. The user sees the default empty state with no indication that something went wrong. Consider displaying an error message or redirecting gracefully.Proposed approach
const loading = ref<boolean>(true); +const fetchError = ref<Error | null>(null); onMounted(async () => { try { user.value = await backend.users.getUser(props.id); // Load roles user.value.realmRoles = (user.value.realmRoles || []).filter((r: string) => r === 'admin' || r === 'create-vaults'); } catch (error) { console.error('Failed to fetch user:', error); + fetchError.value = error instanceof Error ? error : new Error('Unknown error'); } finally { loading.value = false; } });Then render an error state in the template when
fetchErroris set.frontend/src/components/authority/UserDeleteDialog.vue (1)
28-28: Add alt attribute for accessibility.The user avatar image is missing an
altattribute for screen readers.Proposed fix
- <img :src="user.pictureUrl" class="w-8 h-8 rounded-full border" /> + <img :src="user.pictureUrl" :alt="user.name" class="w-8 h-8 rounded-full border" />frontend/src/components/authority/GroupMemberList.vue (1)
30-30: Simplify the key to use onlymember.id.The current key concatenates
member.id + member.name, but sincemember.idis unique, the concatenation is unnecessary. Additionally,member.namemight be undefined, which would result in a key like"idundefined".🔎 Proposed fix
- <tr v-for="member in paginatedMembers" :key="member.id + member.name"> + <tr v-for="member in paginatedMembers" :key="member.id">
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
frontend/src/components/authority/GroupDeleteDialog.vue(1 hunks)frontend/src/components/authority/GroupDetail.vue(1 hunks)frontend/src/components/authority/GroupMemberList.vue(1 hunks)frontend/src/components/authority/GroupMemberRemoveDialog.vue(1 hunks)frontend/src/components/authority/UserDeleteDialog.vue(1 hunks)frontend/src/components/authority/UserDetail.vue(1 hunks)frontend/src/components/authority/UserGroupsList.vue(1 hunks)frontend/src/components/authority/UserInfo.vue(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/src/components/authority/GroupMemberRemoveDialog.vue
- frontend/src/components/authority/GroupDeleteDialog.vue
- frontend/src/components/authority/UserGroupsList.vue
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-18T08:32:28.253Z
Learnt from: iammajid
Repo: cryptomator/hub PR: 376
File: frontend/src/components/authority/GroupInfo.vue:5-5
Timestamp: 2025-12-18T08:32:28.253Z
Learning: In the frontend codebase, ensure that authority DTOs for users and groups always include a pictureUrl by having AuthorityService.fillInMissingPicture() run at the service layer (as in backend.ts). As a result, components rendering users or groups (e.g., GroupInfo.vue and related components) can safely rely on pictureUrl being present and should not implement their own fallback logic for missing images. When reviewing, verify that the service layer populates pictureUrl for all user/group DTOs and that UI components do not duplicate fallback handling.
Applied to files:
frontend/src/components/authority/UserDeleteDialog.vuefrontend/src/components/authority/GroupDetail.vuefrontend/src/components/authority/GroupMemberList.vuefrontend/src/components/authority/UserDetail.vuefrontend/src/components/authority/UserInfo.vue
📚 Learning: 2024-07-27T13:14:40.963Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 282
File: frontend/src/common/userdata.ts:82-88
Timestamp: 2024-07-27T13:14:40.963Z
Learning: The error handling suggestion for `createBrowserKeys` in `frontend/src/common/userdata.ts` was discussed and resolved in PR #282, and should not be repeated in future reviews.
Applied to files:
frontend/src/components/authority/GroupDetail.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (7)
frontend/src/components/authority/UserInfo.vue (1)
39-53: LGTM!The script logic is well implemented:
- Proper use of Composition API with TypeScript
sortedRolescomputed property correctly handles nullablerealmRoleswith nullish coalescing- Locale-agnostic sorting with
localeCompareis appropriateBased on learnings, the component correctly relies on
pictureUrlbeing populated by the service layer rather than implementing fallback logic.frontend/src/components/authority/UserDetail.vue (2)
40-65: Responsive layout duplication is acceptable here.The desktop two-column and mobile single-column layouts have different component ordering and structure. While this duplicates some markup, it's a reasonable trade-off for distinct responsive behaviors.
1-39: LGTM for overall structure and logic.The component is well-structured with:
- Clean separation of loading/content states
- Proper use of Composition API
- Dialog flow with
nextTickfor DOM timing- Group merge logic with deduplication and sorting
Based on learnings, the component correctly relies on
pictureUrlbeing populated at the service layer.Also applies to: 66-144
frontend/src/components/authority/UserDeleteDialog.vue (3)
64-70: Local interface is acceptable for component isolation.While this
Userinterface duplicates fields available in backend DTOs, it defines only the minimal required subset for this component's props. This provides clear documentation of the component's contract and allows flexibility in what can be passed.
100-112: LGTM!The delete operation is well-implemented:
- Clears previous error state before attempting
- Properly handles both Error instances and unknown error types
- Emits events in correct order (delete before closing)
- User-facing error display is in the template (lines 45-47)
1-55: LGTM for dialog structure.The Headless UI dialog implementation follows best practices:
- Proper transition handling with
TransitionRootandTransitionChild- Emits
closevia@after-leaveto ensure animation completes- Form with
novalidateand@submit.preventfor controlled submission- Accessible with
DialogTitleand proper button rolesfrontend/src/components/authority/GroupMemberList.vue (1)
109-112: Consider notifying parent after member removal.The
onGroupMemberDeletedfunction updates the localmembersarray but doesn't invokeprops.onSavedto notify the parent component. In contrast, when members are added viaGroupAddMemberDialog(line 75), the parent is notified through@saved="props.onSaved". This inconsistency might cause the parent's state to become out of sync after deletions.If the parent component needs to track member changes (as suggested by the presence of the
onSavedcallback), consider calling it after deletion as well:🔎 Proposed fix
function onGroupMemberDeleted(deletedMemberId: string) { members.value = members.value.filter(m => m.id !== deletedMemberId); deletingGroupMember.value = undefined; + props.onSaved(members.value); }
| function onMembersSaved(newMembers: AuthorityDto[]) { | ||
| const ids = new Set(group.value.members.map(u => u.id)); | ||
| newMembers.forEach(u => { if (!ids.has(u.id)) group.value.members.push(u); }); | ||
| group.value.members.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null safety to the sort comparison.
Line 114 calls a.name.localeCompare(b.name, ...) without checking if name is defined. If any member has an undefined name, this will throw a TypeError at runtime.
🔎 Proposed fix
function onMembersSaved(newMembers: AuthorityDto[]) {
const ids = new Set(group.value.members.map(u => u.id));
newMembers.forEach(u => { if (!ids.has(u.id)) group.value.members.push(u); });
- group.value.members.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
+ group.value.members.sort((a, b) => {
+ const aKey = a.name?.trim() || a.email || '';
+ const bKey = b.name?.trim() || b.email || '';
+ return aKey.localeCompare(bKey, undefined, { sensitivity: 'base' });
+ });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function onMembersSaved(newMembers: AuthorityDto[]) { | |
| const ids = new Set(group.value.members.map(u => u.id)); | |
| newMembers.forEach(u => { if (!ids.has(u.id)) group.value.members.push(u); }); | |
| group.value.members.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); | |
| } | |
| function onMembersSaved(newMembers: AuthorityDto[]) { | |
| const ids = new Set(group.value.members.map(u => u.id)); | |
| newMembers.forEach(u => { if (!ids.has(u.id)) group.value.members.push(u); }); | |
| group.value.members.sort((a, b) => { | |
| const aKey = a.name?.trim() || a.email || ''; | |
| const bKey = b.name?.trim() || b.email || ''; | |
| return aKey.localeCompare(bKey, undefined, { sensitivity: 'base' }); | |
| }); | |
| } |
🤖 Prompt for AI Agents
In frontend/src/components/authority/GroupDetail.vue around lines 111 to 115,
the sort comparator calls a.name.localeCompare(b.name, ...) without null-safety
which will throw if name is undefined; update the comparator to coerce
undefined/null to a safe string (e.g. const na = a.name ?? '' and const nb =
b.name ?? '') before calling localeCompare, or add explicit checks returning
-1/1/0 when either name is missing, so the sort never calls localeCompare on
undefined.
| </div> | ||
| </td> | ||
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | ||
| <a tabindex="0" class="cursor-pointer text-red-600 hover:text-red-900" :title="t('common.leave')" @click="showDeleteDialog(member)">{{ t('common.remove') }}</a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Use a button element instead of an anchor tag for the remove action.
The remove action uses an anchor tag with a click handler, but anchor tags are semantically meant for navigation. For actions like removing a member, a <button> element provides better semantics and built-in keyboard accessibility.
🔎 Proposed fix
- <a tabindex="0" class="cursor-pointer text-red-600 hover:text-red-900" :title="t('common.leave')" @click="showDeleteDialog(member)">{{ t('common.remove') }}</a>
+ <button type="button" class="text-red-600 hover:text-red-900 focus:outline-hidden focus:ring-2 focus:ring-red-500" :title="t('common.leave')" @click="showDeleteDialog(member)">{{ t('common.remove') }}</button>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <a tabindex="0" class="cursor-pointer text-red-600 hover:text-red-900" :title="t('common.leave')" @click="showDeleteDialog(member)">{{ t('common.remove') }}</a> | |
| <button type="button" class="text-red-600 hover:text-red-900 focus:outline-hidden focus:ring-2 focus:ring-red-500" :title="t('common.leave')" @click="showDeleteDialog(member)">{{ t('common.remove') }}</button> |
🤖 Prompt for AI Agents
In frontend/src/components/authority/GroupMemberList.vue around line 39, the
remove action uses an anchor (<a>) with a click handler which is semantically
incorrect for an action; replace it with a <button> element (type="button") to
convey action semantics and preserve accessibility. Keep the same classes, title
binding and @click handler (showDeleteDialog(member)), remove the href, and
ensure tabindex isn’t needed (or keep tabindex="0" if explicit focus order is
required); also add an aria-label if the visible text isn’t sufficiently
descriptive.
| </td> | ||
| </tr> | ||
| <tr v-if="!filteredUsers.length"> | ||
| <td colspan="3" class="py-4 px-4 text-sm text-center text-gray-500"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct the colspan to match the table structure.
The empty state row uses colspan="3", but the table only has 2 columns (member and actions). This should be colspan="2".
🔎 Proposed fix
- <td colspan="3" class="py-4 px-4 text-sm text-center text-gray-500">
+ <td colspan="2" class="py-4 px-4 text-sm text-center text-gray-500">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <td colspan="3" class="py-4 px-4 text-sm text-center text-gray-500"> | |
| <td colspan="2" class="py-4 px-4 text-sm text-center text-gray-500"> |
🤖 Prompt for AI Agents
In frontend/src/components/authority/GroupMemberList.vue around line 43, the
empty-state table row uses colspan="3" but the table only has two columns
(member and actions); change the colspan attribute value from 3 to 2 so the
empty cell spans both columns and aligns with the table structure.
…tor/hub into feature/user-and-groups # Conflicts: # frontend/src/components/authority/UserEditCreate.vue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
♻️ Duplicate comments (9)
frontend/src/components/authority/UserEditCreate.vue (2)
189-189: Inconsistent i18n key namespace for password visibility aria-label.Line 158 uses
t('common.showPassword')/t('common.hidePassword'), but line 189 usest('userEditCreate.showPassword')/t('userEditCreate.hidePassword'). These should be consistent.🔎 Proposed fix
- <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('userEditCreate.showPassword') : t('userEditCreate.hidePassword')" @click="togglePasswordVisibility"> + <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('common.showPassword') : t('common.hidePassword')" @click="togglePasswordVisibility">
445-452: Optimistic state update may cause UX confusion on API failure.
initialUserDatais updated before the API call succeeds. If the API call fails,userDataHasUnsavedChangeswill returnfalse(since initial matches current), hiding the "unsaved changes" warning even though data wasn't actually saved.Consider moving this update after the successful API response (after line 475, within the try block).
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java (2)
441-450: Remove broad Exception catch block.Lines 447-449 catch all exceptions and wrap them in
RuntimeException, which obscures the original failure and violates codebase patterns. Based on learnings from this project, exceptions should propagate naturally rather than being caught broadly.The specific
ForbiddenExceptionandNotFoundExceptioncatches are appropriate, but the generalExceptioncatch should be removed:🔎 Suggested fix
public Response deleteUser(@PathParam("id") String userId) { - try { - keycloakAdminService.deleteUser(userId); - return Response.noContent().build(); - } catch (ForbiddenException | NotFoundException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("Failed to delete user", e); - } + keycloakAdminService.deleteUser(userId); + return Response.noContent().build(); }If
keycloakAdminService.deleteUserthrows checked exceptions that must be handled, catch only those specific types.Based on learnings, as per coding guidelines, ...
325-356: Address exception handling and cleanup concerns.Two issues to consider:
Exception message exposure (line 352): Returning
e.getMessage()directly fromClientErrorExceptionmay expose internal implementation details. Consider returning a sanitized message or using predefined constants.Orphaned Keycloak user on sync failure (lines 341-344): If Keycloak user creation succeeds but the user isn't found in the database after sync, a
RuntimeExceptionis thrown, leaving an orphaned user in Keycloak. Consider adding cleanup:🔎 Suggested cleanup approach
User user = userRepo.findById(userRepresentation.getId()); if (user == null) { + // Attempt cleanup of orphaned Keycloak user + try { + keycloakAdminService.deleteUser(userRepresentation.getId()); + LOG.warning("Cleaned up orphaned Keycloak user: " + userRepresentation.getId()); + } catch (Exception cleanupEx) { + LOG.severe("Failed to cleanup orphaned Keycloak user: " + userRepresentation.getId()); + } throw new RuntimeException("User was created in Keycloak but not found in database after sync"); }Based on past review comments.
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java (2)
340-344: Reconsider direct database deletion for groups.Similar to
deleteUser, this directly deletes fromgroupRepo. Per past review feedback, consider letting the syncer handle database cleanup for consistency.
175-184: Security concern: Fail-open behavior on exception.When checking federated identity fails (network issue, Keycloak unavailable), returning
falseallows modifications to potentially read-only users. Consider fail-closed behavior:Proposed fix
public boolean isUserReadOnly(String userId) { try { UserResource userResource = realm.users().get(userId); var federatedIdentities = userResource.getFederatedIdentity(); return !federatedIdentities.isEmpty(); } catch (WebApplicationException e) { LOG.warn("Failed to check federated identity for user {}. Keycloak responded with status {}", userId, e.getResponse().getStatus(), e); - return false; + throw new InternalServerErrorException("Unable to verify user editability", e); } }backend/src/main/java/org/cryptomator/hub/api/UserDto.java (1)
166-172: Parameter name inconsistency inwithCountsmethod.The parameter is named
vaultsCountbut maps to record fieldaccessibleVaultCount. Align the naming for clarity:Proposed fix
-public WithCounts withCounts(long groupsCount, long vaultsCount, long devicesCount) { +public WithCounts withCounts(long groupsCount, long accessibleVaultCount, long devicesCount) { return new WithCounts( this, devicesCount, groupsCount, - vaultsCount); + accessibleVaultCount); }frontend/src/common/backend.ts (1)
114-116: Critical: Property name mismatch with backend API.The frontend expects
vaultRolebut the backend'sMemberDto.javaserializes this field asrole(via@JsonProperty("role")). This will causevaultRoleto beundefinedat runtime.export type MemberDto = AuthorityDto & { - vaultRole: VaultRole + role: VaultRole }backend/src/main/java/org/cryptomator/hub/entities/User.java (1)
202-217: Bidirectional ManyToMany mapping issue.Both
User.directGroupMembershipsandGroup.membersdefine@JoinTablefor the same relationship. One side should usemappedByto avoid duplicate relationship management.Recommended fix (make User the inverse side)
-@ManyToMany -@JoinTable(name = "group_membership", - joinColumns = @JoinColumn(name = "member_id", referencedColumnName = "id"), - inverseJoinColumns = @JoinColumn(name = "group_id", referencedColumnName = "id") -) +@ManyToMany(mappedBy = "members") public Set<Group> directGroupMemberships = new HashSet<>();
🧹 Nitpick comments (8)
backend/src/main/java/org/cryptomator/hub/entities/EffectiveVaultAccess.java (2)
199-203: Consider using thegetRole()convenience method.For slightly better encapsulation, use
eva.getRole()instead ofeva.getId().getRole()on line 201.🔎 Proposed refactor
public Collection<VaultAccess.Role> listRoles(UUID vaultId, String authorityId) { return find("#EffectiveVaultAccess.findByAuthorityAndVault", Parameters.with("vaultId", vaultId).and("authorityId", authorityId)).stream() - .map(eva -> eva.getId().getRole()) + .map(EffectiveVaultAccess::getRole) .collect(Collectors.toUnmodifiableSet()); }
77-85: Consider adding fetch joins for the authority relationship in repository queries.The
vaultrelationship is properly fetched viaLEFT JOIN FETCHinUser.javaandVaultAccessqueries. However, theauthorityrelationship lacks equivalent fetch joins in the main repository queries forEffectiveVaultAccess. While current usages ofgetAuthority()occur within@Transactionalmethods, adding explicit fetch joins (similar to thevaultrelationship) would prevent potential lazy loading issues and match the pattern used inVaultAccessrepository queries.frontend/src/components/authority/UserEditCreate.vue (2)
243-251: Unusedpicturefield in UserData interface.The
UserDatainterface defines apicture?: File | nullfield at line 249, but this field is never used in the component. The component only usespictureUrl(string) for the profile picture URL, not a File object.🔎 Proposed fix
interface UserData { firstName: string; lastName: string; username: string; email: string; roles: SelectableRealmRole[]; - picture?: File | null; previewUrl?: string | null; }
337-339: Type assertion may be unnecessary.Lines 337-339 cast
fetchedUserto an inline type to accessfirstNameandlastName. According to the AI summary,UserDtowas updated to include these fields. Verify that the type definition includes them and remove the assertion if possible.🔎 Proposed fix (if UserDto includes firstName/lastName)
- const userData = fetchedUser as { firstName?: string; lastName?: string }; - firstName.value = userData.firstName || ''; - lastName.value = userData.lastName || ''; + firstName.value = fetchedUser.firstName || ''; + lastName.value = fetchedUser.lastName || '';frontend/src/common/backend.ts (1)
135-137: Address or remove the TODO comment.The comment questions whether
VaultDtoWithRoleis unused. It is actually used inUserDto.accessibleVaults(line 84) andGroupDtoWithDetails.vaults(line 141). Consider removing this outdated TODO.backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java (2)
160-173: Reconsider direct database deletion.The method directly deletes from
userRepoafter Keycloak deletion. Based on past review feedback, this should ideally be handled by the syncer to ensure consistency. If immediate deletion is intentional, document why synchronous cleanup is required here.
186-214: Address TODO: Deduplicate sync logic with KeycloakAuthorityPuller.The
syncUsermethod duplicates logic fromKeycloakAuthorityPuller.syncAddedUsers/syncUpdatedUsers. Consider extracting shared logic into a common helper or reusing the puller's methods to reduce maintenance burden.backend/src/main/java/org/cryptomator/hub/api/UserDto.java (1)
62-77: Potential issue: Constructor delegates with duplicated parameters.Line 76 passes
ecdhPublicKeytwice andprivateKeystwice to the delegated constructor. This is intentional for the legacy fallback logic, but the pattern is confusing. Consider adding a comment explaining this.// Delegate to JSON constructor with same value for both new and legacy parameters // (no fallback needed when constructing programmatically) this(id, name, pictureUrl, email, firstName, lastName, language, realmRoles, devices, ecdhPublicKey, ecdhPublicKey, ecdsaPublicKey, privateKeys, privateKeys, setupCode);
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
backend/src/main/java/org/cryptomator/hub/api/UserDto.java(5 hunks)backend/src/main/java/org/cryptomator/hub/api/UsersResource.java(8 hunks)backend/src/main/java/org/cryptomator/hub/api/VaultResource.java(4 hunks)backend/src/main/java/org/cryptomator/hub/entities/EffectiveVaultAccess.java(2 hunks)backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java(1 hunks)backend/src/main/java/org/cryptomator/hub/entities/User.java(7 hunks)backend/src/main/java/org/cryptomator/hub/filters/VaultRole.java(2 hunks)backend/src/main/java/org/cryptomator/hub/filters/VaultRoleFilter.java(2 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java(1 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java(3 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java(4 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakRealmRoles.java(1 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakUserDto.java(1 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java(1 hunks)backend/src/test/java/org/cryptomator/hub/api/UsersResourceIT.java(4 hunks)backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java(3 hunks)backend/src/test/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProviderTest.java(4 hunks)frontend/src/common/backend.ts(6 hunks)frontend/src/components/authority/UserEditCreate.vue(1 hunks)
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-06-06T09:18:42.468Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 343
File: backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java:27-32
Timestamp: 2025-06-06T09:18:42.468Z
Learning: In the KeycloakAuthorityPuller and similar syncer components, don't add broad exception handling to scheduled methods. The Syncer execution context is designed to handle crashes, and it's better to let exceptions propagate rather than catching them broadly. Catching Exception is considered bad practice in this codebase.
Applied to files:
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.javabackend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java
📚 Learning: 2025-12-08T11:37:46.953Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 372
File: backend/src/test/java/org/cryptomator/hub/api/ExceedingLicenseLimitsIT.java:116-121
Timestamp: 2025-12-08T11:37:46.953Z
Learning: In the Cryptomator Hub backend schema, foreign key constraints use ON DELETE CASCADE for authority/user references. Specifically, deleting users or groups from the authority table automatically cascades to delete related rows in vault_access and access_token tables. Test cleanup methods that delete users/groups via repositories do not need explicit cleanup of these dependent tables.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/EffectiveVaultAccess.javabackend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.javabackend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2024-10-17T15:24:07.752Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 296
File: backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java:183-186
Timestamp: 2024-10-17T15:24:07.752Z
Learning: In `VaultRoleFilterTest`, remember that the `securityContext` mock is set in the outer class's setup method at line 46, which applies to nested classes as well. Avoid suggesting redundant setup in nested classes.
Applied to files:
backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.javabackend/src/main/java/org/cryptomator/hub/filters/VaultRoleFilter.javabackend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/test/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProviderTest.java
📚 Learning: 2024-07-27T13:14:40.963Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 282
File: frontend/src/common/userdata.ts:82-88
Timestamp: 2024-07-27T13:14:40.963Z
Learning: The error handling suggestion for `createBrowserKeys` in `frontend/src/common/userdata.ts` was discussed and resolved in PR #282, and should not be repeated in future reviews.
Applied to files:
frontend/src/components/authority/UserEditCreate.vuebackend/src/main/java/org/cryptomator/hub/api/UsersResource.javafrontend/src/common/backend.ts
📚 Learning: 2025-12-18T08:32:28.253Z
Learnt from: iammajid
Repo: cryptomator/hub PR: 376
File: frontend/src/components/authority/GroupInfo.vue:5-5
Timestamp: 2025-12-18T08:32:28.253Z
Learning: In the frontend codebase, ensure that authority DTOs for users and groups always include a pictureUrl by having AuthorityService.fillInMissingPicture() run at the service layer (as in backend.ts). As a result, components rendering users or groups (e.g., GroupInfo.vue and related components) can safely rely on pictureUrl being present and should not implement their own fallback logic for missing images. When reviewing, verify that the service layer populates pictureUrl for all user/group DTOs and that UI components do not duplicate fallback handling.
Applied to files:
frontend/src/components/authority/UserEditCreate.vuefrontend/src/common/backend.ts
📚 Learning: 2025-02-13T10:33:38.555Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 315
File: backend/src/main/java/org/cryptomator/hub/api/UserDto.java:0-0
Timestamp: 2025-02-13T10:33:38.555Z
Learning: In the UserDto class of the Cryptomator Hub project, the `JsonCreator` constructor is used to handle both new and legacy field names (e.g., `ecdhPublicKey`/`publicKey` and `privateKeys`/`privateKey`) during JSON deserialization, while a separate regular constructor is used for programmatic object creation with only the new field names.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakUserDto.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.javabackend/src/main/java/org/cryptomator/hub/api/UserDto.java
📚 Learning: 2025-12-18T08:32:42.414Z
Learnt from: iammajid
Repo: cryptomator/hub PR: 376
File: frontend/src/components/authority/GroupInfo.vue:5-5
Timestamp: 2025-12-18T08:32:42.414Z
Learning: In the Cryptomator Hub frontend codebase (frontend/src/common/backend.ts), AuthorityService.fillInMissingPicture() is applied to all user and group DTOs at the service layer, automatically generating Jdenticon fallback images as data URLs when pictureUrl is missing. This means components rendering users or groups can safely assume pictureUrl will always be present and do not need additional fallback handling.
Applied to files:
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java
📚 Learning: 2024-06-22T04:41:35.492Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 281
File: backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java:1-66
Timestamp: 2024-06-22T04:41:35.492Z
Learning: The `StringArrayType` class in the `backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java` file is used only for read-only purposes, specifically for the `effective_wot` view. The `nullSafeSet` method throws `UnsupportedOperationException` to enforce this behavior.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java
📚 Learning: 2025-03-12T14:53:06.855Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: frontend/src/components/LegacyDeviceList.vue:52-87
Timestamp: 2025-03-12T14:53:06.855Z
Learning: In Cryptomator Hub, legacy devices are only of the 'DESKTOP' type, and there are no 'BROWSER' or 'MOBILE' legacy devices.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-03-13T08:37:00.992Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java:204-218
Timestamp: 2025-03-13T08:37:00.992Z
Learning: The event logging for device removals (`eventLogger.logDeviceRemoved()`) doesn't differentiate between legacy and regular devices in Cryptomator Hub to maintain historical consistency in the logs.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
🧬 Code graph analysis (8)
backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java (1)
frontend/src/common/backend.ts (1)
RealmRole(66-66)
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java (1)
backend/src/main/java/org/cryptomator/hub/api/GroupDto.java (1)
GroupDto(10-46)
backend/src/main/java/org/cryptomator/hub/filters/VaultRole.java (1)
frontend/src/common/backend.ts (1)
RealmRole(66-66)
backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java (1)
frontend/src/common/backend.ts (1)
RealmRole(66-66)
backend/src/test/java/org/cryptomator/hub/api/UsersResourceIT.java (2)
backend/src/test/java/org/cryptomator/hub/api/BillingResourceIT.java (3)
Nested(39-144)Nested(146-169)Nested(171-190)backend/src/test/java/org/cryptomator/hub/api/GroupsResourceIT.java (3)
Nested(91-115)Nested(117-131)Nested(133-365)
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java (3)
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java (1)
ApplicationScoped(22-176)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java (1)
ApplicationScoped(20-144)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakRealmRoles.java (1)
ApplicationScoped(11-29)
frontend/src/common/backend.ts (3)
backend/src/main/java/org/cryptomator/hub/api/GroupDto.java (1)
GroupDto(10-46)backend/src/main/java/org/cryptomator/hub/api/AuthorityDto.java (1)
AuthorityDto(8-45)backend/src/main/java/org/cryptomator/hub/api/MemberDto.java (1)
MemberDto(8-35)
backend/src/main/java/org/cryptomator/hub/api/VaultResource.java (1)
frontend/src/common/backend.ts (3)
RealmRole(66-66)VaultRole(64-64)VaultDtoWithRole(135-137)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (65)
backend/src/main/java/org/cryptomator/hub/entities/EffectiveVaultAccess.java (2)
12-15: LGTM! New imports support the ManyToOne associations.The added JPA imports are appropriate for the new lazy-loaded associations.
91-97: LGTM! Standard getter implementations.The new getters provide appropriate access to the associated entities.
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakRealmRoles.java (1)
26-28: Consider error handling for missing roles.If the role doesn't exist in Keycloak,
toRepresentation()may throw an exception. Consider whether this should be handled gracefully or if throwing is the intended behavior.backend/src/main/java/org/cryptomator/hub/filters/VaultRoleFilter.java (2)
45-48: LGTM!The update correctly uses
kcName()to get the string representation for the security context role check, aligning with theRealmRoleenum introduced in the annotation.
76-80: LGTM!Consistent with the bypass check above. The error message at line 78 appropriately uses the enum directly for human-readable output while
kcName()is used for the actual role comparison.backend/src/main/java/org/cryptomator/hub/filters/VaultRole.java (2)
5-5: LGTM!Import added for the new
RealmRoleenum.
49-49: Good type-safety improvement.Changing from
StringtoRealmRoleenum improves type safety and prevents typos in role names. TheRealmRole.kcName()values ("user","admin","create-vaults") correctly match the frontend'sRealmRoletype.backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java (3)
38-47: LGTM!The logic correctly enriches Keycloak user data with realm roles before syncing. The null check at line 43 properly handles cases where a user might be assigned a role but not exist in the users list.
81-84: LGTM!The new user creation properly populates
firstName,lastName, andrealmRolesfields, aligning with the enriched user data model.
108-111: LGTM!User updates now sync
firstName,lastName, andrealmRolesconsistently with the add path.backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java (4)
43-48: Verify null handling behavior.The implementation converts
nullto an empty array (new String[0]) rather than setting SQLNULL. This means the database will store an empty array when the value isnull. Ensure this is the intended behavior for fields likerealmRoles.If SQL
NULLshould be preserved:🔎 Alternative implementation preserving NULL
public void nullSafeSet(PreparedStatement st, String[] value, int index, SharedSessionContractImplementor session) throws SQLException { - session.doWork(connection -> { - var jdbcArray = connection.createArrayOf("VARCHAR", value == null ? new String[0] : value); - st.setArray(index, jdbcArray); - }); + if (value == null) { + st.setNull(index, Types.ARRAY); + } else { + session.doWork(connection -> { + var jdbcArray = connection.createArrayOf("VARCHAR", value); + st.setArray(index, jdbcArray); + }); + } }Based on learnings, this class was previously read-only for the
effective_wotview. The change enables write operations for storingrealmRoleson users.
51-52: LGTM!Correctly updated to
truesince the type now supports mutations and proper deep copying.
55-58: LGTM!Proper defensive copy implementation for mutable array type.
65-72: LGTM!The
assemblemethod now properly validates the cached type and returns a defensive copy. TheIllegalArgumentExceptionprovides a clear error message for type mismatches.backend/src/test/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProviderTest.java (2)
3-4: LGTM: Test class rename and imports align with role-based user listing.The class rename to
KeycloakAuthorityProviderTestand the addition of Hamcrest/role-related imports properly support the new role-based user listing tests introduced in this PR.Also applies to: 15-16, 27-27
195-229: LGTM: Well-structured tests for role-based user listing.The new "Roles" nested test suite properly exercises the
usersInRolefunctionality with:
- Consistent mocking patterns
- Order-agnostic assertions using Hamcrest
- Coverage of both populated and empty role membership
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakUserDto.java (1)
3-5: LGTM: KeycloakUserDto expanded to include user details and roles.The record now includes
firstName,lastName, androles(asSet<RealmRole>), aligning with the expanded user model and type-safe realm role handling introduced in this PR.backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java (2)
14-14: LGTM: Test updates properly reference RealmRole enum.The test display names now reference
RealmRole.ADMIN, whileisUserInRolecalls correctly use the string representation"admin"to match the JAX-RSSecurityContextAPI.Also applies to: 190-192, 199-201
230-230: LGTM: Annotation updated to use type-safe RealmRole enum.The
@VaultRoleannotation now usesrealmRole = RealmRole.ADMIN, providing type-safe realm role specification.backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java (2)
1-22: LGTM: Well-designed enum for type-safe realm role handling.The
RealmRoleenum provides type-safe realm role handling with proper JSON binding via@JsonPropertyannotations and a clear mapping to Keycloak role names.
24-31: LGTM: fromKcName properly validates input.The method correctly throws
IllegalArgumentExceptionfor unknown role names, ensuring callers are aware of invalid input.backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java (3)
64-73: LGTM: mapToUser correctly populates expanded KeycloakUserDto.The mapping now includes
firstName,lastName, andrealmRoles, properly converting Keycloak role names to the type-safeRealmRoleenum usingfromKcNames.
75-82: LGTM: Improved null-safety in parsePictureUrl.The refactored implementation uses explicit null/containsKey checks instead of catching NPE, making the code more readable and maintainable.
126-143: LGTM: Well-implemented role-based user listing with pagination.The new
usersInRolemethods follow existing patterns in this class:
- Public method delegates to package-visible implementation for testability
- Proper pagination handling for large role memberships
- Consistent with other listing methods (users, groups)
backend/src/main/java/org/cryptomator/hub/api/VaultResource.java (2)
49-49: LGTM: VaultRole annotations updated to use type-safe RealmRole enum.The vault access annotations now use
RealmRole.ADMINandRealmRole.CREATE_VAULTS, providing compile-time type safety for realm role references.Also applies to: 389-389, 403-403
526-544: LGTM: VaultDtoWithRole provides vault info with role context.The new record properly combines vault details with the user's role, supporting enriched user/group detail views. The static factory follows existing patterns and correctly truncates timestamps.
backend/src/test/java/org/cryptomator/hub/api/UsersResourceIT.java (6)
12-16: LGTM: Test dependencies properly configured.New imports and the
@InjectMockforKeycloakAdminServiceproperly support the expanded CRUD operation tests.Also applies to: 20-20, 30-30, 36-36, 58-59
368-399: LGTM: Well-structured test class with proper setup/teardown.The
UserCrudOperationsnested class follows established patterns with:
- Correct admin role annotation
- Proper mock reset for test isolation
- Database setup/cleanup in BeforeAll/AfterAll
401-407: LGTM: Basic list users test validates endpoint access.Simple validation that the admin can retrieve the user list.
409-509: LGTM: Comprehensive POST /users test coverage.Tests properly validate:
- Successful user creation (201)
- Username/email conflicts (409)
- Invalid input validation (400)
511-608: LGTM: GET and PUT tests cover key scenarios.Tests validate:
- Successful retrieval and updates
- Not found handling
- Federated user restrictions (403)
610-639: LGTM: DELETE tests validate removal and restrictions.Tests properly cover:
- Successful deletion (204)
- Not found errors (404)
- Federated user restrictions (403)
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java (6)
11-13: LGTM: Imports and logger support expanded user management.New imports properly support Keycloak integration, exception handling, and group management features.
Also applies to: 27-27, 35-36, 51-51, 58-58
76-83: LGTM: New dependencies properly injected.
Group.RepositoryandKeycloakAdminServicecorrectly support the expanded user management functionality.
195-195: LGTM: getMe methods now include firstName, lastName, and realmRoles.The methods correctly fetch these fields from the database (user entity) and include them in the response, aligning with past review guidance to avoid repeated Keycloak queries.
Also applies to: 219-219
358-402: LGTM: getUser properly restricted and avoids inefficient vault loading.The endpoint is correctly restricted to admins and uses the
user.accessibleVaultsrelationship to efficiently load only the vaults accessible to the user, addressing past performance concerns.Based on past review comments.
404-431: LGTM: updateUser properly implements admin-level user updates.The endpoint correctly:
- Restricts access to admins
- Updates both user details and roles in Keycloak
- Lets specific exceptions propagate naturally (past broad exception handling removed)
Based on past review comments.
452-471: LGTM: DTOs properly structured for user CRUD operations.Both
CreateUserDtoandUpdateUserDtohave appropriate fields with proper validation annotations and type-safeRealmRolehandling.frontend/src/common/backend.ts (7)
66-67: LGTM!The
RealmRoletype correctly mirrors the backend realm role values.
79-84: LGTM!The extension of
UserDtowithfirstName,lastName, andrealmRolesaligns with backend changes. TheaccessibleVaultstype change toVaultDtoWithRole[]correctly includes role information.
91-101: LGTM!The
UserDtoWithCountsandUserDtoWithDetailstypes properly extendUserDtowith additional metadata. TheaccessibleVaultCountnaming avoids the type conflict mentioned in past reviews.
123-142: LGTM!The new DTO types (
CreateUserDto,UpdateUserDto,CreateGroupDto,UpdateGroupDto,VaultDtoWithRole,GroupDtoWithDetails) are well-structured and align with the backend API contracts.
302-353: LGTM!The new
GroupServiceclass provides comprehensive CRUD and membership management. Key observations:
- All methods correctly apply
AuthorityService.fillInMissingPicturefor consistency- Error handling with
rethrowAndConvertIfExpectedfollows existing patterns- The
getGroupmethod properly fills pictures for both the group and its members
369-393: LGTM!The new
UserServicemethods (removeUser,listAll,createUser,getUser,updateUser) are well-implemented:
removeUsercorrectly returnsPromise<void>and handles 404- All methods apply
fillInMissingPictureconsistently
524-526: LGTM!The service map correctly exposes the new
GroupService.backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java (8)
57-60: LGTM!The
@PostConstructinitialization of the realm resource is appropriate for caching the realm reference.
62-121: LGTM with minor observations.The
createUsermethod is well-structured with proper error handling for 409 conflicts. The TODO at line 112 about rollback strategy is a valid concern for future consideration, but the current best-effort approach with logging is acceptable for initial implementation.
127-158: LGTM!The
updateUsermethod correctly:
- Checks for read-only users before modification
- Uses the
setPicturehelper to preserve other attributes- Handles password updates separately with proper credential representation
216-246: LGTM!The
syncGroupmethod properly:
- Handles both new and existing groups
- Syncs picture attributes correctly
- Updates members and triggers
effectiveGroupMembershipRepo.updateGroups
248-264: LGTM!The group membership methods correctly trigger NotFoundException for non-existent groups by calling
toRepresentation()before modifications, and properly sync the group afterward.
266-290: LGTM!The
updateUserRolesmethod correctly:
- Removes roles not in the provided set
- Adds roles that are in the provided set
- Syncs to the database
294-316: LGTM!The
createGroupmethod follows the same pattern ascreateUserwith proper conflict handling and sync.
346-354: LGTM!The
setPicturehelper correctly handles null attributes and preserves other attributes while modifying only the picture.backend/src/main/java/org/cryptomator/hub/api/UserDto.java (6)
16-17: LGTM!Adding
@JsonInclude(JsonInclude.Include.NON_NULL)at the class level is a good practice to reduce response payload size.
19-28: LGTM!The new
firstName,lastName, andrealmRolesfields properly extend the user representation.
30-60: LGTM!The
@JsonCreatorconstructor properly handles both new and legacy field names while incorporating the new fields. The@NotNullannotations on critical fields ensure proper validation.
84-102: LGTM!The new getters for
firstName,lastName, andrealmRolesare properly annotated with@JsonProperty.
149-164: LGTM!The
justPublicInfofactory method correctly incorporates the new fields.
174-196: LGTM!The
withDetailsmethod and theWithCounts/WithDetailsrecords are well-structured with appropriate@JsonUnwrappedand@JsonPropertyannotations.backend/src/main/java/org/cryptomator/hub/entities/User.java (6)
52-83: LGTM!The new NamedQueries for fetching and counting groups, vaults, vault access, and devices for a user are well-structured and use appropriate joins with
EffectiveGroupMembershipandEffectiveVaultAccess.
89-100: LGTM!The new
firstName,lastName, andrealmRolesfields are properly mapped:
realmRolesuses@Type(StringArrayType.class)withString[], which is the correct approach for PostgreSQL array columns- This resolves the previous concern about using
@ColumnwithSet<String>
122-152: LGTM!The getters and setters for the new fields are straightforward and follow the existing patterns.
215-217: LGTM!The
@Immutableannotation onaccessibleVaultscorrectly marks this as a read-only view ofEffectiveVaultAccess.
251-268: LGTM!The
findByIdWithEagerDetailsandfindByIdsrepository methods are well-implemented:
- Eager fetch joins cover all needed relationships
- Batch processing in
findByIdsprevents excessive IN clause sizes
286-326: LGTM!The new repository methods correctly use the named queries and provide clean interfaces for fetching user-related data:
getGroupsForUser,countGroupsForUsergetVaultsForUser,getVaultAccessForUser,countVaultsForUsercountDevicesForUser
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java
Show resolved
Hide resolved
| public GroupRepresentation updateGroup(String groupId, String name, String pictureUrl) { | ||
| GroupResource groupResource = realm.groups().group(groupId); | ||
| GroupRepresentation group = groupResource.toRepresentation(); | ||
|
|
||
| if (!name.isBlank()) { | ||
| group.setName(name); | ||
| } | ||
|
|
||
| if (pictureUrl != null) { | ||
| var attrs = setPicture(group.getAttributes(), pictureUrl); | ||
| group.setAttributes(attrs); | ||
| } | ||
|
|
||
| groupResource.update(group); | ||
| syncGroup(groupId); | ||
| return groupResource.toRepresentation(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null check needed for name parameter.
Line 326 calls name.isBlank() but name could be null, causing a NullPointerException. The createGroup method doesn't have this issue because it requires the name.
Proposed fix
-if (!name.isBlank()) {
+if (name != null && !name.isBlank()) {
group.setName(name);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public GroupRepresentation updateGroup(String groupId, String name, String pictureUrl) { | |
| GroupResource groupResource = realm.groups().group(groupId); | |
| GroupRepresentation group = groupResource.toRepresentation(); | |
| if (!name.isBlank()) { | |
| group.setName(name); | |
| } | |
| if (pictureUrl != null) { | |
| var attrs = setPicture(group.getAttributes(), pictureUrl); | |
| group.setAttributes(attrs); | |
| } | |
| groupResource.update(group); | |
| syncGroup(groupId); | |
| return groupResource.toRepresentation(); | |
| } | |
| public GroupRepresentation updateGroup(String groupId, String name, String pictureUrl) { | |
| GroupResource groupResource = realm.groups().group(groupId); | |
| GroupRepresentation group = groupResource.toRepresentation(); | |
| if (name != null && !name.isBlank()) { | |
| group.setName(name); | |
| } | |
| if (pictureUrl != null) { | |
| var attrs = setPicture(group.getAttributes(), pictureUrl); | |
| group.setAttributes(attrs); | |
| } | |
| groupResource.update(group); | |
| syncGroup(groupId); | |
| return groupResource.toRepresentation(); | |
| } |
🤖 Prompt for AI Agents
In backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java
around lines 322 to 338, the updateGroup method calls name.isBlank() which can
throw a NullPointerException when name is null; change the condition to first
check for null and only set the name when it's non-null and non-blank (e.g. if
(name != null && !name.isBlank()) { group.setName(name); }) so null names leave
the existing group name unchanged; optionally trim the name before checking to
avoid setting blank/whitespace-only values.
| private final EnumMap<RealmRole, RoleRepresentation> cachedRoles = new EnumMap<>(RealmRole.class); | ||
|
|
||
| public RoleRepresentation getRealmRole(RealmRole realmRole) { | ||
| return cachedRoles.computeIfAbsent(realmRole, this::fetchRealmRole); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread-safety concern with EnumMap in application-scoped bean.
EnumMap is not thread-safe, and computeIfAbsent on a non-concurrent map can exhibit race conditions when accessed by multiple threads simultaneously. Since this is an @ApplicationScoped bean, concurrent access is possible.
Consider using ConcurrentHashMap or synchronizing access:
🔎 Proposed fix using ConcurrentHashMap
-import java.util.EnumMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Map;
@ApplicationScoped
public class KeycloakRealmRoles {
@Inject
Keycloak keycloak;
@ConfigProperty(name = "hub.keycloak.realm")
String keycloakRealm;
- private final EnumMap<RealmRole, RoleRepresentation> cachedRoles = new EnumMap<>(RealmRole.class);
+ private final Map<RealmRole, RoleRepresentation> cachedRoles = new ConcurrentHashMap<>();
public RoleRepresentation getRealmRole(RealmRole realmRole) {
return cachedRoles.computeIfAbsent(realmRole, this::fetchRealmRole);
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakRealmRoles.java
around lines 20 to 24, the cachedRoles field is an EnumMap and computeIfAbsent
is called on it from an @ApplicationScoped bean which may be accessed
concurrently; EnumMap is not thread-safe and can race. Replace the EnumMap with
a thread-safe map (e.g., ConcurrentHashMap<RealmRole, RoleRepresentation>) or
guard access with synchronization so compute-if-absent semantics are safe (use
ConcurrentHashMap.computeIfAbsent or synchronize the fetch path) and ensure no
long-running blocking occurs while holding any lock.
| public static Set<RealmRole> fromKcNames(Collection<String> kcNames) { | ||
| if (kcNames == null || kcNames.isEmpty()) { | ||
| return EnumSet.noneOf(RealmRole.class); | ||
| } | ||
| Set<RealmRole> roles = EnumSet.noneOf(RealmRole.class); | ||
| for (String kcName : kcNames) { | ||
| for (RealmRole role : values()) { | ||
| if (role.kcName.equals(kcName)) { | ||
| roles.add(role); | ||
| } | ||
| } | ||
| } | ||
| return roles; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent error handling between fromKcName and fromKcNames.
fromKcName throws IllegalArgumentException for unknown roles, but fromKcNames silently skips them. This inconsistency could cause:
- Authorization issues if a role is misspelled or unrecognized
- Hard-to-debug problems when roles silently disappear
Consider either:
- Logging a warning when unknown roles are encountered
- Throwing an exception for unknown roles (with clear error message listing the invalid names)
- Returning a result object that includes both matched roles and a list of unknown role names
This would make role mapping failures visible and prevent silent authorization degradation.
🤖 Prompt for AI Agents
In backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java around
lines 33 to 46, the fromKcNames method currently ignores unknown kcNames while
fromKcName throws IllegalArgumentException; change fromKcNames to mirror that
behavior by collecting any names that do not match an enum constant and, if any
are found, throw an IllegalArgumentException listing the invalid names (use a
clear message like "Unknown realm role(s): ..." with the list); otherwise return
the EnumSet of matched roles as before — ensure null/empty input still returns
EnumSet.noneOf(RealmRole.class).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by design
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@overheadhunter, understood! I've noted that the different error handling strategies between fromKcName and fromKcNames are intentional for your use case.
✏️ Learnings added
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 376
File: backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java:33-46
Timestamp: 2025-12-19T19:53:33.331Z
Learning: In `backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java`, the `fromKcNames` method intentionally skips unknown Keycloak role names silently, while `fromKcName` throws `IllegalArgumentException` for unknown roles. This asymmetric error handling is by design.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| public async getEffectiveMembers(groupId: string): Promise<UserDto[]> { | ||
| return axiosAuth.get<UserDto[]>(`/groups/${groupId}/effective-members`) | ||
| .then(response => response.data.map(AuthorityService.fillInMissingPicture)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for getEffectiveMembers.
Unlike other methods in this service, getEffectiveMembers doesn't handle expected error codes (e.g., 404 for non-existent group).
Proposed fix
public async getEffectiveMembers(groupId: string): Promise<UserDto[]> {
return axiosAuth.get<UserDto[]>(`/groups/${groupId}/effective-members`)
- .then(response => response.data.map(AuthorityService.fillInMissingPicture));
+ .then(response => response.data.map(AuthorityService.fillInMissingPicture))
+ .catch((error) => rethrowAndConvertIfExpected(error, 404));
}🤖 Prompt for AI Agents
In frontend/src/common/backend.ts around lines 337 to 340, getEffectiveMembers
calls axiosAuth but lacks error handling for expected HTTP errors (e.g., 404).
Wrap the call to axiosAuth.get in a promise chain or try/catch that: on 404
returns an empty array, on other errors rethrows or rejects so callers can
handle them, and still maps response.data through
AuthorityService.fillInMissingPicture when successful; follow the same
error-handling pattern used by other methods in this service.
| public async getUser(userId: string): Promise<UserDtoWithDetails> { | ||
| return axiosAuth.get<UserDtoWithDetails>(`/users/${userId}`).then(response => AuthorityService.fillInMissingPicture(response.data)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for getUser.
Similar to getEffectiveMembers, this method should handle 404 errors for non-existent users.
Proposed fix
public async getUser(userId: string): Promise<UserDtoWithDetails> {
- return axiosAuth.get<UserDtoWithDetails>(`/users/${userId}`).then(response => AuthorityService.fillInMissingPicture(response.data));
+ return axiosAuth.get<UserDtoWithDetails>(`/users/${userId}`)
+ .then(response => AuthorityService.fillInMissingPicture(response.data))
+ .catch((error) => rethrowAndConvertIfExpected(error, 404));
}🤖 Prompt for AI Agents
In frontend/src/common/backend.ts around lines 387 to 389, getUser currently
returns the axios promise without handling 404s; update it to catch Axios
errors, detect a 404 (error.response?.status === 404) and return null in that
case, rethrow other errors, and update the method signature to
Promise<UserDtoWithDetails | null> (or adjust callers) so callers can handle
missing users; use axios.isAxiosError to safely detect Axios errors.
| errors.value.username = t('userEditCreate.error.userAlreadyExists'); | ||
| } | ||
| } else { | ||
| submitError.value = error instanceof Error ? error.message : 'An error occurred'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Use i18n for error message instead of hardcoded string.
The fallback error message 'An error occurred' is hardcoded. Use an i18n key for consistency with the rest of the component.
🔎 Proposed fix
} else {
- submitError.value = error instanceof Error ? error.message : 'An error occurred';
+ submitError.value = error instanceof Error ? error.message : t('common.error');Then add the corresponding key to frontend/src/i18n/en-US.json:
{
"common": {
"error": "An error occurred"
}
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In frontend/src/components/authority/UserEditCreate.vue around line 497, the
fallback error string is hardcoded; replace the literal 'An error occurred' with
the component's i18n lookup (e.g. use the existing i18n/t function or $t call to
fetch a key like common.error) so submitError.value = error instanceof Error ?
error.message : t('common.error'); and then add the corresponding key
"common.error": "An error occurred" to frontend/src/i18n/en-US.json; ensure the
i18n composable/import is present in the component and adjust imports if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
frontend/src/common/backend.ts (2)
341-344: Missing error handling forgetEffectiveMembers.Unlike other methods in
GroupService,getEffectiveMembersdoesn't handle expected error codes (e.g., 404 for non-existent group). This is inconsistent with the established pattern.🔎 Proposed fix
public async getEffectiveMembers(groupId: string): Promise<UserDto[]> { return axiosAuth.get<UserDto[]>(`/groups/${groupId}/effective-members`) - .then(response => response.data.map(AuthorityService.fillInMissingPicture)); + .then(response => response.data.map(AuthorityService.fillInMissingPicture)) + .catch((error) => rethrowAndConvertIfExpected(error, 404)); }
391-393: Missing error handling forgetUser.Similar to other get methods,
getUsershould handle 404 errors for non-existent users to maintain consistency with the established error-handling pattern.🔎 Proposed fix
public async getUser(userId: string): Promise<UserDtoWithDetails> { - return axiosAuth.get<UserDtoWithDetails>(`/users/${userId}`).then(response => AuthorityService.fillInMissingPicture(response.data)); + return axiosAuth.get<UserDtoWithDetails>(`/users/${userId}`) + .then(response => AuthorityService.fillInMissingPicture(response.data)) + .catch((error) => rethrowAndConvertIfExpected(error, 404)); }
🧹 Nitpick comments (3)
frontend/src/components/authority/UserDetail.vue (2)
120-123: ThenewGroupsparameter is unused.
handleGroupsSaveddeclares anewGroupsparameter but reloads the entire user from the backend instead of using the passed data. Either remove the unused parameter or utilize it to avoid the extra network request.🔎 Proposed fix
If the parameter is unnecessary, remove it:
-async function handleGroupsSaved(newGroups: GroupDto[]) { +async function handleGroupsSaved() { user.value = await backend.users.getUser(props.id); user.value.groups.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); }Or use the passed groups to avoid reloading:
async function handleGroupsSaved(newGroups: GroupDto[]) { - user.value = await backend.users.getUser(props.id); - user.value.groups.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); + user.value.groups = newGroups; + user.value.groups.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); }Note: If vault membership may change when group membership changes, the full reload approach is safer.
125-137: Consider displaying an error message to the user.The error handler logs to the console but doesn't provide user-facing feedback. Users won't know why the page appears empty or incomplete if the fetch fails.
🔎 Proposed enhancement
Add an error state and display it in the template:
const loading = ref<boolean>(true); +const error = ref<string | null>(null); onMounted(async () => { try { user.value = await backend.users.getUser(props.id); user.value.groups.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); user.value.realmRoles = user.value.realmRoles.filter(isSelectableRealmRole); } catch (error) { console.error('Failed to fetch user:', error); + error.value = t('user.detail.loadError'); } finally { loading.value = false; } });Then show the error in the template before line 5:
<div v-if="error" class="text-center p-8 text-red-600 text-sm"> {{ error }} </div>frontend/src/common/backend.ts (1)
139-141: Remove the outdated TODO comment.
VaultDtoWithRoleis used at line 88 inUserDto.accessibleVaults: VaultDtoWithRole[], so the "TODO: unused??" comment is incorrect.🔎 Proposed fix
-export type VaultDtoWithRole = VaultDto & { // TODO: unused?? +export type VaultDtoWithRole = VaultDto & { role: VaultRole; }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
frontend/src/common/backend.ts(6 hunks)frontend/src/components/authority/UserDetail.vue(1 hunks)frontend/src/components/authority/UserEditCreate.vue(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/components/authority/UserEditCreate.vue
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-18T08:32:28.253Z
Learnt from: iammajid
Repo: cryptomator/hub PR: 376
File: frontend/src/components/authority/GroupInfo.vue:5-5
Timestamp: 2025-12-18T08:32:28.253Z
Learning: In the frontend codebase, ensure that authority DTOs for users and groups always include a pictureUrl by having AuthorityService.fillInMissingPicture() run at the service layer (as in backend.ts). As a result, components rendering users or groups (e.g., GroupInfo.vue and related components) can safely rely on pictureUrl being present and should not implement their own fallback logic for missing images. When reviewing, verify that the service layer populates pictureUrl for all user/group DTOs and that UI components do not duplicate fallback handling.
Applied to files:
frontend/src/components/authority/UserDetail.vuefrontend/src/common/backend.ts
📚 Learning: 2024-07-27T13:14:40.963Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 282
File: frontend/src/common/userdata.ts:82-88
Timestamp: 2024-07-27T13:14:40.963Z
Learning: The error handling suggestion for `createBrowserKeys` in `frontend/src/common/userdata.ts` was discussed and resolved in PR #282, and should not be repeated in future reviews.
Applied to files:
frontend/src/common/backend.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (2)
frontend/src/common/backend.ts (2)
462-473: LGTM: Visual consistency improvement.Updating the GROUP Jdenticon hues to match USER hues ensures visual consistency across authority types.
528-530: LGTM: Service properly exported.The
GroupServiceis correctly added to the services map, making it available to consumers of the backend module.
| public async updateUser(userId: string, dto: UpdateUserDto): Promise<UserDto> { | ||
| return axiosAuth.put<UserDto>(`/users/${userId}`, dto).then(response => AuthorityService.fillInMissingPicture(response.data)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for updateUser.
updateUser should catch and convert 404 errors to maintain consistency with other update methods (e.g., updateGroup at lines 329-332).
🔎 Proposed fix
public async updateUser(userId: string, dto: UpdateUserDto): Promise<UserDto> {
- return axiosAuth.put<UserDto>(`/users/${userId}`, dto).then(response => AuthorityService.fillInMissingPicture(response.data));
+ return axiosAuth.put<UserDto>(`/users/${userId}`, dto)
+ .then(response => AuthorityService.fillInMissingPicture(response.data))
+ .catch((error) => rethrowAndConvertIfExpected(error, 404));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public async updateUser(userId: string, dto: UpdateUserDto): Promise<UserDto> { | |
| return axiosAuth.put<UserDto>(`/users/${userId}`, dto).then(response => AuthorityService.fillInMissingPicture(response.data)); | |
| } | |
| public async updateUser(userId: string, dto: UpdateUserDto): Promise<UserDto> { | |
| return axiosAuth.put<UserDto>(`/users/${userId}`, dto) | |
| .then(response => AuthorityService.fillInMissingPicture(response.data)) | |
| .catch((error) => rethrowAndConvertIfExpected(error, 404)); | |
| } |
🤖 Prompt for AI Agents
In frontend/src/common/backend.ts around lines 395 to 397, updateUser currently
returns the axios promise directly and lacks the 404-to-NotFound error mapping
used by other update methods; wrap the axiosAuth.put call with a .catch that
checks for err.response?.status === 404 and rethrows the same NotFoundError (or
equivalent) used by updateGroup, otherwise rethrow the original error, and keep
the successful path applying AuthorityService.fillInMissingPicture to
response.data.
| <UserDeviceList :devices="user.devices" :page-size="10" :title="t('user.detail.devices')"/> | ||
| <UserDeviceList :devices="user.legacyDevices" :page-size="10" :visible="user.legacyDevices.length != 0" :title="t('legacyDeviceList.title')" :info="t('legacyDeviceList.title')"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The :info prop duplicates the :title value.
Lines 46 and 62 set :info="t('legacyDeviceList.title')", which is identical to the :title prop. This likely should be different text—perhaps a description or explanation about legacy devices.
🔎 Suggested fix
If an info/description key exists for legacy devices, use it instead:
-<UserDeviceList :devices="user.legacyDevices" :page-size="10" :visible="user.legacyDevices.length != 0" :title="t('legacyDeviceList.title')" :info="t('legacyDeviceList.title')"/>
+<UserDeviceList :devices="user.legacyDevices" :page-size="10" :visible="user.legacyDevices.length != 0" :title="t('legacyDeviceList.title')" :info="t('legacyDeviceList.description')"/>Or omit :info entirely if there's no separate description:
-<UserDeviceList :devices="user.legacyDevices" :page-size="10" :visible="user.legacyDevices.length != 0" :title="t('legacyDeviceList.title')" :info="t('legacyDeviceList.title')"/>
+<UserDeviceList :devices="user.legacyDevices" :page-size="10" :visible="user.legacyDevices.length != 0" :title="t('legacyDeviceList.title')"/>Also applies to: 61-62
🤖 Prompt for AI Agents
In frontend/src/components/authority/UserDetail.vue around lines 45-46 and 61-62
the :info prop for the legacy device UserDeviceList duplicates the :title (both
use t('legacyDeviceList.title')); change :info to use the correct translation
key (e.g. t('legacyDeviceList.info') or t('legacyDeviceList.description')) if
one exists, otherwise remove the :info prop entirely so it no longer repeats the
title.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (7)
backend/src/main/java/org/cryptomator/hub/entities/User.java (1)
181-186: Improper bidirectional many-to-many mapping still present.Both
User.directGroupMembershipsandGroup.membersdefine@JoinTablefor the same relationship. One side should usemappedByto be the inverse side. This was flagged in a previous review.backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java (4)
106-116: Document the best-effort group assignment behavior or implement rollback.The TODO at line 112 asks whether to rollback user creation on group assignment failure. Current behavior continues despite failures. Either document this as intentional best-effort behavior or implement proper rollback to ensure atomicity.
175-184: Fail-open security: exception returns false, allowing modifications to potentially read-only users.If checking federated identity fails (network issue, Keycloak unavailable), the method returns
false, meaning the user appears editable. Consider fail-closed behavior.🔎 Suggested fix
} catch (WebApplicationException e) { LOG.warn("Failed to check federated identity for user {}. Keycloak responded with status {}", userId, e.getResponse().getStatus(), e); - return false; + throw new InternalServerErrorException("Unable to verify user editability", e); }
321-337: Potential NullPointerException: null check needed for name parameter.Line 325 calls
name.isBlank()butnamecould be null, causing aNullPointerException.🔎 Suggested fix
-if (!name.isBlank()) { +if (name != null && !name.isBlank()) { group.setName(name); }
339-343: Direct database deletion bypasses the syncer.Line 342 directly deletes the group via
groupRepo.deleteById(groupId). This should ideally be handled by the syncer for consistency, especially if the syncer runs concurrently. The same applies touserRepo.deleteByIdat line 172.backend/src/main/java/org/cryptomator/hub/api/UsersResource.java (2)
345-351: Do not expose exception messages to clients.Line 348 returns
e.getMessage()directly in the response body. While the message is controlled ("EMAIL_EXISTS", "USERNAME_EXISTS"), exposing raw exception messages is a security anti-pattern. Use a structured error response instead.🔎 Suggested fix
} catch (ClientErrorException e) { - // Return 409 with specific error message (EMAIL_EXISTS or USERNAME_EXISTS) return Response.status(Response.Status.CONFLICT) - .entity(e.getMessage()) - .type(MediaType.TEXT_PLAIN) + .entity(Map.of("error", e.getMessage())) + .type(MediaType.APPLICATION_JSON) .build(); }
438-446: Avoid catching broad Exception; let exceptions propagate naturally.Lines 443-445 catch all exceptions and wrap them in a
RuntimeException. This obscures the original failure. Based on past review comments, this was flagged as addressed but the code still shows the pattern.🔎 Recommended fix
public Response deleteUser(@PathParam("id") String userId) { - try { - keycloakAdminService.deleteUser(userId); - return Response.noContent().build(); - } catch (ForbiddenException | NotFoundException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("Failed to delete user", e); - } + keycloakAdminService.deleteUser(userId); + return Response.noContent().build(); }The
ForbiddenExceptionandNotFoundExceptionare JAX-RS exceptions handled by the framework. Other unexpected exceptions should propagate naturally.
🧹 Nitpick comments (5)
backend/src/main/java/org/cryptomator/hub/entities/UserMetrics.java (1)
13-26: Consider adding DISTINCT to direct_group_count for consistency.Line 16 uses
COUNT(g.group_id)withoutDISTINCT, while lines 17-18 useCOUNT(DISTINCT ...). Although thegroup_membershiptable likely has unique entries per user-group pair, addingDISTINCTwould make the query consistent and guard against potential data anomalies.🔎 Suggested fix
SELECT u.id AS user_id, - COUNT(g.group_id) AS direct_group_count, + COUNT(DISTINCT g.group_id) AS direct_group_count, COUNT(DISTINCT eva.vault_id) AS effective_vault_count, COUNT(DISTINCT d.id) AS device_countbackend/src/main/java/org/cryptomator/hub/entities/User.java (2)
243-248: Potential memory concern with Stream concatenation in findByIds.The
Stream.concatapproach accumulates all partial streams in memory. For very large ID collections, consider returning aListdirectly or using a collector that doesn't hold intermediate streams.🔎 Alternative approach
public Stream<User> findByIds(Collection<String> ids) { - return Batch.of(200).run(ids, Stream.empty(), (batch, result) -> { - var partial = find("id IN :ids", Parameters.with("ids", batch)); - return Stream.concat(result, partial.stream()); - }); + return Batch.of(200).run(ids, new java.util.ArrayList<User>(), (batch, result) -> { + result.addAll(find("id IN :ids", Parameters.with("ids", batch)).list()); + return result; + }).stream(); }
175-190: Inconsistent field visibility: consider making collection fields private.Fields like
accessTokens,devices,directGroupMemberships, andaccessibleVaultsare public, while other entity fields are private with getters/setters. If this is intentional for Panache active record patterns, consider adding a brief comment explaining the design choice.backend/src/main/java/org/cryptomator/hub/api/UsersResource.java (1)
419-419: Consider checking if roles changed before updating.Unlike
createUser(line 333) which checksif (!dto.realmRoles().isEmpty()),updateUseralways callsupdateUserRoles. This causes unnecessary Keycloak API calls when roles haven't changed.backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java (1)
186-213: Address TODO: Deduplicate with KeycloakAuthorityPuller.The TODO at line 186 notes code duplication with
KeycloakAuthorityPuller. Consider extracting common synchronization logic to a shared helper to maintain consistency and reduce duplication.Do you want me to propose a shared helper class for user synchronization logic?
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java(8 hunks)backend/src/main/java/org/cryptomator/hub/entities/User.java(8 hunks)backend/src/main/java/org/cryptomator/hub/entities/UserMetrics.java(1 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java(1 hunks)
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-02-13T10:33:38.555Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 315
File: backend/src/main/java/org/cryptomator/hub/api/UserDto.java:0-0
Timestamp: 2025-02-13T10:33:38.555Z
Learning: In the UserDto class of the Cryptomator Hub project, the `JsonCreator` constructor is used to handle both new and legacy field names (e.g., `ecdhPublicKey`/`publicKey` and `privateKeys`/`privateKey`) during JSON deserialization, while a separate regular constructor is used for programmatic object creation with only the new field names.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2024-10-17T15:24:07.752Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 296
File: backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java:183-186
Timestamp: 2024-10-17T15:24:07.752Z
Learning: In `VaultRoleFilterTest`, remember that the `securityContext` mock is set in the outer class's setup method at line 46, which applies to nested classes as well. Avoid suggesting redundant setup in nested classes.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java
📚 Learning: 2025-06-06T09:18:42.468Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 343
File: backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java:27-32
Timestamp: 2025-06-06T09:18:42.468Z
Learning: In the KeycloakAuthorityPuller and similar syncer components, don't add broad exception handling to scheduled methods. The Syncer execution context is designed to handle crashes, and it's better to let exceptions propagate rather than catching them broadly. Catching Exception is considered bad practice in this codebase.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java
📚 Learning: 2024-07-27T13:14:40.963Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 282
File: frontend/src/common/userdata.ts:82-88
Timestamp: 2024-07-27T13:14:40.963Z
Learning: The error handling suggestion for `createBrowserKeys` in `frontend/src/common/userdata.ts` was discussed and resolved in PR #282, and should not be repeated in future reviews.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java
📚 Learning: 2025-12-08T11:37:46.953Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 372
File: backend/src/test/java/org/cryptomator/hub/api/ExceedingLicenseLimitsIT.java:116-121
Timestamp: 2025-12-08T11:37:46.953Z
Learning: In the Cryptomator Hub backend schema, foreign key constraints use ON DELETE CASCADE for authority/user references. Specifically, deleting users or groups from the authority table automatically cascades to delete related rows in vault_access and access_token tables. Test cleanup methods that delete users/groups via repositories do not need explicit cleanup of these dependent tables.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/entities/User.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java
📚 Learning: 2025-03-12T14:53:06.855Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: frontend/src/components/LegacyDeviceList.vue:52-87
Timestamp: 2025-03-12T14:53:06.855Z
Learning: In Cryptomator Hub, legacy devices are only of the 'DESKTOP' type, and there are no 'BROWSER' or 'MOBILE' legacy devices.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-03-13T08:37:00.992Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java:204-218
Timestamp: 2025-03-13T08:37:00.992Z
Learning: The event logging for device removals (`eventLogger.logDeviceRemoved()`) doesn't differentiate between legacy and regular devices in Cryptomator Hub to maintain historical consistency in the logs.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-12-18T08:32:42.414Z
Learnt from: iammajid
Repo: cryptomator/hub PR: 376
File: frontend/src/components/authority/GroupInfo.vue:5-5
Timestamp: 2025-12-18T08:32:42.414Z
Learning: In the Cryptomator Hub frontend codebase (frontend/src/common/backend.ts), AuthorityService.fillInMissingPicture() is applied to all user and group DTOs at the service layer, automatically generating Jdenticon fallback images as data URLs when pictureUrl is missing. This means components rendering users or groups can safely assume pictureUrl will always be present and do not need additional fallback handling.
Applied to files:
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java
🧬 Code graph analysis (2)
backend/src/main/java/org/cryptomator/hub/entities/User.java (4)
backend/src/main/java/org/cryptomator/hub/entities/EffectiveVaultAccess.java (2)
ApplicationScoped(173-204)Entity(27-205)backend/src/main/java/org/cryptomator/hub/entities/Group.java (2)
ApplicationScoped(47-63)Entity(22-64)backend/src/main/java/org/cryptomator/hub/entities/AccessToken.java (2)
ApplicationScoped(164-178)Entity(22-179)backend/src/main/java/org/cryptomator/hub/entities/UserMetrics.java (1)
Entity(11-57)
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java (3)
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java (1)
ApplicationScoped(22-176)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java (1)
ApplicationScoped(20-144)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakRealmRoles.java (1)
ApplicationScoped(11-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (8)
backend/src/main/java/org/cryptomator/hub/entities/UserMetrics.java (1)
27-57: LGTM!The read-only entity is well-designed with appropriate
@Immutableand@Subselectannotations. The@JoinColumnwithinsertable = false, updatable = falsecorrectly establishes the relationship without allowing modifications through this entity.backend/src/main/java/org/cryptomator/hub/api/UsersResource.java (3)
248-251: LGTM! N+1 query issue resolved with eager metrics loading.The
findAllWithMetrics()approach efficiently fetches user metrics in a single query withLEFT JOIN FETCH, addressing the previous N+1 performance concern.
363-398: LGTM! Efficient eager loading with admin-only access.The endpoint correctly uses
findByIdWithEagerDetails()to load all related entities in a single query, and is properly restricted to admin users.
448-467: LGTM!The DTO records are well-designed with appropriate validation annotations.
@NotNullonrealmRolesensures roles are always provided, while nullable fields inUpdateUserDtoallow partial updates.backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java (3)
57-60: LGTM!The
@PostConstructinitialization pattern is appropriate for caching theRealmResourceinstance.
345-353: LGTM!The
setPicturehelper correctly handles attribute manipulation without mutating the original map, and properly removes or sets the picture attribute based on input.
247-263: LGTM!Group membership operations correctly sync the group after modifications to ensure the database reflects Keycloak state.
backend/src/main/java/org/cryptomator/hub/entities/User.java (1)
68-70: StringArrayType is properly implemented to handle PostgreSQL array columns. The implementation correctly usesTypes.ARRAYandconnection.createArrayOf("VARCHAR", ...)to mapString[]to the database column defined asVARCHAR[]in the migration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (8)
frontend/src/components/authority/UserEditCreate.vue (3)
482-482: Use i18n for error message instead of hardcoded string.The fallback error message
'An error occurred'is hardcoded. Use an i18n key for consistency.🔎 Proposed fix
- submitError.value = error instanceof Error ? error.message : 'An error occurred'; + submitError.value = error instanceof Error ? error.message : t('common.unexpectedError');
189-189: Inconsistent i18n key namespace for password visibility aria-label.Line 158 uses
t('common.showPassword')/t('common.hidePassword'), but line 189 usest('userEditCreate.showPassword')/t('userEditCreate.hidePassword'). These should be consistent.🔎 Proposed fix
- <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('userEditCreate.showPassword') : t('userEditCreate.hidePassword')" @click="togglePasswordVisibility"> + <button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 focus:outline-none" :aria-label="passwordInputType === 'password' ? t('common.showPassword') : t('common.hidePassword')" @click="togglePasswordVisibility">
445-452: Optimistic state update may cause UX confusion on API failure.
initialUserDatais updated before the API call succeeds (lines 445-452). If the API call fails,userDataHasUnsavedChangeswill returnfalsesince initial matches current, hiding the "unsaved changes" warning even though data wasn't actually saved. Move this update after the successful API response.🔎 Proposed fix
processing.value = true; firstName.value = firstName.value.trim(); lastName.value = lastName.value.trim(); username.value = username.value.trim(); email.value = email.value.trim(); pictureUrl.value = pictureUrl.value.trim(); - initialUserData.value = { - firstName: firstName.value, - lastName: lastName.value, - username: username.value, - email: email.value, - roles: [...selectedRoles.value], - previewUrl: pictureUrl.value - }; - try { if (props.mode === 'EDIT') { // ... API call } else { // ... API call } + initialUserData.value = { + firstName: firstName.value, + lastName: lastName.value, + username: username.value, + email: email.value, + roles: [...selectedRoles.value], + previewUrl: pictureUrl.value + }; + userSaved.value = true;frontend/src/common/backend.ts (3)
122-124: Verify property name alignment with backend API.This was flagged in a previous review. The frontend expects
vaultRolebut the backend'sMemberDto.javamay serialize this field asrole. Confirm the backend JSON property name matches.#!/bin/bash # Check the backend MemberDto to verify the JSON property name rg -n "@JsonProperty|vaultRole|\"role\"" --type=java -B 2 -A 2 backend/src/main/java/org/cryptomator/hub/api/MemberDto.java
345-348: Missing error handling forgetEffectiveMembers.Unlike other methods in this service,
getEffectiveMembersdoesn't handle expected error codes (e.g., 404 for non-existent group). This was flagged in a previous review.🔎 Proposed fix
public async getEffectiveMembers(groupId: string): Promise<UserDto[]> { return axiosAuth.get<UserDto[]>(`/groups/${groupId}/effective-members`) - .then(response => response.data.map(AuthorityService.fillInMissingPicture)); + .then(response => response.data.map(AuthorityService.fillInMissingPicture)) + .catch((error) => rethrowAndConvertIfExpected(error, 404)); }
395-401: Missing error handling forgetUserandupdateUser.Both methods should handle 404 errors for consistency with other service methods. This was flagged in a previous review.
🔎 Proposed fix
public async getUser(userId: string): Promise<UserDtoWithDetails> { - return axiosAuth.get<UserDtoWithDetails>(`/users/${userId}`).then(response => AuthorityService.fillInMissingPicture(response.data)); + return axiosAuth.get<UserDtoWithDetails>(`/users/${userId}`) + .then(response => AuthorityService.fillInMissingPicture(response.data)) + .catch((error) => rethrowAndConvertIfExpected(error, 404)); } public async updateUser(userId: string, dto: UpdateUserDto): Promise<UserDto> { - return axiosAuth.put<UserDto>(`/users/${userId}`, dto).then(response => AuthorityService.fillInMissingPicture(response.data)); + return axiosAuth.put<UserDto>(`/users/${userId}`, dto) + .then(response => AuthorityService.fillInMissingPicture(response.data)) + .catch((error) => rethrowAndConvertIfExpected(error, 404)); }backend/src/main/java/org/cryptomator/hub/entities/User.java (1)
181-186: Bidirectional many-to-many mapping still defines@JoinTableon both sides.This was flagged in a previous review. Both
User.directGroupMembershipsandGroup.membersdefine their own@JoinTableannotations for the samegroup_membershiptable. In JPA, one side should usemappedByto be the inverse side.🔎 Recommended fix - Make User the inverse side
- @ManyToMany - @JoinTable(name = "group_membership", - joinColumns = @JoinColumn(name = "member_id", referencedColumnName = "id"), - inverseJoinColumns = @JoinColumn(name = "group_id", referencedColumnName = "id") - ) + @ManyToMany(mappedBy = "members") public Set<Group> directGroupMemberships = new HashSet<>();frontend/src/components/authority/GroupDetail.vue (1)
111-115: Add null safety to the sort comparison.Line 114 calls
a.name.localeCompare(b.name, ...)without checking ifnameis defined. If any member has an undefinedname, this will throw a TypeError at runtime. This was flagged in a previous review.🔎 Proposed fix
function onMembersSaved(newMembers: AuthorityDto[]) { const ids = new Set(group.value.members.map(u => u.id)); newMembers.forEach(u => { if (!ids.has(u.id)) group.value.members.push(u); }); - group.value.members.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); + group.value.members.sort((a, b) => { + const aName = a.name ?? ''; + const bName = b.name ?? ''; + return aName.localeCompare(bName, undefined, { sensitivity: 'base' }); + }); }
🧹 Nitpick comments (2)
frontend/src/components/authority/GroupEditCreate.vue (1)
211-225: Good pattern: State update after successful API call.Unlike
UserEditCreate.vue, this component correctly updatesinitialGroupDataafter the API call succeeds (lines 211-216), preventing the optimistic update issue.However, the
processing.value = falseis only set in the catch block (line 228), not after successful completion. The redirect happens viasetTimeout, but if the user navigates away before the timeout, the processing state remains true.🔎 Consider adding processing reset on success
// Show saved success state groupSaved.value = true; debouncedGroupSaved(); + processing.value = false; // Redirect after successful save setTimeout(() => { router.push('/app/groups'); }, 1000);frontend/src/components/authority/GroupDetail.vue (1)
41-60: Duplicated layout sections for responsive design.Lines 41-52 and 53-60 render the same components twice with different visibility classes (
hidden lg:gridvsgrid lg:hidden). While this works, it duplicates the component tree and could cause issues with component state or event handlers being called twice.Consider using a single layout with CSS Grid reordering instead:
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start pt-3"> <section class="order-1 lg:order-1 grid gap-6"> <GroupInfo :group="group"/> <VaultList class="hidden lg:block" :vaults="group.vaults" :page-size="10" :visible="true"/> </section> <section class="order-2 lg:order-2 grid gap-6"> <GroupMemberList v-model:members="group.members" :group="group" :page-size="10" :on-saved="onMembersSaved" /> </section> <VaultList class="order-3 lg:hidden" :vaults="group.vaults" :page-size="10" :visible="true"/> </div>This keeps a single component instance for
GroupMemberListandGroupInfo.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java(1 hunks)backend/src/main/java/org/cryptomator/hub/entities/User.java(8 hunks)frontend/eslint.config.js(3 hunks)frontend/src/common/backend.ts(8 hunks)frontend/src/components/authority/GroupDetail.vue(1 hunks)frontend/src/components/authority/GroupEditCreate.vue(1 hunks)frontend/src/components/authority/GroupMemberList.vue(1 hunks)frontend/src/components/authority/GroupMemberRemoveDialog.vue(1 hunks)frontend/src/components/authority/UserDetail.vue(1 hunks)frontend/src/components/authority/UserEditCreate.vue(1 hunks)frontend/src/router/index.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- frontend/src/components/authority/GroupMemberRemoveDialog.vue
- frontend/src/components/authority/UserDetail.vue
- frontend/src/router/index.ts
- frontend/src/components/authority/GroupMemberList.vue
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2024-07-27T13:14:40.963Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 282
File: frontend/src/common/userdata.ts:82-88
Timestamp: 2024-07-27T13:14:40.963Z
Learning: The error handling suggestion for `createBrowserKeys` in `frontend/src/common/userdata.ts` was discussed and resolved in PR #282, and should not be repeated in future reviews.
Applied to files:
frontend/src/components/authority/UserEditCreate.vuefrontend/src/components/authority/GroupDetail.vuefrontend/src/common/backend.ts
📚 Learning: 2025-12-18T08:32:28.253Z
Learnt from: iammajid
Repo: cryptomator/hub PR: 376
File: frontend/src/components/authority/GroupInfo.vue:5-5
Timestamp: 2025-12-18T08:32:28.253Z
Learning: In the frontend codebase, ensure that authority DTOs for users and groups always include a pictureUrl by having AuthorityService.fillInMissingPicture() run at the service layer (as in backend.ts). As a result, components rendering users or groups (e.g., GroupInfo.vue and related components) can safely rely on pictureUrl being present and should not implement their own fallback logic for missing images. When reviewing, verify that the service layer populates pictureUrl for all user/group DTOs and that UI components do not duplicate fallback handling.
Applied to files:
frontend/src/components/authority/UserEditCreate.vuefrontend/src/components/authority/GroupDetail.vuefrontend/src/common/backend.tsfrontend/src/components/authority/GroupEditCreate.vue
📚 Learning: 2025-02-13T10:33:38.555Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 315
File: backend/src/main/java/org/cryptomator/hub/api/UserDto.java:0-0
Timestamp: 2025-02-13T10:33:38.555Z
Learning: In the UserDto class of the Cryptomator Hub project, the `JsonCreator` constructor is used to handle both new and legacy field names (e.g., `ecdhPublicKey`/`publicKey` and `privateKeys`/`privateKey`) during JSON deserialization, while a separate regular constructor is used for programmatic object creation with only the new field names.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-12-19T19:53:33.331Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 376
File: backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java:33-46
Timestamp: 2025-12-19T19:53:33.331Z
Learning: In `backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java`, the `fromKcNames` method intentionally skips unknown Keycloak role names silently, while `fromKcName` throws `IllegalArgumentException` for unknown roles. This asymmetric error handling is by design.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-12-08T11:37:46.953Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 372
File: backend/src/test/java/org/cryptomator/hub/api/ExceedingLicenseLimitsIT.java:116-121
Timestamp: 2025-12-08T11:37:46.953Z
Learning: In the Cryptomator Hub backend schema, foreign key constraints use ON DELETE CASCADE for authority/user references. Specifically, deleting users or groups from the authority table automatically cascades to delete related rows in vault_access and access_token tables. Test cleanup methods that delete users/groups via repositories do not need explicit cleanup of these dependent tables.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-03-12T14:53:06.855Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: frontend/src/components/LegacyDeviceList.vue:52-87
Timestamp: 2025-03-12T14:53:06.855Z
Learning: In Cryptomator Hub, legacy devices are only of the 'DESKTOP' type, and there are no 'BROWSER' or 'MOBILE' legacy devices.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-03-13T08:37:00.992Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java:204-218
Timestamp: 2025-03-13T08:37:00.992Z
Learning: The event logging for device removals (`eventLogger.logDeviceRemoved()`) doesn't differentiate between legacy and regular devices in Cryptomator Hub to maintain historical consistency in the logs.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2024-06-22T04:41:35.492Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 281
File: backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java:1-66
Timestamp: 2024-06-22T04:41:35.492Z
Learning: The `StringArrayType` class in the `backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java` file is used only for read-only purposes, specifically for the `effective_wot` view. The `nullSafeSet` method throws `UnsupportedOperationException` to enforce this behavior.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java
🧬 Code graph analysis (1)
backend/src/main/java/org/cryptomator/hub/entities/User.java (6)
backend/src/main/java/org/cryptomator/hub/entities/EffectiveVaultAccess.java (2)
ApplicationScoped(173-204)Entity(27-205)backend/src/main/java/org/cryptomator/hub/entities/Authority.java (2)
ApplicationScoped(95-108)Entity(20-109)backend/src/main/java/org/cryptomator/hub/entities/Vault.java (2)
ApplicationScoped(251-268)Entity(34-269)backend/src/main/java/org/cryptomator/hub/entities/AccessToken.java (2)
ApplicationScoped(164-178)Entity(22-179)backend/src/main/java/org/cryptomator/hub/entities/UserMetrics.java (1)
Entity(11-57)backend/src/main/java/org/cryptomator/hub/entities/Batch.java (1)
Batch(8-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (18)
frontend/eslint.config.js (4)
7-13: LGTM! Standard ignore patterns applied.Excluding coverage reports, build artifacts, and dependencies from linting is a best practice.
49-49: LGTM! Good addition for code maintainability.Enforcing a cyclomatic complexity limit of 10 for TypeScript files helps maintain code quality and readability.
104-104: LGTM! Appropriate complexity threshold for Vue SFCs.Setting a higher complexity limit (15) for Vue files compared to TypeScript files (10) is reasonable, as single-file components naturally have higher complexity due to template logic and component composition.
100-100: Rule disabling is acceptable for modern SFC-based Vue projects, but verify single-word component usage.The
vue/multi-word-component-namesrule requires component names to be multi-word to prevent conflicts with HTML elements, since all HTML elements are single words. However, conflicts only arise when components are used in their lowercase form (HTML templates), while using the uppercase form (SFC templates) completely eliminates conflicts. In SFCs, PascalCase tag names for child components are recommended to differentiate from native HTML elements, and Vue SFC is a compiled format that enables case-sensitive tag names.If this project exclusively uses SFC templates with PascalCase component names (modern Vue 3 default), the rule disabling carries minimal risk. Verify that:
- Single-word components like
<User>,<Button>, or<Header>are not created, or- If used, they only appear in SFC templates with PascalCase casing (not in DOM templates or lowercase forms)
backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java (3)
55-72: LGTM: Correct implementation of mutable UserType methods.The
deepCopy,disassemble, andassemblemethods are correctly implemented for a mutable custom type:
deepCopysafely clones arrays to prevent shared references.disassembleappropriately delegates todeepCopyfor serialization.assembledefensively validates and clones the cached value.These implementations align with the change to
isMutable() = trueand follow Hibernate best practices.
50-53: The architectural shift from read-only to mutable is intentional and necessary for the V23 migration. ThenullSafeSet()method now properly supports write operations viaconnection.createArrayOf()andst.setArray(). The change maintains backward compatibility: theEffectiveWotview continues to function with read-only signature chains, while theUserentity now supports the newrealm_rolescolumn. The implementation correctly implementsUserType<String[]>with proper mutability semantics across all methods (deepCopy,assemble,disassemble).
36-40: The implementation is correct and aligns with intended usage patterns.Regarding the specific code at lines 36-40: Returning
new String[0]when the ResultSet array is null is intentional and correct. TherealmRolesfield in the User entity is initialized withnew String[0]as the default value, and thesignatureChainin EffectiveWot consistently expects array values rather than null. No null-checking code exists for these fields in the codebase, confirming that empty arrays are the expected representation.The resource management in
nullSafeSet(lines 42-48) is correct. The Array object created bycreateArrayOfis passed tost.setArray(), where the JDBC driver takes ownership for lifecycle management. The change toisMutable = trueis intentional and part of the Web of Trust feature enablement (PR #281), transitioning from read-only database views to full write support.backend/src/main/java/org/cryptomator/hub/entities/User.java (2)
68-70: LGTM on the realm roles mapping.The
String[]type with@Type(StringArrayType.class)is the correct approach for mapping a PostgreSQL array column. This properly addresses the storage of realm roles without requiring a separate join table.
84-86: Consider usingFetchType.LAZYexplicitly for clarity and add orphan removal consideration.The
@OneToOnemapping toUserMetricsis lazy, which is appropriate. However, sinceUserMetricsis@Immutableand derived from a@Subselect, this is read-only. The current mapping looks correct for the use case.frontend/src/components/authority/UserEditCreate.vue (2)
243-249: Props type definition uses discriminated union correctly.The discriminated union pattern for
CREATEvsEDITmodes is well-designed, ensuring type safety for theidprop based on mode.
263-272: LGTM on the roles comparison fix.The array comparison now uses
JSON.stringifywith sorted arrays, which correctly compares by value rather than reference. This addresses the previous review feedback.frontend/src/common/backend.ts (3)
37-39: Good addition ofisAxiosErrortype guard export.Exporting the Axios type guard utility enables type-safe error handling in components without importing Axios directly.
70-74: Well-designed type guard for selectable roles.The
isSelectableRealmRoletype guard properly narrowsRealmRoletoSelectableRealmRole, enabling safe filtering in the UI.
310-361: GroupService implementation follows established patterns.The new
GroupServicecorrectly:
- Applies
AuthorityService.fillInMissingPictureto all returned DTOs- Uses
rethrowAndConvertIfExpectedfor error handling on most methods- Returns
Promise<void>for delete/add/remove operationsfrontend/src/components/authority/GroupEditCreate.vue (2)
102-108: Props discriminated union is well-designed.Same clean pattern as
UserEditCreate.vuefor distinguishing CREATE vs EDIT modes with proper type narrowing.
65-65:focus:outline-hiddenis the correct Tailwind CSS v4 utility; no change needed.In Tailwind CSS v4,
outline-hiddenis the standard utility for hiding browser outlines, notoutline-nonewhich was used in v3. The code on line 65 is correct.frontend/src/components/authority/GroupDetail.vue (2)
120-131: Good error handling pattern in fetchGroup.The
fetchGroupfunction properly:
- Resets loading and error state before fetching
- Captures errors to
fetchErrorfor UI display- Uses
finallyto ensure loading state is clearedThis correctly addresses the previous review feedback about user feedback on fetch failures.
16-16: Inconsistent Tailwind class:focus:outline-hidden.Same issue as in
GroupEditCreate.vue-focus:outline-hiddenis not a standard Tailwind utility. Usefocus:outline-noneinstead.⛔ Skipped due to learnings
Learnt from: overheadhunter Repo: cryptomator/hub PR: 306 File: frontend/src/components/RegenerateSetupCodeDialog.vue:85-85 Timestamp: 2025-01-30T21:52:25.038Z Learning: When reviewing code that uses Tailwind CSS v4, the `focus:outline-hidden` utility is the correct way to hide focus outlines, replacing the v3 utility `focus:outline-none`.Learnt from: overheadhunter Repo: cryptomator/hub PR: 306 File: frontend/src/components/SimpleNavigationBar.vue:22-22 Timestamp: 2025-01-30T21:49:08.122Z Learning: When migrating from Tailwind CSS v3 to v4, the `outline-none` utility must be changed to `outline-hidden` as per the official migration guide.Learnt from: overheadhunter Repo: cryptomator/hub PR: 306 File: frontend/src/components/RegenerateSetupCodeDialog.vue:34-34 Timestamp: 2025-01-30T21:52:30.816Z Learning: When migrating from Tailwind CSS v3 to v4, the `outline-none` utility must be changed to `outline-hidden` as per the official migration guide.Learnt from: overheadhunter Repo: cryptomator/hub PR: 306 File: frontend/src/components/SimpleNavigationBar.vue:22-22 Timestamp: 2025-01-30T21:49:08.122Z Learning: When migrating from Tailwind CSS v3 to v4, the opacity modifier syntax has changed - `ring-black ring-opacity-5` should be updated to `ring-black/5` as per the official migration guide.
| @Override | ||
| public void nullSafeSet(PreparedStatement st, String[] value, int index, SharedSessionContractImplementor session) { | ||
| throw new UnsupportedOperationException("Read Only"); | ||
| public void nullSafeSet(PreparedStatement st, String[] value, int index, SharedSessionContractImplementor session) throws SQLException { | ||
| session.doWork(connection -> { | ||
| var jdbcArray = connection.createArrayOf("VARCHAR", value == null ? new String[0] : value); | ||
| st.setArray(index, jdbcArray); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential resource leak: SQL Array not closed.
The java.sql.Array returned by connection.createArrayOf() implements AutoCloseable (as of JDBC 4.2). Not closing it can lead to resource leaks, especially under high load.
🔎 Proposed fix to ensure the array is closed
public void nullSafeSet(PreparedStatement st, String[] value, int index, SharedSessionContractImplementor session) throws SQLException {
session.doWork(connection -> {
- var jdbcArray = connection.createArrayOf("VARCHAR", value == null ? new String[0] : value);
- st.setArray(index, jdbcArray);
+ try (var jdbcArray = connection.createArrayOf("VARCHAR", value == null ? new String[0] : value)) {
+ st.setArray(index, jdbcArray);
+ }
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Override | |
| public void nullSafeSet(PreparedStatement st, String[] value, int index, SharedSessionContractImplementor session) { | |
| throw new UnsupportedOperationException("Read Only"); | |
| public void nullSafeSet(PreparedStatement st, String[] value, int index, SharedSessionContractImplementor session) throws SQLException { | |
| session.doWork(connection -> { | |
| var jdbcArray = connection.createArrayOf("VARCHAR", value == null ? new String[0] : value); | |
| st.setArray(index, jdbcArray); | |
| }); | |
| } | |
| @Override | |
| public void nullSafeSet(PreparedStatement st, String[] value, int index, SharedSessionContractImplementor session) throws SQLException { | |
| session.doWork(connection -> { | |
| try (var jdbcArray = connection.createArrayOf("VARCHAR", value == null ? new String[0] : value)) { | |
| st.setArray(index, jdbcArray); | |
| } | |
| }); | |
| } |
🤖 Prompt for AI Agents
In backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java
around lines 42 to 48, the java.sql.Array returned by
connection.createArrayOf(...) is not closed, risking a resource leak; wrap the
createArrayOf call in a try-with-resources (or otherwise ensure
jdbcArray.close() in a finally) inside the session.doWork lambda so you set the
Array on the PreparedStatement and then close the Array immediately after; keep
the null-to-empty-array logic but ensure the Array is always closed even if
st.setArray throws.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope it does not implement AutoCloweable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
java.sql.Array free method resource leak JDBC
💡 Result:
Yes — java.sql.Array can hold driver/server resources and should be freed to avoid leaks. JDBC added Array.free() (since JDBC 4 / Java 6) so you should call free() when finished (typically in a finally block) because Array does not implement AutoCloseable. Some drivers may release on transaction end or GC, but relying on that can leak resources during long-running transactions. Example:
Array arr = null;
try {
arr = rs.getArray("col");
Object[] data = (Object[]) arr.getArray();
// use data
} finally {
if (arr != null) try { arr.free(); } catch (SQLException ignored) {}
}
References:
- Oracle JDBC tutorial — "Releasing Array Resources" (Array.free()). [1]
- Stack Overflow discussion recommending calling free() for LOBs/Array. [2]
- jOOQ/blog explaining why free() should be called to avoid long-lived resources. [3]
@overheadhunter, you're absolutely right—my apologies for the error. java.sql.Array does not implement AutoCloseable.
However, the resource leak concern is still valid. Array has a free() method (added in JDBC 4) that should be called to release driver/server resources, especially in long-running transactions. Since Array isn't AutoCloseable, you'll need to use a traditional try-finally block:
@Override
public void nullSafeSet(PreparedStatement st, String[] value, int index, SharedSessionContractImplementor session) throws SQLException {
session.doWork(connection -> {
Array jdbcArray = null;
try {
jdbcArray = connection.createArrayOf("VARCHAR", value == null ? new String[0] : value);
st.setArray(index, jdbcArray);
} finally {
if (jdbcArray != null) {
try {
jdbcArray.free();
} catch (SQLException ignored) {
// Log if needed
}
}
}
});
}This ensures the Array resources are properly released. That said, if your JDBC driver automatically frees arrays at transaction end or if this code path isn't high-volume, the risk may be minimal. Let me know if you'd prefer to skip this safeguard!
🐰💭
(Oops, got that one wrong!)
/) /)
( . .)
c(")(")
🧠 Learnings used
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 281
File: backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java:1-66
Timestamp: 2024-06-22T04:41:35.492Z
Learning: The `StringArrayType` class in the `backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java` file is used only for read-only purposes, specifically for the `effective_wot` view. The `nullSafeSet` method throws `UnsupportedOperationException` to enforce this behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still disagree. The PreparedStatement still references the array and should be responsible for freeing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
JDBC PreparedStatement setArray Array free responsibility lifecycle
💡 Result:
Short answer: you (the application) are responsible for freeing java.sql.Array objects — call array.free() when you're done. The Array.free() method exists to release driver/DB resources; relying on PreparedStatement/Connection closure or GC is implementation-dependent and not guaranteed. See JDBC API javadoc for Array.free(), PreparedStatement.setArray() behavior, and driver docs for specifics. [1][2][3][4]
Sources
- java.sql.Array.free() javadoc. [1]
- PreparedStatement.setArray() javadoc (driver converts Array to SQL ARRAY). [2]
- Oracle JDBC note on creating/handling ARRAY objects (driver-specific behavior). [3]
- Example driver implementation (PostgreSQL PgArray) showing Array as a JDBC resource. [4]
🌐 Web query:
java.sql.Array free when passed to PreparedStatement
💡 Result:
Short answer: call Array.free() only after the PreparedStatement has used the Array (i.e., after execute/executeUpdate/executeQuery). The Array remains valid for at least the transaction duration and some drivers may not copy data on setArray, so freeing it before execution can break the statement. Best practice: setArray(...), execute the statement, then free() in a finally/try-with-resources cleanup block. [1][2]
Sources
- Oracle JDBC tutorial — Releasing Array resources (Array.free()). [1]
- PostgreSQL JDBC documentation — Arrays and PreparedStatement.setArray. [2]
@overheadhunter, you're right—I apologize for the confusion. Given the execution model here, calling Array.free() immediately after setArray() would be premature and could break the statement, since the PreparedStatement hasn't executed yet when this lambda completes.
The JDBC docs confirm that Array.free() should only be called after the statement executes. In this case, the Array is passed to st.setArray() inside the session.doWork() lambda, but the actual execution happens later in Hibernate's lifecycle. Since the PreparedStatement holds the reference and will execute after this method returns, it's reasonable to rely on Hibernate's session/statement cleanup mechanisms to handle the Array lifecycle appropriately.
Thank you for the clarification! 🐰
⛔ Learnings removed
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 281
File: backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java:1-66
Timestamp: 2024-06-22T04:41:35.492Z
Learning: The `StringArrayType` class in the `backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java` file is used only for read-only purposes, specifically for the `effective_wot` view. The `nullSafeSet` method throws `UnsupportedOperationException` to enforce this behavior.
🧠 Learnings used
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 281
File: backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java:1-66
Timestamp: 2024-06-22T04:41:35.492Z
Learning: The `StringArrayType` class in the `backend/src/main/java/org/cryptomator/hub/entities/StringArrayType.java` file is used only for read-only purposes, specifically for the `effective_wot` view. The `nullSafeSet` method throws `UnsupportedOperationException` to enforce this behavior.
| } catch (error) { | ||
| console.error('Failed to fetch group:', error); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent failure on group fetch error in EDIT mode.
When fetching the group fails, the error is logged but the user sees an empty form with no indication of the failure. Consider adding error state handling similar to GroupDetail.vue.
🔎 Proposed fix
+const fetchError = ref<Error | null>(null);
onMounted(async () => {
if (props.mode === 'EDIT') {
try {
const group = await backend.groups.getGroup(props.id);
name.value = group.name;
pictureUrl.value = group.pictureUrl ?? '';
initialGroupData.value = {
id: group.id,
name: group.name,
pictureUrl: group.pictureUrl ?? ''
};
} catch (error) {
- console.error('Failed to fetch group:', error);
+ fetchError.value = error instanceof Error ? error : new Error('Unknown Error');
}
}
// ...
});Then add <FetchError v-else-if="fetchError" :error="fetchError" /> in the template.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In frontend/src/components/authority/GroupEditCreate.vue around lines 163 to
165, the catch block only logs the fetch error so EDIT mode shows an empty form;
add a reactive fetchError state (e.g., ref<string|null>), set fetchError to the
caught error message inside the catch, and ensure you clear/set loading
accordingly; then update the template to render <FetchError
v-else-if="fetchError" :error="fetchError" /> (and guard the form with v-else
when no fetchError) so users see a visible error instead of an empty form.
avoid cartesian product explosion by only left-joining with tables that we don't expect to produce more than a handful of rows
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
backend/src/main/java/org/cryptomator/hub/entities/User.java (1)
183-188: Bidirectional many-to-many: verify ownership side.The previous review flagged that both
User.directGroupMembershipsandGroup.membersdefine@JoinTablefor the samegroup_membershiptable. This is still the case. One side should usemappedByto designate the inverse side.If
Groupis intended to be the owning side (common pattern since groups "contain" members), change this to:-@ManyToMany -@JoinTable(name = "group_membership", - joinColumns = @JoinColumn(name = "member_id", referencedColumnName = "id"), - inverseJoinColumns = @JoinColumn(name = "group_id", referencedColumnName = "id") -) +@ManyToMany(mappedBy = "members") public Set<Group> directGroupMemberships = new HashSet<>();
🧹 Nitpick comments (1)
backend/src/main/java/org/cryptomator/hub/entities/User.java (1)
121-127: Consider defensive copying for the mutable array.The getter returns the internal array directly and the setter stores the reference without copying. This allows external code to modify the internal state. While this may be acceptable given your usage patterns, defensive copies are a common safeguard.
🔎 Proposed defensive copy implementation
public String[] getRealmRoles() { - return realmRoles; + return realmRoles != null ? realmRoles.clone() : null; } public void setRealmRoles(String[] realmRoles) { - this.realmRoles = realmRoles; + this.realmRoles = realmRoles != null ? realmRoles.clone() : null; }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
backend/src/main/java/org/cryptomator/hub/entities/User.java(8 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-02-13T10:33:38.555Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 315
File: backend/src/main/java/org/cryptomator/hub/api/UserDto.java:0-0
Timestamp: 2025-02-13T10:33:38.555Z
Learning: In the UserDto class of the Cryptomator Hub project, the `JsonCreator` constructor is used to handle both new and legacy field names (e.g., `ecdhPublicKey`/`publicKey` and `privateKeys`/`privateKey`) during JSON deserialization, while a separate regular constructor is used for programmatic object creation with only the new field names.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-12-19T19:53:33.331Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 376
File: backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java:33-46
Timestamp: 2025-12-19T19:53:33.331Z
Learning: In `backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java`, the `fromKcNames` method intentionally skips unknown Keycloak role names silently, while `fromKcName` throws `IllegalArgumentException` for unknown roles. This asymmetric error handling is by design.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-12-08T11:37:46.953Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 372
File: backend/src/test/java/org/cryptomator/hub/api/ExceedingLicenseLimitsIT.java:116-121
Timestamp: 2025-12-08T11:37:46.953Z
Learning: In the Cryptomator Hub backend schema, foreign key constraints use ON DELETE CASCADE for authority/user references. Specifically, deleting users or groups from the authority table automatically cascades to delete related rows in vault_access and access_token tables. Test cleanup methods that delete users/groups via repositories do not need explicit cleanup of these dependent tables.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-03-12T14:53:06.855Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: frontend/src/components/LegacyDeviceList.vue:52-87
Timestamp: 2025-03-12T14:53:06.855Z
Learning: In Cryptomator Hub, legacy devices are only of the 'DESKTOP' type, and there are no 'BROWSER' or 'MOBILE' legacy devices.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-03-13T08:37:00.992Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java:204-218
Timestamp: 2025-03-13T08:37:00.992Z
Learning: The event logging for device removals (`eventLogger.logDeviceRemoved()`) doesn't differentiate between legacy and regular devices in Cryptomator Hub to maintain historical consistency in the logs.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (4)
backend/src/main/java/org/cryptomator/hub/entities/User.java (4)
70-72: LGTM! Previous JPA mapping concern addressed.The field now correctly uses
String[]with@Type(StringArrayType.class)for proper array-column mapping, resolving the previous issue withSet<String>and@Column.
86-88: LGTM!The
@OneToOnemapping withmappedBycorrectly establishes the inverse side of the bidirectional relationship withUserMetrics, and lazy fetching is appropriate for optional metrics data.
233-258: Good fix: Split queries address Cartesian product concern.The two-query approach properly addresses the previous review's Cartesian product concern. Fetching
accessibleVaultsseparately prevents the multiplicative row explosion, while the comment correctly justifies keepingdirectGroupMemberships,devices, andlegacyDevicesin one query for typical small cardinalities.
260-265: LGTM!Good use of batch processing with
Batch.of(200)to prevent largeINclause issues when querying by multiple IDs.
avoid cartesian product explosion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
backend/src/main/java/org/cryptomator/hub/entities/Group.java (1)
35-37: MakeaccessibleVaultsprivate with a getter for consistency and encapsulation.The
accessibleVaultsfield is public, which breaks encapsulation and is inconsistent with the existing pattern wheremembers(line 33) is private with a getter. While@Immutableprevents Hibernate from tracking changes, the publicSetcan still be modified directly from external code (e.g.,.add(),.clear()), potentially leading to unexpected behavior.🔎 Proposed fix to make the field private with a getter
@Immutable @OneToMany(mappedBy = "authority", fetch = FetchType.LAZY) - public Set<VaultAccess> accessibleVaults = new HashSet<>(); + private Set<VaultAccess> accessibleVaults = new HashSet<>(); + + public Set<VaultAccess> getAccessibleVaults() { + return accessibleVaults; + }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
backend/src/main/java/org/cryptomator/hub/entities/Group.java(3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-08T11:37:46.953Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 372
File: backend/src/test/java/org/cryptomator/hub/api/ExceedingLicenseLimitsIT.java:116-121
Timestamp: 2025-12-08T11:37:46.953Z
Learning: In the Cryptomator Hub backend schema, foreign key constraints use ON DELETE CASCADE for authority/user references. Specifically, deleting users or groups from the authority table automatically cascades to delete related rows in vault_access and access_token tables. Test cleanup methods that delete users/groups via repositories do not need explicit cleanup of these dependent tables.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/Group.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (1)
backend/src/main/java/org/cryptomator/hub/entities/Group.java (1)
51-64: LGTM! Properly mitigates cartesian product concerns.The two-step fetch approach correctly addresses the cartesian product issue flagged in the previous review:
- First query eagerly fetches members via
LEFT JOIN FETCH- Second step explicitly initializes
accessibleVaultsusingHibernate.initialize()This avoids row multiplication and is an efficient solution for loading both collections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java (2)
335-338: Potential orphaned Keycloak user if database sync fails.If Keycloak user creation succeeds but the user is not found in the database (line 335-338), an
InternalServerErrorExceptionis thrown, leaving an orphaned user in Keycloak with no corresponding database record.This was flagged in a previous review but remains unaddressed.
Consider implementing cleanup logic to remove the Keycloak user if sync fails, or investigate why the sync might fail and ensure it happens within a proper transaction context.
🔎 Suggested fix with cleanup
User user = userRepo.findById(userRepresentation.getId()); if (user == null) { + // Attempt cleanup if sync failed + try { + keycloakAdminService.deleteUser(userRepresentation.getId()); + } catch (Exception cleanupEx) { + LOG.severe("Failed to cleanup orphaned Keycloak user: " + userRepresentation.getId()); + } throw new InternalServerErrorException("User was created in Keycloak but not found in database after sync"); }
343-349: Avoid exposing internal error messages to clients.Returning
e.getMessage()directly exposes internal Keycloak error details (like "USERNAME_EXISTS" or "EMAIL_EXISTS") to the client. This was flagged in a previous review.Consider returning a sanitized, user-friendly error message instead of the raw exception message.
Based on past review comments.
🔎 Suggested fix
} catch (ClientErrorException e) { - // Return 409 with specific error message (EMAIL_EXISTS or USERNAME_EXISTS) - return Response.status(Response.Status.CONFLICT) - .entity(e.getMessage()) - .type(MediaType.TEXT_PLAIN) - .build(); + // Return 409 with user-friendly message + String message = "User with this username or email already exists"; + return Response.status(Response.Status.CONFLICT) + .entity(message) + .type(MediaType.TEXT_PLAIN) + .build(); }
🧹 Nitpick comments (4)
backend/src/test/java/org/cryptomator/hub/api/GroupsResourceIT.java (1)
147-164: Test data setup/teardown uses raw SQL with potential orphan risk.The
@BeforeAllsetup inserts into bothauthorityandgroup_detailstables, but the@AfterAllteardown only deletes fromauthority. If there's a foreign key constraint without cascade delete, or ifgroup_detailshas additional cleanup requirements, this could leave orphaned data.🔎 Suggested fix for consistent cleanup
@AfterAll void tearDown() throws SQLException { try (var c = dataSource.getConnection(); var s = c.createStatement()) { s.execute(""" + DELETE FROM "group_details" WHERE "id" = 'newGroupId123'; DELETE FROM "authority" WHERE "id" = 'newGroupId123'; """); } }backend/src/main/java/org/cryptomator/hub/api/GroupsResource.java (3)
71-93:@Transactionalmay be unnecessary on Keycloak-only operations.The
addMemberandremoveMembermethods are marked@Transactionalbut only delegate tokeycloakAdminServicewithout modifying local database state. If Keycloak sync happens asynchronously via a separate process, the transaction boundary here provides no benefit and adds overhead.Consider removing
@Transactionalunless there's a local database operation that should be part of this transaction:@POST @Path("/{groupId}/members/{userId}") @RolesAllowed("admin") -@Transactional @Operation(summary = "add a user to a group") ... @DELETE @Path("/{groupId}/members/{userId}") @RolesAllowed("admin") -@Transactional @Operation(summary = "remove a user from a group")
166-176: Same@Transactionalconsideration as member operations.The
deleteGroupmethod only calls Keycloak service and returns a response. If there's no local database cleanup happening within this transaction boundary,@Transactionalmay be unnecessary.
178-189: Consider consolidating identical DTO records or documenting the separation.
CreateGroupDtoandUpdateGroupDtohave identical fields and validation. While keeping them separate allows future divergence (e.g., different required fields for create vs. update), consider adding a brief comment explaining this design choice, or consolidating into a singleGroupFormDtoif they're expected to remain identical.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
backend/src/main/java/org/cryptomator/hub/api/GroupDto.java(2 hunks)backend/src/main/java/org/cryptomator/hub/api/GroupsResource.java(2 hunks)backend/src/main/java/org/cryptomator/hub/api/UsersResource.java(7 hunks)backend/src/main/java/org/cryptomator/hub/entities/User.java(8 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java(2 hunks)backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java(4 hunks)backend/src/test/java/org/cryptomator/hub/api/GroupsResourceIT.java(5 hunks)backend/src/test/java/org/cryptomator/hub/api/UsersResourceIT.java(5 hunks)backend/src/test/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProviderTest.java(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/src/test/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProviderTest.java
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-12-19T19:53:33.331Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 376
File: backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java:33-46
Timestamp: 2025-12-19T19:53:33.331Z
Learning: In `backend/src/main/java/org/cryptomator/hub/keycloak/RealmRole.java`, the `fromKcNames` method intentionally skips unknown Keycloak role names silently, while `fromKcName` throws `IllegalArgumentException` for unknown roles. This asymmetric error handling is by design.
Applied to files:
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.javabackend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/api/GroupsResource.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.javabackend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-06-06T09:18:42.468Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 343
File: backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java:27-32
Timestamp: 2025-06-06T09:18:42.468Z
Learning: In the KeycloakAuthorityPuller and similar syncer components, don't add broad exception handling to scheduled methods. The Syncer execution context is designed to handle crashes, and it's better to let exceptions propagate rather than catching them broadly. Catching Exception is considered bad practice in this codebase.
Applied to files:
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.javabackend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/api/GroupsResource.java
📚 Learning: 2025-02-13T10:33:38.555Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 315
File: backend/src/main/java/org/cryptomator/hub/api/UserDto.java:0-0
Timestamp: 2025-02-13T10:33:38.555Z
Learning: In the UserDto class of the Cryptomator Hub project, the `JsonCreator` constructor is used to handle both new and legacy field names (e.g., `ecdhPublicKey`/`publicKey` and `privateKeys`/`privateKey`) during JSON deserialization, while a separate regular constructor is used for programmatic object creation with only the new field names.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/api/GroupDto.javabackend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.javabackend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2024-10-17T15:24:07.752Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 296
File: backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java:183-186
Timestamp: 2024-10-17T15:24:07.752Z
Learning: In `VaultRoleFilterTest`, remember that the `securityContext` mock is set in the outer class's setup method at line 46, which applies to nested classes as well. Avoid suggesting redundant setup in nested classes.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java
📚 Learning: 2024-07-27T13:14:40.963Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 282
File: frontend/src/common/userdata.ts:82-88
Timestamp: 2024-07-27T13:14:40.963Z
Learning: The error handling suggestion for `createBrowserKeys` in `frontend/src/common/userdata.ts` was discussed and resolved in PR #282, and should not be repeated in future reviews.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java
📚 Learning: 2025-12-08T11:37:46.953Z
Learnt from: overheadhunter
Repo: cryptomator/hub PR: 372
File: backend/src/test/java/org/cryptomator/hub/api/ExceedingLicenseLimitsIT.java:116-121
Timestamp: 2025-12-08T11:37:46.953Z
Learning: In the Cryptomator Hub backend schema, foreign key constraints use ON DELETE CASCADE for authority/user references. Specifically, deleting users or groups from the authority table automatically cascades to delete related rows in vault_access and access_token tables. Test cleanup methods that delete users/groups via repositories do not need explicit cleanup of these dependent tables.
Applied to files:
backend/src/main/java/org/cryptomator/hub/api/UsersResource.javabackend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-12-18T08:32:42.414Z
Learnt from: iammajid
Repo: cryptomator/hub PR: 376
File: frontend/src/components/authority/GroupInfo.vue:5-5
Timestamp: 2025-12-18T08:32:42.414Z
Learning: In the Cryptomator Hub frontend codebase (frontend/src/common/backend.ts), AuthorityService.fillInMissingPicture() is applied to all user and group DTOs at the service layer, automatically generating Jdenticon fallback images as data URLs when pictureUrl is missing. This means components rendering users or groups can safely assume pictureUrl will always be present and do not need additional fallback handling.
Applied to files:
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java
📚 Learning: 2025-03-12T14:53:06.855Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: frontend/src/components/LegacyDeviceList.vue:52-87
Timestamp: 2025-03-12T14:53:06.855Z
Learning: In Cryptomator Hub, legacy devices are only of the 'DESKTOP' type, and there are no 'BROWSER' or 'MOBILE' legacy devices.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
📚 Learning: 2025-03-13T08:37:00.992Z
Learnt from: SailReal
Repo: cryptomator/hub PR: 331
File: backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java:204-218
Timestamp: 2025-03-13T08:37:00.992Z
Learning: The event logging for device removals (`eventLogger.logDeviceRemoved()`) doesn't differentiate between legacy and regular devices in Cryptomator Hub to maintain historical consistency in the logs.
Applied to files:
backend/src/main/java/org/cryptomator/hub/entities/User.java
🧬 Code graph analysis (3)
backend/src/test/java/org/cryptomator/hub/api/UsersResourceIT.java (1)
backend/src/test/java/org/cryptomator/hub/api/GroupsResourceIT.java (3)
Nested(91-115)Nested(117-131)Nested(133-365)
backend/src/main/java/org/cryptomator/hub/api/GroupDto.java (1)
frontend/src/common/backend.ts (1)
GroupDto(111-118)
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java (1)
frontend/src/common/backend.ts (1)
RealmRole(70-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Tests
🔇 Additional comments (27)
backend/src/test/java/org/cryptomator/hub/api/UsersResourceIT.java (3)
12-16: LGTM: Proper test dependencies and mock setup.The new imports and KeycloakAdminService mock injection are appropriate for testing user CRUD operations with Keycloak integration. This follows the same pattern established in GroupsResourceIT.
Also applies to: 30-30, 57-58
367-398: LGTM: Well-structured test suite with proper lifecycle management.The nested
UserCrudOperationsclass follows the same pattern asGroupCrudOperationsin GroupsResourceIT. The use ofBeforeEachto reset mocks ensures test isolation, andBeforeAll/AfterAllproperly manage test data setup and cleanup.
400-638: LGTM: Comprehensive CRUD test coverage.The test methods provide excellent coverage of all CRUD operations and error scenarios:
- Success cases (201, 200, 204) with proper mock setup
- Conflict scenarios (409) using
ClientErrorException- Validation failures (400)
- Not found scenarios (404) using
NotFoundException- Forbidden scenarios (403) using
ForbiddenExceptionThe tests follow the established patterns from GroupsResourceIT and properly verify interactions with the mocked Keycloak service.
backend/src/main/java/org/cryptomator/hub/api/UsersResource.java (7)
10-37: LGTM: Proper dependencies for Keycloak-backed CRUD operations.The new imports and injected dependencies (KeycloakAdminService, Group.Repository, RealmRole) are appropriate for implementing admin user management with Keycloak synchronization.
Also applies to: 74-81
193-193: LGTM: Consistent DTO construction with extended user attributes.The updated UserDto constructor calls now include
firstName,lastName, andrealmRoles, properly exposing these attributes that are synchronized from Keycloak to the database.Also applies to: 217-217
242-249: LGTM: N+1 query pattern resolved with batch metrics.The updated
getAll()method now usesfindAllWithMetrics()which loads user metrics in batch, eliminating the N+1 query pattern flagged in previous reviews. The admin-only restriction is also properly in place.
352-395: LGTM: Efficient user detail retrieval with proper access control.The
getUserendpoint now properly:
- Restricts access to admin role only (addressing security concern from past reviews)
- Uses
findByIdWithEagerDetails()for efficient eager loading of related entities- Avoids N+1 queries by loading associations in batch
- Fetches user data from the database rather than making additional Keycloak calls
397-424: LGTM: Clean error handling with natural exception propagation.The
updateUsermethod properly lets specific exceptions (ForbiddenException, NotFoundException) propagate naturally from the Keycloak service, avoiding the broad exception catching that was flagged in previous reviews.
426-437: LGTM: Simple and clean delete operation.The
deleteUsermethod properly delegates to the Keycloak service and lets exceptions propagate naturally, avoiding the broad exception catching that was removed in previous iterations.
439-458: LGTM: Well-structured DTOs with proper validation.The
CreateUserDtoandUpdateUserDtorecords are properly designed with:
- Appropriate validation annotations (
@NotNullwhere required)- Clear field names matching API expectations
- Proper use of Java records for immutable data transfer
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java (3)
36-45: LGTM! Pre-sync realm role enrichment is correctly implemented.The pre-sync enrichment logic correctly fetches users for each realm role and populates the
KeycloakUserDto.roles()collection prior to the main sync. The null check on line 41 prevents NPEs when a user representation doesn't have a correspondingKeycloakUserDtoentry.
79-82: LGTM! New user fields populated correctly during sync.The firstName, lastName, and realmRoles fields are correctly populated from the KeycloakUserDto during user additions, following the same pattern as existing fields.
106-109: LGTM! Existing user fields updated correctly during sync.The firstName, lastName, and realmRoles fields are correctly updated from the KeycloakUserDto during user updates, maintaining consistency with the addition logic.
backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityProvider.java (3)
74-79: LGTM! Picture URL parsing is now more defensive.The updated
parsePictureUrlmethod correctly handles null attributes and safely retrieves the first picture URL or returns null, improving robustness.
124-141: LGTM! Role-based user retrieval follows established pagination pattern.The new
usersInRolemethods correctly implement paginated retrieval of users for a given realm role, following the same pagination pattern used byusers(),groups(), anddeepCollectMembers(). The delegation from the public method to the internal helper is clean and testable.
64-70: No action needed. TheRealmRole.fromKcNames()method already safely handles null input by checkingif (kcNames == null || kcNames.isEmpty())on line 34 and returning an empty set. This graceful handling meansuserRepresentation.getRealmRoles()returning null poses no risk.Likely an incorrect or invalid review comment.
backend/src/main/java/org/cryptomator/hub/entities/User.java (5)
58-62: LGTM! First name and last name fields correctly mapped.The firstName and lastName fields are correctly mapped to database columns using standard JPA
@Columnannotations.
67-69: LGTM! Realm roles correctly mapped using custom Hibernate type.The realmRoles field correctly uses
@Type(StringArrayType.class)for mapping a String array, which is a valid approach for persisting string arrays. This addresses the previous issue where a Set was incorrectly mapped with@Column.
83-84: LGTM! Relationship mappings correctly configured.The OneToOne and OneToMany relationships (metrics, accessTokens, devices, accessibleVaults) are correctly configured as inverse sides using
mappedBy, with appropriate fetch strategies and lifecycle options.Also applies to: 174-189
223-228: LGTM! User metrics fetched efficiently with JOIN FETCH.The
findAllWithMetrics()method correctly usesLEFT JOIN FETCHto eagerly load the metrics relationship, avoiding N+1 queries.
230-262: LGTM! Eager loading strategy correctly avoids Cartesian product explosion.The
findByIdWithEagerDetails()method correctly addresses the previous Cartesian product concern by:
- Fetching user with groups, devices, and legacy devices in a single query (reasonable for small collections)
- Separately fetching accessible vaults to prevent row explosion
The
findByIds()method appropriately uses batching to handle large ID collections efficiently.backend/src/test/java/org/cryptomator/hub/api/GroupsResourceIT.java (1)
91-115: LGTM! Well-structured test classes with proper admin security context.The
AsAdminandGroupCrudOperationsnested classes provide clean separation of concerns with appropriate security annotations. The mock reset in@BeforeEachensures test isolation.backend/src/main/java/org/cryptomator/hub/api/GroupDto.java (2)
36-38: Consider package-private visibility forwithDetailsmethod.The
withDetailsmethod has default (package-private) visibility, which is appropriate if it's only used within theapipackage. If it needs to be accessed from other packages, consider making itpublicexplicitly.
40-44: LGTM! Clean record definition with proper serialization annotations.The
WithDetailsrecord effectively combines group information with members and vaults. The@JsonUnwrappedannotation ensures the group properties are flattened into the parent object during serialization.backend/src/main/java/org/cryptomator/hub/api/GroupsResource.java (3)
155-164: Potential stale data return if Keycloak sync is asynchronous.After
keycloakAdminService.updateGroup()succeeds, the method queries the local database and returns the entity. If the sync from Keycloak to the local DB hasn't completed, the returnedGroupDtomight contain stale (pre-update) data, such as the old name.If the sync is known to be synchronous, this is fine. Otherwise, consider returning the data from the Keycloak response directly, or adding a brief sync trigger before querying.
122-144: LGTM! Efficient eager loading for group details.The use of
findByIdWithEagerDetailscombined with stream mapping for members and vaults provides efficient data retrieval without N+1 queries. The@NoCacheannotation ensures fresh data on each request.
105-120: The synchronous Keycloak-to-database sync assumption in lines 109-112 is correct. ThekeycloakAdminService.createGroup()method explicitly callssyncGroup(groupId)before returning, which is a@Transactionalmethod that persists the group to the local database before completion. The subsequentgroupRepo.findById()call will succeed immediately without any race condition.Likely an incorrect or invalid review comment.
| @ManyToMany | ||
| @JoinTable(name = "group_membership", | ||
| joinColumns = @JoinColumn(name = "member_id", referencedColumnName = "id"), | ||
| inverseJoinColumns = @JoinColumn(name = "group_id", referencedColumnName = "id") | ||
| ) | ||
| public Set<Group> directGroupMemberships = new HashSet<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "Group.java" -type fRepository: cryptomator/hub
Length of output: 122
🏁 Script executed:
# Search for the members field in Group class with its annotations
rg -A 5 "@ManyToMany" . -t java --search-path "Group.java" | head -40Repository: cryptomator/hub
Length of output: 502
🏁 Script executed:
# Find Group.java first
fd "Group.java" -t fRepository: cryptomator/hub
Length of output: 120
🏁 Script executed:
# Once found, look for the members field and its @ManyToMany annotation
rg -B 3 -A 5 "Set<.*User>.*members" . -t javaRepository: cryptomator/hub
Length of output: 41
🏁 Script executed:
cat -n backend/src/main/java/org/cryptomator/hub/entities/Group.java | grep -A 10 -B 3 "members"Repository: cryptomator/hub
Length of output: 1524
🏁 Script executed:
# Also check the full structure around ManyToMany annotations in Group
rg -B 5 -A 5 "@ManyToMany" backend/src/main/java/org/cryptomator/hub/entities/Group.javaRepository: cryptomator/hub
Length of output: 487
Fix bidirectional many-to-many relationship ownership violation.
Both User.directGroupMemberships and Group.members define @JoinTable on the same join table (group_membership). In JPA, only one side of a bidirectional many-to-many relationship should own the join table; the other side must use mappedBy.
Change Group.members to:
@ManyToMany(mappedBy = "directGroupMemberships", cascade = {CascadeType.MERGE, CascadeType.PERSIST})
private Set<Authority> members = new HashSet<>();Remove the @JoinTable annotation from the Group side to establish proper relationship ownership.
🤖 Prompt for AI Agents
In backend/src/main/java/org/cryptomator/hub/entities/User.java around lines
180-185: the bidirectional many-to-many uses the same @JoinTable on both sides;
remove the @JoinTable from the Group side and make Group.members the non-owning
side by adding mappedBy = "directGroupMemberships" and the desired cascade
(e.g., CascadeType.MERGE, CascadeType.PERSIST) so the
User.directGroupMemberships remains the owning side.
[ci skip]
Adds a complete user and group management interface for administrators. The user management includes list, detail, create and edit views with full CRUD operations. Users can be created, edited and deleted through the admin UI. The user detail page displays associated devices, legacy devices, groups and vault memberships. Group membership can be managed directly from the user detail view by adding or removing groups. The group management includes list and detail views with member management functionality.



