diff --git a/CHANGELOG.md b/CHANGELOG.md index da396f9c9..8c16b4304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ release channel, you can take advantage of these new features and fixes. ## New Features/Changes +- **Passwordless Login with Passkeys**: You can now associate passkeys (also known as WebAuthn) with your account and + then automatically log in with those passkeys any time. Popular passkeys include those provided by your operating + system (i.e. Windows Hello or Apple's Safari passkey system) or may be provided by third-party software (like + Bitwarden). They're a secure alternative to passwords and two-factor authentication. You can register multiple + passkeys with a single account, and if you ever misplace your passkey, you can still log in with your regular e-mail + address and password (and two-factor auth, if you've set one up). + ## Code Quality/Technical Changes - If you upload media to a folder and that folder is set to auto-assign to a playlist, the media will *instantly* be a diff --git a/composer.json b/composer.json index 4fd31873f..5484a2f42 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ "guzzlehttp/guzzle": "^7.0", "intervention/image": "^2.6", "james-heinrich/getid3": "v2.0.0-beta5", + "lbuchs/webauthn": "^2.1", "league/csv": "^9.6", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-sftp-v3": "^3.0", diff --git a/composer.lock b/composer.lock index b933c310f..f4c08704f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c5af35fc86c5a1ab7fe1dac7fff30c0c", + "content-hash": "89af2db97e6bf2e6179935c6e2d32ea9", "packages": [ { "name": "aws/aws-crt-php", @@ -2878,6 +2878,51 @@ }, "time": "2023-11-08T14:08:06+00:00" }, + { + "name": "lbuchs/webauthn", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/lbuchs/WebAuthn.git", + "reference": "1cc44fbd61d41aec55d0f21f386a1fdd7df1459a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lbuchs/WebAuthn/zipball/1cc44fbd61d41aec55d0f21f386a1fdd7df1459a", + "reference": "1cc44fbd61d41aec55d0f21f386a1fdd7df1459a", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "lbuchs\\WebAuthn\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukas Buchs", + "role": "Developer" + } + ], + "description": "A simple PHP WebAuthn (FIDO2) server library", + "homepage": "https://github.com/lbuchs/webauthn", + "keywords": [ + "Authentication", + "webauthn" + ], + "support": { + "issues": "https://github.com/lbuchs/WebAuthn/issues", + "source": "https://github.com/lbuchs/WebAuthn/tree/v2.1.0" + }, + "time": "2023-10-23T10:19:40+00:00" + }, { "name": "league/csv", "version": "9.11.0", diff --git a/config/routes/api_frontend.php b/config/routes/api_frontend.php index 93da0c86a..62698d00b 100644 --- a/config/routes/api_frontend.php +++ b/config/routes/api_frontend.php @@ -47,6 +47,31 @@ return static function (RouteCollectorProxy $group) { '/api-key/{id}', Controller\Api\Frontend\Account\ApiKeysController::class . ':deleteAction' ); + + $group->get( + '/webauthn/register', + Controller\Api\Frontend\Account\WebAuthn\GetRegistrationAction::class + )->setName('api:frontend:webauthn:register'); + + $group->put( + '/webauthn/register', + Controller\Api\Frontend\Account\WebAuthn\PutRegistrationAction::class + ); + + $group->get( + '/passkeys', + Controller\Api\Frontend\Account\PasskeysController::class . ':listAction' + )->setName('api:frontend:passkeys'); + + $group->get( + '/passkey/{id}', + Controller\Api\Frontend\Account\PasskeysController::class . ':getAction' + )->setName('api:frontend:passkey'); + + $group->delete( + '/passkey/{id}', + Controller\Api\Frontend\Account\PasskeysController::class . ':deleteAction' + ); } ); diff --git a/config/routes/base.php b/config/routes/base.php index ade2ecbac..edd187ebe 100644 --- a/config/routes/base.php +++ b/config/routes/base.php @@ -53,6 +53,11 @@ return static function (RouteCollectorProxy $app) { ->setName('account:recover') ->add(Middleware\EnableView::class); + $app->get('/login/webauthn', Controller\Frontend\Account\WebAuthn\GetValidationAction::class) + ->setName('account:webauthn'); + + $app->post('/login/webauthn', Controller\Frontend\Account\WebAuthn\PostValidationAction::class); + $app->group( '/setup', function (RouteCollectorProxy $group) { diff --git a/frontend/src/components/Account.vue b/frontend/src/components/Account.vue index 5719a6672..5a2e0f5c4 100644 --- a/frontend/src/components/Account.vue +++ b/frontend/src/components/Account.vue @@ -1,245 +1,53 @@ diff --git a/frontend/src/components/Account/ApiKeysPanel.vue b/frontend/src/components/Account/ApiKeysPanel.vue new file mode 100644 index 000000000..bd7511ff0 --- /dev/null +++ b/frontend/src/components/Account/ApiKeysPanel.vue @@ -0,0 +1,104 @@ + + + diff --git a/frontend/src/components/Account/ChangePasswordModal.vue b/frontend/src/components/Account/ChangePasswordModal.vue index a94334508..1e9fb93bb 100644 --- a/frontend/src/components/Account/ChangePasswordModal.vue +++ b/frontend/src/components/Account/ChangePasswordModal.vue @@ -46,16 +46,12 @@ import {ref} from "vue"; import {useAxios} from "~/vendor/axios"; import {useTranslate} from "~/vendor/gettext"; import {ModalFormTemplateRef} from "~/functions/useBaseEditModal.ts"; - -const props = defineProps({ - changePasswordUrl: { - type: String, - required: true - } -}); +import {getApiUrl} from "~/router.ts"; const emit = defineEmits(['relist']); +const changePasswordUrl = getApiUrl('/frontend/account/password'); + const passwordsMatch = (value, siblings) => { return siblings.new_password === value; }; @@ -97,7 +93,7 @@ const {axios} = useAxios(); const onSubmit = () => { ifValid(() => { axios - .put(props.changePasswordUrl, form.value) + .put(changePasswordUrl.value, form.value) .finally(() => { $modal.value?.hide(); emit('relist'); diff --git a/frontend/src/components/Account/EditModal.vue b/frontend/src/components/Account/EditModal.vue index 783981198..8e4f065d6 100644 --- a/frontend/src/components/Account/EditModal.vue +++ b/frontend/src/components/Account/EditModal.vue @@ -25,12 +25,9 @@ import {useVuelidateOnForm} from "~/functions/useVuelidateOnForm"; import {useNotify} from "~/functions/useNotify"; import {useAxios} from "~/vendor/axios"; import {ModalFormTemplateRef} from "~/functions/useBaseEditModal.ts"; +import {getApiUrl} from "~/router.ts"; const props = defineProps({ - userUrl: { - type: String, - required: true - }, supportedLocales: { type: Object, required: true @@ -39,6 +36,8 @@ const props = defineProps({ const emit = defineEmits(['reload']); +const userUrl = getApiUrl('/frontend/account/me'); + const loading = ref(true); const error = ref(null); @@ -77,7 +76,7 @@ const open = () => { $modal.value?.show(); - axios.get(props.userUrl).then((resp) => { + axios.get(userUrl.value).then((resp) => { form.value = mergeExisting(form.value, resp.data); loading.value = false; }).catch(() => { @@ -91,7 +90,7 @@ const doSubmit = () => { axios({ method: 'PUT', - url: props.userUrl, + url: userUrl.value, data: form.value }).then(() => { notifySuccess(); diff --git a/frontend/src/components/Account/PasskeyModal.vue b/frontend/src/components/Account/PasskeyModal.vue new file mode 100644 index 000000000..8e3f86b85 --- /dev/null +++ b/frontend/src/components/Account/PasskeyModal.vue @@ -0,0 +1,193 @@ + + + diff --git a/frontend/src/components/Account/SecurityPanel.vue b/frontend/src/components/Account/SecurityPanel.vue new file mode 100644 index 000000000..1044f90eb --- /dev/null +++ b/frontend/src/components/Account/SecurityPanel.vue @@ -0,0 +1,213 @@ + + + diff --git a/frontend/src/components/Account/UserInfoPanel.vue b/frontend/src/components/Account/UserInfoPanel.vue new file mode 100644 index 000000000..f2a02cf5e --- /dev/null +++ b/frontend/src/components/Account/UserInfoPanel.vue @@ -0,0 +1,77 @@ + + diff --git a/frontend/src/components/Dashboard.vue b/frontend/src/components/Dashboard.vue index 50d5c3672..7d4505c77 100644 --- a/frontend/src/components/Dashboard.vue +++ b/frontend/src/components/Dashboard.vue @@ -8,44 +8,25 @@ role="region" :aria-label="$gettext('Account Details')" > -
- - -
-

- {{ user.name }} -

-

- {{ user.email }} -

-
- - -
+ + + + {{ $gettext('My Account') }} + + + + {{ $gettext('Administration') }} + +