feat: профиль — просмотр, смена имени, email и пароля

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shop
2026-05-17 11:29:30 +03:00
parent f24f35d0fc
commit 14e0e875f1
6 changed files with 311 additions and 40 deletions
+163
View File
@@ -0,0 +1,163 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const { query, formatPrice } = require('../db');
const { getCart, cartCount } = require('../cart');
const { requireAuth } = require('../middleware/auth');
const { ROLE_LABELS } = require('../constants/roles');
const { asyncHandler } = require('../utils/asyncHandler');
const router = express.Router();
router.use((req, res, next) => {
const cart = getCart(req);
res.locals.cartCount = cartCount(cart);
res.locals.formatPrice = formatPrice;
next();
});
async function loadAccountUser(userId) {
const { rows } = await query(
'SELECT id, email, name, role, created_at FROM users WHERE id = $1',
[userId]
);
return rows[0];
}
async function verifyPassword(userId, password) {
const { rows } = await query('SELECT password_hash FROM users WHERE id = $1', [
userId,
]);
if (!rows[0]) return false;
return bcrypt.compareSync(password || '', rows[0].password_hash);
}
function accountRender(res, options) {
const { user, orderCount, error, success, activeTab } = options;
res.render('account/index', {
title: 'Личный кабинет',
user,
orderCount,
roleLabels: ROLE_LABELS,
error: error || null,
success: success || null,
activeTab: activeTab || 'profile',
});
}
router.get(
'/',
requireAuth,
asyncHandler(async (req, res) => {
const user = await loadAccountUser(req.session.userId);
const countResult = await query(
'SELECT COUNT(*)::int AS n FROM orders WHERE user_id = $1',
[user.id]
);
accountRender(res, {
user,
orderCount: countResult.rows[0].n,
success: req.query.success ? decodeURIComponent(String(req.query.success)) : null,
error: req.query.error ? decodeURIComponent(String(req.query.error)) : null,
activeTab: req.query.tab || 'profile',
});
})
);
router.post(
'/profile',
requireAuth,
asyncHandler(async (req, res) => {
const name = (req.body.name || '').trim();
if (!name) {
return res.redirect('/account?tab=profile&error=' + encodeURIComponent('Укажите имя'));
}
await query('UPDATE users SET name = $1 WHERE id = $2', [name, req.session.userId]);
res.redirect('/account?tab=profile&success=' + encodeURIComponent('Имя обновлено'));
})
);
router.post(
'/email',
requireAuth,
asyncHandler(async (req, res) => {
const newEmail = (req.body.email || '').trim().toLowerCase();
const { current_password } = req.body;
if (!newEmail) {
return res.redirect(
'/account?tab=email&error=' + encodeURIComponent('Укажите новый email')
);
}
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRe.test(newEmail)) {
return res.redirect(
'/account?tab=email&error=' + encodeURIComponent('Некорректный email')
);
}
const user = await loadAccountUser(req.session.userId);
if (newEmail === user.email) {
return res.redirect(
'/account?tab=email&error=' + encodeURIComponent('Это уже ваш текущий email')
);
}
if (!(await verifyPassword(req.session.userId, current_password))) {
return res.redirect(
'/account?tab=email&error=' + encodeURIComponent('Неверный текущий пароль')
);
}
try {
await query('UPDATE users SET email = $1 WHERE id = $2', [
newEmail,
req.session.userId,
]);
res.redirect('/account?tab=email&success=' + encodeURIComponent('Email изменён'));
} catch (err) {
if (err.code === '23505') {
return res.redirect(
'/account?tab=email&error=' + encodeURIComponent('Этот email уже занят')
);
}
throw err;
}
})
);
router.post(
'/password',
requireAuth,
asyncHandler(async (req, res) => {
const { current_password, password, password2 } = req.body;
if (!(await verifyPassword(req.session.userId, current_password))) {
return res.redirect(
'/account?tab=password&error=' + encodeURIComponent('Неверный текущий пароль')
);
}
if (!password || password.length < 6) {
return res.redirect(
'/account?tab=password&error=' +
encodeURIComponent('Новый пароль не менее 6 символов')
);
}
if (password !== password2) {
return res.redirect(
'/account?tab=password&error=' + encodeURIComponent('Пароли не совпадают')
);
}
const hash = bcrypt.hashSync(password, 10);
await query('UPDATE users SET password_hash = $1 WHERE id = $2', [
hash,
req.session.userId,
]);
res.redirect('/account?tab=password&success=' + encodeURIComponent('Пароль изменён'));
})
);
module.exports = router;
-23
View File
@@ -115,27 +115,4 @@ router.post('/logout', (req, res) => {
});
});
router.get(
'/account',
requireAuth,
asyncHandler(async (req, res) => {
const { rows } = await query(
'SELECT id, email, name, role, created_at FROM users WHERE id = $1',
[req.session.userId]
);
const user = rows[0];
const countResult = await query(
'SELECT COUNT(*)::int AS n FROM orders WHERE user_id = $1',
[user.id]
);
res.render('account', {
title: 'Личный кабинет',
user,
orderCount: countResult.rows[0].n,
});
})
);
module.exports = router;