Browse Source

feat: account management page with sidebar and nested tab

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4134/head
Pranav C 2 years ago
parent
commit
7d42e4c768
  1. 3
      packages/nc-gui/components.d.ts
  2. 58
      packages/nc-gui/components/admin/License.vue
  3. 223
      packages/nc-gui/components/admin/Token.vue
  4. 28
      packages/nc-gui/components/admin/User.vue
  5. 0
      packages/nc-gui/components/admin/UsersModal.vue
  6. 238
      packages/nc-gui/components/org/Token.vue
  7. 2
      packages/nc-gui/layouts/base.vue
  8. 60
      packages/nc-gui/pages/admin/index.vue
  9. 16
      packages/nc-gui/pages/admin/index/[page].vue
  10. 2
      packages/nc-gui/pages/index/index.vue
  11. 49
      packages/nc-gui/pages/org/[page].vue
  12. 1
      packages/nocodb/package.json
  13. 2
      packages/nocodb/src/lib/Noco.ts
  14. 2
      packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts
  15. 2
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSql.ts
  16. 2
      packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts
  17. 4
      packages/nocodb/src/lib/meta/NcMetaMgr.ts
  18. 2
      packages/nocodb/src/lib/meta/NcMetaMgrEE.ts
  19. 2
      packages/nocodb/src/lib/meta/NcMetaMgrv2.ts
  20. 2
      packages/nocodb/src/lib/meta/api/apiTokenApis.ts
  21. 2
      packages/nocodb/src/lib/meta/api/attachmentApis.ts
  22. 2
      packages/nocodb/src/lib/meta/api/columnApis.ts
  23. 2
      packages/nocodb/src/lib/meta/api/filterApis.ts
  24. 2
      packages/nocodb/src/lib/meta/api/formViewApis.ts
  25. 2
      packages/nocodb/src/lib/meta/api/formViewColumnApis.ts
  26. 2
      packages/nocodb/src/lib/meta/api/galleryViewApis.ts
  27. 2
      packages/nocodb/src/lib/meta/api/gridViewApis.ts
  28. 2
      packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts
  29. 2
      packages/nocodb/src/lib/meta/api/hookApis.ts
  30. 2
      packages/nocodb/src/lib/meta/api/hookFilterApis.ts
  31. 6
      packages/nocodb/src/lib/meta/api/index.ts
  32. 2
      packages/nocodb/src/lib/meta/api/kanbanViewApis.ts
  33. 2
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts
  34. 2
      packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts
  35. 23
      packages/nocodb/src/lib/meta/api/orgTokenApis.ts
  36. 9
      packages/nocodb/src/lib/meta/api/orgUserApis.ts
  37. 2
      packages/nocodb/src/lib/meta/api/pluginApis.ts
  38. 3
      packages/nocodb/src/lib/meta/api/projectApis.ts
  39. 4
      packages/nocodb/src/lib/meta/api/projectUserApis.ts
  40. 2
      packages/nocodb/src/lib/meta/api/sharedBaseApis.ts
  41. 2
      packages/nocodb/src/lib/meta/api/sortApis.ts
  42. 2
      packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts
  43. 4
      packages/nocodb/src/lib/meta/api/sync/helpers/fetchAT.ts
  44. 2
      packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
  45. 2
      packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts
  46. 2
      packages/nocodb/src/lib/meta/api/tableApis.ts
  47. 2
      packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts
  48. 4
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  49. 2
      packages/nocodb/src/lib/meta/api/utilApis.ts
  50. 2
      packages/nocodb/src/lib/meta/api/viewApis.ts
  51. 2
      packages/nocodb/src/lib/meta/api/viewColumnApis.ts
  52. 2
      packages/nocodb/src/lib/meta/helpers/apiMetrics.ts
  53. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  54. 48
      packages/nocodb/src/lib/migrations/v2/nc_021_add_fields_in_token.ts
  55. 35
      packages/nocodb/src/lib/models/ApiToken.ts
  56. 2
      packages/nocodb/src/lib/models/KanbanView.ts
  57. 5
      packages/nocodb/src/lib/models/ProjectUser.ts
  58. 13
      packages/nocodb/src/lib/models/SelectOption.ts
  59. 292
      packages/nocodb/src/lib/utils/Tele.ts
  60. 2
      packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts
  61. 37
      packages/nocodb/src/lib/utils/packageVersion.ts
  62. 4
      packages/nocodb/src/lib/utils/projectAcl.ts
  63. 2
      packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts
  64. 2
      packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts
  65. 2
      packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts
  66. 2
      packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
  67. 3
      packages/nocodb/src/lib/version-upgrader/ncProjectRolesUpgrader.ts
  68. 55
      scripts/sdk/swagger.json

3
packages/nc-gui/components.d.ts vendored

@ -132,6 +132,7 @@ declare module '@vue/runtime-core' {
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
MdiClipboard: typeof import('~icons/mdi/clipboard')['default']
MdiClose: typeof import('~icons/mdi/close')['default']
MdiCloseBox: typeof import('~icons/mdi/close-box')['default']
MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
@ -142,6 +143,7 @@ declare module '@vue/runtime-core' {
MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiCopy: typeof import('~icons/mdi/copy')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default']
@ -178,6 +180,7 @@ declare module '@vue/runtime-core' {
MdiInformation: typeof import('~icons/mdi/information')['default']
MdiJson: typeof import('~icons/mdi/json')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyChainVariant: typeof import('~icons/mdi/key-chain-variant')['default']
MdiKeyChange: typeof import('~icons/mdi/key-change')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']

58
packages/nc-gui/components/admin/License.vue

@ -0,0 +1,58 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import type { RequestParams, UserType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, useApi, useCopy } from '#imports'
const { api, isLoading } = useApi()
const { copy } = useCopy()
let tokens = $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,
})
const loadTokens = async (page = currentPage, limit = currentLimit) => {
currentPage = page
try {
const response: any = await api.orgTokens.list({
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
},
} as RequestParams)
if (!response) return
pagination.total = response.pageInfo.totalRows ?? 0
pagination.pageSize = 10
tokens = response.list as UserType[]
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
loadTokens()
</script>
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull">
<div class="text-xl mt-4">License</div>
<a-divider class="!my-3" />
<a-textarea placeholder="License key" class="!mt-4 max-w-[300px]"></a-textarea>
<a-button class="mt-4 float-right" type="primary">Save license key</a-button>
</div>
</template>
<style scoped></style>

223
packages/nc-gui/components/admin/Token.vue

@ -0,0 +1,223 @@
<script lang="ts" setup>
import { message, Empty, Modal } from 'ant-design-vue'
import type { RequestParams, UserType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, useApi , useCopy} from '#imports'
const { api, isLoading } = useApi()
const { copy } = useCopy()
let tokens = $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,
})
const loadTokens = async (page = currentPage, limit = currentLimit) => {
currentPage = page
try {
const response: any = await api.orgTokens.list({
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
},
} as RequestParams)
if (!response) return
pagination.total = response.pageInfo.totalRows ?? 0
pagination.pageSize = 10
tokens = response.list as UserType[]
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
loadTokens()
const deleteToken = async (userId: string) => {
Modal.confirm({
title: 'Are you sure you want to delete this token?',
type: 'warn',
onOk: async () => {
try {
// todo: delete token
// await api.orgUsers.delete(userId)
// message.success('User deleted successfully')
// await loadUsers()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
</script>
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull">
<div class="text-xl mt-4">Token Management</div>
<a-divider class="!my-3" />
<div class="max-w-[700px] mx-auto p-4">
<div class="py-2 flex">
<div class="flex-grow"></div>
<a-button size="small" @click="showUserModal = true">
<div class="flex items-center gap-1">
<MdiAdd />
Add new token
</div>
</a-button>
</div>
<a-table
:row-key="(record) => record.id"
:data-source="tokens"
:pagination="{ position: ['bottomCenter'] }"
:loading="isLoading"
@change="loadTokens($event.current)"
size="small"
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<!-- Token -->
<a-table-column key="createdBy" :title="$t('labels.createdBy')" data-index="createdBy">
<template #default="{ text }">
<div>
{{ text ?? 'N/A' }}
</div>
</template>
</a-table-column>
<!-- Token -->
<a-table-column key="token" :title="$t('labels.token')" data-index="token">
<template #default="{ text, record }">
<div>
<span v-if="record.show">{{ text }}</span>
<span v-else>****************************************</span>
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ record }">
<div class="flex items-center gap-2">
<a-tooltip placement="bottom">
<template #title>
<span v-if="record.show"> {{ $t('general.hide') }} </span>
<span v-else> {{ $t('general.show') }} </span>
</template>
<a-button type="text" class="!rounded-md" @click="record.show = !record.show">
<template #icon>
<MaterialSymbolsVisibilityOff v-if="record.show" class="flex mx-auto h-[1.1rem]" />
<MaterialSymbolsVisibility v-else class="flex mx-auto h-[1rem]" />
</template>
</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title> {{ $t('general.copy') }} </template>
<a-button type="text" class="!rounded-md" @click="copy(record.token)">
<template #icon>
<MdiContentCopy class="flex mx-auto h-[1rem]" />
</template>
</a-button>
</a-tooltip>
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight" overlay-class-name="nc-dropdown-api-token-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>
<a-menu-item>
<div class="flex flex-row items-center py-3 h-[1rem]" @click="deleteToken(record)">
<MdiDeleteOutline class="flex" />
<div class="text-xs pl-2">{{ $t('general.remove') }}</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
</a-table-column>
</a-table>
</div>
<a-modal
v-model:visible="showNewTokenModal"
:closable="false"
width="28rem"
centered
:footer="null"
wrap-class-name="nc-modal-generate-token"
>
<div class="relative flex flex-col h-full">
<a-button type="text" class="!absolute top-0 right-0 rounded-md -mt-2 -mr-3" @click="showNewTokenModal = false">
<template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
<!-- Generate Token -->
<div class="flex flex-row justify-center w-full -mt-1 mb-3">
<a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title>
</div>
<!-- Description -->
<a-form
ref="form"
:model="selectedTokenData"
name="basic"
layout="vertical"
class="flex flex-col justify-center space-y-6"
no-style
autocomplete="off"
@finish="generateToken"
>
<a-input v-model:value="selectedTokenData.description" :placeholder="$t('labels.description')" />
<!-- Generate -->
<div class="flex flex-row justify-center">
<a-button type="primary" html-type="submit">
{{ $t('general.generate') }}
</a-button>
</div>
</a-form>
</div>
</a-modal>
</div>
</template>
<style scoped></style>

28
packages/nc-gui/components/org/User.vue → packages/nc-gui/components/admin/User.vue

@ -18,6 +18,8 @@ let users = $ref<UserType[]>([])
let currentPage = $ref(1)
const selectedTabKey = ref(0)
const currentLimit = $ref(10)
const showUserModal = ref(false)
@ -106,6 +108,19 @@ const copyInviteUrl = (user: User) => {
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull">
<a-tabs v-model:active-key="selectedTabKey" :open-keys="[]" mode="horizontal" class="nc-auth-tabs ">
<a-tab-pane v-for="(tab, key) of [{label:'Users'},{label:'Settings'}]" :key="key" class="select-none">
<template #tab>
<span>
{{ tab.label }}
</span>
</template>
</a-tab-pane>
</a-tabs>
<template v-if="selectedTabKey === 0">
<!-- <div class="text-xl mt-4">User Management</div>-->
<!-- <a-divider class="!my-3" />-->
<div class="max-w-[700px] mx-auto p-4">
<div class="py-2 flex">
<a-input-search
@ -189,7 +204,7 @@ const copyInviteUrl = (user: User) => {
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex items-center gap-2">
<div class="flex items-center gap-2" v-if="!record.roles.includes('super')">
<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">
@ -226,12 +241,21 @@ const copyInviteUrl = (user: User) => {
</template>
</a-dropdown>
</div>
<span v-else></span>
</template>
</a-table-column>
</a-table>
<LazyOrgUserUsersModal :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
<LazyAdminUsersModal :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
</div>
</template>
<template v-else>
<!-- <div class="text-xl mt-4">Settings</div>-->
<!-- <a-divider class="!my-3" />-->
<a-form-item>
<a-checkbox name="virtual">Enable user signup</a-checkbox>
</a-form-item>
</template>
</div>
</template>

0
packages/nc-gui/components/org/UsersModal.vue → packages/nc-gui/components/admin/UsersModal.vue

238
packages/nc-gui/components/org/Token.vue

@ -1,238 +0,0 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue'
import type { RequestParams, UserType } from 'nocodb-sdk'
import { Role, extractSdkResponseErrorMsg, useApi, useDashboard, useNuxtApp , useCopy} from '#imports'
import type { User } from '~/lib'
const { api, isLoading } = useApi()
const { $e } = useNuxtApp()
const { t } = useI18n()
const { dashboardUrl } = $(useDashboard())
const { copy } = useCopy()
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,
})
const loadUsers = async (page = currentPage, limit = currentLimit) => {
currentPage = page
try {
const response: any = await api.orgUsers.list({
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
query: searchText.value,
},
} as RequestParams)
if (!response) return
pagination.total = response.pageInfo.totalRows ?? 0
pagination.pageSize = 10
users = response.list as UserType[]
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
loadUsers()
const updateRole = async (userId: string, roles: Role) => {
try {
await api.orgUsers.update(userId, {
roles,
} as unknown as UserType)
message.success('Role updated successfully')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const deleteUser = async (userId: string) => {
Modal.confirm({
title: 'Are you sure you want to delete this user?',
type: 'warn',
content:
'On deleting, user will remove from from organization and any sync source(Airtable) created by user will get removed',
onOk: async () => {
try {
await api.orgUsers.delete(userId)
message.success('User deleted successfully')
await loadUsers()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
const resendInvite = async (user: User) => {
try {
await api.orgUsers.resendInvite(user.id)
// Invite email sent successfully
message.success(t('msg.success.inviteEmailSent'))
await loadUsers()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:org-user:resend-invite')
}
const copyInviteUrl = (user: User) => {
if (!user.invite_token) return
copy(`${dashboardUrl}#/signup/${user.invite_token}`)
// Invite URL copied to clipboard
message.success(t('msg.success.inviteURLCopied'))
$e('c:user:copy-url')
}
</script>
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull">
<div class="max-w-[700px] mx-auto p-4">
<div class="py-2 flex">
<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>
<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="pagination"
:loading="isLoading"
@change="loadUsers($event.current)"
size="small"
>
<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>
<!-- 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 :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-table-column key="projectsCount" :title="$t('objects.projects')" data-index="projectsCount">
<template #default="{ text }">
<div>
{{ text }}
</div>
</template>
</a-table-column>-->
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div 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>
<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>
<!-- <a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyInviteUrl(user)">
<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>
</template>
</a-table-column>
</a-table>
<LazyOrgUserUsersModal :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
</div>
</div>
</template>
<style scoped></style>

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

@ -111,7 +111,7 @@ hooks.hook('page:finish', () => {
<nuxt-link
v-e="['c:settings:appstore', { page: true }]"
class="nc-project-menu-item group !no-underline"
to="/org/users"
to="/admin/users"
>
<MdiShieldAccountOutline class="mt-1 group-hover:text-accent" />&nbsp;

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

@ -0,0 +1,60 @@
<template>
<div class="container mx-auto h-full">
<a-layout class="mt-3 h-full overflow-y-auto flex">
<!-- Side tabs -->
<a-layout-sider>
<a-menu :selected-keys="selectedTabKeys" class="tabs-menu h-full" :open-keys="[]">
<a-menu-item
key="users"
@click="navigateTo('/admin/users')"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2">
<MdiAccountSupervisorOutline />
<div class="select-none">
User Management
</div>
</div>
</a-menu-item>
<a-menu-item
key="tokens"
@click="navigateTo('/admin/tokens')"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2">
<MdiShieldKeyOutline />
<div class="select-none">
Tokens
</div>
</div>
</a-menu-item>
<a-menu-item
key="license"
@click="navigateTo('/admin/license')"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2">
<MdiKeyChainVariant />
<div class="select-none">
License
</div>
</div>
</a-menu-item>
</a-menu>
</a-layout-sider>
<!-- Sub Tabs -->
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<NuxtPage />
</a-layout-content>
</a-layout>
</div>
</template>
<script lang="ts" setup>
import { navigateTo } from '#imports'
const $route = useRoute()
const selectedTabKeys = computed(() => [$route.params.page])
</script>

16
packages/nc-gui/pages/admin/index/[page].vue

@ -0,0 +1,16 @@
<template>
<AdminUser v-if="$route.params.page === 'users'" />
<AdminToken v-else-if="$route.params.page === 'tokens'" />
<AdminLicense v-else-if="$route.params.page === 'license'" />
<span v-else></span>
</template>
<script>
export default {
name: 'index'
};
</script>
<style scoped>
</style>

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

@ -1,9 +1,11 @@
<script lang="ts" setup>
import { useRoute, useSidebar } from '#imports'
const route = useRoute()
useSidebar('nc-left-sidebar', { hasSidebar: false })
</script>
<template>

49
packages/nc-gui/pages/org/[page].vue

@ -1,49 +0,0 @@
<template>
<a-layout class="mt-3 h-[75vh] overflow-y-auto flex">
<!-- Side tabs -->
<a-layout-sider>
<a-menu :selected-keys="selectedTabKeys" class="tabs-menu h-full" :open-keys="[]">
<a-menu-item
key="users"
@click="navigateTo('/org/users')"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2">
<MdiAccountSupervisorOutline/>
<div class="select-none">
Users
</div>
</div>
</a-menu-item>
<a-menu-item
key="tokens"
@click="navigateTo('/org/tokens')"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2">
<MdiShieldKeyOutline/>
<div class="select-none">
Tokens
</div>
</div>
</a-menu-item>
</a-menu>
</a-layout-sider>
<!-- Sub Tabs -->
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<OrgUser v-if="$route.params.page === 'users'" />
<OrgToken v-else-if="$route.params.page === 'tokens'" />
</a-layout-content>
</a-layout>
</template>
<script lang="ts" setup>
import { navigateTo } from '#imports'
const $route = useRoute()
const selectedTabKeys = computed(() => [$route.params.page])
</script>

1
packages/nocodb/package.json

@ -111,6 +111,7 @@
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
"node-machine-id": "^1.1.12",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^5.0.0",

2
packages/nocodb/src/lib/Noco.ts

@ -18,6 +18,7 @@ import { v4 as uuidv4 } from 'uuid';
import { NcConfig } from '../interface/config';
import Migrator from './db/sql-migrator/lib/KnexMigrator';
import NcConfigFactory from './utils/NcConfigFactory';
import { Tele } from './utils/Tele';
import NcProjectBuilderCE from './v1-legacy/NcProjectBuilder';
import NcProjectBuilderEE from './v1-legacy/NcProjectBuilderEE';
@ -38,7 +39,6 @@ import NocoCache from './cache/NocoCache';
import registerMetaApis from './meta/api';
import NcPluginMgrv2 from './meta/helpers/NcPluginMgrv2';
import User from './models/User';
import { Tele } from 'nc-help';
import * as http from 'http';
import weAreHiring from './utils/weAreHiring';
import getInstance from './utils/getInstance';

2
packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts

@ -1,5 +1,6 @@
/* eslint-disable no-constant-condition */
import { knex, Knex } from 'knex'
import { Tele } from '../../../utils/Tele';
import Debug from '../../util/Debug';
import Emit from '../../util/emit';
import Result from '../../util/Result';
@ -13,7 +14,6 @@ import mkdirp from 'mkdirp';
import Order from './order';
import * as dataHelp from './data.helper';
import SqlClient from './SqlClient';
import { Tele } from 'nc-help';
const evt = new Emit();
const log = new Debug('KnexClient');

2
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSql.ts

@ -358,7 +358,7 @@ class BaseModelSql extends BaseModel {
driver(this.tnPath).update(mappedData).where(this._wherePk(id))
);
let response = await this.nestedRead(id, this.defaultNestedQueryParams);
const response = await this.nestedRead(id, this.defaultNestedQueryParams);
await this.afterUpdate(response, trx, cookie);
return response;
} catch (e) {

2
packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts

@ -1,12 +1,12 @@
import fs from 'fs';
import path from 'path';
import url from 'url';
import { Tele } from 'nc-help';
import fsExtra from 'fs-extra';
import importFresh from 'import-fresh';
import inflection from 'inflection';
import slash from 'slash';
import { Tele } from '../../utils/Tele';
import SqlClientFactory from '../sql-client/lib/SqlClientFactory';
// import debug from 'debug';

4
packages/nocodb/src/lib/meta/NcMetaMgr.ts

@ -11,7 +11,6 @@ import extract from 'extract-zip';
import isDocker from 'is-docker';
import multer from 'multer';
import { customAlphabet, nanoid } from 'nanoid';
import { Tele } from 'nc-help';
import slash from 'slash';
import { v4 as uuidv4 } from 'uuid';
import { ncp } from 'ncp';
@ -27,6 +26,7 @@ import ExpressXcTsRoutesBt from '../db/sql-mgr/code/routes/xc-ts/ExpressXcTsRout
import ExpressXcTsRoutesHm from '../db/sql-mgr/code/routes/xc-ts/ExpressXcTsRoutesHm';
import NcHelp from '../utils/NcHelp';
import mimetypes, { mimeIcons } from '../utils/mimeTypes';
import { packageVersion } from '../utils/packageVersion'
import projectAcl from '../utils/projectAcl';
import Noco from '../Noco';
import { GqlApiBuilder } from '../v1-legacy/gql/GqlApiBuilder';
@ -34,13 +34,13 @@ import NcPluginMgr from '../v1-legacy/plugins/NcPluginMgr';
import XcCache from '../v1-legacy/plugins/adapters/cache/XcCache';
import { RestApiBuilder } from '../v1-legacy/rest/RestApiBuilder';
import RestAuthCtrl from '../v1-legacy/rest/RestAuthCtrlEE';
import { packageVersion } from 'nc-help';
import NcMetaIO, { META_TABLES } from './NcMetaIO';
import { promisify } from 'util';
import NcTemplateParser from '../v1-legacy/templates/NcTemplateParser';
import { defaultConnectionConfig } from '../utils/NcConfigFactory';
import xcMetaDiff from './handlers/xcMetaDiff';
import { UITypes } from 'nocodb-sdk';
import { Tele } from '../utils/Tele';
const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10);
const XC_PLUGIN_DET = 'XC_PLUGIN_DET';

2
packages/nocodb/src/lib/meta/NcMetaMgrEE.ts

@ -1,5 +1,5 @@
import { Tele } from 'nc-help';
import { v4 as uuidv4 } from 'uuid';
import { Tele } from '../utils/Tele';
import NcMetaMgr from './NcMetaMgr';

2
packages/nocodb/src/lib/meta/NcMetaMgrv2.ts

@ -4,10 +4,10 @@ import multer from 'multer';
import { NcConfig } from '../../interface/config';
import ProjectMgr from '../db/sql-mgr/ProjectMgr';
import { packageVersion } from '../utils/packageVersion'
import projectAcl from '../utils/projectAcl';
import Noco from '../Noco';
import NcPluginMgr from '../v1-legacy/plugins/NcPluginMgr';
import { packageVersion } from 'nc-help';
import NcMetaIO from './NcMetaIO';
import { defaultConnectionConfig } from '../utils/NcConfigFactory';
import ncCreateLookup from './handlersv2/ncCreateLookup';

2
packages/nocodb/src/lib/meta/api/apiTokenApis.ts

@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import ApiToken from '../../models/ApiToken';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function apiTokenList(_req: Request, res: Response) {

2
packages/nocodb/src/lib/meta/api/attachmentApis.ts

@ -2,10 +2,10 @@
import { Request, Response, Router } from 'express';
import multer from 'multer';
import { nanoid } from 'nanoid';
import { Tele } from 'nc-help';
import path from 'path';
import slash from 'slash';
import mimetypes, { mimeIcons } from '../../utils/mimeTypes';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import catchError from '../helpers/catchError';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';

2
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -3,8 +3,8 @@ import Model from '../../models/Model';
import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2';
import Base from '../../models/Base';
import Column from '../../models/Column';
import { Tele } from '../../utils/Tele';
import validateParams from '../helpers/validateParams';
import { Tele } from 'nc-help';
import { customAlphabet } from 'nanoid';
import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';

2
packages/nocodb/src/lib/meta/api/filterApis.ts

@ -1,6 +1,7 @@
import { Request, Response, Router } from 'express';
// @ts-ignore
import Model from '../../models/Model';
import { Tele } from '../../utils/Tele';
// @ts-ignore
import { PagedResponseImpl } from '../helpers/PagedResponse';
// @ts-ignore
@ -11,7 +12,6 @@ import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2';
import Project from '../../models/Project';
import Filter from '../../models/Filter';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore

2
packages/nocodb/src/lib/meta/api/formViewApis.ts

@ -1,6 +1,7 @@
import { Request, Response, Router } from 'express';
// @ts-ignore
import Model from '../../models/Model';
import { Tele } from '../../utils/Tele';
// @ts-ignore
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { FormType, ViewTypes } from 'nocodb-sdk';
@ -11,7 +12,6 @@ import Project from '../../models/Project';
import View from '../../models/View';
import FormView from '../../models/FormView';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore

2
packages/nocodb/src/lib/meta/api/formViewColumnApis.ts

@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import FormViewColumn from '../../models/FormViewColumn';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnUpdate(req: Request, res: Response) {

2
packages/nocodb/src/lib/meta/api/galleryViewApis.ts

@ -2,8 +2,8 @@ import { Request, Response, Router } from 'express';
import { GalleryType, ViewTypes } from 'nocodb-sdk';
import View from '../../models/View';
import GalleryView from '../../models/GalleryView';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function galleryViewGet(req: Request, res: Response<GalleryType>) {
res.json(await GalleryView.get(req.params.galleryViewId));

2
packages/nocodb/src/lib/meta/api/gridViewApis.ts

@ -1,6 +1,7 @@
import { Request, Router } from 'express';
// @ts-ignore
import Model from '../../models/Model';
import { Tele } from '../../utils/Tele';
// @ts-ignore
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { ViewTypes } from 'nocodb-sdk';
@ -10,7 +11,6 @@ import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2';
import Project from '../../models/Project';
import View from '../../models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore

2
packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts

@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import GridViewColumn from '../../models/GridViewColumn';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnList(req: Request, res: Response) {

2
packages/nocodb/src/lib/meta/api/hookApis.ts

@ -1,3 +1,4 @@
import { Tele } from '../../utils/Tele';
import catchError from '../helpers/catchError';
import { Request, Response, Router } from 'express';
import Hook from '../../models/Hook';
@ -7,7 +8,6 @@ import { invokeWebhook } from '../helpers/webhookHelpers';
import Model from '../../models/Model';
import populateSamplePayload from '../helpers/populateSamplePayload';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function hookList(

2
packages/nocodb/src/lib/meta/api/hookFilterApis.ts

@ -1,6 +1,7 @@
import { Request, Response, Router } from 'express';
// @ts-ignore
import Model from '../../models/Model';
import { Tele } from '../../utils/Tele';
// @ts-ignore
import { PagedResponseImpl } from '../helpers/PagedResponse';
// @ts-ignore
@ -11,7 +12,6 @@ import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2';
import Project from '../../models/Project';
import Filter from '../../models/Filter';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore

6
packages/nocodb/src/lib/meta/api/index.ts

@ -1,4 +1,6 @@
import orgUserApis from './orgUserApis'
import { Tele } from '../../utils/Tele';
import orgTokenApis from './orgTokenApis';
import orgUserApis from './orgUserApis';
import projectApis from './projectApis';
import tableApis from './tableApis';
import columnApis from './columnApis';
@ -43,7 +45,6 @@ import {
publicDataExportApis,
publicMetaApis,
} from './publicApis';
import { Tele } from 'nc-help';
import { Server, Socket } from 'socket.io';
import passport from 'passport';
@ -89,6 +90,7 @@ export default function (router: Router, server) {
router.use(pluginApis);
router.use(projectUserApis);
router.use(orgUserApis);
router.use(orgTokenApis);
router.use(sharedBaseApis);
router.use(modelVisibilityApis);
router.use(metaDiffApis);

2
packages/nocodb/src/lib/meta/api/kanbanViewApis.ts

@ -2,8 +2,8 @@ import { Request, Response, Router } from 'express';
import { KanbanType, ViewTypes } from 'nocodb-sdk';
import View from '../../models/View';
import KanbanView from '../../models/KanbanView';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function kanbanViewGet(req: Request, res: Response<KanbanType>) {

2
packages/nocodb/src/lib/meta/api/metaDiffApis.ts

@ -1,5 +1,6 @@
// // Project CRUD
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import Model from '../../models/Model';
import Project from '../../models/Project';
@ -14,7 +15,6 @@ import { getUniqueColumnAliasName } from '../helpers/getUniqueName';
import NcHelp from '../../utils/NcHelp';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import { Tele } from 'nc-help';
import getColumnUiType from '../helpers/getColumnUiType';
import { metaApiMetrics } from '../helpers/apiMetrics';

2
packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts

@ -1,8 +1,8 @@
import Model from '../../models/Model';
import ModelRoleVisibility from '../../models/ModelRoleVisibility';
import { Router } from 'express';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import Project from '../../models/Project';
import { metaApiMetrics } from '../helpers/apiMetrics';
async function xcVisibilityMetaSetAll(req, res) {

23
packages/nocodb/src/lib/meta/api/orgTokenApis.ts

@ -0,0 +1,23 @@
import { Router } from 'express';
import { OrgUserRoles } from '../../../enums/OrgUserRoles';
import ApiToken from '../../models/ApiToken';
import { metaApiMetrics } from '../helpers/apiMetrics';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { PagedResponseImpl } from '../helpers/PagedResponse';
async function tokensList(req, res) {
res.json(
new PagedResponseImpl(await ApiToken.listWithCreatedBy(req.query), {
...req.query,
count: await ApiToken.count(),
})
);
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/tokens',
metaApiMetrics,
ncMetaAclMw(tokensList, 'tokensList', [OrgUserRoles.SUPER])
);
export default router;

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

@ -9,13 +9,13 @@ import SyncSource from '../../models/SyncSource';
import User from '../../models/User';
import Noco from '../../Noco';
import { MetaTable } from '../../utils/globals';
import { Tele } from '../../utils/Tele';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { NcError } from '../helpers/catchError';
import { extractProps } from '../helpers/extractProps';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { randomTokenString } from '../helpers/stringHelpers';
import { Tele } from 'nc-help';
import { sendInviteEmail } from './projectUserApis';
async function userList(req, res) {
@ -54,6 +54,8 @@ async function userDelete(req, res) {
ncMeta
);
// todo: clear cache
// TODO: assign super admin as project owner
for (const projectUser of projectUsers) {
await ProjectUser.delete(
@ -68,13 +70,14 @@ async function userDelete(req, res) {
// delete user
await User.delete(req.params.userId, ncMeta);
await User.delete(req.params.userId, ncMeta);
await ncMeta.commit();
} catch (e) {
await ncMeta.rollback(e);
throw e;
}
res.json(await User.delete(req.params.userId));
res.json({ msg: 'success' });
}
async function userAdd(req, res, next) {

2
packages/nocodb/src/lib/meta/api/pluginApis.ts

@ -1,10 +1,10 @@
import { Request, Response, Router } from 'express';
import { Tele } from '../../utils/Tele';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import Plugin from '../../models/Plugin';
import { PluginType } from 'nocodb-sdk';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function pluginList(_req: Request, res: Response) {

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

@ -2,6 +2,8 @@ import { Request, Response } from 'express';
import Project from '../../models/Project';
import { ModelTypes, ProjectListType, UITypes } from 'nocodb-sdk';
import DOMPurify from 'isomorphic-dompurify';
import { packageVersion } from '../../utils/packageVersion'
import { Tele } from '../../utils/Tele';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import syncMigration from '../helpers/syncMigration';
import { IGNORE_TABLES } from '../../utils/common/BaseApiBuilder';
@ -17,7 +19,6 @@ import ProjectUser from '../../models/ProjectUser';
import { customAlphabet } from 'nanoid';
import Noco from '../../Noco';
import isDocker from 'is-docker';
import { packageVersion, Tele } from 'nc-help';
import { NcError } from '../helpers/catchError';
import getColumnUiType from '../helpers/getColumnUiType';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';

4
packages/nocodb/src/lib/meta/api/projectUserApis.ts

@ -1,4 +1,5 @@
import { OrgUserRoles } from '../../../enums/OrgUserRoles'
import { OrgUserRoles } from '../../../enums/OrgUserRoles';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Router } from 'express';
import { PagedResponseImpl } from '../helpers/PagedResponse';
@ -7,7 +8,6 @@ import validator from 'validator';
import { NcError } from '../helpers/catchError';
import { v4 as uuidv4 } from 'uuid';
import User from '../../models/User';
import { Tele } from 'nc-help';
import Audit from '../../models/Audit';
import NocoCache from '../../cache/NocoCache';
import { CacheGetType, CacheScope, MetaTable } from '../../utils/globals';

2
packages/nocodb/src/lib/meta/api/sharedBaseApis.ts

@ -1,7 +1,7 @@
import { Router } from 'express';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { v4 as uuidv4 } from 'uuid';
import { Tele } from 'nc-help';
import Project from '../../models/Project';
import { NcError } from '../helpers/catchError';
// todo: load from config

2
packages/nocodb/src/lib/meta/api/sortApis.ts

@ -1,6 +1,7 @@
import { Request, Response, Router } from 'express';
// @ts-ignore
import Model from '../../models/Model';
import { Tele } from '../../utils/Tele';
// @ts-ignore
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { SortListType, TableReqType, TableType } from 'nocodb-sdk';
@ -10,7 +11,6 @@ import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2';
import Project from '../../models/Project';
import Sort from '../../models/Sort';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore

2
packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts

@ -8,10 +8,10 @@ import {
getNestedParams,
limitParam,
offsetParam,
shuffleParam,
referencedRowIdParam,
relationTypeParam,
rowIdParam,
shuffleParam,
sortParam,
whereParam,
} from './params';

4
packages/nocodb/src/lib/meta/api/sync/helpers/fetchAT.ts

@ -1,12 +1,12 @@
const axios = require('axios').default;
var info: any = {
const info: any = {
initialized: false,
};
async function initialize(shareId) {
info.cookie = '';
let url = `https://airtable.com/${shareId}`;
const url = `https://airtable.com/${shareId}`;
try {
const hreq = await axios

2
packages/nocodb/src/lib/meta/api/sync/helpers/job.ts

@ -1,6 +1,6 @@
import { Tele } from '../../../../utils/Tele';
import FetchAT from './fetchAT';
import { UITypes } from 'nocodb-sdk';
import { Tele } from 'nc-help';
// import * as sMap from './syncMap';
import { Api } from 'nocodb-sdk';

2
packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts

@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import { Tele } from 'nc-help';
import SyncSource from '../../../models/SyncSource';
import { Tele } from '../../../utils/Tele';
import { PagedResponseImpl } from '../../helpers/PagedResponse';
import ncMetaAclMw from '../../helpers/ncMetaAclMw';

2
packages/nocodb/src/lib/meta/api/tableApis.ts

@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import Model from '../../models/Model';
import { Tele } from '../../utils/Tele';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { Tele } from 'nc-help';
import DOMPurify from 'isomorphic-dompurify';
import {
AuditOperationSubTypes,

2
packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts

@ -1,7 +1,6 @@
import User from '../../../models/User';
import { v4 as uuidv4 } from 'uuid';
import { promisify } from 'util';
import { Tele } from 'nc-help';
import bcrypt from 'bcryptjs';
import Noco from '../../../Noco';
@ -10,6 +9,7 @@ import ProjectUser from '../../../models/ProjectUser';
import { validatePassword } from 'nocodb-sdk';
import boxen from 'boxen';
import NocoCache from '../../../cache/NocoCache';
import { Tele } from '../../../utils/Tele';
const { isEmail } = require('validator');
const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 };

4
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -1,6 +1,7 @@
import { Request, Response } from 'express';
import { TableType, validatePassword } from 'nocodb-sdk';
import { OrgUserRoles } from '../../../../enums/OrgUserRoles'
import { OrgUserRoles } from '../../../../enums/OrgUserRoles';
import { Tele } from '../../../utils/Tele'
import catchError, { NcError } from '../../helpers/catchError';
const { isEmail } = require('validator');
import * as ejs from 'ejs';
@ -8,7 +9,6 @@ import * as ejs from 'ejs';
import bcrypt from 'bcryptjs';
import { promisify } from 'util';
import User from '../../../models/User';
import { Tele } from 'nc-help';
const { v4: uuidv4 } = require('uuid');
import Audit from '../../../models/Audit';

2
packages/nocodb/src/lib/meta/api/utilApis.ts

@ -1,12 +1,12 @@
// // Project CRUD
import { Request, Response } from 'express';
import { packageVersion } from 'nc-help';
import { ViewTypes } from 'nocodb-sdk';
import Project from '../../models/Project';
import Noco from '../../Noco';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import { MetaTable } from '../../utils/globals';
import { packageVersion } from '../../utils/packageVersion'
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import SqlMgrv2 from '../../db/sql-mgr/v2/SqlMgrv2';
import NcConfigFactory, {

2
packages/nocodb/src/lib/meta/api/viewApis.ts

@ -1,6 +1,7 @@
import { Request, Response, Router } from 'express';
// @ts-ignore
import Model from '../../models/Model';
import { Tele } from '../../utils/Tele';
// @ts-ignore
import { PagedResponseImpl } from '../helpers/PagedResponse';
// @ts-ignore
@ -12,7 +13,6 @@ import Project from '../../models/Project';
import View from '../../models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { xcVisibilityMetaGet } from './modelVisibilityApis';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
// @ts-ignore
export async function viewGet(req: Request, res: Response<Table>) {}

2
packages/nocodb/src/lib/meta/api/viewColumnApis.ts

@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import View from '../../models/View';
import { Tele } from '../../utils/Tele';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnList(req: Request, res: Response) {

2
packages/nocodb/src/lib/meta/helpers/apiMetrics.ts

@ -1,5 +1,5 @@
import { Request } from 'express';
import { Tele } from 'nc-help';
import { Tele } from '../../utils/Tele';
const countMap = {};

4
packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts

@ -8,6 +8,7 @@ import * as nc_017_add_user_token_version_column from './v2/nc_017_add_user_toke
import * as nc_018_add_meta_in_view from './v2/nc_018_add_meta_in_view';
import * as nc_019_add_meta_in_meta_tables from './v2/nc_019_add_meta_in_meta_tables';
import * as nc_020_kanban_view from './v2/nc_020_kanban_view';
import * as nc_021_add_fields_in_token from './v2/nc_021_add_fields_in_token';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -27,6 +28,7 @@ export default class XcMigrationSourcev2 {
'nc_018_add_meta_in_view',
'nc_019_add_meta_in_meta_tables',
'nc_020_kanban_view',
'nc_021_add_fields_in_token',
]);
}
@ -56,6 +58,8 @@ export default class XcMigrationSourcev2 {
return nc_019_add_meta_in_meta_tables;
case 'nc_020_kanban_view':
return nc_020_kanban_view;
case 'nc_021_add_fields_in_token':
return nc_021_add_fields_in_token;
}
}
}

48
packages/nocodb/src/lib/migrations/v2/nc_021_add_fields_in_token.ts

@ -0,0 +1,48 @@
import Knex from 'knex';
import { MetaTable } from '../../utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.API_TOKENS, (table) => {
table.string('fk_user_id', 20);
table.foreign('fk_user_id').references(`${MetaTable.USERS}.id`);
});
await knex.schema.alterTable(MetaTable.SYNC_SOURCE, (table) => {
table.dropForeign(['fk_user_id']);
});
};
const down = async (knex) => {
await knex.schema.alterTable(MetaTable.API_TOKENS, (table) => {
table.dropForeign(['fk_user_id']);
table.dropColumn('fk_user_id');
});
await knex.schema.alterTable(MetaTable.SYNC_SOURCE, (table) => {
table.foreign('fk_user_id').references(`${MetaTable.USERS}.id`);
});
};
export { up, down };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

35
packages/nocodb/src/lib/models/ApiToken.ts

@ -46,6 +46,7 @@ export default class ApiToken {
}
return tokens?.map((t) => new ApiToken(t));
}
static async delete(token, ncMeta = Noco.ncMeta) {
await NocoCache.deepDel(
CacheScope.API_TOKEN,
@ -68,4 +69,38 @@ export default class ApiToken {
}
return data && new ApiToken(data);
}
public static async count(ncMeta = Noco.ncMeta): Promise<number> {
const qb = ncMeta.knex(MetaTable.API_TOKENS);
return (await qb.count('id', { as: 'count' }).first())?.count ?? 0;
}
public static async listWithCreatedBy(
{ limit = 10, offset = 0 }: { limit: number; offset: number },
ncMeta = Noco.ncMeta
) {
const queryBuilder = ncMeta
.knex(MetaTable.API_TOKENS)
.offset(offset)
.limit(limit)
.select(
`${MetaTable.API_TOKENS}.id`,
`${MetaTable.API_TOKENS}.token`,
`${MetaTable.API_TOKENS}.description`,
`${MetaTable.API_TOKENS}.fk_user_id`,
`${MetaTable.API_TOKENS}.project_id`,
`${MetaTable.API_TOKENS}.created_at`,
`${MetaTable.API_TOKENS}.updated_at`
)
.select(
ncMeta
.knex(MetaTable.USERS)
.select('email')
.whereRaw(
`${MetaTable.USERS}.id = ${MetaTable.API_TOKENS}.fk_user_id`
)
.as('created_by')
);
return queryBuilder;
}
}

2
packages/nocodb/src/lib/models/KanbanView.ts

@ -11,7 +11,7 @@ export default class KanbanView implements KanbanType {
base_id?: string;
fk_grp_col_id?: string;
fk_cover_image_col_id?: string;
meta?: string | object;
meta?: string | Record<string, any>;
// below fields are not in use at this moment
// keep them for time being

5
packages/nocodb/src/lib/models/ProjectUser.ts

@ -184,7 +184,10 @@ export default class ProjectUser {
});
}
static async getProjectsList(userId:string, ncMeta = Noco.ncMeta): Promise<ProjectUser[]> {
static async getProjectsList(
userId: string,
ncMeta = Noco.ncMeta
): Promise<ProjectUser[]> {
return await ncMeta.metaList2(null, null, MetaTable.PROJECT_USERS, {
condition: { fk_user_id: userId },
});

13
packages/nocodb/src/lib/models/SelectOption.ts

@ -89,10 +89,15 @@ export default class SelectOption {
title: string,
ncMeta = Noco.ncMeta
): Promise<SelectOption> {
let data = await ncMeta.metaGet2(null, null, MetaTable.COL_SELECT_OPTIONS, {
fk_column_id,
title,
});
const data = await ncMeta.metaGet2(
null,
null,
MetaTable.COL_SELECT_OPTIONS,
{
fk_column_id,
title,
}
);
return data && new SelectOption(data);
}

292
packages/nocodb/src/lib/utils/Tele.ts

@ -0,0 +1,292 @@
import Emittery from 'emittery';
import { machineIdSync } from 'node-machine-id';
import axios from 'axios';
import os from 'os';
import isDocker from 'is-docker';
import { packageVersion } from './packageVersion';
import Analytics from '@rudderstack/rudder-sdk-node';
const isDisabled = !!process.env.NC_DISABLE_TELE;
const cache = !!process.env.NC_REDIS_URL;
const executable = !!process.env.NC_BINARY_BUILD;
const litestream = !!(
process.env.AWS_ACCESS_KEY_ID &&
process.env.AWS_SECRET_ACCESS_KEY &&
process.env.AWS_BUCKET
);
const sendEvt = () => {
try {
const upTime = Math.round(process.uptime() / 3600);
Tele.emit('evt', {
evt_type: 'alive',
count: global.NC_COUNT,
upTime,
});
Tele.event({
event: 'alive',
id: Tele.machineId,
data: {
cache,
upTime,
litestream,
executable,
},
});
} catch {}
};
setInterval(sendEvt, 4 * 60 * 60 * 1000);
class Tele {
public static machineId: string;
public static emitter: Emittery;
private static config: Record<string, any>;
private static client: any;
static emit(event, data) {
try {
this._init();
Tele.emitter.emit(event, data);
} catch (e) {}
}
static init(config) {
Tele.config = config;
Tele._init();
}
static page(args) {
this.emit('page', args);
}
static event(args) {
this.emit('ph_event', args);
}
static _init() {
try {
if (!Tele.emitter) {
Tele.emitter = new Emittery();
Tele.machineId = machineIdSync();
let package_id = '';
let xc_version = '';
try {
xc_version = process.env.NC_SERVER_UUID;
package_id = packageVersion;
} catch (e) {
console.log(e);
}
const teleData: Record<string, any> = {
package_id,
os_type: os.type(),
os_platform: os.platform(),
os_release: os.release(),
node_version: process.version,
docker: isDocker(),
xc_version: xc_version,
env: process.env.NODE_ENV || 'production',
oneClick: !!process.env.NC_ONE_CLICK,
};
teleData.machine_id = `${machineIdSync()},,`;
Tele.emitter.on('evt_app_started', async (msg: Record<string, any>) => {
try {
await waitForMachineId(teleData);
if (isDisabled) return;
if (msg && msg.count !== undefined) {
global.NC_COUNT = msg.count;
}
await axios.post('https://nocodb.com/api/v1/telemetry', {
...teleData,
evt_type: 'started',
payload: {
count: global.NC_COUNT,
},
});
} catch (e) {
} finally {
sendEvt();
}
});
Tele.emitter.on('evt', async (payload: Record<string, any>) => {
try {
await waitForMachineId(teleData);
if (payload.check) {
teleData.machine_id = `${machineIdSync()},,`;
}
if (isDisabled) return;
if (payload.evt_type === 'project:invite') {
global.NC_COUNT = payload.count || global.NC_COUNT;
}
if (payload.evt_type === 'user:first_signup') {
global.NC_COUNT = +global.NC_COUNT || 1;
}
await axios.post('https://nocodb.com/api/v1/telemetry', {
...teleData,
evt_type: payload.evt_type,
payload: payload,
});
} catch (e) {
// console.log(e)
}
});
Tele.emitter.on(
'evt_api_created',
async (data: Record<string, any>) => {
try {
await waitForMachineId(teleData);
const stats = {
...teleData,
table_count: data.tablesCount || 0,
relation_count: data.relationsCount || 0,
view_count: data.viewsCount || 0,
api_count: data.apiCount || 0,
function_count: data.functionsCount || 0,
procedure_count: data.proceduresCount || 0,
mysql: data.dbType === 'mysql2' ? 1 : 0,
pg: data.dbType === 'pg' ? 1 : 0,
mssql: data.dbType === 'mssql' ? 1 : 0,
sqlite3: data.dbType === 'sqlite3' ? 1 : 0,
oracledb: data.dbType === 'oracledb' ? 1 : 0,
rest: data.type === 'rest' ? 1 : 0,
graphql: data.type === 'graphql' ? 1 : 0,
grpc: data.type === 'grpc' ? 1 : 0,
time_taken: data.timeTaken,
};
if (isDisabled) return;
await axios.post(
'https://nocodb.com/api/v1/telemetry/apis_created',
stats
);
} catch (e) {}
}
);
Tele.emitter.on('evt_subscribe', async (email) => {
try {
if (isDisabled) return;
await axios.post(
'https://nocodb.com/api/v1/newsletter/sdhjh34u3yuy34bj343jhj4iwolaAdsdj3434uiut4nn',
{
email,
}
);
} catch (e) {}
});
Tele.emitter.on('page', async (args: Record<string, any>) => {
try {
if (isDisabled) return;
const instanceMeta = await this.getInstanceMeta();
this.client.track({
userId: args.id || `${this.machineId}:public`,
distinctId: args.id || `${this.machineId}:public`,
event: '$pageview',
properties: {
...teleData,
...instanceMeta,
$current_url: args.path,
},
});
} catch (e) {}
});
Tele.emitter.on('ph_event', async (args: Record<string, any>) => {
try {
if (isDisabled) return;
const instanceMeta = await this.getInstanceMeta();
let id = args.id;
if (!id) {
if (args.event && args.event.startsWith('a:api:')) {
id = this.machineId;
} else {
id = `${this.machineId}:public`;
}
}
this.client.track({
userId: id,
distinctId: id,
event: args.event,
properties: {
...teleData,
...instanceMeta,
...(args.data || {}),
},
});
} catch (e) {}
});
}
} catch (e) {}
try {
if (!this.client) {
this.client = new Analytics(
'26w4gRDLSWVX0rtMR0enhTIOu7G',
'https://nocodbnavehd.dataplane.rudderstack.com/v1/batch',
{
logLevel: '-1',
}
);
}
} catch (e) {}
}
static async getInstanceMeta() {
try {
return (
(Tele.config &&
Tele.config.instance &&
(await Tele.config.instance())) ||
{}
);
} catch {
return {};
}
}
static get id() {
return this.machineId || machineIdSync();
}
}
async function waitForMachineId(teleData) {
let i = 5;
while (i-- && !teleData.machine_id) {
await new Promise((resolve) => setTimeout(() => resolve(null), 500));
}
}
// keep polling the site url to avoid going machine idle
if (process.env.NC_PUBLIC_URL) {
setInterval(() => {
axios({
method: 'get',
url: process.env.NC_PUBLIC_URL,
})
.then(() => {})
.catch(() => {});
}, 2 * 60 * 60 * 1000);
}
if (process.env.NC_ONE_CLICK) {
try {
Tele.emit('evt', {
evt_type: 'ONE_CLICK',
});
} catch (e) {
//
}
}
export { Tele };

2
packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts

@ -9,7 +9,6 @@ import {
PgClient,
SqlClient,
// SqlClientFactory,
Tele,
} from 'nc-help';
import XcDynamicChanges from '../../../interface/XcDynamicChanges';
@ -22,6 +21,7 @@ import NcProjectBuilder from '../../v1-legacy/NcProjectBuilder';
import Noco from '../../Noco';
import NcMetaIO from '../../meta/NcMetaIO';
import XcCache from '../../v1-legacy/plugins/adapters/cache/XcCache';
import { Tele } from '../Tele';
import BaseModel from './BaseModel';
import { XcCron } from './XcCron';

37
packages/nocodb/src/lib/utils/packageVersion.ts

@ -0,0 +1,37 @@
import fs from 'fs';
import path from 'path';
let packageInfo: Record<string, any> = {};
try {
packageInfo = JSON.parse(
fs.readFileSync(
path.join(process.cwd(), 'node_modules', 'nocodb', 'package.json'),
'utf8'
)
);
} catch {
try {
// check within executable
packageInfo = JSON.parse(
fs.readFileSync(
path.join(
path.dirname(process['pkg']?.['defaultEntrypoint']),
'node_modules',
'nocodb',
'package.json'
),
'utf8'
)
);
} catch {
try {
packageInfo = JSON.parse(
fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')
);
} catch {}
}
}
const packageVersion = packageInfo?.version;
export { packageVersion, packageInfo };

4
packages/nocodb/src/lib/utils/projectAcl.ts

@ -1,4 +1,4 @@
import { OrgUserRoles } from '../../enums/OrgUserRoles'
import { OrgUserRoles } from '../../enums/OrgUserRoles';
export default {
owner: {
@ -303,4 +303,4 @@ export default {
auditRowUpdate: true,
},
},
}
};

2
packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts

@ -3,13 +3,13 @@ import path from 'path';
import axios from 'axios';
import { Router } from 'express';
import { Tele } from 'nc-help';
import { NcConfig } from '../../interface/config';
import SqlClientFactory from '../db/sql-client/lib/SqlClientFactory';
import Migrator from '../db/sql-migrator/lib/KnexMigrator';
import Noco from '../Noco';
import { Tele } from '../utils/Tele';
import { GqlApiBuilder } from './gql/GqlApiBuilder';
import { XCEeError } from '../meta/NcMetaMgr';
import { RestApiBuilder } from './rest/RestApiBuilder';

2
packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts

@ -3,7 +3,6 @@ import { promisify } from 'util';
import bcrypt from 'bcryptjs';
import * as ejs from 'ejs';
import * as jwt from 'jsonwebtoken';
import { Tele } from 'nc-help';
import passport from 'passport';
import { Strategy as AuthTokenStrategy } from 'passport-auth-token';
import { Strategy as GithubStrategy } from 'passport-github';
@ -30,6 +29,7 @@ const { isEmail } = require('validator');
import axios from 'axios';
import IEmailAdapter from '../../../interface/IEmailAdapter';
import { Tele } from '../../utils/Tele';
import XcCache from '../plugins/adapters/cache/XcCache';
passport.serializeUser(function (

2
packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts

@ -1,8 +1,8 @@
import { Tele } from 'nc-help';
import passport from 'passport';
import { Strategy } from 'passport-jwt';
import { v4 as uuidv4 } from 'uuid';
import validator from 'validator';
import { Tele } from '../../utils/Tele';
import XcCache from '../plugins/adapters/cache/XcCache';

2
packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts

@ -2,6 +2,7 @@ import { NcConfig } from '../../interface/config';
import debug from 'debug';
import NcMetaIO from '../meta/NcMetaIO';
import { Tele } from '../utils/Tele';
import ncProjectEnvUpgrader from './ncProjectEnvUpgrader';
import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045';
import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000';
@ -9,7 +10,6 @@ import ncDataTypesUpgrader from './ncDataTypesUpgrader';
import ncProjectRolesUpgrader from './ncProjectRolesUpgrader';
const log = debug('nc:version-upgrader');
import { Tele } from 'nc-help';
import boxen from 'boxen';
export interface NcUpgraderCtx {

3
packages/nocodb/src/lib/version-upgrader/ncProjectRolesUpgrader.ts

@ -1,4 +1,4 @@
import { OrgUserRoles } from '../../enums/OrgUserRoles'
import { OrgUserRoles } from '../../enums/OrgUserRoles';
import { MetaTable } from '../utils/globals';
import { NcUpgraderCtx } from './NcUpgrader';
@ -10,6 +10,7 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
user.roles = user.roles
.split(',')
.map((r) => {
// update old role names with new roles
if (r === 'user') {
return OrgUserRoles.CREATOR;
} else if (r === 'user-new') {

55
scripts/sdk/swagger.json

@ -441,9 +441,54 @@
},
"parameters": []
},
"/api/v1/tokens": {
"get": {
"summary": "Organisation API Tokens List",
"operationId": "org-tokens-list",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"users": {
"type": "object",
"properties": {
"list": {
"type": "array",
"uniqueItems": true,
"minItems": 1,
"items": {
"$ref": "#/components/schemas/ApiToken"
}
},
"pageInfo": {
"$ref": "#/components/schemas/Paginated"
}
},
"required": [
"list",
"pageInfo"
]
}
}
}
}
}
}
},
"description": "",
"tags": [
"Org tokens"
]
},
"parameters": []
},
"/api/v1/users": {
"get": {
"summary": "Project users",
"summary": "Organisation Users",
"operationId": "org-users-list",
"responses": {
"200": {
@ -486,7 +531,7 @@
},
"parameters": [],
"post": {
"summary": "Project User Add",
"summary": "Organisation User Add",
"operationId": "org-users-add",
"responses": {
"200": {
@ -524,7 +569,7 @@
}
],
"patch": {
"summary": "",
"summary": "Organisation User Update",
"operationId": "org-users-update",
"responses": {
"200": {
@ -547,7 +592,7 @@
}
},
"delete": {
"summary": "",
"summary": "Organisation User Delete",
"operationId": "org-users-delete",
"responses": {
"200": {
@ -572,7 +617,7 @@
}
],
"post": {
"summary": "",
"summary": "Organisation User Invite",
"operationId": "org-users-resend-invite",
"responses": {
"200": {

Loading…
Cancel
Save