db4bc9bfe1
Co-authored-by: Cursor <cursoragent@cursor.com>
239 lines
7.1 KiB
JavaScript
239 lines
7.1 KiB
JavaScript
const express = require('express');
|
|
const bcrypt = require('bcryptjs');
|
|
const { query, formatPrice } = require('../db');
|
|
const { getCart, cartCount } = require('../cart');
|
|
const { requireAuth } = require('../middleware/auth');
|
|
const { requireCookieConsent } = require('../middleware/cookieConsent');
|
|
const { ROLES, ROLE_LABELS } = require('../constants/roles');
|
|
const { asyncHandler } = require('../utils/asyncHandler');
|
|
const { expireOldReservations } = require('../services/reservations');
|
|
const webauthn = require('../services/webauthn');
|
|
|
|
const router = express.Router();
|
|
|
|
router.use(requireCookieConsent);
|
|
|
|
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, passkey_enabled, loyalty_points 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,
|
|
reservations,
|
|
error,
|
|
success,
|
|
activeTab,
|
|
formatPrice,
|
|
passkeys,
|
|
isAdmin,
|
|
} = options;
|
|
res.render('account/index', {
|
|
title: 'Личный кабинет',
|
|
user,
|
|
orderCount,
|
|
reservations: reservations || [],
|
|
passkeys: passkeys || [],
|
|
isAdmin: Boolean(isAdmin),
|
|
roleLabels: ROLE_LABELS,
|
|
formatPrice: formatPrice || res.locals.formatPrice,
|
|
error: error || null,
|
|
success: success || null,
|
|
activeTab: activeTab || 'profile',
|
|
});
|
|
}
|
|
|
|
router.get(
|
|
'/',
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
await expireOldReservations();
|
|
const user = await loadAccountUser(req.session.userId);
|
|
const countResult = await query(
|
|
'SELECT COUNT(*)::int AS n FROM orders WHERE user_id = $1',
|
|
[user.id]
|
|
);
|
|
|
|
const { rows: reservations } = await query(
|
|
`SELECT r.*, p.name AS product_name, p.slug AS product_slug, p.price_cents, p.image_url
|
|
FROM reservations r
|
|
JOIN products p ON p.id = r.product_id
|
|
WHERE r.user_id = $1
|
|
ORDER BY r.created_at DESC`,
|
|
[user.id]
|
|
);
|
|
|
|
const passkeys = await webauthn.getCredentialsForUser(user.id);
|
|
|
|
accountRender(res, {
|
|
user,
|
|
orderCount: countResult.rows[0].n,
|
|
reservations,
|
|
passkeys,
|
|
isAdmin: user.role === ROLES.ADMIN,
|
|
formatPrice,
|
|
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('Пароль изменён'));
|
|
})
|
|
);
|
|
|
|
router.post(
|
|
'/passkey/disable',
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const { current_password } = req.body;
|
|
if (!(await verifyPassword(req.session.userId, current_password))) {
|
|
return res.redirect(
|
|
'/account?tab=passkey&error=' + encodeURIComponent('Неверный пароль')
|
|
);
|
|
}
|
|
await webauthn.disablePasskeys(req.session.userId);
|
|
res.redirect(
|
|
'/account?tab=passkey&success=' + encodeURIComponent('Вход по passkey отключён')
|
|
);
|
|
})
|
|
);
|
|
|
|
router.post(
|
|
'/passkey/credentials/:id/delete',
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const { current_password } = req.body;
|
|
if (!(await verifyPassword(req.session.userId, current_password))) {
|
|
return res.redirect(
|
|
'/account?tab=passkey&error=' + encodeURIComponent('Неверный пароль')
|
|
);
|
|
}
|
|
const credId = parseInt(req.params.id, 10);
|
|
if (!Number.isFinite(credId)) {
|
|
return res.redirect('/account?tab=passkey&error=' + encodeURIComponent('Некорректный ключ'));
|
|
}
|
|
const ok = await webauthn.deleteCredential(req.session.userId, credId);
|
|
if (!ok) {
|
|
return res.redirect('/account?tab=passkey&error=' + encodeURIComponent('Ключ не найден'));
|
|
}
|
|
res.redirect('/account?tab=passkey&success=' + encodeURIComponent('Passkey удалён'));
|
|
})
|
|
);
|
|
|
|
module.exports = router;
|