diff --git a/packages/nc-gui/assets/img/brand/Transparent.png b/packages/nc-gui/assets/img/brand/Transparent.png
deleted file mode 100644
index d7a68ffa31..0000000000
Binary files a/packages/nc-gui/assets/img/brand/Transparent.png and /dev/null differ
diff --git a/packages/nc-gui/assets/img/brand/favicon-128.png b/packages/nc-gui/assets/img/brand/favicon-128.png
deleted file mode 100644
index ecc0ed692f..0000000000
Binary files a/packages/nc-gui/assets/img/brand/favicon-128.png and /dev/null differ
diff --git a/packages/nc-gui/assets/img/brand/favicon-16.png b/packages/nc-gui/assets/img/brand/favicon-16.png
deleted file mode 100644
index ebaa144cc7..0000000000
Binary files a/packages/nc-gui/assets/img/brand/favicon-16.png and /dev/null differ
diff --git a/packages/nc-gui/assets/img/brand/favicon-32.png b/packages/nc-gui/assets/img/brand/favicon-32.png
deleted file mode 100644
index 1c835348ea..0000000000
Binary files a/packages/nc-gui/assets/img/brand/favicon-32.png and /dev/null differ
diff --git a/packages/nc-gui/assets/img/brand/favicon-64.png b/packages/nc-gui/assets/img/brand/favicon-64.png
deleted file mode 100644
index 286a79d9d8..0000000000
Binary files a/packages/nc-gui/assets/img/brand/favicon-64.png and /dev/null differ
diff --git a/packages/nc-gui/assets/img/brand/full-logo.png b/packages/nc-gui/assets/img/brand/full-logo.png
new file mode 100644
index 0000000000..adad1a5cca
Binary files /dev/null and b/packages/nc-gui/assets/img/brand/full-logo.png differ
diff --git a/packages/nc-gui/assets/img/brand/text.png b/packages/nc-gui/assets/img/brand/text.png
new file mode 100644
index 0000000000..b13fbb2772
Binary files /dev/null and b/packages/nc-gui/assets/img/brand/text.png differ
diff --git a/packages/nc-gui/assets/img/icon.png b/packages/nc-gui/assets/img/icon.png
deleted file mode 100644
index d7a68ffa31..0000000000
Binary files a/packages/nc-gui/assets/img/icon.png and /dev/null differ
diff --git a/packages/nc-gui/assets/img/icons/256.png b/packages/nc-gui/assets/img/icons/256.png
deleted file mode 100644
index ba362f49b5..0000000000
Binary files a/packages/nc-gui/assets/img/icons/256.png and /dev/null differ
diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts
index 72d94ba7b7..39218772d5 100644
--- a/packages/nc-gui/components.d.ts
+++ b/packages/nc-gui/components.d.ts
@@ -104,6 +104,7 @@ declare module '@vue/runtime-core' {
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
+ MdiAccountCircleOutline: typeof import('~icons/mdi/account-circle-outline')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
@@ -130,6 +131,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']
@@ -140,12 +142,14 @@ 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']
MdiDelete: typeof import('~icons/mdi/delete')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
+ MdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiDownload: typeof import('~icons/mdi/download')['default']
MdiDownloadOutline: typeof import('~icons/mdi/download-outline')['default']
@@ -176,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']
@@ -202,6 +207,7 @@ declare module '@vue/runtime-core' {
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
+ MdiShieldAccountOutline: typeof import('~icons/mdi/shield-account-outline')['default']
MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiSort: typeof import('~icons/mdi/sort')['default']
diff --git a/packages/nc-gui/components/account/AppStore.vue b/packages/nc-gui/components/account/AppStore.vue
new file mode 100644
index 0000000000..2a61f43be8
--- /dev/null
+++ b/packages/nc-gui/components/account/AppStore.vue
@@ -0,0 +1,8 @@
+
+
+
diff --git a/packages/nc-gui/components/account/License.vue b/packages/nc-gui/components/account/License.vue
new file mode 100644
index 0000000000..f12d793a28
--- /dev/null
+++ b/packages/nc-gui/components/account/License.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/account/ResetPassword.vue b/packages/nc-gui/components/account/ResetPassword.vue
new file mode 100644
index 0000000000..804196f102
--- /dev/null
+++ b/packages/nc-gui/components/account/ResetPassword.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
{{ $t('activity.changePwd') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/account/SignupSettings.vue b/packages/nc-gui/components/account/SignupSettings.vue
new file mode 100644
index 0000000000..7311679a76
--- /dev/null
+++ b/packages/nc-gui/components/account/SignupSettings.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
Settings
+
+
+
+ {{ $t('labels.inviteOnlySignup') }}
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/account/Token.vue b/packages/nc-gui/components/account/Token.vue
new file mode 100644
index 0000000000..967c1ef3b3
--- /dev/null
+++ b/packages/nc-gui/components/account/Token.vue
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/account/UserList.vue b/packages/nc-gui/components/account/UserList.vue
new file mode 100644
index 0000000000..a6ac569901
--- /dev/null
+++ b/packages/nc-gui/components/account/UserList.vue
@@ -0,0 +1,281 @@
+
+
+
+
+
User Management
+
+
+
+
+
+
+
{
+ showUserModal = true
+ userMadalKey++
+ }
+ "
+ >
+
+
+ Invite new user
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ text }}
+
+
+
+
+
+
+
+
+
Super Admin
+
+
+ {{ $t(`objects.roleType.orgLevelCreator`) }}
+
+ {{ $t('msg.info.roles.orgCreator') }}
+
+
+
+
+ {{ $t(`objects.roleType.orgLevelViewer`) }}
+
+ {{ $t('msg.info.roles.orgViewer') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('activity.resendInvite') }}
+
+
+
+
+
+
{{ $t('activity.copyInviteURL') }}
+
+
+
+
+
+
+
{{ $t('activity.copyPasswordResetURL') }}
+
+
+
+
+
+
{{ $t('general.delete') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/account/UsersModal.vue b/packages/nc-gui/components/account/UsersModal.vue
new file mode 100644
index 0000000000..14665b8561
--- /dev/null
+++ b/packages/nc-gui/components/account/UsersModal.vue
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
{{ $t('activity.inviteUser') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ inviteUrl }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('msg.info.userInviteNoSMTP') }}
+ {{ usersData.invitationToken && usersData.emails }}
+
+
+
+
+
+
+
+
{{ $t('activity.inviteMore') }}
+
+
+
+
+
+
+
+
+
+
{{ $t('activity.inviteUser') }}
+
+
+
+
+
+
+
+ {{ $t('datatype.Email') }}:
+
+
+
+
+
+
+
+ {{ $t('labels.selectUserRole') }}
+
+
+
+ {{ $t(`objects.roleType.orgLevelCreator`) }}
+
+ {{ $t('msg.info.roles.orgCreator') }}
+
+
+
+
+ {{ $t(`objects.roleType.orgLevelViewer`) }}
+
+ {{ $t('msg.info.roles.orgViewer') }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('activity.invite') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/lang/bn_IN.json b/packages/nc-gui/lang/bn_IN.json
index 001e4e305e..5b2fa5209a 100644
--- a/packages/nc-gui/lang/bn_IN.json
+++ b/packages/nc-gui/lang/bn_IN.json
@@ -591,6 +591,7 @@
"tableDeleted": "Deleted table successfully",
"generatePublicShareableReadonlyBase": "Generate publicly shareable readonly base",
"deleteViewConfirmation": "Are you sure you want to delete this view?",
+ "deleteTokenConfirmation": "Are you sure you want to delete this token?",
"deleteTableConfirmation": "Do you want to delete the table",
"showM2mTables": "Show M2M Tables",
"deleteKanbanStackConfirmation": "Deleting this stack will also remove the select option `{stackToBeDeleted}` from the `{groupingField}`. The records will move to the uncategorized stack."
diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json
index 87ef39984f..08db4c8610 100644
--- a/packages/nc-gui/lang/en.json
+++ b/packages/nc-gui/lang/en.json
@@ -104,7 +104,9 @@
"creator": "Creator",
"editor": "Editor",
"commenter": "Commenter",
- "viewer": "Viewer"
+ "viewer": "Viewer",
+ "orgLevelCreator": "Organization Level Creator",
+ "orgLevelViewer": "Organization Level Viewer"
},
"sqlVIew": "SQL View"
},
@@ -200,6 +202,7 @@
"codeSnippet": "Code Snippet"
},
"labels": {
+ "createdBy": "Created By",
"notifyVia": "Notify Via",
"projName": "Project name",
"tableName": "Table name",
@@ -296,7 +299,8 @@
"signUpWithGoogle": "Sign up with Google",
"signInWithGoogle": "Sign in with Google",
"agreeToTos": "By signing up, you agree to the Terms of Service",
- "welcomeToNc": "Welcome to NocoDB!"
+ "welcomeToNc": "Welcome to NocoDB!",
+ "inviteOnlySignup": "Allow signup only using invite url"
},
"activity": {
"createProject": "Create Project",
@@ -341,12 +345,14 @@
"invite": "Invite",
"inviteMore": "Invite more",
"inviteTeam": "Invite Team",
+ "inviteUser": "Invite User",
"inviteToken": "Invite Token",
"newUser": "New User",
"editUser": "Edit user",
"deleteUser": "Remove user from project",
"resendInvite": "Resend invite E-mail",
"copyInviteURL": "Copy invite URL",
+ "copyPasswordResetURL": "Copy password reset URL",
"newRole": "New role",
"reloadRoles": "Reload roles",
"nextPage": "Next page",
@@ -480,6 +486,10 @@
},
"msg": {
"info": {
+ "roles": {
+ "orgCreator": "Creator can create new projects and access any invited project.",
+ "orgViewer": "Viewer is not allowed to create new projects but they can access any invited project."
+ },
"footerInfo": "Rows per page",
"upload": "Select file to Upload",
"upload_sub": "or drag and drop file",
@@ -653,7 +663,8 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
- "fieldRequired": "{value} cannot be empty."
+ "fieldRequired": "{value} cannot be empty.",
+ "projectNotAccessible": "Project not accessible"
},
"toast": {
"exportMetadata": "Project metadata exported successfully",
@@ -683,13 +694,16 @@
"tableDataExported": "Successfully exported all table data",
"updated": "Successfully updated",
"sharedViewDeleted": "Deleted shared view successfully",
+ "userDeleted": "User deleted successfully",
"viewRenamed": "View renamed successfully",
"tokenGenerated": "Token generated successfully",
"tokenDeleted": "Token deleted successfully",
"userAddedToProject": "Successfully added user to project",
+ "userAdded": "Successfully added user",
"userDeletedFromProject": "Successfully deleted user from project",
"inviteEmailSent": "Invite Email sent successfully",
"inviteURLCopied": "Invite URL copied to clipboard",
+ "passwordResetURLCopied": "Password reset URL copied to clipboard",
"shareableURLCopied": "Copied shareable base URL to clipboard!",
"embeddableHTMLCodeCopied": "Copied embeddable HTML code!",
"userDetailsUpdated": "Successfully updated the user details",
@@ -699,7 +713,9 @@
"webhookTested": "Webhook tested successfully",
"columnUpdated": "Column updated",
"columnCreated": "Column created",
- "passwordChanged": "Password changed successfully. Please login again."
+ "passwordChanged": "Password changed successfully. Please login again.",
+ "settingsSaved": "Settings saved successfully",
+ "roleUpdated": "Role updated successfully"
}
}
}
diff --git a/packages/nc-gui/layouts/base.vue b/packages/nc-gui/layouts/base.vue
index 48f0913312..b7eac5bfce 100644
--- a/packages/nc-gui/layouts/base.vue
+++ b/packages/nc-gui/layouts/base.vue
@@ -1,5 +1,5 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/pages/account/index/[page].vue b/packages/nc-gui/pages/account/index/[page].vue
new file mode 100644
index 0000000000..cd4c698ad8
--- /dev/null
+++ b/packages/nc-gui/pages/account/index/[page].vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/packages/nc-gui/pages/account/index/users.vue b/packages/nc-gui/pages/account/index/users.vue
new file mode 100644
index 0000000000..911c8a330b
--- /dev/null
+++ b/packages/nc-gui/pages/account/index/users.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/packages/nc-gui/pages/account/index/users/[[nestedPage]].vue b/packages/nc-gui/pages/account/index/users/[[nestedPage]].vue
new file mode 100644
index 0000000000..a90f035d44
--- /dev/null
+++ b/packages/nc-gui/pages/account/index/users/[[nestedPage]].vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/pages/index/index.vue b/packages/nc-gui/pages/index/index.vue
index bbd4f3a5b4..b188e4929f 100644
--- a/packages/nc-gui/pages/index/index.vue
+++ b/packages/nc-gui/pages/index/index.vue
@@ -12,7 +12,7 @@ useSidebar('nc-left-sidebar', { hasSidebar: false })
class="min-h-[calc(100vh_-_var(--header-height))] bg-primary bg-opacity-5 flex flex-wrap justify-between xl:flex-nowrap gap-6 py-6 px-4 md:(px-12 pt-65px)"
>
-
@@ -21,7 +21,10 @@ useSidebar('nc-left-sidebar', { hasSidebar: false })
-
+
diff --git a/packages/nc-gui/pages/signup/[[token]].vue b/packages/nc-gui/pages/signup/[[token]].vue
index a9204c852d..d16c1879e5 100644
--- a/packages/nc-gui/pages/signup/[[token]].vue
+++ b/packages/nc-gui/pages/signup/[[token]].vue
@@ -99,7 +99,11 @@ function resetError() {
-
+
{{ error }}
diff --git a/packages/nc-gui/plugins/tele.ts b/packages/nc-gui/plugins/tele.ts
index cd9c0b1ca1..4c21654c36 100644
--- a/packages/nc-gui/plugins/tele.ts
+++ b/packages/nc-gui/plugins/tele.ts
@@ -37,6 +37,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
socket.emit('page', {
path: to.matched[0].path + (to.query && to.query.type ? `?type=${to.query.type}` : ''),
+ pid: route?.params?.projectId,
})
})
@@ -48,6 +49,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
event: evt,
...(data || {}),
path: route?.matched?.[0]?.path,
+ pid: route?.params?.projectId,
})
}
},
diff --git a/packages/noco-docs/content/en/developer-resources/rest-apis.md b/packages/noco-docs/content/en/developer-resources/rest-apis.md
index 7a31457af6..202004db11 100644
--- a/packages/noco-docs/content/en/developer-resources/rest-apis.md
+++ b/packages/noco-docs/content/en/developer-resources/rest-apis.md
@@ -173,6 +173,15 @@ Currently, the default value for {orgs} is
noco. Users will be able to ch
| Meta | Get | utils | appVersion | /api/v1/version |
| Meta | Get | utils | appHealth | /api/v1/health |
| Meta | Get | utils | aggregatedMetaInfo | /api/v1/aggregated-meta-info |
+| Meta | Get | orgUsers | list | /api/v1/users |
+| Meta | Post | orgUsers | add | /api/v1/users |
+| Meta | Patch | orgUsers | update | /api/v1/users/{userId} |
+| Meta | Delete | orgUsers | delete | /api/v1/users/{userId} |
+| Meta | Get | orgTokens | list | /api/v1/tokens |
+| Meta | Post | orgTokens | create | /api/v1/tokens |
+| Meta | Delete | orgTokens | delete | /api/v1/tokens/{token} |
+| Meta | Get | orgAppSettings | get | /api/v1/app-settings |
+| Meta | Post | orgAppSettings | set | /api/v1/app-settings |
## Query params
diff --git a/packages/nocodb-sdk/src/lib/Api.ts b/packages/nocodb-sdk/src/lib/Api.ts
index da6597d0ff..495d7ca56c 100644
--- a/packages/nocodb-sdk/src/lib/Api.ts
+++ b/packages/nocodb-sdk/src/lib/Api.ts
@@ -16,7 +16,6 @@ export interface UserType {
lastname: string;
/** @format email */
email: string;
- /** @format email */
roles?: string;
/**
* @format date
@@ -545,6 +544,9 @@ export interface ApiTokenType {
id?: string;
token?: string;
description?: string;
+ fk_user_id?: string;
+ created_at?: any;
+ updated_at?: any;
}
export interface HookLogType {
@@ -1189,6 +1191,303 @@ export class Api<
...params,
}),
};
+ orgTokens = {
+ /**
+ * No description
+ *
+ * @tags Org tokens
+ * @name List
+ * @summary Organisation API Tokens List
+ * @request GET:/api/v1/tokens
+ * @response `200` `{
+ users?: {
+ list: ((ApiTokenType & {
+ created_by?: string,
+
+}))[],
+ pageInfo: PaginatedType,
+
+},
+
+}` OK
+ */
+ list: (params: RequestParams = {}) =>
+ this.request<
+ {
+ users?: {
+ list: (ApiTokenType & {
+ created_by?: string;
+ })[];
+ pageInfo: PaginatedType;
+ };
+ },
+ any
+ >({
+ path: `/api/v1/tokens`,
+ method: 'GET',
+ format: 'json',
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org tokens
+ * @name Create
+ * @request POST:/api/v1/tokens
+ * @response `200` `void` OK
+ */
+ create: (data: ApiTokenType, params: RequestParams = {}) =>
+ this.request
({
+ path: `/api/v1/tokens`,
+ method: 'POST',
+ body: data,
+ type: ContentType.Json,
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org tokens
+ * @name Delete
+ * @request DELETE:/api/v1/tokens/{token}
+ * @response `200` `void` OK
+ */
+ delete: (token: string, params: RequestParams = {}) =>
+ this.request({
+ path: `/api/v1/tokens/${token}`,
+ method: 'DELETE',
+ ...params,
+ }),
+ };
+ orgLicense = {
+ /**
+ * No description
+ *
+ * @tags Org license
+ * @name Get
+ * @summary App license get
+ * @request GET:/api/v1/license
+ * @response `200` `{
+ key?: string,
+
+}` OK
+ */
+ get: (params: RequestParams = {}) =>
+ this.request<
+ {
+ key?: string;
+ },
+ any
+ >({
+ path: `/api/v1/license`,
+ method: 'GET',
+ format: 'json',
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org license
+ * @name Set
+ * @summary App license get
+ * @request POST:/api/v1/license
+ * @response `200` `void` OK
+ */
+ set: (
+ data: {
+ key?: string;
+ },
+ params: RequestParams = {}
+ ) =>
+ this.request({
+ path: `/api/v1/license`,
+ method: 'POST',
+ body: data,
+ type: ContentType.Json,
+ ...params,
+ }),
+ };
+ orgAppSettings = {
+ /**
+ * No description
+ *
+ * @tags Org app settings
+ * @name Get
+ * @summary App settings get
+ * @request GET:/api/v1/app-settings
+ * @response `200` `{
+ invite_only_signup?: boolean,
+
+}` OK
+ */
+ get: (params: RequestParams = {}) =>
+ this.request<
+ {
+ invite_only_signup?: boolean;
+ },
+ any
+ >({
+ path: `/api/v1/app-settings`,
+ method: 'GET',
+ format: 'json',
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org app settings
+ * @name Set
+ * @summary App app settings get
+ * @request POST:/api/v1/app-settings
+ * @response `200` `void` OK
+ */
+ set: (
+ data: {
+ invite_only_signup?: boolean;
+ },
+ params: RequestParams = {}
+ ) =>
+ this.request({
+ path: `/api/v1/app-settings`,
+ method: 'POST',
+ body: data,
+ type: ContentType.Json,
+ ...params,
+ }),
+ };
+ orgUsers = {
+ /**
+ * No description
+ *
+ * @tags Org users
+ * @name List
+ * @summary Organisation Users
+ * @request GET:/api/v1/users
+ * @response `200` `{
+ users?: {
+ list: (UserType)[],
+ pageInfo: PaginatedType,
+
+},
+
+}` OK
+ */
+ list: (params: RequestParams = {}) =>
+ this.request<
+ {
+ users?: {
+ list: UserType[];
+ pageInfo: PaginatedType;
+ };
+ },
+ any
+ >({
+ path: `/api/v1/users`,
+ method: 'GET',
+ format: 'json',
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org users
+ * @name Add
+ * @summary Organisation User Add
+ * @request POST:/api/v1/users
+ * @response `200` `any` OK
+ */
+ add: (data: UserType, params: RequestParams = {}) =>
+ this.request({
+ path: `/api/v1/users`,
+ method: 'POST',
+ body: data,
+ type: ContentType.Json,
+ format: 'json',
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org users
+ * @name Update
+ * @summary Organisation User Update
+ * @request PATCH:/api/v1/users/{userId}
+ * @response `200` `void` OK
+ */
+ update: (userId: string, data: UserType, params: RequestParams = {}) =>
+ this.request({
+ path: `/api/v1/users/${userId}`,
+ method: 'PATCH',
+ body: data,
+ type: ContentType.Json,
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org users
+ * @name Delete
+ * @summary Organisation User Delete
+ * @request DELETE:/api/v1/users/{userId}
+ * @response `200` `void` OK
+ */
+ delete: (userId: string, params: RequestParams = {}) =>
+ this.request({
+ path: `/api/v1/users/${userId}`,
+ method: 'DELETE',
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org users
+ * @name ResendInvite
+ * @summary Organisation User Invite
+ * @request POST:/api/v1/users/{userId}/resend-invite
+ * @response `200` `void` OK
+ */
+ resendInvite: (userId: string, params: RequestParams = {}) =>
+ this.request({
+ path: `/api/v1/users/${userId}/resend-invite`,
+ method: 'POST',
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags Org users
+ * @name GeneratePasswordResetToken
+ * @summary Organisation User Generate Password Reset Token
+ * @request POST:/api/v1/users/{userId}/generate-reset-url
+ * @response `200` `{
+ reset_password_token?: string,
+ reset_password_url?: string,
+
+}` OK
+ */
+ generatePasswordResetToken: (userId: string, params: RequestParams = {}) =>
+ this.request<
+ {
+ reset_password_token?: string;
+ reset_password_url?: string;
+ },
+ any
+ >({
+ path: `/api/v1/users/${userId}/generate-reset-url`,
+ method: 'POST',
+ format: 'json',
+ ...params,
+ }),
+ };
project = {
/**
* No description
@@ -3572,7 +3871,7 @@ export class Api<
*/
commentCount: (
query: {
- ids: any[];
+ ids: any;
fk_model_id: string;
},
params: RequestParams = {}
diff --git a/packages/nocodb-sdk/src/lib/globals.ts b/packages/nocodb-sdk/src/lib/globals.ts
index 3e30e8c11e..216a3567b7 100644
--- a/packages/nocodb-sdk/src/lib/globals.ts
+++ b/packages/nocodb-sdk/src/lib/globals.ts
@@ -34,6 +34,7 @@ export enum AuditOperationTypes {
WEBHOOKS = 'WEBHOOKS',
AUTHENTICATION = 'AUTHENTICATION',
TABLE_COLUMN = 'TABLE_COLUMN',
+ ORG_USER = 'ORG_USER',
}
export enum AuditOperationSubTypes {
diff --git a/packages/nocodb/src/enums/OrgUserRoles.ts b/packages/nocodb/src/enums/OrgUserRoles.ts
new file mode 100644
index 0000000000..c5b1888256
--- /dev/null
+++ b/packages/nocodb/src/enums/OrgUserRoles.ts
@@ -0,0 +1,5 @@
+export enum OrgUserRoles {
+ SUPER_ADMIN = 'super',
+ CREATOR = 'org-level-creator',
+ VIEWER = 'org-level-viewer',
+}
diff --git a/packages/nocodb/src/lib/Noco.ts b/packages/nocodb/src/lib/Noco.ts
index 3ca0a2b256..91cf8baac9 100644
--- a/packages/nocodb/src/lib/Noco.ts
+++ b/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 'nc-help';
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';
@@ -101,7 +101,7 @@ export default class Noco {
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
- process.env.NC_VERSION = '0098004';
+ process.env.NC_VERSION = '0098005';
// if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources
if (process.env.NC_MINIMAL_DBS) {
diff --git a/packages/nocodb/src/lib/constants/index.ts b/packages/nocodb/src/lib/constants/index.ts
new file mode 100644
index 0000000000..e52388f60a
--- /dev/null
+++ b/packages/nocodb/src/lib/constants/index.ts
@@ -0,0 +1,2 @@
+export const NC_LICENSE_KEY = 'nc-license-key';
+export const NC_APP_SETTINGS = 'nc-app-settings';
diff --git a/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts b/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts
index bb772e7b30..afabc99b93 100644
--- a/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts
+++ b/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 { knex, Knex } from 'knex';
+import { Tele } from 'nc-help';
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');
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSql.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSql.ts
index 61a6f9fcd1..24fc43fbc6 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSql.ts
+++ b/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) {
diff --git a/packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts b/packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts
index b0cc0af366..884c98ac74 100644
--- a/packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts
+++ b/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 'nc-help';
import SqlClientFactory from '../sql-client/lib/SqlClientFactory';
// import debug from 'debug';
diff --git a/packages/nocodb/src/lib/meta/NcMetaMgr.ts b/packages/nocodb/src/lib/meta/NcMetaMgr.ts
index 5f43431f9d..789279dde6 100644
--- a/packages/nocodb/src/lib/meta/NcMetaMgr.ts
+++ b/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 'nc-help';
const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10);
const XC_PLUGIN_DET = 'XC_PLUGIN_DET';
diff --git a/packages/nocodb/src/lib/meta/NcMetaMgrEE.ts b/packages/nocodb/src/lib/meta/NcMetaMgrEE.ts
index 39f82ca579..dfa8dff104 100644
--- a/packages/nocodb/src/lib/meta/NcMetaMgrEE.ts
+++ b/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 'nc-help';
import NcMetaMgr from './NcMetaMgr';
diff --git a/packages/nocodb/src/lib/meta/NcMetaMgrv2.ts b/packages/nocodb/src/lib/meta/NcMetaMgrv2.ts
index f71b00865b..371b353427 100644
--- a/packages/nocodb/src/lib/meta/NcMetaMgrv2.ts
+++ b/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';
diff --git a/packages/nocodb/src/lib/meta/api/apiTokenApis.ts b/packages/nocodb/src/lib/meta/api/apiTokenApis.ts
index 3afcf26cbe..4d202a1d7e 100644
--- a/packages/nocodb/src/lib/meta/api/apiTokenApis.ts
+++ b/packages/nocodb/src/lib/meta/api/apiTokenApis.ts
@@ -1,21 +1,35 @@
import { Request, Response, Router } from 'express';
+import { OrgUserRoles } from '../../../enums/OrgUserRoles';
+import { Tele } from 'nc-help';
+import { NcError } from '../helpers/catchError';
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) {
- res.json(await ApiToken.list());
+export async function apiTokenList(req: Request, res: Response) {
+ res.json(await ApiToken.list(req['user'].id));
}
export async function apiTokenCreate(req: Request, res: Response) {
Tele.emit('evt', { evt_type: 'apiToken:created' });
- res.json(await ApiToken.insert(req.body));
+ res.json(await ApiToken.insert({ ...req.body, fk_user_id: req['user'].id }));
}
export async function apiTokenDelete(req: Request, res: Response) {
+ const apiToken = await ApiToken.getByToken(req.params.apiTokenId);
+ if (
+ !req['user'].roles.includes(OrgUserRoles.SUPER_ADMIN) &&
+ apiToken.fk_user_id !== req['user'].id
+ ) {
+ NcError.notFound('Token not found');
+ }
Tele.emit('evt', { evt_type: 'apiToken:deleted' });
+
+ // todo: verify token belongs to the user
res.json(await ApiToken.delete(req.params.token));
}
+// todo: add reset token api to regenerate token
+
+// deprecated apis
const router = Router({ mergeParams: true });
router.get(
diff --git a/packages/nocodb/src/lib/meta/api/attachmentApis.ts b/packages/nocodb/src/lib/meta/api/attachmentApis.ts
index 0c113d57da..8af6ebfa46 100644
--- a/packages/nocodb/src/lib/meta/api/attachmentApis.ts
+++ b/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 'nc-help';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import catchError from '../helpers/catchError';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';
diff --git a/packages/nocodb/src/lib/meta/api/columnApis.ts b/packages/nocodb/src/lib/meta/api/columnApis.ts
index 66f5b65220..9ce86881c4 100644
--- a/packages/nocodb/src/lib/meta/api/columnApis.ts
+++ b/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 validateParams from '../helpers/validateParams';
import { Tele } from 'nc-help';
+import validateParams from '../helpers/validateParams';
import { customAlphabet } from 'nanoid';
import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
diff --git a/packages/nocodb/src/lib/meta/api/ee/orgTokenApis.ts b/packages/nocodb/src/lib/meta/api/ee/orgTokenApis.ts
new file mode 100644
index 0000000000..2aa2f8a304
--- /dev/null
+++ b/packages/nocodb/src/lib/meta/api/ee/orgTokenApis.ts
@@ -0,0 +1,22 @@
+import { OrgUserRoles } from '../../../../enums/OrgUserRoles';
+import ApiToken from '../../../models/ApiToken';
+import { PagedResponseImpl } from '../../helpers/PagedResponse';
+
+export async function apiTokenListEE(req, res) {
+ let fk_user_id = req.user.id;
+
+ // if super admin get all tokens
+ if (req.user.roles.includes(OrgUserRoles.SUPER_ADMIN)) {
+ fk_user_id = undefined;
+ }
+
+ res.json(
+ new PagedResponseImpl(
+ await ApiToken.listWithCreatedBy({ ...req.query, fk_user_id }),
+ {
+ ...req.query,
+ count: await ApiToken.count({}),
+ }
+ )
+ );
+}
diff --git a/packages/nocodb/src/lib/meta/api/filterApis.ts b/packages/nocodb/src/lib/meta/api/filterApis.ts
index 963250d322..98b586663b 100644
--- a/packages/nocodb/src/lib/meta/api/filterApis.ts
+++ b/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 'nc-help';
// @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
diff --git a/packages/nocodb/src/lib/meta/api/formViewApis.ts b/packages/nocodb/src/lib/meta/api/formViewApis.ts
index e7e2e903bc..d5932ea0d6 100644
--- a/packages/nocodb/src/lib/meta/api/formViewApis.ts
+++ b/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 'nc-help';
// @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
diff --git a/packages/nocodb/src/lib/meta/api/formViewColumnApis.ts b/packages/nocodb/src/lib/meta/api/formViewColumnApis.ts
index 711a589dfb..0485cb79be 100644
--- a/packages/nocodb/src/lib/meta/api/formViewColumnApis.ts
+++ b/packages/nocodb/src/lib/meta/api/formViewColumnApis.ts
@@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import FormViewColumn from '../../models/FormViewColumn';
-import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
+import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnUpdate(req: Request, res: Response) {
diff --git a/packages/nocodb/src/lib/meta/api/galleryViewApis.ts b/packages/nocodb/src/lib/meta/api/galleryViewApis.ts
index c184c693be..56ca905ffb 100644
--- a/packages/nocodb/src/lib/meta/api/galleryViewApis.ts
+++ b/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 ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
+import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function galleryViewGet(req: Request, res: Response) {
res.json(await GalleryView.get(req.params.galleryViewId));
diff --git a/packages/nocodb/src/lib/meta/api/gridViewApis.ts b/packages/nocodb/src/lib/meta/api/gridViewApis.ts
index 8c6e3789c2..94c815f9eb 100644
--- a/packages/nocodb/src/lib/meta/api/gridViewApis.ts
+++ b/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 'nc-help';
// @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
diff --git a/packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts b/packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts
index 6d30b387e3..b19147a323 100644
--- a/packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts
+++ b/packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts
@@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import GridViewColumn from '../../models/GridViewColumn';
-import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
+import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnList(req: Request, res: Response) {
diff --git a/packages/nocodb/src/lib/meta/api/hookApis.ts b/packages/nocodb/src/lib/meta/api/hookApis.ts
index 614bf5b6a6..cde045c5e6 100644
--- a/packages/nocodb/src/lib/meta/api/hookApis.ts
+++ b/packages/nocodb/src/lib/meta/api/hookApis.ts
@@ -1,3 +1,4 @@
+import { Tele } from 'nc-help';
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(
diff --git a/packages/nocodb/src/lib/meta/api/hookFilterApis.ts b/packages/nocodb/src/lib/meta/api/hookFilterApis.ts
index 05a4e9c272..4e714479ed 100644
--- a/packages/nocodb/src/lib/meta/api/hookFilterApis.ts
+++ b/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 'nc-help';
// @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
diff --git a/packages/nocodb/src/lib/meta/api/index.ts b/packages/nocodb/src/lib/meta/api/index.ts
index 579dd53088..082fe97d24 100644
--- a/packages/nocodb/src/lib/meta/api/index.ts
+++ b/packages/nocodb/src/lib/meta/api/index.ts
@@ -1,3 +1,7 @@
+import { Tele } from 'nc-help';
+import orgLicenseApis from './orgLicenseApis'
+import orgTokenApis from './orgTokenApis';
+import orgUserApis from './orgUserApis';
import projectApis from './projectApis';
import tableApis from './tableApis';
import columnApis from './columnApis';
@@ -42,7 +46,6 @@ import {
publicDataExportApis,
publicMetaApis,
} from './publicApis';
-import { Tele } from 'nc-help';
import { Server, Socket } from 'socket.io';
import passport from 'passport';
@@ -87,6 +90,9 @@ export default function (router: Router, server) {
router.use(hookApis);
router.use(pluginApis);
router.use(projectUserApis);
+ router.use(orgUserApis);
+ router.use(orgTokenApis);
+ router.use(orgLicenseApis);
router.use(sharedBaseApis);
router.use(modelVisibilityApis);
router.use(metaDiffApis);
diff --git a/packages/nocodb/src/lib/meta/api/kanbanViewApis.ts b/packages/nocodb/src/lib/meta/api/kanbanViewApis.ts
index 6bf385fec5..df94f1fe94 100644
--- a/packages/nocodb/src/lib/meta/api/kanbanViewApis.ts
+++ b/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 ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
+import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function kanbanViewGet(req: Request, res: Response) {
diff --git a/packages/nocodb/src/lib/meta/api/metaDiffApis.ts b/packages/nocodb/src/lib/meta/api/metaDiffApis.ts
index eee915bb2e..3257881ae4 100644
--- a/packages/nocodb/src/lib/meta/api/metaDiffApis.ts
+++ b/packages/nocodb/src/lib/meta/api/metaDiffApis.ts
@@ -1,5 +1,6 @@
// // Project CRUD
+import { Tele } from 'nc-help';
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';
diff --git a/packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts b/packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts
index 02c2b1a93a..68f95e928b 100644
--- a/packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts
+++ b/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 ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
+import ncMetaAclMw from '../helpers/ncMetaAclMw';
import Project from '../../models/Project';
import { metaApiMetrics } from '../helpers/apiMetrics';
async function xcVisibilityMetaSetAll(req, res) {
diff --git a/packages/nocodb/src/lib/meta/api/orgLicenseApis.ts b/packages/nocodb/src/lib/meta/api/orgLicenseApis.ts
new file mode 100644
index 0000000000..259738c881
--- /dev/null
+++ b/packages/nocodb/src/lib/meta/api/orgLicenseApis.ts
@@ -0,0 +1,40 @@
+import { Router } from 'express';
+import { OrgUserRoles } from '../../../enums/OrgUserRoles';
+import { NC_LICENSE_KEY } from '../../constants'
+import Store from '../../models/Store';
+import { metaApiMetrics } from '../helpers/apiMetrics';
+import ncMetaAclMw from '../helpers/ncMetaAclMw';
+
+
+
+async function licenseGet(_req, res) {
+ const license = await Store.get(NC_LICENSE_KEY);
+
+ res.json({ key: license?.value });
+}
+
+async function licenseSet(req, res) {
+ await Store.saveOrUpdate({ value: req.body.key, key: NC_LICENSE_KEY });
+
+ res.json({ msg: 'License key saved' });
+}
+
+const router = Router({ mergeParams: true });
+router.get(
+ '/api/v1/license',
+ metaApiMetrics,
+ ncMetaAclMw(licenseGet, 'licenseGet', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+router.post(
+ '/api/v1/license',
+ metaApiMetrics,
+ ncMetaAclMw(licenseSet, 'licenseSet', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+
+export default router;
diff --git a/packages/nocodb/src/lib/meta/api/orgTokenApis.ts b/packages/nocodb/src/lib/meta/api/orgTokenApis.ts
new file mode 100644
index 0000000000..98b76a3750
--- /dev/null
+++ b/packages/nocodb/src/lib/meta/api/orgTokenApis.ts
@@ -0,0 +1,81 @@
+import { Request, Response, Router } from 'express';
+import { OrgUserRoles } from '../../../enums/OrgUserRoles';
+import ApiToken from '../../models/ApiToken';
+import { Tele } from 'nc-help';
+import { metaApiMetrics } from '../helpers/apiMetrics';
+import { NcError } from '../helpers/catchError';
+import getHandler from '../helpers/getHandler';
+import ncMetaAclMw from '../helpers/ncMetaAclMw';
+import { PagedResponseImpl } from '../helpers/PagedResponse';
+import { apiTokenListEE } from './ee/orgTokenApis';
+
+async function apiTokenList(req, res) {
+ const fk_user_id = req.user.id;
+ let includeUnmappedToken = false;
+ if (req['user'].roles.includes(OrgUserRoles.SUPER_ADMIN)) {
+ includeUnmappedToken = true;
+ }
+
+ res.json(
+ new PagedResponseImpl(
+ await ApiToken.listWithCreatedBy({
+ ...req.query,
+ fk_user_id,
+ includeUnmappedToken,
+ }),
+ {
+ ...req.query,
+ count: await ApiToken.count({
+ includeUnmappedToken,
+ fk_user_id,
+ }),
+ }
+ )
+ );
+}
+
+export async function apiTokenCreate(req: Request, res: Response) {
+ Tele.emit('evt', { evt_type: 'org:apiToken:created' });
+ res.json(await ApiToken.insert({ ...req.body, fk_user_id: req['user'].id }));
+}
+
+export async function apiTokenDelete(req: Request, res: Response) {
+ const fk_user_id = req['user'].id;
+ const apiToken = await ApiToken.getByToken(req.params.token);
+ if (
+ !req['user'].roles.includes(OrgUserRoles.SUPER_ADMIN) &&
+ apiToken.fk_user_id !== fk_user_id
+ ) {
+ NcError.notFound('Token not found');
+ }
+ Tele.emit('evt', { evt_type: 'org:apiToken:deleted' });
+ res.json(await ApiToken.delete(req.params.token));
+}
+
+const router = Router({ mergeParams: true });
+
+router.get(
+ '/api/v1/tokens',
+ metaApiMetrics,
+ ncMetaAclMw(getHandler(apiTokenList, apiTokenListEE), 'apiTokenList', {
+ // allowedRoles: [OrgUserRoles.SUPER],
+ blockApiTokenAccess: true,
+ })
+);
+router.post(
+ '/api/v1/tokens',
+ metaApiMetrics,
+ ncMetaAclMw(apiTokenCreate, 'apiTokenCreate', {
+ // allowedRoles: [OrgUserRoles.SUPER],
+ blockApiTokenAccess: true,
+ })
+);
+router.delete(
+ '/api/v1/tokens/:token',
+ metaApiMetrics,
+ ncMetaAclMw(apiTokenDelete, 'apiTokenDelete', {
+ // allowedRoles: [OrgUserRoles.SUPER],
+ blockApiTokenAccess: true,
+ })
+);
+export default router;
diff --git a/packages/nocodb/src/lib/meta/api/orgUserApis.ts b/packages/nocodb/src/lib/meta/api/orgUserApis.ts
new file mode 100644
index 0000000000..f31be978b5
--- /dev/null
+++ b/packages/nocodb/src/lib/meta/api/orgUserApis.ts
@@ -0,0 +1,329 @@
+import { Router } from 'express';
+import {
+ AuditOperationSubTypes,
+ AuditOperationTypes,
+ PluginCategory,
+} from 'nocodb-sdk';
+import { v4 as uuidv4 } from 'uuid';
+import validator from 'validator';
+import { OrgUserRoles } from '../../../enums/OrgUserRoles';
+import { NC_APP_SETTINGS } from '../../constants';
+import Audit from '../../models/Audit';
+import ProjectUser from '../../models/ProjectUser';
+import Store from '../../models/Store';
+import SyncSource from '../../models/SyncSource';
+import User from '../../models/User';
+import Noco from '../../Noco';
+import { MetaTable } from '../../utils/globals';
+import { Tele } from 'nc-help';
+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 { sendInviteEmail } from './projectUserApis';
+
+async function userList(req, res) {
+ res.json(
+ new PagedResponseImpl(await User.list(req.query), {
+ ...req.query,
+ count: await User.count(req.query),
+ })
+ );
+}
+
+async function userUpdate(req, res) {
+ const updateBody = extractProps(req.body, ['roles']);
+
+ const user = await User.get(req.params.userId);
+
+ if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) {
+ NcError.badRequest('Cannot update super admin roles');
+ }
+
+ res.json(await User.update(req.params.userId, updateBody));
+}
+
+async function userDelete(req, res) {
+ const ncMeta = await Noco.ncMeta.startTransaction();
+ try {
+ const user = await User.get(req.params.userId, ncMeta);
+
+ if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) {
+ NcError.badRequest('Cannot delete super admin');
+ }
+
+ // delete project user entry and assign to super admin
+ const projectUsers = await ProjectUser.getProjectsList(
+ req.params.userId,
+ ncMeta
+ );
+
+ // todo: clear cache
+
+ // TODO: assign super admin as project owner
+ for (const projectUser of projectUsers) {
+ await ProjectUser.delete(
+ projectUser.project_id,
+ projectUser.fk_user_id,
+ ncMeta
+ );
+ }
+
+ // delete sync source entry
+ await SyncSource.deleteByUserId(req.params.userId, ncMeta);
+
+ // delete user
+ await User.delete(req.params.userId, ncMeta);
+ await ncMeta.commit();
+ } catch (e) {
+ await ncMeta.rollback(e);
+ throw e;
+ }
+
+ res.json({ msg: 'success' });
+}
+
+async function userAdd(req, res, next) {
+ // allow only viewer or creator role
+ if (
+ req.body.roles &&
+ ![OrgUserRoles.VIEWER, OrgUserRoles.CREATOR].includes(req.body.roles)
+ ) {
+ NcError.badRequest('Invalid role');
+ }
+
+ // extract emails from request body
+ const emails = (req.body.email || '')
+ .toLowerCase()
+ .split(/\s*,\s*/)
+ .map((v) => v.trim());
+
+ // check for invalid emails
+ const invalidEmails = emails.filter((v) => !validator.isEmail(v));
+
+ if (!emails.length) {
+ return NcError.badRequest('Invalid email address');
+ }
+ if (invalidEmails.length) {
+ NcError.badRequest('Invalid email address : ' + invalidEmails.join(', '));
+ }
+
+ const invite_token = uuidv4();
+ const error = [];
+
+ for (const email of emails) {
+ // add user to project if user already exist
+ const user = await User.getByEmail(email);
+
+ if (user) {
+ NcError.badRequest('User already exist');
+ } else {
+ try {
+ // create new user with invite token
+ await User.insert({
+ invite_token,
+ invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
+ email,
+ roles: req.body.roles || OrgUserRoles.VIEWER,
+ token_version: randomTokenString(),
+ });
+
+ const count = await User.count();
+ Tele.emit('evt', { evt_type: 'org:user:invite', count });
+
+ await Audit.insert({
+ op_type: AuditOperationTypes.ORG_USER,
+ op_sub_type: AuditOperationSubTypes.INVITE,
+ user: req.user.email,
+ description: `invited ${email} to ${req.params.projectId} project `,
+ ip: req.clientIp,
+ });
+ // in case of single user check for smtp failure
+ // and send back token if failed
+ if (
+ emails.length === 1 &&
+ !(await sendInviteEmail(email, invite_token, req))
+ ) {
+ return res.json({ invite_token, email });
+ } else {
+ sendInviteEmail(email, invite_token, req);
+ }
+ } catch (e) {
+ console.log(e);
+ if (emails.length === 1) {
+ return next(e);
+ } else {
+ error.push({ email, error: e.message });
+ }
+ }
+ }
+ }
+
+ if (emails.length === 1) {
+ res.json({
+ msg: 'success',
+ });
+ } else {
+ return res.json({ invite_token, emails, error });
+ }
+}
+
+async function userSettings(_req, _res): Promise {
+ NcError.notImplemented();
+}
+
+async function userInviteResend(req, res): Promise {
+ const user = await User.get(req.params.userId);
+
+ if (!user) {
+ NcError.badRequest(`User with id '${req.params.userId}' not found`);
+ }
+
+ const invite_token = uuidv4();
+
+ await User.update(user.id, {
+ invite_token,
+ invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
+ });
+
+ const pluginData = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
+ category: PluginCategory.EMAIL,
+ active: true,
+ });
+
+ if (!pluginData) {
+ NcError.badRequest(
+ `No Email Plugin is found. Please go to App Store to configure first or copy the invitation URL to users instead.`
+ );
+ }
+
+ await sendInviteEmail(user.email, invite_token, req);
+
+ await Audit.insert({
+ op_type: AuditOperationTypes.ORG_USER,
+ op_sub_type: AuditOperationSubTypes.RESEND_INVITE,
+ user: user.email,
+ description: `resent a invite to ${user.email} `,
+ ip: req.clientIp,
+ });
+
+ res.json({ msg: 'success' });
+}
+
+async function generateResetUrl(req, res) {
+ const user = await User.get(req.params.userId);
+
+ if (!user) {
+ NcError.badRequest(`User with id '${req.params.userId}' not found`);
+ }
+ const token = uuidv4();
+ await User.update(user.id, {
+ email: user.email,
+ reset_password_token: token,
+ reset_password_expires: new Date(Date.now() + 60 * 60 * 1000),
+ token_version: null,
+ });
+
+ res.json({
+ reset_password_token: token,
+ reset_password_url: req.ncSiteUrl + `/auth/password/reset/${token}`,
+ });
+}
+
+async function appSettingsGet(_req, res) {
+ let settings = {};
+ try {
+ settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value);
+ } catch {}
+ res.json(settings);
+}
+
+async function appSettingsSet(req, res) {
+ await Store.saveOrUpdate({
+ value: JSON.stringify(req.body),
+ key: NC_APP_SETTINGS,
+ });
+
+ res.json({ msg: 'Settings saved' });
+}
+
+const router = Router({ mergeParams: true });
+router.get(
+ '/api/v1/users',
+ metaApiMetrics,
+ ncMetaAclMw(userList, 'userList', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+router.patch(
+ '/api/v1/users/:userId',
+ metaApiMetrics,
+ ncMetaAclMw(userUpdate, 'userUpdate', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+router.delete(
+ '/api/v1/users/:userId',
+ metaApiMetrics,
+ ncMetaAclMw(userDelete, 'userDelete', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+router.post(
+ '/api/v1/users',
+ metaApiMetrics,
+ ncMetaAclMw(userAdd, 'userAdd', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+router.post(
+ '/api/v1/users/settings',
+ metaApiMetrics,
+ ncMetaAclMw(userSettings, 'userSettings', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+router.post(
+ '/api/v1/users/:userId/resend-invite',
+ metaApiMetrics,
+ ncMetaAclMw(userInviteResend, 'userInviteResend', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+
+router.post(
+ '/api/v1/users/:userId/generate-reset-url',
+ metaApiMetrics,
+ ncMetaAclMw(generateResetUrl, 'generateResetUrl', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+
+router.get(
+ '/api/v1/app-settings',
+ metaApiMetrics,
+ ncMetaAclMw(appSettingsGet, 'appSettingsGet', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+
+router.post(
+ '/api/v1/app-settings',
+ metaApiMetrics,
+ ncMetaAclMw(appSettingsSet, 'appSettingsSet', {
+ allowedRoles: [OrgUserRoles.SUPER_ADMIN],
+ blockApiTokenAccess: true,
+ })
+);
+
+export default router;
diff --git a/packages/nocodb/src/lib/meta/api/pluginApis.ts b/packages/nocodb/src/lib/meta/api/pluginApis.ts
index f4476bd05f..e0a39646af 100644
--- a/packages/nocodb/src/lib/meta/api/pluginApis.ts
+++ b/packages/nocodb/src/lib/meta/api/pluginApis.ts
@@ -1,10 +1,10 @@
import { Request, Response, Router } from 'express';
+import { Tele } from 'nc-help';
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) {
diff --git a/packages/nocodb/src/lib/meta/api/projectApis.ts b/packages/nocodb/src/lib/meta/api/projectApis.ts
index 44b688aa43..028bdc078f 100644
--- a/packages/nocodb/src/lib/meta/api/projectApis.ts
+++ b/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 'nc-help';
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';
diff --git a/packages/nocodb/src/lib/meta/api/projectUserApis.ts b/packages/nocodb/src/lib/meta/api/projectUserApis.ts
index de4cf2871a..14bd19c337 100644
--- a/packages/nocodb/src/lib/meta/api/projectUserApis.ts
+++ b/packages/nocodb/src/lib/meta/api/projectUserApis.ts
@@ -1,3 +1,5 @@
+import { OrgUserRoles } from '../../../enums/OrgUserRoles';
+import { Tele } from 'nc-help';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Router } from 'express';
import { PagedResponseImpl } from '../helpers/PagedResponse';
@@ -6,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';
@@ -63,11 +64,6 @@ async function userInvite(req, res, next): Promise {
);
}
- // todo : provide a different role
- await User.update(user.id, {
- roles: 'user',
- });
-
await ProjectUser.insert({
project_id: req.params.projectId,
fk_user_id: user.id,
@@ -102,7 +98,7 @@ async function userInvite(req, res, next): Promise {
invite_token,
invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
email,
- roles: 'user',
+ roles: OrgUserRoles.VIEWER,
token_version: randomTokenString(),
});
@@ -267,7 +263,7 @@ async function projectUserInviteResend(req, res): Promise {
res.json({ msg: 'success' });
}
-async function sendInviteEmail(
+export async function sendInviteEmail(
email: string,
token: string,
req: any
diff --git a/packages/nocodb/src/lib/meta/api/sharedBaseApis.ts b/packages/nocodb/src/lib/meta/api/sharedBaseApis.ts
index da076d9c0d..e1ce599a8f 100644
--- a/packages/nocodb/src/lib/meta/api/sharedBaseApis.ts
+++ b/packages/nocodb/src/lib/meta/api/sharedBaseApis.ts
@@ -1,7 +1,7 @@
import { Router } from 'express';
+import { Tele } from 'nc-help';
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
diff --git a/packages/nocodb/src/lib/meta/api/sortApis.ts b/packages/nocodb/src/lib/meta/api/sortApis.ts
index 8dd07b38f7..6e40501bbb 100644
--- a/packages/nocodb/src/lib/meta/api/sortApis.ts
+++ b/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 'nc-help';
// @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
diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts b/packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts
index 039690c3a8..fe9b81a89c 100644
--- a/packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts
+++ b/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';
diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/fetchAT.ts b/packages/nocodb/src/lib/meta/api/sync/helpers/fetchAT.ts
index 7e5361dbe8..a99947dc09 100644
--- a/packages/nocodb/src/lib/meta/api/sync/helpers/fetchAT.ts
+++ b/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
diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts b/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
index bd82f8f26c..28c5bd6c28 100644
--- a/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
+++ b/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
@@ -1,6 +1,6 @@
+import { Tele } from 'nc-help';
import FetchAT from './fetchAT';
import { UITypes } from 'nocodb-sdk';
-import { Tele } from 'nc-help';
// import * as sMap from './syncMap';
import { Api } from 'nocodb-sdk';
diff --git a/packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts b/packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts
index 7f7eaf55ce..48549044f1 100644
--- a/packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts
+++ b/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 'nc-help';
import { PagedResponseImpl } from '../../helpers/PagedResponse';
import ncMetaAclMw from '../../helpers/ncMetaAclMw';
diff --git a/packages/nocodb/src/lib/meta/api/tableApis.ts b/packages/nocodb/src/lib/meta/api/tableApis.ts
index e33a103404..8074ed3976 100644
--- a/packages/nocodb/src/lib/meta/api/tableApis.ts
+++ b/packages/nocodb/src/lib/meta/api/tableApis.ts
@@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import Model from '../../models/Model';
-import { PagedResponseImpl } from '../helpers/PagedResponse';
import { Tele } from 'nc-help';
+import { PagedResponseImpl } from '../helpers/PagedResponse';
import DOMPurify from 'isomorphic-dompurify';
import {
AuditOperationSubTypes,
diff --git a/packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts b/packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts
index 62f096019b..8f9bfdce74 100644
--- a/packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts
+++ b/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 'nc-help';
const { isEmail } = require('validator');
const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 };
diff --git a/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts b/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
index 37b4c08456..cc261f1e71 100644
--- a/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
+++ b/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
@@ -1,3 +1,4 @@
+import { OrgUserRoles } from '../../../../enums/OrgUserRoles';
import User from '../../../models/User';
import ProjectUser from '../../../models/ProjectUser';
import { promisify } from 'util';
@@ -27,20 +28,46 @@ import Plugin from '../../../models/Plugin';
export function initStrategies(router): void {
passport.use(
'authtoken',
- new AuthTokenStrategy({ headerFields: ['xc-token'] }, (token, done) => {
- ApiToken.getByToken(token)
- .then((apiToken) => {
- if (apiToken) {
- done(null, { roles: 'editor' });
- } else {
- return done({ msg: 'Invalid tok' });
- }
- })
- .catch((e) => {
- console.log(e);
- done({ msg: 'Invalid tok' });
- });
- })
+ new AuthTokenStrategy(
+ { headerFields: ['xc-token'], passReqToCallback: true },
+ (req, token, done) => {
+ ApiToken.getByToken(token)
+ .then((apiToken) => {
+ if (!apiToken) {
+ return done({ msg: 'Invalid token' });
+ }
+
+ if (!apiToken.fk_user_id) return done(null, { roles: 'editor' });
+ User.get(apiToken.fk_user_id)
+ .then((user) => {
+ user['is_api_token'] = true;
+ if (req.ncProjectId) {
+ ProjectUser.get(req.ncProjectId, user.id)
+ .then(async (projectUser) => {
+ user.roles = projectUser?.roles || user.roles;
+ user.roles =
+ user.roles === 'owner' ? 'owner,creator' : user.roles;
+ // + (user.roles ? `,${user.roles}` : '');
+ // todo : cache
+ // await NocoCache.set(`${CacheScope.USER}:${key}`, user);
+ done(null, user);
+ })
+ .catch((e) => done(e));
+ } else {
+ return done(null, user);
+ }
+ })
+ .catch((e) => {
+ console.log(e);
+ done({ msg: 'User not found' });
+ });
+ })
+ .catch((e) => {
+ console.log(e);
+ done({ msg: 'Invalid token' });
+ });
+ }
+ )
);
passport.serializeUser(function (
@@ -91,6 +118,19 @@ export function initStrategies(router): void {
...Noco.getConfig().auth.jwt.options,
},
async (req, jwtPayload, done) => {
+ // todo: improve this
+ if (
+ req.ncProjectId &&
+ jwtPayload.roles?.split(',').includes(OrgUserRoles.SUPER_ADMIN)
+ ) {
+ return User.getByEmail(jwtPayload?.email).then(async (user) => {
+ return done(null, {
+ ...user,
+ roles: `owner,creator,${OrgUserRoles.SUPER_ADMIN}`,
+ });
+ });
+ }
+
const keyVals = [jwtPayload?.email];
if (req.ncProjectId) {
keyVals.push(req.ncProjectId);
@@ -129,7 +169,7 @@ export function initStrategies(router): void {
ProjectUser.get(req.ncProjectId, user.id)
.then(async (projectUser) => {
- user.roles = projectUser?.roles || 'user';
+ user.roles = projectUser?.roles || user.roles;
user.roles =
user.roles === 'owner' ? 'owner,creator' : user.roles;
// + (user.roles ? `,${user.roles}` : '');
@@ -247,7 +287,7 @@ export function initStrategies(router): void {
if (req.ncProjectId) {
ProjectUser.get(req.ncProjectId, user.id)
.then(async (projectUser) => {
- user.roles = projectUser?.roles || 'user';
+ user.roles = projectUser?.roles || user.roles;
user.roles =
user.roles === 'owner' ? 'owner,creator' : user.roles;
// + (user.roles ? `,${user.roles}` : '');
diff --git a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts b/packages/nocodb/src/lib/meta/api/userApi/userApis.ts
index b4ab59de0c..4d1f54616b 100644
--- a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts
+++ b/packages/nocodb/src/lib/meta/api/userApi/userApis.ts
@@ -1,13 +1,17 @@
import { Request, Response } from 'express';
import { TableType, validatePassword } from 'nocodb-sdk';
+import { OrgUserRoles } from '../../../../enums/OrgUserRoles';
+import { NC_APP_SETTINGS } from '../../../constants';
+import Store from '../../../models/Store';
+import { Tele } from 'nc-help';
import catchError, { NcError } from '../../helpers/catchError';
+
const { isEmail } = require('validator');
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';
@@ -84,10 +88,10 @@ export async function signup(req: Request, res: Response) {
NcError.badRequest('User already exist');
}
} else {
- let roles = 'user';
+ let roles: string = OrgUserRoles.CREATOR;
if (await User.isFirst()) {
- roles = 'user,super';
+ roles = `${OrgUserRoles.CREATOR},${OrgUserRoles.SUPER_ADMIN}`;
// todo: update in nc_store
// roles = 'owner,creator,editor'
Tele.emit('evt', {
@@ -95,10 +99,15 @@ export async function signup(req: Request, res: Response) {
count: 1,
});
} else {
- if (process.env.NC_INVITE_ONLY_SIGNUP) {
+ 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 = 'user_new';
+ roles = OrgUserRoles.VIEWER;
}
}
diff --git a/packages/nocodb/src/lib/meta/api/utilApis.ts b/packages/nocodb/src/lib/meta/api/utilApis.ts
index eb002e6d1b..cdd9f0a321 100644
--- a/packages/nocodb/src/lib/meta/api/utilApis.ts
+++ b/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, {
diff --git a/packages/nocodb/src/lib/meta/api/viewApis.ts b/packages/nocodb/src/lib/meta/api/viewApis.ts
index 9a7d80872e..b062a2b2ef 100644
--- a/packages/nocodb/src/lib/meta/api/viewApis.ts
+++ b/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 'nc-help';
// @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) {}
diff --git a/packages/nocodb/src/lib/meta/api/viewColumnApis.ts b/packages/nocodb/src/lib/meta/api/viewColumnApis.ts
index af6aee8f8e..2fc4df7625 100644
--- a/packages/nocodb/src/lib/meta/api/viewColumnApis.ts
+++ b/packages/nocodb/src/lib/meta/api/viewColumnApis.ts
@@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express';
import View from '../../models/View';
-import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
+import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { metaApiMetrics } from '../helpers/apiMetrics';
export async function columnList(req: Request, res: Response) {
diff --git a/packages/nocodb/src/lib/meta/helpers/apiMetrics.ts b/packages/nocodb/src/lib/meta/helpers/apiMetrics.ts
index 82bae677ab..d78623d901 100644
--- a/packages/nocodb/src/lib/meta/helpers/apiMetrics.ts
+++ b/packages/nocodb/src/lib/meta/helpers/apiMetrics.ts
@@ -3,7 +3,7 @@ import { Tele } from 'nc-help';
const countMap = {};
-const metrics = async (req: Request, c = 50) => {
+const metrics = async (req: Request, c = 150) => {
if (!req?.route?.path) return;
const event = `a:api:${req.route.path}:${req.method}`;
countMap[event] = (countMap[event] || 0) + 1;
@@ -14,7 +14,7 @@ const metrics = async (req: Request, c = 50) => {
};
const metaApiMetrics = (req: Request, _res, next) => {
- metrics(req, 10).then(() => {});
+ metrics(req, 50).then(() => {});
next();
};
export default (req: Request, _res, next) => {
diff --git a/packages/nocodb/src/lib/meta/helpers/getHandler.ts b/packages/nocodb/src/lib/meta/helpers/getHandler.ts
new file mode 100644
index 0000000000..f41e899d6d
--- /dev/null
+++ b/packages/nocodb/src/lib/meta/helpers/getHandler.ts
@@ -0,0 +1,16 @@
+import express from 'express';
+import { NC_LICENSE_KEY } from '../../constants';
+import Store from '../../models/Store';
+
+export default function getHandler(
+ defaultHandler: express.Handler,
+ eeHandler: express.Handler
+): express.Handler {
+ return async (...args) => {
+ const key = await Store.get(NC_LICENSE_KEY);
+ if (!key?.value) {
+ return defaultHandler(...args);
+ }
+ return eeHandler(...args);
+ };
+}
diff --git a/packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts b/packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts
index b024ef7ab4..7034d235a9 100644
--- a/packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts
+++ b/packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts
@@ -1,22 +1,38 @@
+import { OrgUserRoles } from '../../../enums/OrgUserRoles';
import projectAcl from '../../utils/projectAcl';
import { NextFunction, Request, Response } from 'express';
import catchError, { NcError } from './catchError';
import extractProjectIdAndAuthenticate from './extractProjectIdAndAuthenticate';
-export default function (handlerFn, permissionName) {
+export default function (
+ handlerFn,
+ permissionName,
+ {
+ allowedRoles,
+ blockApiTokenAccess,
+ }: {
+ allowedRoles?: (OrgUserRoles | string)[];
+ blockApiTokenAccess?: boolean;
+ } = {}
+) {
return [
extractProjectIdAndAuthenticate,
catchError(async function authMiddleware(req, _res, next) {
const roles = req?.session?.passport?.user?.roles;
+ if (req?.session?.passport?.user?.is_api_token && blockApiTokenAccess) {
+ NcError.forbidden('Not allowed with API token');
+ }
if (
+ (!allowedRoles || allowedRoles.some((role) => roles?.[role])) &&
!(
roles?.creator ||
roles?.owner ||
roles?.editor ||
roles?.viewer ||
roles?.commenter ||
- roles?.user ||
- roles?.user_new
+ roles?.[OrgUserRoles.SUPER_ADMIN] ||
+ roles?.[OrgUserRoles.CREATOR] ||
+ roles?.[OrgUserRoles.VIEWER]
)
) {
NcError.unauthorized('Unauthorized access');
diff --git a/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts b/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
index cbe127f711..152343f484 100644
--- a/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
+++ b/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;
}
}
}
diff --git a/packages/nocodb/src/lib/migrations/v2/nc_021_add_fields_in_token.ts b/packages/nocodb/src/lib/migrations/v2/nc_021_add_fields_in_token.ts
new file mode 100644
index 0000000000..9bb9c31176
--- /dev/null
+++ b/packages/nocodb/src/lib/migrations/v2/nc_021_add_fields_in_token.ts
@@ -0,0 +1,26 @@
+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 };
diff --git a/packages/nocodb/src/lib/models/ApiToken.ts b/packages/nocodb/src/lib/models/ApiToken.ts
index 6b480e067f..028c194c59 100644
--- a/packages/nocodb/src/lib/models/ApiToken.ts
+++ b/packages/nocodb/src/lib/models/ApiToken.ts
@@ -11,6 +11,7 @@ import NocoCache from '../cache/NocoCache';
export default class ApiToken {
project_id?: string;
db_alias?: string;
+ fk_user_id?: string;
description?: string;
permissions?: string;
token?: string;
@@ -29,6 +30,7 @@ export default class ApiToken {
await ncMeta.metaInsert(null, null, MetaTable.API_TOKENS, {
description: apiToken.description,
token,
+ fk_user_id: apiToken.fk_user_id,
});
await NocoCache.appendToList(
CacheScope.API_TOKEN,
@@ -38,14 +40,17 @@ export default class ApiToken {
return this.getByToken(token);
}
- static async list(ncMeta = Noco.ncMeta) {
- let tokens = await NocoCache.getList(CacheScope.API_TOKEN, []);
- if (!tokens.length) {
- tokens = await ncMeta.metaList(null, null, MetaTable.API_TOKENS);
- await NocoCache.setList(CacheScope.API_TOKEN, [], tokens);
- }
+ static async list(userId: string, ncMeta = Noco.ncMeta) {
+ // let tokens = await NocoCache.getList(CacheScope.API_TOKEN, []);
+ // if (!tokens.length) {
+ const tokens = await ncMeta.metaList(null, null, MetaTable.API_TOKENS, {
+ condition: { fk_user_id: userId },
+ });
+ // await NocoCache.setList(CacheScope.API_TOKEN, [], tokens);
+ // }
return tokens?.map((t) => new ApiToken(t));
}
+
static async delete(token, ncMeta = Noco.ncMeta) {
await NocoCache.deepDel(
CacheScope.API_TOKEN,
@@ -68,4 +73,72 @@ export default class ApiToken {
}
return data && new ApiToken(data);
}
+
+ public static async count(
+ {
+ fk_user_id,
+ includeUnmappedToken = false,
+ }: { fk_user_id?: string; includeUnmappedToken?: boolean } = {},
+ ncMeta = Noco.ncMeta
+ ): Promise {
+ const qb = ncMeta.knex(MetaTable.API_TOKENS);
+
+ if (fk_user_id) {
+ qb.where(`${MetaTable.API_TOKENS}.fk_user_id`, fk_user_id);
+ }
+
+ if (includeUnmappedToken) {
+ qb.orWhereNull(`${MetaTable.API_TOKENS}.fk_user_id`);
+ }
+
+ return (await qb.count('id', { as: 'count' }).first())?.count ?? 0;
+ }
+
+ public static async listWithCreatedBy(
+ {
+ limit = 10,
+ offset = 0,
+ fk_user_id,
+ includeUnmappedToken = false,
+ }: {
+ limit: number;
+ offset: number;
+ fk_user_id?: string;
+ includeUnmappedToken: boolean;
+ },
+ 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')
+ );
+
+ if (fk_user_id) {
+ queryBuilder.where(`${MetaTable.API_TOKENS}.fk_user_id`, fk_user_id);
+ }
+
+ if (includeUnmappedToken) {
+ queryBuilder.orWhereNull(`${MetaTable.API_TOKENS}.fk_user_id`);
+ }
+
+ return queryBuilder;
+ }
}
diff --git a/packages/nocodb/src/lib/models/KanbanView.ts b/packages/nocodb/src/lib/models/KanbanView.ts
index c583aa01af..52bd2bf088 100644
--- a/packages/nocodb/src/lib/models/KanbanView.ts
+++ b/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;
// below fields are not in use at this moment
// keep them for time being
diff --git a/packages/nocodb/src/lib/models/ProjectUser.ts b/packages/nocodb/src/lib/models/ProjectUser.ts
index 219e9140ac..03560d1ae7 100644
--- a/packages/nocodb/src/lib/models/ProjectUser.ts
+++ b/packages/nocodb/src/lib/models/ProjectUser.ts
@@ -183,4 +183,13 @@ export default class ProjectUser {
project_id: projectId,
});
}
+
+ static async getProjectsList(
+ userId: string,
+ ncMeta = Noco.ncMeta
+ ): Promise {
+ return await ncMeta.metaList2(null, null, MetaTable.PROJECT_USERS, {
+ condition: { fk_user_id: userId },
+ });
+ }
}
diff --git a/packages/nocodb/src/lib/models/SelectOption.ts b/packages/nocodb/src/lib/models/SelectOption.ts
index 86899667e4..28ce016c44 100644
--- a/packages/nocodb/src/lib/models/SelectOption.ts
+++ b/packages/nocodb/src/lib/models/SelectOption.ts
@@ -89,10 +89,15 @@ export default class SelectOption {
title: string,
ncMeta = Noco.ncMeta
): Promise {
- 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);
}
diff --git a/packages/nocodb/src/lib/models/Store.ts b/packages/nocodb/src/lib/models/Store.ts
new file mode 100644
index 0000000000..3fffcb122f
--- /dev/null
+++ b/packages/nocodb/src/lib/models/Store.ts
@@ -0,0 +1,47 @@
+import { NcError } from '../meta/helpers/catchError';
+import { extractProps } from '../meta/helpers/extractProps';
+import Noco from '../Noco';
+import { MetaTable } from '../utils/globals';
+import { SortType } from 'nocodb-sdk';
+
+// Store is used for storing key value pairs
+export default class Store {
+ key?: string;
+ value?: string;
+ type?: string;
+ env?: string;
+ tag?: string;
+ project_id?: string;
+ db_alias?: string;
+
+ constructor(data: Partial) {
+ Object.assign(this, data);
+ }
+
+ public static get(key: string, ncMeta = Noco.ncMeta): Promise {
+ return ncMeta.metaGet(null, null, MetaTable.STORE, { key });
+ }
+
+ static async saveOrUpdate(store: Store, ncMeta = Noco.ncMeta) {
+ if (!store.key) {
+ NcError.badRequest('Key is required');
+ }
+
+ const insertObj = extractProps(store, [
+ 'key',
+ 'value',
+ 'type',
+ 'env',
+ 'tag',
+ ]);
+
+ const existing = await Store.get(store.key, ncMeta);
+ if (existing) {
+ await ncMeta.metaUpdate(null, null, MetaTable.STORE, insertObj, {
+ key: store.key,
+ });
+ } else {
+ await ncMeta.metaInsert(null, null, MetaTable.STORE, insertObj);
+ }
+ }
+}
diff --git a/packages/nocodb/src/lib/models/SyncSource.ts b/packages/nocodb/src/lib/models/SyncSource.ts
index 3c14aa81d2..39f7bf7489 100644
--- a/packages/nocodb/src/lib/models/SyncSource.ts
+++ b/packages/nocodb/src/lib/models/SyncSource.ts
@@ -1,3 +1,4 @@
+import { NcError } from '../meta/helpers/catchError';
import Noco from '../Noco';
import { MetaTable } from '../utils/globals';
import { extractProps } from '../meta/helpers/extractProps';
@@ -132,4 +133,12 @@ export default class SyncSource {
syncSourceId
);
}
+
+ static async deleteByUserId(userId: string, ncMeta = Noco.ncMeta) {
+ if (!userId) NcError.badRequest('User Id is required');
+
+ return await ncMeta.metaDelete(null, null, MetaTable.SYNC_SOURCE, {
+ fk_user_id: userId,
+ });
+ }
}
diff --git a/packages/nocodb/src/lib/models/User.ts b/packages/nocodb/src/lib/models/User.ts
index 0e3b5ba82c..1620b95c9b 100644
--- a/packages/nocodb/src/lib/models/User.ts
+++ b/packages/nocodb/src/lib/models/User.ts
@@ -1,9 +1,10 @@
import { UserType } from 'nocodb-sdk';
+import { NcError } from '../meta/helpers/catchError';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
import Noco from '../Noco';
import { extractProps } from '../meta/helpers/extractProps';
import NocoCache from '../cache/NocoCache';
-import { NcError } from '../meta/helpers/catchError';
+
export default class User implements UserType {
id: string;
@@ -64,6 +65,7 @@ export default class User implements UserType {
return this.get(id, ncMeta);
}
+
public static async update(id, user: Partial, ncMeta = Noco.ncMeta) {
const updateObj = extractProps(user, [
'email',
@@ -89,12 +91,20 @@ export default class User implements UserType {
// set email prop to avoid generation of invalid cache key
updateObj.email = (await this.get(id, ncMeta))?.email?.toLowerCase();
}
+
+ // get old user
+ const existingUser = await this.get(id, ncMeta);
+
+ // delete the emailbased cache to avoid unexpected behaviour since we can update email as well
+ await NocoCache.del(`${CacheScope.USER}:${existingUser.email}`);
+
+ // as is unknown, delete user:___ in cache
+ await NocoCache.delAll(CacheScope.USER, `${existingUser.email}___*`);
+
// get existing cache
const keys = [
// update user:
`${CacheScope.USER}:${id}`,
- // update user:
- `${CacheScope.USER}:${user.email}`,
];
for (const key of keys) {
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
@@ -104,12 +114,11 @@ export default class User implements UserType {
await NocoCache.set(key, o);
}
}
- // as is unknown, delete user:___ in cache
- await NocoCache.delAll(CacheScope.USER, `${user.email}___*`);
// set meta
return await ncMeta.metaUpdate(null, null, MetaTable.USERS, updateObj, id);
}
+
public static async getByEmail(_email: string, ncMeta = Noco.ncMeta) {
const email = _email?.toLowerCase();
let user =
@@ -134,13 +143,24 @@ export default class User implements UserType {
return false;
}
- static async count(ncMeta = Noco.ncMeta) {
- return (
- await ncMeta.knex(MetaTable.USERS).count('id', { as: 'count' }).first()
- )?.count;
+ public static async count(
+ {
+ query = '',
+ }: {
+ query?: string;
+ } = {},
+ ncMeta = Noco.ncMeta
+ ): Promise {
+ const qb = ncMeta.knex(MetaTable.USERS);
+
+ if (query) {
+ qb.where('email', 'like', `%${query.toLowerCase?.()}%`);
+ }
+
+ return (await qb.count('id', { as: 'count' }).first()).count;
}
- static async get(userId, ncMeta = Noco.ncMeta) {
+ static async get(userId, ncMeta = Noco.ncMeta): Promise {
let user =
userId &&
(await NocoCache.get(
@@ -187,9 +207,9 @@ export default class User implements UserType {
`${MetaTable.USERS}.lastname`,
`${MetaTable.USERS}.username`,
`${MetaTable.USERS}.email_verified`,
+ `${MetaTable.USERS}.invite_token`,
`${MetaTable.USERS}.created_at`,
`${MetaTable.USERS}.updated_at`,
- `${MetaTable.USERS}.invite_token`,
`${MetaTable.USERS}.roles`
)
.select(
@@ -210,8 +230,17 @@ export default class User implements UserType {
static async delete(userId: string, ncMeta = Noco.ncMeta) {
if (!userId) NcError.badRequest('userId is required');
+
+ const user = await this.get(userId, ncMeta);
+
+ if (!user) NcError.badRequest('User not found');
+
+ // clear all user related cache
await NocoCache.delAll(CacheScope.USER, `${userId}___*`);
+ await NocoCache.delAll(CacheScope.USER, `${user.email}___*`);
await NocoCache.del(`${CacheScope.USER}:${userId}`);
+ await NocoCache.del(`${CacheScope.USER}:${user.email}`);
+
await ncMeta.metaDelete(null, null, MetaTable.USERS, userId);
}
}
diff --git a/packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts b/packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts
index 20ae8fb8c3..179262edee 100644
--- a/packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts
+++ b/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 'nc-help';
import BaseModel from './BaseModel';
import { XcCron } from './XcCron';
diff --git a/packages/nocodb/src/lib/utils/globals.ts b/packages/nocodb/src/lib/utils/globals.ts
index 54c43cecef..4f69d116ac 100644
--- a/packages/nocodb/src/lib/utils/globals.ts
+++ b/packages/nocodb/src/lib/utils/globals.ts
@@ -37,6 +37,7 @@ export enum MetaTable {
API_TOKENS = 'nc_api_tokens',
SYNC_SOURCE = 'nc_sync_source_v2',
SYNC_LOGS = 'nc_sync_logs_v2',
+ STORE = 'nc_store',
}
export const orderedMetaTables = [
diff --git a/packages/nocodb/src/lib/utils/packageVersion.ts b/packages/nocodb/src/lib/utils/packageVersion.ts
new file mode 100644
index 0000000000..272ac8b2ff
--- /dev/null
+++ b/packages/nocodb/src/lib/utils/packageVersion.ts
@@ -0,0 +1,37 @@
+import fs from 'fs';
+import path from 'path';
+
+let packageInfo: Record = {};
+
+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 };
diff --git a/packages/nocodb/src/lib/utils/projectAcl.ts b/packages/nocodb/src/lib/utils/projectAcl.ts
index b10a856bb5..bfae7a7ecd 100644
--- a/packages/nocodb/src/lib/utils/projectAcl.ts
+++ b/packages/nocodb/src/lib/utils/projectAcl.ts
@@ -1,3 +1,5 @@
+import { OrgUserRoles } from '../../enums/OrgUserRoles';
+
export default {
owner: {
exclude: {
@@ -271,15 +273,21 @@ export default {
dataCount: true,
},
},
- user_new: {
+ [OrgUserRoles.VIEWER]: {
include: {
+ apiTokenList: true,
+ apiTokenCreate: true,
+ apiTokenDelete: true,
passwordChange: true,
projectList: true,
},
},
- super: '*',
- user: {
+ [OrgUserRoles.SUPER_ADMIN]: '*',
+ [OrgUserRoles.CREATOR]: {
include: {
+ apiTokenList: true,
+ apiTokenCreate: true,
+ apiTokenDelete: true,
upload: true,
uploadViaURL: true,
passwordChange: true,
diff --git a/packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts b/packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts
index 44fa27b891..46af16fa9e 100644
--- a/packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts
+++ b/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 'nc-help';
import { GqlApiBuilder } from './gql/GqlApiBuilder';
import { XCEeError } from '../meta/NcMetaMgr';
import { RestApiBuilder } from './rest/RestApiBuilder';
diff --git a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts b/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts
index e0bdb821f0..00a85d4ef6 100644
--- a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts
+++ b/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 'nc-help';
import XcCache from '../plugins/adapters/cache/XcCache';
passport.serializeUser(function (
@@ -315,7 +315,7 @@ export default class RestAuthCtrl {
if (apiToken) {
done(null, { roles: 'editor' });
} else {
- return done({ msg: 'Invalid tok' });
+ return done({ msg: 'Invalid token' });
}
})
);
diff --git a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts b/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts
index ba9af7ed00..a91eeb5558 100644
--- a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts
+++ b/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 'nc-help';
import XcCache from '../plugins/adapters/cache/XcCache';
diff --git a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
index 3fab1baf5e..a51d04910b 100644
--- a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
+++ b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
@@ -2,13 +2,14 @@ import { NcConfig } from '../../interface/config';
import debug from 'debug';
import NcMetaIO from '../meta/NcMetaIO';
+import { Tele } from 'nc-help';
import ncProjectEnvUpgrader from './ncProjectEnvUpgrader';
import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045';
import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000';
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 {
@@ -33,6 +34,7 @@ export default class NcUpgrader {
{ name: '0011045', handler: ncProjectEnvUpgrader0011045 },
{ name: '0090000', handler: ncProjectUpgraderV2_0090000 },
{ name: '0098004', handler: ncDataTypesUpgrader },
+ { name: '0098005', handler: ncProjectRolesUpgrader },
];
if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) {
return;
diff --git a/packages/nocodb/src/lib/version-upgrader/ncProjectRolesUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/ncProjectRolesUpgrader.ts
new file mode 100644
index 0000000000..e1f204501a
--- /dev/null
+++ b/packages/nocodb/src/lib/version-upgrader/ncProjectRolesUpgrader.ts
@@ -0,0 +1,43 @@
+import { OrgUserRoles } from '../../enums/OrgUserRoles';
+import { NC_APP_SETTINGS } from '../constants';
+import Store from '../models/Store';
+import { MetaTable } from '../utils/globals';
+import { NcUpgraderCtx } from './NcUpgrader';
+
+/** Upgrader for upgrading roles */
+export default async function ({ ncMeta }: NcUpgraderCtx) {
+ const users = await ncMeta.metaList2(null, null, MetaTable.USERS);
+
+ for (const user of users) {
+ 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') {
+ return OrgUserRoles.VIEWER;
+ }
+ return r;
+ })
+ .join(',');
+ await ncMeta.metaUpdate(
+ null,
+ null,
+ MetaTable.USERS,
+ { roles: user.roles },
+ user.id
+ );
+ }
+
+ // set invite only signup if user have environment variable set
+ if (process.env.NC_INVITE_ONLY_SIGNUP) {
+ await Store.saveOrUpdate(
+ {
+ value: '{ "invite_only_signup": true }',
+ key: NC_APP_SETTINGS,
+ },
+ ncMeta
+ );
+ }
+}
diff --git a/packages/nocodb/tests/unit/rest/index.test.ts b/packages/nocodb/tests/unit/rest/index.test.ts
index 2944f6395a..05ef09c84a 100644
--- a/packages/nocodb/tests/unit/rest/index.test.ts
+++ b/packages/nocodb/tests/unit/rest/index.test.ts
@@ -1,5 +1,6 @@
import 'mocha';
import authTests from './tests/auth.test';
+import orgTests from './tests/org.test';
import projectTests from './tests/project.test';
import tableTests from './tests/table.test';
import tableRowTests from './tests/tableRow.test';
@@ -7,6 +8,7 @@ import viewRowTests from './tests/viewRow.test';
function restTests() {
authTests();
+ orgTests();
projectTests();
tableTests();
tableRowTests();
@@ -15,4 +17,4 @@ function restTests() {
export default function () {
describe('Rest', restTests);
-}
\ No newline at end of file
+}
diff --git a/packages/nocodb/tests/unit/rest/tests/auth.test.ts b/packages/nocodb/tests/unit/rest/tests/auth.test.ts
index 4b0ae080b0..f7bbd59988 100644
--- a/packages/nocodb/tests/unit/rest/tests/auth.test.ts
+++ b/packages/nocodb/tests/unit/rest/tests/auth.test.ts
@@ -74,7 +74,7 @@ function authTests() {
.get('/api/v1/auth/user/me')
.unset('xc-auth')
.expect(200);
-
+
if (!response.body?.roles?.guest) {
return new Error('User should be guest');
}
diff --git a/packages/nocodb/tests/unit/rest/tests/org.test.ts b/packages/nocodb/tests/unit/rest/tests/org.test.ts
new file mode 100644
index 0000000000..ea774f7896
--- /dev/null
+++ b/packages/nocodb/tests/unit/rest/tests/org.test.ts
@@ -0,0 +1,222 @@
+import { expect } from 'chai'
+import 'mocha'
+import request from 'supertest'
+import { OrgUserRoles } from '../../../../src/enums/OrgUserRoles'
+import init from '../../init'
+
+function authTests() {
+ let context
+
+ beforeEach(async function() {
+ context = await init()
+ })
+
+ it('Get users list', async () => {
+ const response = await request(context.app)
+ .get('/api/v1/users')
+ .set('xc-auth', context.token)
+ .expect(200)
+
+ expect(response.body).to.have.keys(['list', 'pageInfo'])
+ expect(response.body.list).to.have.length(1)
+
+ })
+
+ it('Invite a new user', async () => {
+
+ const response = await request(context.app)
+ .post('/api/v1/users')
+ .set('xc-auth', context.token).send({ email: 'a@nocodb.com' })
+ .expect(200)
+
+ console.log(response.body)
+
+ expect(response.body).to.have.property('invite_token').to.be.a('string')
+// todo: verify invite token
+ })
+
+ it('Update user role', async () => {
+ const email = 'a@nocodb.com'
+ // invite a user
+ await request(context.app)
+ .post('/api/v1/users')
+ .set('xc-auth', context.token).send({ email })
+ .expect(200)
+ const response = await request(context.app)
+ .get('/api/v1/users')
+ .set('xc-auth', context.token)
+ .expect(200)
+ expect(response.body.list).to.have.length(2)
+
+ const user = response.body.list.find(u => u.email === email)
+
+ expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER)
+
+
+ await request(context.app)
+ .patch('/api/v1/users/' + user.id)
+ .set('xc-auth', context.token)
+ .send({ roles: OrgUserRoles.CREATOR })
+ .expect(200)
+
+
+ const response2 = await request(context.app)
+ .get('/api/v1/users')
+ .set('xc-auth', context.token)
+ .expect(200)
+ expect(response2.body.list).to.have.length(2)
+
+ const user2 = response2.body.list.find(u => u.email === email)
+
+ expect(user2).to.have.property('roles').to.be.equal(OrgUserRoles.CREATOR)
+ })
+
+ it('Remove user', async () => {
+ const email = 'a@nocodb.com'
+ // invite a user
+ await request(context.app)
+ .post('/api/v1/users')
+ .set('xc-auth', context.token).send({ email })
+ .expect(200)
+
+ const response = await request(context.app)
+ .get('/api/v1/users')
+ .set('xc-auth', context.token)
+ .expect(200)
+ expect(response.body.list).to.have.length(2)
+
+ const user = response.body.list.find(u => u.email === email)
+
+ expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER)
+
+
+ await request(context.app)
+ .delete('/api/v1/users/' + user.id)
+ .set('xc-auth', context.token)
+ .expect(200)
+
+
+ const response2 = await request(context.app)
+ .get('/api/v1/users')
+ .set('xc-auth', context.token)
+ .expect(200)
+ expect(response2.body.list).to.have.length(1)
+
+ })
+
+
+ it('Get token list', async () => {
+ const response = await request(context.app)
+ .get('/api/v1/tokens')
+ .set('xc-auth', context.token)
+ .expect(200)
+
+ expect(response.body).to.have.keys(['list', 'pageInfo'])
+ expect(response.body.list).to.have.length(0)
+
+ })
+
+ it('Generate token', async () => {
+ const r = await request(context.app)
+ .post('/api/v1/tokens')
+ .set('xc-auth', context.token)
+ .send({ description: 'test' })
+ .expect(200)
+
+ const response = await request(context.app)
+ .get('/api/v1/tokens')
+ .set('xc-auth', context.token)
+ .expect(200)
+
+ expect(response.body).to.have.keys(['list', 'pageInfo'])
+ expect(response.body.list).to.have.length(1)
+ expect(response.body.list[0]).to.have.property('token').to.be.a('string')
+ expect(response.body.list[0]).to.have.property('description').to.be.a('string').to.be.eq('test')
+
+ })
+
+ it('Delete token', async () => {
+ const r = await request(context.app)
+ .post('/api/v1/tokens')
+ .set('xc-auth', context.token)
+ .send({ description: 'test' })
+ .expect(200)
+
+ let response = await request(context.app)
+ .get('/api/v1/tokens')
+ .set('xc-auth', context.token)
+ .expect(200)
+
+ expect(response.body).to.have.keys(['list', 'pageInfo'])
+ expect(response.body.list).to.have.length(1)
+
+ await request(context.app)
+ .delete('/api/v1/tokens/' + r.body.token)
+ .set('xc-auth', context.token)
+ .expect(200)
+
+
+ response = await request(context.app)
+ .get('/api/v1/tokens')
+ .set('xc-auth', context.token)
+ .expect(200)
+
+ expect(response.body).to.have.keys(['list', 'pageInfo'])
+ expect(response.body.list).to.have.length(0)
+
+ })
+
+ it.only('Disable/Enable signup', async () => {
+ const args = {
+ email: 'dummyuser@example.com',
+ password: 'A1234abh2@dsad',
+ };
+
+
+ await request(context.app)
+ .post('/api/v1/app-settings')
+ .set('xc-auth', context.token)
+ .send({ invite_only_signup: true })
+ .expect(200)
+
+
+ const failedRes = await request(context.app)
+ .post('/api/v1/auth/user/signup')
+ .send(args)
+ .expect(400)
+
+ expect(failedRes.body).to.be.an('object')
+ .to.have.property('msg')
+ .to.be.equal('Not allowed to signup, contact super admin.')
+
+ await request(context.app)
+ .post('/api/v1/app-settings')
+ .set('xc-auth', context.token)
+ .send({ invite_only_signup: false })
+ .expect(200)
+
+
+ const successRes = await request(context.app)
+ .post('/api/v1/auth/user/signup')
+ .send(args)
+ .expect(200)
+
+ expect(successRes.body).to.be.an('object')
+ .to.have.property('token')
+ .to.be.a('string')
+
+
+ const userMeRes = await request(context.app)
+ .get('/api/v1/auth/user/me')
+ .set('xc-auth', successRes.body.token)
+ .expect(200)
+
+ expect(userMeRes.body).to.be.an('object')
+ .to.have.property('email')
+ .to.be.eq(args.email)
+ })
+
+}
+
+export default function() {
+}
diff --git a/scripts/cypress/integration/common/5c_super_user_role.js b/scripts/cypress/integration/common/5c_super_user_role.js
index ed51a2171d..6935e18c5e 100644
--- a/scripts/cypress/integration/common/5c_super_user_role.js
+++ b/scripts/cypress/integration/common/5c_super_user_role.js
@@ -1,9 +1,7 @@
-import { loginPage } from "../../support/page_objects/navigation";
-import { roles } from "../../support/page_objects/projectConstants";
-
export const genTest = (apiType, dbType) => {
describe(`${apiType.toUpperCase()} api - Super user test`, () => {
- before(() => {});
+ before(() => {
+ });
beforeEach(() => {
cy.restoreLocalStorage();
@@ -13,71 +11,155 @@ export const genTest = (apiType, dbType) => {
cy.saveLocalStorage();
});
- after(() => {});
+ after(() => {
+ });
it(`Open App store page and check slack app`, () => {
- cy.visit("/#/apps").then((win) => {
- cy.get(".nc-app-store-title").should("exist");
- cy.get(".nc-app-store-card-Slack").should("exist");
+ cy.visit('/#/apps').then((win) => {
+ cy.get('.nc-app-store-title').should('exist');
+ cy.get('.nc-app-store-card-Slack').should('exist');
// install slack app
- cy.get(".nc-app-store-card-Slack .install-btn").invoke(
- "attr",
- "style",
- "right: 10px"
+ cy.get('.nc-app-store-card-Slack .install-btn').invoke(
+ 'attr',
+ 'style',
+ 'right: 10px'
);
cy.get(
- ".nc-app-store-card-Slack .install-btn .nc-app-store-card-install"
+ '.nc-app-store-card-Slack .install-btn .nc-app-store-card-install'
).click();
- cy.getActiveModal(".nc-modal-plugin-install")
+ cy.getActiveModal('.nc-modal-plugin-install')
.find('[placeholder="Channel Name"]')
- .type("Test channel");
+ .type('Test channel');
- cy.getActiveModal(".nc-modal-plugin-install")
+ cy.getActiveModal('.nc-modal-plugin-install')
.find('[placeholder="Webhook URL"]')
- .type("http://test.com");
+ .type('http://test.com');
- cy.getActiveModal(".nc-modal-plugin-install")
+ cy.getActiveModal('.nc-modal-plugin-install')
.find('button:contains("Save")')
.click();
- cy.toastWait("Successfully installed");
+ cy.toastWait('Successfully installed');
cy.get(
- ".nc-app-store-card-Slack .install-btn .nc-app-store-card-install"
- ).should("not.exist");
+ '.nc-app-store-card-Slack .install-btn .nc-app-store-card-install'
+ ).should('not.exist');
// update slack app config
- cy.get(".nc-app-store-card-Slack .install-btn .nc-app-store-card-edit")
- .should("exist")
+ cy.get('.nc-app-store-card-Slack .install-btn .nc-app-store-card-edit')
+ .should('exist')
.click();
- cy.getActiveModal(".nc-modal-plugin-install")
- .should("exist")
+ cy.getActiveModal('.nc-modal-plugin-install')
+ .should('exist')
.find('[placeholder="Channel Name"]')
- .should("have.value", "Test channel")
+ .should('have.value', 'Test channel')
.clear()
- .type("Test channel 2");
+ .type('Test channel 2');
- cy.getActiveModal(".nc-modal-plugin-install")
+ cy.getActiveModal('.nc-modal-plugin-install')
.get('button:contains("Save")')
.click();
- cy.toastWait("Successfully installed");
+ cy.toastWait('Successfully installed');
// reset slack app
- cy.get(".nc-app-store-card-Slack .install-btn .nc-app-store-card-reset")
- .should("exist")
+ cy.get('.nc-app-store-card-Slack .install-btn .nc-app-store-card-reset')
+ .should('exist')
.click();
- cy.getActiveModal(".nc-modal-plugin-uninstall")
- .should("exist")
+ cy.getActiveModal('.nc-modal-plugin-uninstall')
+ .should('exist')
.find('button:contains("Confirm")')
.click();
- cy.toastWait("Plugin uninstalled successfully");
+ cy.toastWait('Plugin uninstalled successfully');
});
});
+
+ it(`Open super user management page and add/delete user`, () => {
+ // delay for avoiding error related to vue router change
+ cy.wait(500);
+
+ cy.visit('/#/account/users').then((win) => {
+ cy.get('[data-cy="nc-super-user-list"]').should('exist')
+ .find('tbody tr').then((rows) => {
+ const initialUserCount = rows.length;
+
+ cy.get('[data-cy=\'nc-super-user-invite\'')
+ .click();
+
+ // additional wait to ensure the modal is fully loaded
+ cy.getActiveModal('.nc-modal-invite-user').should('exist');
+ cy.getActiveModal('.nc-modal-invite-user')
+ .find('input[placeholder="E-mail"]')
+ .should('exist');
+
+ cy.getActiveModal('.nc-modal-invite-user')
+ .find('input[placeholder="E-mail"]')
+ .type('test@nocodb.com');
+ cy.getActiveModal('.nc-modal-invite-user')
+ .find('.ant-select.nc-user-roles')
+ .click();
+
+ cy.getActiveModal('.nc-modal-invite-user')
+ .find('button.ant-btn-primary')
+ .click();
+
+ cy.toastWait('Successfully added user');
+
+
+ cy.getActiveModal().find('[data-cy="nc-root-user-invite-modal-close"]').click();
+
+
+ cy.get('[data-cy="nc-super-user-list"]').should('exist')
+ .find('tbody tr').should('have.length', initialUserCount +1)
+ .last().find('[data-cy="nc-super-user-delete"]').click();
+
+ cy.getActiveModal().find('.ant-modal-confirm-btns .ant-btn-primary').click();
+
+ cy.toastWait('User deleted successfully');
+
+
+ cy.get('[data-cy="nc-super-user-list"]').should('exist')
+ .find('tbody tr').should('have.length', initialUserCount);
+ });
+ });
+ });
+
+ it('User management settings', () => {
+
+ });
+
+ it(`Token management`, () => {
+ // delay for avoiding error related to vue router change
+ cy.wait(500);
+ cy.visit('/#/account/tokens').then((win) => {
+
+ cy.get('[data-cy="nc-token-list"]').should('exist').find(':contains("No Data")').should('exist');
+ cy.get('[data-cy="nc-token-create"]').click();
+ cy.get('[data-cy="nc-token-modal-description"]').type('Descriptqion');
+ cy.get('[data-cy="nc-token-modal-save"]').click();
+ cy.toastWait('Token generated successfully');
+
+ cy.get('[data-cy="nc-token-list"]').should('exist')
+ .find('tbody tr').should('have.length', 1);
+
+ cy.get('.nc-token-menu').click();
+
+ cy.getActiveMenu('.nc-dropdown-api-token-mgmt').find('.ant-dropdown-menu-item:contains("Remove")').click();
+
+
+ cy.getActiveModal().find('.ant-modal-confirm-btns .ant-btn-primary').click();
+
+ cy.toastWait('Token deleted successfully');
+
+ });
+ });
+
+
});
+
};
diff --git a/scripts/cypress/integration/common/6h_change_password.js b/scripts/cypress/integration/common/6h_change_password.js
index 7ccc4747ab..32957916fa 100644
--- a/scripts/cypress/integration/common/6h_change_password.js
+++ b/scripts/cypress/integration/common/6h_change_password.js
@@ -14,7 +14,7 @@ export const genTest = (apiType, dbType) => {
cy.get("[data-cy='nc-menu-accounts']").click();
cy.get("[data-cy='nc-menu-accounts__user-settings']").click();
-
+ cy.get('.user-management-tab-label:contains("Reset Password")').should('exist').click()
cy.get("[data-cy='nc-user-settings-form']").should("exist");
});
diff --git a/scripts/cypress/integration/test/restRoles.js b/scripts/cypress/integration/test/restRoles.js
index f226d30c70..8ec3bc482c 100644
--- a/scripts/cypress/integration/test/restRoles.js
+++ b/scripts/cypress/integration/test/restRoles.js
@@ -11,8 +11,8 @@ const nocoTestSuite = (apiType, dbType) => {
setCurrentMode(apiType, dbType);
t01.genTest(apiType, dbType);
- t5a.genTest(apiType, dbType);
- t5b.genTest(apiType, dbType);
+ // t5a.genTest(apiType, dbType);
+ // t5b.genTest(apiType, dbType);
t5c.genTest(apiType, dbType);
};
diff --git a/scripts/sdk/swagger.json b/scripts/sdk/swagger.json
index bb920e9f0d..cfe2c4cd86 100644
--- a/scripts/sdk/swagger.json
+++ b/scripts/sdk/swagger.json
@@ -441,6 +441,397 @@
},
"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",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/ApiToken"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "created_by": {
+ "type": "string"
+ }
+ }
+ }
+ ],
+ "type": "object"
+ }
+ },
+ "pageInfo": {
+ "$ref": "#/components/schemas/Paginated"
+ }
+ },
+ "required": [
+ "list",
+ "pageInfo"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "",
+ "tags": [
+ "Org tokens"
+ ]
+ },
+ "parameters": [],
+ "post": {
+ "summary": "",
+ "operationId": "org-tokens-create",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiToken"
+ }
+ }
+ }
+ },
+ "tags": [
+ "Org tokens"
+ ]
+ }
+ },
+ "/api/v1/license": {
+ "get": {
+ "summary": "App license get",
+ "operationId": "org-license-get",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "",
+ "tags": [
+ "Org license"
+ ]
+ },
+ "parameters": [],
+ "post": {
+ "summary": "App license get",
+ "operationId": "org-license-set",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ "Org license"
+ ]
+ }
+ },
+ "/api/v1/app-settings": {
+ "get": {
+ "summary": "App settings get",
+ "operationId": "org-app-settings-get",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "invite_only_signup": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "",
+ "tags": [
+ "Org app settings"
+ ]
+ },
+ "parameters": [],
+ "post": {
+ "summary": "App app settings get",
+ "operationId": "org-app-settings-set",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "invite_only_signup": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ "Org app settings"
+ ]
+ }
+ },
+ "/api/v1/tokens/{token}": {
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "name": "token",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "delete": {
+ "summary": "",
+ "operationId": "org-tokens-delete",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "tags": [
+ "Org tokens"
+ ]
+ }
+ },
+ "/api/v1/users": {
+ "get": {
+ "summary": "Organisation Users",
+ "operationId": "org-users-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/User"
+ }
+ },
+ "pageInfo": {
+ "$ref": "#/components/schemas/Paginated"
+ }
+ },
+ "required": [
+ "list",
+ "pageInfo"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "",
+ "tags": [
+ "Org users"
+ ]
+ },
+ "parameters": [],
+ "post": {
+ "summary": "Organisation User Add",
+ "operationId": "org-users-add",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "tags": [
+ "Org users"
+ ]
+ }
+ },
+ "/api/v1/users/{userId}": {
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "name": "userId",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "patch": {
+ "summary": "Organisation User Update",
+ "operationId": "org-users-update",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "tags": [
+ "Org users"
+ ],
+ "description": "",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "delete": {
+ "summary": "Organisation User Delete",
+ "operationId": "org-users-delete",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "tags": [
+ "Org users"
+ ],
+ "description": ""
+ }
+ },
+ "/api/v1/users/{userId}/resend-invite": {
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "name": "userId",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "post": {
+ "summary": "Organisation User Invite",
+ "operationId": "org-users-resend-invite",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "tags": [
+ "Org users"
+ ]
+ }
+ },
+ "/api/v1/users/{userId}/generate-reset-url": {
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "name": "userId",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "post": {
+ "summary": "Organisation User Generate Password Reset Token",
+ "operationId": "org-users-generate-password-reset-token",
+ "tags": [
+ "Org users"
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "reset_password_token": {
+ "type": "string"
+ },
+ "reset_password_url": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/v1/db/meta/projects/{projectId}/users": {
"get": {
"summary": "Project users",
@@ -4961,9 +5352,6 @@
"description": "",
"parameters": [
{
- "schema": {
- "type": "array"
- },
"in": "query",
"name": "ids",
"required": true
@@ -6291,8 +6679,7 @@
"format": "email"
},
"roles": {
- "type": "string",
- "format": "email"
+ "type": "string"
},
"date_of_birth": {
"type": "string",
@@ -8496,7 +8883,12 @@
},
"description": {
"type": "string"
- }
+ },
+ "fk_user_id": {
+ "type": "string"
+ },
+ "created_at": {},
+ "updated_at": {}
}
},
"HookLog": {
diff --git a/tests/playwright/package.json b/tests/playwright/package.json
index 62257e568f..d685046cce 100644
--- a/tests/playwright/package.json
+++ b/tests/playwright/package.json
@@ -11,6 +11,7 @@
"test:repeat": "TRACE=true npx playwright test --workers=4 --repeat-each=12",
"test:quick": "TRACE=true PW_QUICK_TEST=1 npx playwright test --workers=4",
"test:debug": "./startPlayWrightServer.sh; PW_TEST_REUSE_CONTEXT=1 PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:31000/ PWDEBUG=console npx playwright test -c playwright.config.ts --headed --project=chromium --retries 0 --timeout 0 --workers 1 --max-failures=1",
+ "test:debug:watch": "npx nodemon -e ts -w ./ -x \"npm run test:debug\"",
"test:debug:quick:sqlite": "./startPlayWrightServer.sh; PW_QUICK_TEST=1 PW_TEST_REUSE_CONTEXT=1 PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:31000/ PWDEBUG=console npx playwright test -c playwright.config.ts --headed --project=chromium --retries 0 --timeout 5 --workers 1 --max-failures=1",
"ci:test": "npx playwright test --workers=2",
"ci:test:shard:1": "npx playwright test --workers=2 --shard=1/2",
diff --git a/tests/playwright/pages/Account/Settings.ts b/tests/playwright/pages/Account/Settings.ts
new file mode 100644
index 0000000000..fa5fbcd1f6
--- /dev/null
+++ b/tests/playwright/pages/Account/Settings.ts
@@ -0,0 +1,41 @@
+import { expect } from '@playwright/test';
+import BasePage from '../Base';
+import { AccountPage } from './index';
+
+export class AccountSettingsPage extends BasePage {
+ private accountPage: AccountPage;
+
+ constructor(accountPage: AccountPage) {
+ super(accountPage.rootPage);
+ this.accountPage = accountPage;
+ }
+
+ async goto() {
+ await this.rootPage.goto('/#/account/users/settings', { waitUntil: 'networkidle' });
+ }
+
+ async waitUntilContentLoads() {
+ return this.rootPage.waitForResponse(resp => resp.url().includes('api/v1/app-settings') && resp.status() === 200);
+ }
+
+ get() {
+ return this.accountPage.get().locator(`[data-testid="nc-app-settings"]`);
+ }
+
+ getInviteOnlyCheckbox() {
+ return this.get().locator(`.nc-invite-only-signup-checkbox`);
+ }
+
+ async getInviteOnlyCheckboxValue() {
+ return this.get().locator(`.nc-invite-only-signup-checkbox`).isChecked();
+ }
+
+ async checkInviteOnlySignupCheckbox(value: boolean) {
+ return expect(await this.getInviteOnlyCheckboxValue()).toBe(value);
+ }
+
+ async toggleInviteOnlyCheckbox() {
+ await this.getInviteOnlyCheckbox().click();
+ await this.verifyToast({ message: 'Settings saved successfully' });
+ }
+}
diff --git a/tests/playwright/pages/Account/Token.ts b/tests/playwright/pages/Account/Token.ts
new file mode 100644
index 0000000000..4bed02e320
--- /dev/null
+++ b/tests/playwright/pages/Account/Token.ts
@@ -0,0 +1,52 @@
+import { Locator } from '@playwright/test';
+import BasePage from '../Base';
+import { AccountPage } from './index';
+
+export class AccountTokenPage extends BasePage {
+ readonly createBtn: Locator;
+ readonly createModal: Locator;
+ private accountPage: AccountPage;
+
+ constructor(accountPage: AccountPage) {
+ super(accountPage.rootPage);
+ this.accountPage = accountPage;
+ this.createBtn = this.get().locator(`[data-testid="nc-token-create"]`);
+ this.createModal = accountPage.rootPage.locator(`.nc-modal-generate-token`);
+ }
+
+ async goto() {
+ await this.rootPage.goto('/#/account/tokens', { waitUntil: 'networkidle' });
+ }
+
+ get() {
+ return this.accountPage.get().locator(`[data-testid="nc-token-list"]`);
+ }
+
+ async createToken({ description }: { description: string }) {
+ await this.createBtn.click();
+ await this.createModal.locator(`input[placeholder="Description"]`).fill(description);
+ await this.createModal.locator(`[data-testid="nc-token-modal-save"]`).click();
+ await this.verifyToast({ message: 'Token generated successfully' });
+ }
+
+ getTokenRow({ idx = 0 }) {
+ return this.get().locator(`tr:nth-child(${idx})`);
+ }
+
+ async toggleVisibility({ idx = 0 }) {
+ const row = this.getTokenRow({ idx });
+ await row.locator('.nc-toggle-token-visibility').click();
+ }
+
+ async openRowActionMenu({ description }: { description: string }) {
+ const userRow = this.get().locator(`tr:has-text("${description}")`);
+ return userRow.locator(`.nc-token-menu`).click();
+ }
+
+ async deleteToken({ description }: { description: string }) {
+ await this.openRowActionMenu({ description });
+ await this.rootPage.locator('[data-testid="nc-token-row-action-icon"] .nc-delete-token').click();
+ await this.rootPage.locator('.ant-modal-confirm-confirm button:has-text("Ok")').click();
+ await this.verifyToast({ message: 'Token deleted successfully' });
+ }
+}
diff --git a/tests/playwright/pages/Account/Users.ts b/tests/playwright/pages/Account/Users.ts
new file mode 100644
index 0000000000..cfccd459a1
--- /dev/null
+++ b/tests/playwright/pages/Account/Users.ts
@@ -0,0 +1,81 @@
+import { Locator } from '@playwright/test';
+import BasePage from '../Base';
+import { AccountPage } from './index';
+
+export class AccountUsersPage extends BasePage {
+ readonly inviteUserBtn: Locator;
+ readonly inviteUserModal: Locator;
+ private accountPage: AccountPage;
+
+ constructor(accountPage: AccountPage) {
+ super(accountPage.rootPage);
+ this.accountPage = accountPage;
+ this.inviteUserBtn = this.get().locator(`[data-testid="nc-super-user-invite"]`);
+ this.inviteUserModal = accountPage.rootPage.locator(`.nc-modal-invite-user`);
+ }
+
+ async goto() {
+ await this.rootPage.goto('/#/account/users/list', { waitUntil: 'networkidle' });
+ }
+
+ get() {
+ return this.accountPage.get().locator(`[data-testid="nc-super-user-list"]`);
+ }
+
+ async invite({ email: _email, role }: { email: string; role: string }) {
+ const email = this.prefixEmail(_email);
+
+ await this.inviteUserBtn.click();
+ await this.inviteUserModal.locator(`input[placeholder="E-mail"]`).fill(email);
+ await this.inviteUserModal.locator(`.nc-user-roles`).click();
+ const userRoleModal = this.rootPage.locator(`.nc-dropdown-user-role`);
+ await userRoleModal.locator(`.nc-role-option:has-text("${role}")`).click();
+ await this.inviteUserModal.locator(`button:has-text("Invite")`).click();
+ await this.verifyToast({ message: 'Successfully added user' });
+
+ return await this.inviteUserModal.locator(`.ant-alert-message`).innerText();
+ }
+
+ prefixEmail(email: string) {
+ const parallelId = process.env.TEST_PARALLEL_INDEX ?? '0';
+ return `nc_test_${parallelId}_${email}`;
+ }
+
+ async closeInvite() {
+ // two btn-icon-only in invite modal: close & copy url
+ await this.inviteUserModal.locator(`button.ant-btn-icon-only:visible`).first().click();
+ }
+
+ getUserRow({ email: _email }: { email: string }) {
+ const email = this.prefixEmail(_email);
+ return this.get().locator(`tr:has-text("${email}")`);
+ }
+
+ async updateRole({ email, role }: { email: string; role: string }) {
+ const userRow = this.getUserRow({ email });
+
+ await userRow.locator(`.nc-user-roles`).click();
+
+ // todo: replace delay with waitForSelector
+ await new Promise(resolve => setTimeout(resolve, 400));
+
+ await this.rootPage.locator(`.nc-users-list-role-option:visible:has-text("${role}")`).click();
+ await this.verifyToast({ message: 'Successfully updated the user details' });
+ }
+
+ async inviteMore() {
+ await this.inviteUserModal.locator(`button:has-text("Invite More")`).click();
+ }
+
+ async openRowActionMenu({ email }: { email: string }) {
+ const userRow = this.getUserRow({ email });
+ return userRow.locator(`.nc-user-row-action`).click();
+ }
+
+ async deleteUser({ email }: { email: string }) {
+ await this.openRowActionMenu({ email });
+ await this.rootPage.locator('[data-testid="nc-super-user-delete"]:visible').click();
+ await this.rootPage.locator('.ant-modal-confirm-confirm button:has-text("Ok")').click();
+ await this.verifyToast({ message: 'User deleted successfully' });
+ }
+}
diff --git a/tests/playwright/pages/Account/index.ts b/tests/playwright/pages/Account/index.ts
new file mode 100644
index 0000000000..c2f4da62bb
--- /dev/null
+++ b/tests/playwright/pages/Account/index.ts
@@ -0,0 +1,22 @@
+import { Page } from '@playwright/test';
+import BasePage from '../Base';
+
+export class AccountPage extends BasePage {
+ constructor(page: Page) {
+ super(page);
+ }
+
+ get() {
+ return this.rootPage.locator('body');
+ }
+
+ async openAppMenu() {
+ await this.rootPage.locator('.nc-menu-accounts').click();
+ }
+
+ async signOut() {
+ await this.openAppMenu();
+ await this.rootPage.locator('div.nc-project-menu-item:has-text("Sign Out"):visible').click();
+ await this.rootPage.locator('[data-testid="nc-form-signin"]:visible').waitFor();
+ }
+}
diff --git a/tests/playwright/pages/ProjectsPage/index.ts b/tests/playwright/pages/ProjectsPage/index.ts
index 2aa9f41481..3c336dd6ff 100644
--- a/tests/playwright/pages/ProjectsPage/index.ts
+++ b/tests/playwright/pages/ProjectsPage/index.ts
@@ -54,6 +54,10 @@ export class ProjectsPage extends BasePage {
await this.rootPage.waitForTimeout(2000);
}
+ async checkProjectCreateButton({ exists = true }) {
+ await expect(this.rootPage.locator('.nc-new-project-menu:visible')).toHaveCount(exists ? 1 : 0);
+ }
+
async reloadProjects() {
const reloadUiAction = this.get().locator('[data-testid="projects-reload-button"]').click();
await this.waitForResponse({
diff --git a/tests/playwright/pages/SigninPage/index.ts b/tests/playwright/pages/SigninPage/index.ts
new file mode 100644
index 0000000000..6a1cc05cc8
--- /dev/null
+++ b/tests/playwright/pages/SigninPage/index.ts
@@ -0,0 +1,52 @@
+import { Page } from '@playwright/test';
+import BasePage from '../Base';
+import { ProjectsPage } from '../ProjectsPage';
+import { expect } from '@playwright/test';
+
+export class SigninPage extends BasePage {
+ readonly projectsPage: ProjectsPage;
+
+ constructor(rootPage: Page) {
+ super(rootPage);
+ this.projectsPage = new ProjectsPage(rootPage);
+ }
+
+ prefixEmail(email: string) {
+ const parallelId = process.env.TEST_PARALLEL_INDEX ?? '0';
+ return `nc_test_${parallelId}_${email}`;
+ }
+
+ goto() {
+ return this.rootPage.goto('/#/signin/', { waitUntil: 'networkidle' });
+ }
+
+ get() {
+ return this.rootPage.locator('html');
+ }
+
+ async signIn({
+ email,
+ password,
+ withoutPrefix,
+ expectedError,
+ }: {
+ email: string;
+ password: string;
+ withoutPrefix?: boolean;
+ expectedError?: string;
+ }) {
+ if (!withoutPrefix) email = this.prefixEmail(email);
+
+ const signUp = this.get();
+ await signUp.locator('button:has-text("SIGN IN")').waitFor();
+
+ await signUp.locator(`input[placeholder="Enter your work email"]`).fill(email);
+ await signUp.locator(`input[placeholder="Enter your password"]`).fill(password);
+ await signUp.locator(`button:has-text("SIGN IN")`).click();
+ if (expectedError) {
+ await expect(signUp.getByTestId('nc-signin-error')).toHaveText(expectedError);
+ } else {
+ await this.projectsPage.waitToBeRendered();
+ }
+ }
+}
diff --git a/tests/playwright/pages/SignupPage/index.ts b/tests/playwright/pages/SignupPage/index.ts
index 95416f7725..5c9b415507 100644
--- a/tests/playwright/pages/SignupPage/index.ts
+++ b/tests/playwright/pages/SignupPage/index.ts
@@ -1,6 +1,7 @@
import { Page } from '@playwright/test';
import BasePage from '../Base';
import { ProjectsPage } from '../ProjectsPage';
+import { expect } from '@playwright/test';
export class SignupPage extends BasePage {
readonly projectsPage: ProjectsPage;
@@ -16,22 +17,36 @@ export class SignupPage extends BasePage {
}
goto() {
- return this.rootPage.goto('/#/signup/');
+ return this.rootPage.goto('/#/signup/', { waitUntil: 'networkidle' });
}
get() {
return this.rootPage.locator('html');
}
- async signUp({ email, password, withoutPrefix }: { email: string; password: string; withoutPrefix?: boolean }) {
+ async signUp({
+ email,
+ password,
+ withoutPrefix,
+ expectedError,
+ }: {
+ email: string;
+ password: string;
+ withoutPrefix?: boolean;
+ expectedError?: string;
+ }) {
if (!withoutPrefix) email = this.prefixEmail(email);
- const signUp = this.rootPage;
+ const signUp = this.get();
await signUp.locator('button:has-text("SIGN UP")').waitFor();
await signUp.locator(`input[placeholder="Enter your work email"]`).fill(email);
await signUp.locator(`input[placeholder="Enter your password"]`).fill(password);
await signUp.locator(`button:has-text("SIGN UP")`).click();
- await this.projectsPage.waitToBeRendered();
+ if (expectedError) {
+ await expect(signUp.getByTestId('nc-signup-error')).toHaveText(expectedError);
+ } else {
+ await this.projectsPage.waitToBeRendered();
+ }
}
}
diff --git a/tests/playwright/setup/index.ts b/tests/playwright/setup/index.ts
index 764d36795f..a738d5c095 100644
--- a/tests/playwright/setup/index.ts
+++ b/tests/playwright/setup/index.ts
@@ -57,7 +57,7 @@ const setup = async ({ page, isEmptyProject }: { page: Page; isEmptyProject?: bo
const project = response.data.project;
- await page.goto(`/#/nc/${project.id}/auth`);
+ await page.goto(`/#/nc/${project.id}/auth`, { waitUntil: 'networkidle' });
return { project, token, dbType } as NcContext;
};
diff --git a/tests/playwright/tests/accountTokenManagement.spec.ts b/tests/playwright/tests/accountTokenManagement.spec.ts
new file mode 100644
index 0000000000..7093318b92
--- /dev/null
+++ b/tests/playwright/tests/accountTokenManagement.spec.ts
@@ -0,0 +1,24 @@
+import { test } from '@playwright/test';
+import { AccountPage } from '../pages/Account';
+import { AccountTokenPage } from '../pages/Account/Token';
+import setup from '../setup';
+
+test.describe('User roles', () => {
+ let accountTokenPage: AccountTokenPage;
+ let accountPage: AccountPage;
+ // @ts-ignore
+ let context: any;
+
+ test.beforeEach(async ({ page }) => {
+ context = await setup({ page });
+ accountPage = new AccountPage(page);
+ accountTokenPage = new AccountTokenPage(accountPage);
+ });
+
+ test('Create and Delete token', async () => {
+ test.slow();
+ await accountTokenPage.goto();
+ await accountTokenPage.createToken({ description: 'test token' });
+ await accountTokenPage.deleteToken({ description: 'test token' });
+ });
+});
diff --git a/tests/playwright/tests/accountUserManagement.spec.ts b/tests/playwright/tests/accountUserManagement.spec.ts
new file mode 100644
index 0000000000..285a818749
--- /dev/null
+++ b/tests/playwright/tests/accountUserManagement.spec.ts
@@ -0,0 +1,78 @@
+import { test } from '@playwright/test';
+import { AccountPage } from '../pages/Account';
+import { AccountUsersPage } from '../pages/Account/Users';
+import { ProjectsPage } from '../pages/ProjectsPage';
+import { SignupPage } from '../pages/SignupPage';
+import setup from '../setup';
+
+const roleDb = [
+ { email: 'creator@nocodb.com', role: 'Organization Level Creator', url: '' },
+ { email: 'viewer@nocodb.com', role: 'Organization Level Viewer', url: '' },
+];
+
+test.describe('User roles', () => {
+ let accountUsersPage: AccountUsersPage;
+ let accountPage: AccountPage;
+ let signupPage: SignupPage;
+ let projectsPage: ProjectsPage;
+ // @ts-ignore
+ let context: any;
+
+ test.beforeEach(async ({ page }) => {
+ context = await setup({ page });
+ accountPage = new AccountPage(page);
+ accountUsersPage = new AccountUsersPage(accountPage);
+
+ signupPage = new SignupPage(accountPage.rootPage);
+ projectsPage = new ProjectsPage(accountPage.rootPage);
+ });
+
+ test('Invite user, update role and delete user', async () => {
+ test.slow();
+
+ await accountUsersPage.goto();
+
+ // invite user
+ for (let i = 0; i < roleDb.length; i++) {
+ roleDb[i].url = await accountUsersPage.invite({
+ email: roleDb[i].email,
+ role: roleDb[i].role,
+ });
+ await accountUsersPage.closeInvite();
+ await signupAndVerify(i);
+ await accountPage.rootPage.reload({ waitUntil: 'networkidle' });
+ await accountUsersPage.goto();
+ }
+
+ // update role
+ for (let i = 0; i < roleDb.length; i++) {
+ await accountUsersPage.updateRole({
+ email: roleDb[i].email,
+ role: 'Organization Level Viewer',
+ });
+ }
+
+ // delete user
+ for (let i = 0; i < roleDb.length; i++) {
+ await accountUsersPage.deleteUser({
+ email: roleDb[i].email,
+ });
+ }
+ });
+
+ // signup and verify create project button exist or not based on role
+ async function signupAndVerify(roleIdx: number) {
+ await accountPage.signOut();
+
+ await accountPage.rootPage.goto(roleDb[roleIdx].url);
+
+ await signupPage.signUp({
+ email: roleDb[roleIdx].email,
+ password: 'Password123.',
+ });
+
+ await projectsPage.checkProjectCreateButton({
+ exists: roleDb[roleIdx].role === 'Organization Level Creator',
+ });
+ }
+});
diff --git a/tests/playwright/tests/accountUserSettings.spec.ts b/tests/playwright/tests/accountUserSettings.spec.ts
new file mode 100644
index 0000000000..c6fe4b06cb
--- /dev/null
+++ b/tests/playwright/tests/accountUserSettings.spec.ts
@@ -0,0 +1,65 @@
+import { test } from '@playwright/test';
+import { AccountPage } from '../pages/Account';
+import { AccountSettingsPage } from '../pages/Account/Settings';
+import { SignupPage } from '../pages/SignupPage';
+import setup from '../setup';
+
+test.describe('App settings', () => {
+ let accountSettingsPage: AccountSettingsPage;
+ let accountPage: AccountPage;
+ // @ts-ignore
+ let context: any;
+
+ test.beforeEach(async ({ page }) => {
+ context = await setup({ page });
+ accountPage = new AccountPage(page);
+ accountSettingsPage = new AccountSettingsPage(accountPage);
+ });
+
+ test('Toggle invite only signup', async () => {
+ test.slow();
+
+ await accountSettingsPage.goto();
+
+ // todo: remove after route navigation issue resolved
+ await accountSettingsPage.rootPage.reload({ waitUntil: 'networkidle' });
+
+ await accountSettingsPage.waitUntilContentLoads();
+
+ // enable invite only signup
+ if (!(await accountSettingsPage.getInviteOnlyCheckboxValue())) {
+ await accountSettingsPage.toggleInviteOnlyCheckbox();
+ await accountSettingsPage.checkInviteOnlySignupCheckbox(true);
+ }
+
+ await accountPage.signOut();
+
+ const signupPage = new SignupPage(accountPage.rootPage);
+ await signupPage.goto();
+
+ await signupPage.signUp({
+ email: 'test-user-1@nocodb.com',
+ password: 'Password123.',
+ expectedError: 'Not allowed to signup, contact super admin.',
+ });
+
+ await signupPage.rootPage.reload({ waitUntil: 'networkidle' });
+
+ await accountSettingsPage.goto();
+
+ await accountSettingsPage.waitUntilContentLoads();
+
+ await accountSettingsPage.checkInviteOnlySignupCheckbox(true);
+ await accountSettingsPage.toggleInviteOnlyCheckbox();
+ await accountSettingsPage.checkInviteOnlySignupCheckbox(false);
+
+ await accountPage.signOut();
+
+ await signupPage.goto();
+
+ await signupPage.signUp({
+ email: 'test-user-1@nocodb.com',
+ password: 'Password123.',
+ });
+ });
+});
diff --git a/tests/playwright/tests/rolesSuperUser.spec.ts b/tests/playwright/tests/rolesSuperUser.spec.ts
index de835ca818..80dcf58c06 100644
--- a/tests/playwright/tests/rolesSuperUser.spec.ts
+++ b/tests/playwright/tests/rolesSuperUser.spec.ts
@@ -14,7 +14,7 @@ test.describe('Super user', () => {
test('AppStore access', async () => {
await dashboard.closeTab({ title: 'Team & Auth' });
- await dashboard.rootPage.goto('/#/apps');
+ await dashboard.rootPage.goto('/#/account/apps', { waitUntil: 'networkidle' });
await dashboard.rootPage.waitForLoadState('load');
const appPage = await dashboard.rootPage;