Browse Source

feat: add option to toggle user signup enable/disable

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4134/head
Pranav C 2 years ago
parent
commit
5256e7b849
  1. 42
      packages/nc-gui/components/admin/SignupSettings.vue
  2. 231
      packages/nc-gui/components/admin/UserList.vue
  3. 15
      packages/nc-gui/components/admin/UserManagement.vue
  4. 4
      packages/nc-gui/layouts/base.vue
  5. 4
      packages/nc-gui/lib/constants.ts
  6. 4
      packages/nc-gui/pages/admin/index.vue
  7. 2
      packages/nocodb/src/lib/constants/index.ts
  8. 3
      packages/nocodb/src/lib/meta/api/orgLicenseApis.ts
  9. 31
      packages/nocodb/src/lib/meta/api/orgUserApis.ts
  10. 54
      scripts/sdk/swagger.json

42
packages/nc-gui/components/admin/SignupSettings.vue

@ -1,9 +1,41 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, useApi } from '#imports'
const { api } = useApi()
let settings = $ref({ enable_user_signup: false })
const loadSettings = async () => {
try {
const response = await api.orgAppSettings.get()
settings = response
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const saveSettings = async () => {
try {
await api.orgAppSettings.set(settings)
message.success('Settings ky updated')
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
loadSettings()
</script>
<template>
<div>
<div class="text-xl">Settings</div>
<a-divider class="!my-3" />
<a-form-item>
<a-checkbox name="virtual">Enable user signup</a-checkbox>
</a-form-item>
<div class="text-xl">Settings</div>
<a-divider class="!my-3" />
<a-form-item>
<a-checkbox v-model:checked="settings.enable_user_signup" name="virtual" @change="saveSettings"
>Enable user signup</a-checkbox
>
</a-form-item>
</div>
</template>

231
packages/nc-gui/components/admin/UserList.vue

@ -18,14 +18,12 @@ let users = $ref<UserType[]>([])
let currentPage = $ref(1)
const currentLimit = $ref(10)
const showUserModal = ref(false)
const searchText = ref<string>('')
const pagination = reactive({
total: 0,
pageSize: 10,
@ -118,85 +116,84 @@ const copyPasswordResetUrl = async (user: User) => {
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
<div>
<div class="text-xl ">User Management</div>
<a-divider class="!my-3" />
<div class="max-w-[900px] mx-auto p-4">
<div class="py-2 flex gap-4 items-center">
<a-input-search
v-model:value="searchText"
size="small"
class="max-w-[300px]"
placeholder="Filter by email"
@blur="loadUsers"
@keydown.enter="loadUsers"
>
</a-input-search>
<div class="flex-grow"></div>
<MdiReload class="cursor-pointer" @click="loadUsers" />
<a-button size="small" @click="showUserModal = true">
<div class="flex items-center gap-1">
<MdiAdd />
Invite new user
</div>
</a-button>
</div>
<a-table
:row-key="(record) => record.id"
:data-source="users"
:pagination="{ position: ['bottomCenter'] }"
:loading="isLoading"
<div class="text-xl">User Management</div>
<a-divider class="!my-3" />
<div class="max-w-[900px] mx-auto p-4">
<div class="py-2 flex gap-4 items-center">
<a-input-search
v-model:value="searchText"
size="small"
@change="loadUsers($event.current)"
class="max-w-[300px]"
placeholder="Filter by email"
@blur="loadUsers"
@keydown.enter="loadUsers"
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</a-input-search>
<div class="flex-grow"></div>
<MdiReload class="cursor-pointer" @click="loadUsers" />
<a-button size="small" @click="showUserModal = true">
<div class="flex items-center gap-1">
<MdiAdd />
Invite new user
</div>
</a-button>
</div>
<a-table
:row-key="(record) => record.id"
:data-source="users"
:pagination="{ position: ['bottomCenter'] }"
:loading="isLoading"
size="small"
@change="loadUsers($event.current)"
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<!-- Email -->
<a-table-column key="email" :title="$t('labels.email')" data-index="email">
<template #default="{ text }">
<div>
{{ text }}
</div>
</template>
</a-table-column>
<!-- Email -->
<a-table-column key="email" :title="$t('labels.email')" data-index="email">
<template #default="{ text }">
<div>
{{ text }}
</div>
</template>
</a-table-column>
<!-- Role -->
<a-table-column key="roles" :title="$t('objects.role')" data-index="roles">
<template #default="{ record }">
<div>
<div v-if="record.roles.includes('super')" class="font-weight-bold">Super Admin</div>
<a-select
v-else
v-model:value="record.roles"
class="w-[220px]"
:dropdown-match-select-width="false"
@change="updateRole(record.id, record.roles)"
>
<a-select-option :value="Role.OrgLevelCreator" :label="$t(`objects.roleType.orgLevelCreator`)">
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"
<!-- Role -->
<a-table-column key="roles" :title="$t('objects.role')" data-index="roles">
<template #default="{ record }">
<div>
<div v-if="record.roles.includes('super')" class="font-weight-bold">Super Admin</div>
<a-select
v-else
v-model:value="record.roles"
class="w-[220px]"
:dropdown-match-select-width="false"
@change="updateRole(record.id, record.roles)"
>
<a-select-option :value="Role.OrgLevelCreator" :label="$t(`objects.roleType.orgLevelCreator`)">
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"
>Creator can create new projects and access any invited project.</span
>
</a-select-option>
>
</a-select-option>
<a-select-option :value="Role.OrgLevelViewer" :label="$t(`objects.roleType.orgLevelViewer`)">
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"
<a-select-option :value="Role.OrgLevelViewer" :label="$t(`objects.roleType.orgLevelViewer`)">
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"
>Viewer is not allowed to create new projects but they can access any invited project.</span
>
</a-select-option>
</a-select>
</div>
</template>
</a-table-column>
<!-- &lt;!&ndash; Projects &ndash;&gt;
>
</a-select-option>
</a-select>
</div>
</template>
</a-table-column>
<!-- &lt;!&ndash; Projects &ndash;&gt;
<a-table-column key="projectsCount" :title="$t('objects.projects')" data-index="projectsCount">
<template #default="{ text }">
<div>
@ -205,58 +202,56 @@ const copyPasswordResetUrl = async (user: User) => {
</template>
</a-table-column> -->
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div v-if="!record.roles.includes('super')" class="flex items-center gap-2">
<MdiDeleteOutline class="nc-action-btn cursor-pointer" @click="deleteUser(text)" />
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight"
overlay-class-name="nc-dropdown-user-mgmt">
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<IcBaselineMoreVert />
</div>
</a-button>
</div>
<template #overlay>
<a-menu>
<template v-if="record.invite_token">
<a-menu-item>
<!-- Resend invite Email -->
<div class="flex flex-row items-center py-3" @click="resendInvite(record)">
<MdiEmailArrowRightOutline class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.resendInvite') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyInviteUrl(record)">
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyInviteURL') }}</div>
</div>
</a-menu-item>
</template>
<a-menu-item v-else>
<div class="flex flex-row items-center py-3" @click="copyPasswordResetUrl(record)">
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div v-if="!record.roles.includes('super')" class="flex items-center gap-2">
<MdiDeleteOutline class="nc-action-btn cursor-pointer" @click="deleteUser(text)" />
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight" overlay-class-name="nc-dropdown-user-mgmt">
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<IcBaselineMoreVert />
</div>
</a-button>
</div>
<template #overlay>
<a-menu>
<template v-if="record.invite_token">
<a-menu-item>
<!-- Resend invite Email -->
<div class="flex flex-row items-center py-3" @click="resendInvite(record)">
<MdiEmailArrowRightOutline class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.resendInvite') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyInviteUrl(record)">
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyPasswordResetURL') }}</div>
<div class="text-xs pl-2">{{ $t('activity.copyInviteURL') }}</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<span v-else></span>
</template>
</a-table-column>
</a-table>
<LazyAdminUsersModal :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
</div>
</template>
<a-menu-item v-else>
<div class="flex flex-row items-center py-3" @click="copyPasswordResetUrl(record)">
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyPasswordResetURL') }}</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<span v-else></span>
</template>
</a-table-column>
</a-table>
<LazyAdminUsersModal :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
</div>
</div>
</template>

15
packages/nc-gui/components/admin/UserManagement.vue

@ -1,13 +1,15 @@
<script lang="ts" setup>
import { useUIPermission } from '~/composables/useUIPermission'
const { isUIAllowed } = useUIPermission()
const tabs = [
...(isUIAllowed('superAdminUserManagement') ? [{ label: 'Users', key: 'users' },
{ label: 'Settings', key: 'settings' }] : []),
...(isUIAllowed('superAdminUserManagement')
? [
{ label: 'Users', key: 'users' },
{ label: 'Settings', key: 'settings' },
]
: []),
{ label: 'Reset Password', key: 'password-reset' },
]
@ -17,9 +19,7 @@ const selectedTabKey = ref(tabs[0].key)
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-4">
<a-tabs v-model:active-key="selectedTabKey" :open-keys="[]" mode="horizontal" class="nc-auth-tabs">
<a-tab-pane
v-for="(tab) of tabs"
:key="tab.key" class="select-none">
<a-tab-pane v-for="tab of tabs" :key="tab.key" class="select-none">
<template #tab>
<span>
{{ tab.label }}
@ -28,7 +28,6 @@ const selectedTabKey = ref(tabs[0].key)
</a-tab-pane>
</a-tabs>
<template v-if="selectedTabKey === 'users'">
<LazyAdminUserList class="mt-10" />
</template>
<template v-else-if="selectedTabKey === 'settings'">

4
packages/nc-gui/layouts/base.vue

@ -107,7 +107,7 @@ hooks.hook('page:finish', () => {
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<!-- <a-menu-item v-if="isUIAllowed('appStore')" key="0" class="!rounded-t">
<!-- <a-menu-item v-if="isUIAllowed('appStore')" key="0" class="!rounded-t">
<nuxt-link
v-e="['c:settings:appstore', { page: true }]"
class="nc-project-menu-item group !no-underline"
@ -120,7 +120,7 @@ hooks.hook('page:finish', () => {
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />-->
<a-menu-divider class="!m-0" /> -->
<a-menu-item key="1" class="!rounded-b group">
<div v-e="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">

4
packages/nc-gui/lib/constants.ts

@ -31,14 +31,14 @@ export const rolePermissions = {
exclude: {
appStore: true,
superAdminUserManagement: true,
appLicense:true
appLicense: true,
},
},
[ProjectRole.Owner]: {
exclude: {
appStore: true,
superAdminUserManagement: true,
appLicense:true
appLicense: true,
},
},
[ProjectRole.Editor]: {

4
packages/nc-gui/pages/admin/index.vue

@ -6,7 +6,7 @@ const $route = useRoute()
const selectedTabKeys = computed(() => [$route.params.page])
const {isUIAllowed} = useUIPermission()
const { isUIAllowed } = useUIPermission()
</script>
<template>
@ -39,10 +39,10 @@ const {isUIAllowed} = useUIPermission()
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('appLicense')"
key="license"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
@click="navigateTo('/admin/license')"
v-if="isUIAllowed('appLicense')"
>
<div class="flex items-center space-x-2">
<MdiKeyChainVariant />

2
packages/nocodb/src/lib/constants/index.ts

@ -0,0 +1,2 @@
export const LICENSE_KEY = 'nc-license-key';
export const NC_APP_SETTINGS = 'nc-app-settings';

3
packages/nocodb/src/lib/meta/api/orgLicenseApis.ts

@ -1,10 +1,11 @@
import { Router } from 'express';
import { OrgUserRoles } from '../../../enums/OrgUserRoles';
import { LICENSE_KEY } from '../../constants'
import Store from '../../models/Store';
import { metaApiMetrics } from '../helpers/apiMetrics';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
const LICENSE_KEY = 'nc-license-key';
async function licenseGet(_req, res) {
const license = await Store.get(LICENSE_KEY);

31
packages/nocodb/src/lib/meta/api/orgUserApis.ts

@ -3,8 +3,10 @@ import { PluginCategory } from 'nocodb-sdk';
import { v4 as uuidv4 } from 'uuid';
import validator from 'validator';
import { OrgUserRoles } from '../../../enums/OrgUserRoles';
import { NC_APP_SETTINGS } from '../../constants';
import Audit from '../../models/Audit';
import ProjectUser from '../../models/ProjectUser';
import Store from '../../models/Store';
import SyncSource from '../../models/SyncSource';
import User from '../../models/User';
import Noco from '../../Noco';
@ -227,6 +229,23 @@ async function generateResetUrl(req, res) {
});
}
async function appSettingsGet(_req, res) {
let settings = {};
try {
settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value);
} catch {}
res.json(settings);
}
async function appSettingsSet(req, res) {
await Store.saveOrUpdate({
value: JSON.stringify(req.body),
key: NC_APP_SETTINGS,
});
res.json({ msg: 'License key saved' });
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/users',
@ -283,23 +302,25 @@ router.post(
ncMetaAclMw(generateResetUrl, 'generateResetUrl', {
allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
}));
})
);
router.get(
'/api/v1/users/settings',
'/api/v1/app-settings',
metaApiMetrics,
ncMetaAclMw(generateResetUrl, 'generateResetUrl', {
ncMetaAclMw(appSettingsGet, 'appSettingsGet', {
allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
})
);
router.post(
'/api/v1/users/settings',
'/api/v1/app-settings',
metaApiMetrics,
ncMetaAclMw(generateResetUrl, 'generateResetUrl', {
ncMetaAclMw(appSettingsSet, 'appSettingsSet', {
allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
})
);
export default router;

54
scripts/sdk/swagger.json

@ -572,6 +572,60 @@
]
}
},
"/api/v1/app-settings": {
"get": {
"summary": "App settings get",
"operationId": "org-app-settings-get",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"enable_user_signup": {
"type": "boolean"
}
}
}
}
}
}
},
"description": "",
"tags": [
"Org app settings"
]
},
"parameters": [],
"post": {
"summary": "App app settings get",
"operationId": "org-app-settings-set",
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"enable_user_signup": {
"type": "boolean"
}
}
}
}
}
},
"tags": [
"Org app settings"
]
}
},
"/api/v1/tokens/{token}": {
"parameters": [
{

Loading…
Cancel
Save