Initial SMM panel implementation
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const { getDb } = require('../config/db');
|
||||
const { AppError } = require('../middleware/error-handler');
|
||||
|
||||
const ACCESS_TOKEN_TTL = '15m';
|
||||
const REFRESH_TOKEN_BYTES = 48;
|
||||
const RESET_TOKEN_BYTES = 32;
|
||||
const RESET_TOKEN_TTL_MINUTES = 30;
|
||||
|
||||
function getJwtSecret() {
|
||||
return process.env.JWT_SECRET || 'development_only_change_me';
|
||||
}
|
||||
|
||||
function hashToken(token) {
|
||||
return crypto.createHash('sha256').update(token).digest('hex');
|
||||
}
|
||||
|
||||
function addMinutes(date, minutes) {
|
||||
return new Date(date.getTime() + minutes * 60 * 1000);
|
||||
}
|
||||
|
||||
function addDays(date, days) {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
function sanitizeUser(user) {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
status: user.status,
|
||||
balance: user.balance
|
||||
};
|
||||
}
|
||||
|
||||
function createAccessToken(user) {
|
||||
return jwt.sign(
|
||||
{
|
||||
user_id: user.id,
|
||||
role: user.role
|
||||
},
|
||||
getJwtSecret(),
|
||||
{
|
||||
expiresIn: ACCESS_TOKEN_TTL
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function createRefreshToken(userId, trx = getDb()) {
|
||||
const refreshToken = crypto.randomBytes(REFRESH_TOKEN_BYTES).toString('base64url');
|
||||
const tokenHash = hashToken(refreshToken);
|
||||
|
||||
await trx('refresh_tokens').insert({
|
||||
user_id: userId,
|
||||
token_hash: tokenHash,
|
||||
expires_at: addDays(new Date(), 30)
|
||||
});
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
async function register(input) {
|
||||
const db = getDb();
|
||||
const passwordHash = await bcrypt.hash(input.password, 12);
|
||||
|
||||
try {
|
||||
const [userId] = await db('users').insert({
|
||||
username: input.username,
|
||||
email: input.email,
|
||||
password_hash: passwordHash,
|
||||
balance: '0.0000'
|
||||
});
|
||||
|
||||
const user = await db('users').where({ id: userId }).first();
|
||||
|
||||
return {
|
||||
user: sanitizeUser(user)
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_ENTRY') {
|
||||
throw new AppError('Username or email is already in use', 409, 'ACCOUNT_EXISTS');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function login(input) {
|
||||
const db = getDb();
|
||||
const genericError = new AppError('Invalid email or password', 401, 'INVALID_CREDENTIALS');
|
||||
const user = await db('users').where({ email: input.email }).first();
|
||||
|
||||
if (!user) {
|
||||
throw genericError;
|
||||
}
|
||||
|
||||
const passwordMatches = await bcrypt.compare(input.password, user.password_hash);
|
||||
|
||||
if (!passwordMatches) {
|
||||
throw genericError;
|
||||
}
|
||||
|
||||
if (user.status !== 'active') {
|
||||
throw new AppError('Account is not active', 403, 'ACCOUNT_INACTIVE');
|
||||
}
|
||||
|
||||
const accessToken = createAccessToken(user);
|
||||
const refreshToken = await createRefreshToken(user.id);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 15 * 60,
|
||||
user: sanitizeUser(user)
|
||||
};
|
||||
}
|
||||
|
||||
async function logout(refreshToken) {
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
await db('refresh_tokens')
|
||||
.where({ token_hash: hashToken(refreshToken) })
|
||||
.whereNull('revoked_at')
|
||||
.update({ revoked_at: new Date() });
|
||||
}
|
||||
|
||||
async function forgotPassword(email) {
|
||||
const db = getDb();
|
||||
const user = await db('users').select('id').where({ email }).first();
|
||||
const response = {
|
||||
status: 'ok',
|
||||
message: 'If the account exists, a reset link will be sent'
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(RESET_TOKEN_BYTES).toString('base64url');
|
||||
|
||||
await db('password_reset_tokens').insert({
|
||||
user_id: user.id,
|
||||
token_hash: hashToken(token),
|
||||
expires_at: addMinutes(new Date(), RESET_TOKEN_TTL_MINUTES)
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
response.resetToken = token;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function resetPassword(input) {
|
||||
const db = getDb();
|
||||
const tokenHash = hashToken(input.token);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const resetToken = await trx('password_reset_tokens')
|
||||
.where({ token_hash: tokenHash })
|
||||
.whereNull('used_at')
|
||||
.where('expires_at', '>', new Date())
|
||||
.forUpdate()
|
||||
.first();
|
||||
|
||||
if (!resetToken) {
|
||||
throw new AppError('Invalid or expired reset token', 400, 'INVALID_RESET_TOKEN');
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(input.password, 12);
|
||||
|
||||
await trx('users').where({ id: resetToken.user_id }).update({
|
||||
password_hash: passwordHash
|
||||
});
|
||||
|
||||
await trx('password_reset_tokens').where({ id: resetToken.id }).update({
|
||||
used_at: new Date()
|
||||
});
|
||||
|
||||
await trx('refresh_tokens')
|
||||
.where({ user_id: resetToken.user_id })
|
||||
.whereNull('revoked_at')
|
||||
.update({ revoked_at: new Date() });
|
||||
});
|
||||
}
|
||||
|
||||
async function getMe(userId) {
|
||||
const db = getDb();
|
||||
const user = await db('users')
|
||||
.select('id', 'username', 'email', 'role', 'status', 'balance')
|
||||
.where({ id: userId })
|
||||
.first();
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404, 'USER_NOT_FOUND');
|
||||
}
|
||||
|
||||
return {
|
||||
user: sanitizeUser(user)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
forgotPassword,
|
||||
getMe,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
resetPassword
|
||||
};
|
||||
Reference in New Issue
Block a user