feat: Ability for the logged in user to create a new account (#985)

Co-authored-by: Divyesh <dkothari@box8.in>
Co-authored-by: Pranav Raj S <pranav@thoughtwoot.com>
This commit is contained in:
Divyesh Kothari 2020-07-26 12:54:50 +05:30 committed by GitHub
parent 858b72a404
commit 89ed0b425b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 219 additions and 28 deletions

View file

@ -2,15 +2,18 @@
class AccountBuilder class AccountBuilder
include CustomExceptions::Account include CustomExceptions::Account
pattr_initialize [:account_name!, :email!, :confirmed!] pattr_initialize [:account_name!, :email!, :confirmed!, :user]
def perform def perform
validate_email if @user.nil?
validate_user validate_email
validate_user
end
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@account = create_account @account = create_account
@user = create_and_link_user @user = create_and_link_user
end end
[@user, @account]
rescue StandardError => e rescue StandardError => e
@account&.destroy @account&.destroy
puts e.inspect puts e.inspect
@ -42,13 +45,7 @@ class AccountBuilder
end end
def create_and_link_user def create_and_link_user
password = Time.now.to_i if @user.present? || create_user
@user = User.new(email: @email,
password: password,
password_confirmation: password,
name: email_to_name(@email))
@user.confirm if @confirmed
if @user.save!
link_user_to_account(@user, @account) link_user_to_account(@user, @account)
@user @user
else else
@ -68,4 +65,14 @@ class AccountBuilder
name = email[/[^@]+/] name = email[/[^@]+/]
name.split('.').map(&:capitalize).join(' ') name.split('.').map(&:capitalize).join(' ')
end end
def create_user
password = Time.now.to_i
@user = User.new(email: @email,
password: password,
password_confirmation: password,
name: email_to_name(@email))
@user.confirm if @confirmed
@user.save!
end
end end

View file

@ -14,14 +14,15 @@ class Api::V1::AccountsController < Api::BaseController
with: :render_error_response with: :render_error_response
def create def create
@user = AccountBuilder.new( @user, @account = AccountBuilder.new(
account_name: account_params[:account_name], account_name: account_params[:account_name],
email: account_params[:email], email: account_params[:email],
confirmed: confirmed? confirmed: confirmed?,
user: current_user
).perform ).perform
if @user if @user
send_auth_headers(@user) send_auth_headers(@user)
render partial: 'devise/auth.json', locals: { resource: @user } render 'api/v1/accounts/create.json', locals: { resource: @user }
else else
render_error_response(CustomExceptions::Account::SignupFailed.new({})) render_error_response(CustomExceptions::Account::SignupFailed.new({}))
end end

View file

@ -15,7 +15,8 @@ class DashboardController < ActionController::Base
'WIDGET_BRAND_URL', 'WIDGET_BRAND_URL',
'TERMS_URL', 'TERMS_URL',
'PRIVACY_URL', 'PRIVACY_URL',
'DISPLAY_MANIFEST' 'DISPLAY_MANIFEST',
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD'
) )
end end
end end

View file

@ -1,9 +1,14 @@
/* global axios */
import ApiClient from './ApiClient'; import ApiClient from './ApiClient';
class AccountAPI extends ApiClient { class AccountAPI extends ApiClient {
constructor() { constructor() {
super('', { accountScoped: true }); super('', { accountScoped: true });
} }
createAccount(data) {
return axios.post(`${this.apiVersion}/accounts`, data);
}
} }
export default new AccountAPI(); export default new AccountAPI();

View file

@ -34,7 +34,7 @@
class="dropdown-pane top" class="dropdown-pane top"
> >
<ul class="vertical dropdown menu"> <ul class="vertical dropdown menu">
<li v-if="currentUser.accounts.length > 1"> <li v-if="showChangeAccountOption">
<button <button
class="button clear change-accounts--button" class="button clear change-accounts--button"
@click="changeAccount" @click="changeAccount"
@ -94,6 +94,58 @@
</label> </label>
</a> </a>
</div> </div>
<div
v-if="globalConfig.createNewAccountFromDashboard"
class="modal-footer delete-item"
>
<button
class="button success large expanded nice"
@click="createAccount"
>
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
</button>
</div>
</woot-modal>
<woot-modal
:show="showCreateAccountModal"
:on-close="onCloseCreate"
class="account-selector--modal"
>
<div class="column content-box">
<woot-modal-header
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
/>
<form class="row" @submit.prevent="addAccount()">
<div class="medium-12 columns">
<label :class="{ error: $v.accountName.$error }">
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
<input
v-model.trim="accountName"
type="text"
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
@input="$v.accountName.$touch"
/>
</label>
</div>
<div class="modal-footer medium-12 columns">
<div class="medium-12 columns">
<woot-submit-button
:disabled="
$v.accountName.$invalid ||
$v.accountName.$invalid ||
uiFlags.isCreating
"
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
button-class="large expanded"
/>
</div>
</div>
</form>
</div>
</woot-modal> </woot-modal>
</aside> </aside>
</template> </template>
@ -108,13 +160,16 @@ import SidebarItem from './SidebarItem';
import { frontendURL } from '../../helper/URLHelper'; import { frontendURL } from '../../helper/URLHelper';
import Thumbnail from '../widgets/Thumbnail'; import Thumbnail from '../widgets/Thumbnail';
import { getSidebarItems } from '../../i18n/default-sidebar'; import { getSidebarItems } from '../../i18n/default-sidebar';
import { required, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
// import accountMixin from '../../../../../mixins/account';
export default { export default {
components: { components: {
SidebarItem, SidebarItem,
Thumbnail, Thumbnail,
}, },
mixins: [clickaway, adminMixin], mixins: [clickaway, adminMixin, alertMixin],
props: { props: {
route: { route: {
type: String, type: String,
@ -125,8 +180,18 @@ export default {
return { return {
showOptionsMenu: false, showOptionsMenu: false,
showAccountModal: false, showAccountModal: false,
showCreateAccountModal: false,
accountName: '',
vertical: 'bottom',
horizontal: 'center',
}; };
}, },
validations: {
accountName: {
required,
minLength: minLength(1),
},
},
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
@ -134,8 +199,15 @@ export default {
inboxes: 'inboxes/getInboxes', inboxes: 'inboxes/getInboxes',
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole', currentRole: 'getCurrentRole',
uiFlags: 'agents/getUIFlags',
accountLabels: 'labels/getLabelsOnSidebar', accountLabels: 'labels/getLabelsOnSidebar',
}), }),
showChangeAccountOption() {
if (this.globalConfig.createNewAccountFromDashboard) {
return true;
}
return this.currentUser.accounts.length > 1;
},
sidemenuItems() { sidemenuItems() {
return getSidebarItems(this.accountId); return getSidebarItems(this.accountId);
}, },
@ -230,6 +302,29 @@ export default {
onClose() { onClose() {
this.showAccountModal = false; this.showAccountModal = false;
}, },
createAccount() {
this.showAccountModal = false;
this.showCreateAccountModal = true;
},
onCloseCreate() {
this.showCreateAccountModal = false;
},
async addAccount() {
try {
const account_id = await this.$store.dispatch('accounts/create', {
account_name: this.accountName,
});
this.onClose();
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
window.location = `/app/accounts/${account_id}/dashboard`;
} catch (error) {
if (error.response.status === 422) {
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
} else {
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
}
}
},
}, },
}; };
</script> </script>

View file

@ -122,5 +122,22 @@
"INTEGRATIONS": "Integrations", "INTEGRATIONS": "Integrations",
"ACCOUNT_SETTINGS": "Account Settings", "ACCOUNT_SETTINGS": "Account Settings",
"LABELS": "Labels" "LABELS": "Labels"
},
"CREATE_ACCOUNT": {
"NEW_ACCOUNT": "New Account",
"SELECTOR_SUBTITLE": "Create a new account",
"API": {
"SUCCESS_MESSAGE": "Account created successfully",
"EXIST_MESSAGE": "Account already exists",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
},
"FORM": {
"NAME": {
"LABEL": "Account Name",
"PLACEHOLDER": "Wayne Enterprises"
},
"SUBMIT": "Submit"
}
} }
} }

View file

@ -48,6 +48,18 @@ export const actions = {
throw new Error(error); throw new Error(error);
} }
}, },
create: async ({ commit }, accountInfo) => {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCreating: true });
try {
const response = await AccountAPI.createAccount(accountInfo);
const account_id = response.data.data.account_id;
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCreating: false });
return account_id;
} catch (error) {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCreating: false });
throw error;
}
},
}; };
export const mutations = { export const mutations = {

View file

@ -8,6 +8,10 @@ const accountData = {
locale: 'en', locale: 'en',
}; };
const newAccountInfo = {
accountName: 'Company two',
};
const commit = jest.fn(); const commit = jest.fn();
global.axios = axios; global.axios = axios;
jest.mock('axios'); jest.mock('axios');
@ -53,4 +57,27 @@ describe('#actions', () => {
]); ]);
}); });
}); });
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({
data: { data: { id: 1, name: 'John' } },
});
await actions.create({ commit, getters }, newAccountInfo);
expect(commit.mock.calls).toEqual([
[types.default.SET_ACCOUNT_UI_FLAG, { isCreating: true }],
[types.default.SET_ACCOUNT_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.create({ commit, getters }, newAccountInfo)
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_ACCOUNT_UI_FLAG, { isCreating: true }],
[types.default.SET_ACCOUNT_UI_FLAG, { isCreating: false }],
]);
});
});
}); });

View file

@ -1,19 +1,21 @@
const { const {
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
INSTALLATION_NAME: installationName,
LOGO_THUMBNAIL: logoThumbnail, LOGO_THUMBNAIL: logoThumbnail,
LOGO: logo, LOGO: logo,
INSTALLATION_NAME: installationName,
WIDGET_BRAND_URL: widgetBrandURL,
TERMS_URL: termsURL,
PRIVACY_URL: privacyURL, PRIVACY_URL: privacyURL,
TERMS_URL: termsURL,
WIDGET_BRAND_URL: widgetBrandURL,
} = window.globalConfig; } = window.globalConfig;
const state = { const state = {
logoThumbnail, createNewAccountFromDashboard,
logo,
installationName, installationName,
widgetBrandURL, logo,
termsURL, logoThumbnail,
privacyURL, privacyURL,
termsURL,
widgetBrandURL,
}; };
export const getters = { export const getters = {

View file

@ -59,6 +59,7 @@ class User < ApplicationRecord
# The validation below has been commented out as it does not # The validation below has been commented out as it does not
# work because :validatable in devise overrides this. # work because :validatable in devise overrides this.
# validates_uniqueness_of :email, scope: :account_id # validates_uniqueness_of :email, scope: :account_id
validates :email, :name, presence: true validates :email, :name, presence: true
has_many :account_users, dependent: :destroy has_many :account_users, dependent: :destroy

View file

@ -0,0 +1,23 @@
json.data do
json.id resource.id
json.provider resource.provider
json.uid resource.uid
json.name resource.name
json.display_name resource.display_name
json.email resource.email
json.account_id @account.id
json.pubsub_token resource.pubsub_token
json.role resource.active_account_user&.role
json.inviter_id resource.active_account_user&.inviter_id
json.confirmed resource.confirmed?
json.avatar_url resource.avatar_url
json.access_token resource.access_token.token
json.accounts do
json.array! resource.account_users do |account_user|
json.id account_user.account_id
json.name account_user.account.name
json.active_at account_user.active_at
json.role account_user.role
end
end
end

View file

@ -15,7 +15,7 @@
- name: DISPLAY_MANIFEST - name: DISPLAY_MANIFEST
value: true value: true
- name: MAILER_INBOUND_EMAIL_DOMAIN - name: MAILER_INBOUND_EMAIL_DOMAIN
value: value:
- name: MAILER_SUPPORT_EMAIL - name: MAILER_SUPPORT_EMAIL
value: value:
- name: CREATE_NEW_ACCOUNT_FROM_DASHBOARD - name: CREATE_NEW_ACCOUNT_FROM_DASHBOARD

View file

@ -15,9 +15,9 @@ RSpec.describe 'Accounts API', type: :request do
end end
it 'calls account builder' do it 'calls account builder' do
allow(account_builder).to receive(:perform).and_return(user) allow(account_builder).to receive(:perform).and_return([user, account])
params = { account_name: 'test', email: email } params = { account_name: 'test', email: email, user: nil }
post api_v1_accounts_url, post api_v1_accounts_url,
params: params, params: params,
@ -31,7 +31,7 @@ RSpec.describe 'Accounts API', type: :request do
it 'renders error response on invalid params' do it 'renders error response on invalid params' do
allow(account_builder).to receive(:perform).and_return(nil) allow(account_builder).to receive(:perform).and_return(nil)
params = { account_name: nil, email: nil } params = { account_name: nil, email: nil, user: nil }
post api_v1_accounts_url, post api_v1_accounts_url,
params: params, params: params,
@ -46,7 +46,7 @@ RSpec.describe 'Accounts API', type: :request do
it 'ignores confirmed param when called with out super admin token' do it 'ignores confirmed param when called with out super admin token' do
allow(account_builder).to receive(:perform).and_return(nil) allow(account_builder).to receive(:perform).and_return(nil)
params = { account_name: 'test', email: email, confirmed: true } params = { account_name: 'test', email: email, confirmed: true, user: nil }
post api_v1_accounts_url, post api_v1_accounts_url,
params: params, params: params,