Skip to content

Commit

Permalink
Merge branch 'permission-based-auth' into rewrite-admin-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
eilrix committed May 20, 2022
2 parents caa160e + 46ee78a commit 105b6bb
Show file tree
Hide file tree
Showing 24 changed files with 390 additions and 87 deletions.
2 changes: 1 addition & 1 deletion system/admin-panel/src/pages/product/Product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const ProductPage = () => {
stockAmount
stockStatus
manageStock
categories(pagedParams: { pageSize: 10000 }) {
categories {
id
}
customMeta (keys: ${JSON.stringify(getCustomMetaKeysFor(EDBEntity.Product))})
Expand Down
4 changes: 4 additions & 0 deletions system/core/backend/src/models/entities/order.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export class Order extends BaseEntity implements TOrder {
@UpdateDateColumn()
updateDate?: Date | null;

@Field(() => Boolean, { nullable: true })
@Column({ type: "boolean", default: true, nullable: true })
isEnabled?: boolean | null;

@JoinTable()
@ManyToMany(type => Coupon)
coupons?: Coupon[] | null;
Expand Down
3 changes: 3 additions & 0 deletions system/core/backend/src/models/inputs/order.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ export class OrderInput extends BasePageInput implements TOrderInput {

@Field(type => [String], { nullable: true })
couponCodes?: string[] | null;

@Field(type => Boolean, { nullable: true })
isEnabled?: boolean | null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,12 @@ export class ProductCategoryRepository extends TreeRepository<ProductCategory> {
return true;
}

async getCategoriesOfProduct(productId: number, params?: TPagedParams<TProductCategory>): Promise<TProductCategory[]> {
async getCategoriesOfProduct(productId: number): Promise<TProductCategory[]> {
logger.log('ProductCategoryRepository::getCategoriesOfProduct id: ' + productId);
const qb = this.createQueryBuilder(this.metadata.tablePath);
applyGetManyFromOne(qb, this.metadata.tablePath, 'products',
getCustomRepository(ProductRepository).metadata.tablePath, productId);

applyGetPaged(qb, this.metadata.tablePath, params);
return await qb.getMany();
}

Expand Down
2 changes: 1 addition & 1 deletion system/core/backend/src/repositories/product.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class ProductRepository extends BaseRepository<Product> {
}
if (options.withCategories) {
product.categories = await getCustomRepository(ProductCategoryRepository)
.getCategoriesOfProduct(product.id, { pageSize: 1000 });
.getCategoriesOfProduct(product.id);
}
if (options.withVariants) {
product.variants = await this.getProductVariantsOfProduct(product.id);
Expand Down
6 changes: 6 additions & 0 deletions system/core/common/src/types/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ export type TOrderCore = {
id?: number | null;
createDate?: Date | null;
updateDate?: Date | null;
isEnabled?: boolean | null;
status?: TOrderStatus | null;
cart?: string | TStoreListItem[] | null;
orderTotalPrice?: number | null;
Expand Down Expand Up @@ -776,6 +777,11 @@ export type TCmsAdminSettings = {
* Role names available for sign-up public API.
*/
signupRoles?: string[];

/**
* Show unapproved product reviews. Hide by default
*/
showUnapprovedReviews?: boolean;
}

/**
Expand Down
22 changes: 13 additions & 9 deletions system/server/src/dto/admin-cms-settings.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,19 @@ export class AdminCmsSettingsDto extends CmsSettingsDto {
@ApiProperty()
signupRoles?: string[];

parseSettings(settings: TCmsSettings) {
super.parseSettings(settings);

this.smtpConnectionString = settings.smtpConnectionString;
this.sendFromEmail = settings.sendFromEmail;
this.customFields = settings.customFields;
this.customEntities = settings.customEntities;
this.signupEnabled = settings.signupEnabled;
this.signupRoles = settings.signupRoles;
@ApiProperty()
showUnapprovedReviews?: boolean;

parseSettings(config: TCmsSettings) {
super.parseSettings(config);

this.smtpConnectionString = config.smtpConnectionString;
this.sendFromEmail = config.sendFromEmail;
this.customFields = config.customFields;
this.customEntities = config.customEntities;
this.signupEnabled = config.signupEnabled;
this.signupRoles = config.signupRoles;
this.showUnapprovedReviews = config.showUnapprovedReviews;
return this;
}
}
Expand Down
42 changes: 38 additions & 4 deletions system/server/src/helpers/data-filters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { TPagedList, TPagedParams, TPermissionName } from '@cromwell/core';
import { applyDataFilters, DeleteManyInput, TFilterableEntities, TGraphQLContext } from '@cromwell/core-backend';
import { matchPermissions, TBaseFilter, TPagedList, TPagedParams, TPermissionName } from '@cromwell/core';
import {
applyDataFilters,
CustomEntityRepository,
DeleteManyInput,
TFilterableEntities,
TGraphQLContext,
} from '@cromwell/core-backend';
import { HttpException, HttpStatus } from '@nestjs/common';
import { getCustomRepository } from 'typeorm';

import { resetAllPagesCache } from './reset-page';

Expand All @@ -8,6 +16,7 @@ export const getByIdWithFilters = async <TEntityKey extends keyof TFilterableEnt
entity: TEntityKey,
ctx: TGraphQLContext,
permissions: TPermissionName[],
getDisabledPermissions: TPermissionName[],
id: number,
getById: (id: number) => Promise<TFilterableEntities[TEntityKey]['entity']>
): Promise<TFilterableEntities[TEntityKey]['entity']> => {
Expand All @@ -16,18 +25,23 @@ export const getByIdWithFilters = async <TEntityKey extends keyof TFilterableEnt
user: ctx?.user,
permissions,
})).id;
return (await applyDataFilters(entity, 'getOneByIdOutput', {
const data = (await applyDataFilters(entity, 'getOneByIdOutput', {
id,
data: await getById(id),
user: ctx?.user,
permissions,
})).data;
if (!matchPermissions(ctx.user, getDisabledPermissions) && data.isEnabled === false) {
throw new HttpException(`${entity} ${id} not found!`, HttpStatus.NOT_FOUND);
}
return data;
}

export const getBySlugWithFilters = async <TEntityKey extends keyof TFilterableEntities>(
entity: TEntityKey,
ctx: TGraphQLContext,
permissions: TPermissionName[],
getDisabledPermissions: TPermissionName[],
slug: string,
getBySlug: (slug: string) => Promise<TFilterableEntities[TEntityKey]['entity']>
): Promise<TFilterableEntities[TEntityKey]['entity']> => {
Expand All @@ -36,24 +50,33 @@ export const getBySlugWithFilters = async <TEntityKey extends keyof TFilterableE
user: ctx?.user,
permissions,
})).slug;
return (await applyDataFilters(entity, 'getOneBySlugOutput', {
const data = (await applyDataFilters(entity, 'getOneBySlugOutput', {
slug,
data: await getBySlug(slug),
user: ctx?.user,
permissions,
})).data;
if (!matchPermissions(ctx.user, getDisabledPermissions) && data.isEnabled === false) {
throw new HttpException(`${entity} ${slug} not found!`, HttpStatus.NOT_FOUND);
}
return data;
}

export const getManyWithFilters = async <TEntityKey extends keyof TFilterableEntities>(
entity: TEntityKey,
ctx: TGraphQLContext,
permissions: TPermissionName[],
getDisabledPermissions: TPermissionName[],
params: TPagedParams<TFilterableEntities[TEntityKey]['entity']> | undefined,
filter: TFilterableEntities[TEntityKey]['filter'] | undefined,
getMany: (params?: TPagedParams<TFilterableEntities[TEntityKey]['entity']>,
filter?: TFilterableEntities[TEntityKey]['filter']) =>
Promise<TPagedList<TFilterableEntities[TEntityKey]['entity']>>
): Promise<TPagedList<TFilterableEntities[TEntityKey]['entity']>> => {
if (!matchPermissions(ctx.user, getDisabledPermissions)) {
filter = setupFilterForEnabledOnly(filter);
}

const result = (await applyDataFilters(entity, 'getManyInput', {
params,
filter,
Expand Down Expand Up @@ -161,3 +184,14 @@ export const deleteManyWithFilters = async <TEntityKey extends keyof TFilterable
permissions,
}).then(res => { resetAllPagesCache(); return res; })).success;
}

export const setupFilterForEnabledOnly = <T extends TBaseFilter>(filter?: T): T => {
if (!filter) (filter as any) = {};
if (!filter!.filters) filter!.filters = [];
filter!.filters.push({
key: 'isEnabled',
value: getCustomRepository(CustomEntityRepository).getSqlBoolStr(true),
exact: true,
});
return filter!;
}
4 changes: 2 additions & 2 deletions system/server/src/resolvers/attribute.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class AttributeResolver {
@Ctx() ctx: TGraphQLContext,
@Arg("id", () => Int) id: number,
): Promise<TAttribute> {
return getByIdWithFilters('Attribute', ctx, [], id,
return getByIdWithFilters('Attribute', ctx, [], ['read_attributes'], id,
(...args) => this.repository.getAttribute(...args));
}

Expand All @@ -50,7 +50,7 @@ export class AttributeResolver {
@Arg("pagedParams", { nullable: true }) pagedParams?: PagedParamsInput<TAttribute>,
@Arg("filterParams", () => BaseFilterInput, { nullable: true }) filterParams?: BaseFilterInput,
): Promise<TPagedList<TAttribute> | undefined> {
return getManyWithFilters('Attribute', ctx, [], pagedParams, filterParams,
return getManyWithFilters('Attribute', ctx, [], ['read_attributes'], pagedParams, filterParams,
(...args) => this.repository.getFilteredAttributes(...args));
}

Expand Down
4 changes: 2 additions & 2 deletions system/server/src/resolvers/coupon.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class CouponResolver {
@Ctx() ctx: TGraphQLContext,
@Arg("id", () => Int) id: number,
): Promise<TCoupon | undefined> {
return getByIdWithFilters('Coupon', ctx, ['read_coupons'], id,
return getByIdWithFilters('Coupon', ctx, ['read_coupons'], ['read_coupons'], id,
(...args) => this.repository.getCouponById(...args));
}

Expand All @@ -54,7 +54,7 @@ export class CouponResolver {
@Arg("pagedParams", { nullable: true }) pagedParams?: PagedParamsInput<TCoupon>,
@Arg("filterParams", () => BaseFilterInput, { nullable: true }) filterParams?: BaseFilterInput,
): Promise<TPagedList<TCoupon> | undefined> {
return getManyWithFilters('Coupon', ctx, ['read_coupons'], pagedParams, filterParams,
return getManyWithFilters('Coupon', ctx, ['read_coupons'], ['read_coupons'], pagedParams, filterParams,
(...args) => this.repository.getFilteredCoupons(...args));
}

Expand Down
9 changes: 6 additions & 3 deletions system/server/src/resolvers/custom-entity.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export class CustomEntityResolver {
@Arg("id", () => Int) id: number
): Promise<TCustomEntity | undefined> {
const permissions = await this.checkPermissions(entityType, 'read', ctx);
return getByIdWithFilters('CustomEntity', ctx, permissions, id,
const getAllPermissions = [...new Set([...permissions, 'read_custom_entities'])] as TPermissionName[];
return getByIdWithFilters('CustomEntity', ctx, permissions, getAllPermissions, id,
(...args) => this.repository.getCustomEntityById(...args));
}

Expand All @@ -122,7 +123,8 @@ export class CustomEntityResolver {
@Arg("slug") slug: string
): Promise<TCustomEntity | undefined> {
const permissions = await this.checkPermissions(entityType, 'read', ctx);
return getBySlugWithFilters('CustomEntity', ctx, permissions, slug,
const getAllPermissions = [...new Set([...permissions, 'read_custom_entities'])] as TPermissionName[];
return getBySlugWithFilters('CustomEntity', ctx, permissions, getAllPermissions, slug,
(...args) => this.repository.getCustomEntityBySlug(...args));
}

Expand All @@ -133,7 +135,8 @@ export class CustomEntityResolver {
@Arg("filterParams", { nullable: true }) filterParams?: CustomEntityFilterInput,
): Promise<TPagedList<TCustomEntity> | undefined> {
const permissions = await this.checkPermissions(filterParams?.entityType, 'read', ctx);
return getManyWithFilters('CustomEntity', ctx, permissions, pagedParams, filterParams,
const getAllPermissions = [...new Set([...permissions, 'read_custom_entities'])] as TPermissionName[];
return getManyWithFilters('CustomEntity', ctx, permissions, getAllPermissions, pagedParams, filterParams,
(...args) => this.repository.getFilteredCustomEntities(...args));
}

Expand Down
8 changes: 4 additions & 4 deletions system/server/src/resolvers/order.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class OrderResolver {
@Arg("id", () => Int) id: number,
): Promise<TOrder> {
if (!ctx?.user?.roles?.length) throw new HttpException('Access denied.', HttpStatus.UNAUTHORIZED);
const order = await getByIdWithFilters('Order', ctx, ['read_orders', 'read_my_orders'], id,
const order = await getByIdWithFilters('Order', ctx, ['read_orders', 'read_my_orders'], ['read_orders'], id,
(...args) => this.repository.getOrderById(...args));

this.checkUserAccess(ctx, order?.userId);
Expand All @@ -75,7 +75,7 @@ export class OrderResolver {
@Ctx() ctx: TGraphQLContext,
@Arg("slug") slug: string
): Promise<TOrder | undefined> {
return getBySlugWithFilters('Order', ctx, ['read_orders'], slug,
return getBySlugWithFilters('Order', ctx, ['read_orders'], ['read_orders'], slug,
(...args) => this.repository.getOrderBySlug(...args));
}

Expand All @@ -86,7 +86,7 @@ export class OrderResolver {
@Arg("pagedParams", { nullable: true }) pagedParams?: PagedParamsInput<TOrder>,
@Arg("filterParams", { nullable: true }) filterParams?: OrderFilterInput,
): Promise<TPagedList<TOrder> | undefined> {
return getManyWithFilters('Order', ctx, ['read_orders'], pagedParams, filterParams,
return getManyWithFilters('Order', ctx, ['read_orders'], ['read_orders'], pagedParams, filterParams,
(...args) => this.repository.getFilteredOrders(...args));
}

Expand All @@ -98,7 +98,7 @@ export class OrderResolver {
@Arg("pagedParams", { nullable: true }) pagedParams?: PagedParamsInput<TOrder>
): Promise<TPagedList<TOrder> | undefined> {
this.checkUserAccess(ctx, userId);
return getManyWithFilters('Order', ctx, ['read_orders', 'read_my_orders'], pagedParams, { userId },
return getManyWithFilters('Order', ctx, ['read_orders', 'read_my_orders'], ['read_orders'], pagedParams, { userId },
(...args) => this.repository.getFilteredOrders(...args));
}

Expand Down
46 changes: 30 additions & 16 deletions system/server/src/resolvers/post.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class PostResolver {
@Ctx() ctx: TGraphQLContext,
@Arg("id", () => Int) id: number,
): Promise<TPost> {
return getByIdWithFilters('Post', ctx, [], id,
return getByIdWithFilters('Post', ctx, [], ['read_posts'], id,
async (id) => {
const post = this.filterDrafts([await this.repository.getPostById(id)], ctx)[0];
if (!post) throw new HttpException(`Post ${id} not found!`, HttpStatus.NOT_FOUND);
Expand All @@ -87,7 +87,7 @@ export class PostResolver {
@Ctx() ctx: TGraphQLContext,
@Arg("slug") slug: string,
): Promise<TPost | undefined> {
return getBySlugWithFilters('Post', ctx, [], slug,
return getBySlugWithFilters('Post', ctx, [], ['read_posts'], slug,
async (slug) => {
const post = this.filterDrafts([await this.repository.getPostBySlug(slug)], ctx)[0];
if (!post) throw new HttpException(`Post ${slug} not found!`, HttpStatus.NOT_FOUND);
Expand All @@ -101,15 +101,13 @@ export class PostResolver {
@Arg("pagedParams", { nullable: true }) pagedParams?: PagedParamsInput<TPost>,
@Arg("filterParams", { nullable: true }) filterParams?: PostFilterInput,
): Promise<TPagedList<TPost> | undefined> {
return getManyWithFilters('Post', ctx, [], pagedParams, filterParams,
(pagedParams, filterParams) => {
if (!this.canGetDraft(ctx)) {
// No auth, return only published posts
if (!filterParams) filterParams = {};
filterParams.published === true;
}
return this.repository.getFilteredPosts(pagedParams, filterParams);
});
if (!this.canGetDraft(ctx)) {
// No auth, return only published posts
if (!filterParams) filterParams = {};
filterParams.published === true;
}
return getManyWithFilters('Post', ctx, [], ['read_posts'], pagedParams, filterParams,
(...args) => this.repository.getFilteredPosts(...args));
}

@Authorized<TPermissionName>('create_post')
Expand Down Expand Up @@ -155,18 +153,34 @@ export class PostResolver {
}

@FieldResolver(() => User, { nullable: true })
async [authorKey](@Root() post: Post): Promise<TUser | undefined> {
async [authorKey](
@Ctx() ctx: TGraphQLContext,
@Root() post: Post
): Promise<TUser | undefined> {
try {
if (post.authorId)
return await this.userRepository.getUserById(post.authorId);
if (post.authorId) {
const user = await this.userRepository.getUserById(post.authorId);
if (user.isEnabled === false && !matchPermissions(ctx.user, ['read_users'])) {
return;
}
return user;
}
} catch (e) {
logger.error(e);
}
}

@FieldResolver(() => [Tag], { nullable: true })
async [tagsKey](@Root() post: Post): Promise<TTag[] | undefined | null> {
return this.repository.getTagsOfPost(post.id);
async [tagsKey](
@Ctx() ctx: TGraphQLContext,
@Root() post: Post
): Promise<TTag[] | undefined | null> {
const tags = await this.repository.getTagsOfPost(post.id);

if (!matchPermissions(ctx.user, ['read_tags'])) {
return tags?.filter(tag => tag.isEnabled !== false);
}
return tags;
}

@FieldResolver(() => GraphQLJSONObject, { nullable: true })
Expand Down

0 comments on commit 105b6bb

Please sign in to comment.