Browse Source

Merge pull request #4696 from nocodb/fix/misc

Miscellaneous bug fixes and enhancements
pull/4706/head
Pranav C 2 years ago committed by GitHub
parent
commit
77fea98c1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/nc-gui/components.d.ts
  2. 15
      packages/nc-gui/components/dashboard/TreeView.vue
  3. 67
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  4. 6
      packages/nc-gui/components/general/ViewIcon.vue
  5. 12
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  6. 31
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  7. 2
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  8. 1
      packages/nc-gui/composables/useGlobal/types.ts
  9. 13
      packages/nc-gui/composables/useSharedView.ts
  10. 2
      packages/nc-gui/layouts/shared-view.vue
  11. 8
      packages/nc-gui/middleware/auth.global.ts
  12. 72
      packages/nc-gui/pages/index/index/create-external.vue
  13. 16
      packages/nocodb/src/lib/Noco.ts
  14. 19
      packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts
  15. 19
      packages/nocodb/src/lib/db/sql-client/lib/ee/SqlClientFactoryEE.ts
  16. 11
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts
  17. 8
      packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts
  18. 58
      packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
  19. 81
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  20. 3
      packages/nocodb/src/lib/meta/api/utilApis.ts
  21. 1
      packages/nocodb/src/lib/migrations/v2/nc_013_sync_source.ts

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

@ -90,6 +90,7 @@ declare module '@vue/runtime-core' {
LogosMysqlIcon: typeof import('~icons/logos/mysql-icon')['default'] LogosMysqlIcon: typeof import('~icons/logos/mysql-icon')['default']
LogosPostgresql: typeof import('~icons/logos/postgresql')['default'] LogosPostgresql: typeof import('~icons/logos/postgresql')['default']
LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default'] LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSnowflakeIcon: typeof import('~icons/logos/snowflake-icon')['default']
LogosSwagger: typeof import('~icons/logos/swagger')['default'] LogosSwagger: typeof import('~icons/logos/swagger')['default']
MaterialSymbolsAccountTreeRounded: typeof import('~icons/material-symbols/account-tree-rounded')['default'] MaterialSymbolsAccountTreeRounded: typeof import('~icons/material-symbols/account-tree-rounded')['default']
MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default'] MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default']
@ -142,6 +143,7 @@ declare module '@vue/runtime-core' {
MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default'] MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default'] MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['default'] MdiCodeJson: typeof import('~icons/mdi/code-json')['default']
MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiCog: typeof import('~icons/mdi/cog')['default'] MdiCog: typeof import('~icons/mdi/cog')['default']
MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default'] MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
@ -226,7 +228,6 @@ declare module '@vue/runtime-core' {
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default'] MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStorefrontOutline: typeof import('~icons/mdi/storefront-outline')['default'] MdiStorefrontOutline: typeof import('~icons/mdi/storefront-outline')['default']
MdiTable: typeof import('~icons/mdi/table')['default'] MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiTableColumnPlusAfter: typeof import('~icons/mdi/table-column-plus-after')['default'] MdiTableColumnPlusAfter: typeof import('~icons/mdi/table-column-plus-after')['default']
MdiTableColumnPlusBefore: typeof import('~icons/mdi/table-column-plus-before')['default'] MdiTableColumnPlusBefore: typeof import('~icons/mdi/table-column-plus-before')['default']
MdiTableLarge: typeof import('~icons/mdi/table-large')['default'] MdiTableLarge: typeof import('~icons/mdi/table-large')['default']

15
packages/nc-gui/components/dashboard/TreeView.vue

@ -18,6 +18,7 @@ import {
ref, ref,
resolveComponent, resolveComponent,
useDialog, useDialog,
useGlobal,
useNuxtApp, useNuxtApp,
useProject, useProject,
useRoute, useRoute,
@ -46,6 +47,8 @@ const route = useRoute()
const [searchActive, toggleSearchActive] = useToggle() const [searchActive, toggleSearchActive] = useToggle()
const { appInfo } = useGlobal()
const toggleDialog = inject(ToggleDialogInj, () => {}) const toggleDialog = inject(ToggleDialogInj, () => {})
const keys = $ref<Record<string, number>>({}) const keys = $ref<Record<string, number>>({})
@ -414,7 +417,11 @@ const setIcon = async (icon: string, table: TableType) => {
MSSQL MSSQL
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE)"> <a-menu-item
v-if="appInfo.ee"
key="connect-new-source"
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE)"
>
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-project-menu-item group">
<LogosSnowflakeIcon class="group-hover:text-accent" /> <LogosSnowflakeIcon class="group-hover:text-accent" />
Snowflake Snowflake
@ -530,7 +537,11 @@ const setIcon = async (icon: string, table: TableType) => {
MSSQL MSSQL
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE)"> <a-menu-item
v-if="appInfo.ee"
key="connect-new-source"
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE)"
>
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-project-menu-item group">
<LogosSnowflakeIcon class="group-hover:text-accent" /> <LogosSnowflakeIcon class="group-hover:text-accent" />
Snowflake Snowflake

67
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -8,7 +8,7 @@ import {
DefaultConnection, DefaultConnection,
SQLiteConnection, SQLiteConnection,
SSLUsage, SSLUsage,
clientTypes, clientTypes as _clientTypes,
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
fieldRequiredValidator, fieldRequiredValidator,
@ -21,6 +21,7 @@ import {
readFile, readFile,
ref, ref,
useApi, useApi,
useGlobal,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
watch, watch,
@ -30,6 +31,8 @@ const { connectionType } = defineProps<{ connectionType: ClientType }>()
const emit = defineEmits(['baseCreated']) const emit = defineEmits(['baseCreated'])
const { appInfo } = useGlobal()
const { project, loadProject } = useProject() const { project, loadProject } = useProject()
const useForm = Form.useForm const useForm = Form.useForm
@ -68,7 +71,43 @@ const customFormState = ref<ProjectCreateForm>({
extraParameters: [], extraParameters: [],
}) })
const clientTypes = computed(() => {
return _clientTypes.filter((type) => {
return appInfo.value?.ee || type.value !== ClientType.SNOWFLAKE
})
})
const validators = computed(() => { const validators = computed(() => {
let clientValidations: Record<string, any[]> = {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
}
switch (formState.dataSource.client) {
case ClientType.SQLITE:
clientValidations = {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
break
case ClientType.SNOWFLAKE:
clientValidations = {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
break
case ClientType.PG:
case ClientType.MSSQL:
clientValidations['dataSource.searchPath.0'] = [fieldRequiredValidator()]
break
}
return { return {
'title': [ 'title': [
{ {
@ -79,31 +118,7 @@ const validators = computed(() => {
], ],
'extraParameters': [extraParameterValidator], 'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()], 'dataSource.client': [fieldRequiredValidator()],
...(formState.dataSource.client === ClientType.SQLITE ...clientValidations,
? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
: formState.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
...([ClientType.PG, ClientType.MSSQL].includes(formState.dataSource.client)
? {
'dataSource.searchPath.0': [fieldRequiredValidator()],
}
: {}),
}),
} }
}) })

6
packages/nc-gui/components/general/ViewIcon.vue

@ -1,11 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Icon as IcIcon } from '@iconify/vue' import { Icon as IcIcon } from '@iconify/vue'
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import { viewIcons } from '#imports' import { toRef, viewIcons } from '#imports'
const { meta: viewMeta } = defineProps<{ const props = defineProps<{
meta: TableType meta: TableType
}>() }>()
const viewMeta = toRef(props, 'meta')
</script> </script>
<template> <template>

12
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -168,9 +168,9 @@ function onStopEdit() {
<div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2" data-testid="view-item"> <div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2" data-testid="view-item">
<div class="flex w-auto min-w-5" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`"> <div class="flex w-auto min-w-5" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<a-dropdown :trigger="['click']" @click.stop> <a-dropdown :trigger="['click']" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'"> <component :is="isUIAllowed('viewIconCustomisation') ? Tooltip : 'div'">
<GeneralViewIcon :meta="props.view" class="nc-view-icon"></GeneralViewIcon> <GeneralViewIcon :meta="props.view" class="nc-view-icon"></GeneralViewIcon>
<template v-if="isUIAllowed('tableIconCustomisation')" #title>Change icon</template> <template v-if="isUIAllowed('viewIconCustomisation')" #title>Change icon</template>
</component> </component>
<template v-if="isUIAllowed('viewIconCustomisation')" #overlay> <template v-if="isUIAllowed('viewIconCustomisation')" #overlay>
@ -179,7 +179,13 @@ function onStopEdit() {
</a-dropdown> </a-dropdown>
</div> </div>
<a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" /> <a-input
v-if="isEditing"
:ref="focusInput"
v-model:value="vModel.title"
@blur="onCancel"
@keydown.stop="onKeyDown($event)"
/>
<div v-else> <div v-else>
<LazyGeneralTruncateText>{{ vModel.alias || vModel.title }}</LazyGeneralTruncateText> <LazyGeneralTruncateText>{{ vModel.alias || vModel.title }}</LazyGeneralTruncateText>

31
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -196,6 +196,26 @@ watch(passwordProtected, (value) => {
const { locale } = useI18n() const { locale } = useI18n()
const isRtl = computed(() => isRtlLang(locale.value as any)) const isRtl = computed(() => isRtlLang(locale.value as any))
const iframeCode = computed(() => {
if (!sharedViewUrl.value) return
return `<iframe class="nc-embed"
"src="${sharedViewUrl.value}?embed"
frameborder="0"
width="100%"
height="700"
style="background: transparent; border: 1px solid #ddd"/>`
})
const copyIframeCode = async () => {
if (iframeCode.value) {
await copy(iframeCode.value)
// Copied to clipboard
message.success(t('msg.info.copiedToClipboard'))
}
}
</script> </script>
<template> <template>
@ -228,7 +248,7 @@ const isRtl = computed(() => isRtlLang(locale.value as any))
data-testid="nc-modal-share-view__link" data-testid="nc-modal-share-view__link"
class="share-link-box !bg-primary !bg-opacity-5 ring-1 ring-accent ring-opacity-100" class="share-link-box !bg-primary !bg-opacity-5 ring-1 ring-accent ring-opacity-100"
> >
<div class="flex-1 h-min text-xs">{{ sharedViewUrl }}</div> <div class="flex-1 h-min text-xs text-gray-500">{{ sharedViewUrl }}</div>
<a v-e="['c:view:share:open-url']" :href="sharedViewUrl" target="_blank"> <a v-e="['c:view:share:open-url']" :href="sharedViewUrl" target="_blank">
<MdiOpenInNew class="text-sm text-gray-500 mt-2" /> <MdiOpenInNew class="text-sm text-gray-500 mt-2" />
@ -237,6 +257,13 @@ const isRtl = computed(() => isRtlLang(locale.value as any))
<MdiContentCopy v-e="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" /> <MdiContentCopy v-e="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" />
</div> </div>
<div
class="flex gap-1 items-center pb-1 text-gray-500 cursor-pointer font-weight-medium mb-2 mt-4 pl-1"
@click="copyIframeCode"
>
<MdiCodeTags class="text-gray-500" /> Embed this view in your site
</div>
<div class="px-1 mt-2 flex flex-col gap-3"> <div class="px-1 mt-2 flex flex-col gap-3">
<!-- todo: i18n --> <!-- todo: i18n -->
<div class="text-gray-500 border-b-1">Options</div> <div class="text-gray-500 border-b-1">Options</div>
@ -352,7 +379,7 @@ const isRtl = computed(() => isRtlLang(locale.value as any))
<style scoped> <style scoped>
.share-link-box { .share-link-box {
@apply flex p-2 w-full items-center items-center gap-1 bg-gray-100 rounded; @apply flex p-2 w-full items-center items-center gap-2 bg-gray-100 rounded;
} }
:deep(.ant-collapse-header) { :deep(.ant-collapse-header) {

2
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -95,7 +95,7 @@ useMenuCloseOnEsc(open)
<GeneralViewIcon :meta="selectedView"></GeneralViewIcon> <GeneralViewIcon :meta="selectedView"></GeneralViewIcon>
<span class="!text-sm font-weight-normal"> <span class="!text-sm font-weight-normal">
<GeneralTruncateText>{{ selectedView?.title }}</GeneralTruncateText> <GeneralTruncateText :key="selectedView?.title">{{ selectedView?.title }}</GeneralTruncateText>
</span> </span>
<component :is="Icon" class="text-gray-500" :class="`nc-icon-${selectedView?.lock_type}`" /> <component :is="Icon" class="text-gray-500" :class="`nc-icon-${selectedView?.lock_type}`" />

1
packages/nc-gui/composables/useGlobal/types.ts

@ -25,6 +25,7 @@ export interface AppInfo {
teleEnabled: boolean teleEnabled: boolean
type: string type: string
version: string version: string
ee?: boolean
} }
export interface StoredState { export interface StoredState {

13
packages/nc-gui/composables/useSharedView.ts

@ -17,7 +17,7 @@ export function useSharedView() {
const { appInfo } = $(useGlobal()) const { appInfo } = $(useGlobal())
const { loadProject } = useProject() const { project } = useProject()
const appInfoDefaultLimit = appInfo.defaultLimit || 25 const appInfoDefaultLimit = appInfo.defaultLimit || 25
@ -77,7 +77,16 @@ export function useSharedView() {
await setMeta(viewMeta.model) await setMeta(viewMeta.model)
await loadProject(true, viewMeta.project_id) // if project is not defined then set it with an object containing base
if (!project.value?.bases)
project.value = {
bases: [
{
id: viewMeta.base_id,
type: viewMeta.client,
},
],
}
const relatedMetas = { ...viewMeta.relatedMetas } const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key])) Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))

2
packages/nc-gui/layouts/shared-view.vue

@ -59,7 +59,7 @@ export default {
</template> </template>
<div v-else class="text-xl font-semibold truncate text-white nc-shared-view-title flex gap-2 items-center"> <div v-else class="text-xl font-semibold truncate text-white nc-shared-view-title flex gap-2 items-center">
<GeneralViewIcon class="!text-xl" :meta="sharedView" /> <GeneralViewIcon v-if="sharedView" class="!text-xl" :meta="sharedView" />
{{ sharedView?.title }} {{ sharedView?.title }}
</div> </div>
</div> </div>

8
packages/nc-gui/middleware/auth.global.ts

@ -1,6 +1,6 @@
import type { Api } from 'nocodb-sdk' import type { Api } from 'nocodb-sdk'
import type { Actions } from '~/composables/useGlobal/types' import type { Actions } from '~/composables/useGlobal/types'
import { defineNuxtRouteMiddleware, message, navigateTo, useApi, useGlobal, useRoles } from '#imports' import { defineNuxtRouteMiddleware, extractSdkResponseErrorMsg, message, navigateTo, useApi, useGlobal, useRoles } from '#imports'
/** /**
* Global auth middleware * Global auth middleware
@ -98,10 +98,8 @@ async function tryGoogleAuth(api: Api<any>, signIn: Actions['signIn']) {
) )
signIn(token) signIn(token)
} catch (e: any) { } catch (e) {
if (e.response && e.response.data && e.response.data.msg) { message.error({ content: await extractSdkResponseErrorMsg(e) })
message.error({ content: e.response.data.msg })
}
} }
const newURL = window.location.href.split('?')[0] const newURL = window.location.href.split('?')[0]

72
packages/nc-gui/pages/index/index/create-external.vue

@ -7,7 +7,7 @@ import {
Form, Form,
Modal, Modal,
SSLUsage, SSLUsage,
clientTypes, clientTypes as _clientTypes,
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
fieldRequiredValidator, fieldRequiredValidator,
@ -28,6 +28,8 @@ import {
watch, watch,
} from '#imports' } from '#imports'
const { appInfo } = useGlobal()
const useForm = Form.useForm const useForm = Form.useForm
const testSuccess = ref(false) const testSuccess = ref(false)
@ -64,7 +66,43 @@ const customFormState = ref<ProjectCreateForm>({
extraParameters: [], extraParameters: [],
}) })
const clientTypes = computed(() => {
return _clientTypes.filter((type) => {
return appInfo.value?.ee || type.value !== ClientType.SNOWFLAKE
})
})
const validators = computed(() => { const validators = computed(() => {
let clientValidations: Record<string, any[]> = {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
}
switch (formState.dataSource.client) {
case ClientType.SQLITE:
clientValidations = {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
break
case ClientType.SNOWFLAKE:
clientValidations = {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
break
case ClientType.PG:
case ClientType.MSSQL:
clientValidations['dataSource.searchPath.0'] = [fieldRequiredValidator()]
break
}
return { return {
'title': [ 'title': [
{ {
@ -75,31 +113,7 @@ const validators = computed(() => {
], ],
'extraParameters': [extraParameterValidator], 'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()], 'dataSource.client': [fieldRequiredValidator()],
...(formState.dataSource.client === ClientType.SQLITE ...clientValidations,
? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
: formState.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
...([ClientType.PG, ClientType.MSSQL].includes(formState.dataSource.client)
? {
'dataSource.searchPath.0': [fieldRequiredValidator()],
}
: {}),
}),
} }
}) })
@ -484,7 +498,7 @@ onMounted(async () => {
</template> </template>
<a-form-item label="SSL mode"> <a-form-item label="SSL mode">
<a-select v-model:value="formState.sslUse" dropdown-class-name="nc-dropdown-ssl-mode" @select="onSSLModeChange"> <a-select v-model:value="formState.sslUse" dropdown-class-name="nc-dropdown-ssl-mode" @select="onSSLModeChange">
<a-select-option v-for="opt in Object.values(SSLUsage)" :key="opt" :value="opt">{{ opt }}</a-select-option> <a-select-option v-for="opt in Object.values(SSLUsage)" :key="opt" :value="opt">{{ opt }} </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@ -547,7 +561,9 @@ onMounted(async () => {
</div> </div>
</div> </div>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewParam"> <a-button type="dashed" class="w-full caption mt-2" @click="addNewParam">
<div class="flex items-center justify-center"><MdiPlus /></div> <div class="flex items-center justify-center">
<MdiPlus />
</div>
</a-button> </a-button>
</a-card> </a-card>
</a-form-item> </a-form-item>

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

@ -16,7 +16,9 @@ import requestIp from 'request-ip';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { NcConfig } from '../interface/config'; import { NcConfig } from '../interface/config';
import { NC_LICENSE_KEY } from './constants';
import Migrator from './db/sql-migrator/lib/KnexMigrator'; import Migrator from './db/sql-migrator/lib/KnexMigrator';
import Store from './models/Store';
import NcConfigFactory from './utils/NcConfigFactory'; import NcConfigFactory from './utils/NcConfigFactory';
import { Tele } from 'nc-help'; import { Tele } from 'nc-help';
@ -53,6 +55,7 @@ const NcProjectBuilder = process.env.EE
export default class Noco { export default class Noco {
private static _this: Noco; private static _this: Noco;
private static ee: boolean;
public static get dashboardUrl(): string { public static get dashboardUrl(): string {
let siteUrl = `http://localhost:${process.env.PORT || 8080}`; let siteUrl = `http://localhost:${process.env.PORT || 8080}`;
@ -192,6 +195,7 @@ export default class Noco {
} }
await Noco._ncMeta.metaInit(); await Noco._ncMeta.metaInit();
await Noco.loadEEState();
await this.initJwt(); await this.initJwt();
await initAdminFromEnv(); await initAdminFromEnv();
@ -542,4 +546,16 @@ export default class Noco {
public static getConfig(): NcConfig { public static getConfig(): NcConfig {
return Noco.config; return Noco.config;
} }
public static isEE(): boolean {
return Noco.ee;
}
public static async loadEEState(): Promise<boolean> {
try {
return (Noco.ee = !!(await Store.get(NC_LICENSE_KEY)));
} catch {
return (Noco.ee = false);
}
}
} }

19
packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts

@ -1,3 +1,5 @@
import Noco from '../../../Noco';
import SqlClientFactoryEE from './ee/SqlClientFactoryEE';
import MySqlClient from './mysql/MysqlClient'; import MySqlClient from './mysql/MysqlClient';
import MssqlClient from './mssql/MssqlClient'; import MssqlClient from './mssql/MssqlClient';
import OracleClient from './oracle/OracleClient'; import OracleClient from './oracle/OracleClient';
@ -6,10 +8,8 @@ import PgClient from './pg/PgClient';
import YugabyteClient from './pg/YugabyteClient'; import YugabyteClient from './pg/YugabyteClient';
import TidbClient from './mysql/TidbClient'; import TidbClient from './mysql/TidbClient';
import VitessClient from './mysql/VitessClient'; import VitessClient from './mysql/VitessClient';
import SfClient from './snowflake/SnowflakeClient';
import { SnowflakeClient } from 'nc-help';
class SqlClientFactory { export class SqlClientFactory {
static create(connectionConfig) { static create(connectionConfig) {
connectionConfig.meta = connectionConfig.meta || {}; connectionConfig.meta = connectionConfig.meta || {};
connectionConfig.pool = connectionConfig.pool || { min: 0, max: 5 }; connectionConfig.pool = connectionConfig.pool || { min: 0, max: 5 };
@ -33,13 +33,18 @@ class SqlClientFactory {
if (connectionConfig.meta.dbtype === 'yugabyte') if (connectionConfig.meta.dbtype === 'yugabyte')
return new YugabyteClient(connectionConfig); return new YugabyteClient(connectionConfig);
return new PgClient(connectionConfig); return new PgClient(connectionConfig);
} else if (connectionConfig.client === 'snowflake') {
connectionConfig.client = SnowflakeClient;
return new SfClient(connectionConfig);
} }
throw new Error('Database not supported'); throw new Error('Database not supported');
} }
} }
export default SqlClientFactory; export default class {
static create(connectionConfig) {
if (Noco.isEE()) {
return SqlClientFactoryEE.create(connectionConfig);
}
return SqlClientFactory.create(connectionConfig);
}
}

19
packages/nocodb/src/lib/db/sql-client/lib/ee/SqlClientFactoryEE.ts

@ -0,0 +1,19 @@
import { SqlClientFactory } from '../SqlClientFactory';
import SfClient from '../snowflake/SnowflakeClient';
import { SnowflakeClient } from 'nc-help';
class SqlClientFactoryEE {
static create(connectionConfig) {
connectionConfig.meta = connectionConfig.meta || {};
connectionConfig.pool = connectionConfig.pool || { min: 0, max: 5 };
connectionConfig.meta.dbtype = connectionConfig.meta.dbtype || '';
if (connectionConfig.client === 'snowflake') {
connectionConfig.client = SnowflakeClient;
return new SfClient(connectionConfig);
}
return SqlClientFactory.create(connectionConfig);
}
}
export default SqlClientFactoryEE;

11
packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts

@ -124,8 +124,10 @@ async function getDataList(model, view: View, req) {
); );
count = await baseModel.count(listArgs); count = await baseModel.count(listArgs);
} catch (e) { } catch (e) {
// show empty result instead of throwing error here console.log(e);
// e.g. search some text in a numeric field NcError.internalServerError(
'Internal Server Error, check server log for more details'
);
} }
return new PagedResponseImpl(data, { return new PagedResponseImpl(data, {
@ -281,8 +283,9 @@ async function getGroupedDataList(model, view: View, req) {
}); });
} catch (e) { } catch (e) {
console.log(e); console.log(e);
// show empty result instead of throwing error here NcError.internalServerError(
// e.g. search some text in a numeric field 'Internal Server Error, check server log for more details'
);
} }
return data; return data;
} }

8
packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts

@ -328,7 +328,9 @@ async function dataRead(req: Request, res: Response, next) {
); );
} catch (e) { } catch (e) {
console.log(e); console.log(e);
res.status(500).json({ msg: e.message }); NcError.internalServerError(
'Internal Server Error, check server log for more details'
);
} }
} }
@ -465,6 +467,10 @@ async function getDataList(model, view: View, req) {
} catch (e) { } catch (e) {
// show empty result instead of throwing error here // show empty result instead of throwing error here
// e.g. search some text in a numeric field // e.g. search some text in a numeric field
console.log(e);
NcError.internalServerError(
'Internal Server Error, check server log for more details'
);
} }
return new PagedResponseImpl(data, { return new PagedResponseImpl(data, {

58
packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts

@ -7,7 +7,6 @@ import passport from 'passport';
import passportJWT from 'passport-jwt'; import passportJWT from 'passport-jwt';
import { Strategy as AuthTokenStrategy } from 'passport-auth-token'; import { Strategy as AuthTokenStrategy } from 'passport-auth-token';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { randomTokenString } from '../../helpers/stringHelpers';
const PassportLocalStrategy = require('passport-local').Strategy; const PassportLocalStrategy = require('passport-local').Strategy;
const ExtractJwt = passportJWT.ExtractJwt; const ExtractJwt = passportJWT.ExtractJwt;
@ -24,6 +23,7 @@ import { CacheGetType, CacheScope } from '../../../utils/globals';
import ApiToken from '../../../models/ApiToken'; import ApiToken from '../../../models/ApiToken';
import Noco from '../../../Noco'; import Noco from '../../../Noco';
import Plugin from '../../../models/Plugin'; import Plugin from '../../../models/Plugin';
import { registerNewUserIfAllowed } from './userApis';
export function initStrategies(router): void { export function initStrategies(router): void {
passport.use( passport.use(
@ -284,41 +284,35 @@ export function initStrategies(router): void {
User.getByEmail(email) User.getByEmail(email)
.then(async (user) => { .then(async (user) => {
if (req.ncProjectId) { if (user) {
ProjectUser.get(req.ncProjectId, user.id) // if project id defined extract project level roles
.then(async (projectUser) => { if (req.ncProjectId) {
user.roles = projectUser?.roles || user.roles; ProjectUser.get(req.ncProjectId, user.id)
user.roles = .then(async (projectUser) => {
user.roles === 'owner' ? 'owner,creator' : user.roles; user.roles = projectUser?.roles || user.roles;
// + (user.roles ? `,${user.roles}` : ''); user.roles =
user.roles === 'owner' ? 'owner,creator' : user.roles;
// + (user.roles ? `,${user.roles}` : '');
done(null, user); done(null, user);
}) })
.catch((e) => done(e)); .catch((e) => done(e));
} else {
// const roles = projectUser?.roles ? JSON.parse(projectUser.roles) : {guest: true};
if (user) {
return done(null, user);
} else { } else {
let roles = 'editor';
if (!(await User.isFirst())) {
roles = 'owner';
}
if (roles === 'editor') {
return done(new Error('User not found'));
}
const salt = await promisify(bcrypt.genSalt)(10);
user = await await User.insert({
email: profile.emails[0].value,
password: '',
salt,
roles,
email_verified: true,
token_version: randomTokenString(),
});
return done(null, user); return done(null, user);
} }
// if user not found create new user if allowed
// or return error
} else {
const salt = await promisify(bcrypt.genSalt)(10);
const user = await registerNewUserIfAllowed({
firstname: null,
lastname: null,
email_verification_token: null,
email: profile.emails[0].value,
password: '',
salt,
});
return done(null, user);
} }
}) })
.catch((err) => { .catch((err) => {

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

@ -25,6 +25,58 @@ import Noco from '../../../Noco';
import { genJwt } from './helpers'; import { genJwt } from './helpers';
import { randomTokenString } from '../../helpers/stringHelpers'; import { randomTokenString } from '../../helpers/stringHelpers';
export async function registerNewUserIfAllowed({
firstname,
lastname,
email,
salt,
password,
email_verification_token,
}: {
firstname;
lastname;
email: string;
salt: any;
password;
email_verification_token;
}) {
let roles: string = OrgUserRoles.CREATOR;
if (await User.isFirst()) {
roles = `${OrgUserRoles.CREATOR},${OrgUserRoles.SUPER_ADMIN}`;
// todo: update in nc_store
// roles = 'owner,creator,editor'
Tele.emit('evt', {
evt_type: 'project:invite',
count: 1,
});
} else {
let settings: { invite_only_signup?: boolean } = {};
try {
settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value);
} catch {}
if (settings?.invite_only_signup) {
NcError.badRequest('Not allowed to signup, contact super admin.');
} else {
roles = OrgUserRoles.VIEWER;
}
}
const token_version = randomTokenString();
return await User.insert({
firstname,
lastname,
email,
salt,
password,
email_verification_token,
roles,
token_version,
});
}
export async function signup(req: Request, res: Response<TableType>) { export async function signup(req: Request, res: Response<TableType>) {
const { const {
email: _email, email: _email,
@ -88,40 +140,13 @@ export async function signup(req: Request, res: Response<TableType>) {
NcError.badRequest('User already exist'); NcError.badRequest('User already exist');
} }
} else { } else {
let roles: string = OrgUserRoles.CREATOR; await registerNewUserIfAllowed({
if (await User.isFirst()) {
roles = `${OrgUserRoles.CREATOR},${OrgUserRoles.SUPER_ADMIN}`;
// todo: update in nc_store
// roles = 'owner,creator,editor'
Tele.emit('evt', {
evt_type: 'project:invite',
count: 1,
});
} else {
let settings: { invite_only_signup?: boolean } = {};
try {
settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value);
} catch {}
if (settings?.invite_only_signup) {
NcError.badRequest('Not allowed to signup, contact super admin.');
} else {
roles = OrgUserRoles.VIEWER;
}
}
const token_version = randomTokenString();
await User.insert({
firstname, firstname,
lastname, lastname,
email, email,
salt, salt,
password, password,
email_verification_token, email_verification_token,
roles,
token_version,
}); });
} }
user = await User.getByEmail(email); user = await User.getByEmail(email);

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

@ -3,7 +3,9 @@ import { Request, Response } from 'express';
import { compareVersions, validate } from 'compare-versions'; import { compareVersions, validate } from 'compare-versions';
import { ViewTypes } from 'nocodb-sdk'; import { ViewTypes } from 'nocodb-sdk';
import { NC_LICENSE_KEY } from '../../constants';
import Project from '../../models/Project'; import Project from '../../models/Project';
import Store from '../../models/Store';
import Noco from '../../Noco'; import Noco from '../../Noco';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import { MetaTable } from '../../utils/globals'; import { MetaTable } from '../../utils/globals';
@ -55,6 +57,7 @@ export async function appInfo(req: Request, res: Response) {
ncMin: !!process.env.NC_MIN, ncMin: !!process.env.NC_MIN,
teleEnabled: !process.env.NC_DISABLE_TELE, teleEnabled: !process.env.NC_DISABLE_TELE,
ncSiteUrl: (req as any).ncSiteUrl, ncSiteUrl: (req as any).ncSiteUrl,
ee: !!(await Store.get(NC_LICENSE_KEY)),
}; };
res.json(result); res.json(result);

1
packages/nocodb/src/lib/migrations/v2/nc_013_sync_source.ts

@ -38,6 +38,7 @@ const up = async (knex: Knex) => {
}; };
const down = async (knex) => { const down = async (knex) => {
await knex.schema.dropTable(MetaTable.SYNC_LOGS);
await knex.schema.dropTable(MetaTable.SYNC_SOURCE); await knex.schema.dropTable(MetaTable.SYNC_SOURCE);
}; };

Loading…
Cancel
Save