diff --git a/backend/src/main/java/io/papermc/hangar/components/auth/controller/CredentialController.java b/backend/src/main/java/io/papermc/hangar/components/auth/controller/CredentialController.java index 1e09309d6f..cc4ea46652 100644 --- a/backend/src/main/java/io/papermc/hangar/components/auth/controller/CredentialController.java +++ b/backend/src/main/java/io/papermc/hangar/components/auth/controller/CredentialController.java @@ -187,6 +187,15 @@ public void unregisterWebauthnDevice(@RequestBody final String id) { this.credentialsService.checkRemoveBackupCodes(); } + @Privileged + @RequireAal(1) + @PostMapping(value = "/webauthn/rename", consumes = MediaType.APPLICATION_JSON_VALUE) + public void renameWebauthn(@RequestBody final RenameRequest renameRequest) { + this.webAuthNService.renameDevice(this.getHangarPrincipal().getUserId(), renameRequest.id(), renameRequest.displayName()); + } + + public record RenameRequest(String id, String displayName) {} + /* * TOTP */ diff --git a/backend/src/main/java/io/papermc/hangar/components/auth/service/WebAuthNService.java b/backend/src/main/java/io/papermc/hangar/components/auth/service/WebAuthNService.java index 088281bd41..ebecf723e9 100644 --- a/backend/src/main/java/io/papermc/hangar/components/auth/service/WebAuthNService.java +++ b/backend/src/main/java/io/papermc/hangar/components/auth/service/WebAuthNService.java @@ -129,6 +129,18 @@ public void updateCredential(final long userId, final String credentialId, final } + public void renameDevice(final long userId, final String credentialId, final String newName) { + final WebAuthNCredential webAuthNCredential = this.getWebAuthNCredential(userId); + final var any = webAuthNCredential.credentials().stream().filter(c -> c.id().equals(credentialId)).findAny(); + if (any.isPresent()) { + final WebAuthNCredential.WebAuthNDevice oldDevice = any.get(); + final WebAuthNCredential.WebAuthNDevice patchedDevice = new WebAuthNCredential.WebAuthNDevice(oldDevice.id(), oldDevice.addedAt(), oldDevice.publicKey(), newName, oldDevice.authenticator(), oldDevice.isPasswordLess(), oldDevice.attestationType()); + webAuthNCredential.credentials().remove(oldDevice); + webAuthNCredential.credentials().add(patchedDevice); + this.credentialsService.updateCredential(userId, webAuthNCredential); + } + } + private WebAuthNCredential getWebAuthNCredential(final long userId) { final UserCredentialTable credential = this.credentialsService.getCredential(userId, CredentialType.WEBAUTHN); if (credential == null) { diff --git a/frontend/src/pages/auth/settings/security.vue b/frontend/src/pages/auth/settings/security.vue index 584c012e09..8964ca3bdd 100644 --- a/frontend/src/pages/auth/settings/security.vue +++ b/frontend/src/pages/auth/settings/security.vue @@ -16,7 +16,7 @@ import PageTitle from "~/components/design/PageTitle.vue"; import Modal from "~/components/modals/Modal.vue"; import PrettyTime from "~/components/design/PrettyTime.vue"; -const props = defineProps<{ +defineProps<{ settings?: AuthSettings; }>(); const emit = defineEmits<{ @@ -81,6 +81,40 @@ async function unregisterAuthenticator(authenticator: AuthSettings["authenticato loading.value = false; } +const newAuthenticatorName = ref(); +const currentlyRenamingAuthenticator = ref(); +const authenticatorRenameModal = ref(); + +function renameAuthenticatorModal(authenticator: AuthSettings["authenticators"][0]) { + newAuthenticatorName.value = authenticator.displayName; + currentlyRenamingAuthenticator.value = authenticator; + authenticatorRenameModal.value.isOpen = true; + v.value.$reset(); +} + +async function renameAuthenticator() { + if (!(await v.value.$validate())) return; + if (!currentlyRenamingAuthenticator.value) { + notification.error("Something went wrong, please try again"); + return; + } + loading.value = true; + try { + await useInternalApi("auth/webauthn/rename", "POST", { id: currentlyRenamingAuthenticator.value.id, displayName: newAuthenticatorName.value }); + authenticatorRenameModal.value.isOpen = false; + emit("refreshSettings"); + } catch (e) { + if (e.response?.data?.message === "error.privileged") { + await router.push(useAuth.loginUrl(route.path) + "&privileged=true"); + } else if (e?.toString()?.startsWith("NotAllowedError")) { + notification.error("Security Key Authentication failed!"); + } else { + notification.fromError(i18n, e); + } + } + loading.value = false; +} + const totpData = ref<{ secret: string; qrCode: string } | undefined>(); async function setupTotp() { @@ -237,10 +271,22 @@ async function generateNewCodes() {
  • {{ authenticator.displayName }} (added at ) +
  • + + + +
    - +