Skip to content

Commit

Permalink
Merge pull request #1411 from numbersprotocol/fix-prevent-user-from-l…
Browse files Browse the repository at this point in the history
…isting-same-asset-to-CC

fix: prevent user from performing List in CaptureClub with the same a…
  • Loading branch information
shc261392 authored Mar 25, 2022
2 parents 85c7090 + 41dde45 commit caa98d3
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 27 deletions.
84 changes: 78 additions & 6 deletions src/app/features/home/details/actions/actions.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute } from '@angular/router';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { combineLatest, of } from 'rxjs';
import { catchError, concatMap, first, map, tap } from 'rxjs/operators';
import { combineLatest, forkJoin, of } from 'rxjs';
import { catchError, concatMap, first, map, take, tap } from 'rxjs/operators';
import { ActionsDialogComponent } from '../../../../shared/actions/actions-dialog/actions-dialog.component';
import {
Action,
Expand All @@ -14,13 +14,17 @@ import {
import { OrderHistoryService } from '../../../../shared/actions/service/order-history.service';
import { BlockingActionService } from '../../../../shared/blocking-action/blocking-action.service';
import { DiaBackendAuthService } from '../../../../shared/dia-backend/auth/dia-backend-auth.service';
import { DiaBackendSeriesRepository } from '../../../../shared/dia-backend/series/dia-backend-series-repository.service';
import {
DiaBackendStoreService,
NetworkAppOrder,
} from '../../../../shared/dia-backend/store/dia-backend-store.service';
import { ErrorService } from '../../../../shared/error/error.service';
import { OrderDetailDialogComponent } from '../../../../shared/order-detail-dialog/order-detail-dialog.component';
import { isNonNullable } from '../../../../utils/rx-operators/rx-operators';
import {
isNonNullable,
VOID$,
} from '../../../../utils/rx-operators/rx-operators';

@UntilDestroy()
@Component({
Expand All @@ -34,7 +38,8 @@ export class ActionsPage {
.pipe(catchError((err: unknown) => this.errorService.toastError$(err)));

private readonly id$ = this.route.paramMap.pipe(
map(params => params.get('id'))
map(params => params.get('id')),
isNonNullable()
);

constructor(
Expand All @@ -47,9 +52,71 @@ export class ActionsPage {
private readonly snackBar: MatSnackBar,
private readonly dialog: MatDialog,
private readonly storeService: DiaBackendStoreService,
private readonly orderHistoryService: OrderHistoryService
private readonly orderHistoryService: OrderHistoryService,
private readonly diaBackendStoreService: DiaBackendStoreService,
private readonly diaBackendSeriesRepository: DiaBackendSeriesRepository
) {}

canPerformAction$(action: Action) {
if (action.title_text === 'List in CaptureClub') {
/*
Workaround:
Currently there isn't a simple way to check whether an asset is listed in
CaptureClub or not. So I first query List all Products API with
associated_id parameter set to the assets cid. And then use list series
API and check through all nested collections. See discussion here
https://app.asana.com/0/0/1201558520076805/1201995911008176/f
*/
return this.id$.pipe(
concatMap(cid =>
forkJoin([
this.diaBackendStoreService.listAllProducts$({
associated_id: cid,
service_name: 'CaptureClub',
}),
of(cid),
])
),
concatMap(([response, cid]) => {
if (response.count > 0) {
throw new Error(
this.translocoService.translate('message.hasListedInCaptureClub')
);
}
return of(cid);
}),
concatMap(async cid => {
let currentOffset = 0;
const limit = 100;
while (true) {
const response = await this.diaBackendSeriesRepository
.fetchAll$({ offset: currentOffset, limit })
.toPromise();
const listedAsSeries = response.results.some(serie =>
serie.collections.some(collection =>
collection.assets.some(asset => asset.cid === cid)
)
);
if (listedAsSeries) {
throw new Error(
this.translocoService.translate(
'message.hasListedInCaptureClub'
)
);
}
if (response.next == null) {
break;
}
currentOffset += response.results.length;
}
return VOID$;
}),
take(1)
);
}
return VOID$;
}

openActionDialog$(action: Action) {
return combineLatest([
this.actionsService.getParams$(action.params_list_custom_param1),
Expand Down Expand Up @@ -128,8 +195,13 @@ export class ActionsPage {
}

doAction(action: Action) {
this.openActionDialog$(action)
this.blockingActionService
.run$(this.canPerformAction$(action))
.pipe(
catchError((err: unknown) => {
return this.errorService.toastError$(err);
}),
concatMap(() => this.openActionDialog$(action)),
concatMap(createOrderInput =>
this.blockingActionService.run$(
this.createOrder$(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { BASE_URL } from '../secret';
providedIn: 'root',
})
export class DiaBackendSeriesRepository {
private readonly fetchCollectedSeriesCount$ = this.fetch$({
private readonly fetchCollectedSeriesCount$ = this.fetchAll$({
limit: 1,
collected: true,
}).pipe(pluck('count'));

readonly fetchCollectedSeriesList$ = this.fetchCollectedSeriesCount$.pipe(
switchMap(count => this.fetch$({ limit: count, collected: true }))
switchMap(count => this.fetchAll$({ limit: count, collected: true }))
);

constructor(
Expand All @@ -28,23 +28,16 @@ export class DiaBackendSeriesRepository {
return this.read$(id);
}

private fetch$({
limit,
collected,
}: {
limit?: number;
collected?: boolean;
}) {
fetchAll$(
queryParams: {
limit?: number;
collected?: boolean;
offset?: number;
} = {}
) {
return defer(() => this.authService.getAuthHeaders()).pipe(
concatMap(headers => {
let params = new HttpParams();

if (limit !== undefined) {
params = params.set('limit', `${limit}`);
}
if (collected !== undefined) {
params = params.set('collected', `${collected}`);
}
const params = new HttpParams({ fromObject: queryParams });
return this.httpClient.get<PaginatedResponse<DiaBackendSeries>>(
`${BASE_URL}/api/v3/series/`,
{ headers, params }
Expand All @@ -70,6 +63,7 @@ export interface DiaBackendSeries {
readonly collections: {
readonly assets: {
readonly id: string;
readonly cid: string;
readonly asset_file_thumbnail: string;
readonly collected: boolean;
}[];
Expand Down
48 changes: 46 additions & 2 deletions src/app/shared/dia-backend/store/dia-backend-store.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { defer } from 'rxjs';
import { concatMap } from 'rxjs/operators';
import { DiaBackendAuthService } from '../auth/dia-backend-auth.service';
import { PaginatedResponse } from '../pagination';
import { BASE_URL } from '../secret';

@Injectable({
Expand Down Expand Up @@ -41,11 +42,25 @@ export class DiaBackendStoreService {
})
);
}

listAllProducts$(
queryParams: { associated_id?: string; service_name?: string } = {}
) {
return defer(() => this.authService.getAuthHeadersWithApiKey()).pipe(
concatMap(headers => {
const params = new HttpParams({ fromObject: queryParams });
return this.httpClient.get<PaginatedResponse<Product>>(
`${BASE_URL}/api/v3/store/products/`,
{ headers, params }
);
})
);
}
}

export interface NetworkAppOrder {
id: string;
status: 'completed' | 'canceled' | 'pending';
status: 'created' | 'success' | 'failure' | 'pending';
network_app_id: string;
network_app_name: string;
action: string;
Expand All @@ -63,3 +78,32 @@ export interface NetworkAppOrder {
created_at: string;
expired_at: string;
}

export interface Product {
id: string;
associated_id: string;
type: string;
service_name: string;
subject: string;
description: string;
tags: string[];
cover_image: string;
cover_image_thumbnail: string;
product_url: string;
external_product_url: string;
price: string;
price_base: 'usd' | 'num';
quantity: number;
in_stock: number;
nft_token_id: number;
nft_token_uri: string;
nft_blockchain_name: string;
nft_contract_address: string;
owner: string;
owner_name: string;
original_creator: string;
original_creator_name: string;
enabled: boolean;
created_at: string;
updated_at: string;
}
3 changes: 2 additions & 1 deletion src/assets/i18n/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@
"mintNftToken": "Mint NFT token",
"mintNftAlert": "Once the NFT is minted, photo and its information can be accessed publicly. Do you still want to proceed?",
"sentSuccessfully": "Sent Successfully",
"confirmCopyPrivateKey": "Warning: Please do not expose your private key to anyone. Lost of private key may cause your account to be lost permanently."
"confirmCopyPrivateKey": "Warning: Please do not expose your private key to anyone. Lost of private key may cause your account to be lost permanently.",
"hasListedInCaptureClub": "This asset has already been listed in CaptureClub."
},
"error": {
"validationError": "Invalid operation. Please check your input again.",
Expand Down
3 changes: 2 additions & 1 deletion src/assets/i18n/zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@
"mintNftToken": "鑄造 NFT 代幣",
"mintNftAlert": "NFT 代幣鑄造後,影像以及所有資訊都將可被公開檢視。確定要繼續嗎?",
"sentSuccessfully": "成功送出",
"confirmCopyPrivateKey": "警告:請不要向任何人揭露您的金鑰,未保管好金鑰可能導致您永遠失去您的帳號。"
"confirmCopyPrivateKey": "警告:請不要向任何人揭露您的金鑰,未保管好金鑰可能導致您永遠失去您的帳號。",
"hasListedInCaptureClub": "該資產已在 CaptureClub 上架。"
},
"error": {
"validationError": "操作或輸入有誤,請確認您的操作正確。",
Expand Down

0 comments on commit caa98d3

Please sign in to comment.