Merge pull request #115 from owncloud/feature/create-delete-accounts

Add create/delete capabilities and UI
This commit is contained in:
Benedikt Kulmann
2020-09-09 21:16:09 +02:00
committed by GitHub
17 changed files with 1450 additions and 1888 deletions

View File

@@ -0,0 +1,6 @@
Enhancement: Add create account form
We've added a form to create new users above the accounts list.
https://github.com/owncloud/product/issues/148
https://github.com/owncloud/ocis-accounts/pull/115

View File

@@ -0,0 +1,6 @@
Enhancement: Add delete accounts action
We've added an action into the actions dropdown to enable admins to delete users.
https://github.com/owncloud/product/issues/148
https://github.com/owncloud/ocis-accounts/pull/115

6
go.sum
View File

@@ -912,6 +912,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -1042,7 +1043,9 @@ github.com/micro/micro/v2 v2.0.1-0.20200210100719-f38a1d8d5348/go.mod h1:MQBt/cB
github.com/micro/micro/v2 v2.5.1-0.20200418121137-24e9b206767c/go.mod h1:fqqaYbJGYzSBi7Ms2Adly7Xzw9+WIRBAucUjwGmYeFY=
github.com/micro/micro/v2 v2.8.0 h1:AMqpnKsOBnuGHjU0jVmTL17BRdsOx0FbvI/Gkl2uLrA=
github.com/micro/micro/v2 v2.8.0/go.mod h1:VTIGqEBLAMh22q72DnGd95iJSQY/3yvXd9GIIooQ69c=
github.com/micro/protoc-gen-micro v1.0.0 h1:qKh5S3I1RfenhIs5mqDFJLwRlRDlgin7XWiUKZbpwLM=
github.com/micro/protoc-gen-micro v1.0.0/go.mod h1:C8ij4DJhapBmypcT00AXdb0cZ675/3PqUO02buWWqbE=
github.com/micro/protoc-gen-micro/v2 v2.3.0 h1:PBbGeNh4BOy1w4eRdeo4yWJJNWGLnaJX6/h55I74EXE=
github.com/micro/protoc-gen-micro/v2 v2.3.0/go.mod h1:gcsUvKSTTTalq+pqdUbFS40OTsURpYgL5+yUguR1djk=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
@@ -1157,6 +1160,7 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
@@ -1164,6 +1168,7 @@ github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@@ -1979,6 +1984,7 @@ google.golang.org/genproto v0.0.0-20200624020401-64a14ca9d1ad h1:uAwc13+y0Y8QZLT
google.golang.org/genproto v0.0.0-20200624020401-64a14ca9d1ad/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc/examples v0.0.0-20200824180931-410880dd7d91 h1:eUaF7ghTaPu2Ivm9aqGW31Zr9aVB8k1KO1m3lo7lbj8=
google.golang.org/grpc/examples v0.0.0-20200824180931-410880dd7d91/go.mod h1:wQWkdCkP0Pl3MzFPvfqTNUnXA2eIVY4eakDiKJvniKc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

View File

@@ -76,5 +76,8 @@
],
"peerDependencies": {
"owncloud-design-system": "^1.7.0"
},
"dependencies": {
"validator": "^13.1.1"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -294,14 +294,15 @@ func NewGroupsServiceEndpoints() []*api.Endpoint {
&api.Endpoint{
Name: "GroupsService.RemoveMember",
Path: []string{"/api/v0/groups/{group_id=*}/members/{account_id}/$ref"},
Method: []string{"DELETE"},
Method: []string{"POST"},
Body: "*",
Handler: "rpc",
},
&api.Endpoint{
Name: "GroupsService.ListMembers",
Path: []string{"/api/v0/groups/{id=*}/members/$ref"},
Method: []string{"GET"},
Method: []string{"POST"},
Body: "*",
Handler: "rpc",
},
}
@@ -501,14 +502,15 @@ func RegisterGroupsServiceHandler(s server.Server, hdlr GroupsServiceHandler, op
opts = append(opts, api.WithEndpoint(&api.Endpoint{
Name: "GroupsService.RemoveMember",
Path: []string{"/api/v0/groups/{group_id=*}/members/{account_id}/$ref"},
Method: []string{"DELETE"},
Method: []string{"POST"},
Body: "*",
Handler: "rpc",
}))
opts = append(opts, api.WithEndpoint(&api.Endpoint{
Name: "GroupsService.ListMembers",
Path: []string{"/api/v0/groups/{id=*}/members/$ref"},
Method: []string{"GET"},
Method: []string{"POST"},
Body: "*",
Handler: "rpc",
}))
return s.Handle(s.NewHandler(&GroupsService{h}, opts...))

View File

@@ -328,7 +328,7 @@ func (h *webGroupsServiceHandler) RemoveMember(w http.ResponseWriter, r *http.Re
return
}
render.Status(r, http.StatusOK)
render.Status(r, http.StatusCreated)
render.JSON(w, r, resp)
}
@@ -352,7 +352,7 @@ func (h *webGroupsServiceHandler) ListMembers(w http.ResponseWriter, r *http.Req
return
}
render.Status(r, http.StatusOK)
render.Status(r, http.StatusCreated)
render.JSON(w, r, resp)
}
@@ -368,8 +368,8 @@ func RegisterGroupsServiceWeb(r chi.Router, i GroupsServiceHandler, middlewares
r.MethodFunc("POST", "/api/v0/accounts/groups-update", handler.UpdateGroup)
r.MethodFunc("POST", "/api/v0/accounts/groups-delete", handler.DeleteGroup)
r.MethodFunc("POST", "/api/v0/groups/{group_id=*}/members/$ref", handler.AddMember)
r.MethodFunc("DELETE", "/api/v0/groups/{group_id=*}/members/{account_id}/$ref", handler.RemoveMember)
r.MethodFunc("GET", "/api/v0/groups/{id=*}/members/$ref", handler.ListMembers)
r.MethodFunc("POST", "/api/v0/groups/{group_id=*}/members/{account_id}/$ref", handler.RemoveMember)
r.MethodFunc("POST", "/api/v0/groups/{id=*}/members/$ref", handler.ListMembers)
}
// ListAccountsRequestJSONMarshaler describes the default jsonpb.Marshaler used by all

View File

@@ -119,7 +119,8 @@ service GroupsService {
rpc RemoveMember(RemoveMemberRequest) returns (Group) {
// All request parameters go into body.
option (google.api.http) = {
delete: "/api/v0/groups/{group_id=*}/members/{account_id}/$ref"
// URLs are broken
post: "/api/v0/groups/{group_id=*}/members/{account_id}/$ref"
body: "*"
};
}
@@ -127,7 +128,8 @@ service GroupsService {
rpc ListMembers(ListMembersRequest) returns (ListMembersResponse) {
// All request parameters go into body.
option (google.api.http) = {
get: "/api/v0/groups/{id=*}/members/$ref"
// URLs are broken
post: "/api/v0/groups/{id=*}/members/$ref"
body: "*"
};
}

View File

@@ -146,7 +146,7 @@
]
}
},
"/api/v0/groups/groups-create": {
"/api/v0/accounts/groups-create": {
"post": {
"summary": "Creates an account",
"operationId": "CreateGroup",
@@ -173,7 +173,7 @@
]
}
},
"/api/v0/groups/groups-delete": {
"/api/v0/accounts/groups-delete": {
"post": {
"summary": "Deletes an account",
"operationId": "DeleteGroup",
@@ -200,7 +200,7 @@
]
}
},
"/api/v0/groups/groups-get": {
"/api/v0/accounts/groups-get": {
"post": {
"summary": "Gets an account",
"operationId": "GetGroup",
@@ -227,7 +227,7 @@
]
}
},
"/api/v0/groups/groups-list": {
"/api/v0/accounts/groups-list": {
"post": {
"summary": "Lists accounts",
"operationId": "ListGroups",
@@ -254,7 +254,7 @@
]
}
},
"/api/v0/groups/groups-update": {
"/api/v0/accounts/groups-update": {
"post": {
"summary": "Updates an account",
"operationId": "UpdateGroup",
@@ -281,7 +281,7 @@
]
}
},
"/v0/groups/{group_id}/members/$ref": {
"/api/v0/groups/{group_id}/members/$ref": {
"post": {
"summary": "group:addmember https://docs.microsoft.com/en-us/graph/api/group-post-members?view=graph-rest-1.0\u0026tabs=http",
"operationId": "AddMember",
@@ -315,8 +315,8 @@
]
}
},
"/v0/groups/{group_id}/members/{account_id}/$ref": {
"delete": {
"/api/v0/groups/{group_id}/members/{account_id}/$ref": {
"post": {
"summary": "group:removemember https://docs.microsoft.com/en-us/graph/api/group-delete-members?view=graph-rest-1.0",
"operationId": "RemoveMember",
"responses": {
@@ -341,6 +341,14 @@
"in": "path",
"required": true,
"type": "string"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/settingsRemoveMemberRequest"
}
}
],
"tags": [
@@ -348,8 +356,8 @@
]
}
},
"/v0/groups/{id}/members/$ref": {
"get": {
"/api/v0/groups/{id}/members/$ref": {
"post": {
"summary": "group:listmembers https://docs.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-1.0",
"operationId": "ListMembers",
"responses": {
@@ -369,36 +377,12 @@
"type": "string"
},
{
"name": "page_size",
"in": "query",
"required": false,
"type": "integer",
"format": "int32"
},
{
"name": "page_token",
"description": "Optional. A pagination token returned from a previous call to `Get`\nthat indicates from where search should continue.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "field_mask.paths",
"description": "The set of field mask paths.",
"in": "query",
"required": false,
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi"
},
{
"name": "query",
"description": "Optional. Search criteria used to select the groups to return.\nIf no search criteria is specified then all groups will be\nreturned. TODO update query language\nQuery expressions can be used to restrict results based upon\nthe account properties where the operators `=`, `NOT`, `AND` and `OR`\ncan be used along with the suffix wildcard symbol `*`.\n\nThe string properties in a query expression should use escaped quotes\nfor values that include whitespace to prevent unexpected behavior.\n\nSome example queries are:\n\n* Query `display_name=Th*` returns accounts whose display_name\nstarts with \"Th\"\n* Query `display_name=\\\\\"Test String\\\\\"` returns groups with\ndisplay names that include both \"Test\" and \"String\"",
"in": "query",
"required": false,
"type": "string"
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/settingsListMembersRequest"
}
}
],
"tags": [
@@ -819,6 +803,32 @@
}
}
},
"settingsListMembersRequest": {
"type": "object",
"properties": {
"page_size": {
"type": "integer",
"format": "int32"
},
"page_token": {
"type": "string",
"title": "Optional. A pagination token returned from a previous call to `Get`\nthat indicates from where search should continue"
},
"field_mask": {
"$ref": "#/definitions/protobufFieldMask",
"description": "Optional. Used to specify a subset of fields that should be\nreturned by a get operation or modified by an update operation."
},
"query": {
"type": "string",
"description": "TODO update query language\nQuery expressions can be used to restrict results based upon\nthe account properties where the operators `=`, `NOT`, `AND` and `OR`\ncan be used along with the suffix wildcard symbol `*`.\n\nThe string properties in a query expression should use escaped quotes\nfor values that include whitespace to prevent unexpected behavior.\n\nSome example queries are:\n\n* Query `display_name=Th*` returns accounts whose display_name\nstarts with \"Th\"\n* Query `display_name=\\\\\"Test String\\\\\"` returns groups with\ndisplay names that include both \"Test\" and \"String\"",
"title": "Optional. Search criteria used to select the groups to return.\nIf no search criteria is specified then all groups will be\nreturned"
},
"id": {
"type": "string",
"title": "The id of the group to list members from"
}
}
},
"settingsListMembersResponse": {
"type": "object",
"properties": {
@@ -888,6 +898,19 @@
}
}
},
"settingsRemoveMemberRequest": {
"type": "object",
"properties": {
"group_id": {
"type": "string",
"title": "The id of the group to remove a member from"
},
"account_id": {
"type": "string",
"title": "The account id to remove"
}
}
},
"settingsUpdateAccountRequest": {
"type": "object",
"properties": {

View File

@@ -2,6 +2,17 @@
<div>
<div class="uk-container uk-padding">
<h1 v-text="$gettext('Accounts')" />
<oc-button
v-if="numberOfSelectedAccounts < 1 && !isAccountCreationInProgress"
id="accounts-new-account-trigger"
key="create-accounts-button"
v-text="$gettext('Create new user')"
variation="primary"
:disabled="isAccountCreationInProgress || !isInitialized"
:uk-tooltip="disabledCreateAccountBtnTooltip"
@click="setAccountCreationProgress(true)"
/>
<accounts-list-new-account-row v-if="isAccountCreationInProgress" @close="setAccountCreationProgress(false)" />
<oc-grid v-if="numberOfSelectedAccounts > 0" key="selected-accounts-info" gutter="small" class="uk-flex-middle">
<span v-text="selectionInfoText" />
<span>|</span>
@@ -42,9 +53,14 @@
<script>
import { mapGetters, mapActions, mapState, mapMutations } from 'vuex'
import AccountsList from './accounts/AccountsList.vue'
import AccountsListNewAccountRow from './accounts/AccountsListNewAccountRow.vue'
export default {
name: 'App',
components: { AccountsList },
components: { AccountsList, AccountsListNewAccountRow },
data: () => ({
isAccountCreationInProgress: false
}),
computed: {
...mapGetters('Accounts', ['isInitialized', 'getAccountsSorted']),
...mapState('Accounts', ['selectedAccounts']),
@@ -85,18 +101,41 @@ export default {
})
}
actions.push({
id: 'accounts-actions-dropdown-action-delete',
label: this.$gettext('Delete'),
handler: this.deleteAccounts
})
return actions
},
disabledCreateAccountBtnTooltip () {
if (!this.isInitialized) {
return this.$gettext('Loading users')
}
if (this.isAccountCreationInProgress) {
return this.$gettext('User creation is already in progress')
}
return null
}
},
methods: {
...mapActions('Accounts', ['initialize', 'toggleAccountStatus']),
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION'])
...mapActions('Accounts', ['initialize', 'toggleAccountStatus', 'deleteAccounts']),
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION']),
setAccountCreationProgress (isInProgress) {
this.isAccountCreationInProgress = isInProgress
}
},
created () {
this.initialize()
},
beforeDestroy () {
this.RESET_ACCOUNTS_SELECTION()
this.setAccountCreationProgress(false)
}
}
</script>

View File

@@ -0,0 +1,145 @@
<template>
<div class="uk-flex uk-flex-top">
<oc-grid gutter="small">
<label>
<oc-text-input
id="accounts-new-account-input-username"
type="text"
v-model="username"
:error-message="usernameError"
:placeholder="$gettext('Username')"
:disabled="isInProgress"
@keydown.enter="createAccount"
/>
</label>
<label>
<oc-text-input
id="accounts-new-account-input-email"
type="email"
v-model="email"
:error-message="emailError"
:placeholder="$gettext('Email')"
:disabled="isInProgress"
@keydown.enter="createAccount"
/>
</label>
<label class="uk-margin-xsmall-right">
<oc-text-input
id="accounts-new-account-input-password"
type="password"
v-model="password"
:error-message="passwordError"
:placeholder="$gettext('Password')"
:disabled="isInProgress"
@keydown.enter="createAccount"
/>
</label>
<div>
<oc-button
v-text="$gettext('Cancel')"
@click="emitClose"
class="uk-margin-xsmall-right"
:disabled="isInProgress"
/>
<oc-button
id="accounts-new-account-button-confirm"
variation="primary"
:disabled="isInProgress"
@click="createAccount"
>
<oc-spinner
v-if="isInProgress"
key="account-creation-in-progress"
size="xsmall"
class="uk-margin-xsmall-right"
aria-hidden="true"
/>
<span v-text="isInProgress ? $gettext('Creating') : $gettext('Create')" />
</oc-button>
</div>
</oc-grid>
</div>
</template>
<script>
import isEmail from 'validator/es/lib/isEmail'
import isEmpty from 'validator/es/lib/isEmpty'
import debounce from 'debounce'
import { mapActions } from 'vuex'
export default {
name: 'AccountsListNewAccountRow',
data: () => ({
username: '',
usernameError: '',
email: '',
emailError: '',
password: '',
passwordError: '',
isInProgress: false
}),
methods: {
...mapActions('Accounts', ['createNewAccount']),
emitClose () {
this.$emit('close')
},
createAccount () {
if (!(this.checkUsername() & this.checkEmail() & this.checkPassword())) {
return
}
this.isInProgress = true
this.createNewAccount({ username: this.username, email: this.email, password: this.password }).finally(() => {
this.isInProgress = false
})
this.emitClose()
},
checkUsername () {
if (isEmpty(this.username)) {
debounce(this.usernameError = this.$gettext('Username cannot be empty'), 500)
return false
}
this.usernameError = ''
return true
},
checkEmail () {
if (isEmpty(this.email)) {
debounce(this.emailError = this.$gettext('Email cannot be empty'), 500)
return false
}
if (!isEmail(this.email)) {
debounce(this.emailError = this.$gettext('Invalid email address'), 500)
return false
}
this.emailError = ''
return true
},
checkPassword () {
// Later on some restrictions might be applied here
if (isEmpty(this.password)) {
debounce(this.passwordError = this.$gettext('Password cannot be empty'), 500)
return false
}
this.passwordError = ''
return true
}
}
}
</script>
<style>
#accounts-new-account-button-confirm > span {
display: flex;
align-items: center;
}
</style>

View File

@@ -1,5 +1,10 @@
/* eslint-disable camelcase */
import { AccountsService_ListAccounts, AccountsService_UpdateAccount } from '../client/accounts'
import {
AccountsService_ListAccounts,
AccountsService_UpdateAccount,
AccountsService_CreateAccount,
AccountsService_DeleteAccount
} from '../client/accounts'
import { RoleService_ListRoles } from '../client/settings'
/* eslint-enable camelcase */
import { injectAuthToken } from '../helpers/auth'
@@ -56,6 +61,16 @@ const mutations = {
RESET_ACCOUNTS_SELECTION (state) {
state.selectedAccounts = []
},
PUSH_NEW_ACCOUNT (state, account) {
state.accounts.push(account)
},
DELETE_ACCOUNT (state, accountId) {
const accountIndex = state.accounts.findIndex(account => account.id === accountId)
state.accounts.splice(accountIndex, 1)
}
}
@@ -163,6 +178,74 @@ const actions = {
}, { root: true })
}
commit('RESET_ACCOUNTS_SELECTION')
},
async createNewAccount ({ rootGetters, commit, dispatch }, account) {
injectAuthToken(rootGetters.user.token)
const response = await AccountsService_CreateAccount({
$domain: rootGetters.configuration.server,
body: {
account: {
on_premises_sam_account_name: account.username,
preferred_name: account.username,
mail: account.email,
password_profile: {
password: account.password
},
account_enabled: true,
display_name: account.username
}
}
})
if (response.status === 201) {
commit('PUSH_NEW_ACCOUNT', response.data)
} else {
dispatch('showMessage', {
title: 'Failed to create account',
desc: response.statusText,
status: 'danger'
}, { root: true })
}
},
async deleteAccounts ({ rootGetters, state, commit, dispatch }) {
const failedAccounts = []
injectAuthToken(rootGetters.user.token)
for (const account of state.selectedAccounts) {
const response = await AccountsService_DeleteAccount({
$domain: rootGetters.configuration.server,
body: {
id: account.id
}
})
if (response.status === 201 || response.status === 204) {
commit('DELETE_ACCOUNT', account.id)
} else {
failedAccounts.push({ account: account.diisplayName, statusText: response.statusText })
}
}
if (failedAccounts.length === 1) {
dispatch('showMessage', {
title: 'Failed to delete account',
desc: failedAccounts[0].statusText,
status: 'danger'
}, { root: true })
}
if (failedAccounts.length > 1) {
dispatch('showMessage', {
title: 'Failed to delete accounts',
desc: 'Could not delete multiple accounts',
status: 'danger'
}, { root: true })
}
commit('RESET_ACCOUNTS_SELECTION')
}
}

View File

@@ -59,3 +59,15 @@ Feature: Accounts
Then the status indicator of user "einstein,marie" should be "enabled" on the WebUI
# And user "einstein" should be able to log in
# And user "marie" should be able to log in
Scenario: create a user
Given user "Moss" has logged in using the webUI
And the user browses to the accounts page
When the user creates a new user with username "bob", email "bob@example.org" and password "bob" using the WebUI
Then user "bob" should be displayed in the accounts list on the WebUI
Scenario: delete a user
Given user "Moss" has logged in using the webUI
And the user browses to the accounts page
When the user deletes user "bob" using the WebUI
Then user "bob" should not be displayed in the accounts list on the WebUI

View File

@@ -14,13 +14,14 @@ module.exports = {
return this.waitForElementVisible('@accountsListTable')
},
isUserListed: async function (username) {
let user
const usernameInTable = util.format(this.elements.userInAccountsList.selector, username)
await this.useXpath().waitForElementVisible(usernameInTable)
.getText(usernameInTable, (result) => {
user = result
})
return user.value
return true
},
isUserDeleted: async function (username) {
const usernameInTable = util.format(this.elements.userInAccountsList.selector, username)
await this.useXpath().waitForElementNotPresent(usernameInTable)
return true
},
selectRole: function (username, role) {
@@ -43,17 +44,9 @@ module.exports = {
},
toggleUserStatus: function (usernames, status) {
usernames = usernames.split(',')
const actionSelector = status === 'enabled' ? this.elements.enableAction : this.elements.disableAction
// Select users
for (const username of usernames) {
const checkboxSelector =
util.format(this.elements.rowByUsername.selector, username) +
this.elements.rowCheckbox.selector
this.useXpath().click(checkboxSelector)
}
this.selectUsers(usernames)
return this
.waitForElementVisible('@actionsDropdownTrigger')
@@ -76,6 +69,38 @@ module.exports = {
}
return this
},
deleteUsers: function (usernames) {
this.selectUsers(usernames)
return this
.waitForElementVisible('@actionsDropdownTrigger')
.click('@actionsDropdownTrigger')
.click('@deleteAction')
},
selectUsers: function (usernames) {
usernames = usernames.split(',')
for (const username of usernames) {
const checkboxSelector =
util.format(this.elements.rowByUsername.selector, username) +
this.elements.rowCheckbox.selector
this.useXpath().click(checkboxSelector)
}
return this
},
createUser: function (username, email, password) {
return this
.click('@accountsNewAccountTrigger')
.setValue('@newAccountInputUsername', username)
.setValue('@newAccountInputEmail', email)
.setValue('@newAccountInputPassword', password)
.click('@newAccountButtonConfirm')
}
},
@@ -129,6 +154,24 @@ module.exports = {
statusIndicator: {
selector: '//span[contains(@class, "accounts-status-indicator-%s")]',
locateStrategy: 'xpath'
},
newAccountInputUsername: {
selector: '#accounts-new-account-input-username'
},
newAccountInputEmail: {
selector: '#accounts-new-account-input-email'
},
newAccountInputPassword: {
selector: '#accounts-new-account-input-password'
},
newAccountButtonConfirm: {
selector: '#accounts-new-account-button-confirm'
},
accountsNewAccountTrigger: {
selector: '#accounts-new-account-trigger'
},
deleteAction: {
selector: '#accounts-actions-dropdown-action-delete'
}
}
}

View File

@@ -7,9 +7,15 @@ When('the user browses to the accounts page', function () {
})
Then('user {string} should be displayed in the accounts list on the WebUI', async function (username) {
await client.page.accountsPage().accountsList(username)
await client.page.accountsPage().accountsList()
const userListed = await client.page.accountsPage().isUserListed(username)
return assert.strictEqual(userListed, username)
return assert.strictEqual(userListed, true)
})
Then('user {string} should not be displayed in the accounts list on the WebUI', async function (username) {
await client.page.accountsPage().accountsList()
const userDeleted = await client.page.accountsPage().isUserDeleted(username)
return assert.strictEqual(userDeleted, true)
})
Given('the user has changed the role of user {string} to {string}', function (username, role) {
@@ -42,3 +48,14 @@ When('the user enables user/users {string} using the WebUI', function (usernames
Then('the status indicator of user/users {string} should be {string} on the WebUI', function (usernames, status) {
return client.page.accountsPage().checkUsersStatus(usernames, status)
})
When(
'the user creates a new user with username {string}, email {string} and password {string} using the WebUI',
function (username, email, password) {
return client.page.accountsPage().createUser(username, email, password)
}
)
When('the user deletes user/users {string} using the WebUI', function (usernames) {
return client.page.accountsPage().deleteUsers(usernames)
})

View File

@@ -6348,6 +6348,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
validator@^13.1.1:
version "13.1.1"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.1.1.tgz#f8811368473d2173a9d8611572b58c5783f223bf"
integrity sha512-8GfPiwzzRoWTg7OV1zva1KvrSemuMkv07MA9TTl91hfhe+wKrsrgVN4H2QSFd/U/FhiU3iWPYVgvbsOGwhyFWw==
vasync@1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/vasync/-/vasync-1.6.2.tgz#568edcf40b2b5c35b1cc048cad085de4739703fb"