diff --git a/packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue b/packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue index ef100d2db9..823edbc787 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue @@ -70,9 +70,9 @@ async function changeLockType(type: LockType) { if (!view.value) return - if (type === 'personal') { - // Coming soon - return message.info(t('msg.toast.futureRelease')) + // if default view block the change since it's not allowed + if (type === 'personal' && view.value.is_default) { + return message.info(t('msg.toast.notAllowedToChangeDefaultView')) } try { view.value.lock_type = type @@ -306,6 +306,10 @@ const onDelete = async () => { + + + + diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index 74274b11a5..cb6d850470 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -1806,7 +1806,8 @@ "formEmailSMTP": "Please activate SMTP plugin in App store for enabling email notification", "collabView": "Successfully Switched to collaborative view", "lockedView": "Successfully Switched to locked view", - "futureRelease": "Coming soon!" + "futureRelease": "Coming soon!", + "notAllowedToChangeDefaultView": "You are not allowed to change the default view" }, "success": { "licenseKeyUpdated": "License Key Updated", diff --git a/packages/nc-gui/lib/enums.ts b/packages/nc-gui/lib/enums.ts index df97fe0c36..366884de03 100644 --- a/packages/nc-gui/lib/enums.ts +++ b/packages/nc-gui/lib/enums.ts @@ -1,3 +1,5 @@ +import { ViewLockType } from 'nocodb-sdk' + export { ClientType, IntegrationCategoryType, SyncDataType } from 'nocodb-sdk' export enum Language { @@ -50,11 +52,7 @@ export enum NavigateDir { PREV, } -export enum LockType { - Personal = 'personal', - Locked = 'locked', - Collaborative = 'collaborative', -} +export { ViewLockType as LockType } export enum TabType { TABLE = 'table', diff --git a/packages/nocodb-sdk/src/lib/enums.ts b/packages/nocodb-sdk/src/lib/enums.ts index 375cfc6f5f..c02830a779 100644 --- a/packages/nocodb-sdk/src/lib/enums.ts +++ b/packages/nocodb-sdk/src/lib/enums.ts @@ -434,3 +434,9 @@ export enum IntegrationCategoryType { STORAGE = 'storage', OTHERS = 'others', } + +export enum ViewLockType { + Personal = 'personal', + Locked = 'locked', + Collaborative = 'collaborative', +} diff --git a/packages/nocodb/src/controllers/meta-diffs.controller.ts b/packages/nocodb/src/controllers/meta-diffs.controller.ts index b2abb16d34..98ac24aae6 100644 --- a/packages/nocodb/src/controllers/meta-diffs.controller.ts +++ b/packages/nocodb/src/controllers/meta-diffs.controller.ts @@ -1,10 +1,10 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import { GlobalGuard } from '~/guards/global/global.guard'; import { MetaDiffsService } from '~/services/meta-diffs.service'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard'; import { TenantContext } from '~/decorators/tenant-context.decorator'; -import { NcContext } from '~/interface/config'; +import { NcContext, NcRequest } from '~/interface/config'; @Controller() @UseGuards(MetaApiLimiterGuard, GlobalGuard) @@ -32,10 +32,12 @@ export class MetaDiffsController { @TenantContext() context: NcContext, @Param('baseId') baseId: string, @Param('sourceId') sourceId: string, + @Req() req: NcRequest, ) { return await this.metaDiffsService.baseMetaDiff(context, { sourceId, baseId, + user: req.user, }); } } diff --git a/packages/nocodb/src/helpers/populateMeta.ts b/packages/nocodb/src/helpers/populateMeta.ts index 6e1c22f2e2..d3d900454d 100644 --- a/packages/nocodb/src/helpers/populateMeta.ts +++ b/packages/nocodb/src/helpers/populateMeta.ts @@ -3,6 +3,7 @@ import { isVirtualCol, RelationTypes } from 'nocodb-sdk'; import { pluralize, singularize } from 'inflection'; import { isLinksOrLTAR } from 'nocodb-sdk'; import { getUniqueColumnAliasName, getUniqueColumnName } from './getUniqueName'; +import type { UserType } from 'nocodb-sdk'; import type { RollupColumn } from '~/models'; import type LinkToAnotherRecordColumn from '~/models/LinkToAnotherRecordColumn'; import type Source from '~/models/Source'; @@ -205,9 +206,17 @@ export async function extractAndGenerateManyToManyRelations( export async function populateMeta( context: NcContext, - source: Source, - base: Base, - logger?: (message: string) => void, + { + source, + base, + logger, + user, + }: { + source: Source; + base: Base; + logger?: (message: string) => void; + user: UserType; + }, ): Promise { const info = { type: 'rest', @@ -347,6 +356,7 @@ export async function populateMeta( title: table.title, type: table.type || 'table', order: table.order, + user_id: user.id, }, ); @@ -489,6 +499,7 @@ export async function populateMeta( // todo: sanitize type: ModelTypes.VIEW, order: table.order, + user_id: user.id, }, ); diff --git a/packages/nocodb/src/meta/migrations/v2/nc_066_personal_view.ts b/packages/nocodb/src/meta/migrations/v2/nc_066_personal_view.ts index 7dc89aa4dd..8780fc63b0 100644 --- a/packages/nocodb/src/meta/migrations/v2/nc_066_personal_view.ts +++ b/packages/nocodb/src/meta/migrations/v2/nc_066_personal_view.ts @@ -3,15 +3,15 @@ import { MetaTable } from '~/utils/globals'; const up = async (knex: Knex) => { await knex.schema.alterTable(MetaTable.VIEWS, (table) => { - table.boolean('is_personal').defaultTo(false); table.string('created_by', 20).index(); + table.string('owned_by', 20).index(); }); }; const down = async (knex: Knex) => { await knex.schema.alterTable(MetaTable.SOURCES, (table) => { - table.dropColumn('is_personal'); table.dropColumn('created_by'); + table.dropColumn('owned_by'); }); }; diff --git a/packages/nocodb/src/models/Model.ts b/packages/nocodb/src/models/Model.ts index 8cc813452b..00a4590789 100644 --- a/packages/nocodb/src/models/Model.ts +++ b/packages/nocodb/src/models/Model.ts @@ -140,6 +140,7 @@ export default class Model implements TableType { mm?: BoolType; type?: ModelTypes; source_id?: string; + user_id: string; }, ncMeta = Noco.ncMeta, ) { @@ -199,6 +200,8 @@ export default class Model implements TableType { type: ViewTypes.GRID, base_id: baseId, source_id: sourceId, + created_by: model.user_id, + owned_by: model.user_id, }, { getColumns: async () => insertedColumns, @@ -1154,7 +1157,7 @@ export default class Model implements TableType { context: NcContext, { modelId, - userId + userId, }: { modelId: string; userId?: string; diff --git a/packages/nocodb/src/models/View.ts b/packages/nocodb/src/models/View.ts index 998f39eb9c..9471223a6f 100644 --- a/packages/nocodb/src/models/View.ts +++ b/packages/nocodb/src/models/View.ts @@ -74,7 +74,7 @@ export default class View implements ViewType { type: ViewTypes; lock_type?: ViewType['lock_type']; created_by?: string; - is_personal?: boolean; + owned_by?: string; fk_model_id: string; model?: Model; @@ -1263,7 +1263,10 @@ export default class View implements ViewType { password?: string; uuid?: string; meta?: any; + owned_by?: string; + created_by?: string; }, + includeCreatedByAndUpdateBy = false, ncMeta = Noco.ncMeta, ) { const updateObj = extractProps(body, [ @@ -1275,6 +1278,7 @@ export default class View implements ViewType { 'password', 'meta', 'uuid', + ...(includeCreatedByAndUpdateBy ? ['owned_by', 'created_by'] : []) ]); const oldView = await this.get(context, viewId, ncMeta); @@ -1986,6 +1990,8 @@ export default class View implements ViewType { copy_from_id?: string; fk_grp_col_id?: string; calendar_range?: Partial[]; + created_by: string; + owned_by: string; }, model: { getColumns: (context: NcContext, ncMeta?) => Promise; @@ -2002,6 +2008,8 @@ export default class View implements ViewType { 'base_id', 'source_id', 'meta', + 'created_by', + 'owned_by', ]); if (!insertObj.order) { diff --git a/packages/nocodb/src/services/bases.service.ts b/packages/nocodb/src/services/bases.service.ts index cb7f6e2dfa..4f3c40ffdc 100644 --- a/packages/nocodb/src/services/bases.service.ts +++ b/packages/nocodb/src/services/bases.service.ts @@ -282,7 +282,7 @@ export class BasesService { // populate metadata if existing table for (const source of await base.getSources()) { if (process.env.NC_CLOUD !== 'true' && !base.is_meta) { - const info = await populateMeta(context, source, base); + const info = await populateMeta(context, {source, base, user: param.user}); this.appHooksService.emit(AppEvents.APIS_CREATED, { info, diff --git a/packages/nocodb/src/services/calendars.service.ts b/packages/nocodb/src/services/calendars.service.ts index 0e8a5de173..fb4c5e9661 100644 --- a/packages/nocodb/src/services/calendars.service.ts +++ b/packages/nocodb/src/services/calendars.service.ts @@ -45,6 +45,8 @@ export class CalendarsService { type: ViewTypes.CALENDAR, base_id: model.base_id, source_id: model.source_id, + created_by: param.user.id, + owned_by: param.user.id, }, model, ); diff --git a/packages/nocodb/src/services/columns.service.ts b/packages/nocodb/src/services/columns.service.ts index b4de119103..c26e474801 100644 --- a/packages/nocodb/src/services/columns.service.ts +++ b/packages/nocodb/src/services/columns.service.ts @@ -3067,6 +3067,7 @@ export class ColumnsService { base: Base; reuse?: ReusableParams; colExtra?: any; + user: UserType; }, ) { validateParams(['parentId', 'childId', 'type'], param.column); @@ -3369,6 +3370,7 @@ export class ColumnsService { // todo: sanitize mm: true, columns: associateTableCols, + user_id: param.user.id, }, ); diff --git a/packages/nocodb/src/services/forms.service.ts b/packages/nocodb/src/services/forms.service.ts index 6e409fee6c..29ccca24c9 100644 --- a/packages/nocodb/src/services/forms.service.ts +++ b/packages/nocodb/src/services/forms.service.ts @@ -53,6 +53,8 @@ export class FormsService { type: ViewTypes.FORM, base_id: model.base_id, source_id: model.source_id, + created_by: param.user.id, + owned_by: param.user.id, }, model, ); diff --git a/packages/nocodb/src/services/galleries.service.ts b/packages/nocodb/src/services/galleries.service.ts index 2a093c6d20..24d85ed471 100644 --- a/packages/nocodb/src/services/galleries.service.ts +++ b/packages/nocodb/src/services/galleries.service.ts @@ -47,6 +47,8 @@ export class GalleriesService { type: ViewTypes.GALLERY, base_id: model.base_id, source_id: model.source_id, + created_by: param.user.id, + owned_by: param.user.id, }, model, ); diff --git a/packages/nocodb/src/services/grids.service.ts b/packages/nocodb/src/services/grids.service.ts index 8f141ea29e..3df1b3d8f6 100644 --- a/packages/nocodb/src/services/grids.service.ts +++ b/packages/nocodb/src/services/grids.service.ts @@ -37,6 +37,8 @@ export class GridsService { type: ViewTypes.GRID, base_id: model.base_id, source_id: model.source_id, + created_by: param.req?.user.id, + owned_by: param.req?.user.id, }, model, ); diff --git a/packages/nocodb/src/services/kanbans.service.ts b/packages/nocodb/src/services/kanbans.service.ts index e8ffb835f3..039d68dfca 100644 --- a/packages/nocodb/src/services/kanbans.service.ts +++ b/packages/nocodb/src/services/kanbans.service.ts @@ -46,6 +46,8 @@ export class KanbansService { type: ViewTypes.KANBAN, base_id: model.base_id, source_id: model.source_id, + owned_by: param.user.id, + created_by: param.user.id, }, model, ); diff --git a/packages/nocodb/src/services/maps.service.ts b/packages/nocodb/src/services/maps.service.ts index 3e12393393..a22afe7c64 100644 --- a/packages/nocodb/src/services/maps.service.ts +++ b/packages/nocodb/src/services/maps.service.ts @@ -42,6 +42,8 @@ export class MapsService { type: ViewTypes.MAP, base_id: model.base_id, source_id: model.source_id, + created_by: param.user.id, + owned_by: param.user.id, }, model, ); diff --git a/packages/nocodb/src/services/meta-diffs.service.ts b/packages/nocodb/src/services/meta-diffs.service.ts index 68145e8b06..fd40f37f7a 100644 --- a/packages/nocodb/src/services/meta-diffs.service.ts +++ b/packages/nocodb/src/services/meta-diffs.service.ts @@ -8,6 +8,7 @@ import { UITypes, } from 'nocodb-sdk'; import { pluralize, singularize } from 'inflection'; +import type { UserType } from 'nocodb-sdk'; import type { LinksColumn, LinkToAnotherRecordColumn } from '~/models'; import type { NcContext } from '~/interface/config'; import { AppHooksService } from '~/services/app-hooks/app-hooks.service'; @@ -647,7 +648,7 @@ export class MetaDiffsService { async baseMetaDiff( context: NcContext, - param: { baseId: string; sourceId: string }, + param: { baseId: string; sourceId: string; user: UserType }, ) { const base = await Base.getWithInfo(context, param.baseId); const source = await Source.get(context, param.sourceId); @@ -662,9 +663,17 @@ export class MetaDiffsService { async syncBaseMeta( context: NcContext, - base: Base, - source: Source, - throwOnFail = false, + { + base, + source, + throwOnFail = false, + user, + }: { + base: Base; + source: Source; + throwOnFail?: boolean; + user: UserType; + }, ) { if (source.is_meta) { if (throwOnFail) NcError.badRequest('Cannot sync meta source'); @@ -711,6 +720,7 @@ export class MetaDiffsService { source, ), type: ModelTypes.TABLE, + user_id: user.id, }); for (const column of columns) { @@ -738,6 +748,7 @@ export class MetaDiffsService { table_name: table_name, title: getTableNameAlias(table_name, base.prefix, source), type: ModelTypes.VIEW, + user_id: user.id, }); for (const column of columns) { @@ -909,7 +920,7 @@ export class MetaDiffsService { async metaDiffSync(context: NcContext, param: { baseId: string; req: any }) { const base = await Base.getWithInfo(context, param.baseId); for (const source of base.sources) { - await this.syncBaseMeta(context, base, source); + await this.syncBaseMeta(context, { base, source, user: param.req.user }); } this.appHooksService.emit(AppEvents.META_DIFF_SYNC, { @@ -931,7 +942,12 @@ export class MetaDiffsService { const base = await Base.getWithInfo(context, param.baseId); const source = await Source.get(context, param.sourceId); - await this.syncBaseMeta(context, base, source, true); + await this.syncBaseMeta(context, { + base, + source, + throwOnFail: true, + user: param.req.user, + }); this.appHooksService.emit(AppEvents.META_DIFF_SYNC, { base, diff --git a/packages/nocodb/src/services/sources.service.ts b/packages/nocodb/src/services/sources.service.ts index f2551509e7..0802dd5cf5 100644 --- a/packages/nocodb/src/services/sources.service.ts +++ b/packages/nocodb/src/services/sources.service.ts @@ -173,7 +173,7 @@ export class SourcesService { param.logger?.('Populating meta'); - const info = await populateMeta(context, source, base, param.logger); + const info = await populateMeta(context, {source, base, logger:param.logger, user: param.req.user}); await populateRollupColumnAndHideLTAR(context, source, base); diff --git a/packages/nocodb/src/services/tables.service.ts b/packages/nocodb/src/services/tables.service.ts index 3a8c6af779..93800fcb88 100644 --- a/packages/nocodb/src/services/tables.service.ts +++ b/packages/nocodb/src/services/tables.service.ts @@ -10,7 +10,9 @@ import { ProjectRoles, RelationTypes, UITypes, + ViewLockType, } from 'nocodb-sdk'; +import { LockType } from 'nc-gui/lib/enums'; import { MetaDiffsService } from './meta-diffs.service'; import { ColumnsService } from './columns.service'; import type { @@ -353,9 +355,13 @@ export class TablesService { ); //await View.list(param.tableId) - table.views = viewList.filter((table: any) => { - return Object.keys(param.user?.roles).some( - (role) => param.user?.roles[role] && !table.disabled[role], + table.views = viewList.filter((view: any) => { + return ( + Object.keys(param.user?.roles).some( + (role) => param.user?.roles[role] && !view.disabled[role], + ) && + (view.lock_type !== ViewLockType.Locked || + view.fk_owned_by === param.user.id) ); }); diff --git a/packages/nocodb/src/services/views.service.ts b/packages/nocodb/src/services/views.service.ts index 5d55bded25..7c1f0cc19a 100644 --- a/packages/nocodb/src/services/views.service.ts +++ b/packages/nocodb/src/services/views.service.ts @@ -10,6 +10,7 @@ import { AppHooksService } from '~/services/app-hooks/app-hooks.service'; import { validatePayload } from '~/helpers'; import { NcError } from '~/helpers/catchError'; import { Model, ModelRoleVisibility, View } from '~/models'; +import {WorkspaceUser} from "~/ee/models"; // todo: move async function xcVisibilityMetaGet( @@ -136,18 +137,72 @@ export class ViewsService { 'swagger.json#/components/schemas/ViewUpdateReq', param.view, ); + const oldView = await View.get(context, param.viewId); - const view = await View.get(context, param.viewId); - - if (!view) { + if (!oldView) { NcError.viewNotFound(param.viewId); } - const result = await View.update(context, param.viewId, param.view); + let ownedBy = oldView.owned_by; + let createdBy = oldView.created_by; + let includeCreatedByAndUpdateBy = false; + + // check if the lock_type changing to `personal` and only allow if user is the owner + // if the owned_by is not the same as the user, then throw error + // if owned_by is empty, then only allow owner of project to change + if ( + param.view.lock_type === 'personal' && + param.view.lock_type !== oldView.lock_type + ) { + // if owned_by is not empty then check if the user is the owner of the project + if (ownedBy && ownedBy !== param.user.id) { + NcError.unauthorized('Only owner can change to personal view'); + } + + // if empty then check if current user is the owner of the project then allow and update the owned_by + if (!ownedBy && (param.user as any).base_roles?.[ProjectRoles.OWNER]) { + includeCreatedByAndUpdateBy = true; + ownedBy = param.user.id; + if (!createdBy) { + createdBy = param.user.id; + } + } else if (!ownedBy) { + // todo: move to catchError + NcError.unauthorized('Only owner can change to personal view'); + } + } + + if(ownedBy && param.view.owned_by && param.user.id === ownedBy) { + ownedBy = param.view.owned_by + + // verify if the new owned_by is a valid user who have access to the base/workspace + // if not then throw error + const baseUser = await BaseUser.get(context,param.view.owned_by, context.base_id); + + if(!baseUser){ + NcError.badRequest('Invalid user'); + } + + // // todo: ee only + // if(!baseUser) { + // const workspace = await WorkspaceUser + // } + } + + const result = await View.update( + context, + param.viewId, + { + ...param.view, + owned_by: ownedBy, + created_by: createdBy, + }, + includeCreatedByAndUpdateBy, + ); this.appHooksService.emit(AppEvents.VIEW_UPDATE, { view: { - ...view, + ...oldView, ...param.view, }, user: param.user,