diff --git a/dashboard/src/Index.vue b/dashboard/src/Index.vue index 5bb88212e..03659f7ec 100644 --- a/dashboard/src/Index.vue +++ b/dashboard/src/Index.vue @@ -459,12 +459,9 @@ onMounted(async () => { ready.value = true; - // when done, redirect the user to setup 2fa if it is mandatory and neither totp (totpEnabled) nor passkey is setup - if (config.value.mandatory2FA && !profile.value.totpEnabled) { - const [error, result] = await profileModel.getPasskey(); - if (error) return window.cloudron.onError(error); - - if (!result) return window.location.href = VIEWS.PROFILE; + // when done, redirect the user to setup 2fa if it is mandatory and neither totp nor passkey is setup + if (config.value.mandatory2FA && !profile.value.totpEnabled && !profile.value.hasPasskey) { + return window.location.href = VIEWS.PROFILE; } }); diff --git a/dashboard/src/models/ProfileModel.js b/dashboard/src/models/ProfileModel.js index 1a9324b00..8040c916c 100644 --- a/dashboard/src/models/ProfileModel.js +++ b/dashboard/src/models/ProfileModel.js @@ -234,17 +234,6 @@ function create() { if (error || result.status !== 201) return [error || result]; return [null, result.body]; }, - async getPasskey() { - let error, result; - try { - result = await fetcher.get(`${API_ORIGIN}/api/v1/profile/passkey`, { access_token: accessToken }); - } catch (e) { - error = e; - } - - if (error || result.status !== 200) return [error || result]; - return [null, result.body.passkey]; - }, async getPasskeyRegistrationOptions() { let error, result; try { diff --git a/dashboard/src/views/ProfileView.vue b/dashboard/src/views/ProfileView.vue index df6c2baeb..399073b92 100644 --- a/dashboard/src/views/ProfileView.vue +++ b/dashboard/src/views/ProfileView.vue @@ -100,15 +100,8 @@ async function onRevokeAllWebAndCliTokens() { await profileModel.logout(); } -const userPasskey = ref(null); const enableTwoFADialog = useTemplateRef('enableTwoFADialog'); -const has2FA = computed(() => profile.value.totpEnabled || !!userPasskey.value); - -async function loadPasskey() { - const [error, result] = await profileModel.getPasskey(); - if (error) return console.error('Failed to load passkey', error); - userPasskey.value = result; -} +const has2FA = computed(() => profile.value.totpEnabled || profile.value.hasPasskey); async function onOpenTwoFASetupDialog(method) { enableTwoFADialog.value.open(method); @@ -116,7 +109,6 @@ async function onOpenTwoFASetupDialog(method) { async function onEnableTwoFASuccess() { await refreshProfile(); - await loadPasskey(); } async function onTwoFADisable(method) { @@ -125,7 +117,6 @@ async function onTwoFADisable(method) { async function onTwoFADisableSuccess() { await refreshProfile(); - await loadPasskey(); } // Init @@ -161,10 +152,8 @@ onMounted(async () => { webadminTokens.value = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_WEBADMIN || c.clientId === TOKEN_TYPES.ID_DEVELOPMENT || c.clientId === 'dashboard' || c.clientId === 'development'; }); cliTokens.value = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_CLI; }); - await loadPasskey(); - // check if we should show the 2fa setup - if (config.value.mandatory2FA && !profile.value.totpEnabled && !userPasskey.value) { + if (config.value.mandatory2FA && !profile.value.totpEnabled && !profile.value.hasPasskey) { onOpenTwoFASetupDialog(); } }); @@ -232,9 +221,9 @@ onMounted(async () => {
{{ $t('profile.twoFactorAuth.totpEnabled') }}
-
{{ $t('profile.notSet') }}
+
{{ $t('profile.notSet') }}
-
+
@@ -243,11 +232,11 @@ onMounted(async () => { -
{{ $t('profile.twoFactorAuth.passkeyEnabled') }}
-
{{ $t('profile.notSet') }}
+
{{ $t('profile.twoFactorAuth.passkeyEnabled') }}
+
{{ $t('profile.notSet') }}
-
- +
+
diff --git a/src/routes/profile.js b/src/routes/profile.js index f50199988..331adbe78 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -26,6 +26,8 @@ async function canEditProfile(req, res, next) { async function get(req, res, next) { assert.strictEqual(typeof req.user, 'object'); + const userPasskeys = await passkeys.listByUserId(req.user.id); + next(new HttpSuccess(200, { id: req.user.id, username: req.user.username, @@ -33,6 +35,7 @@ async function get(req, res, next) { fallbackEmail: req.user.fallbackEmail, displayName: req.user.displayName, totpEnabled: req.user.totpEnabled, + hasPasskey: userPasskeys.length > 0, role: req.user.role, source: req.user.source, hasBackgroundImage: req.user.hasBackgroundImage, @@ -232,17 +235,6 @@ async function destroyUserSession(req, res, next) { next(new HttpSuccess(204)); } -async function getPasskey(req, res, next) { - assert.strictEqual(typeof req.user, 'object'); - - const [error, result] = await safe(passkeys.listByUserId(req.user.id)); - if (error) return next(BoxError.toHttpError(error)); - - // Only one passkey per user - return first one or null - const passkey = result.length > 0 ? passkeys.removePrivateFields(result[0]) : null; - next(new HttpSuccess(200, { passkey })); -} - async function getPasskeyRegistrationOptions(req, res, next) { assert.strictEqual(typeof req.user, 'object'); @@ -301,7 +293,6 @@ export default { disableTotp, setNotificationConfig, destroyUserSession, - getPasskey, getPasskeyRegistrationOptions, registerPasskey, deletePasskey diff --git a/src/server.js b/src/server.js index c831b2bee..66c8d461b 100644 --- a/src/server.js +++ b/src/server.js @@ -202,7 +202,6 @@ async function initializeExpressSync() { router.del ('/api/v1/profile/sessions', token, authorizeUser, routes.profile.destroyUserSession); // passkey routes (singular - only one passkey per user) - router.get ('/api/v1/profile/passkey', token, authorizeUser, routes.profile.getPasskey); router.post('/api/v1/profile/passkey/register/options', json, token, authorizeUser, routes.profile.getPasskeyRegistrationOptions); router.post('/api/v1/profile/passkey/register', json, token, authorizeUser, routes.profile.registerPasskey); router.post('/api/v1/profile/passkey/disable', json, token, authorizeUser, routes.users.verifyPassword, routes.profile.deletePasskey);