Skip to content

Commit

Permalink
Merge pull request #62 from Tonomy-Foundation/feature/59-server-creat…
Browse files Browse the repository at this point in the history
…es-account

Feature/59 server creates account
  • Loading branch information
theblockstalk authored Aug 30, 2023
2 parents a96b42a + 792cdb3 commit 001f866
Show file tree
Hide file tree
Showing 32 changed files with 415 additions and 72 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ yarn run start:dev
yarn run start:prod
```

## Environment variables and configuration

`NODE_ENV` - Determines which config file in `./src/config` to use
`CREATE_ACCOUNT_PRIVATE_KEY` - The private key used to sign the transaction to create a new account
`HCAPTCHA_SECRET` - The hCaptcha account secret key

## Test

```bash
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
"@nestjs/platform-socket.io": "^9.2.1",
"@nestjs/swagger": "^6.1.4",
"@nestjs/websockets": "^9.2.1",
"@tonomy/tonomy-id-sdk": "^0.14.1",
"@tonomy/tonomy-id-sdk": "development",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"hcaptcha": "^0.1.1",
"helmet": "^7.0.0",
"nestjs-asyncapi": "^1.0.4",
"reflect-metadata": "^0.1.13",
Expand Down
20 changes: 20 additions & 0 deletions src/accounts/accounts.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AccountsController } from './accounts.controller';
import { AccountsService } from './accounts.service';

describe('AccountsController', () => {
let controller: AccountsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AccountsController],
providers: [AccountsService],
}).compile();

controller = module.get<AccountsController>(AccountsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
74 changes: 74 additions & 0 deletions src/accounts/accounts.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
Body,
Controller,
HttpException,
HttpStatus,
Logger,
Post,
Res,
} from '@nestjs/common';
import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { AccountsService } from './accounts.service';
import {
CreateAccountRequest,
CreateAccountResponse,
} from './dto/create-account.dto';
import { Response } from 'express';

@Controller('accounts')
export class AccountsController {
private readonly logger = new Logger(AccountsController.name);
constructor(private accountService: AccountsService) { }

@Post()
@ApiOperation({
summary: 'Create a new Tonomy ID account on the blockchain',
})
@ApiParam({
name: 'usernameHash',
description: 'sha256 hash of username',
required: true,
type: 'string',
example: 'b06ecffb7ad2e992e82c1f3a23341bca36f8337f74032c00c489c21b00f66e52',
})
@ApiParam({
name: 'salt',
description: 'Salt used to generate the private key',
required: true,
type: 'string',
example: 'b06ecffb7ad2e992e82c1f3a23341bca36f8337f74032c00c489c21b00f66e52',
})
@ApiParam({
name: 'publicKey',
description: 'Public key that will control the account',
example: 'PUB_K1_6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5BoDq63',
required: true,
})
@ApiParam({
name: 'captchaToken',
description: 'The hCaptcha token',
required: true,
type: 'string',
example: '10000000-aaaa-bbbb-cccc-000000000001',
})
@ApiResponse({
status: HttpStatus.CREATED,
description: 'New account created',
type: CreateAccountResponse,
})
async createAccount(
@Body() createAccountDto: CreateAccountRequest,
@Res() response: Response,
): Promise<void> {
try {
const val = await this.accountService.createAccount(createAccountDto);

response.status(HttpStatus.CREATED).send(val);
} catch (e) {
if (e instanceof HttpException) throw e;
console.error(e);
this.logger.error(e);
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
9 changes: 9 additions & 0 deletions src/accounts/accounts.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { AccountsController } from './accounts.controller';
import { AccountsService } from './accounts.service';

@Module({
controllers: [AccountsController],
providers: [AccountsService],
})
export class AccountsModule {}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { AccountsService } from './accounts.service';

describe('UsersService', () => {
let service: UsersService;
describe('AccountsService', () => {
let service: AccountsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
providers: [AccountsService],
}).compile();

service = module.get<UsersService>(UsersService);
service = module.get<AccountsService>(AccountsService);
});

it('should be defined', () => {
Expand Down
106 changes: 106 additions & 0 deletions src/accounts/accounts.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import {
CreateAccountRequest,
CreateAccountResponse,
} from './dto/create-account.dto';
import { Name, PrivateKey } from '@wharfkit/antelope';
import settings from '../settings';
import { PushTransactionResponse } from '@wharfkit/antelope/src/api/v1/types';
import {
IDContract,
EosioUtil,
AntelopePushTransactionError,
} from '@tonomy/tonomy-id-sdk';
import { verify } from 'hcaptcha';

const idContract = IDContract.Instance;

@Injectable()
export class AccountsService {
private readonly logger = new Logger(AccountsService.name);

async createAccount(
createAccountRequest: CreateAccountRequest,
): Promise<CreateAccountResponse> {
this.logger.debug('createAccount()');

if (!createAccountRequest)
throw new HttpException(
'CreateAccountRequest not provided',
HttpStatus.BAD_REQUEST,
);
if (!createAccountRequest.usernameHash)
throw new HttpException(
'UsernameHash not provided',
HttpStatus.BAD_REQUEST,
);
if (!createAccountRequest.publicKey)
throw new HttpException(
'Public key not provided',
HttpStatus.BAD_REQUEST,
);
if (!createAccountRequest.salt)
throw new HttpException('Salt not provided', HttpStatus.BAD_REQUEST);
if (!createAccountRequest.captchaToken)
throw new HttpException(
'Captcha token not provided',
HttpStatus.BAD_REQUEST,
);

const verifyResponse = await verify(
settings.secrets.hCaptchaSecret,
createAccountRequest.captchaToken,
);

if (!verifyResponse.success) {
throw new HttpException(
{
message: 'Captcha verification failed',
errors: verifyResponse['error-codes'],
},
HttpStatus.BAD_REQUEST,
);
}

const idTonomyActiveKey = PrivateKey.from(
settings.secrets.createAccountPrivateKey,
);

let res: PushTransactionResponse;

try {
res = await idContract.newperson(
createAccountRequest.usernameHash,
createAccountRequest.publicKey,
createAccountRequest.salt,
EosioUtil.createSigner(idTonomyActiveKey),
);
} catch (e) {
if (
e instanceof AntelopePushTransactionError &&
e.hasErrorCode(3050003) &&
e.hasTonomyErrorCode('TCON1000')
) {
throw new HttpException(
'Username is already taken',
HttpStatus.BAD_REQUEST,
);
}

throw e;
}

const newAccountAction =
res.processed.action_traces[0].inline_traces[0].act;

const accountName = Name.from(newAccountAction.data.name);

if (settings.config.loggerLevel === 'debug')
this.logger.debug('createAccount()', accountName.toString());

return {
transactionId: res.transaction_id,
accountName: accountName.toString(),
};
}
}
44 changes: 44 additions & 0 deletions src/accounts/dto/create-account.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ApiProperty } from '@nestjs/swagger';

export class CreateAccountRequest {
@ApiProperty({
required: true,
description: 'sha256 hash of username',
example: 'b06ecffb7ad2e992e82c1f3a23341bca36f8337f74032c00c489c21b00f66e52',
})
usernameHash?: string;

@ApiProperty({
required: true,
description: 'Salt used to generate the private key',
example: 'b06ecffb7ad2e992e82c1f3a23341bca36f8337f74032c00c489c21b00f66e52',
})
salt?: string;

@ApiProperty({
required: true,
description: 'Public key that will control the account',
example: 'PUB_K1_6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5BoDq63',
})
publicKey?: string;

@ApiProperty({
required: true,
description: 'The hCaptcha token',
example: '10000000-aaaa-bbbb-cccc-000000000001',
type: 'string',
})
captchaToken?: string;
}

export class CreateAccountResponse {
@ApiProperty({
example: 'dfd401c1dcd5fd4ff7836dfe5e3b54630a077ea01643c1529ac20e4e03b26763',
})
transactionId!: string;

@ApiProperty({
example: 'tonomyacc1',
})
accountName!: string;
}
5 changes: 3 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { CommunicationModule } from './communication/communication.module';
import { AccountsModule } from './accounts/accounts.module';

@Module({
imports: [UsersModule],
imports: [CommunicationModule, AccountsModule],
controllers: [AppController],
providers: [AppService],
})
Expand Down
19 changes: 19 additions & 0 deletions src/communication/communication.gateway.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommunicationGateway } from './communication.gateway';
import { CommunicationService } from './communication.service';

describe('CommunicationGateway', () => {
let gateway: CommunicationGateway;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CommunicationGateway, CommunicationService],
}).compile();

gateway = module.get<CommunicationGateway>(CommunicationGateway);
});

it('should be defined', () => {
expect(gateway).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
OnGatewayDisconnect,
BaseWsExceptionFilter,
} from '@nestjs/websockets';
import { UsersService } from './users.service';
import { CommunicationService } from './communication.service';
import {
HttpException,
HttpStatus,
Expand All @@ -21,7 +21,7 @@ import { MessageDto, MessageRto } from './dto/message.dto';
import { Client } from './dto/client.dto';
import { WsExceptionFilter } from './ws-exception/ws-exception.filter';
import { AuthenticationMessage } from '@tonomy/tonomy-id-sdk';
import { UsersGuard } from './users.guard';
import { CommunicationGuard } from './communication.guard';

@UseFilters(WsExceptionFilter)
@UsePipes(new TransformVcPipe())
Expand All @@ -33,9 +33,9 @@ import { UsersGuard } from './users.guard';
},
})
@UseFilters(new BaseWsExceptionFilter())
export class UsersGateway implements OnGatewayDisconnect {
private readonly logger = new Logger(UsersGateway.name);
constructor(private readonly usersService: UsersService) {}
export class CommunicationGateway implements OnGatewayDisconnect {
private readonly logger = new Logger(CommunicationGateway.name);
constructor(private readonly usersService: CommunicationService) {}

/**
* Logs in the user and added it to the loggedIn map
Expand Down Expand Up @@ -73,7 +73,7 @@ export class UsersGateway implements OnGatewayDisconnect {
* @returns void
*/
@SubscribeMessage('message')
@UseGuards(UsersGuard)
@UseGuards(CommunicationGuard)
@AsyncApiPub({
channel: 'message',
message: {
Expand Down
7 changes: 7 additions & 0 deletions src/communication/communication.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CommunicationGuard } from './communication.guard';

describe('CommunicationGuard', () => {
it('should be defined', () => {
expect(new CommunicationGuard()).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { Observable } from 'rxjs';

@Injectable()
export class UsersGuard implements CanActivate {
export class CommunicationGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
Expand Down
Loading

0 comments on commit 001f866

Please sign in to comment.