From 9b08ee4fed80870d1f471ef9ec79d63c94bb6e43 Mon Sep 17 00:00:00 2001 From: Andrew Besedin Date: Thu, 19 Feb 2026 01:22:32 +0300 Subject: [PATCH 1/5] add: add auth session --- shared/constants.ts | 2 + src/controllers/auth.controller.ts | 35 ++++++++++++ src/interfaces/bodies/auth/RequestAuthBody.ts | 13 +++++ .../responses/auth/RequestAuthRes.ts | 7 +++ src/models/AuthMessages.ts | 53 +++++++++++++++++++ src/routes/auth.router.ts | 8 +++ src/schemes/AuthMessage.ts | 46 ++++++++++++++++ 7 files changed, 164 insertions(+) create mode 100644 src/interfaces/bodies/auth/RequestAuthBody.ts create mode 100644 src/interfaces/responses/auth/RequestAuthRes.ts create mode 100644 src/models/AuthMessages.ts create mode 100644 src/schemes/AuthMessage.ts diff --git a/shared/constants.ts b/shared/constants.ts index 204f551..ae8726f 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -1 +1,3 @@ export const NON_NEGATIVE_REAL_NUMBER_REGEX = /^\d+(\.\d+)?$/; + +export const AUTH_MESSAGE_EXPIRATION_TIME_MS = 5 * 60 * 1000; // 5 minutes diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 20dacc9..80e17e8 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,13 +1,38 @@ import jwt from 'jsonwebtoken'; import dotenv from 'dotenv'; import { Request, Response } from 'express'; +import crypto from 'crypto'; + import AuthData from '@/interfaces/bodies/user/AuthData.js'; +import RequestAuthBody from '@/interfaces/bodies/auth/RequestAuthBody.js'; +import authMessagesModel from '@/models/AuthMessages.js'; +import { AUTH_MESSAGE_EXPIRATION_TIME_MS } from 'shared/constants.js'; +import RequestAuthRes from '@/interfaces/responses/auth/RequestAuthRes.js'; import validateWallet from '../methods/validateWallet.js'; import userModel from '../models/User.js'; dotenv.config(); class AuthController { + requestAuth = async (req: Request, res: Response) => { + const { address, alias } = req.body as RequestAuthBody; + + const message = crypto.randomUUID(); + const expiresAt = new Date(Date.now() + AUTH_MESSAGE_EXPIRATION_TIME_MS); + + const authMessageRow = await authMessagesModel.create({ + address, + alias, + message, + expiresAt, + }); + + return res.status(200).send({ + success: true, + data: authMessageRow.message, + }); + }; + async auth(req: Request, res: Response) { try { const userData: AuthData = req.body.data; @@ -18,6 +43,16 @@ class AuthController { return res.status(400).send({ success: false, data: 'Invalid auth data' }); } + const authMessageRow = await authMessagesModel.findOne({ + address, + alias, + message, + }); + + if (!authMessageRow) { + return res.status(400).send({ success: false, data: 'Invalid auth message' }); + } + const dataValid = !!( userData && userData.address && diff --git a/src/interfaces/bodies/auth/RequestAuthBody.ts b/src/interfaces/bodies/auth/RequestAuthBody.ts new file mode 100644 index 0000000..6f1d368 --- /dev/null +++ b/src/interfaces/bodies/auth/RequestAuthBody.ts @@ -0,0 +1,13 @@ +import { body } from 'express-validator'; + +interface RequestAuthBody { + address: string; + alias: string; +} + +export const requestAuthBodyValidator = [ + body('address').isString().notEmpty(), + body('alias').isString().notEmpty(), +]; + +export default RequestAuthBody; diff --git a/src/interfaces/responses/auth/RequestAuthRes.ts b/src/interfaces/responses/auth/RequestAuthRes.ts new file mode 100644 index 0000000..a200f5f --- /dev/null +++ b/src/interfaces/responses/auth/RequestAuthRes.ts @@ -0,0 +1,7 @@ +interface RequestAuthRes { + success: true; + // Auth message + data: string; +} + +export default RequestAuthRes; diff --git a/src/models/AuthMessages.ts b/src/models/AuthMessages.ts new file mode 100644 index 0000000..7326da9 --- /dev/null +++ b/src/models/AuthMessages.ts @@ -0,0 +1,53 @@ +import { Transaction } from 'sequelize'; + +import AuthMessage from '@/schemes/AuthMessage'; + +class AuthMessagesModel { + create = async ( + { + address, + alias, + message, + expiresAt, + }: { + address: string; + alias: string; + message: string; + expiresAt: Date; + }, + { transaction }: { transaction?: Transaction } = {}, + ): Promise => { + const authMessage = await AuthMessage.create( + { + address, + alias, + message, + expiresAt, + }, + { transaction }, + ); + + return authMessage; + }; + + findOne = async ({ + address, + alias, + message, + }: { + address: string; + alias: string; + message: string; + }): Promise => + AuthMessage.findOne({ + where: { + address, + alias, + message, + }, + }); +} + +const authMessagesModel = new AuthMessagesModel(); + +export default authMessagesModel; diff --git a/src/routes/auth.router.ts b/src/routes/auth.router.ts index 77dfc5b..43f6af4 100644 --- a/src/routes/auth.router.ts +++ b/src/routes/auth.router.ts @@ -1,8 +1,16 @@ import express from 'express'; + +import middleware from '@/middleware/middleware.js'; +import { requestAuthBodyValidator } from '@/interfaces/bodies/auth/RequestAuthBody.js'; import authController from '../controllers/auth.controller.js'; const authRouter = express.Router(); +authRouter.post( + '/request-auth', + middleware.expressValidator(requestAuthBodyValidator), + authController.requestAuth.bind(authController), +); authRouter.post('/auth', authController.auth); export default authRouter; diff --git a/src/schemes/AuthMessage.ts b/src/schemes/AuthMessage.ts new file mode 100644 index 0000000..01e9725 --- /dev/null +++ b/src/schemes/AuthMessage.ts @@ -0,0 +1,46 @@ +import sequelize from '@/sequelize'; +import { DataTypes, Model } from 'sequelize'; + +class AuthMessage extends Model { + declare readonly id: number; + declare address: string; + declare alias: string; + declare message: string; + declare expiresAt: Date; + + declare readonly createdAt: Date; + declare readonly updatedAt: Date; +} + +AuthMessage.init( + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + address: { + type: DataTypes.STRING, + allowNull: false, + }, + alias: { + type: DataTypes.STRING, + allowNull: false, + }, + message: { + type: DataTypes.STRING, + allowNull: false, + }, + expiresAt: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + { + sequelize, + modelName: 'AuthMessage', + tableName: 'auth_messages', + }, +); + +export default AuthMessage; From 1493a2512c07d6fcc0e88968f9fc4bab0eff598b Mon Sep 17 00:00:00 2001 From: Andrew Besedin Date: Thu, 19 Feb 2026 12:03:02 +0300 Subject: [PATCH 2/5] update: add AuthMessage item delete on successful auth --- src/controllers/auth.controller.ts | 35 ++++++++++++++++++++++-------- src/models/AuthMessages.ts | 22 +++++++++++++++++++ src/models/User.ts | 11 ++++++---- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 80e17e8..5c06917 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -8,6 +8,7 @@ import RequestAuthBody from '@/interfaces/bodies/auth/RequestAuthBody.js'; import authMessagesModel from '@/models/AuthMessages.js'; import { AUTH_MESSAGE_EXPIRATION_TIME_MS } from 'shared/constants.js'; import RequestAuthRes from '@/interfaces/responses/auth/RequestAuthRes.js'; +import sequelize from '@/sequelize.js'; import validateWallet from '../methods/validateWallet.js'; import userModel from '../models/User.js'; @@ -63,17 +64,33 @@ class AuthController { return res.status(400).send({ success: false, data: 'Invalid auth data' }); } - const success = await userModel.add(userData); + let token: string | undefined; - if (success) { - const token = jwt.sign( - { ...userData }, - process.env.JWT_SECRET || '', - neverExpires ? undefined : { expiresIn: '24h' }, - ); - res.status(200).send({ success, data: token }); + await sequelize.transaction(async (transaction) => { + const success = await userModel.add(userData, { transaction }); + + if (success) { + await authMessagesModel.deleteOne( + { + address, + alias, + message, + }, + { transaction }, + ); + + token = jwt.sign( + { ...userData }, + process.env.JWT_SECRET || '', + neverExpires ? undefined : { expiresIn: '24h' }, + ); + } + }); + + if (token !== undefined) { + res.status(200).send({ success: true, data: token }); } else { - res.status(500).send({ success, data: 'Internal error' }); + res.status(500).send({ success: false, data: 'Internal error' }); } } catch (err) { console.log(err); diff --git a/src/models/AuthMessages.ts b/src/models/AuthMessages.ts index 7326da9..d105126 100644 --- a/src/models/AuthMessages.ts +++ b/src/models/AuthMessages.ts @@ -46,6 +46,28 @@ class AuthMessagesModel { message, }, }); + + deleteOne = async ( + { + address, + alias, + message, + }: { + address: string; + alias: string; + message: string; + }, + { transaction }: { transaction?: Transaction } = {}, + ): Promise => { + await AuthMessage.destroy({ + where: { + address, + alias, + message, + }, + transaction, + }); + }; } const authMessagesModel = new AuthMessagesModel(); diff --git a/src/models/User.ts b/src/models/User.ts index 999ab65..83135fc 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,4 +1,4 @@ -import { Op } from 'sequelize'; +import { Op, Transaction } from 'sequelize'; import Offer from '../schemes/Offer'; import GetUserBody from '../interfaces/bodies/user/GetUserBody'; import SetFavouriteCurrsBody from '../interfaces/bodies/user/SetFavouriteCurrsBody'; @@ -15,7 +15,7 @@ class UserModel { return selected; } - async add(userData: UserData) { + async add(userData: UserData, { transaction }: { transaction?: Transaction } = {}) { try { const userRow = await this.getUserRow(userData.address); if (userRow) return true; @@ -27,12 +27,15 @@ class UserModel { if (oldAddressOfCurrentAlias) { await User.update( { address: userData.address }, - { where: { alias: userData.alias } }, + { where: { alias: userData.alias }, transaction }, ); return true; } - await User.create({ alias: userData.alias, address: userData.address }); + await User.create( + { alias: userData.alias, address: userData.address }, + { transaction }, + ); return true; } catch (err) { From d7fe71069025f8cb7c7b059f7007ab890ccec26e Mon Sep 17 00:00:00 2001 From: Andrew Besedin Date: Thu, 19 Feb 2026 12:13:39 +0300 Subject: [PATCH 3/5] update: update request-auth endpoint route --- src/routes/auth.router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/auth.router.ts b/src/routes/auth.router.ts index 43f6af4..fd7d8ca 100644 --- a/src/routes/auth.router.ts +++ b/src/routes/auth.router.ts @@ -7,7 +7,7 @@ import authController from '../controllers/auth.controller.js'; const authRouter = express.Router(); authRouter.post( - '/request-auth', + '/auth/request-auth', middleware.expressValidator(requestAuthBodyValidator), authController.requestAuth.bind(authController), ); From 094cc327d7d8227ada018b6024680d3416695aac Mon Sep 17 00:00:00 2001 From: Andrew Besedin Date: Thu, 19 Feb 2026 13:14:03 +0300 Subject: [PATCH 4/5] update: add auth message expiration validation --- src/controllers/auth.controller.ts | 5 ++++- src/models/AuthMessages.ts | 2 +- src/schemes/AuthMessage.ts | 5 ++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 5c06917..dfec3ee 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -50,7 +50,10 @@ class AuthController { message, }); - if (!authMessageRow) { + const isAuthMessageValid = + !!authMessageRow && authMessageRow.expires_at.getTime() > Date.now(); + + if (!isAuthMessageValid) { return res.status(400).send({ success: false, data: 'Invalid auth message' }); } diff --git a/src/models/AuthMessages.ts b/src/models/AuthMessages.ts index d105126..a4b4922 100644 --- a/src/models/AuthMessages.ts +++ b/src/models/AuthMessages.ts @@ -22,7 +22,7 @@ class AuthMessagesModel { address, alias, message, - expiresAt, + expires_at: expiresAt, }, { transaction }, ); diff --git a/src/schemes/AuthMessage.ts b/src/schemes/AuthMessage.ts index 01e9725..ef8ab38 100644 --- a/src/schemes/AuthMessage.ts +++ b/src/schemes/AuthMessage.ts @@ -6,7 +6,7 @@ class AuthMessage extends Model { declare address: string; declare alias: string; declare message: string; - declare expiresAt: Date; + declare expires_at: Date; declare readonly createdAt: Date; declare readonly updatedAt: Date; @@ -31,7 +31,7 @@ AuthMessage.init( type: DataTypes.STRING, allowNull: false, }, - expiresAt: { + expires_at: { type: DataTypes.DATE, allowNull: false, }, @@ -39,7 +39,6 @@ AuthMessage.init( { sequelize, modelName: 'AuthMessage', - tableName: 'auth_messages', }, ); From 2ea7db5154915f3e29a18d917a338a8db090daa7 Mon Sep 17 00:00:00 2001 From: Andrew Besedin Date: Thu, 19 Feb 2026 13:42:54 +0300 Subject: [PATCH 5/5] update: add auth messages clean service for periodic expired auth messages clean --- src/models/AuthMessages.ts | 16 +++++++++- src/server.ts | 2 ++ src/workers/authMessagesCleanService.ts | 41 +++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/workers/authMessagesCleanService.ts diff --git a/src/models/AuthMessages.ts b/src/models/AuthMessages.ts index a4b4922..0cbee63 100644 --- a/src/models/AuthMessages.ts +++ b/src/models/AuthMessages.ts @@ -1,4 +1,4 @@ -import { Transaction } from 'sequelize'; +import { Op, Transaction } from 'sequelize'; import AuthMessage from '@/schemes/AuthMessage'; @@ -68,6 +68,20 @@ class AuthMessagesModel { transaction, }); }; + + deleteAllExpired = async ( + { now }: { now: Date }, + { transaction }: { transaction?: Transaction } = {}, + ): Promise => { + await AuthMessage.destroy({ + where: { + expires_at: { + [Op.lt]: now, + }, + }, + transaction, + }); + }; } const authMessagesModel = new AuthMessagesModel(); diff --git a/src/server.ts b/src/server.ts index 4c20936..2c5ba0e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,6 +3,7 @@ import express from 'express'; import http from 'http'; import { Server } from 'socket.io'; +import authMessagesCleanService from '@/workers/authMessagesCleanService'; import authRouter from './routes/auth.router'; import offersRouter from './routes/offers.router'; import userRouter from './routes/user.router'; @@ -73,6 +74,7 @@ process.on('unhandledRejection', (reason, promise) => { assetsUpdateChecker.run(); ordersModerationService.run(); + authMessagesCleanService.run(); exchangeModel.runPairStatsDaemon(); statsModel.init(); diff --git a/src/workers/authMessagesCleanService.ts b/src/workers/authMessagesCleanService.ts new file mode 100644 index 0000000..aa26c9a --- /dev/null +++ b/src/workers/authMessagesCleanService.ts @@ -0,0 +1,41 @@ +import authMessagesModel from '@/models/AuthMessages'; + +const CLEAN_INTERVAL = 60 * 60 * 1000; // 1 hour + +class AuthMessagesCleanService { + run = async () => { + console.log( + `Auth messages clean service is running. Cleaning interval: ${CLEAN_INTERVAL / 1000} sec.`, + ); + + async function clean() { + console.log(`[${new Date()}] Cleaning auth messages...`); + + await authMessagesModel.deleteAllExpired({ now: new Date() }); + + console.log(`[${new Date()}] Auth messages cleaned.`); + } + + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await clean(); + } catch (error) { + console.log( + `[${new Date()}] Error while cleaning auth messages. Continuing on next iteration. Error:`, + ); + console.error(error); + } + + console.log( + `[${new Date()}] Auth messages cleaned. Next cleaning in ${CLEAN_INTERVAL / 1000} sec.`, + ); + + await new Promise((resolve) => setTimeout(resolve, CLEAN_INTERVAL)); + } + }; +} + +const authMessagesCleanService = new AuthMessagesCleanService(); + +export default authMessagesCleanService;