diff --git a/backend/src/main/java/org/cryptomator/hub/api/UsersResource.java b/backend/src/main/java/org/cryptomator/hub/api/UsersResource.java index 84604ea2..96efb20d 100644 --- a/backend/src/main/java/org/cryptomator/hub/api/UsersResource.java +++ b/backend/src/main/java/org/cryptomator/hub/api/UsersResource.java @@ -7,12 +7,14 @@ import jakarta.validation.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.cryptomator.hub.entities.AccessToken; import org.cryptomator.hub.entities.Device; import org.cryptomator.hub.entities.User; import org.eclipse.microprofile.jwt.JsonWebToken; @@ -76,6 +78,24 @@ public UserDto getMe(@QueryParam("withDevices") boolean withDevices) { return new UserDto(user.id, user.name, user.pictureUrl, user.email, devices, user.publicKey, user.privateKey, user.setupCode); } + @POST + @Path("/me/reset") + @RolesAllowed("user") + @NoCache + @Transactional + @Operation(summary = "resets the user account") + @APIResponse(responseCode = "204", description = "deleted keys, devices and access permissions") + public Response resetMe() { + User user = User.findById(jwt.getSubject()); + user.publicKey = null; + user.privateKey = null; + user.setupCode = null; + user.persist(); + Device.deleteByOwner(user.id); + AccessToken.deleteByUser(user.id); + return Response.noContent().build(); + } + @GET @Path("/") @RolesAllowed("user") diff --git a/backend/src/main/java/org/cryptomator/hub/entities/AccessToken.java b/backend/src/main/java/org/cryptomator/hub/entities/AccessToken.java index 5628b5fb..daf432d5 100644 --- a/backend/src/main/java/org/cryptomator/hub/entities/AccessToken.java +++ b/backend/src/main/java/org/cryptomator/hub/entities/AccessToken.java @@ -1,6 +1,7 @@ package org.cryptomator.hub.entities; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.panache.common.Parameters; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; @@ -9,6 +10,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.MapsId; +import jakarta.persistence.NamedQuery; import jakarta.persistence.NoResultException; import jakarta.persistence.Table; @@ -17,6 +19,7 @@ import java.util.UUID; @Entity +@NamedQuery(name = "AccessToken.deleteByUser", query = "DELETE FROM AccessToken a WHERE a.id.userId = :userId") @Table(name = "access_token") public class AccessToken extends PanacheEntityBase { @@ -81,6 +84,11 @@ public static AccessToken unlock(UUID vaultId, String userId) { } } + + public static void deleteByUser(String userId) { + delete("#AccessToken.deleteByUser", Parameters.with("userId", userId)); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/backend/src/main/java/org/cryptomator/hub/entities/Device.java b/backend/src/main/java/org/cryptomator/hub/entities/Device.java index e0c20559..94e37a9e 100644 --- a/backend/src/main/java/org/cryptomator/hub/entities/Device.java +++ b/backend/src/main/java/org/cryptomator/hub/entities/Device.java @@ -24,6 +24,7 @@ @NamedQuery(name = "Device.findByIdAndOwner", query = "SELECT d FROM Device d WHERE d.id = :deviceId AND d.owner.id = :userId" ) +@NamedQuery(name = "Device.deleteByOwner", query = "DELETE FROM Device d WHERE d.owner.id = :userId") @NamedQuery(name = "Device.allInList", query = """ SELECT d @@ -99,4 +100,9 @@ public static Device findByIdAndUser(String deviceId, String userId) throws NoRe public static Stream findAllInList(List ids) { return find("#Device.allInList", Parameters.with("ids", ids)).stream(); } + + public static void deleteByOwner(String userId) { + delete("#Device.deleteByOwner", Parameters.with("userId", userId)); + } + } diff --git a/frontend/src/common/backend.ts b/frontend/src/common/backend.ts index fc5520cc..d7d4fd4a 100644 --- a/frontend/src/common/backend.ts +++ b/frontend/src/common/backend.ts @@ -297,6 +297,10 @@ class UserService { return axiosAuth.get(`/users/me?withDevices=${withDevices}`).then(response => UserDto.copy(response.data)); } + public async resetMe(): Promise { + return axiosAuth.post('/users/me/reset'); + } + public async listAll(): Promise { return axiosAuth.get('/users/').then(response => { return response.data.map(dto => UserDto.copy(dto)); diff --git a/frontend/src/common/crypto.ts b/frontend/src/common/crypto.ts index 62e11cd8..a513986a 100644 --- a/frontend/src/common/crypto.ts +++ b/frontend/src/common/crypto.ts @@ -411,6 +411,28 @@ export class BrowserKeys { }); } + /** + * Deletes the key pair for the given user. + * @returns a promise resolving on success + */ + public static async delete(userId: string): Promise { + const db = await new Promise((resolve, reject) => { + const req = indexedDB.open('hub'); + req.onsuccess = evt => { resolve(req.result); }; + req.onerror = evt => { reject(req.error); }; + req.onupgradeneeded = evt => { req.result.createObjectStore('keys'); }; + }); + return new Promise((resolve, reject) => { + const transaction = db.transaction('keys', 'readwrite'); + const keyStore = transaction.objectStore('keys'); + const query = keyStore.delete(userId); + query.onsuccess = evt => { resolve(); }; + query.onerror = evt => { reject(query.error); }; + }).finally(() => { + db.close(); + }); + } + /** * Stores the key pair in the browser's IndexedDB. See https://www.w3.org/TR/WebCryptoAPI/#concepts-key-storage * @returns a promise that will resolve if the key pair has been saved diff --git a/frontend/src/components/ResetUserAccountDialog.vue b/frontend/src/components/ResetUserAccountDialog.vue index 20500adf..562fdb84 100644 --- a/frontend/src/components/ResetUserAccountDialog.vue +++ b/frontend/src/components/ResetUserAccountDialog.vue @@ -52,6 +52,9 @@ import { Dialog, DialogOverlay, DialogPanel, DialogTitle, TransitionChild, Trans import { ExclamationTriangleIcon } from '@heroicons/vue/24/outline'; import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; +import backend, { UserDto } from '../common/backend'; +import { BrowserKeys } from '../common/crypto'; +import router from '../router'; const { t } = useI18n({ useScope: 'global' }); @@ -69,6 +72,10 @@ defineExpose({ show }); +const props = defineProps<{ + me : UserDto +}>(); + async function show() { open.value = true; } @@ -77,7 +84,9 @@ async function resetUserAccount() { onResetError.value = null; try { processing.value = true; - // TODO + await backend.users.resetMe(); + await BrowserKeys.delete(props.me.id); + router.go(0); // reload current page } catch (error) { console.error('Resetting user account failed.', error); onResetError.value = error instanceof Error ? error : new Error('Unknown Error'); diff --git a/frontend/src/components/SetupUserKey.vue b/frontend/src/components/SetupUserKey.vue index 28040862..2e7253e1 100644 --- a/frontend/src/components/SetupUserKey.vue +++ b/frontend/src/components/SetupUserKey.vue @@ -145,7 +145,7 @@ - +