Skip to content

Commit

Permalink
fix: share permission (#614)
Browse files Browse the repository at this point in the history
* fix: share Page Submission Form and Copy Interface Validation

* chore: share module

* fix: field id is error

* fix: share page in share error

* chore: add share page without permission i18n

* chore: remove useless console

* feat: add MenuDeleteItem components

* chore: i18n

* fix: submit visible field check and share meta allow copy set limit
  • Loading branch information
boris-w authored May 21, 2024
1 parent 04c6aea commit 18c67bb
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 32 deletions.
5 changes: 4 additions & 1 deletion apps/nestjs-backend/src/features/share/share.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ export class ShareController {

@UseGuards(AuthGuard)
@Get('/:shareId/view')
async getShareView(@Param('shareId') shareId: string): Promise<ShareViewGetVo> {
async getShareView(
@Param('shareId') shareId: string,
@Request() _req?: any
): Promise<ShareViewGetVo> {
return await this.shareService.getShareView(shareId);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/nestjs-backend/src/features/share/share.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ import { ShareService } from './share.service';
],
providers: [ShareService, DbProvider, ShareSocketService],
controllers: [ShareController],
exports: [ShareService],
exports: [ShareService, ShareSocketService],
})
export class ShareModule {}
44 changes: 35 additions & 9 deletions apps/nestjs-backend/src/features/share/share.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
IShareViewCollaboratorsRo,
} from '@teable/openapi';
import { Knex } from 'knex';
import { pick } from 'lodash';
import { isEmpty, pick } from 'lodash';
import { InjectModel } from 'nest-knexjs';
import { InjectDbProvider } from '../../db-provider/db.provider';
import { IDbProvider } from '../../db-provider/db.provider.interface';
Expand Down Expand Up @@ -83,13 +83,19 @@ export class ShareService {
viewId: view.id,
filterHidden: !shareMeta?.includeHiddenField,
});
const { records } = await this.recordService.getRecords(tableId, {
viewId,
skip: 0,
take: 50,
fieldKeyType: FieldKeyType.Id,
projection: fields.map((f) => f.id),
});

let records: IRecordsVo['records'] = [];
if (view.type !== ViewType.Form) {
const recordsData = await this.recordService.getRecords(tableId, {
viewId,
skip: 0,
take: 50,
fieldKeyType: FieldKeyType.Id,
projection: fields.map((f) => f.id),
});
records = recordsData.records;
}

return {
shareMeta,
shareId,
Expand Down Expand Up @@ -140,8 +146,24 @@ export class ShareService {
}

async formSubmit(shareInfo: IShareViewInfo, shareViewFormSubmitRo: ShareViewFormSubmitRo) {
const { tableId } = shareInfo;
const { tableId, view } = shareInfo;
const { fields } = shareViewFormSubmitRo;
if (view.type !== ViewType.Form) {
throw new ForbiddenException('view type is not form');
}
// check field hidden
const visibleFields = await this.fieldService.getFieldsByQuery(tableId, {
viewId: view.id,
filterHidden: !view.shareMeta?.includeHiddenField,
});

if (
(!visibleFields.length && !isEmpty(fields)) ||
visibleFields.some((field) => !(field.id in fields))
) {
throw new ForbiddenException('field is hidden, not allowed');
}

const { records } = await this.prismaService.$tx(async () => {
return await this.recordOpenApiService.createRecords(tableId, {
records: [{ fields }],
Expand All @@ -155,6 +177,10 @@ export class ShareService {
}

async copy(shareInfo: IShareViewInfo, shareViewCopyRo: IRangesRo) {
if (!shareInfo.view.shareMeta?.allowCopy) {
throw new ForbiddenException('not allowed to copy');
}

return this.selectionService.copy(shareInfo.tableId, {
viewId: shareInfo.view.id,
...shareViewCopyRo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,7 @@ export class ViewOpenApiController {
@Body(new ZodValidationPipe(viewShareMetaRoSchema))
viewShareMetaRo: IViewShareMetaRo
): Promise<void> {
return await this.viewOpenApiService.setViewProperty(
tableId,
viewId,
'shareMeta',
viewShareMetaRo
);
return await this.viewOpenApiService.updateShareMeta(tableId, viewId, viewShareMetaRo);
}

@Permissions('view|update')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
IGetViewFilterLinkRecordsVo,
IUpdateOrderRo,
IUpdateRecordOrdersRo,
IViewShareMetaRo,
} from '@teable/openapi';
import { Knex } from 'knex';
import { InjectModel } from 'nest-knexjs';
Expand Down Expand Up @@ -205,6 +206,26 @@ export class ViewOpenApiService {
});
}

async updateShareMeta(tableId: string, viewId: string, viewShareMetaRo: IViewShareMetaRo) {
const curView = await this.prismaService.view
.findFirstOrThrow({
select: { type: true },
where: { tableId, id: viewId, deletedTime: null },
})
.catch(() => {
throw new BadRequestException('View not found');
});

// allow copy view type
if (
'allowCopy' in viewShareMetaRo &&
![ViewType.Grid, ViewType.Gantt].includes(curView.type as ViewType)
) {
throw new BadRequestException(`View type(${curView.type}) not support copy`);
}
return await this.setViewProperty(tableId, viewId, 'shareMeta', viewShareMetaRo);
}

async setViewProperty(
tableId: string,
viewId: string,
Expand Down
6 changes: 3 additions & 3 deletions apps/nestjs-backend/src/utils/is-not-hidden-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ export const isNotHiddenField = (
const { stackFieldId, coverFieldId } = (options ?? {}) as IKanbanViewOptions;
return (
[stackFieldId, coverFieldId].includes(fieldId) ||
Boolean((columnMeta[fieldId] as { visible?: boolean }).visible)
Boolean((columnMeta[fieldId] as { visible?: boolean })?.visible)
);
}

if ([ViewType.Form].includes(viewType)) {
return Boolean((columnMeta[fieldId] as { visible?: boolean }).visible);
return Boolean((columnMeta[fieldId] as { visible?: boolean })?.visible);
}
return !(columnMeta[fieldId] as { hidden?: boolean }).hidden;
return !(columnMeta[fieldId] as { hidden?: boolean })?.hidden;
};
106 changes: 100 additions & 6 deletions apps/nestjs-backend/test/share.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import {
getShareViewCollaborators as apiGetShareViewCollaborators,
getBaseCollaboratorList as apiGetBaseCollaboratorList,
updateViewColumnMeta as apiUpdateViewColumnMeta,
updateViewShareMeta as apiUpdateViewShareMeta,
SHARE_VIEW_COPY,
} from '@teable/openapi';
import type { ITableFullVo, ShareViewGetVo } from '@teable/openapi';
import { map } from 'lodash';
import { createAnonymousUserAxios } from './utils/axios-instance/anonymous-user';
import { getError } from './utils/get-error';
import {
createTable,
createView,
Expand Down Expand Up @@ -74,12 +77,27 @@ describe('OpenAPI ShareController (e2e)', () => {
await app.close();
});

it('api/:shareId/view (GET)', async () => {
const result = await anonymousUser.get<ShareViewGetVo>(urlBuilder(SHARE_VIEW_GET, { shareId }));
const shareViewData = result.data;
// filter hidden field
expect(shareViewData.fields.length).toEqual(fieldIds.length - 1);
expect(shareViewData.viewId).toEqual(viewId);
describe('api/:shareId/view (GET)', async () => {
it('should return view', async () => {
const result = await anonymousUser.get<ShareViewGetVo>(
urlBuilder(SHARE_VIEW_GET, { shareId })
);
const shareViewData = result.data;
// filter hidden field
expect(shareViewData.fields.length).toEqual(fieldIds.length - 1);
expect(shareViewData.viewId).toEqual(viewId);
});

it('records return [] in form view', async () => {
const result = await createView(tableId, formViewRo);
const formViewId = result.id;
const shareResult = await apiEnableShareView({ tableId, viewId: formViewId });
const formViewShareId = shareResult.data.shareId;
const resultData = await anonymousUser.get<ShareViewGetVo>(
urlBuilder(SHARE_VIEW_GET, { shareId: formViewShareId })
);
expect(resultData.data.records).toEqual([]);
});
});

describe('api/:shareId/view/form-submit (POST)', () => {
Expand All @@ -104,6 +122,34 @@ describe('OpenAPI ShareController (e2e)', () => {
const record = result.data as IRecord;
expect(record.createdBy).toEqual(ANONYMOUS_USER_ID);
});

it('submit exclude form view', async () => {
const result = await createView(tableId, gridViewRo);
const gridViewId = result.id;
const shareResult = await apiEnableShareView({ tableId, viewId: gridViewId });
const gridViewShareId = shareResult.data.shareId;
const error = await getError(() =>
anonymousUser.post(urlBuilder(SHARE_VIEW_FORM_SUBMIT, { shareId: gridViewShareId }), {
fields: {},
})
);
expect(error?.status).toEqual(403);
});

it('submit include hidden field', async () => {
const hiddenFieldId = fieldIds[fieldIds.length - 1];
await updateViewColumnMeta(tableId, formViewId, [
{ fieldId: fieldIds[fieldIds.length - 1], columnMeta: { visible: false } },
]);
const error = await getError(() =>
anonymousUser.post(urlBuilder(SHARE_VIEW_FORM_SUBMIT, { shareId: fromViewShareId }), {
fields: {
[hiddenFieldId]: null,
},
})
);
expect(error?.status).toEqual(403);
});
});

describe('api/:shareId/view/link-records (GET)', () => {
Expand Down Expand Up @@ -348,4 +394,52 @@ describe('OpenAPI ShareController (e2e)', () => {
});
});
});

describe('api/:shareId/view/copy (PATCH)', () => {
let gridViewId: string;
let gridViewShareId: string;

beforeEach(async () => {
const result = await createView(tableId, gridViewRo);
gridViewId = result.id;

const shareResult = await apiEnableShareView({ tableId, viewId: gridViewId });
await apiUpdateViewShareMeta(tableId, gridViewId, { allowCopy: true });
gridViewShareId = shareResult.data.shareId;
});

it('should return 200', async () => {
const result = await anonymousUser.get(
urlBuilder(SHARE_VIEW_COPY, { shareId: gridViewShareId }),
{
params: {
ranges: JSON.stringify([
[0, 0],
[1, 1],
]),
},
}
);
expect(result.status).toEqual(200);
});

it('share not allow copy', async () => {
const result = await createView(tableId, gridViewRo);
const gridViewId = result.id;

const shareResult = await apiEnableShareView({ tableId, viewId: gridViewId });
const gridViewShareId = shareResult.data.shareId;
const error = await getError(() =>
anonymousUser.get(urlBuilder(SHARE_VIEW_COPY, { shareId: gridViewShareId }), {
params: {
ranges: JSON.stringify([
[0, 0],
[1, 1],
]),
},
})
);
expect(error?.status).toEqual(403);
});
});
});
44 changes: 44 additions & 0 deletions apps/nestjs-backend/test/view.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
updateViewName,
getViewFilterLinkRecords,
getTableById,
updateViewShareMeta,
enableShareView,
} from '@teable/openapi';
import { getError } from './utils/get-error';
import {
createField,
getFields,
Expand Down Expand Up @@ -306,4 +309,45 @@ describe('OpenAPI ViewController (e2e)', () => {
]);
});
});

describe('/api/table/{tableId}/view/:viewId/column-meta (PUT)', () => {
let tableId: string;
let gridViewId: string;
let formViewId: string;
beforeAll(async () => {
const table = await createTable(baseId, { name: 'table' });
tableId = table.id;
const gridView = await createView(table.id, {
name: 'Grid view',
type: ViewType.Grid,
});
gridViewId = gridView.id;
const formView = await createView(table.id, {
name: 'Form view',
type: ViewType.Form,
});
formViewId = formView.id;
await enableShareView({ tableId, viewId: formViewId });
await enableShareView({ tableId, viewId: gridViewId });
});

afterAll(async () => {
await deleteTable(baseId, tableId);
});

it('update allowCopy success', async () => {
await updateViewShareMeta(tableId, gridViewId, { allowCopy: true });
const view = await getView(tableId, gridViewId);
expect(view.shareMeta?.allowCopy).toBe(true);
});

it('update allowCopy with disallowed view types', async () => {
const error = await getError(() =>
updateViewShareMeta(tableId, formViewId, { allowCopy: true })
);

expect(error?.status).toEqual(400);
expect(error?.message).toEqual(`View type(${ViewType.Form}) not support copy`);
});
});
});
Loading

0 comments on commit 18c67bb

Please sign in to comment.