Skip to content

Commit

Permalink
[@dhealthdapps/backend] feat(widgets): implementation of working sock…
Browse files Browse the repository at this point in the history
…et, add comments, add logger
  • Loading branch information
kravchenkodhealth authored and evias committed Jan 3, 2023
1 parent e4f20ab commit 1979cc6
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 39 deletions.
51 changes: 44 additions & 7 deletions runtime/backend/src/common/gateways/AuthGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,67 @@ import { EventEmitter2, OnEvent } from "@nestjs/event-emitter";
import { ValidateChallengeScheduler } from "../schedulers/ValidateChallengeScheduler";
import { BaseGateway } from "./BaseGateway";

/**
* @label COMMON
* @class AuthGateway
* @description This class extends baseGateway. It's
* responsible for handling authentication requests,
* running validation scheduler, permitting to frontend to call get /token.
* <br /><br />
* This class can be used by adding different
* @SubscribeMessage handlers.
*
* @since v0.2.0
*/
@Injectable()
export class AuthGateway
extends BaseGateway
{
export class AuthGateway extends BaseGateway {
/**
* Construct an instance of class.
*
* @access public
* @param {ValidateChallengeScheduler} validateChallengeScheduler start validation of the challenge
* @param {EventEmitter2} clients Required by base gateway dependency.
*/
constructor(
private readonly validateChallengeScheduler: ValidateChallengeScheduler,
protected readonly emitter: EventEmitter2,
) {
super(emitter);
}

/**
* This method handles starting of challenge validation.
* Gets trigged by "auth.open" emit from handleConnection().
* Calls .startCronJob from validateChallengeScheduler.
*
* @param {any} payload Contains challenge string
* @returns {void} Emits "auth.open" event which triggers validating of the received challenge
*/
@OnEvent("auth.open")
handleEvent(payload: any) {
this.validateChallengeScheduler.startCronJob(payload.challenge);
return { msg: "You're connected" };
}

/**
* This method handles auth.close event triggered by client.
*
* @returns {void}
*/
@SubscribeMessage("auth.close")
close() {
console.log("AUTHGATEWAY: Connection closed");
this.logger.log("AUTHGATEWAY: Client disconnected");
}

@SubscribeMessage("auth.complete")
/**
* This method handles auth.close event,
* which is getting triggered by validateChallengeScheduler when challenge on chain.
* Sends auth.complete message to the client.
*
* @returns {void} Emits "auth.complete" event which informs client that token may be queried.
*/
@OnEvent("auth.complete")
complete() {
console.log("AUTHGATEWAY: Complete");
this.ws.send("auth.complete");
this.logger.log("AUTHGATEWAY: Complete");
}
}
92 changes: 89 additions & 3 deletions runtime/backend/src/common/gateways/BaseGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,31 @@ import {
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
MessageBody,
} from "@nestjs/websockets";
import { Server } from "https";
import cookie from "cookie";
import cookieParser from "cookie-parser";
import { EventEmitter2 } from "@nestjs/event-emitter";
import { Socket } from "dgram";
import { HttpException, HttpStatus } from "@nestjs/common";

// internal dependencies
import dappConfigLoader from "../../../config/dapp";
import { LogService } from "../services";

const dappConfig = dappConfigLoader();

/**
* @label COMMON
* @class BaseGateway
* @description This class serves as the *base class* for
* custom gateways which are connecting with the client through the websocket.
* <br /><br />
* This class can be used by extending it and adding different
* @SubscribeMessage handlers.
*
* @since v0.2.0
*/
@WebSocketGateway(80, {
path: "/ws",
cors: {
Expand All @@ -37,18 +49,69 @@ const dappConfig = dappConfigLoader();
export abstract class BaseGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
/**
* Construct an instance of the base gateway,
* initialize clients, logger and emitter properties.
*
* @access public
* @param {EventEmitter2} emitter Emitting events once connections/updates appear.
* @param {Array} clients Store connected client challenges.
* @param {LogService} logger Log important data to console or files based on configuration.
*/
constructor(protected readonly emitter: EventEmitter2) {
this.clients = [];
this.logger = new LogService(`${dappConfig.dappName}/gateway`);
}

/**
* This property permits to log information to the console or in files
* depending on the configuration. This logger instance can be accessed
* by extending listeners to use a common log process.
*
* @access protected
* @var {LogService}
*/
protected logger: LogService;

/**
* This property implements gateway server
* which is broadcasts different messages to single or multiple clients.
*
* @access protected
* @var {Server}
*/
@WebSocketServer()
server: any;
server: Server;

/**
* This property implements list of currently connected
* clients by storing their challenges. Challenge gets removed from list once client disconnects.
*
* @access protected
* @var {string[]}
*/
protected clients: string[];

/**
* This property stores socket instance which
* is getting assigned in it on connection. Used for sending messages/emitting events from child classes.
*
* @access protected
* @var {Socket}
*/
protected ws: Socket;

/**
* This method handles connection via websocket with the client.
* It also extracts challenge cookie from request, decodes it and stores in clients list.
* <br /><br />
* In case of a *successful* challenge decoding "auth.open" event will be fired.
*
* @param {any} ws Websocket connection param, holds server and client info.
* @param {any} req Request param which allows to access cookies
* @returns {Promise<void>} Emits "auth.open" event which triggers validating of the received challenge
* @throws {HttpException} Challenge wasn't attached to request cookies
*/
async handleConnection(ws: any, req: any) {
// parse challenge from cookie
const c: any = cookie.parse(req.headers.cookie);
Expand All @@ -57,25 +120,48 @@ export abstract class BaseGateway
process.env.SECURITY_AUTH_TOKEN_SECRET,
) as string;

// if challenge couldn't be parsed or parsed incorrectly - throw an error
if (!decoded)
throw new HttpException("Unauthorized", HttpStatus.UNAUTHORIZED);

// store ws connection to allow send messages to the client in child classes
this.ws = ws;

// add cookie to ws object
ws.challenge = decoded;
// push challenge to client list
this.clients.push(decoded);

// trigger auth.open event with challenge passed
this.emitter.emit("auth.open", { challenge: decoded });
ws.emit("connection_test", { msg: "its a test" });

this.logger.log("client connected", this.clients);
}

/**
* This method handles closing connection with the client.
* After client was disconnected - remove his challenge from list.
* <br /><br />
* In case of a *successful* validation attempts, i.e. when the `challenge`
* parameter **has been found** in a recent transfer transaction's message,
* a document will be *insert* in the collection `authChallenges`.
*
* @param {any} ws Websocket connection param, holds server and client info.
* @returns {void} Removes client challenge from list
*/
handleDisconnect(ws: any) {
const str = ws.challenge;
this.clients = this.clients.filter((c) => c !== str);

this.logger.log("Client disconnected", this.clients);
}

/**
* This method handles gateway initialize hook.
*
* @param {Server} server Websocket connection param, holds server and client info.
* @returns {void} Removes client challenge from list
*/
afterInit(server: Server) {
this.logger.log("Gateway initialized");
}
Expand Down
116 changes: 111 additions & 5 deletions runtime/backend/src/common/schedulers/ValidateChallengeScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,46 @@
* @license LGPL-3.0
*/

// external dependencies
import { Injectable } from "@nestjs/common";
import { CronJob } from "cron";
import { SchedulerRegistry } from "@nestjs/schedule";
import { EventEmitter2 } from "@nestjs/event-emitter";

// internal dependencies
import { AuthService } from "../services";
import { LogService } from "../services";
import dappConfigLoader from "../../../config/dapp";

const dappConfig = dappConfigLoader();

/**
* @label COMMON
* @class ValidateChallengeScheduler
* @description This class implements dynamic scheduler
* which can be started dynamically based on
* available connection. While it's running
* it validate received challenge. Stops once challenge becomes
* valid or 30 minutes have been passed.
*
* @since v0.2.0
*/
@Injectable()
export class ValidateChallengeScheduler {
/**
* Construct an instance of the scheduler.
*
* @access public
* @param {SchedulerRegistry} schedulerRegistry Add scheduler to Nest.js schedulers registry.
* @param {AuthService} authService Contains .validateChallenge method.
* @param {EventEmitter2} emitter Emitting of successfully validated challenge to proper handler.
*/
constructor(
private readonly schedulerRegistry: SchedulerRegistry,
protected readonly authService: AuthService,
protected readonly emitter: EventEmitter2,
) {
// initialize cronJob with provided params
this.job = new CronJob(
this.cronExpression, // cronTime
this.validate.bind(this), // onTick
Expand All @@ -35,35 +63,113 @@ export class ValidateChallengeScheduler {
`statistics:cronjobs:leaderboards:D`,
this.job,
);

// initialize logger
this.logger = new LogService(
`${dappConfig.dappName}/ValidateChallengeScheduler`,
);
}

/**
* This property permits to log information to the console or in files
* depending on the configuration. This logger instance can be accessed
* by extending listeners to use a common log process.
*
* @access protected
* @var {LogService}
*/
protected logger: LogService;

/**
* This property contains scheduler time,
* marks tick time when provided function will be called.
* Example: call validate() each 10 seconds
*
* @access protected
* @var {string}
*/
protected cronExpression = "*/10 * * * * *"; // each 10 seconds

/**
* This property contains created and stored CronJob.
*
* @access protected
* @var {CronJob}
*/
protected job: CronJob;

/**
* This property stores received
* challenge. Gets cleared once cron stops.
*
* @access protected
* @var {string}
*/
protected challenge: string;

/**
* This property stores stopCronJobTimeout,
* inside of it setTimeout() is getting set,
* when cronJob starts.
*
* @access protected
* @var {string}
*/
protected stopCronJobTimeout: any;

/**
* This property stores amount of time,
* after which cronJob should be stopped.
*
* @access protected
* @var {number}
*/
protected stopTimeoutAmount = 1800000;

/**
* This method implements validation process
* which runs by scheduler each period of time.
*
* @param {any} payload Contains challenge string
* @returns {void} Emits "auth.open" event which triggers validating of the received challenge
*/
protected async validate() {
try {
const payload = await this.authService.validateChallenge(this.challenge);
// after challenge validated successfully - stop running cron
this.stopCronJob();
console.log({ payload });
const payload = await this.authService.validateChallenge(
this.challenge,
false,
);

if (null !== payload) {
// after challenge validated successfully - stop running cron
this.stopCronJob();
this.emitter.emit("auth.complete");
this.logger.log("successfully validated challenge", this.challenge);
}
} catch (err) {
console.log("Error validate()", err);
// if challenge isn't on chain - print info to the console
this.logger.error("failed to validate challenge", err);
}
}

/**
* This method stops scheduler cronJob,
* clears timeout as scheduler has been stopped.
*
* @returns {void} Stops cronJob, clears challenge, clears timeout.
*/
protected stopCronJob() {
this.job.stop();
this.challenge = "";
clearTimeout(this.stopCronJobTimeout);
}

/**
* This method starts scheduler cronJob,
* sets received challenge and sets cronJob timeout.
*
* @returns {void} Starts cronJob, sets challenge, sets timeout.
*/
public startCronJob(challenge: string) {
this.challenge = challenge;

Expand Down
Loading

0 comments on commit 1979cc6

Please sign in to comment.