Browse Source

fix: Bulk add users

Enabled option to add bulk add user with comma separated emails

re #300

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/350/head
Pranav C 3 years ago
parent
commit
12ab1d2d8a
  1. 22
      packages/nc-gui/components/auth/userManagement.vue
  2. 96
      packages/nc-gui/components/project/spreadsheet/components/extras.vue
  3. 81
      packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue
  4. 1194
      packages/nc-gui/layouts/default.vue
  5. 1
      packages/nc-gui/nuxt.config.js
  6. 2
      packages/nc-gui/pages/projects/index.vue
  7. 9
      packages/nc-gui/plugins/globalEventBus.js
  8. 1
      packages/nc-gui/static/lang/en.json
  9. 1
      packages/nc-gui/static/lang/fr.json
  10. 1
      packages/nc-gui/static/lang/ja.json
  11. 2
      packages/nc-gui/static/lang/zh.json
  12. 2
      packages/nc-lib-gui/package.json
  13. 103
      packages/nocodb/src/lib/noco/rest/RestAuthCtrlEE.ts

22
packages/nc-gui/components/auth/userManagement.vue

@ -346,7 +346,7 @@
></dlg-label-submit-cancel>
<v-dialog v-model="userEditDialog" :width="invite_token ? 700 :480" @close="invite_token = null">
<v-dialog v-model="userEditDialog" :width="invite_token ? 700 :700" @close="invite_token = null">
<v-card v-if="selectedUser" class="px-15 py-5 " style="min-height: 100%">
<h4 class="text-center text-capitalize mt-2 d-100 display-1">
<template v-if="invite_token">Copy Invite Token</template>
@ -388,7 +388,10 @@
<p class="caption grey--text mt-3"> Looks like you have not configured mailer yet! <br>Please copy above
invite
link and send it to {{ invite_token && invite_token.email }}.</p>
link and send it to {{ invite_token && (invite_token.email || invite_token.emails && invite_token.emails.join(', ')) }}.</p>
<!-- todo: show error message if failed-->
</div>
<template v-else>
<v-form v-model="valid" @submit.prevent="saveUser">
@ -403,6 +406,8 @@
v-model="selectedUser.email"
:rules="emailRules"
@input="edited=true"
validate-on-blur
hint="You can add multiple comma(,) separated emails"
label="Email"></v-text-field>
</v-col>
<v-col cols="12">
@ -476,14 +481,21 @@ export default {
valid: null,
emailRules: [
v => !!v || 'E-mail is required',
v => isEmail(v) || 'E-mail must be valid'
v => {
const invalidEmails = (v||'').split(/\s*,\s*/).filter(e => !isEmail(e));
return !invalidEmails.length || `"${invalidEmails.join(', ')}" - invalid email`
}
],
userList: []
}),
async created() {
this.$eventBus.$on('show-add-user', this.addUser);
await this.loadUsers();
await this.loadRoles();
},
beforeDestroy() {
this.$eventBus.$off('show-add-user', this.addUser);
},
methods: {
simpleAnim() {
var count = 30;
@ -619,7 +631,7 @@ export default {
addUser() {
this.invite_token = null;
this.selectedUser = {
roles: 'creator,editor'
roles: 'editor'
}
this.userEditDialog = true
},
@ -733,7 +745,7 @@ export default {
this.selectedUser = this.users[i];
}
}
}
},
}
</script>

96
packages/nc-gui/components/project/spreadsheet/components/extras.vue

@ -0,0 +1,96 @@
<template>
<div class="wrapper">
<div class="d-flex justify-end">
<v-list
width="100%"
class="
flex-shrink-1
text-left
elevation-1
rounded-sm
community-card
item
"
:class="{ active: showCommunity }"
dense
>
<v-list-item
dense
target="_blank"
href="https://calendly.com/nocodb"
>
<!-- Book a Free DEMO -->
<v-list-item-title>
<v-icon class="mr-1" small :color="textColors[3]">mdi-calendar-month
</v-icon>
<span class="caption" :title="$t('projects.show_community_message_2')">{{
$t('projects.show_community_message_2')
}}</span></v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item dense href="https://discord.gg/5RgZmkW" target="_blank">
<!-- Get your questions answered -->
<v-list-item-title>
<v-icon class="mr-1" small :color="textColors[0]">mdi-discord</v-icon>
<span class="caption" :title="$t('projects.show_community_message_3_short')">{{
$t('projects.show_community_message_3_short')
}}</span>
</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item dense href="https://twitter.com/NocoDB" target="_blank">
<!-- Follow NocoDB -->
<v-list-item-title>
<v-icon class="mr-1" small :color="textColors[1]">mdi-twitter</v-icon>
<span class="caption" title="$t('projects.show_community_message_4')"> {{
$t('projects.show_community_message_4')
}}</span></v-list-item-title>
</v-list-item>
</v-list>
</div>
<sponsor-mini
:class="{ active: !showCommunity }" class="item" :nav="true"></sponsor-mini>
</div>
</template>
<script>
import SponsorMini from "~/components/sponsorMini";
import colors from "~/mixins/colors";
export default {
name: "extras",
data: () => ({
showCommunity: true
}),
mixins: [colors],
components: {SponsorMini},
mounted() {
setInterval(() => this.showCommunity = !this.showCommunity, 60000)
}
}
</script>
<style scoped lang="scss">
.wrapper {
position: relative;
.item {
z-index: -1;
opacity: 0;
position: absolute;
transition: .6s opacity;
bottom: 0;
right: 0;
width: 100%;
&.active {
z-index: 1;
position: relative;
opacity: 1;
}
}
}
</style>

81
packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue

@ -29,7 +29,7 @@
x-small
v-if="viewIcons[view.show_as]"
:color="viewIcons[view.show_as].color"
>{{ viewIcons[view.show_as].icon }}
>{{ viewIcons[view.show_as].icon }}
</v-icon>
<v-icon color="primary" small v-else>mdi-table</v-icon>
</v-list-item-icon>
@ -49,7 +49,7 @@
@blur="updateViewName(view)"
/>
<template v-else
><span v-on="on">{{ view.alias || view.title }}</span></template
><span v-on="on">{{ view.alias || view.title }}</span></template
>
</div>
</template>
@ -93,13 +93,14 @@
</x-icon>
</template>
<v-icon v-if="view.id === selectedViewId" small class="check-icon"
>mdi-check-bold</v-icon
>mdi-check-bold
</v-icon
>
</v-list-item>
</v-list-item-group>
</v-list>
<template v-if="hideViews && _isUIAllowed('virtualViewsCreateOrEdit')">
<v-divider class="advance-menu-divider"> </v-divider>
<v-divider class="advance-menu-divider"></v-divider>
<v-list
dense
@ -121,7 +122,7 @@
@mouseleave="overShieldIcon = false"
icon-class="ml-2"
small
>mdi-shield-lock-outline
>mdi-shield-lock-outline
</x-icon>
</template>
<!-- Only visible to Creator -->
@ -137,7 +138,7 @@
<v-icon color="blue" x-small>mdi-grid-large</v-icon>
</v-list-item-icon>
<v-list-item-title
><span class="font-weight-regular">
><span class="font-weight-regular">
<!-- Grid -->
{{ $t('nav_drawer.virtual_views.grid') }}
</span></v-list-item-title
@ -156,7 +157,7 @@
<v-icon color="orange" x-small>mdi-camera-image</v-icon>
</v-list-item-icon>
<v-list-item-title
><span class="font-weight-regular">
><span class="font-weight-regular">
<!-- Gallery -->
{{ $t('nav_drawer.virtual_views.gallery') }}
@ -183,7 +184,7 @@
<v-icon x-small>mdi-calendar</v-icon>
</v-list-item-icon>
<v-list-item-title
><span class="font-weight-regular">
><span class="font-weight-regular">
<!-- Calendar -->
{{ $t('nav_drawer.virtual_views.calendar') }}
</span></v-list-item-title
@ -208,7 +209,7 @@
<v-icon x-small>mdi-tablet-dashboard</v-icon>
</v-list-item-icon>
<v-list-item-title
><span class="font-weight-regular">
><span class="font-weight-regular">
<!-- Kanban -->
{{ $t('nav_drawer.virtual_views.kanban') }}
</span></v-list-item-title
@ -233,7 +234,7 @@
<v-icon x-small class="mt-n1">mdi-form-select</v-icon>
</v-list-item-icon>
<v-list-item-title
><span class="font-weight-regular">
><span class="font-weight-regular">
<!-- Form -->
{{ $t('nav_drawer.virtual_views.form') }}
@ -256,9 +257,13 @@
v-if="time - $store.state.windows.miniSponsorCard > 15 * 60 * 1000"
>
<v-icon small class="close-icon" @click="hideMiniSponsorCard"
>mdi-close-circle-outline</v-icon
>mdi-close-circle-outline
</v-icon
>
<sponsor-mini :nav="true"></sponsor-mini>
<extras></extras>
</div>
<!--<div class="text-center">
<v-hover >
@ -285,7 +290,7 @@
<span
class="body-2 grey--text"
@dblclick="$emit('update:showAdvanceOptions', !showAdvanceOptions)"
>Advanced</span
>Advanced</span
>
<v-tooltip top>
<template v-slot:activator="{ on }">
@ -296,7 +301,7 @@
@mouseleave="overAdvShieldIcon = false"
icon-class="ml-2"
small
>mdi-shield-lock-outline
>mdi-shield-lock-outline
</x-icon>
</template>
<span class="caption">
@ -338,28 +343,6 @@
</v-list>
</v-menu>
</v-list-item>
<!-- </template>-->
<!-- <v-card dense class="backgroundColor">-->
<!-- <v-container fluid @click.stop>-->
<!-- <v-text-field-->
<!-- label="Password"-->
<!-- hint="Enter shared view password"-->
<!-- flat-->
<!-- solo-->
<!-- dense-->
<!-- v-model="sharedViewPassword"-->
<!-- ></v-text-field>-->
<!-- <div class="text-right">-->
<!-- <v-btn small color="primary" @click="genShareLink()">Create</v-btn>-->
<!-- </div>-->
<!-- </v-container>-->
<!-- </v-card>-->
<!-- </v-menu>-->
<!-- </template>-->
<!-- Generate shared view url-->
<!-- </v-tooltip>-->
<v-tooltip bottom>
<template v-slot:activator="{ on }">
@ -407,12 +390,12 @@
</p>
<div style="border-radius: 4px" class="share-link-box body-2 pa-2 d-flex align-center">
{{ shareLink.url }}
<v-spacer> </v-spacer>
<v-spacer></v-spacer>
<a :href="shareLink.url" style="text-decoration: none" target="_blank">
<v-icon small class="mx-2">mdi-open-in-new</v-icon>
</a>
<v-icon small class="pointer" @click="copyShareUrlToClipboard"
>mdi-content-copy
>mdi-content-copy
</v-icon>
</div>
@ -461,11 +444,11 @@
<script>
import CreateViewDialog from '@/components/project/spreadsheet/dialog/createViewDialog';
import SponsorMini from '@/components/sponsorMini';
import Extras from "~/components/project/spreadsheet/components/extras";
export default {
name: 'spreadsheetNavDrawer',
components: { SponsorMini, CreateViewDialog },
components: {Extras, CreateViewDialog},
props: {
showAdvanceOptions: Boolean,
hideViews: Boolean,
@ -505,11 +488,11 @@ export default {
overShieldIcon: false,
viewsList: [],
viewIcons: {
grid: { icon: 'mdi-grid-large', color: 'blue' },
form: { icon: 'mdi-form-select', color: 'pink' },
calendar: { icon: 'mdi-calendar', color: 'purple' },
gallery: { icon: 'mdi-camera-image', color: 'orange' },
kanban: { icon: 'mdi-tablet-dashboard', color: 'green' },
grid: {icon: 'mdi-grid-large', color: 'blue'},
form: {icon: 'mdi-form-select', color: 'pink'},
calendar: {icon: 'mdi-calendar', color: 'purple'},
gallery: {icon: 'mdi-camera-image', color: 'orange'},
kanban: {icon: 'mdi-tablet-dashboard', color: 'green'},
},
copyViewRef: null,
shareLink: {},
@ -592,7 +575,7 @@ export default {
async saveShareLinkPassword() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [
{ dbAlias: this.nodes.dbAlias },
{dbAlias: this.nodes.dbAlias},
'updateSharedViewLinkPassword',
{
id: this.shareLink.id,
@ -639,7 +622,7 @@ export default {
},
async updateViewName(view) {
try {
await this.sqlOp({ dbAlias: this.nodes.dbAlias }, 'xcVirtualTableRename', {
await this.sqlOp({dbAlias: this.nodes.dbAlias}, 'xcVirtualTableRename', {
id: view.id,
title: view.title,
alias: view.alias,
@ -661,7 +644,7 @@ export default {
},
async deleteView(view) {
try {
await this.sqlOp({ dbAlias: this.nodes.dbAlias }, 'xcVirtualTableDelete', {
await this.sqlOp({dbAlias: this.nodes.dbAlias}, 'xcVirtualTableDelete', {
id: view.id,
title: view.alias || view.title,
parent_model_title: this.table,
@ -675,7 +658,7 @@ export default {
async genShareLink() {
this.showShareModel = true;
const sharedViewUrl = await this.$store.dispatch('sqlMgr/ActSqlOp', [
{ dbAlias: this.nodes.dbAlias },
{dbAlias: this.nodes.dbAlias},
'createSharedViewLink',
{
model_name: this.table,

1194
packages/nc-gui/layouts/default.vue

File diff suppressed because it is too large Load Diff

1
packages/nc-gui/nuxt.config.js

@ -55,6 +55,7 @@ export default {
"@/plugins/vueClipboard",
"@/plugins/globalComponentLoader",
"@/plugins/globalMixin",
"@/plugins/globalEventBus",
"~/plugins/i18n.js",
{src: '~plugins/projectLoader.js', ssr: false}
],

2
packages/nc-gui/pages/projects/index.vue

@ -508,7 +508,7 @@
<v-list-item
dense
target="_blank"
href="https://calendly.com/xgenecloud/demo"
href="https://calendly.com/nocodb"
>
<v-list-item-icon>
<v-icon class="ml-2" :color="textColors[3]"

9
packages/nc-gui/plugins/globalEventBus.js

@ -0,0 +1,9 @@
import Vue from 'vue';
const GlobalPlugins = {
install(v) {
v.prototype.$eventBus = new Vue();
},
};
Vue.use(GlobalPlugins);

1
packages/nc-gui/static/lang/en.json

@ -22,6 +22,7 @@
"projects.show_community_message_1_2": "us on Github",
"projects.show_community_message_2": "Book a Free DEMO",
"projects.show_community_message_3": "Get your questions answered",
"projects.show_community_message_3_short": "Join Discord",
"projects.show_community_message_4": "Follow NocoDB",
"projects.search.no_result": "Your search for {search} found no results",
"projects.ext_db.title.edit": "Edit Project",

1
packages/nc-gui/static/lang/fr.json

@ -22,6 +22,7 @@
"projects.show_community_message_1_2": "sur Github",
"projects.show_community_message_2": "Planifier une démo gratuite",
"projects.show_community_message_3": "Obtenir des réponses à vos questions",
"projects.show_community_message_3_short": "Join Discord",
"projects.show_community_message_4": "Suivre NocoDB",
"projects.search.no_result": "Votre recherche pour {search} n'a renvoyée aucun résultat",
"projects.ext_db.title.edit": "Editer le projet",

1
packages/nc-gui/static/lang/ja.json

@ -22,6 +22,7 @@
"projects.show_community_message_1_2": "us on Github",
"projects.show_community_message_2": "Book a Free DEMO",
"projects.show_community_message_3": "Get your questions answered",
"projects.show_community_message_3_short": "Join Discord",
"projects.show_community_message_4": "Follow NocoDB",
"projects.search.no_result": "Your search for {search} found no results",
"projects.ext_db.title.edit": "Edit Project",

2
packages/nc-gui/static/lang/zh.json

@ -22,6 +22,7 @@
"projects.show_community_message_1_2": "us on Github",
"projects.show_community_message_2": "Book a Free DEMO",
"projects.show_community_message_3": "Get your questions answered",
"projects.show_community_message_3_short": "Join Discord",
"projects.show_community_message_4": "Follow NocoDB",
"projects.search.no_result": "Your search for {search} found no results",
"projects.ext_db.title.edit": "Edit Project",
@ -128,4 +129,3 @@
"management.meta.operation_4.desc": "Import project meta zip file and restart.",
"management.meta.operation_5.desc": "Clear all metadata from meta tables."
}

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.2.3",
"version": "0.2.4",
"description": "> TODO: description",
"author": "“pranavxc” <pranavxc@gmail.com>",
"homepage": "https://gitlab.com/xgenecloud-ts/xgenecloud-ts#readme",

103
packages/nocodb/src/lib/noco/rest/RestAuthCtrlEE.ts

@ -11,15 +11,17 @@ export default class RestAuthCtrlEE extends RestAuthCtrl {
protected async addAdmin(req, res, next): Promise<any> {
// if (!this.config?.mailer || !this.emailClient) {
// return next(new Error('SMTP config is not found'));
// }
const emails = (req.body.email || '').split(/\s*,\s*/).map(v => v.trim());
const email = req.body.email;
if (!email || !validator.isEmail(email)) {
// check for invalid emails
const invalidEmails = emails.filter(v => !validator.isEmail(v))
if (!emails.length) {
return next(new Error('Invalid email address'));
}
if (invalidEmails.length) {
return next(new Error('Invalid email address : ' + invalidEmails.join(', ')));
}
// todo: handle roles which contains super
if (!req.session?.passport?.user?.roles?.owner && req.body.roles.indexOf('owner') > -1) {
@ -27,45 +29,68 @@ export default class RestAuthCtrlEE extends RestAuthCtrl {
}
const invite_token = uuidv4();
const error = [];
const user = await this.users.where({email}).first();
if (user) {
if (!await this.xcMeta.isUserHaveAccessToProject(req.body.project_id, user.id)) {
await this.xcMeta.projectAddUser(req.body.project_id, user.id, 'editor');
}
} else {
try {
await this.users.insert({
invite_token,
invite_token_expires: new Date(Date.now() + (24 * 60 * 60 * 1000)),
email,
roles: 'user'
});
const {id} = await this.users.where({email}).first();
await this.xcMeta.projectAddUser(req.body.project_id, id, req.body.roles);
for (const email of emails) {
if (!await this.sendInviteEmail(email, invite_token, req)) {
res.json({invite_token, email})
// add user to project if user already exist
const user = await this.users.where({email}).first();
if (user) {
if (!await this.xcMeta.isUserHaveAccessToProject(req.body.project_id, user.id)) {
await this.xcMeta.projectAddUser(req.body.project_id, user.id, 'editor');
}
this.xcMeta.audit(req.body.project_id, null, 'nc_audit', {
op_type: 'AUTHENTICATION',
op_sub_type: 'INVITE',
user: req.user.email,
description: `invited ${email} to ${req.body.project_id} project `, ip: req.clientIp
})
} else {
try {
// create new user with invite token
await this.users.insert({
invite_token,
invite_token_expires: new Date(Date.now() + (24 * 60 * 60 * 1000)),
email,
roles: 'user'
});
const {id} = await this.users.where({email}).first();
// add user to project
await this.xcMeta.projectAddUser(req.body.project_id, id, req.body.roles);
Tele.emit('evt', {evt_type: 'project:invite'})
this.xcMeta.audit(req.body.project_id, null, 'nc_audit', {
op_type: 'AUTHENTICATION',
op_sub_type: 'INVITE',
user: req.user.email,
description: `invited ${email} to ${req.body.project_id} project `, ip: req.clientIp
})
// in case of single user check for smtp failure
// and send back token if failed
if (emails.length === 1 && !await this.sendInviteEmail(email, invite_token, req)) {
return res.json({invite_token, email});
} else {
this.sendInviteEmail(email, invite_token, req)
}
} catch (e) {
if (emails.length === 1) {
return next(e);
} else {
error.push({email, error: e.message})
}
}
} catch (e) {
return next(e);
}
}
Tele.emit('evt', {evt_type: 'project:invite'})
this.xcMeta.audit(req.body.project_id, null, 'nc_audit', {
op_type: 'AUTHENTICATION',
op_sub_type: 'INVITE',
user: req.user.email,
description: `invited ${email} to ${req.body.project_id} project `, ip: req.clientIp
})
}
res.json({
msg: 'success'
})
if (emails.length === 1) {
res.json({
msg: 'success'
})
} else {
return res.json({invite_token, emails, error});
}
}
protected async updateAdmin(req, res, next): Promise<any> {

Loading…
Cancel
Save