Merge branch 'bvander/integration-progress' into 'master'

integration progress

See merge request grassrootseconomics/cic-staff-client!7
This commit is contained in:
Blair Vanderlugt 2021-03-29 03:25:37 +00:00
commit f6330c95aa
44 changed files with 361 additions and 21035 deletions

View File

@ -1,5 +1,6 @@
# Logging levels => TRACE = 0|DEBUG = 1|INFO = 2|LOG = 3|WARN = 4|ERROR = 5|FATAL = 6|OFF = 7
LOG_LEVEL=
SERVER_LEVEL=
SERVER_LOG_LEVEL=
CIC_CHAIN_ID=
CIC_LOGGING_URL=
CIC_META_URL=

20786
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,8 +13,8 @@ const environmentVars = `import {NgxLoggerLevel} from 'ngx-logger';
export const environment = {
production: ${isProduction},
bloxbergChainId: ${process.env.CIC_CHAIN_ID || 8996},
level: ${process.env.LOG_LEVEL || 'NgxLoggerLevel.OFF'},
serverLogLevel: ${process.env.SERVER_LEVEL || 'NgxLoggerLevel.OFF'},
logLevel: ${process.env.LOG_LEVEL || 'NgxLoggerLevel.ERROR'},
serverLogLevel: ${process.env.SERVER_LOG_LEVEL || 'NgxLoggerLevel.OFF'},
loggingUrl: '${process.env.CIC_LOGGING_URL || 'http://localhost:8000'}',
cicMetaUrl: '${process.env.CIC_META_URL || 'https://meta.dev.grassrootseconomics.net'}',
publicKeysUrl: '${process.env.CIC_KEYS_URL || 'http://localhost:8000/keys.asc'}',

View File

@ -0,0 +1,37 @@
function exportCsv(arrayData: any[], filename: string, delimiter = ','): void {
if (arrayData === undefined) { return; }
const header = Object.keys(arrayData[0]).join(delimiter) + '\n';
let csv = header;
arrayData.forEach(obj => {
let row = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
row.push(obj[key]);
}
}
csv += row.join(delimiter) + '\n';
});
const csvData = new Blob([csv], {type: 'text/csv'});
const csvUrl = URL.createObjectURL(csvData);
// csvUrl = 'data:text/csv;charset=utf-8,' + encodeURI(csv);
let downloadLink = document.createElement('a');
downloadLink.href = csvUrl;
downloadLink.target = '_blank';
downloadLink.download = filename + '.csv';
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
}
function removeSpecialChar(str: string): string {
if (str === null || str === '') {
return '';
}
return str.replace(/[^a-zA-Z0-9 ]/g, '');
}
export {
exportCsv
};

View File

@ -1,12 +1,16 @@
import {ErrorHandler, Injectable} from '@angular/core';
import {LoggingService} from '@app/_services/logging.service';
import {HttpErrorResponse} from '@angular/common/http';
import {Router} from '@angular/router';
@Injectable()
export class GlobalErrorHandler extends ErrorHandler {
private sentencesForWarningLogging: string[] = [];
constructor(private loggingService: LoggingService) {
constructor(
private loggingService: LoggingService,
private router: Router
) {
super();
}
@ -31,15 +35,17 @@ export class GlobalErrorHandler extends ErrorHandler {
}
logError(error: any): void {
const route = this.router.url;
if (error instanceof HttpErrorResponse) {
this.loggingService.sendErrorLevelMessage(
`There was an HTTP error. ${error.message}, Status code: ${(error as HttpErrorResponse).status}`, this, {error});
`There was an HTTP error on route ${route}.\n${error.message}.\nStatus code: ${(error as HttpErrorResponse).status}`,
this, {error});
} else if (error instanceof TypeError) {
this.loggingService.sendErrorLevelMessage(`There was a Type error. ${error.message}`, this, {error});
this.loggingService.sendErrorLevelMessage(`There was a Type error on route ${route}.\n${error.message}`, this, {error});
} else if (error instanceof Error) {
this.loggingService.sendErrorLevelMessage(`There was a general error. ${error.message}`, this, {error});
this.loggingService.sendErrorLevelMessage(`There was a general error on route ${route}.\n${error.message}`, this, {error});
} else {
this.loggingService.sendErrorLevelMessage('Nobody threw an error but something happened!', this, {error});
this.loggingService.sendErrorLevelMessage(`Nobody threw an error but something happened on route ${route}!`, this, {error});
}
}

View File

@ -4,3 +4,5 @@ export * from '@app/_helpers/mock-backend';
export * from '@app/_helpers/array-sum';
export * from '@app/_helpers/http-getter';
export * from '@app/_helpers/global-error-handler';
export * from '@app/_helpers/export-csv';
export * from '@app/_helpers/read-csv';

View File

@ -0,0 +1,31 @@
let objCsv = {
size: 0,
dataFile: []
};
function readCsv(input: any): any {
if (input.files && input.files[0]) {
let reader = new FileReader();
reader.readAsBinaryString(input.files[0]);
reader.onload = event => {
objCsv.size = event.total;
// @ts-ignore
objCsv.dataFile = event.target.result;
return parseData(objCsv.dataFile);
};
}
}
function parseData(data: any): any {
let csvData = [];
const lineBreak = data.split('\n');
lineBreak.forEach(res => {
csvData.push(res.split(','));
});
console.table(csvData);
return csvData;
}
export {
readCsv
};

View File

@ -6,26 +6,43 @@ import {
HttpInterceptor, HttpErrorResponse
} from '@angular/common/http';
import {Observable, throwError} from 'rxjs';
import {catchError} from 'rxjs/operators';
import {ErrorDialogService} from '@app/_services';
import {catchError, retry} from 'rxjs/operators';
import {ErrorDialogService, LoggingService} from '@app/_services';
import {Router} from '@angular/router';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private errorDialogService: ErrorDialogService) {}
constructor(
private errorDialogService: ErrorDialogService,
private loggingService: LoggingService,
private router: Router
) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(catchError((err: HttpErrorResponse) => {
if (isDevMode()) {
this.errorDialogService.openDialog({
message: err.error.message || err.statusText || 'Unknown Error',
status: err.status || 0
});
}
if ([401, 403].indexOf(err.status) !== -1) {
location.reload(true);
}
return throwError(err);
}));
return next.handle(request).pipe(
catchError((err: HttpErrorResponse) => {
let errorMessage;
if (err.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
errorMessage = `An error occurred: ${err.error.message}`;
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
errorMessage = `Backend returned code ${err.status}, body was: ${JSON.stringify(err.error)}`;
}
this.loggingService.sendErrorLevelMessage(errorMessage, this, {error: err});
switch (err.status) {
case 401: // unauthorized
this.router.navigateByUrl('/auth').then();
break;
case 403: // forbidden
location.reload(true);
break;
}
// Return an observable with a user-facing error message.
return throwError(err);
})
);
}
}

View File

@ -19,11 +19,6 @@ export class HttpConfigInterceptor implements HttpInterceptor {
request = request.clone({headers: request.headers.set('Authorization', 'Bearer ' + token)});
}
if (!request.headers.has('Content-Type')) {
request = request.clone({headers: request.headers.set('Content-Type', 'application/json')});
}
request = request.clone({headers: request.headers.set('Accept', 'application/json')});
return next.handle(request);
}
}

View File

@ -78,10 +78,10 @@ export const defaultAccount: AccountDetails = {
value: '',
}],
fn: [{
value: 'GE',
value: 'Sarafu Contract',
}],
n: [{
value: ['GE'],
value: ['Sarafu', 'Contract'],
}],
tel: [{
meta: {

View File

@ -1,11 +1,12 @@
import { KeyStore } from 'cic-client-meta';
const openpgp = require('openpgp');
// TODO should we put this on the mutalble key store object
import * as openpgp from 'openpgp';
const keyring = new openpgp.Keyring();
interface MutableKeyStore extends KeyStore {
loadKeyring(): Promise<void>;
loadKeyring(): void;
importKeyPair(publicKey: any, privateKey: any): Promise<void>;
importPublicKey(publicKey: any): Promise<void>;
importPublicKey(publicKey: any): void;
importPrivateKey(privateKey: any): Promise<void>;
getPublicKeys(): Array<any>;
getTrustedKeys(): Array<any>;
@ -13,7 +14,8 @@ interface MutableKeyStore extends KeyStore {
getEncryptKeys(): Array<any>;
getPrivateKeys(): Array<any>;
getPrivateKey(): any;
isValidKey(key: any): boolean;
isValidKey(key: any): Promise<boolean>;
isEncryptedPrivateKey(privateKey: any): Promise<boolean>;
getFingerprint(): string;
getKeyId(key: any): string;
getPrivateKeyId(): string;
@ -33,8 +35,6 @@ class MutablePgpKeyStore implements MutableKeyStore{
async loadKeyring(): Promise<void> {
await keyring.load();
// clear any keys already in the keychain
// keyring.clear();
await keyring.store();
}
@ -43,8 +43,8 @@ class MutablePgpKeyStore implements MutableKeyStore{
await keyring.privateKeys.importKey(privateKey);
}
async importPublicKey(publicKey: any): Promise<void> {
await keyring.publicKeys.importKey(publicKey);
importPublicKey(publicKey: any): void {
keyring.publicKeys.importKey(publicKey);
}
async importPrivateKey(privateKey: any): Promise<void> {
@ -72,15 +72,30 @@ class MutablePgpKeyStore implements MutableKeyStore{
}
getPrivateKey(): any {
return keyring.privateKeys.keys[0];
return keyring.privateKeys && keyring.privateKeys.keys[0];
}
isValidKey(key): boolean {
return typeof key === openpgp.Key;
async isValidKey(key): Promise<boolean> {
// There is supposed to be an opengpg.readKey() method but I can't find it?
const _key = await openpgp.key.readArmored(key);
return !_key.err;
}
async isEncryptedPrivateKey(privateKey: any): Promise<boolean> {
const imported = await openpgp.key.readArmored(privateKey);
for (let i = 0; i < imported.keys.length; i++) {
const key = imported.keys[i];
if (key.isDecrypted()) {
return false;
}
}
return true;
}
getFingerprint(): string {
return keyring.privateKeys.keys[0].keyPacket.fingerprint;
// TODO Handle multiple keys
return keyring.privateKeys && keyring.privateKeys.keys[0] && keyring.privateKeys.keys[0].keyPacket &&
keyring.privateKeys.keys[0].keyPacket.fingerprint;
}
getKeyId(key: any): string {
@ -88,7 +103,8 @@ class MutablePgpKeyStore implements MutableKeyStore{
}
getPrivateKeyId(): string {
return keyring.privateKeys.keys[0].getKeyId().toHex();
// TODO is there a library that comes with angular for doing this?
return keyring.privateKeys && keyring.privateKeys.keys[0] && keyring.privateKeys.keys[0].getKeyId().toHex();
}
getKeysForId(keyId: string): Array<any> {

View File

@ -3,10 +3,10 @@ import { hobaParseChallengeHeader } from '@src/assets/js/hoba.js';
import { signChallenge } from '@src/assets/js/hoba-pgp.js';
import {environment} from '@src/environments/environment';
import {LoggingService} from '@app/_services/logging.service';
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
import {MutableKeyStore, MutablePgpKeyStore} from '@app/_pgp';
import {ErrorDialogService} from '@app/_services/error-dialog.service';
import {first} from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import {Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
@ -18,10 +18,11 @@ export class AuthService {
mutableKeyStore: MutableKeyStore = new MutablePgpKeyStore();
constructor(
private httpWrapperService: HttpWrapperService,
private httpClient: HttpClient,
private loggingService: LoggingService,
private errorDialogService: ErrorDialogService
) {
// TODO setting these together shoulds be atomic
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
}
@ -46,7 +47,7 @@ export class AuthService {
throw new Error('login rejected');
}
this.sessionLoginCount++;
this.setState('Click button to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken);
this.setState('Click button to log in');
return;
});
xhr.send();
@ -66,7 +67,7 @@ export class AuthService {
this.sessionToken = xhr.getResponseHeader('Token');
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
this.sessionLoginCount++;
this.setState('Click button to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken);
this.setState('Click button to log in');
return;
});
xhr.send();
@ -107,8 +108,12 @@ export class AuthService {
async loginResponse(o): Promise<any> {
const r = await signChallenge(o.challenge, o.realm, environment.cicMetaUrl, this.mutableKeyStore);
this.sendResponse(r);
try {
const r = await signChallenge(o.challenge, o.realm, environment.cicMetaUrl, this.mutableKeyStore);
this.sendResponse(r);
} catch (error) {
this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'});
}
}
loginView(): void {
@ -119,13 +124,20 @@ export class AuthService {
async setKey(privateKeyArmored): Promise<boolean> {
try {
await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
const isValidKeyCheck = await this.mutableKeyStore.isValidKey(privateKeyArmored);
if (!isValidKeyCheck) {
throw Error('The private key is invalid');
}
const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored);
if (!isEncryptedKeyCheck) {
throw Error('The private key doesn\'t have a password!');
}
const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);
} catch (err) {
this.loggingService.sendErrorLevelMessage('Failed setting key', this, {error: err});
this.loggingService.sendErrorLevelMessage(`Failed to set key: ${err.message || err.statusText}`, this, {error: err});
this.errorDialogService.openDialog({
message: `Failed to set key, Enter your private key again. Reason: ${err.error.message || err.statusText}`,
status: err.status
message: `Failed to set key: ${err.message || err.statusText}`,
});
return false;
}
@ -135,6 +147,7 @@ export class AuthService {
logout(): void {
sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN'));
this.sessionToken = undefined;
window.location.reload(true);
}
@ -144,12 +157,11 @@ export class AuthService {
return trustedUsers;
}
async getPublicKeys(): Promise<void> {
this.httpWrapperService.get(`${environment.publicKeysUrl}`).pipe(first()).subscribe(async res => {
await this.mutableKeyStore.importPublicKey(res.body);
}, error => {
this.loggingService.sendErrorLevelMessage('There was an error fetching public keys!', this, {error});
});
getPublicKeys(): Observable<any> {
return this.httpClient.get(`${environment.publicKeysUrl}`, {responseType: 'text'});
}
async getPrivateKeys(): Promise<void> {
if (this.privateKey !== undefined) {
await this.mutableKeyStore.importPrivateKey(this.privateKey);
}

View File

@ -59,11 +59,11 @@ export class BlockSyncService {
});
if (address === null) {
this.transactionService.getAllTransactions(offset, limit).pipe(first()).subscribe(res => {
this.fetcher(settings, res.body);
this.fetcher(settings, res);
});
} else {
this.transactionService.getAddressTransactions(address, offset, limit).pipe(first()).subscribe(res => {
this.fetcher(settings, res.body);
this.fetcher(settings, res);
});
}
}

View File

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {ErrorDialogComponent} from '@app/shared/error-dialog/error-dialog.component';
import {LoggingService} from '@app/_services/logging.service';
@Injectable({
providedIn: 'root'
@ -11,7 +10,6 @@ export class ErrorDialogService {
constructor(
public dialog: MatDialog,
private loggingService: LoggingService
) { }
openDialog(data): any {
@ -24,10 +22,6 @@ export class ErrorDialogService {
data
});
dialogRef.afterClosed().subscribe(result => {
this.loggingService.sendInfoLevelMessage('The dialog was closed');
this.isDialogOpen = false;
const res = result;
});
dialogRef.afterClosed().subscribe(() => this.isDialogOpen = false);
}
}

View File

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { HttpWrapperService } from './http-wrapper.service';
describe('HttpWrapperService', () => {
let service: HttpWrapperService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(HttpWrapperService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,60 +0,0 @@
import { Injectable } from '@angular/core';
import {HttpClient, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';
import * as moment from 'moment';
import { Moment } from 'moment';
import {LoggingService} from '@app/_services/logging.service';
@Injectable({
providedIn: 'root'
})
export class HttpWrapperService {
constructor(
private http: HttpClient,
private loggingService: LoggingService,
) { }
get(url: string, options?: any): Observable<Response> {
return this.request('GET', url, null, options);
}
post(url: string, body: any, options?: any): Observable<Response> {
return this.request('POST', url, body, options);
}
put(url: string, body: any, options?: any): Observable<Response> {
return this.request('PUT', url, body, options);
}
delete(url: string, options?: any): Observable<Response> {
return this.request('DELETE', url, null, options);
}
private logTime(startMoment: Moment, url: string, method: string): void {
const requestDuration = moment().diff(startMoment, 'milliseconds');
this.loggingService.sendInfoLevelMessage(`HTTP ${method}, URL: ${url}, Duration: ${requestDuration} ms`);
}
private request(method: string, url: string, body?: any, options?: any): Observable<any> {
this.loggingService.sendInfoLevelMessage(`Options: ${options}`);
return Observable.create((observer: any) => {
const requestBeginTime = moment();
this.http.request(new HttpRequest(method, url, body, options)).subscribe((response) => {
this.loggingService.sendInfoLevelMessage(response);
this.logTime(requestBeginTime, `${url}`, method);
observer.next(response);
observer.complete();
}, (error) => {
switch (error.status) {
case 403:
observer.complete();
break;
default:
observer.error(error);
break;
}
});
});
}
}

View File

@ -5,5 +5,4 @@ export * from '@app/_services/token.service';
export * from '@app/_services/block-sync.service';
export * from '@app/_services/location.service';
export * from '@app/_services/logging.service';
export * from '@app/_services/http-wrapper.service';
export * from '@app/_services/error-dialog.service';

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import {BehaviorSubject} from 'rxjs';
import {environment} from '@src/environments/environment';
import {first} from 'rxjs/operators';
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
import {HttpClient} from '@angular/common/http';
@Injectable({
providedIn: 'root'
@ -13,10 +13,10 @@ export class LocationService {
locationsSubject = this.locationsList.asObservable();
constructor(
private httpWrapperService: HttpWrapperService,
private httpClient: HttpClient,
) { }
getLocations(): void {
this.httpWrapperService.get(`${environment.cicCacheUrl}/locations`).pipe(first()).subscribe(res => this.locationsList.next(res.body));
this.httpClient.get(`${environment.cicCacheUrl}/locations`).pipe(first()).subscribe(res => this.locationsList.next(res));
}
}

View File

@ -4,8 +4,8 @@ import {BehaviorSubject, Observable} from 'rxjs';
import {HttpGetter} from '@app/_helpers';
import {CICRegistry} from 'cic-client';
import Web3 from 'web3';
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
import {Registry, TokenRegistry} from '@app/_eth';
import {HttpClient} from '@angular/common/http';
@Injectable({
providedIn: 'root'
@ -20,7 +20,7 @@ export class TokenService {
tokensSubject = this.tokensList.asObservable();
constructor(
private httpWrapperService: HttpWrapperService,
private httpClient: HttpClient,
) { }
async getTokens(): Promise<any> {
@ -30,7 +30,7 @@ export class TokenService {
}
getTokenBySymbol(symbol: string): Observable<any> {
return this.httpWrapperService.get(`${environment.cicCacheUrl}/tokens/${symbol}`);
return this.httpClient.get(`${environment.cicCacheUrl}/tokens/${symbol}`);
}
async getTokenBalance(address: string): Promise<number> {

View File

@ -13,8 +13,8 @@ import * as secp256k1 from 'secp256k1';
import {AuthService} from '@app/_services/auth.service';
import {defaultAccount} from '@app/_models';
import {LoggingService} from '@app/_services/logging.service';
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
import {Registry} from '@app/_eth';
import {HttpClient} from '@angular/common/http';
const Web3 = require('web3');
const vCard = require('vcard-parser');
@ -30,18 +30,18 @@ export class TransactionService {
registry = new Registry(environment.registryAddress);
constructor(
private httpWrapperService: HttpWrapperService,
private httpClient: HttpClient,
private authService: AuthService,
private userService: UserService,
private loggingService: LoggingService
) { }
getAllTransactions(offset: number, limit: number): Observable<any> {
return this.httpWrapperService.get(`${environment.cicCacheUrl}/tx/${offset}/${limit}`);
return this.httpClient.get(`${environment.cicCacheUrl}/tx/${offset}/${limit}`);
}
getAddressTransactions(address: string, offset: number, limit: number): Observable<any> {
return this.httpWrapperService.get(`${environment.cicCacheUrl}/tx/${address}/${offset}/${limit}`);
return this.httpClient.get(`${environment.cicCacheUrl}/tx/${address}/${offset}/${limit}`);
}
async setTransaction(transaction, cacheSize: number): Promise<void> {

View File

@ -6,7 +6,6 @@ import {first} from 'rxjs/operators';
import {ArgPair, Envelope, Syncable, User} from 'cic-client-meta';
import {MetaResponse} from '@app/_models';
import {LoggingService} from '@app/_services/logging.service';
import {HttpWrapperService} from '@app/_services/http-wrapper.service';
import {TokenService} from '@app/_services/token.service';
import {AccountIndex, Registry} from '@app/_eth';
import {MutableKeyStore, MutablePgpKeyStore, PGPSigner, Signer} from '@app/_pgp';
@ -35,8 +34,7 @@ export class UserService {
staffSubject = this.staffList.asObservable();
constructor(
private http: HttpClient,
private httpWrapperService: HttpWrapperService,
private httpClient: HttpClient,
private loggingService: LoggingService,
private tokenService: TokenService
) {
@ -44,16 +42,16 @@ export class UserService {
resetPin(phone: string): Observable<any> {
const params = new HttpParams().set('phoneNumber', phone);
return this.httpWrapperService.get(`${environment.cicUssdUrl}/pin`, {params});
return this.httpClient.get(`${environment.cicUssdUrl}/pin`, {params});
}
getAccountStatus(phone: string): any {
const params = new HttpParams().set('phoneNumber', phone);
return this.httpWrapperService.get(`${environment.cicUssdUrl}/pin`, {params});
return this.httpClient.get(`${environment.cicUssdUrl}/pin`, {params});
}
getLockedAccounts(offset: number, limit: number): any {
return this.httpWrapperService.get(`${environment.cicUssdUrl}/accounts/locked/${offset}/${limit}`);
return this.httpClient.get(`${environment.cicUssdUrl}/accounts/locked/${offset}/${limit}`);
}
async changeAccountInfo(address: string, name: string, phoneNumber: string, age: string, type: string, bio: string, gender: string,
@ -75,8 +73,8 @@ export class UserService {
accountInfo.vcard = vCard.generate(accountInfo.vcard);
reqBody.m.data = accountInfo;
const accountKey = await User.toKey(address);
this.httpWrapperService.get(`${environment.cicMetaUrl}/${accountKey}`, { headers: this.headers }).pipe(first()).subscribe(async res => {
const syncableAccount: Syncable = Envelope.fromJSON(JSON.stringify(res.body)).unwrap();
this.httpClient.get(`${environment.cicMetaUrl}/${accountKey}`, { headers: this.headers }).pipe(first()).subscribe(async res => {
const syncableAccount: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
let update = [];
for (const prop in reqBody) {
update.push(new ArgPair(prop, reqBody[prop]));
@ -94,67 +92,47 @@ export class UserService {
async updateMeta(syncableAccount: Syncable, accountKey: string, headers: HttpHeaders): Promise<any> {
const envelope = await this.wrap(syncableAccount , this.signer);
const reqBody = envelope.toJSON();
this.httpWrapperService.put(`${environment.cicMetaUrl}/${accountKey}`, reqBody , { headers }).pipe(first()).subscribe(res => {
this.loggingService.sendInfoLevelMessage(`Response: ${res.body}`);
this.httpClient.put(`${environment.cicMetaUrl}/${accountKey}`, reqBody , { headers }).pipe(first()).subscribe(res => {
this.loggingService.sendInfoLevelMessage(`Response: ${res}`);
});
}
getAccounts(): void {
this.httpWrapperService.get(`${environment.cicCacheUrl}/accounts`).pipe(first()).subscribe(res => this.accountsList.next(res.body));
this.httpClient.get(`${environment.cicCacheUrl}/accounts`).pipe(first()).subscribe(res => this.accountsList.next(res));
}
getAccountById(id: number): Observable<any> {
return this.httpWrapperService.get(`${environment.cicCacheUrl}/accounts/${id}`);
return this.httpClient.get(`${environment.cicCacheUrl}/accounts/${id}`);
}
getActions(): void {
this.httpWrapperService.get(`${environment.cicCacheUrl}/actions`).pipe(first()).subscribe(res => this.actionsList.next(res.body));
this.httpClient.get(`${environment.cicCacheUrl}/actions`).pipe(first()).subscribe(res => this.actionsList.next(res));
}
getActionById(id: string): any {
return this.httpWrapperService.get(`${environment.cicCacheUrl}/actions/${id}`);
return this.httpClient.get(`${environment.cicCacheUrl}/actions/${id}`);
}
approveAction(id: string): Observable<any> {
return this.httpWrapperService.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: true });
return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: true });
}
revokeAction(id: string): Observable<any> {
return this.httpWrapperService.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: false });
return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: false });
}
getHistoryByUser(id: string): Observable<any> {
return this.httpWrapperService.get(`${environment.cicCacheUrl}/history/${id}`);
}
getStaff(): void {
this.httpWrapperService.get(`${environment.cicCacheUrl}/staff`).pipe(first()).subscribe(res => this.staffList.next(res.body));
}
getStaffById(id: string): Observable<any> {
return this.httpWrapperService.get(`${environment.cicCacheUrl}/staff/${id}`);
}
activateStaff(id: string): Observable<any> {
return this.httpWrapperService.post(`${environment.cicCacheUrl}/staff/${id}`, {status: 'activated'});
}
deactivateStaff(id: string): Observable<any> {
return this.httpWrapperService.post(`${environment.cicCacheUrl}/staff/${id}`, {status: 'deactivated'});
}
changeStaffType(id: string, type: string): Observable<any> {
return this.httpWrapperService.post(`${environment.cicCacheUrl}/staff/${id}`, {accountType: type});
return this.httpClient.get(`${environment.cicCacheUrl}/history/${id}`);
}
getAccountDetailsFromMeta(userKey: string): Observable<any> {
return this.http.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers });
return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers });
}
getUser(userKey: string): any {
return this.httpWrapperService.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers })
return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers })
.pipe(first()).subscribe(async res => {
return Envelope.fromJSON(JSON.stringify(res.body)).unwrap();
return Envelope.fromJSON(JSON.stringify(res)).unwrap();
});
}

View File

@ -1,5 +1,6 @@
import {ChangeDetectionStrategy, Component, HostListener} from '@angular/core';
import {AuthService, LoggingService, TokenService, TransactionService} from '@app/_services';
import {AuthService, ErrorDialogService, LoggingService, TransactionService} from '@app/_services';
import {catchError} from 'rxjs/operators';
@Component({
selector: 'app-root',
@ -15,14 +16,17 @@ export class AppComponent {
constructor(
private authService: AuthService,
private tokenService: TokenService,
private transactionService: TransactionService,
private loggingService: LoggingService,
private errorDialogService: ErrorDialogService
) {
(async () => {
await this.authService.mutableKeyStore.loadKeyring();
await this.authService.getPublicKeys();
this.loggingService.sendInfoLevelMessage(await this.tokenService.getTokens());
this.authService.getPublicKeys()
.pipe(catchError(async (error) => {
this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error});
this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
})).subscribe(this.authService.mutableKeyStore.importPublicKey);
})();
this.mediaQuery.addListener(this.onResize);
this.onResize(this.mediaQuery);

View File

@ -31,7 +31,7 @@ import {MutablePgpKeyStore} from '@app/_pgp';
SharedModule,
MatTableModule,
LoggerModule.forRoot({
level: environment.level,
level: environment.logLevel,
serverLogLevel: environment.serverLogLevel,
serverLoggingUrl: `${environment.loggingUrl}/api/logs/`,
disableConsoleLogging: false

View File

@ -29,8 +29,7 @@ export class AuthComponent implements OnInit {
if (this.authService.privateKey !== undefined) {
const setKey = await this.authService.setKey(this.authService.privateKey);
if (setKey && this.authService.sessionToken !== undefined) {
this.authService.setState(
'Click button to perform login ' + this.authService.sessionLoginCount + ' with token ' + this.authService.sessionToken);
this.authService.setState('Click button to log in');
}
}
}

View File

@ -14,7 +14,7 @@
<ol class="breadcrumb">
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
<li class="breadcrumb-item"><a routerLink="/accounts">Accounts</a></li>
<!-- <li *ngIf="account" class="breadcrumb-item active" aria-current="page">{{account?.vcard?.fn[0].value}}</li>-->
<li *ngIf="account !== undefined" class="breadcrumb-item active" aria-current="page">{{account?.vcard?.fn[0].value}}</li>
</ol>
</nav>
<div *ngIf="!account" class="text-center">
@ -33,13 +33,11 @@
<h3>
<strong> {{account?.vcard?.fn[0].value}} </strong>
</h3>
<span class="ml-auto"><strong>Balance:</strong> {{account?.balance}} SRF</span>
<span class="ml-auto"><strong>Balance:</strong> {{account?.balance | tokenRatio}} SRF</span>
<span class="ml-2"><strong>Created:</strong> {{account?.date_registered | date}}</span>
<span class="ml-2"><strong>Address:</strong><a href="{{bloxbergLink}}" target="_blank"> {{account?.identities.evm['bloxberg:8996']}} </a></span>
</div>
</div>
<app-disbursement *ngIf="isDisbursing" (cancelDisbursmentEvent)="addTransfer()" [account]="account">
</app-disbursement>
<div *ngIf="account" class="card mt-3 mb-3">
<div class="card-body">
<form [formGroup]="accountInfoForm" (ngSubmit)="saveInfo()">
@ -217,22 +215,22 @@
<thead class="thead-dark">
<tr>
<th scope="col">NAME</th>
<th scope="col">ACCOUNT TYPE</th>
<th scope="col">BALANCE</th>
<th scope="col">CREATED</th>
<th scope="col">STATUS</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{account?.name}}</td>
<td>{{account?.type}}</td>
<td>{{account?.created}}</td>
<td>{{account?.vcard?.fn[0].value}}</td>
<td>{{account?.balance | tokenRatio}}</td>
<td>{{account?.date_registered | date}}</td>
<td>
<span *ngIf="account?.status === 'active'" class="badge badge-success badge-pill">
{{account?.status}}
<span *ngIf="accountStatus === 'active'" class="badge badge-success badge-pill">
{{accountStatus}}
</span>
<span *ngIf="account?.status === 'blocked'" class="badge badge-danger badge-pill">
{{account?.status}}
<span *ngIf="accountStatus === 'blocked'" class="badge badge-danger badge-pill">
{{accountStatus}}
</span>
</td>
</tr>
@ -242,50 +240,6 @@
</div>
</div>
<div class="card">
<mat-card-title class="card-header">
History
</mat-card-title>
<div class="card-body">
<mat-form-field appearance="outline">
<mat-label> Filter </mat-label>
<input matInput type="text" (keyup)="doHistoryFilter($event.target.value)" placeholder="Filter">
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
<mat-table class="mat-elevation-z10" [dataSource]="historyDataSource" matSort #HistoryTableSort="matSort"
matSortActive="sender" matSortDirection="asc" matSortDisableClear>
<ng-container matColumnDef="user">
<mat-header-cell *matHeaderCellDef mat-sort-header> User </mat-header-cell>
<mat-cell *matCellDef="let history"> {{history.userName}} </mat-cell>
</ng-container>
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef mat-sort-header> Action </mat-header-cell>
<mat-cell *matCellDef="let history"> {{history.action}} </mat-cell>
</ng-container>
<ng-container matColumnDef="staff">
<mat-header-cell *matHeaderCellDef mat-sort-header> Staff </mat-header-cell>
<mat-cell *matCellDef="let history"> {{history.staff}} </mat-cell>
</ng-container>
<ng-container matColumnDef="timestamp">
<mat-header-cell *matHeaderCellDef mat-sort-header> Timestamp </mat-header-cell>
<mat-cell *matCellDef="let history"> {{history.timestamp | date}} </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="historyDisplayedColumns"></mat-header-row>
<mat-row *matRowDef="let history; columns: historyDisplayedColumns" matRipple></mat-row>
</mat-table>
<mat-paginator #HistoryTablePaginator="matPaginator" [pageSize]="10"
[pageSizeOptions]="[10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
</div>
<mat-tab-group dynamicHeight mat-align-tabs="start">
<mat-tab label="Transactions">
<app-transaction-details [transaction]="transaction"></app-transaction-details>
@ -303,6 +257,7 @@
<mat-option value="reclamation">RECLAMATION</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv(transactions, 'transactions')"> EXPORT </button>
</div>
</div>
@ -376,6 +331,7 @@
<mat-option value="group">GROUPACCOUNT</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv(accounts, 'accounts')"> EXPORT </button>
</div>
<mat-form-field appearance="outline">

View File

@ -6,7 +6,7 @@ import {BlockSyncService, LocationService, LoggingService, TokenService, Transac
import {ActivatedRoute, Params, Router} from '@angular/router';
import {first} from 'rxjs/operators';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {CustomErrorStateMatcher} from '@app/_helpers';
import {CustomErrorStateMatcher, exportCsv} from '@app/_helpers';
import {Envelope, User} from 'cic-client-meta';
const vCard = require('vcard-parser');
@ -31,21 +31,14 @@ export class AccountDetailsComponent implements OnInit {
@ViewChild('UserTablePaginator', {static: true}) userTablePaginator: MatPaginator;
@ViewChild('UserTableSort', {static: true}) userTableSort: MatSort;
historyDataSource: MatTableDataSource<any>;
historyDisplayedColumns = ['user', 'action', 'staff', 'timestamp'];
@ViewChild('HistoryTablePaginator', {static: true}) historyTablePaginator: MatPaginator;
@ViewChild('HistoryTableSort', {static: true}) historyTableSort: MatSort;
accountInfoForm: FormGroup;
account: any;
accountAddress: string;
accountBalance: number;
accountStatus: any;
metaAccount: any;
accounts: any[] = [];
accountsType = 'all';
date: string;
time: number;
isDisbursing = false;
locations: any;
transaction: any;
transactions: any[];
@ -86,6 +79,7 @@ export class AccountDetailsComponent implements OnInit {
this.loggingService.sendInfoLevelMessage(this.account);
this.accountBalance = await this.tokenService.getTokenBalance(this.accountAddress);
this.account.vcard = vCard.parse(atob(this.account.vcard));
this.userService.getAccountStatus(this.account.vcard?.tel[0].value).pipe(first()).subscribe(response => this.accountStatus = response);
this.accountInfoForm.patchValue({
name: this.account.vcard?.fn[0].value,
phoneNumber: this.account.vcard?.tel[0].value,
@ -99,11 +93,6 @@ export class AccountDetailsComponent implements OnInit {
locationType: this.account.location.area_type,
});
});
this.userService.getHistoryByUser(this.accountAddress).pipe(first()).subscribe(response => {
this.historyDataSource = new MatTableDataSource<any>(response.body);
this.historyDataSource.paginator = this.historyTablePaginator;
this.historyDataSource.sort = this.historyTableSort;
});
this.blockSyncService.blockSync(this.accountAddress);
});
this.userService.getAccounts();
@ -127,13 +116,6 @@ export class AccountDetailsComponent implements OnInit {
this.transactionsDataSource.sort = this.transactionTableSort;
this.transactions = transactions;
});
const d = new Date();
this.date = `${d.getDate()}/${d.getMonth()}/${d.getFullYear()}`;
}
addTransfer(): void {
this.isDisbursing = !this.isDisbursing;
}
doTransactionFilter(value: string): void {
@ -144,10 +126,6 @@ export class AccountDetailsComponent implements OnInit {
this.userDataSource.filter = value.trim().toLocaleLowerCase();
}
doHistoryFilter(value: string): void {
this.historyDataSource.filter = value.trim().toLocaleLowerCase();
}
viewTransaction(transaction): void {
this.transaction = transaction;
}
@ -160,7 +138,7 @@ export class AccountDetailsComponent implements OnInit {
async saveInfo(): Promise<void> {
this.submitted = true;
if (this.accountInfoForm.invalid) { return; }
if (this.accountInfoForm.invalid || !confirm('Change user\'s profile information?')) { return; }
const accountKey = await this.userService.changeAccountInfo(
this.account.address,
this.accountInfoFormStub.name.value,
@ -202,12 +180,17 @@ export class AccountDetailsComponent implements OnInit {
}
resetPin(): void {
if (!confirm('Reset user\'s pin?')) { return; }
this.userService.resetPin(this.account.phone).pipe(first()).subscribe(res => {
this.loggingService.sendInfoLevelMessage(`Response: ${res.body}`);
this.loggingService.sendInfoLevelMessage(`Response: ${res}`);
});
}
public trackByName(index, item): string {
return item.name;
}
downloadCsv(data: any, filename: string): void {
exportCsv(data, filename);
}
}

View File

@ -33,7 +33,7 @@
<mat-option value="group">GROUPACCOUNT</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" routerLink="/accounts/export" type="button" class="btn btn-outline-primary ml-auto"> EXPORT </button>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button>
</div>
<mat-form-field appearance="outline">

View File

@ -4,6 +4,7 @@ import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {LoggingService, UserService} from '@app/_services';
import {Router} from '@angular/router';
import {exportCsv} from '@app/_helpers';
@Component({
selector: 'app-accounts',
@ -63,4 +64,16 @@ export class AccountsComponent implements OnInit {
this.dataSource.data = this.accounts.filter(account => account.type === this.accountsType);
}
}
refreshPaginator(): void {
if (!this.dataSource.paginator) {
this.dataSource.paginator = this.paginator;
}
this.paginator._changePageSize(this.paginator.pageSize);
}
downloadCsv(): void {
exportCsv(this.accounts, 'accounts');
}
}

View File

@ -43,7 +43,7 @@ export class CreateAccountComponent implements OnInit {
onSubmit(): void {
this.submitted = true;
if (this.createForm.invalid) { return; }
if (this.createForm.invalid || !confirm('Create account?')) { return; }
this.submitted = false;
}

View File

@ -33,7 +33,7 @@ export class DisbursementComponent implements OnInit {
async createTransfer(): Promise<void> {
this.submitted = true;
if (this.disbursementForm.invalid) { return; }
if (this.disbursementForm.invalid || !confirm('Make transfer?')) { return; }
if (this.disbursementFormStub.transactionType.value === 'transfer') {
await this.transactionService.transferRequest(
this.account.token,

View File

@ -28,7 +28,7 @@ export class ExportAccountsComponent implements OnInit {
export(): void {
this.submitted = true;
if (this.exportForm.invalid) { return; }
if (this.exportForm.invalid || !confirm('Export accounts?')) { return; }
this.submitted = false;
}
}

View File

@ -18,7 +18,10 @@
</nav>
<div class="card">
<mat-card-title class="card-header">
Actions
<div class="row">
Actions
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button>
</div>
</mat-card-title>
<div class="card-body">
@ -66,7 +69,7 @@
<mat-header-cell *matHeaderCellDef> APPROVE </mat-header-cell>
<mat-cell *matCellDef="let action">
<button mat-raised-button color="primary" *ngIf="!action.approval" class="btn btn-outline-success" (click)="approveAction(action)"> Approve </button>
<button mat-raised-button color="warn" *ngIf="action.approval" class="btn btn-outline-danger" (click)="revertAction(action)"> Revert </button>
<button mat-raised-button color="warn" *ngIf="action.approval" class="btn btn-outline-danger" (click)="disapproveAction(action)"> Disapprove </button>
</mat-cell>
</ng-container>

View File

@ -5,6 +5,7 @@ import {MatSort} from '@angular/material/sort';
import {LoggingService, UserService} from '@app/_services';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {first} from 'rxjs/operators';
import {exportCsv} from '@app/_helpers';
@Component({
selector: 'app-admin',
@ -23,6 +24,7 @@ export class AdminComponent implements OnInit {
dataSource: MatTableDataSource<any>;
displayedColumns = ['expand', 'user', 'role', 'action', 'status', 'approve'];
action: any;
actions: any;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
@ -36,6 +38,8 @@ export class AdminComponent implements OnInit {
this.dataSource = new MatTableDataSource<any>(actions);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.actions = actions;
console.log(this.actions);
});
}
@ -51,16 +55,22 @@ export class AdminComponent implements OnInit {
}
approveAction(action: any): void {
this.userService.approveAction(action.id).pipe(first()).subscribe(res => this.loggingService.sendInfoLevelMessage(res.body));
if (!confirm('Approve action?')) { return; }
this.userService.approveAction(action.id).pipe(first()).subscribe(res => this.loggingService.sendInfoLevelMessage(res));
this.userService.getActions();
}
revertAction(action: any): void {
this.userService.revokeAction(action.id).pipe(first()).subscribe(res => this.loggingService.sendInfoLevelMessage(res.body));
disapproveAction(action: any): void {
if (!confirm('Disapprove action?')) { return; }
this.userService.revokeAction(action.id).pipe(first()).subscribe(res => this.loggingService.sendInfoLevelMessage(res));
this.userService.getActions();
}
expandCollapse(row): void {
row.isExpanded = !row.isExpanded;
}
downloadCsv(): void {
exportCsv(this.actions, 'actions');
}
}

View File

@ -28,7 +28,7 @@ export class InviteComponent implements OnInit {
invite(): void {
this.submitted = true;
if (this.inviteForm.invalid) { return; }
if (this.inviteForm.invalid || !confirm('Invite user?')) { return; }
this.submitted = false;
}
}

View File

@ -29,7 +29,7 @@ export class OrganizationComponent implements OnInit {
onSubmit(): void {
this.submitted = true;
if (this.organizationForm.invalid) { return; }
if (this.organizationForm.invalid || !confirm('Set organization information?')) { return; }
this.submitted = false;
}
}

View File

@ -50,7 +50,7 @@
</div>
<hr>
<div class="card-body">
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary"> LOGOUT ADMIN </button>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary" (click)="logout()"> LOGOUT ADMIN </button>
</div>
</div>
</div>
@ -59,6 +59,7 @@
<mat-card-title class="card-header">
<div class="row">
TRUSTED USERS
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button>
</div>
</mat-card-title>
<div class="card-body">

View File

@ -4,6 +4,7 @@ import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {AuthService} from '@app/_services';
import {Staff} from '@app/_models/staff';
import {exportCsv} from '@app/_helpers';
@Component({
selector: 'app-settings',
@ -36,4 +37,12 @@ export class SettingsComponent implements OnInit {
doFilter(value: string): void {
this.dataSource.filter = value.trim().toLocaleLowerCase();
}
downloadCsv(): void {
exportCsv(this.trustedUsers, 'users');
}
logout(): void {
this.authService.logout();
}
}

View File

@ -18,7 +18,7 @@ export class TokenDetailsComponent implements OnInit {
) {
this.route.paramMap.subscribe((params: Params) => {
this.tokenService.getTokenBySymbol(params.get('id')).pipe(first()).subscribe(res => {
this.token = res.body;
this.token = res;
});
});
}

View File

@ -18,7 +18,10 @@
</nav>
<div class="card">
<mat-card-title class="card-header">
Tokens
<div class="row">
Tokens
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button>
</div>
</mat-card-title>
<div class="card-body">
<mat-form-field appearance="outline">

View File

@ -4,6 +4,7 @@ import {MatSort} from '@angular/material/sort';
import {LoggingService, TokenService} from '@app/_services';
import {MatTableDataSource} from '@angular/material/table';
import {Router} from '@angular/router';
import {exportCsv} from '@app/_helpers';
@Component({
selector: 'app-tokens',
@ -41,4 +42,8 @@ export class TokensComponent implements OnInit {
async viewToken(token): Promise<void> {
await this.router.navigateByUrl(`/tokens/${token.symbol}`);
}
downloadCsv(): void {
exportCsv(this.tokens, 'tokens');
}
}

View File

@ -36,6 +36,7 @@
<mat-option value="reclamation">RECLAMATION</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button>
</div>
<mat-form-field appearance="outline">

View File

@ -3,6 +3,7 @@ import {BlockSyncService, TransactionService} from '@app/_services';
import {MatTableDataSource} from '@angular/material/table';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {exportCsv} from '@app/_helpers';
@Component({
selector: 'app-transactions',
@ -61,4 +62,8 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
this.transactionDataSource.paginator = this.paginator;
this.transactionDataSource.sort = this.sort;
}
downloadCsv(): void {
exportCsv(this.transactions, 'transactions');
}
}

View File

@ -3,8 +3,8 @@
<p>
Message: {{ data.message }}
</p>
<p>
Status: {{ data.status }}
<p *ngIf="data.status">
Status: {{ data?.status }}
</p>
</div>
</div>

View File

@ -15,7 +15,7 @@
</head>
<body class="mat-typography">
<app-root></app-root>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script async src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script async src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script async src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
</body>