Merge pull request #28001 from nextcloud/backport/27379/stable22

This commit is contained in:
John Molakvoæ 2021-07-16 11:45:08 +02:00 committed by GitHub
commit 0263eea033
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1655 additions and 276 deletions

View file

@ -119,7 +119,10 @@
_registerEvents: function() {
var self = this;
_.each(this._inputFields, function(field) {
if (field === 'avatar') {
if (
field === 'avatar' ||
field === 'email'
) {
return;
}
self.$('#' + field).keyUpDelayedOrEnter(_.bind(self._onInputChanged, self), true);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7,6 +7,7 @@ declare(strict_types=1);
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Christopher Ng <chrng8@gmail.com>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
@ -48,6 +49,8 @@ use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Settings\ISettings;
use OCP\Accounts\IAccountProperty;
use OCP\AppFramework\Services\IInitialState;
class PersonalInfo implements ISettings {
@ -65,6 +68,8 @@ class PersonalInfo implements ISettings {
private $l10nFactory;
/** @var IL10N */
private $l;
/** @var IInitialState */
private $initialStateService;
public function __construct(
IConfig $config,
@ -73,7 +78,8 @@ class PersonalInfo implements ISettings {
IAccountManager $accountManager,
IAppManager $appManager,
IFactory $l10nFactory,
IL10N $l
IL10N $l,
IInitialState $initialStateService
) {
$this->config = $config;
$this->userManager = $userManager;
@ -82,6 +88,7 @@ class PersonalInfo implements ISettings {
$this->appManager = $appManager;
$this->l10nFactory = $l10nFactory;
$this->l = $l;
$this->initialStateService = $initialStateService;
}
public function getForm(): TemplateResponse {
@ -138,6 +145,14 @@ class PersonalInfo implements ISettings {
'groups' => $this->getGroups($user),
] + $messageParameters + $languageParameters + $localeParameters;
$emails = $this->getEmails($account);
$accountParameters = [
'displayNameChangeSupported' => $user->canChangeDisplayName(),
];
$this->initialStateService->provideInitialState('emails', $emails);
$this->initialStateService->provideInitialState('accountParameters', $accountParameters);
return new TemplateResponse('settings', 'settings/personal/personal.info', $parameters, '');
}
@ -180,6 +195,39 @@ class PersonalInfo implements ISettings {
return $groups;
}
/**
* returns the primary email and additional emails in an
* associative array
*
* @param IAccount $account
* @return array
*/
private function getEmails(IAccount $account): array {
$primaryEmail = [
'value' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(),
'scope' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(),
'verified' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getVerified(),
];
$additionalEmails = array_map(
function (IAccountProperty $property) {
return [
'value' => $property->getValue(),
'scope' => $property->getScope(),
'verified' => $property->getVerified(),
];
},
$account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties()
);
$emails = [
'primaryEmail' => $primaryEmail,
'additionalEmails' => $additionalEmails,
];
return $emails;
}
/**
* returns the user language, common language and other languages in an
* associative array

View file

@ -0,0 +1,78 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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 <http://www.gnu.org/licenses/>.
-->
<template>
<button
:disabled="disabled"
@click.stop.prevent="onClick">
<span class="icon icon-add" />
{{ t('settings', 'Add') }}
</button>
</template>
<script>
export default {
name: 'AddButton',
props: {
disabled: {
type: Boolean,
default: true,
},
},
methods: {
onClick(e) {
this.$emit('click', e)
},
},
}
</script>
<style lang="scss" scoped>
button {
height: 44px;
padding: 0 16px;
border: none;
background-color: transparent;
&:hover {
background-color: rgba(127, 127, 127, .15);
}
&:enabled {
opacity: 0.4 !important;
.icon {
opacity: 0.8 !important;
}
}
&:enabled:hover {
background-color: rgba(127, 127, 127, .25);
opacity: 0.8 !important;
}
.icon {
margin-right: 8px;
}
}
</style>

View file

@ -0,0 +1,323 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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 <http://www.gnu.org/licenses/>.
-->
<template>
<div>
<div class="email-container">
<input
ref="email"
type="email"
:name="inputName"
:placeholder="inputPlaceholder"
:value="email"
autocapitalize="none"
autocomplete="on"
autocorrect="off"
required="true"
@input="onEmailChange">
<div class="email-actions-container">
<transition name="fade">
<span v-if="showCheckmarkIcon" class="icon-checkmark" />
<span v-else-if="showErrorIcon" class="icon-error" />
</transition>
<FederationControl v-if="!primary"
class="federation-control"
:disabled="federationDisabled"
:email="email"
:scope.sync="localScope"
@update:scope="onScopeChange" />
<Actions
class="actions-email"
:aria-label="t('settings', 'Email options')"
:disabled="deleteDisabled"
:force-menu="true">
<ActionButton
:aria-label="deleteEmailLabel"
:close-after-click="true"
icon="icon-delete"
@click.stop.prevent="deleteEmail">
{{ deleteEmailLabel }}
</ActionButton>
</Actions>
</div>
</div>
<em v-if="primary">
{{ t('settings', 'Primary email for password reset and notifications') }}
</em>
</div>
</template>
<script>
import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import { showError } from '@nextcloud/dialogs'
import debounce from 'debounce'
import FederationControl from './FederationControl'
import { savePrimaryEmail, saveAdditionalEmail, updateAdditionalEmail, removeAdditionalEmail } from '../../../service/PersonalInfoService'
export default {
name: 'Email',
components: {
Actions,
ActionButton,
FederationControl,
},
props: {
email: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
primary: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
},
data() {
return {
initialEmail: this.email,
localScope: this.scope,
showCheckmarkIcon: false,
showErrorIcon: false,
}
},
computed: {
inputName() {
if (this.primary) {
return 'email'
}
return 'additionalEmail[]'
},
inputPlaceholder() {
if (this.primary) {
return t('settings', 'Your email address')
}
return t('settings', 'Additional email address {index}', { index: this.index + 1 })
},
federationDisabled() {
return !this.initialEmail
},
deleteDisabled() {
return !this.containsNoWhitespace(this.email)
},
deleteEmailLabel() {
if (this.primary) {
return t('settings', 'Remove primary email')
}
return t('settings', 'Delete email')
},
},
methods: {
onEmailChange(e) {
this.$emit('update:email', e.target.value)
// $nextTick() ensures that references to this.email further down the chain give the correct non-outdated value
this.$nextTick(() => this.debounceEmailChange())
},
debounceEmailChange: debounce(async function() {
if ((this.$refs.email?.checkValidity() && this.containsNoWhitespace(this.email)) || this.email === '') {
if (this.primary) {
await this.updatePrimaryEmail()
} else {
if (this.initialEmail && this.email === '') {
await this.deleteAdditionalEmail()
} else if (this.initialEmail === '') {
await this.addAdditionalEmail()
} else {
await this.updateAdditionalEmail()
}
}
}
}, 500),
async deleteEmail() {
if (this.primary) {
this.$emit('update:email', '')
this.$nextTick(async() => await this.updatePrimaryEmail())
} else {
await this.deleteAdditionalEmail()
}
},
async updatePrimaryEmail() {
try {
const responseData = await savePrimaryEmail(this.email)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
if (this.email === '') {
this.handleResponse('error', 'Unable to delete primary email address', e)
} else {
this.handleResponse('error', 'Unable to update primary email address', e)
}
}
},
async addAdditionalEmail() {
try {
const responseData = await saveAdditionalEmail(this.email)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to add additional email address', e)
}
},
async updateAdditionalEmail() {
try {
const responseData = await updateAdditionalEmail(this.initialEmail, this.email)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to update additional email address', e)
}
},
async deleteAdditionalEmail() {
try {
const responseData = await removeAdditionalEmail(this.initialEmail)
this.handleDeleteAdditionalEmail(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to delete additional email address', e)
}
},
containsNoWhitespace(string) {
return /^\S+$/.test(string)
},
handleDeleteAdditionalEmail(status) {
if (status === 'ok') {
this.$emit('deleteAdditionalEmail')
} else {
this.handleResponse('error', 'Unable to delete additional email address', {})
}
},
handleResponse(status, errorMessage, error) {
if (status === 'ok') {
// Ensure that local initialEmail state reflects server state
this.initialEmail = this.email
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
showError(t('settings', errorMessage))
this.logger.error(errorMessage, error)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
}
},
onScopeChange(scope) {
this.$emit('update:scope', scope)
},
},
}
</script>
<style lang="scss" scoped>
.email-container {
display: grid;
align-items: center;
input[type=email] {
grid-area: 1 / 1;
}
.email-actions-container {
grid-area: 1 / 1;
justify-self: flex-end;
height: 30px;
display: flex;
gap: 0 2px;
margin-right: 5px;
.actions-email {
opacity: 0.4 !important;
&:hover {
opacity: 0.8 !important;
}
&::v-deep button {
height: 30px !important;
min-height: 30px !important;
width: 30px !important;
min-width: 30px !important;
}
}
.federation-control {
&::v-deep button {
// TODO remove this hack
padding-bottom: 7px;
height: 30px !important;
min-height: 30px !important;
width: 30px !important;
min-width: 30px !important;
}
}
.icon-checkmark,
.icon-error {
height: 30px !important;
min-height: 30px !important;
width: 30px !important;
min-width: 30px !important;
top: 0;
right: 0;
float: none;
}
}
}
.fade-enter-active {
transition: opacity 200ms ease-out;
}
.fade-leave-active {
transition: opacity 300ms ease-out;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View file

@ -0,0 +1,180 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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 <http://www.gnu.org/licenses/>.
-->
<template>
<form
ref="form"
class="section"
@submit.stop.prevent="() => {}">
<HeaderBar
:can-edit-emails="isDisplayNameChangeSupported"
:is-valid-form="isValidForm"
:scope.sync="primaryEmail.scope"
@addAdditionalEmail="onAddAdditionalEmail" />
<template v-if="isDisplayNameChangeSupported">
<Email
:primary="true"
:scope.sync="primaryEmail.scope"
:email.sync="primaryEmail.value"
@update:email="onUpdateEmail" />
<Email v-for="(additionalEmail, index) in additionalEmails"
:key="index"
:index="index"
:scope.sync="additionalEmail.scope"
:email.sync="additionalEmail.value"
@update:email="onUpdateEmail"
@deleteAdditionalEmail="onDeleteAdditionalEmail(index)" />
</template>
<span v-else>
{{ primaryEmail.value || t('settings', 'No email address set') }}
</span>
</form>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import { showError } from '@nextcloud/dialogs'
import '@nextcloud/dialogs/styles/toast.scss'
import HeaderBar from './HeaderBar'
import Email from './Email'
import { savePrimaryEmail, removeAdditionalEmail } from '../../../service/PersonalInfoService'
import { DEFAULT_ADDITIONAL_EMAIL_SCOPE } from '../../../constants/AccountPropertyConstants'
const { additionalEmails, primaryEmail } = loadState('settings', 'emails', {})
const accountParams = loadState('settings', 'accountParameters', {})
export default {
name: 'EmailSection',
components: {
HeaderBar,
Email,
},
data() {
return {
accountParams,
additionalEmails,
primaryEmail,
isValidForm: true,
}
},
computed: {
isDisplayNameChangeSupported() {
return this.accountParams.displayNameChangeSupported
},
primaryEmailValue: {
get() {
return this.primaryEmail.value
},
set(value) {
this.primaryEmail.value = value
},
},
firstAdditionalEmail() {
if (this.additionalEmails.length) {
return this.additionalEmails[0].value
}
return null
},
},
mounted() {
this.$nextTick(() => this.updateFormValidity())
},
methods: {
onAddAdditionalEmail() {
if (this.$refs.form?.checkValidity()) {
this.additionalEmails.push({ value: '', scope: DEFAULT_ADDITIONAL_EMAIL_SCOPE })
this.$nextTick(() => this.updateFormValidity())
}
},
onDeleteAdditionalEmail(index) {
this.$delete(this.additionalEmails, index)
},
async onUpdateEmail() {
this.$nextTick(() => this.updateFormValidity())
if (this.primaryEmailValue === '' && this.firstAdditionalEmail) {
const deletedEmail = this.firstAdditionalEmail
await this.deleteFirstAdditionalEmail()
this.primaryEmailValue = deletedEmail
await this.updatePrimaryEmail()
this.$nextTick(() => this.updateFormValidity())
}
},
async updatePrimaryEmail() {
try {
const responseData = await savePrimaryEmail(this.primaryEmailValue)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to update primary email address', e)
}
},
async deleteFirstAdditionalEmail() {
try {
const responseData = await removeAdditionalEmail(this.firstAdditionalEmail)
this.handleDeleteFirstAdditionalEmail(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to delete additional email address', e)
}
},
handleDeleteFirstAdditionalEmail(status) {
if (status === 'ok') {
this.$delete(this.additionalEmails, 0)
} else {
this.handleResponse('error', 'Unable to delete additional email address', {})
}
},
handleResponse(status, errorMessage, error) {
if (status !== 'ok') {
showError(t('settings', errorMessage))
this.logger.error(errorMessage, error)
}
},
updateFormValidity() {
this.isValidForm = this.$refs.form?.checkValidity()
},
},
}
</script>
<style lang="scss" scoped>
form::v-deep button {
&:disabled {
cursor: default;
}
}
</style>

View file

@ -0,0 +1,160 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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 <http://www.gnu.org/licenses/>.
-->
<template>
<Actions
class="actions-federation"
:aria-label="t('settings', 'Change privacy level of email')"
:default-icon="scopeIcon"
:disabled="disabled">
<ActionButton v-for="federationScope in federationScopes"
:key="federationScope.name"
class="forced-action"
:class="{ 'forced-active': scope === federationScope.name }"
:aria-label="federationScope.tooltip"
:close-after-click="true"
:icon="federationScope.iconClass"
:title="federationScope.displayName"
@click.stop.prevent="changeScope(federationScope.name)">
{{ federationScope.tooltip }}
</ActionButton>
</Actions>
</template>
<script>
import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import { showError } from '@nextcloud/dialogs'
import { SCOPE_ENUM, SCOPE_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryEmailScope, saveAdditionalEmailScope } from '../../../service/PersonalInfoService'
// TODO hardcoded for email, should abstract this for other sections
const excludedScopes = [SCOPE_ENUM.PRIVATE]
export default {
name: 'FederationControl',
components: {
Actions,
ActionButton,
},
props: {
primary: {
type: Boolean,
default: false,
},
email: {
type: String,
default: '',
},
scope: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
initialScope: this.scope,
federationScopes: Object.values(SCOPE_PROPERTY_ENUM).filter(({ name }) => !excludedScopes.includes(name)),
}
},
computed: {
scopeIcon() {
return SCOPE_PROPERTY_ENUM[this.scope].iconClass
},
},
methods: {
async changeScope(scope) {
this.$emit('update:scope', scope)
this.$nextTick(async() => {
if (this.primary) {
await this.updatePrimaryEmailScope()
} else {
await this.updateAdditionalEmailScope()
}
})
},
async updatePrimaryEmailScope() {
try {
const responseData = await savePrimaryEmailScope(this.scope)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to update federation scope of the primary email', e)
}
},
async updateAdditionalEmailScope() {
try {
const responseData = await saveAdditionalEmailScope(this.email, this.scope)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to update federation scope of additional email', e)
}
},
handleResponse(status, errorMessage, error) {
if (status === 'ok') {
this.initialScope = this.scope
} else {
this.$emit('update:scope', this.initialScope)
showError(t('settings', errorMessage))
this.logger.error(errorMessage, error)
}
},
},
}
</script>
<style lang="scss" scoped>
.actions-federation {
opacity: 0.4 !important;
&:hover {
opacity: 0.8 !important;
}
}
.forced-active {
background-color: var(--color-primary-light) !important;
box-shadow: inset 2px 0 var(--color-primary) !important;
}
.forced-action {
&::v-deep p {
width: 150px !important;
padding: 8px 0 !important;
color: var(--color-main-text) !important;
font-size: 12.8px !important;
line-height: 1.5em !important;
}
}
</style>

View file

@ -0,0 +1,94 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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 <http://www.gnu.org/licenses/>.
-->
<template>
<h3>
<label for="email">
{{ t('settings', 'Email') }}
</label>
<FederationControl
class="federation-control"
:primary="true"
:scope.sync="localScope"
@update:scope="onScopeChange" />
<AddButton v-if="canEditEmails"
class="add-button"
:disabled="!isValidForm"
@click.stop.prevent="addAdditionalEmail" />
</h3>
</template>
<script>
import FederationControl from './FederationControl'
import AddButton from './AddButton'
export default {
name: 'HeaderBar',
components: {
FederationControl,
AddButton,
},
props: {
canEditEmails: {
type: Boolean,
default: true,
},
isValidForm: {
type: Boolean,
default: true,
},
scope: {
type: String,
required: true,
},
},
data() {
return {
localScope: this.scope,
}
},
methods: {
addAdditionalEmail() {
this.$emit('addAdditionalEmail')
},
onScopeChange(scope) {
this.$emit('update:scope', scope)
},
},
}
</script>
<style lang="scss" scoped>
.federation-control {
margin: -12px 0 0 8px;
}
.add-button {
margin: -12px 0 0 auto !important;
}
</style>

View file

@ -0,0 +1,83 @@
/**
* @copyright 2021, Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
/*
* SYNC to be kept in sync with lib/public/Accounts/IAccountManager.php
*/
/** Enum of account properties */
export const ACCOUNT_PROPERTY_ENUM = Object.freeze({
AVATAR: 'avatar',
DISPLAYNAME: 'displayname',
PHONE: 'phone',
EMAIL: 'email',
WEBSITE: 'website',
ADDRESS: 'address',
TWITTER: 'twitter',
EMAIL_COLLECTION: 'additional_mail',
})
/** Enum of scopes */
export const SCOPE_ENUM = Object.freeze({
PRIVATE: 'v2-private',
LOCAL: 'v2-local',
FEDERATED: 'v2-federated',
PUBLISHED: 'v2-published',
})
/** Scope suffix */
export const SCOPE_SUFFIX = 'Scope'
/** Default additional email scope */
export const DEFAULT_ADDITIONAL_EMAIL_SCOPE = SCOPE_ENUM.LOCAL
/**
* Enum of scope names to properties
*
* *Used for federation control*
*/
export const SCOPE_PROPERTY_ENUM = Object.freeze({
[SCOPE_ENUM.PRIVATE]: {
name: SCOPE_ENUM.PRIVATE,
displayName: t('settings', 'Private'),
tooltip: t('settings', 'Only visible to people matched via phone number integration through Talk on mobile'),
iconClass: 'icon-phone',
},
[SCOPE_ENUM.LOCAL]: {
name: SCOPE_ENUM.LOCAL,
displayName: t('settings', 'Local'),
tooltip: t('settings', 'Only visible to people on this instance and guests'),
iconClass: 'icon-password',
},
[SCOPE_ENUM.FEDERATED]: {
name: SCOPE_ENUM.FEDERATED,
displayName: t('settings', 'Federated'),
tooltip: t('settings', 'Only synchronize to trusted servers'),
iconClass: 'icon-contacts-dark',
},
[SCOPE_ENUM.PUBLISHED]: {
name: SCOPE_ENUM.PUBLISHED,
displayName: t('settings', 'Published'),
tooltip: t('settings', 'Synchronize to trusted servers and the global and public address book'),
iconClass: 'icon-link',
},
})

View file

@ -0,0 +1,38 @@
/**
* @copyright 2021, Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import logger from './logger'
import EmailSection from './components/PersonalInfo/EmailSection/EmailSection'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken)
Vue.prototype.t = t
Vue.prototype.logger = logger
const View = Vue.extend(EmailSection)
export default new View({
el: '#vue-emailsection',
})

View file

@ -0,0 +1,153 @@
/**
* @copyright 2021, Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
import axios from '@nextcloud/axios'
import { getCurrentUser } from '@nextcloud/auth'
import { generateOcsUrl } from '@nextcloud/router'
import confirmPassword from '@nextcloud/password-confirmation'
import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../constants/AccountPropertyConstants'
/**
* Save the primary email of the user
*
* @param {string} email the primary email
* @returns {Object}
*/
export const savePrimaryEmail = async(email) => {
const userId = getCurrentUser().uid
// TODO upgrade @nextcloud/router to v2.0 so we can remove the .slice() trailing slash hacks (same below)
const url = generateOcsUrl(`cloud/users/${userId}`, 2).slice(0, -1)
await confirmPassword()
const res = await axios.put(url, {
key: ACCOUNT_PROPERTY_ENUM.EMAIL,
value: email,
})
return res.data
}
/**
* Save an additional email of the user
*
* *Will be appended to the user's additional emails*
*
* @param {string} email the additional email
* @returns {Object}
*/
export const saveAdditionalEmail = async(email) => {
const userId = getCurrentUser().uid
const url = generateOcsUrl(`cloud/users/${userId}`, 2).slice(0, -1)
await confirmPassword()
const res = await axios.put(url, {
key: ACCOUNT_PROPERTY_ENUM.EMAIL_COLLECTION,
value: email,
})
return res.data
}
/**
* Remove an additional email of the user
*
* @param {string} email the additional email
* @returns {Object}
*/
export const removeAdditionalEmail = async(email) => {
const userId = getCurrentUser().uid
const url = generateOcsUrl(`cloud/users/${userId}/${ACCOUNT_PROPERTY_ENUM.EMAIL_COLLECTION}`, 2).slice(0, -1)
await confirmPassword()
const res = await axios.put(url, {
key: email,
value: '',
})
return res.data
}
/**
* Update an additional email of the user
*
* @param {string} prevEmail the additional email to be updated
* @param {string} newEmail the new additional email
* @returns {Object}
*/
export const updateAdditionalEmail = async(prevEmail, newEmail) => {
const userId = getCurrentUser().uid
const url = generateOcsUrl(`cloud/users/${userId}/${ACCOUNT_PROPERTY_ENUM.EMAIL_COLLECTION}`, 2).slice(0, -1)
await confirmPassword()
const res = await axios.put(url, {
key: prevEmail,
value: newEmail,
})
return res.data
}
/**
* Save the federation scope for the primary email of the user
*
* @param {string} scope the federation scope
* @returns {Object}
*/
export const savePrimaryEmailScope = async(scope) => {
const userId = getCurrentUser().uid
const url = generateOcsUrl(`cloud/users/${userId}`, 2).slice(0, -1)
await confirmPassword()
const res = await axios.put(url, {
key: `${ACCOUNT_PROPERTY_ENUM.EMAIL}${SCOPE_SUFFIX}`,
value: scope,
})
return res.data
}
/**
* Save the federation scope for the additional email of the user
*
* @param {string} email the additional email
* @param {string} scope the federation scope
* @returns {Object}
*/
export const saveAdditionalEmailScope = async(email, scope) => {
const userId = getCurrentUser().uid
const url = generateOcsUrl(`cloud/users/${userId}/${ACCOUNT_PROPERTY_ENUM.EMAIL_COLLECTION}${SCOPE_SUFFIX}`, 2).slice(0, -1)
await confirmPassword()
const res = await axios.put(url, {
key: email,
value: scope,
})
return res.data
}

View file

@ -31,6 +31,7 @@ script('settings', [
'federationsettingsview',
'federationscopemenu',
'settings/personalInfo',
'vue-settings-personal-info',
]);
?>
@ -126,52 +127,7 @@ script('settings', [
</form>
</div>
<div class="personal-settings-setting-box">
<form id="emailform" class="section">
<h3>
<label for="email"><?php p($l->t('Email')); ?></label>
<a href="#" class="federation-menu" aria-label="<?php p($l->t('Change privacy level of email')); ?>">
<span class="icon-federation-menu icon-password">
<span class="icon-triangle-s"></span>
</span>
</a>
</h3>
<div class="verify <?php if ($_['email'] === '' || $_['emailScope'] !== 'public') {
p('hidden');
} ?>">
<img id="verify-email" title="<?php p($_['emailMessage']); ?>" data-status="<?php p($_['emailVerification']) ?>" src="
<?php
switch ($_['emailVerification']) {
case \OC\Accounts\AccountManager::VERIFICATION_IN_PROGRESS:
p(image_path('core', 'actions/verifying.svg'));
break;
case \OC\Accounts\AccountManager::VERIFIED:
p(image_path('core', 'actions/verified.svg'));
break;
default:
p(image_path('core', 'actions/verify.svg'));
}
?>">
</div>
<input type="email" name="email" id="email" value="<?php p($_['email']); ?>"
<?php if (!$_['displayNameChangeSupported']) {
print_unescaped('class="hidden"');
} ?>
placeholder="<?php p($l->t('Your email address')); ?>"
autocomplete="on" autocapitalize="none" autocorrect="off" />
<span class="icon-checkmark hidden"></span>
<span class="icon-error hidden" ></span>
<?php if (!$_['displayNameChangeSupported']) { ?>
<span><?php if (isset($_['email']) && !empty($_['email'])) {
p($_['email']);
} else {
p($l->t('No email address set'));
}?></span>
<?php } ?>
<?php if ($_['displayNameChangeSupported']) { ?>
<em><?php p($l->t('For password reset and notifications')); ?></em>
<?php } ?>
<input type="hidden" id="emailscope" value="<?php p($_['emailScope']) ?>">
</form>
<div id="vue-emailsection" class="section"></div>
</div>
<div class="personal-settings-setting-box">
<form id="phoneform" class="section">
@ -223,8 +179,8 @@ script('settings', [
</h3>
<?php if ($_['lookupServerUploadEnabled']) { ?>
<div class="verify <?php if ($_['website'] === '' || $_['websiteScope'] !== 'public') {
p('hidden');
} ?>">
p('hidden');
} ?>">
<img id="verify-website" title="<?php p($_['websiteMessage']); ?>" data-status="<?php p($_['websiteVerification']) ?>" src="
<?php
switch ($_['websiteVerification']) {

View file

@ -2,6 +2,7 @@
* @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Christopher Ng <chrng8@gmail.com>
* @author Jan C. Borchardt <hey@jancborchardt.net>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
@ -25,13 +26,26 @@
const path = require('path')
// TODO use @nextcloud/webpack-vue-config
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf)$/,
loader: 'url-loader',
options: {
name: '[name].[ext]?[hash]',
},
},
]
},
entry: {
'settings-apps-users-management': path.join(__dirname, 'src', 'main-apps-users-management'),
'settings-admin-security': path.join(__dirname, 'src', 'main-admin-security'),
'settings-personal-security': path.join(__dirname, 'src', 'main-personal-security'),
'settings-personal-webauthn': path.join(__dirname, 'src', 'main-personal-webauth'),
'settings-nextcloud-pdf': path.join(__dirname, 'src', 'main-nextcloud-pdf'),
'settings-personal-info': path.join(__dirname, 'src', 'main-personal-info'),
},
output: {
path: path.resolve(__dirname, './js'),