diff --git a/runtime/backend/src/common/gateways/AuthGateway.ts b/runtime/backend/src/common/gateways/AuthGateway.ts
index 784bf1fb..23e7bd5a 100644
--- a/runtime/backend/src/common/gateways/AuthGateway.ts
+++ b/runtime/backend/src/common/gateways/AuthGateway.ts
@@ -16,10 +16,27 @@ 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.
+ *
+ * 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,
@@ -27,19 +44,39 @@ export class AuthGateway
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");
}
}
diff --git a/runtime/backend/src/common/gateways/BaseGateway.ts b/runtime/backend/src/common/gateways/BaseGateway.ts
index 435858ff..26016440 100644
--- a/runtime/backend/src/common/gateways/BaseGateway.ts
+++ b/runtime/backend/src/common/gateways/BaseGateway.ts
@@ -15,12 +15,13 @@ 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";
@@ -28,6 +29,17 @@ 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.
+ *
+ * This class can be used by extending it and adding different
+ * @SubscribeMessage handlers.
+ *
+ * @since v0.2.0
+ */
@WebSocketGateway(80, {
path: "/ws",
cors: {
@@ -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.
+ *
+ * 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} 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);
@@ -57,6 +120,13 @@ 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
@@ -64,11 +134,21 @@ export abstract class BaseGateway
// 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.
+ *
+ * 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);
@@ -76,6 +156,12 @@ export abstract class BaseGateway
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");
}
diff --git a/runtime/backend/src/common/schedulers/ValidateChallengeScheduler.ts b/runtime/backend/src/common/schedulers/ValidateChallengeScheduler.ts
index 86e11a08..030a7b6e 100644
--- a/runtime/backend/src/common/schedulers/ValidateChallengeScheduler.ts
+++ b/runtime/backend/src/common/schedulers/ValidateChallengeScheduler.ts
@@ -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
@@ -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;
diff --git a/runtime/backend/src/common/services/AuthService.ts b/runtime/backend/src/common/services/AuthService.ts
index 881f32fb..ec64f478 100644
--- a/runtime/backend/src/common/services/AuthService.ts
+++ b/runtime/backend/src/common/services/AuthService.ts
@@ -219,7 +219,6 @@ export class AuthService {
private readonly accountSessionsService: AccountSessionsService,
private readonly challengesService: ChallengesService,
private jwtService: JwtService,
- protected readonly authGateWay: AuthGateway,
) {
const name = this.configService.get("dappName");
const domain = this.configService.get("frontendApp.host");
@@ -303,15 +302,19 @@ export class AuthService {
* parameter **has been found** in a recent transfer transaction's message,
* a document will be *insert* in the collection `authChallenges`.
*
- * @param {string} challenge An authentication challenge, as created with {@link getChallenge}.
+ * @param {AccessTokenRequest} param0 An authentication challenge, as created with {@link getChallenge}.
+ * @param {boolean} enableStorage Flag that defines if challenge should be stored in database for the case when need to check validity of challenge. Defaults to true.
* @returns {Promise} An authenticated account session described with {@link AuthenticationPayload}.
* @throws {HttpException} Given challenge could not be found in recent transactions.
*/
- public async validateChallenge({
- challenge,
- sub,
- registry,
- }: AccessTokenRequest): Promise {
+ public async validateChallenge(
+ {
+ challenge,
+ sub,
+ registry,
+ }: AccessTokenRequest,
+ enableStorage: boolean = true,
+ ): Promise {
// does not permit multiple usage of challenges
const challengeUsed: boolean = await this.challengesService.exists(
new AuthChallengeQuery({
@@ -350,19 +353,21 @@ export class AuthService {
const logger = new LogService(AppConfiguration.dappName);
logger.log(`Authorizing log-in challenge for "${authorizedUser.address}"`);
- // stores a validated authentication challenge
- // in the database collection `authChallenges`
- await this.challengesService.createOrUpdate(
- new AuthChallengeQuery({
- challenge,
- } as AuthChallengeDocument),
- {
- usedBy: authorizedAddr.plain(),
- usedAt: new Date().valueOf(),
- },
- );
+ if (enableStorage === true) {
+ // stores a validated authentication challenge
+ // in the database collection `authChallenges`
+ await this.challengesService.createOrUpdate(
+ new AuthChallengeQuery({
+ challenge: challenge,
+ } as AuthChallengeDocument),
+ {
+ usedBy: authorizedAddr.plain(),
+ usedAt: new Date().valueOf(),
+ },
+ );
+ }
- this.authGateWay.server.emit("auth.complete");
+ // this.authGateWay.server.emit("auth.complete");
// returns the authorized user details
return authorizedUser;
diff --git a/runtime/dapp-frontend-vue/src/views/LoginScreen/LoginScreen.ts b/runtime/dapp-frontend-vue/src/views/LoginScreen/LoginScreen.ts
index be5bb409..20c54a73 100644
--- a/runtime/dapp-frontend-vue/src/views/LoginScreen/LoginScreen.ts
+++ b/runtime/dapp-frontend-vue/src/views/LoginScreen/LoginScreen.ts
@@ -276,6 +276,14 @@ export default class LoginScreen extends MetaView {
);
}
+ /**
+ * This property contains
+ * client side websocket connection
+ * which is getting initialized on mounted() hook.
+ *
+ * @access public
+ * @returns {any}
+ */
public wsConnection: any = null;
/**
@@ -328,15 +336,11 @@ export default class LoginScreen extends MetaView {
const handler = this.fetchToken;
this.wsConnection.onmessage = function (evt: any) {
- if (evt.data === "auth.open") {
+ if (evt.data === "auth.complete") {
handler();
}
};
- // this.wsConnection.emit("auth.open", { data: "test msg" }, (res: any) => {
- // console.log({ res });
- // });
-
try {
// make sure referral code is saved
if (this.$route.params.refCode) {
@@ -390,6 +394,7 @@ export default class LoginScreen extends MetaView {
clearTimeout(this.globalIntervalTimer);
}
+ // close connection when route left
this.wsConnection.close();
}