diff --git a/backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java b/backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java index fcd1943d4..3f302b54f 100644 --- a/backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java +++ b/backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java @@ -102,6 +102,21 @@ public List getSomeLegacy(@QueryParam("ids") List deviceIds) return legacyDeviceRepo.findAllInList(deviceIds).map(DeviceDto::fromEntity).toList(); } + /** + * @deprecated to be removed in #333 + */ + @Deprecated(since = "1.3.0", forRemoval = true) + @GET + @Path("/has-legacy-devices") + @RolesAllowed("admin") + @Produces(MediaType.APPLICATION_JSON) + @Transactional + @Operation(summary = "checks if any user has legacy devices") + @APIResponse(responseCode = "200") + public boolean hasAnyLegacyDevices() { + return legacyDeviceRepo.existsAny(); + } + @PUT @Path("/{deviceId}") @RolesAllowed("user") diff --git a/backend/src/main/java/org/cryptomator/hub/entities/LegacyDevice.java b/backend/src/main/java/org/cryptomator/hub/entities/LegacyDevice.java index 95d69f769..84b7dba01 100644 --- a/backend/src/main/java/org/cryptomator/hub/entities/LegacyDevice.java +++ b/backend/src/main/java/org/cryptomator/hub/entities/LegacyDevice.java @@ -98,6 +98,10 @@ public Stream findAllInList(List ids) { return find("#LegacyDevice.allInList", Parameters.with("ids", ids)).stream(); } + public boolean existsAny() { + return count() > 0; + } + public void deleteByOwner(String userId) { delete("#LegacyDevice.deleteByOwner", Parameters.with("userId", userId)); } diff --git a/frontend/src/common/backend.ts b/frontend/src/common/backend.ts index 8f93559a7..06adb08f0 100644 --- a/frontend/src/common/backend.ts +++ b/frontend/src/common/backend.ts @@ -414,6 +414,11 @@ class DeviceService { return axiosAuth.get(`/devices/legacy-devices?${query}`).then(response => response.data); } + /** @deprecated since version 1.3.0, to be removed in https://github.com/cryptomator/hub/issues/333 */ + public async hasLegacyDevices(): Promise { + return axiosAuth.get('/devices/has-legacy-devices').then(response => response.data); + } + public async removeDevice(deviceId: string): Promise> { return axiosAuth.delete(`/devices/${deviceId}`) .catch((error) => rethrowAndConvertIfExpected(error, 404)); diff --git a/frontend/src/components/AdminSettings.vue b/frontend/src/components/AdminSettings.vue index 10a42bb3d..f8716a2f5 100644 --- a/frontend/src/components/AdminSettings.vue +++ b/frontend/src/components/AdminSettings.vue @@ -16,6 +16,10 @@ + + {{ t('legacyDeviceBanner.admin.description') }} + +
@@ -239,6 +243,7 @@ import { FetchUpdateError, LatestVersionDto, updateChecker } from '../common/upd import { debounce } from '../common/util'; import FetchError from './FetchError.vue'; import AdminSettingsEmergencyAccess from './AdminSettingsEmergencyAccess.vue'; +import ContentBanner from './ContentBanner.vue'; const { t, d } = useI18n({ useScope: 'global' }); const props = defineProps<{ @@ -252,6 +257,7 @@ const form = ref(); const processing = ref(false); const onFetchError = ref(); const errorOnFetchingUpdates = ref(false); +const hasLegacyDevices = ref(false); onMounted(async () => { keycloakAdminRealmURL.value = `${cfg.value.keycloakUrl}/admin/${cfg.value.keycloakRealm}/console/`; @@ -268,6 +274,7 @@ async function fetchData() { billing.value = await backend.billing.get(); version.value = await versionDto; latestVersion.value = await versionAvailable; + hasLegacyDevices.value = await backend.devices.hasLegacyDevices(); const settings = await backend.settings.get(); wotMaxDepth.value = settings.wotMaxDepth; diff --git a/frontend/src/components/LegacyDeviceList.vue b/frontend/src/components/LegacyDeviceList.vue index 0cc34fa5a..b29cb8a04 100644 --- a/frontend/src/components/LegacyDeviceList.vue +++ b/frontend/src/components/LegacyDeviceList.vue @@ -9,6 +9,10 @@
+ + {{ t('legacyDeviceBanner.user.description') }} + +

{{ t('legacyDeviceList.title') }}

@@ -100,6 +104,7 @@ import { computed, onMounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import backend, { DeviceDto, NotFoundError, UserDto } from '../common/backend'; import userdata from '../common/userdata'; +import ContentBanner from './ContentBanner.vue'; import FetchError from './FetchError.vue'; const { t, d } = useI18n({ useScope: 'global' }); diff --git a/frontend/src/components/VaultList.vue b/frontend/src/components/VaultList.vue index 5649a30da..c156737b2 100644 --- a/frontend/src/components/VaultList.vue +++ b/frontend/src/components/VaultList.vue @@ -10,6 +10,14 @@ + + {{ t('legacyDeviceBanner.admin.description') }} + + + + {{ t('legacyDeviceBanner.user.description') }} + +

{{ t('vaultList.title') }}

@@ -171,6 +179,8 @@ const roleOfSelectedVault = computed(() => { const isAdmin = ref(false); const canCreateVaults = ref(false); +const hasLegacyDevices = ref(false); +const anyUserHasLegacyDevices = ref(false); const licenseStatus = ref(); const isLicenseViolated = computed(() => { if (licenseStatus.value) { @@ -206,12 +216,15 @@ async function fetchData() { try { me.value = await userdata.me; isAdmin.value = (await auth).hasRole('admin'); + const meWithLegacy = await userdata.meWithLegacyDevicesAndLastAccess; + hasLegacyDevices.value = (meWithLegacy.devices?.length ?? 0) > 0; canCreateVaults.value = (await auth).hasRole('create-vaults'); settings.value = await backend.settings.get(); if (isAdmin.value) { filterOptions.value['allVaults'] = t('vaultList.filter.entry.allVaults'); + anyUserHasLegacyDevices.value = await backend.devices.hasLegacyDevices(); } accessibleVaults.value = (await backend.vaults.listAccessible()).filter(v => !v.archived).sort((a, b) => a.name.localeCompare(b.name)); ownedVaults.value = (await backend.vaults.listAccessible('OWNER')).sort((a, b) => a.name.localeCompare(b.name)); diff --git a/frontend/src/i18n/en-US.json b/frontend/src/i18n/en-US.json index dea67374c..07088081b 100644 --- a/frontend/src/i18n/en-US.json +++ b/frontend/src/i18n/en-US.json @@ -408,6 +408,9 @@ "legacyDeviceList.ipAddress": "IP Address", "legacyDeviceList.lastAccess": "Last Vault Access", "legacyDeviceList.lastAccess.toolTip": "May be missing for devices accessed with older versions.", + "legacyDeviceBanner.title": "Legacy Devices Will Be Dropped", + "legacyDeviceBanner.user.description": "Your legacy devices will no longer be supported in the next major release. Please update Cryptomator on those devices or remove them.", + "legacyDeviceBanner.admin.description": "One or more users still have legacy devices. Legacy device support will be dropped in the next major release. Please ask affected users to update Cryptomator or remove their legacy devices.", "manageAccountKey.title": "Account Key",