Add contact, upgrade versions, add docs
This commit is contained in:
@@ -15,3 +15,5 @@ S3_SECRET_KEY="localminio"
|
||||
S3_BUCKET="devbucket"
|
||||
|
||||
FOCO_LIVE_AIRTABLE_APIKEY=
|
||||
|
||||
MAILGUN_SEND_KEY_HOOLI=
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -37,3 +37,6 @@ lerna-debug.log*
|
||||
!.vscode/extensions.json
|
||||
|
||||
data/
|
||||
|
||||
# Ignore Bruno for now
|
||||
api.us.dev/
|
@@ -41,7 +41,8 @@
|
||||
"haversine-ts": "^1.2.0",
|
||||
"hbs": "^4.2.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"minio": "^7.1.3",
|
||||
"mailgun.js": "^10.2.3",
|
||||
"minio": "^8.0.1",
|
||||
"open-graph-scraper": "^6.3.0",
|
||||
"prom-client": "^15.0.0",
|
||||
"ramda": "^0.29.0",
|
||||
|
@@ -10,7 +10,6 @@ import { DomainrproxyModule } from './domainrproxy/domainrproxy.module';
|
||||
import configuration from './config/configuration';
|
||||
import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager';
|
||||
import { IrcbotModule } from './ircbot/ircbot.module';
|
||||
import { OgScraperModule } from './ogscraper/ogscraper.module';
|
||||
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
|
||||
import { MinioModule } from './minio/minio.module';
|
||||
import { KvModule } from './kv/kv.module';
|
||||
@@ -28,6 +27,8 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
|
||||
import { AdsbExchangeModule } from './adsb-exchange/adsb-exchange.module';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { JunkDrawerModule } from './junk-drawer/junk-drawer.module';
|
||||
import { EmailModule } from './email/email.module';
|
||||
import { ContactModule } from './contact/contact.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -91,7 +92,6 @@ import { JunkDrawerModule } from './junk-drawer/junk-drawer.module';
|
||||
UsersModule,
|
||||
DomainrproxyModule,
|
||||
IrcbotModule,
|
||||
OgScraperModule,
|
||||
MinioModule,
|
||||
KvModule,
|
||||
RedirectsModule,
|
||||
@@ -102,6 +102,8 @@ import { JunkDrawerModule } from './junk-drawer/junk-drawer.module';
|
||||
JobsModule,
|
||||
AdsbExchangeModule,
|
||||
JunkDrawerModule,
|
||||
EmailModule,
|
||||
ContactModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
@@ -111,5 +113,6 @@ import { JunkDrawerModule } from './junk-drawer/junk-drawer.module';
|
||||
useClass: CacheInterceptor,
|
||||
},
|
||||
],
|
||||
exports: [PrometheusModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
@@ -26,13 +26,28 @@ class HashDto {
|
||||
rounds?: number;
|
||||
}
|
||||
|
||||
class LoginDto {
|
||||
@ApiProperty({
|
||||
description: 'The username to authenticate',
|
||||
default: 'admin',
|
||||
})
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The password to authenticate',
|
||||
default: 'password',
|
||||
})
|
||||
password: string;
|
||||
}
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) { }
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('login')
|
||||
@ApiBody({ type: LoginDto })
|
||||
signIn(@Body() signInDto: Record<string, any>) {
|
||||
return this.authService.signIn(signInDto.username, signInDto.password);
|
||||
}
|
||||
|
@@ -44,4 +44,7 @@ export default () => ({
|
||||
bucketName: process.env.JUNK_DRAWER_BUCKET_NAME ?? 'junk-drawer',
|
||||
rootPath: process.env.JUNK_DRAWER_ROOT_PATH ?? '',
|
||||
},
|
||||
mailgun: {
|
||||
hooliKey: process.env.MAILGUN_SEND_KEY_HOOLI ?? '',
|
||||
},
|
||||
});
|
||||
|
85
src/contact/contact.controller.ts
Normal file
85
src/contact/contact.controller.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiConsumes, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { EmailService } from 'src/email/email.service';
|
||||
import { PowService } from 'src/pow/pow.service';
|
||||
|
||||
interface Destination {
|
||||
email: string;
|
||||
powChallenge: boolean;
|
||||
}
|
||||
|
||||
type Destinations = {
|
||||
[key: string]: Destination;
|
||||
};
|
||||
|
||||
const destinations: Destinations = {
|
||||
focolive: {
|
||||
// email: 'hello@fortcollinslive.com',
|
||||
email: 'mailtest@chip.bz',
|
||||
powChallenge: true,
|
||||
},
|
||||
};
|
||||
|
||||
@Controller('contact')
|
||||
@ApiTags('contact')
|
||||
export class ContactController {
|
||||
constructor(
|
||||
private readonly powService: PowService,
|
||||
private readonly emailService: EmailService,
|
||||
) {}
|
||||
|
||||
@Get(':destination/challenge')
|
||||
@ApiParam({
|
||||
name: 'destination',
|
||||
required: true,
|
||||
description: 'The destination to send the email to, the contact "class"',
|
||||
})
|
||||
async getChallenge(@Param('destination') destination: string) {
|
||||
const dest = destinations[destination];
|
||||
if (!dest) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (dest.powChallenge) {
|
||||
return {
|
||||
challenge: await this.powService.generateChallenge(),
|
||||
difficulty: this.powService.getDifficulty(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@Post(':destination')
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async sendEmail(
|
||||
@Param('destination') destination: string,
|
||||
@Body('challenge') challenge: string,
|
||||
@Body('proof') proof: string,
|
||||
@Body('subject') subject: string,
|
||||
@Body('text') text: string,
|
||||
) {
|
||||
const dest = destinations[destination];
|
||||
if (!dest) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (dest.powChallenge) {
|
||||
if (!(await this.powService.verifyChallenge(challenge, proof))) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
const result = await this.emailService.sendEmail(
|
||||
[dest.email],
|
||||
subject,
|
||||
text,
|
||||
);
|
||||
this.powService.markChallengeAsComplete(challenge);
|
||||
return result;
|
||||
}
|
||||
}
|
12
src/contact/contact.module.ts
Normal file
12
src/contact/contact.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ContactController } from './contact.controller';
|
||||
import { EmailService } from 'src/email/email.service';
|
||||
import { PowService } from 'src/pow/pow.service';
|
||||
import { PowModule } from 'src/pow/pow.module';
|
||||
|
||||
@Module({
|
||||
imports: [PowModule],
|
||||
controllers: [ContactController],
|
||||
providers: [EmailService],
|
||||
})
|
||||
export class ContactModule {}
|
@@ -9,7 +9,7 @@ import {
|
||||
} from '@nestjs/swagger';
|
||||
import { DomainrParsedStatusResult } from './domainrproxy.service';
|
||||
|
||||
@ApiTags('Domainr')
|
||||
@ApiTags('domainr')
|
||||
@Controller('domainrproxy')
|
||||
export class DomainrproxyController {
|
||||
constructor(private readonly proxyService: DomainrproxyService) {}
|
||||
|
8
src/email/email.module.ts
Normal file
8
src/email/email.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EmailService } from './email.service';
|
||||
|
||||
@Module({
|
||||
providers: [EmailService],
|
||||
exports: [EmailService],
|
||||
})
|
||||
export class EmailModule {}
|
36
src/email/email.service.ts
Normal file
36
src/email/email.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import Mailgun from 'mailgun.js';
|
||||
import * as formdata from 'form-data';
|
||||
import { IMailgunClient } from 'mailgun.js/Interfaces';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
private readonly mailgun: IMailgunClient | undefined;
|
||||
private static readonly emailFromDomain = 'hello.hooli.co';
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const mailgun = new Mailgun(formdata);
|
||||
const hooliKey = this.configService.get<string>('mailgun.hooliKey');
|
||||
if (!hooliKey) {
|
||||
return;
|
||||
}
|
||||
this.mailgun = mailgun.client({
|
||||
username: 'api',
|
||||
key: hooliKey,
|
||||
});
|
||||
}
|
||||
|
||||
async sendEmail(to: string[], subject: string, text: string) {
|
||||
if (!this.mailgun) {
|
||||
return;
|
||||
}
|
||||
return this.mailgun.messages.create(EmailService.emailFromDomain, {
|
||||
from: `HooliMail <hooli-mail@${EmailService.emailFromDomain}>`,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html: text,
|
||||
});
|
||||
}
|
||||
}
|
@@ -10,7 +10,6 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { FileService } from './file.service';
|
||||
import { Post } from '@nestjs/common';
|
||||
import { UploadedObjectInfo } from 'minio';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { ItemBucketMetadata, UploadedObjectInfo } from 'minio';
|
||||
import { ItemBucketMetadata } from 'minio';
|
||||
import { UploadedObjectInfo } from 'minio/dist/main/internal/type';
|
||||
import { MinioService } from 'src/minio/minio.service';
|
||||
|
||||
@Injectable()
|
||||
|
@@ -83,7 +83,6 @@ export class JunkDrawerController {
|
||||
@Body('description') description: string,
|
||||
@Body('private-ish') privateIsh: boolean,
|
||||
): Promise<any> {
|
||||
console.log(privateIsh);
|
||||
const uniqueSlug = generateUniqueSlug({
|
||||
random: privateIsh,
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Queue } from 'bull';
|
||||
import { UploadedObjectInfo } from 'minio';
|
||||
import { UploadedObjectInfo } from 'minio/dist/main/internal/type';
|
||||
import { MinioService } from 'src/minio/minio.service';
|
||||
|
||||
@Injectable()
|
||||
|
@@ -2,7 +2,9 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Client, ItemBucketMetadata, UploadedObjectInfo } from 'minio';
|
||||
import { Client, ItemBucketMetadata } from 'minio';
|
||||
import { UploadedObjectInfo } from 'minio/dist/main/internal/type';
|
||||
import { open, readFileSync } from 'fs';
|
||||
|
||||
@Injectable()
|
||||
export class MinioService {
|
||||
@@ -30,12 +32,8 @@ export class MinioService {
|
||||
filePath: string,
|
||||
metadata?: ItemBucketMetadata,
|
||||
): Promise<UploadedObjectInfo> {
|
||||
return await this.client.fPutObject(
|
||||
bucketName,
|
||||
objectName,
|
||||
filePath,
|
||||
metadata,
|
||||
);
|
||||
const file = readFileSync(filePath);
|
||||
return this.uploadBuffer(bucketName, objectName, file, metadata);
|
||||
}
|
||||
|
||||
public async uploadBuffer(
|
||||
@@ -44,10 +42,11 @@ export class MinioService {
|
||||
buffer: Buffer,
|
||||
metadata?: ItemBucketMetadata,
|
||||
): Promise<UploadedObjectInfo> {
|
||||
return await this.client.putObject(
|
||||
return this.client.putObject(
|
||||
bucketName,
|
||||
objectName,
|
||||
buffer,
|
||||
buffer.length,
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
@@ -1,34 +0,0 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
const ogs = require('open-graph-scraper');
|
||||
import { SuccessResult } from 'open-graph-scraper';
|
||||
import { OgScraperService } from './ogscraper.service';
|
||||
import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
||||
import { Histogram } from 'prom-client';
|
||||
|
||||
class ScrapeOgDto {
|
||||
@ApiProperty({
|
||||
description: 'URL of the page to fetch Open Graph metadata of',
|
||||
example:
|
||||
'https://qz.com/1903322/why-pivot-tables-are-the-spreadsheets-most-powerful-tool',
|
||||
})
|
||||
url: string;
|
||||
}
|
||||
|
||||
@Controller('ogscraper')
|
||||
@ApiTags('open-graph-scraper')
|
||||
export class OgScraperController {
|
||||
constructor(
|
||||
private readonly ogScraperService: OgScraperService,
|
||||
@InjectMetric('generation_time')
|
||||
public generationTime: Histogram<string>,
|
||||
) {}
|
||||
|
||||
@Post('')
|
||||
async scrapeOg(@Body() body: ScrapeOgDto): Promise<SuccessResult> {
|
||||
const end = this.generationTime.startTimer();
|
||||
const response = await this.ogScraperService.getOg(body.url);
|
||||
end();
|
||||
return response;
|
||||
}
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OgScraperController } from './ogscraper.controller';
|
||||
import { OgScraperService } from './ogscraper.service';
|
||||
import {
|
||||
PrometheusModule,
|
||||
makeHistogramProvider,
|
||||
} from '@willsoto/nestjs-prometheus';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrometheusModule.register({
|
||||
customMetricPrefix: 'ogscraper',
|
||||
defaultMetrics: {
|
||||
enabled: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [OgScraperController],
|
||||
providers: [
|
||||
OgScraperService,
|
||||
makeHistogramProvider({
|
||||
name: 'generation_time',
|
||||
help: 'Open Graph Scraping response times',
|
||||
}),
|
||||
],
|
||||
exports: [OgScraperService],
|
||||
})
|
||||
export class OgScraperModule {}
|
@@ -1,20 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
const ogs = require('open-graph-scraper');
|
||||
import { SuccessResult } from 'open-graph-scraper';
|
||||
|
||||
@Injectable()
|
||||
export class OgScraperService {
|
||||
constructor(public readonly configService: ConfigService) {}
|
||||
|
||||
async getOg(url: string): Promise<SuccessResult> {
|
||||
return ogs({
|
||||
url,
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
'user-agent': this.configService.get<string>('userAgent') || '',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,37 +1,66 @@
|
||||
import { Body, Controller, Get, Post, Query, Render } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
Render,
|
||||
} from '@nestjs/common';
|
||||
import { PowService } from './pow.service';
|
||||
import { ApiBody, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiBody, ApiProperty, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
class SolveDto {
|
||||
@ApiProperty({
|
||||
description: 'The challenge to solve',
|
||||
})
|
||||
challenge: string;
|
||||
}
|
||||
|
||||
@ApiTags('pow')
|
||||
@Controller('pow')
|
||||
export class PowController {
|
||||
constructor(
|
||||
private readonly powService: PowService,
|
||||
) { }
|
||||
constructor(
|
||||
private readonly powService: PowService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get('')
|
||||
@Render('pow/index')
|
||||
async index() {
|
||||
return {
|
||||
difficulty: this.powService.getDifficulty(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('challenge')
|
||||
async generateChallenge() {
|
||||
return this.powService.generateChallenge();
|
||||
}
|
||||
|
||||
@Post('challenge')
|
||||
@ApiBody({ schema: { properties: { challenge: { type: 'string' }, proof: { type: 'string' } } } })
|
||||
async verifyChallenge(@Body() body: { challenge: string, proof: string }) {
|
||||
return this.powService.verifyChallenge(body.challenge, body.proof);
|
||||
}
|
||||
|
||||
@Post('challenge/complete')
|
||||
@ApiBody({ schema: { properties: { challenge: { type: 'string' } } } })
|
||||
async markChallengeAsComplete(@Body() body: { challenge: string }) {
|
||||
return this.powService.markChallengeAsComplete(body.challenge);
|
||||
@Get('')
|
||||
@Render('pow/index')
|
||||
async index() {
|
||||
return {
|
||||
difficulty: this.powService.getDifficulty(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('challenge')
|
||||
async generateChallenge() {
|
||||
return this.powService.generateChallenge();
|
||||
}
|
||||
|
||||
@Post('challenge')
|
||||
@ApiBody({
|
||||
schema: {
|
||||
properties: { challenge: { type: 'string' }, proof: { type: 'string' } },
|
||||
},
|
||||
})
|
||||
async verifyChallenge(@Body() body: { challenge: string; proof: string }) {
|
||||
return this.powService.verifyChallenge(body.challenge, body.proof);
|
||||
}
|
||||
|
||||
@Post('challenge/complete')
|
||||
@ApiBody({ schema: { properties: { challenge: { type: 'string' } } } })
|
||||
async markChallengeAsComplete(@Body() body: { challenge: string }) {
|
||||
return this.powService.markChallengeAsComplete(body.challenge);
|
||||
}
|
||||
|
||||
@Post('challenge/solve')
|
||||
async solveChallenge(@Body() body: SolveDto) {
|
||||
if (this.configService.get<boolean>('isProduction')) {
|
||||
throw new BadRequestException('This endpoint is disabled in production');
|
||||
}
|
||||
|
||||
return this.powService.performChallenge(body.challenge);
|
||||
}
|
||||
}
|
||||
|
@@ -1,44 +1,34 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PowService } from './pow.service';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { PowController } from './pow.controller';
|
||||
import { PrometheusModule, makeGaugeProvider } from '@willsoto/nestjs-prometheus';
|
||||
import { makeGaugeProvider } from '@willsoto/nestjs-prometheus';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule.register({
|
||||
ttl: 5,
|
||||
max: 10,
|
||||
}),
|
||||
PrometheusModule.register({
|
||||
customMetricPrefix: 'pow',
|
||||
defaultMetrics: {
|
||||
enabled: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [PowService,
|
||||
imports: [],
|
||||
providers: [
|
||||
PowService,
|
||||
makeGaugeProvider({
|
||||
name: 'challenges_generated',
|
||||
name: 'pow_challenges_generated',
|
||||
help: 'The total number of POW challenges generated',
|
||||
}),
|
||||
makeGaugeProvider({
|
||||
name: 'challenges_completed',
|
||||
name: 'pow_challenges_completed',
|
||||
help: 'The total number of POW challenges completed',
|
||||
}),
|
||||
makeGaugeProvider({
|
||||
name: 'successful_verifies',
|
||||
name: 'pow_successful_verifies',
|
||||
help: 'The total number of successful POW challenge verifications',
|
||||
}),
|
||||
makeGaugeProvider({
|
||||
name: 'failed_verifies',
|
||||
name: 'pow_failed_verifies',
|
||||
help: 'The total number of failed POW challenge verifications',
|
||||
}),
|
||||
makeGaugeProvider({
|
||||
name: 'difficulty',
|
||||
name: 'pow_difficulty',
|
||||
help: 'The current POW difficulty',
|
||||
}),
|
||||
],
|
||||
controllers: [PowController]
|
||||
controllers: [PowController],
|
||||
exports: [PowService],
|
||||
})
|
||||
export class PowModule { }
|
||||
export class PowModule {}
|
||||
|
@@ -4,102 +4,99 @@ import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { randomBytes, createHash } from 'crypto';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class PowService {
|
||||
private readonly logger = new Logger(PowService.name);
|
||||
private difficulty = 5;
|
||||
private readonly logger = new Logger(PowService.name);
|
||||
private difficulty = 5;
|
||||
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||
@InjectMetric('challenges_generated') private challengesGenerated: any,
|
||||
@InjectMetric('challenges_completed') private challengesCompleted: any,
|
||||
@InjectMetric('successful_verifies') private successfulVerifies: any,
|
||||
@InjectMetric('failed_verifies') private failedVerifies: any,
|
||||
@InjectMetric('difficulty') private powDifficulty: any,
|
||||
) {
|
||||
this.powDifficulty.set(this.difficulty);
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||
@InjectMetric('pow_challenges_generated') private challengesGenerated: any,
|
||||
@InjectMetric('pow_challenges_completed') private challengesCompleted: any,
|
||||
@InjectMetric('pow_successful_verifies') private successfulVerifies: any,
|
||||
@InjectMetric('pow_failed_verifies') private failedVerifies: any,
|
||||
@InjectMetric('pow_difficulty') private powDifficulty: any,
|
||||
) {
|
||||
this.powDifficulty.set(this.difficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a proof of work challenge, stored to redis for verification within
|
||||
* the next 60 seconds.
|
||||
*/
|
||||
async generateChallenge() {
|
||||
const challenge = this.generateRandom256BitString();
|
||||
this.logger.verbose(`Generated challenge: ${challenge}`);
|
||||
this.challengesGenerated.inc();
|
||||
await this.cacheManager.set(challenge, true, 60 * 1000);
|
||||
return challenge;
|
||||
}
|
||||
|
||||
generateRandom256BitString() {
|
||||
const randomString = randomBytes(32).toString('hex');
|
||||
return randomString;
|
||||
}
|
||||
|
||||
hashAndCheck(string: string) {
|
||||
return this.hashPassesDifficulty(this.hashString(string), this.difficulty);
|
||||
}
|
||||
|
||||
hashPassesDifficulty(hash: string, difficulty: number) {
|
||||
return hash.startsWith('0'.repeat(difficulty));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the proof of work submitted has a leading number of
|
||||
* zeroes equal to the challenge length and the challenge exists.
|
||||
*/
|
||||
async verifyChallenge(challenge: string, proof: string): Promise<boolean> {
|
||||
const expected = await this.cacheManager.get<boolean>(challenge);
|
||||
const success = expected ? this.hashAndCheck(proof + challenge) : false;
|
||||
if (success) {
|
||||
this.successfulVerifies.inc();
|
||||
} else {
|
||||
this.failedVerifies.inc();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a proof of work challenge, stored to redis for verification within
|
||||
* the next 10 seconds.
|
||||
*/
|
||||
async generateChallenge() {
|
||||
const challenge = this.generateRandom256BitString();
|
||||
this.logger.verbose(`Generated challenge: ${challenge}`);
|
||||
this.challengesGenerated.inc();
|
||||
await this.cacheManager.set(challenge, true, 60 * 1000);
|
||||
return challenge;
|
||||
async markChallengeAsComplete(challenge: string) {
|
||||
this.challengesCompleted.inc();
|
||||
await this.cacheManager.del(challenge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a proof of work challenge to find a proof that hashes to a value
|
||||
*/
|
||||
async performChallenge(challenge: string) {
|
||||
let proof = this.generateRandom256BitString();
|
||||
let hash = this.hashString(proof + challenge);
|
||||
while (!this.hashPassesDifficulty(hash, this.difficulty)) {
|
||||
proof = this.generateRandom256BitString();
|
||||
hash = this.hashString(proof + challenge);
|
||||
}
|
||||
return { proof, hash };
|
||||
}
|
||||
|
||||
generateRandom256BitString() {
|
||||
const randomString = randomBytes(32).toString('hex');
|
||||
return randomString;
|
||||
}
|
||||
/**
|
||||
* sha512 the provided string and return the result.
|
||||
*/
|
||||
hashString(input: string) {
|
||||
return createHash('sha512').update(input).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the difficulty of the proof of work challenge.
|
||||
*/
|
||||
setDifficulty(difficulty: number) {
|
||||
this.difficulty = difficulty;
|
||||
this.powDifficulty.set(this.difficulty);
|
||||
}
|
||||
|
||||
hashAndCheck(string: string) {
|
||||
return this.hashPassesDifficulty(this.hashString(string), this.difficulty);
|
||||
}
|
||||
|
||||
hashPassesDifficulty(hash: string, difficulty: number) {
|
||||
return hash.startsWith('0'.repeat(difficulty));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the proof of work submitted has a leading number of
|
||||
* zeroes equal to the challenge length and the challenge exists.
|
||||
*/
|
||||
async verifyChallenge(challenge: string, proof: string): Promise<boolean> {
|
||||
const expected = await this.cacheManager.get<boolean>(challenge);
|
||||
const success = expected ? this.hashAndCheck(proof + challenge) : false;
|
||||
if (success) {
|
||||
this.successfulVerifies.inc();
|
||||
} else {
|
||||
this.failedVerifies.inc();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
async markChallengeAsComplete(challenge: string) {
|
||||
this.challengesCompleted.inc();
|
||||
await this.cacheManager.del(challenge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a proof of work challenge to find a proof that hashes to a value
|
||||
*/
|
||||
async performChallenge(challenge: string) {
|
||||
let proof = this.generateRandom256BitString();
|
||||
let hash = this.hashString(proof + challenge);
|
||||
while (!this.hashPassesDifficulty(hash, this.difficulty)) {
|
||||
proof = this.generateRandom256BitString();
|
||||
hash = this.hashString(proof + challenge);
|
||||
}
|
||||
return { proof, hash };
|
||||
}
|
||||
|
||||
/**
|
||||
* sha512 the provided string and return the result.
|
||||
*/
|
||||
hashString(input: string) {
|
||||
return createHash('sha512').update(input).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the difficulty of the proof of work challenge.
|
||||
*/
|
||||
setDifficulty(difficulty: number) {
|
||||
this.difficulty = difficulty;
|
||||
this.powDifficulty.set(this.difficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current difficulty of the proof of work challenge.
|
||||
*/
|
||||
getDifficulty() {
|
||||
return this.difficulty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current difficulty of the proof of work challenge.
|
||||
*/
|
||||
getDifficulty() {
|
||||
return this.difficulty;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user