Merge branch 'feature/confirm-user-resend-confirmation' into 'develop'

Confirm user account, resend confirmation email

Closes #49

See merge request pleroma/admin-fe!68
This commit is contained in:
Maxim Filippov 2019-11-30 15:58:17 +00:00
commit 4d7889d76a
65 changed files with 145 additions and 33 deletions

View file

@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Relay management
- Ability to fetch all statuses from a given instance
- Grouped reports: now you can view reports, which are grouped by status (pagination is not implemented yet, though)
- Ability to confirm users' emails and resend confirmation emails
### Fixed

View file

@ -136,4 +136,24 @@ export async function fetchUserStatuses(id, authHost, godmode, token) {
})
}
export async function confirmUserEmail(nicknames, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: '/api/pleroma/admin/users/confirm_email',
method: 'patch',
headers: authHeaders(token),
data: { nicknames }
})
}
export async function resendConfirmationEmail(nicknames, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: '/api/pleroma/admin/users/resend_confirmation_email',
method: 'patch',
headers: authHeaders(token),
data: { nicknames }
})
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}

View file

@ -1,5 +1,5 @@
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'// svg组件
import SvgIcon from '@/components/element-ui/SvgIcon'// svg组件
// register globally
Vue.component('svg-icon', SvgIcon)

View file

@ -104,7 +104,7 @@ export default {
},
components: {
documentation: 'Documentation',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/element-ui/Dropzone.',
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
backToTopTips2: 'You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally',
@ -175,6 +175,7 @@ export default {
external: 'external',
deactivated: 'deactivated',
active: 'active',
unconfirmed: 'unconfirmed',
actions: 'Actions',
activate: 'Activate',
deactivate: 'Deactivate',
@ -213,6 +214,8 @@ export default {
addTagForMultipleUsersConfirmation: 'Are you sure you want to apply tag to all selected users?',
removeTagFromMultipleUsersConfirmation: 'Are you sure you want to remove tag from all selected users?',
requirePasswordResetConfirmation: 'Are you sure you want to require password reset for all selected users?',
confirmAccountsConfirmation: 'Are you sure you want to confirm emails for all selected users?',
resendEmailConfirmation: 'Are you sure you want to resend confirmation email for all selected users?',
mailerMustBeEnabled: 'To require user\'s password reset you must enable mailer.',
ok: 'Okay',
completed: 'Completed',
@ -230,7 +233,11 @@ export default {
invalidNicknameError: 'Username can include "a-z", "A-Z" and "0-9" characters',
getPasswordResetToken: 'Get password reset token',
passwordResetTokenCreated: 'Password reset token was created',
accountCreated: 'New account was created!'
accountCreated: 'New account was created!',
unconfirmedEmail: 'User didn\'t confirm the email',
confirmAccount: 'Confirm account',
confirmAccounts: 'Confirm accounts',
resendConfirmation: 'Resend confirmation email'
},
statuses: {
statuses: 'Statuses',

View file

@ -95,7 +95,7 @@ export default {
},
components: {
documentation: 'Documentación',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/element-ui/Dropzone.',
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
backToTopTips2: 'You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally',

View file

@ -96,7 +96,7 @@ export default {
},
components: {
documentation: 'Documentacion',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/element-ui/Dropzone.',
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
backToTopTips2: 'You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally',

View file

@ -95,7 +95,7 @@ export default {
},
components: {
documentation: '文档',
dropzoneTips: '由于我司业务有特殊需求,而且要传七牛 所以没用第三方,选择了自己封装。代码非常的简单,具体代码你可以在这里看到 @/components/Dropzone',
dropzoneTips: '由于我司业务有特殊需求,而且要传七牛 所以没用第三方,选择了自己封装。代码非常的简单,具体代码你可以在这里看到 @/components/element-ui/Dropzone',
stickyTips: '当页面滚动到预设的位置会吸附在顶部',
backToTopTips1: '页面滚动到指定位置会在右下角出现返回顶部按钮',
backToTopTips2: '可自定义按钮的样式、show/hide、出现的高度、返回的位置 如需文字提示可在外部使用Element的el-tooltip元素',

View file

@ -12,7 +12,9 @@ import {
searchUsers,
tagUser,
untagUser,
requirePasswordReset
requirePasswordReset,
confirmUserEmail,
resendConfirmationEmail
} from '@/api/users'
const users = {
@ -151,6 +153,31 @@ const users = {
}
dispatch('SuccessMessage')
},
async ConfirmUsersEmail({ commit, dispatch, getters, state }, users) {
const updatedUsers = users.map(user => {
return { ...user, confirmation_pending: false }
})
commit('SWAP_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
try {
await confirmUserEmail(usersNicknames, getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('SuccessMessage')
},
async ResendConfirmationEmail({ dispatch, getters }, users) {
const usersNicknames = users.map(user => user.nickname)
try {
await resendConfirmationEmail(usersNicknames, getters.authHost, getters.token)
} catch (_e) {
return
}
dispatch('SuccessMessage')
},
async DeleteRight({ commit, dispatch, getters, state }, { users, right }) {
const updatedUsers = users.map(user => {
return user.local ? { ...user, roles: { ...user.roles, [right]: false }} : user

View file

@ -5,7 +5,7 @@
</template>
<script>
import Chart from '@/components/Charts/keyboard'
import Chart from '@/components/element-ui/Charts/keyboard'
export default {
name: 'KeyboardChart',

View file

@ -5,7 +5,7 @@
</template>
<script>
import Chart from '@/components/Charts/lineMarker'
import Chart from '@/components/element-ui/Charts/lineMarker'
export default {
name: 'LineChart',

View file

@ -5,7 +5,7 @@
</template>
<script>
import Chart from '@/components/Charts/mixChart'
import Chart from '@/components/element-ui/Charts/mixChart'
export default {
name: 'MixChart',

View file

@ -28,8 +28,8 @@
<script>
import { mapGetters } from 'vuex'
import PanThumb from '@/components/PanThumb'
import Mallki from '@/components/TextHoverEffect/Mallki'
import PanThumb from '@/components/element-ui/PanThumb'
import Mallki from '@/components/element-ui/TextHoverEffect/Mallki'
export default {
components: { PanThumb, Mallki },

View file

@ -43,7 +43,7 @@
</template>
<script>
import GithubCorner from '@/components/GithubCorner'
import GithubCorner from '@/components/element-ui/GithubCorner'
import PanelGroup from './components/PanelGroup'
import LineChart from './components/LineChart'
import RaddarChart from './components/RaddarChart'

View file

@ -18,8 +18,8 @@
<script>
import { mapGetters } from 'vuex'
import PanThumb from '@/components/PanThumb'
import GithubCorner from '@/components/GithubCorner'
import PanThumb from '@/components/element-ui/PanThumb'
import GithubCorner from '@/components/element-ui/GithubCorner'
export default {
name: 'DashboardEditor',

View file

@ -7,7 +7,7 @@
</div>
</template>
<script>
import DropdownMenu from '@/components/Share/dropdownMenu'
import DropdownMenu from '@/components/element-ui/Share/dropdownMenu'
export default {
name: 'Documentation',

View file

@ -18,7 +18,7 @@
<script>
import { mapGetters } from 'vuex'
import Hamburger from '@/components/Hamburger'
import Hamburger from '@/components/element-ui/Hamburger'
export default {
components: {

View file

@ -26,7 +26,7 @@
</template>
<script>
import ScrollPane from '@/components/ScrollPane'
import ScrollPane from '@/components/element-ui/ScrollPane'
import { generateTitle } from '@/utils/i18n'
import path from 'path'

View file

@ -49,7 +49,7 @@
</template>
<script>
import SvgIcon from '@/components/SvgIcon'
import SvgIcon from '@/components/element-ui/SvgIcon'
import localforage from 'localforage'
import _ from 'lodash'
import i18n from '@/lang'

View file

@ -64,7 +64,7 @@
import moment from 'moment'
import ModerateUserDropdown from './ModerateUserDropdown'
import ReportCard from './ReportCard'
import Status from '../../status/Status'
import Status from '@/components/Status'
export default {
name: 'Report',

View file

@ -81,7 +81,7 @@
<script>
import moment from 'moment'
import Status from '../../status/Status'
import Status from '@/components/Status'
import ModerateUserDropdown from './ModerateUserDropdown'
export default {

View file

@ -21,7 +21,7 @@
<script>
import { mapGetters } from 'vuex'
import Status from '../status/Status'
import Status from '@/components/Status'
export default {
name: 'Statuses',

View file

@ -26,6 +26,15 @@
@click.native="revokeRightFromMultipleUsers('moderator')">
{{ $t('users.revokeModerator') }}
</el-dropdown-item>
<el-dropdown-item
divided
@click.native="confirmAccountsForMultipleUsers">
{{ $t('users.confirmAccounts') }}
</el-dropdown-item>
<el-dropdown-item
@click.native="resendConfirmationForMultipleUsers">
{{ $t('users.resendConfirmation') }}
</el-dropdown-item>
<el-dropdown-item
divided
@click.native="activateMultipleUsers">
@ -209,6 +218,18 @@ export default {
const filtered = this.selectedUsers.filter(user => user.local)
filtered.map(user => this.$store.dispatch('RequirePasswordReset', user))
this.$emit('apply-action')
},
confirmAccounts: () => {
const filtered = this.selectedUsers.filter(user => user.local && user.confirmation_pending)
const confirmAccountFn = async(users) => await this.$store.dispatch('ConfirmUsersEmail', users)
applyAction(filtered, confirmAccountFn)
},
resendConfirmation: () => {
const filtered = this.selectedUsers.filter(user => user.local && user.confirmation_pending)
const resendConfirmationFn = async(users) => await this.$store.dispatch('ResendConfirmationEmail', users)
applyAction(filtered, resendConfirmationFn)
}
}
},
@ -276,6 +297,20 @@ export default {
removeTag(tag)
)
},
confirmAccountsForMultipleUsers() {
const { confirmAccounts } = this.mappers()
this.confirmMessage(
this.$t('users.confirmAccountsConfirmation'),
confirmAccounts
)
},
resendConfirmationForMultipleUsers() {
const { resendConfirmation } = this.mappers()
this.confirmMessage(
this.$t('users.resendEmailConfirmation'),
resendConfirmation
)
},
confirmMessage(message, applyAction) {
this.$confirm(message, {
confirmButtonText: this.$t('users.ok'),

View file

@ -57,6 +57,11 @@
<el-tag v-if="scope.row.roles.moderator">
<span>{{ isDesktop ? $t('users.moderator') : getFirstLetter($t('users.moderator')) }}</span>
</el-tag>
<el-tooltip :content="$t('users.unconfirmedEmail')" effect="dark">
<el-tag v-if="scope.row.confirmation_pending" type="info">
{{ isDesktop ? $t('users.unconfirmed') : getFirstLetter($t('users.unconfirmed')) }}
</el-tag>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('users.actions')" fixed="right">
@ -88,6 +93,17 @@
@click.native="handleDeletion(scope.row)">
{{ $t('users.deleteAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.local && scope.row.confirmation_pending"
divided
@click.native="handleEmailConfirmation(scope.row)">
{{ $t('users.confirmAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.local && scope.row.confirmation_pending"
@click.native="handleConfirmationResend(scope.row)">
{{ $t('users.resendConfirmation') }}
</el-dropdown-item>
<el-dropdown-item
:divided="showAdminAction(scope.row)"
:class="{ 'active-tag': scope.row.tags.includes('force_nsfw') }"
@ -301,6 +317,12 @@ export default {
user.roles[right]
? this.$store.dispatch('DeleteRight', { users: [user], right })
: this.$store.dispatch('AddRight', { users: [user], right })
},
handleEmailConfirmation(user) {
this.$store.dispatch('ConfirmUsersEmail', [user])
},
handleConfirmationResend(user) {
this.$store.dispatch('ResendConfirmationEmail', [user])
}
}
}

View file

@ -83,7 +83,7 @@
</template>
<script>
import Status from '../status/Status'
import Status from '@/components/Status'
export default {
name: 'UsersShow',

View file

@ -1,7 +1,7 @@
import Vuex from 'vuex'
import { mount, createLocalVue, config } from '@vue/test-utils'
import Element from 'element-ui'
import Status from '@/views/status/Status'
import Status from '@/components/Status'
import storeConfig from './store.conf'
import { cloneDeep } from 'lodash'
import flushPromises from 'flush-promises'

View file

@ -163,7 +163,7 @@ describe('Apply users actions to multiple users', () => {
const activateMultipleUsersStub = jest.fn()
wrapper.setMethods({ activateMultipleUsers: activateMultipleUsersStub })
wrapper.find(`.el-dropdown-menu__item:nth-child(5)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(7)`).trigger('click')
expect(wrapper.vm.activateMultipleUsers).toHaveBeenCalled()
const activate = wrapper.vm.mappers().activate
@ -190,7 +190,7 @@ describe('Apply users actions to multiple users', () => {
const deactivateMultipleUsersStub = jest.fn()
wrapper.setMethods({ deactivateMultipleUsers: deactivateMultipleUsersStub })
wrapper.find(`.el-dropdown-menu__item:nth-child(6)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(8)`).trigger('click')
expect(wrapper.vm.deactivateMultipleUsers).toHaveBeenCalled()
const deactivate = wrapper.vm.mappers().deactivate
@ -221,7 +221,7 @@ describe('Apply users actions to multiple users', () => {
const deleteMultipleUsersStub = jest.fn()
wrapper.setMethods({ deleteMultipleUsers: deleteMultipleUsersStub })
wrapper.find(`.el-dropdown-menu__item:nth-child(7)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(9)`).trigger('click')
expect(wrapper.vm.deleteMultipleUsers).toHaveBeenCalled()
const remove = wrapper.vm.mappers().remove
@ -247,15 +247,15 @@ describe('Apply users actions to multiple users', () => {
const addTagForMultipleUsersStub = jest.fn()
wrapper.setMethods({ addTagForMultipleUsers: addTagForMultipleUsersStub })
wrapper.find(`.el-dropdown-menu__item:nth-child(9) button:nth-child(1)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(11) button:nth-child(1)`).trigger('click')
expect(wrapper.vm.addTagForMultipleUsers).toHaveBeenCalled()
expect(wrapper.vm.addTagForMultipleUsers).toHaveBeenCalledWith('force_nsfw')
wrapper.find(`.el-dropdown-menu__item:nth-child(11) button:nth-child(1)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(13) button:nth-child(1)`).trigger('click')
expect(wrapper.vm.addTagForMultipleUsers).toHaveBeenCalled()
expect(wrapper.vm.addTagForMultipleUsers).toHaveBeenCalledWith('force_unlisted')
wrapper.find(`.el-dropdown-menu__item:nth-child(13) button:nth-child(1)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(15 ) button:nth-child(1)`).trigger('click')
expect(wrapper.vm.addTagForMultipleUsers).toHaveBeenCalled()
expect(wrapper.vm.addTagForMultipleUsers).toHaveBeenCalledWith('disable_remote_subscription')
@ -287,15 +287,15 @@ describe('Apply users actions to multiple users', () => {
const removeTagFromMultipleUsersStub = jest.fn()
wrapper.setMethods({ removeTagFromMultipleUsers: removeTagFromMultipleUsersStub })
wrapper.find(`.el-dropdown-menu__item:nth-child(10) button:nth-child(2)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(12) button:nth-child(2)`).trigger('click')
expect(wrapper.vm.removeTagFromMultipleUsers).toHaveBeenCalled()
expect(wrapper.vm.removeTagFromMultipleUsers).toHaveBeenCalledWith('strip_media')
wrapper.find(`.el-dropdown-menu__item:nth-child(12) button:nth-child(2)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(14) button:nth-child(2)`).trigger('click')
expect(wrapper.vm.removeTagFromMultipleUsers).toHaveBeenCalled()
expect(wrapper.vm.removeTagFromMultipleUsers).toHaveBeenCalledWith('sandbox')
wrapper.find(`.el-dropdown-menu__item:nth-child(14) button:nth-child(2)`).trigger('click')
wrapper.find(`.el-dropdown-menu__item:nth-child(16) button:nth-child(2)`).trigger('click')
expect(wrapper.vm.removeTagFromMultipleUsers).toHaveBeenCalled()
expect(wrapper.vm.removeTagFromMultipleUsers).toHaveBeenCalledWith('disable_any_subscription')