From 2aae76db8066911e1de337e451f3efd08fadd8cc Mon Sep 17 00:00:00 2001 From: dartcafe Date: Fri, 8 Jul 2022 22:50:48 +0200 Subject: [PATCH] allow public users changing their name Signed-off-by: dartcafe --- appinfo/routes.php | 1 + lib/AppInfo/Application.php | 2 + lib/Controller/PublicController.php | 10 +++ lib/Event/ShareChangedDisplayNameEvent.php | 33 ++++++++++ lib/Event/ShareEvent.php | 1 + lib/Listener/ShareListener.php | 8 ++- lib/Service/ActivityService.php | 8 +++ lib/Service/ShareService.php | 30 ++++++++- src/js/components/User/UserMenu.vue | 77 +++++++++++++++++++++- src/js/store/modules/share.js | 22 +++++++ 10 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 lib/Event/ShareChangedDisplayNameEvent.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 0d43de069..b4cba9354 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -31,6 +31,7 @@ return [ ['name' => 'public#get_votes', 'url' => '/s/{token}/votes', 'verb' => 'GET'], ['name' => 'public#get_subscription', 'url' => '/s/{token}/subscription', 'verb' => 'GET'], ['name' => 'public#set_email_address', 'url' => '/s/{token}/email', 'verb' => 'PUT'], + ['name' => 'public#set_display_name', 'url' => '/s/{token}/name/{displayName}', 'verb' => 'PUT'], ['name' => 'public#delete_email_address', 'url' => '/s/{token}/email', 'verb' => 'DELETE'], ['name' => 'public#set_vote', 'url' => '/s/{token}/vote', 'verb' => 'PUT'], diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 73aff0ad2..44a5ec05f 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -51,6 +51,7 @@ use OCA\Polls\Event\PollOwnerChangeEvent; use OCA\Polls\Event\PollRestoredEvent; use OCA\Polls\Event\PollTakeoverEvent; use OCA\Polls\Event\PollUpdatedEvent; +use OCA\Polls\Event\ShareChangedDisplayNameEvent; use OCA\Polls\Event\ShareCreateEvent; use OCA\Polls\Event\ShareTypeChangedEvent; use OCA\Polls\Event\ShareChangedEmailEvent; @@ -103,6 +104,7 @@ class Application extends App implements IBootstrap { $context->registerEventListener(PollTakeoverEvent::class, PollListener::class); $context->registerEventListener(PollUpdatedEvent::class, PollListener::class); $context->registerEventListener(ShareChangedEmailEvent::class, ShareListener::class); + $context->registerEventListener(ShareChangedDisplayNameEvent::class, ShareListener::class); $context->registerEventListener(ShareChangedRegistrationConstraintEvent::class, ShareListener::class); $context->registerEventListener(ShareCreateEvent::class, ShareListener::class); $context->registerEventListener(ShareDeletedEvent::class, ShareListener::class); diff --git a/lib/Controller/PublicController.php b/lib/Controller/PublicController.php index 1bbb36695..de7ec36a7 100644 --- a/lib/Controller/PublicController.php +++ b/lib/Controller/PublicController.php @@ -301,6 +301,16 @@ class PublicController extends Controller { return $this->response(fn () => ['result' => $this->systemService->validateEmailAddress($emailAddress), 'emailAddress' => $emailAddress]); } + /** + * Change displayName + * @PublicPage + * @NoAdminRequired + */ + public function setDisplayName(string $token, string $displayName): JSONResponse { + return $this->response(fn () => ['share' => $this->shareService->setDisplayname($token, $displayName)]); + } + + /** * Set EmailAddress * @PublicPage diff --git a/lib/Event/ShareChangedDisplayNameEvent.php b/lib/Event/ShareChangedDisplayNameEvent.php new file mode 100644 index 000000000..4b7068689 --- /dev/null +++ b/lib/Event/ShareChangedDisplayNameEvent.php @@ -0,0 +1,33 @@ + + * + * @author René Gieling + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Polls\Event; + +use OCA\Polls\Db\Share; + +class ShareChangedDisplayNameEvent extends ShareEvent { + public function __construct(Share $share) { + parent::__construct($share); + $this->activitySubject = self::CHANGE_DISPLAY_NAME; + } +} diff --git a/lib/Event/ShareEvent.php b/lib/Event/ShareEvent.php index f6fa29744..0b3a9fc80 100644 --- a/lib/Event/ShareEvent.php +++ b/lib/Event/ShareEvent.php @@ -29,6 +29,7 @@ abstract class ShareEvent extends BaseEvent { public const ADD = 'share_add'; public const ADD_PUBLIC = 'share_add_public'; public const CHANGE_EMAIL = 'share_change_email'; + public const CHANGE_DISPLAY_NAME = 'share_change_display_name'; public const CHANGE_TYPE = 'share_change_type'; public const CHANGE_REG_CONSTR = 'share_change_reg_const'; public const REGISTRATION = 'share_registration'; diff --git a/lib/Listener/ShareListener.php b/lib/Listener/ShareListener.php index 3e4562c07..34208f11d 100644 --- a/lib/Listener/ShareListener.php +++ b/lib/Listener/ShareListener.php @@ -30,7 +30,13 @@ use OCA\Polls\Exceptions\InvalidClassException; class ShareListener extends BaseListener { /** @var array */ - protected $watchTables = [Watch::OBJECT_SHARES, Watch::OBJECT_POLLS]; + protected $watchTables = [ + Watch::OBJECT_SHARES, + Watch::OBJECT_POLLS, + Watch::OBJECT_VOTES, + Watch::OBJECT_OPTIONS, + Watch::OBJECT_COMMENTS + ]; protected function checkClass() : void { if (!($this->event instanceof ShareEvent)) { diff --git a/lib/Service/ActivityService.php b/lib/Service/ActivityService.php index bf6875c4b..e3590010c 100644 --- a/lib/Service/ActivityService.php +++ b/lib/Service/ActivityService.php @@ -184,6 +184,10 @@ class ActivityService { return $this->userIsActor ? $this->l10n->t('You have changed your email address') : $this->l10n->t('{sharee} has changed his email address'); + case ShareEvent::CHANGE_DISPLAY_NAME: + return $this->userIsActor + ? $this->l10n->t('You have changed your name') + : $this->l10n->t('{sharee} has changed his name'); case ShareEvent::CHANGE_TYPE: return $this->userIsActor ? $this->l10n->t('You have changed the share type') @@ -330,6 +334,10 @@ class ActivityService { return $this->userIsActor ? $this->l10n->t('You have changed your email address') : $this->l10n->t('Email address of {sharee} has been changed'); + case ShareEvent::CHANGE_DISPLAY_NAME: + return $this->userIsActor + ? $this->l10n->t('You have changed your name') + : $this->l10n->t('Displaynname of {sharee} has been changed'); case ShareEvent::CHANGE_TYPE: return $this->userIsActor ? $this->l10n->t('You have changed the share type') diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index cd29787dd..feb848133 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -40,6 +40,7 @@ use OCA\Polls\Exceptions\InvalidUsernameException; use OCA\Polls\Db\PollMapper; use OCA\Polls\Db\ShareMapper; use OCA\Polls\Db\Share; +use OCA\Polls\Event\ShareChangedDisplayNameEvent; use OCA\Polls\Event\ShareCreateEvent; use OCA\Polls\Event\ShareTypeChangedEvent; use OCA\Polls\Event\ShareChangedEmailEvent; @@ -342,8 +343,7 @@ class ShareService { } /** - * Set emailAddress to personal share - * or update an email share with the username + * Set emailAddress of personal share * * @return Share */ @@ -368,6 +368,32 @@ class ShareService { return $this->share; } + /** + * Set displayName of personal share + * + * @return Share + */ + public function setDisplayName(string $token, string $displayName): Share { + try { + $this->share = $this->shareMapper->findByToken($token); + } catch (DoesNotExistException $e) { + throw new NotFoundException('Token ' . $token . ' does not exist'); + } + + if ($this->share->getType() === Share::TYPE_EXTERNAL) { + $this->systemService->validatePublicUsername($displayName, $token); + $this->share->setDisplayName($displayName); + // TODO: Send confirmation + $this->share = $this->shareMapper->update($this->share); + } else { + throw new InvalidShareTypeException('Displayname can only be changed in external shares.'); + } + + $this->eventDispatcher->dispatchTyped(new ShareChangedDisplayNameEvent($this->share)); + + return $this->share; + } + /** * Delete emailAddress of personal share * diff --git a/src/js/components/User/UserMenu.vue b/src/js/components/User/UserMenu.vue index 22b2d0b63..4fe89f91f 100644 --- a/src/js/components/User/UserMenu.vue +++ b/src/js/components/User/UserMenu.vue @@ -32,7 +32,6 @@ {{ t('polls', 'Edit Email Address') }} + + + {{ t('polls', 'Change name') }} + state.share, subscribed: (state) => state.subscription.subscribed, emailAddress: (state) => state.share.emailAddress, + displayName: (state) => state.poll.acl.displayName, }), hasCookie() { @@ -143,6 +159,10 @@ export default { return this.emailAddress === this.emailAddressTemp }, + displayNameUnchanged() { + return this.displayName === this.displayNameTemp + }, + check() { if (this.checking) { return { @@ -164,6 +184,27 @@ export default { } }, + checkDisplayName() { + if (this.displayNameChecking) { + return { + result: t('polls', 'Checking name …'), + status: 'checking', + } + } + + if (this.displayNameUnchanged) { + return { + result: '', + status: '', + } + } + + return { + result: this.displayNameCheckResult, + status: this.displayNameCheckStatus, + } + }, + personalLink() { return window.location.origin + this.$router.resolve({ @@ -177,10 +218,14 @@ export default { emailAddress() { this.emailAddressTemp = this.emailAddress }, + displayName() { + this.displayNameTemp = this.displayName + }, }, created() { this.emailAddressTemp = this.emailAddress + this.displayNameTemp = this.displayName }, methods: { @@ -223,6 +268,27 @@ export default { } }, 500), + validateDisplayName: debounce(async function(value) { + const endpoint = 'apps/polls/check/username' + + this.displayNameTemp = value + try { + this.displayNameChecking = true + await axios.post(generateUrl(endpoint), { + headers: { Accept: 'application/json' }, + userName: this.displayNameTemp, + token: this.$route.params.token, + }) + this.displayNameCheckResult = t('polls', 'valid name.') + this.displayNameCheckStatus = 'success' + } catch { + this.displayNameCheckResult = t('polls', 'Invalid email address.') + this.displayNameCheckStatus = 'error' + } finally { + this.displayNameChecking = false + } + }, 500), + async submitEmailAddress() { try { await this.$store.dispatch('share/updateEmailAddress', { emailAddress: this.emailAddressTemp }) @@ -232,6 +298,15 @@ export default { } }, + async submitDisplayName() { + try { + await this.$store.dispatch('share/updateDisplayName', { displayName: this.displayNameTemp }) + showSuccess(t('polls', 'Name changed.')) + } catch { + showError(t('polls', 'Error changing name.')) + } + }, + async resendInvitation() { try { const response = await this.$store.dispatch('share/resendInvitation') diff --git a/src/js/store/modules/share.js b/src/js/store/modules/share.js index a1c53a8d1..6a4ec7672 100644 --- a/src/js/store/modules/share.js +++ b/src/js/store/modules/share.js @@ -124,6 +124,28 @@ const actions = { } }, + async updateDisplayName(context, payload) { + if (context.rootState.route.name !== 'publicVote') { + return + } + + const endPoint = `apps/polls/s/${context.rootState.route.params.token}/name/${payload.displayName}` + + try { + const response = await axios.put(generateUrl(endPoint), { + headers: { Accept: 'application/json' }, + }) + context.commit('set', { share: response.data.share }) + context.dispatch('poll/get', null, { root: true }) + context.dispatch('comments/list', null, { root: true }) + context.dispatch('votes/list', null, { root: true }) + context.dispatch('options/list', null, { root: true }) + } catch (e) { + console.error('Error changing name', { error: e.response }, { payload }) + throw e + } + }, + async deleteEmailAddress(context, payload) { if (context.rootState.route.name !== 'publicVote') { return