Merge pull request #22 from hyle-team/feature/add-auth-session
feat: add auth session
This commit is contained in:
commit
8cd3b54b16
10 changed files with 278 additions and 13 deletions
|
|
@ -1 +1,3 @@
|
|||
export const NON_NEGATIVE_REAL_NUMBER_REGEX = /^\d+(\.\d+)?$/;
|
||||
|
||||
export const AUTH_MESSAGE_EXPIRATION_TIME_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
13
src/interfaces/bodies/auth/RequestAuthBody.ts
Normal file
13
src/interfaces/bodies/auth/RequestAuthBody.ts
Normal 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;
|
||||
7
src/interfaces/responses/auth/RequestAuthRes.ts
Normal file
7
src/interfaces/responses/auth/RequestAuthRes.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
interface RequestAuthRes {
|
||||
success: true;
|
||||
// Auth message
|
||||
data: string;
|
||||
}
|
||||
|
||||
export default RequestAuthRes;
|
||||
89
src/models/AuthMessages.ts
Normal file
89
src/models/AuthMessages.ts
Normal 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;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
45
src/schemes/AuthMessage.ts
Normal file
45
src/schemes/AuthMessage.ts
Normal 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;
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
41
src/workers/authMessagesCleanService.ts
Normal file
41
src/workers/authMessagesCleanService.ts
Normal 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;
|
||||
Loading…
Add table
Reference in a new issue