194 lines
5.3 KiB
Vue
194 lines
5.3 KiB
Vue
<template>
|
|
<modal
|
|
id="api_keys_modal"
|
|
ref="$modal"
|
|
size="md"
|
|
centered
|
|
:title="$gettext('Add New Passkey')"
|
|
no-enforce-focus
|
|
@hidden="onHidden"
|
|
>
|
|
<template #default>
|
|
<div
|
|
v-show="error != null"
|
|
class="alert alert-danger"
|
|
>
|
|
{{ error }}
|
|
</div>
|
|
|
|
<form
|
|
v-if="isSupported"
|
|
class="form vue-form"
|
|
@submit.prevent="doSubmit"
|
|
>
|
|
<form-group-field
|
|
id="form_name"
|
|
:field="v$.name"
|
|
autofocus
|
|
class="mb-3"
|
|
:label="$gettext('Passkey Nickname')"
|
|
/>
|
|
|
|
<form-markup id="form_select_passkey">
|
|
<template #label>
|
|
{{ $gettext('Select Passkey') }}
|
|
</template>
|
|
|
|
<p class="card-text">
|
|
{{ $gettext('Click the button below to open your browser window to select a passkey.') }}
|
|
</p>
|
|
|
|
<p
|
|
v-if="form.createResponse"
|
|
class="card-text"
|
|
>
|
|
{{ $gettext('A passkey has been selected. Submit this form to add it to your account.') }}
|
|
</p>
|
|
<div
|
|
v-else
|
|
class="buttons"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
@click="selectPasskey"
|
|
>
|
|
{{ $gettext('Select Passkey') }}
|
|
</button>
|
|
</div>
|
|
</form-markup>
|
|
|
|
<invisible-submit-button />
|
|
</form>
|
|
|
|
<div v-else>
|
|
<p class="card-text">
|
|
{{
|
|
$gettext('Your browser does not support passkeys. Consider updating your browser to the latest version.')
|
|
}}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<template #modal-footer="slotProps">
|
|
<slot
|
|
name="modal-footer"
|
|
v-bind="slotProps"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
@click="hide"
|
|
>
|
|
{{ $gettext('Close') }}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="btn"
|
|
:class="(v$.$invalid) ? 'btn-danger' : 'btn-primary'"
|
|
@click="doSubmit"
|
|
>
|
|
{{ $gettext('Add New Passkey') }}
|
|
</button>
|
|
</slot>
|
|
</template>
|
|
</modal>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import InvisibleSubmitButton from "~/components/Common/InvisibleSubmitButton.vue";
|
|
import FormGroupField from "~/components/Form/FormGroupField.vue";
|
|
import {required} from '@vuelidate/validators';
|
|
import {ref} from "vue";
|
|
import {useVuelidateOnForm} from "~/functions/useVuelidateOnForm";
|
|
import {useAxios} from "~/vendor/axios";
|
|
import Modal from "~/components/Common/Modal.vue";
|
|
import {ModalTemplateRef, useHasModal} from "~/functions/useHasModal.ts";
|
|
import FormMarkup from "~/components/Form/FormMarkup.vue";
|
|
import {getApiUrl} from "~/router.ts";
|
|
import useWebAuthn from "~/functions/useWebAuthn.ts";
|
|
|
|
const emit = defineEmits(['relist']);
|
|
|
|
const registerWebAuthnUrl = getApiUrl('/frontend/account/webauthn/register');
|
|
|
|
const error = ref(null);
|
|
|
|
const {form, resetForm, v$, validate} = useVuelidateOnForm(
|
|
{
|
|
name: {required},
|
|
createResponse: {required}
|
|
},
|
|
{
|
|
name: '',
|
|
createResponse: null
|
|
}
|
|
);
|
|
|
|
const clearContents = () => {
|
|
resetForm();
|
|
error.value = null;
|
|
};
|
|
|
|
const $modal = ref<ModalTemplateRef>(null);
|
|
const {show, hide} = useHasModal($modal);
|
|
|
|
const create = () => {
|
|
clearContents();
|
|
show();
|
|
};
|
|
|
|
const onHidden = () => {
|
|
clearContents();
|
|
emit('relist');
|
|
};
|
|
|
|
const {axios} = useAxios();
|
|
|
|
const {isSupported, processServerArgs, processRegisterResponse} = useWebAuthn();
|
|
|
|
const selectPasskey = async () => {
|
|
// GET registration options from the endpoint that calls
|
|
const registerArgs = await axios.get(registerWebAuthnUrl.value).then(r => processServerArgs(r.data));
|
|
|
|
let attResp;
|
|
try {
|
|
// Pass the options to the authenticator and wait for a response
|
|
attResp = await navigator.credentials.create(registerArgs);
|
|
form.value.createResponse = processRegisterResponse(attResp);
|
|
} catch (error) {
|
|
// Some basic error handling
|
|
if (error.name === 'InvalidStateError') {
|
|
error.value = 'Error: Authenticator was probably already registered by user';
|
|
} else {
|
|
error.value = error;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const doSubmit = async () => {
|
|
const isValid = await validate();
|
|
if (!isValid) {
|
|
return;
|
|
}
|
|
|
|
error.value = null;
|
|
|
|
axios({
|
|
method: 'PUT',
|
|
url: registerWebAuthnUrl.value,
|
|
data: form.value
|
|
}).then(() => {
|
|
hide();
|
|
}).catch((error) => {
|
|
error.value = error.response.data.message;
|
|
});
|
|
};
|
|
|
|
defineExpose({
|
|
create
|
|
});
|
|
</script>
|