mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-15 15:21:14 +00:00
add webauthn (passkey) support
This commit is contained in:
@@ -140,6 +140,7 @@ const currentYear = ref(new Date().getFullYear())
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer></template>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
|
@@ -29,7 +29,8 @@
|
||||
"label": "Passwort",
|
||||
"placeholder": "Bitte geben Sie Ihr Passwort ein"
|
||||
},
|
||||
"button": "Anmelden"
|
||||
"button": "Anmelden",
|
||||
"button-webauthn": "Passkey verwenden"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
@@ -188,6 +189,35 @@
|
||||
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
||||
"button-enable-text": "API aktivieren",
|
||||
"api-link": "API Dokumentation"
|
||||
},
|
||||
"webauthn": {
|
||||
"headline": "Passkey-Einstellungen",
|
||||
"abstract": "Passkeys sind eine moderne Möglichkeit, Benutzer ohne Passwort zu authentifizieren. Sie werden sicher in Ihrem Browser gespeichert und können verwendet werden, um sich im WireGuard-Portal anzumelden.",
|
||||
"active-description": "Mindestens ein Passkey ist derzeit für Ihr Benutzerkonto aktiv.",
|
||||
"inactive-description": "Für Ihr Benutzerkonto sind derzeit keine Passkeys registriert. Drücken Sie die Schaltfläche unten, um einen neuen Passkey zu registrieren.",
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"created": "Erstellt",
|
||||
"actions": ""
|
||||
},
|
||||
"credentials-list": "Derzeit registrierte Passkeys",
|
||||
"modal-delete": {
|
||||
"headline": "Passkey löschen",
|
||||
"abstract": "Sind Sie sicher, dass Sie diesen Passkey löschen möchten? Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
|
||||
"created": "Erstellt:",
|
||||
"button-delete": "Löschen",
|
||||
"button-cancel": "Abbrechen"
|
||||
},
|
||||
"button-rename-title": "Umbenennen",
|
||||
"button-rename-text": "Passkey umbenennen.",
|
||||
"button-save-title": "Speichern",
|
||||
"button-save-text": "Neuen Namen des Passkeys speichern.",
|
||||
"button-cancel-title": "Abbrechen",
|
||||
"button-cancel-text": "Umbenennung des Passkeys abbrechen.",
|
||||
"button-delete-title": "Löschen",
|
||||
"button-delete-text": "Passkey löschen. Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
|
||||
"button-register-title": "Passkey registrieren",
|
||||
"button-register-text": "Einen neuen Passkey registrieren, um Ihr Konto zu sichern."
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
|
@@ -29,7 +29,8 @@
|
||||
"label": "Password",
|
||||
"placeholder": "Please enter your password"
|
||||
},
|
||||
"button": "Sign in"
|
||||
"button": "Sign in",
|
||||
"button-webauthn": "Use Passkey"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
@@ -188,6 +189,35 @@
|
||||
"button-enable-title": "Enable API, this will generate a new token.",
|
||||
"button-enable-text": "Enable API",
|
||||
"api-link": "API Documentation"
|
||||
},
|
||||
"webauthn": {
|
||||
"headline": "Passkey Settings",
|
||||
"abstract": "Passkeys are a modern way to authenticate users without the need for passwords. They are stored securely in your browser and can be used to log in to the WireGuard Portal.",
|
||||
"active-description": "At least one passkey is currently active for your user account.",
|
||||
"inactive-description": "No passkeys are currently registered for your user account. Press the button below to register a new passkey.",
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"created": "Created",
|
||||
"actions": ""
|
||||
},
|
||||
"credentials-list": "Currently registered Passkeys",
|
||||
"modal-delete": {
|
||||
"headline": "Delete Passkey",
|
||||
"abstract": "Are you sure you want to delete this passkey? You will not be able to log in with this passkey anymore.",
|
||||
"created": "Created:",
|
||||
"button-delete": "Delete",
|
||||
"button-cancel": "Cancel"
|
||||
},
|
||||
"button-rename-title": "Rename",
|
||||
"button-rename-text": "Rename the passkey.",
|
||||
"button-save-title": "Save",
|
||||
"button-save-text": "Save the new name of the passkey.",
|
||||
"button-cancel-title": "Cancel",
|
||||
"button-cancel-text": "Cancel the renaming of the passkey.",
|
||||
"button-delete-title": "Delete",
|
||||
"button-delete-text": "Delete the passkey. You will not be able to log in with this passkey anymore.",
|
||||
"button-register-title": "Register Passkey",
|
||||
"button-register-text": "Register a new Passkey to secure your account."
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
|
@@ -3,13 +3,17 @@ import { defineStore } from 'pinia'
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||
import router from '../router'
|
||||
import { browserSupportsWebAuthn,startRegistration,startAuthentication } from '@simplewebauthn/browser';
|
||||
import {base64_url_encode} from "@/helpers/encoding";
|
||||
|
||||
export const authStore = defineStore('auth',{
|
||||
state: () => ({
|
||||
// initialize state from local storage to enable user to stay logged in
|
||||
user: JSON.parse(localStorage.getItem('user')),
|
||||
providers: [],
|
||||
returnUrl: localStorage.getItem('returnUrl')
|
||||
returnUrl: localStorage.getItem('returnUrl'),
|
||||
webAuthnCredentials: [],
|
||||
fetching: false,
|
||||
}),
|
||||
getters: {
|
||||
UserIdentifier: (state) => state.user?.Identifier || 'unknown',
|
||||
@@ -18,6 +22,14 @@ export const authStore = defineStore('auth',{
|
||||
IsAuthenticated: (state) => state.user != null,
|
||||
IsAdmin: (state) => state.user?.IsAdmin || false,
|
||||
ReturnUrl: (state) => state.returnUrl || '/',
|
||||
IsWebAuthnEnabled: (state) => {
|
||||
if (state.webAuthnCredentials) {
|
||||
return state.webAuthnCredentials.length > 0
|
||||
}
|
||||
return false
|
||||
},
|
||||
WebAuthnCredentials: (state) => state.webAuthnCredentials || [],
|
||||
isFetching: (state) => state.fetching,
|
||||
},
|
||||
actions: {
|
||||
SetReturnUrl(link) {
|
||||
@@ -60,6 +72,23 @@ export const authStore = defineStore('auth',{
|
||||
return Promise.reject(err)
|
||||
})
|
||||
},
|
||||
// LoadWebAuthnCredentials returns promise that might have been rejected if the session was not authenticated.
|
||||
async LoadWebAuthnCredentials() {
|
||||
this.fetching = true
|
||||
return apiWrapper.get(`/auth/webauthn/credentials`)
|
||||
.then(credentials => {
|
||||
this.setWebAuthnCredentials(credentials)
|
||||
})
|
||||
.catch(error => {
|
||||
this.setWebAuthnCredentials([])
|
||||
console.log("Failed to load webauthn credentials:", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: error,
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
},
|
||||
// Login returns promise that might have been rejected if the login attempt was not successful.
|
||||
async Login(username, password) {
|
||||
return apiWrapper.post(`/auth/login`, { username, password })
|
||||
@@ -93,6 +122,157 @@ export const authStore = defineStore('auth',{
|
||||
|
||||
await router.push('/login')
|
||||
},
|
||||
async RegisterWebAuthn() {
|
||||
// check if the browser supports WebAuthn
|
||||
if (!browserSupportsWebAuthn()) {
|
||||
console.error("WebAuthn is not supported by this browser.");
|
||||
notify({
|
||||
title: "WebAuthn not supported",
|
||||
text: "This browser does not support WebAuthn.",
|
||||
type: 'error'
|
||||
});
|
||||
return Promise.reject(new Error("WebAuthn not supported"));
|
||||
}
|
||||
|
||||
this.fetching = true
|
||||
console.log("Starting WebAuthn registration...")
|
||||
await apiWrapper.post(`/auth/webauthn/register/start`, {})
|
||||
.then(optionsJSON => {
|
||||
notify({
|
||||
title: "Passkey registration",
|
||||
text: "Starting passkey registration, follow the instructions in the browser."
|
||||
});
|
||||
console.log("Started WebAuthn registration with options: ", optionsJSON)
|
||||
|
||||
return startRegistration({ optionsJSON: optionsJSON.publicKey }).then(attResp => {
|
||||
console.log("Finishing WebAuthn registration...")
|
||||
return apiWrapper.post(`/auth/webauthn/register/finish`, attResp)
|
||||
.then(credentials => {
|
||||
console.log("Passkey registration finished successfully: ", credentials)
|
||||
this.setWebAuthnCredentials(credentials)
|
||||
notify({
|
||||
title: "Passkey registration",
|
||||
text: "A new passkey has been registered successfully!",
|
||||
type: 'success'
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.fetching = false
|
||||
console.error("Failed to register passkey:", err);
|
||||
notify({
|
||||
title: "Passkey registration failed",
|
||||
text: err,
|
||||
type: 'error'
|
||||
});
|
||||
})
|
||||
}).catch(err => {
|
||||
this.fetching = false
|
||||
console.error("Failed to start WebAuthn registration:", err);
|
||||
notify({
|
||||
title: "Failed to start Passkey registration",
|
||||
text: err,
|
||||
type: 'error'
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.fetching = false
|
||||
console.error("Failed to start WebAuthn registration:", err);
|
||||
notify({
|
||||
title: "Failed to start WebAuthn registration",
|
||||
text: err,
|
||||
type: 'error'
|
||||
});
|
||||
})
|
||||
},
|
||||
async DeleteWebAuthnCredential(credentialId) {
|
||||
this.fetching = true
|
||||
return apiWrapper.delete(`/auth/webauthn/credential/${base64_url_encode(credentialId)}`)
|
||||
.then(credentials => {
|
||||
this.setWebAuthnCredentials(credentials)
|
||||
notify({
|
||||
title: "Success",
|
||||
text: "Passkey deleted successfully!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.fetching = false
|
||||
console.error("Failed to delete webauthn credential:", err);
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: err,
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
},
|
||||
async RenameWebAuthnCredential(credential) {
|
||||
this.fetching = true
|
||||
return apiWrapper.put(`/auth/webauthn/credential/${base64_url_encode(credential.ID)}`, {
|
||||
Name: credential.Name,
|
||||
})
|
||||
.then(credentials => {
|
||||
this.setWebAuthnCredentials(credentials)
|
||||
notify({
|
||||
title: "Success",
|
||||
text: "Passkey renamed successfully!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.fetching = false
|
||||
console.error("Failed to rename webauthn credential", credential.ID, ":", err);
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: err,
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoginWebAuthn() {
|
||||
// check if the browser supports WebAuthn
|
||||
if (!browserSupportsWebAuthn()) {
|
||||
console.error("WebAuthn is not supported by this browser.");
|
||||
notify({
|
||||
title: "WebAuthn not supported",
|
||||
text: "This browser does not support WebAuthn.",
|
||||
type: 'error'
|
||||
});
|
||||
return Promise.reject(new Error("WebAuthn not supported"));
|
||||
}
|
||||
|
||||
this.fetching = true
|
||||
console.log("Starting WebAuthn login...")
|
||||
await apiWrapper.post(`/auth/webauthn/login/start`, {})
|
||||
.then(optionsJSON => {
|
||||
console.log("Started WebAuthn login with options: ", optionsJSON)
|
||||
|
||||
return startAuthentication({ optionsJSON: optionsJSON.publicKey }).then(asseResp => {
|
||||
console.log("Finishing WebAuthn login ...")
|
||||
return apiWrapper.post(`/auth/webauthn/login/finish`, asseResp)
|
||||
.then(user => {
|
||||
console.log("Passkey login finished successfully for user:", user.Identifier)
|
||||
this.ResetReturnUrl()
|
||||
this.setUserInfo(user)
|
||||
return user.Identifier
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to login with passkey:", err)
|
||||
this.setUserInfo(null)
|
||||
return Promise.reject(new Error("login failed"))
|
||||
})
|
||||
}).catch(err => {
|
||||
console.error("Failed to finish passkey login:", err)
|
||||
this.setUserInfo(null)
|
||||
return Promise.reject(new Error("login failed"))
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to start passkey login:", err)
|
||||
this.setUserInfo(null)
|
||||
return Promise.reject(new Error("login failed"))
|
||||
})
|
||||
},
|
||||
// -- internal setters
|
||||
setUserInfo(userInfo) {
|
||||
// store user details and jwt in local storage to keep user logged in between page refreshes
|
||||
@@ -120,5 +300,9 @@ export const authStore = defineStore('auth',{
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
},
|
||||
setWebAuthnCredentials(credentials) {
|
||||
this.fetching = false
|
||||
this.webAuthnCredentials = credentials
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -129,7 +129,7 @@ export const profileStore = defineStore('profile', {
|
||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
||||
.then(this.setUser)
|
||||
.catch(error => {
|
||||
this.setPeers([])
|
||||
this.fetching = false
|
||||
console.log("Failed to activate API for ", currentUser, ": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
@@ -143,7 +143,7 @@ export const profileStore = defineStore('profile', {
|
||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
||||
.then(this.setUser)
|
||||
.catch(error => {
|
||||
this.setPeers([])
|
||||
this.fetching = false
|
||||
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import {computed, ref} from "vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {authStore} from "@/stores/auth";
|
||||
import router from '../router/index.js'
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
@@ -17,6 +17,11 @@ const usernameInvalid = computed(() => username.value === "")
|
||||
const passwordInvalid = computed(() => password.value === "")
|
||||
const disableLoginBtn = computed(() => username.value === "" || password.value === "" || loggingIn.value)
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await settings.LoadSettings()
|
||||
})
|
||||
|
||||
const login = async function () {
|
||||
console.log("Performing login for user:", username.value);
|
||||
loggingIn.value = true;
|
||||
@@ -28,7 +33,34 @@ const login = async function () {
|
||||
type: 'success',
|
||||
});
|
||||
loggingIn.value = false;
|
||||
settings.LoadSettings(); // only logs errors, does not throw
|
||||
settings.LoadSettings(); // reload full settings
|
||||
router.push(auth.ReturnUrl);
|
||||
})
|
||||
.catch(error => {
|
||||
notify({
|
||||
title: "Login failed!",
|
||||
text: "Authentication failed!",
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
//loggingIn.value = false;
|
||||
// delay the user from logging in for a short amount of time
|
||||
setTimeout(() => loggingIn.value = false, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const loginWebAuthn = async function () {
|
||||
console.log("Performing webauthn login");
|
||||
loggingIn.value = true;
|
||||
auth.LoginWebAuthn()
|
||||
.then(uid => {
|
||||
notify({
|
||||
title: "Logged in",
|
||||
text: "Authentication succeeded!",
|
||||
type: 'success',
|
||||
});
|
||||
loggingIn.value = false;
|
||||
settings.LoadSettings(); // reload full settings
|
||||
router.push(auth.ReturnUrl);
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -85,17 +117,25 @@ const externalLogin = function (provider) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5 d-flex">
|
||||
<div :class="{'col-lg-4':auth.LoginProviders.length < 3, 'col-lg-12':auth.LoginProviders.length >= 3}" class="d-flex mb-2">
|
||||
<button :disabled="disableLoginBtn" class="btn btn-primary flex-fill" type="submit" @click.prevent="login">
|
||||
<div class="row mt-5 mb-2">
|
||||
<div class="col-lg-4">
|
||||
<button :disabled="disableLoginBtn" class="btn btn-primary" type="submit" @click.prevent="login">
|
||||
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="{'col-lg-8':auth.LoginProviders.length < 3, 'col-lg-12':auth.LoginProviders.length >= 3}" class="d-flex mb-2">
|
||||
<div class="col-lg-8 mb-2 text-end">
|
||||
<button v-if="settings.Setting('WebAuthnEnabled')" class="btn btn-primary" type="submit" @click.prevent="loginWebAuthn">
|
||||
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5 d-flex">
|
||||
<div class="col-lg-12 d-flex mb-2">
|
||||
<!-- OpenIdConnect / OAUTH providers -->
|
||||
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
||||
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
|
||||
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
||||
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
import {onMounted, ref} from "vue";
|
||||
import { profileStore } from "@/stores/profile";
|
||||
import { settingsStore } from "@/stores/settings";
|
||||
import { authStore } from "../stores/auth";
|
||||
@@ -10,8 +10,30 @@ const auth = authStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await profile.LoadUser()
|
||||
await auth.LoadWebAuthnCredentials()
|
||||
})
|
||||
|
||||
const selectedCredential = ref({})
|
||||
|
||||
function enableRename(credential) {
|
||||
credential.renameMode = true;
|
||||
credential.tempName = credential.Name; // Store the original name
|
||||
}
|
||||
|
||||
function cancelRename(credential) {
|
||||
credential.renameMode = false;
|
||||
credential.tempName = null; // Discard changes
|
||||
}
|
||||
|
||||
async function saveRename(credential) {
|
||||
try {
|
||||
await auth.RenameWebAuthnCredential({ ...credential, Name: credential.tempName });
|
||||
credential.Name = credential.tempName; // Update the name
|
||||
credential.renameMode = false;
|
||||
} catch (error) {
|
||||
console.error("Failed to rename credential:", error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -69,4 +91,86 @@ onMounted(async () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-light p-5 mt-5" v-if="settings.Setting('WebAuthnEnabled')">
|
||||
<h2 class="display-7">{{ $t('settings.webauthn.headline') }}</h2>
|
||||
<p class="lead">{{ $t('settings.webauthn.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p v-if="auth.IsWebAuthnEnabled">{{ $t('settings.webauthn.active-description') }}</p>
|
||||
<p v-else>{{ $t('settings.webauthn.inactive-description') }}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<button class="input-group-text btn btn-primary" :title="$t('settings.webauthn.button-register-text')" @click.prevent="auth.RegisterWebAuthn" :disabled="auth.isFetching">
|
||||
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.webauthn.button-register-title') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="auth.WebAuthnCredentials.length > 0" class="mt-4">
|
||||
<h3>{{ $t('settings.webauthn.credentials-list') }}</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%">{{ $t('settings.webauthn.table.name') }}</th>
|
||||
<th style="width: 20%">{{ $t('settings.webauthn.table.created') }}</th>
|
||||
<th style="width: 30%">{{ $t('settings.webauthn.table.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="credential in auth.webAuthnCredentials" :key="credential.ID">
|
||||
<td class="align-middle">
|
||||
<div v-if="credential.renameMode">
|
||||
<input v-model="credential.tempName" class="form-control" type="text" />
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ credential.Name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{ credential.CreatedAt }}
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
<div v-if="credential.renameMode">
|
||||
<button class="btn btn-success me-1" :title="$t('settings.webauthn.button-save-text')" @click.prevent="saveRename(credential)" :disabled="auth.isFetching">
|
||||
{{ $t('settings.webauthn.button-save-title') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" :title="$t('settings.webauthn.button-cancel-text')" @click.prevent="cancelRename(credential)">
|
||||
{{ $t('settings.webauthn.button-cancel-title') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button class="btn btn-secondary me-1" :title="$t('settings.webauthn.button-rename-text')" @click.prevent="enableRename(credential)">
|
||||
{{ $t('settings.webauthn.button-rename-title') }}
|
||||
</button>
|
||||
<button class="btn btn-danger" :title="$t('settings.webauthn.button-delete-text')" data-bs-toggle="modal" data-bs-target="#webAuthnDeleteModal" :disabled="auth.isFetching" @click="selectedCredential=credential">
|
||||
{{ $t('settings.webauthn.button-delete-title') }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="webAuthnDeleteModal" tabindex="-1" aria-labelledby="webAuthnDeleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title" id="webAuthnDeleteModalLabel">{{ $t('settings.webauthn.modal-delete.headline') }}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" :aria-label="$t('settings.webauthn.modal-delete.button-cancel')"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 class="mb-3">{{ selectedCredential.Name }} <small class="text-body-secondary">({{ $t('settings.webauthn.modal-delete.created') }} {{ selectedCredential.CreatedAt }})</small></h5>
|
||||
<p class="mb-0">{{ $t('settings.webauthn.modal-delete.abstract') }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t('settings.webauthn.modal-delete.button-cancel') }}</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmWebAuthnDelete" @click="auth.DeleteWebAuthnCredential(selectedCredential.ID)" :disabled="auth.isFetching" data-bs-dismiss="modal">{{ $t('settings.webauthn.modal-delete.button-delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
Reference in New Issue
Block a user