2022-03-15 14:47:07 +01:00
< template >
< div >
2022-07-07 18:04:04 +02:00
< setting -tabs :activeTab ="'settings.webauthn.devices'" > < / s e t t i n g - t a b s >
2022-03-15 14:47:07 +01:00
< div class = "options-tabs" >
< form -wrapper >
2022-03-24 14:58:30 +01:00
< div v -if = " isRemoteUser " class = "notification is-warning has-text-centered" v -html = " $ t ( ' auth.auth_handled_by_proxy ' ) " / >
2022-03-15 14:47:07 +01:00
< h4 class = "title is-4 has-text-grey-light" > { { $t ( 'auth.webauthn.security_devices' ) } } < / h4 >
< div class = "is-size-7-mobile" >
{ { $t ( 'auth.webauthn.security_devices_legend' ) } }
< / div >
< div class = "mt-3" >
2022-09-30 14:00:22 +02:00
< a tabindex = "0" @click ="register" @keyup.enter ="register" >
2022-09-21 21:38:53 +02:00
< font -awesome -icon : icon = "['fas', 'plus-circle']" / > & nbsp ; { { $t ( 'auth.webauthn.register_a_new_device' ) } }
2022-03-15 14:47:07 +01:00
< / a >
< / div >
<!-- credentials list -- >
< div v-if ="credentials.length > 0" class="field" >
2023-02-01 17:21:55 +01:00
< div v-for ="credential in credentials" :key="credential.id" class="group-item is-size-5 is-size-6-mobile" >
2022-03-15 14:47:07 +01:00
{ { displayName ( credential ) } }
<!-- revoke link -- >
2023-02-01 17:21:55 +01:00
< button class = "button tag is-pulled-right" : class = "$root.showDarkMode ? 'is-dark':'is-white'" @click ="revokeCredential(credential.id)" :title ="$t('settings.revoke')" >
2022-03-15 14:47:07 +01:00
{ { $t ( 'settings.revoke' ) } }
2022-09-18 16:04:59 +02:00
< / button >
2022-03-15 14:47:07 +01:00
<!-- edit link -- >
<!-- < router -link : to = "{ name: '' }" class = "has-text-grey pl-1" :title ="$t('commons.rename')" >
< font -awesome -icon : icon = "['fas', 'pen-square']" / >
< / r o u t e r - l i n k > - - >
< / div >
< div class = "mt-2 is-size-7 is-pulled-right" >
{ { $t ( 'auth.webauthn.revoking_a_device_is_permanent' ) } }
< / div >
< / div >
< div v-if ="isFetching && credentials.length === 0" class="has-text-centered mt-6" >
< span class = "is-size-4" >
< font -awesome -icon : icon = "['fas', 'spinner']" spin / >
< / span >
< / div >
< h4 class = "title is-4 pt-6 has-text-grey-light" > { { $t ( 'settings.options' ) } } < / h4 >
2022-07-18 15:30:58 +02:00
< div class = "field" >
2022-04-06 09:48:07 +02:00
{ { $t ( 'auth.webauthn.need_a_security_device_to_enable_options' ) } }
< / div >
2022-03-15 14:47:07 +01:00
< form >
<!-- use webauthn only -- >
2023-02-25 22:40:18 +01:00
< form -checkbox v -on : useWebauthnOnly = "savePreference('useWebauthnOnly', $event)" :form ="form" fieldName = "useWebauthnOnly" :label ="$t('auth.webauthn.use_webauthn_only.label')" :help ="$t('auth.webauthn.use_webauthn_only.help')" : disabled = "isRemoteUser || credentials.length === 0" / >
2022-03-15 14:47:07 +01:00
< / form >
<!-- footer -- >
< vue -footer :showButtons ="true" >
<!-- close button -- >
< p class = "control" >
2023-08-21 14:46:05 +02:00
< router -link
: to = "{ path: $route.params.returnTo, params: { toRefresh: false } }"
class = "button is-rounded"
: class = "{'is-dark' : $root.showDarkMode}"
tabindex = "0"
role = "button"
: aria - label = "$t('commons.close_the_x_page', {pagetitle: $router.currentRoute.meta.title})" >
{ { $t ( 'commons.close' ) } }
< / r o u t e r - l i n k >
2022-03-15 14:47:07 +01:00
< / p >
< / v u e - f o o t e r >
< / f o r m - w r a p p e r >
< / div >
< / div >
< / template >
< script >
import Form from './../../components/Form'
2022-11-14 17:13:24 +01:00
import WebAuthn from './../../components/WebAuthn'
2022-03-15 14:47:07 +01:00
export default {
data ( ) {
return {
form : new Form ( {
useWebauthnOnly : null ,
} ) ,
credentials : [ ] ,
isFetching : false ,
2022-03-24 14:58:30 +01:00
isRemoteUser : false ,
2022-11-14 17:13:24 +01:00
webauthn : new WebAuthn ( )
2022-03-15 14:47:07 +01:00
}
} ,
async mounted ( ) {
2022-08-10 18:39:41 +02:00
2023-02-25 22:40:18 +01:00
const { data } = await this . form . get ( '/api/v1/user/preferences' )
2022-03-15 14:47:07 +01:00
this . form . fillWithKeyValueObject ( data )
this . form . setOriginal ( )
this . fetchCredentials ( )
} ,
methods : {
/ * *
2023-02-25 22:40:18 +01:00
* Save a preference
2022-03-15 14:47:07 +01:00
* /
2023-02-25 22:40:18 +01:00
savePreference ( preferenceName , event ) {
this . axios . put ( '/api/v1/user/preferences/' + preferenceName , { value : event } ) . then ( response => {
2022-03-15 14:47:07 +01:00
this . $notify ( { type : 'is-success' , text : this . $t ( 'settings.forms.setting_saved' ) } )
2023-02-17 17:12:53 +01:00
this . $root . userPreferences [ response . data . key ] = response . data . value
2022-03-15 14:47:07 +01:00
} )
} ,
/ * *
* Get all credentials from backend
* /
async fetchCredentials ( ) {
this . isFetching = true
2022-03-29 15:02:43 +02:00
await this . axios . get ( '/webauthn/credentials' , { returnError : true } )
. then ( response => {
this . credentials = response . data
} )
. catch ( error => {
if ( error . response . status === 400 ) {
2022-03-24 14:58:30 +01:00
this . isRemoteUser = true
}
2022-03-29 15:02:43 +02:00
else {
this . $router . push ( { name : 'genericError' , params : { err : error . response } } ) ;
}
2022-03-15 14:47:07 +01:00
} )
this . isFetching = false
} ,
/ * *
* Register a new security device
* /
async register ( ) {
2022-03-24 14:58:30 +01:00
if ( this . isRemoteUser ) {
this . $notify ( { type : 'is-warning' , text : this . $t ( 'errors.unsupported_with_reverseproxy' ) } )
return false
}
2022-03-15 14:47:07 +01:00
// Check https context
if ( ! window . isSecureContext ) {
this . $notify ( { type : 'is-danger' , text : this . $t ( 'errors.https_required' ) } )
return false
}
// Check browser support
2022-11-14 17:13:24 +01:00
if ( this . webauthn . doesntSupportWebAuthn ) {
2022-03-15 14:47:07 +01:00
this . $notify ( { type : 'is-danger' , text : this . $t ( 'errors.browser_does_not_support_webauthn' ) } )
return false
}
const registerOptions = await this . axios . post ( '/webauthn/register/options' ) . then ( res => res . data )
2022-11-14 17:13:24 +01:00
const publicKey = this . webauthn . parseIncomingServerOptions ( registerOptions )
2022-03-15 14:47:07 +01:00
let bufferedCredentials
try {
bufferedCredentials = await navigator . credentials . create ( { publicKey } )
}
catch ( error ) {
if ( error . name == 'AbortError' ) {
this . $notify ( { type : 'is-warning' , text : this . $t ( 'errors.aborted_by_user' ) } )
}
2022-11-14 17:13:24 +01:00
else if ( error . name == 'SecurityError' ) {
this . $notify ( { type : 'is-danger' , text : this . $t ( 'errors.security_error_check_rpid' ) } )
}
else if ( error . name == 'InvalidStateError' ) {
2022-03-15 14:47:07 +01:00
this . $notify ( { type : 'is-danger' , text : this . $t ( 'errors.security_device_unsupported' ) } )
}
2022-11-14 17:13:24 +01:00
else if ( error . name == 'NotAllowedError' ) {
this . $notify ( { type : 'is-danger' , text : this . $t ( 'errors.not_allowed_operation' ) } )
}
2023-02-21 09:29:05 +01:00
else if ( error . name == 'NotSupportedError' ) {
this . $notify ( { type : 'is-danger' , text : this . $t ( 'errors.unsupported_operation' ) } )
}
2022-03-15 14:47:07 +01:00
return false
}
2022-11-14 17:13:24 +01:00
const publicKeyCredential = this . webauthn . parseOutgoingCredentials ( bufferedCredentials ) ;
2022-03-15 14:47:07 +01:00
2023-07-05 10:07:40 +02:00
this . axios . post ( '/webauthn/register' , publicKeyCredential , { returnError : true } )
. then ( response => {
2022-03-28 17:09:29 +02:00
this . $router . push ( { name : 'settings.webauthn.editCredential' , params : { id : publicKeyCredential . id , name : this . $t ( 'auth.webauthn.my_device' ) } } )
2022-03-15 14:47:07 +01:00
} )
2023-07-05 10:07:40 +02:00
. catch ( error => {
if ( error . response . status === 422 ) {
this . $notify ( { type : 'is-danger' , text : error . response . data . message } )
}
else {
this . $router . push ( { name : 'genericError' , params : { err : error . response } } ) ;
}
} )
2022-03-15 14:47:07 +01:00
} ,
/ * *
* revoke a credential
* /
async revokeCredential ( credentialId ) {
if ( confirm ( this . $t ( 'auth.confirm.revoke_device' ) ) ) {
await this . axios . delete ( '/webauthn/credentials/' + credentialId ) . then ( response => {
// Remove the revoked credential from the collection
this . credentials = this . credentials . filter ( a => a . id !== credentialId )
2022-08-10 18:39:41 +02:00
if ( this . credentials . length == 0 ) {
this . form . useWebauthnOnly = false
2023-02-17 17:12:53 +01:00
this . $root . userPreferences [ 'useWebauthnOnly' ] = false
2022-08-10 18:39:41 +02:00
}
2022-03-15 14:47:07 +01:00
this . $notify ( { type : 'is-success' , text : this . $t ( 'auth.webauthn.device_revoked' ) } )
} ) ;
}
} ,
/ * *
* Always display a printable name
* /
displayName ( credential ) {
2022-11-14 17:13:24 +01:00
return credential . alias ? credential . alias : this . $t ( 'auth.webauthn.my_device' ) + ' (#' + credential . id . substring ( 0 , 10 ) + ')'
2022-03-15 14:47:07 +01:00
} ,
} ,
2023-08-21 14:46:05 +02:00
beforeRouteEnter ( to , from , next ) {
next ( vm => {
if ( from . params . returnTo ) {
to . params . returnTo = from . params . returnTo
}
else {
to . params . returnTo = from . name
? from . path
: '/accounts'
}
} )
} ,
beforeRouteLeave ( to , from , next ) {
if ( to . name == 'accounts' ) {
this . $notify ( { clean : true } )
}
next ( )
}
2022-03-15 14:47:07 +01:00
}
< / script >