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:
parent
858b72a404
commit
89ed0b425b
13 changed files with 219 additions and 28 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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
|
||||||
|
|
23
app/views/api/v1/accounts/create.json.jbuilder
Normal file
23
app/views/api/v1/accounts/create.json.jbuilder
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue