Merge pull request #22 from hyle-team/feature/add-auth-session

feat: add auth session
This commit is contained in:
Dmitrii Kolpakov 2026-02-20 06:02:11 +07:00 committed by GitHub
commit 8cd3b54b16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 278 additions and 13 deletions

View file

@ -1 +1,3 @@
export const NON_NEGATIVE_REAL_NUMBER_REGEX = /^\d+(\.\d+)?$/;
export const AUTH_MESSAGE_EXPIRATION_TIME_MS = 5 * 60 * 1000; // 5 minutes

View file

@ -1,13 +1,39 @@
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 sequelize from '@/sequelize.js';
import validateWallet from '../methods/validateWallet.js';
import userModel from '../models/User.js';
dotenv.config();
class AuthController {
requestAuth = async (req: Request, res: Response<RequestAuthRes>) => {
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 +44,19 @@ class AuthController {
return res.status(400).send({ success: false, data: 'Invalid auth data' });
}
const authMessageRow = await authMessagesModel.findOne({
address,
alias,
message,
});
const isAuthMessageValid =
!!authMessageRow && authMessageRow.expires_at.getTime() > Date.now();
if (!isAuthMessageValid) {
return res.status(400).send({ success: false, data: 'Invalid auth message' });
}
const dataValid = !!(
userData &&
userData.address &&
@ -28,17 +67,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);

View file

@ -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;

View file

@ -0,0 +1,7 @@
interface RequestAuthRes {
success: true;
// Auth message
data: string;
}
export default RequestAuthRes;

View file

@ -0,0 +1,89 @@
import { Op, 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<AuthMessage> => {
const authMessage = await AuthMessage.create(
{
address,
alias,
message,
expires_at: expiresAt,
},
{ transaction },
);
return authMessage;
};
findOne = async ({
address,
alias,
message,
}: {
address: string;
alias: string;
message: string;
}): Promise<AuthMessage | null> =>
AuthMessage.findOne({
where: {
address,
alias,
message,
},
});
deleteOne = async (
{
address,
alias,
message,
}: {
address: string;
alias: string;
message: string;
},
{ transaction }: { transaction?: Transaction } = {},
): Promise<void> => {
await AuthMessage.destroy({
where: {
address,
alias,
message,
},
transaction,
});
};
deleteAllExpired = async (
{ now }: { now: Date },
{ transaction }: { transaction?: Transaction } = {},
): Promise<void> => {
await AuthMessage.destroy({
where: {
expires_at: {
[Op.lt]: now,
},
},
transaction,
});
};
}
const authMessagesModel = new AuthMessagesModel();
export default authMessagesModel;

View file

@ -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) {

View file

@ -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(
'/auth/request-auth',
middleware.expressValidator(requestAuthBodyValidator),
authController.requestAuth.bind(authController),
);
authRouter.post('/auth', authController.auth);
export default authRouter;

View file

@ -0,0 +1,45 @@
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 expires_at: 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,
},
expires_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
sequelize,
modelName: 'AuthMessage',
},
);
export default AuthMessage;

View file

@ -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();

View file

@ -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;