/
TimurIsm
/
OpenEyes
Обзор
Документация
Войти
/
TimurIsm
/
OpenEyes
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
back/controllers/userController.js
496 строк
21 KB
TimurIsm
first_commit
16 май 2026, 15:34
16 май 2026, 15:34
4d3f198
Код
Авторство
О чём код?
const ApiError = require('../error/ApiError'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { User } = require('../models/models'); const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail, sendPasswordResetConfirmationEmail, sendAccountDeletionEmail, sendAccountDeletionConfirmationEmail, sendPasswordChangeEmail } = require('../utils/emailService'); const { TOKEN_TYPES, createTokenObject, isValidToken, isTokenType, removeToken, clearAllTokens} = require('../utils/tokenManager'); const LOGIN_DELAY = 2000; const generateJwt = (id, email, role, isVerified) => { return jwt.sign( { id, email, role, isVerified }, process.env.SECRET_KEY, { expiresIn: '24h' } ); }; const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); class UserController { async registration(req, res, next) { try { const { email, password } = req.body; const role = req.body.role || 'USER'; if (!password || password.length < 6 || password.length > 30) { return next(ApiError.badRequest('Пароль должен быть от 6 до 30 символов!')); } if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/.test(password)) { return next(ApiError.badRequest('Пароль должен содержать хотя бы одну заглавную букву, одну строчную букву, одну цифру и один специальный символ!')); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!email || !emailRegex.test(email)) { return next(ApiError.badRequest('Некорректный email!')); } const existingUser = await User.findOne({ where: { email } }); if (existingUser) { if (existingUser.isVerified) { return next(ApiError.badRequest('Пользователь с таким email уже существует!')); } else { await existingUser.destroy(); } } const hashPassword = await bcrypt.hash(password, 5); const user = await User.create({ email, role, password: hashPassword, isVerified: false, tokens: {} }); const emailToken = createTokenObject(TOKEN_TYPES.EMAIL_VERIFICATION); user.tokens = { [TOKEN_TYPES.EMAIL_VERIFICATION]: emailToken }; await user.save(); try { await sendVerificationEmail(email, emailToken.token, user.id); } catch (emailError) { await user.destroy(); return next(ApiError.internal('Не удалось отправить письмо подтверждения! Попробуйте позже.')); } return res.status(200).json({ success: true, message: 'Письмо с подтверждением отправлено на ваш email. Проверьте почту для завершения регистрации.', requiresVerification: true }); } catch (error) { return next(ApiError.internal('Ошибка при регистрации!')); } } async verifyEmail(req, res, next) { try { const { token, userId } = req.query; if (!token || !userId) { return next(ApiError.badRequest('Неверная ссылка подтверждения!')); } if (!isTokenType(token, TOKEN_TYPES.EMAIL_VERIFICATION)) { return next(ApiError.badRequest('Неверный тип токена для подтверждения email!')); } const user = await User.findByPk(userId); if (!user) { return next(ApiError.badRequest('Пользователь не найден!')); } const tokenData = user.tokens?.[TOKEN_TYPES.EMAIL_VERIFICATION]; if (!isValidToken(tokenData, token, TOKEN_TYPES.EMAIL_VERIFICATION)) { if (tokenData && new Date(tokenData.expires) < new Date()) { await removeToken(user, TOKEN_TYPES.EMAIL_VERIFICATION); return next(ApiError.badRequest('Срок действия ссылки истек! Зарегистрируйтесь снова.')); } return next(ApiError.badRequest('Неверная или устаревшая ссылка подтверждения!')); } user.isVerified = true; await removeToken(user, TOKEN_TYPES.EMAIL_VERIFICATION); try { await sendWelcomeEmail(user.email); } catch (emailError) { console.warn('Не удалось отправить приветственное письмо:', emailError.message); } const jwtToken = generateJwt(user.id, user.email, user.role, user.isVerified); return res.status(200).json({ success: true, message: 'Email успешно подтвержден!', token: jwtToken, user: { id: user.id, email: user.email, role: user.role, isVerified: user.isVerified, createdAt: user.createdAt } }); } catch (error) { return next(ApiError.internal('Ошибка при подтверждении email!')); } } async resendVerificationEmail(req, res, next) { try { const { email } = req.body; if (!email) { return next(ApiError.badRequest('Email обязателен!')); } const user = await User.findOne({ where: { email, isVerified: false } }); if (!user) { return res.status(200).json({ success: true, message: 'Если пользователь существует и не подтвержден, письмо будет отправлено.' }); } const newEmailToken = createTokenObject(TOKEN_TYPES.EMAIL_VERIFICATION); user.tokens = { ...user.tokens, [TOKEN_TYPES.EMAIL_VERIFICATION]: newEmailToken }; await user.save(); await sendVerificationEmail(email, newEmailToken.token, user.id); return res.status(200).json({ success: true, message: 'Письмо с подтверждением отправлено повторно.' }); } catch (error) { return next(ApiError.internal('Ошибка при повторной отправке письма!')); } } async login(req, res, next) { try { const { email, password } = req.body; const user = await User.findOne({ where: { email } }); const isValidUser = user && bcrypt.compareSync(password, user.password); if (!isValidUser) { await delay(LOGIN_DELAY); return next(ApiError.internal('Указан неверный email или пароль!')); } if (!user.isVerified) { await delay(LOGIN_DELAY); return next(ApiError.forbidden('Пожалуйста, подтвердите ваш email перед входом.')); } await clearAllTokens(user); const token = generateJwt(user.id, user.email, user.role, user.isVerified); return res.json({ token, user: { id: user.id, email: user.email, role: user.role, isVerified: user.isVerified, createdAt: user.createdAt } }); } catch (error) { await delay(LOGIN_DELAY); return next(ApiError.internal('Ошибка при входе!')); } } async check(req, res, next) { try { const user = await User.findByPk(req.user.id); if (!user || !user.isVerified) { return next(ApiError.unauthorized('Пользователь не найден или не подтвержден!')); } await clearAllTokens(user); const token = generateJwt(user.id, user.email, user.role, user.isVerified); return res.json({ token, user: { id: user.id, email: user.email, role: user.role, isVerified: user.isVerified, createdAt: user.createdAt } }); } catch (error) { return next(ApiError.internal('Ошибка проверки авторизации!')); } } async requestPasswordReset(req, res, next) { try { const { email } = req.body; if (!email) { return next(ApiError.badRequest('Email обязателен!')); } const user = await User.findOne({ where: { email, isVerified: true } }); if (!user) { await delay(500); return res.status(200).json({ success: true, message: 'Если пользователь с таким email существует и подтвержден, инструкции по сбросу пароля будут отправлены.' }); } const resetToken = createTokenObject(TOKEN_TYPES.PASSWORD_RESET); user.tokens = { ...user.tokens, [TOKEN_TYPES.PASSWORD_RESET]: resetToken }; await user.save(); try { await sendPasswordResetEmail(email, resetToken.token, user.id); } catch (emailError) { await removeToken(user, TOKEN_TYPES.PASSWORD_RESET); return next(ApiError.internal('Не удалось отправить письмо для сброса пароля. Попробуйте позже.')); } return res.status(200).json({ success: true, message: 'Инструкции по сбросу пароля отправлены на ваш email.' }); } catch (error) { return next(ApiError.internal('Ошибка при запросе сброса пароля!')); } } async resetPassword(req, res, next) { try { const { token, userId, newPassword } = req.body; if (!token || !userId || !newPassword) { return next(ApiError.badRequest('Неверные данные для сброса пароля!')); } const isPasswordReset = token.startsWith(`${TOKEN_TYPES.PASSWORD_RESET}_`); const isPasswordChange = token.startsWith(`${TOKEN_TYPES.PASSWORD_CHANGE}_`); if (!isPasswordReset && !isPasswordChange) { return next(ApiError.badRequest('Неверный тип токена! Допустимы только токены сброса (pr_) или смены (pc_) пароля.')); } const tokenType = isPasswordReset ? TOKEN_TYPES.PASSWORD_RESET : TOKEN_TYPES.PASSWORD_CHANGE; if (newPassword.length < 6 || newPassword.length > 30) { return next(ApiError.badRequest('Пароль должен быть от 6 до 30 символов!')); } if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/.test(newPassword)) { return next(ApiError.badRequest('Пароль должен содержать хотя бы одну заглавную букву, одну строчную букву, одну цифру и один специальный символ!')); } const user = await User.findByPk(userId); if (!user) { return next(ApiError.badRequest('Пользователь не найден!')); } const tokenData = user.tokens?.[tokenType]; if (!isValidToken(tokenData, token, tokenType)) { if (tokenData && new Date(tokenData.expires) < new Date()) { await removeToken(user, tokenType); return next(ApiError.badRequest('Срок действия ссылки истек! Запросите новую ссылку.')); } return next(ApiError.badRequest('Неверная или устаревшая ссылка!')); } const hashPassword = await bcrypt.hash(newPassword, 5); user.password = hashPassword; await removeToken(user, tokenType); await user.save(); try { await sendPasswordResetConfirmationEmail(user.email); } catch (emailError) { console.warn('Не удалось отправить письмо подтверждения:', emailError.message); } return res.status(200).json({ success: true, message: 'Пароль успешно изменен! Теперь вы можете войти с новым паролем.' }); } catch (error) { return next(ApiError.internal('Ошибка при сбросе пароля!')); } } async requestAccountDeletion(req, res, next) { try { const userId = req.user.id; const user = await User.findByPk(userId); if (!user) { return next(ApiError.badRequest('Пользователь не найден!')); } const deletionToken = createTokenObject(TOKEN_TYPES.ACCOUNT_DELETION); user.tokens = { ...user.tokens, [TOKEN_TYPES.ACCOUNT_DELETION]: deletionToken }; await user.save(); try { await sendAccountDeletionEmail(user.email, deletionToken.token, user.id); } catch (emailError) { await removeToken(user, TOKEN_TYPES.ACCOUNT_DELETION); return next(ApiError.internal('Не удалось отправить письмо для удаления аккаунта. Попробуйте позже.')); } return res.status(200).json({ success: true, message: 'Письмо для подтверждения удаления аккаунта отправлено на ваш email.' }); } catch (error) { return next(ApiError.internal('Ошибка при запросе удаления аккаунта!')); } } async confirmAccountDeletion(req, res, next) { try { const { token, userId } = req.query; if (!token || !userId) { return res.status(400).json({ success: false, message: 'Неверная ссылка подтверждения удаления.' }); } if (!isTokenType(token, TOKEN_TYPES.ACCOUNT_DELETION)) { return res.status(400).json({ success: false, message: 'Неверный тип токена для удаления аккаунта!' }); } const userIdNum = parseInt(userId); if (isNaN(userIdNum)) { return res.status(400).json({ success: false, message: 'Неверный идентификатор пользователя.' }); } const user = await User.findByPk(userIdNum); if (!user) { return res.status(400).json({ success: false, message: 'Пользователь не найден.' }); } const tokenData = user.tokens?.[TOKEN_TYPES.ACCOUNT_DELETION]; if (!isValidToken(tokenData, token, TOKEN_TYPES.ACCOUNT_DELETION)) { if (tokenData && new Date(tokenData.expires) < new Date()) { await removeToken(user, TOKEN_TYPES.ACCOUNT_DELETION); return res.status(400).json({ success: false, message: 'Срок действия ссылки истек! Запросите новую ссылку для удаления аккаунта.' }); } return res.status(400).json({ success: false, message: 'Ссылка для удаления учетной записи недействительна!' }); } const userEmail = user.email; try { await user.destroy(); try { await sendAccountDeletionConfirmationEmail(userEmail); } catch (emailError) { console.warn('Не удалось отправить письмо подтверждения:', emailError.message); } return res.status(200).json({ success: true, message: 'Ваша учетная запись и все связанные с ней данные были успешно удалены.' }); } catch (deleteError) { return res.status(500).json({ success: false, message: 'Ошибка при удалении аккаунта. Попробуйте позже.' }); } } catch (error) { return res.status(500).json({ success: false, message: 'Ошибка при подтверждении удаления аккаунта!' }); } } async requestPasswordChange(req, res, next) { try { const userId = req.user.id; const { email } = req.body; const user = await User.findByPk(userId); if (!user) { return next(ApiError.badRequest('Пользователь не найден!')); } if (!user.isVerified) { return next(ApiError.forbidden('Пожалуйста, подтвердите ваш email перед изменением пароля.')); } if (user.email !== email) { return next(ApiError.badRequest('Email не совпадает с email вашего аккаунта!')); } const resetToken = createTokenObject(TOKEN_TYPES.PASSWORD_CHANGE); user.tokens = { ...user.tokens, [TOKEN_TYPES.PASSWORD_CHANGE]: resetToken }; await user.save(); try { await sendPasswordChangeEmail(user.email, resetToken.token, user.id, user.email.split('@')[0]); } catch (emailError) { await removeToken(user, TOKEN_TYPES.PASSWORD_CHANGE); return next(ApiError.internal('Не удалось отправить письмо для изменения пароля. Попробуйте позже.')); } return res.status(200).json({ success: true, message: 'Письмо для изменения пароля отправлено на ваш email.' }); } catch (error) { return next(ApiError.internal('Ошибка при запросе изменения пароля!')); } } } module.exports = new UserController();