Skip to content

Commit 1ae4310

Browse files
SailRealiammajid
andcommitted
Move device last access handling into user resource
Co-authored-by: iammajid <[email protected]>
1 parent 65846b3 commit 1ae4310

File tree

6 files changed

+40
-30
lines changed

6 files changed

+40
-30
lines changed

backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,12 @@ public record DeviceDto(@JsonProperty("id") @ValidId String id,
174174
@JsonProperty("publicKey") @NotNull @OnlyBase64Chars String publicKey,
175175
@JsonProperty("userPrivateKey") @NotNull @ValidJWE String userPrivateKeys, // singular name for history reasons (don't break client compatibility)
176176
@JsonProperty("owner") @ValidId String ownerId,
177-
@JsonProperty("creationTime") Instant creationTime) {
177+
@JsonProperty("creationTime") Instant creationTime,
178+
@JsonProperty("lastIpAddress") String lastIpAddress,
179+
@JsonProperty("lastAccessTime") Instant lastAccessTime) {
178180

179181
public static DeviceDto fromEntity(Device entity) {
180-
return new DeviceDto(entity.getId(), entity.getName(), entity.getType(), entity.getPublickey(), entity.getUserPrivateKeys(), entity.getOwner().getId(), entity.getCreationTime().truncatedTo(ChronoUnit.MILLIS));
182+
return new DeviceDto(entity.getId(), entity.getName(), entity.getType(), entity.getPublickey(), entity.getUserPrivateKeys(), entity.getOwner().getId(), entity.getCreationTime().truncatedTo(ChronoUnit.MILLIS), null, null);
181183
}
182184

183185
}

backend/src/main/java/org/cryptomator/hub/api/UsersResource.java

+20-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import org.cryptomator.hub.entities.User;
2525
import org.cryptomator.hub.entities.Vault;
2626
import org.cryptomator.hub.entities.WotEntry;
27+
import org.cryptomator.hub.entities.events.AuditEvent;
2728
import org.cryptomator.hub.entities.events.EventLogger;
29+
import org.cryptomator.hub.entities.events.VaultKeyRetrievedEvent;
2830
import org.eclipse.microprofile.jwt.JsonWebToken;
2931
import org.eclipse.microprofile.openapi.annotations.Operation;
3032
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
@@ -58,6 +60,8 @@ public class UsersResource {
5860
WotEntry.Repository wotRepo;
5961
@Inject
6062
EffectiveWot.Repository effectiveWotRepo;
63+
@Inject
64+
AuditEvent.Repository auditEventRepo;
6165

6266
@Inject
6367
JsonWebToken jwt;
@@ -156,10 +160,23 @@ public Response updateMyAccessTokens(@NotNull Map<UUID, String> tokens) {
156160
@Operation(summary = "get the logged-in user")
157161
@APIResponse(responseCode = "200", description = "returns the current user")
158162
@APIResponse(responseCode = "404", description = "no user matching the subject of the JWT passed as Bearer Token")
159-
public UserDto getMe(@QueryParam("withDevices") boolean withDevices) {
163+
public UserDto getMe(@QueryParam("withDevices") boolean withDevices, @QueryParam("withLastAccess") boolean withLastAccess) {
160164
User user = userRepo.findById(jwt.getSubject());
161-
Function<Device, DeviceResource.DeviceDto> mapDevices = d -> new DeviceResource.DeviceDto(d.getId(), d.getName(), d.getType(), d.getPublickey(), d.getUserPrivateKeys(), d.getOwner().getId(), d.getCreationTime().truncatedTo(ChronoUnit.MILLIS));
162-
var devices = withDevices ? user.devices.stream().map(mapDevices).collect(Collectors.toSet()) : Set.<DeviceResource.DeviceDto>of();
165+
Set<DeviceResource.DeviceDto> devices;
166+
if (withLastAccess) {
167+
var deviceEntities = user.devices.stream().toList();
168+
var deviceIds = deviceEntities.stream().map(Device::getId).toList();
169+
var events = auditEventRepo.findLastVaultKeyRetrieve(deviceIds).collect(Collectors.toMap(VaultKeyRetrievedEvent::getDeviceId, Function.identity()));
170+
devices = deviceEntities.stream().map(d -> {
171+
var event = events.get(d.getId());
172+
var lastIpAddress = (event != null) ? event.getIpAddress() : null;
173+
var lastAccessTime = (event != null) ? event.getTimestamp() : null;
174+
return new DeviceResource.DeviceDto(d.getId(), d.getName(), d.getType(), d.getPublickey(), d.getUserPrivateKeys(), d.getOwner().getId(), d.getCreationTime().truncatedTo(ChronoUnit.MILLIS), lastIpAddress, lastAccessTime);
175+
}).collect(Collectors.toSet());
176+
} else {
177+
Function<Device, DeviceResource.DeviceDto> mapDevices = d -> new DeviceResource.DeviceDto(d.getId(), d.getName(), d.getType(), d.getPublickey(), d.getUserPrivateKeys(), d.getOwner().getId(), d.getCreationTime().truncatedTo(ChronoUnit.MILLIS), null, null);
178+
devices = withDevices ? user.devices.stream().map(mapDevices).collect(Collectors.toSet()) : Set.of();
179+
}
163180
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getLanguage(), devices, user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getSetupCode());
164181
}
165182

frontend/src/common/auditlog.ts

-10
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,6 @@ class AuditLogService {
189189
}))
190190
.catch((error) => rethrowAndConvertIfExpected(error, 402));
191191
}
192-
193-
public async lastVaultKeyRetrieveEvents(deviceIds: string[]): Promise<AuditEventVaultKeyRetrieveDto[]> {
194-
const query = `deviceIds=${deviceIds.join('&deviceIds=')}`;
195-
return axiosAuth.get<AuditEventVaultKeyRetrieveDto[]>(`/auditlog/last-vault-key-retrieve?${query}`)
196-
.then(response => response.data.map(dto => {
197-
dto.timestamp = new Date(dto.timestamp);
198-
return dto;
199-
}))
200-
.catch((error) => rethrowAndConvertIfExpected(error, 402));
201-
}
202192
}
203193

204194
/* Export */

frontend/src/common/backend.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export type DeviceDto = {
5555
publicKey: string;
5656
userPrivateKey: string;
5757
creationTime: Date;
58+
lastIpAddress?: string;
59+
lastAccessTime?: Date;
5860
};
5961

6062
export type VaultRole = 'MEMBER' | 'OWNER';
@@ -248,8 +250,8 @@ class UserService {
248250
return axiosAuth.put('/users/me', dto);
249251
}
250252

251-
public async me(withDevices: boolean = false): Promise<UserDto> {
252-
return axiosAuth.get<UserDto>(`/users/me?withDevices=${withDevices}`).then(response => AuthorityService.fillInMissingPicture(response.data));
253+
public async me(withDevices: boolean = false, withLastAccess: boolean = false): Promise<UserDto> {
254+
return axiosAuth.get<UserDto>(`/users/me?withDevices=${withDevices}&withLastAccess=${withLastAccess}`).then(response => AuthorityService.fillInMissingPicture(response.data));
253255
}
254256

255257
public async resetMe(): Promise<void> {

frontend/src/common/userdata.ts

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { JWEParser } from './jwe';
55

66
class UserData {
77
#me?: Promise<UserDto>;
8+
#meWithLastAccess?: Promise<UserDto>;
89
#browserKeys?: Promise<BrowserKeys | undefined>;
910

1011
/**
@@ -17,6 +18,14 @@ class UserData {
1718
return this.#me;
1819
}
1920

21+
public get meWithLastAccess(): Promise<UserDto> {
22+
if (!this.#meWithLastAccess) {
23+
this.#meWithLastAccess = backend.users.me(true, true);
24+
this.#me = this.#meWithLastAccess;
25+
}
26+
return this.#meWithLastAccess;
27+
}
28+
2029
/**
2130
* Gets the device key pair stored for this user in the currently used browser.
2231
*/

frontend/src/components/DeviceList.vue

+3-13
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@
7777
{{ d(device.creationTime, 'short') }}
7878
</td>
7979
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
80-
<div v-if="lastVaultKeyRetrieveEvents.has(device.id)">{{ d(lastVaultKeyRetrieveEvents.get(device.id)!.timestamp, 'short') }}</div>
80+
<div v-if="device.lastAccessTime">{{ d(device.lastAccessTime, 'short') }}</div>
8181
</td>
8282
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
83-
<div v-if="lastVaultKeyRetrieveEvents.has(device.id) && lastVaultKeyRetrieveEvents.get(device.id)?.ipAddress">{{ lastVaultKeyRetrieveEvents.get(device.id)!.ipAddress! }}</div>
83+
<div v-if="device.lastIpAddress">{{ device.lastIpAddress }}</div>
8484
</td>
8585
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
8686
<a v-if="device.id != myDevice?.id" tabindex="0" class="text-red-600 hover:text-red-900" @click="removeDevice(device)">{{ t('common.remove') }}</a>
@@ -106,7 +106,6 @@
106106
import { ComputerDesktopIcon, DevicePhoneMobileIcon, QuestionMarkCircleIcon, WindowIcon } from '@heroicons/vue/24/solid';
107107
import { onMounted, ref } from 'vue';
108108
import { useI18n } from 'vue-i18n';
109-
import auditlog, { AuditEventVaultKeyRetrieveDto } from '../common/auditlog';
110109
import backend, { DeviceDto, NotFoundError, UserDto } from '../common/backend';
111110
import userdata from '../common/userdata';
112111
import FetchError from './FetchError.vue';
@@ -118,24 +117,15 @@ const myDevice = ref<DeviceDto>();
118117
const onFetchError = ref<Error | null>();
119118
const onRemoveDeviceError = ref< {[id: string]: Error} >({});
120119
121-
const lastVaultKeyRetrieveEvents = ref<Map<string, AuditEventVaultKeyRetrieveDto>>(new Map());
122-
123120
onMounted(async () => {
124121
await fetchData();
125122
});
126123
127124
async function fetchData() {
128125
onFetchError.value = null;
129126
try {
130-
me.value = await userdata.me;
127+
me.value = await userdata.meWithLastAccess;
131128
myDevice.value = await userdata.browser;
132-
const deviceIds = me.value.devices.map(d => d.id);
133-
const events = await auditlog.service.lastVaultKeyRetrieveEvents(deviceIds);
134-
events.forEach(e => {
135-
if (e.deviceId != undefined) {
136-
lastVaultKeyRetrieveEvents.value.set(e.deviceId, e);
137-
}
138-
});
139129
} catch (error) {
140130
console.error('Retrieving device list failed.', error);
141131
onFetchError.value = error instanceof Error ? error : new Error('Unknown Error');

0 commit comments

Comments
 (0)