213 lines
7.1 KiB
Plaintext
213 lines
7.1 KiB
Plaintext
import { Router } from 'express';
|
|
import { env } from '../config/env.js';
|
|
import { loginUser, logoutUser, refreshUserSession, registerUser, loginWithTelegram } from '../services/auth.service.js';
|
|
import { authRequired } from '../middleware/authRequired.js';
|
|
import { findUserById, updateUserBalance } from '../repositories/user.repository.js';
|
|
|
|
const router = Router();
|
|
|
|
function buildAccessCookieOptions() {
|
|
return {
|
|
httpOnly: true,
|
|
secure: env.COOKIE_SECURE,
|
|
sameSite: env.COOKIE_SAME_SITE,
|
|
// Domain не указывается для __Host- префикса (требование RFC 6265)
|
|
path: '/',
|
|
maxAge: env.ACCESS_TOKEN_TTL_SEC * 1000,
|
|
};
|
|
}
|
|
|
|
function buildRefreshCookieOptions() {
|
|
return {
|
|
httpOnly: true,
|
|
secure: env.COOKIE_SECURE,
|
|
sameSite: env.COOKIE_SAME_SITE,
|
|
// Domain не указывается для __Host- префикса (требование RFC 6265)
|
|
path: '/',
|
|
maxAge: env.REFRESH_TOKEN_TTL_SEC * 1000,
|
|
};
|
|
}
|
|
|
|
function buildCsrfCookieOptions() {
|
|
return {
|
|
httpOnly: false, // фронту надо прочитать и отправить в header
|
|
secure: env.COOKIE_SECURE,
|
|
sameSite: env.COOKIE_SAME_SITE,
|
|
// Domain не указывается для работы с __Host- префиксом
|
|
path: '/',
|
|
maxAge: env.REFRESH_TOKEN_TTL_SEC * 1000,
|
|
};
|
|
}
|
|
|
|
router.post('/login', async (req, res, next) => {
|
|
try {
|
|
const { email, password } = req.body;
|
|
|
|
const result = await loginUser({
|
|
email,
|
|
password,
|
|
userAgent: req.get('user-agent') || null,
|
|
ipAddress: req.ip || null,
|
|
});
|
|
|
|
res.cookie(env.COOKIE_ACCESS_NAME, result.accessToken, buildAccessCookieOptions());
|
|
res.cookie(env.COOKIE_REFRESH_NAME, result.refreshToken, buildRefreshCookieOptions());
|
|
res.cookie(env.COOKIE_CSRF_NAME, result.csrfToken, buildCsrfCookieOptions());
|
|
|
|
res.json({
|
|
ok: true,
|
|
accessToken: result.accessToken,
|
|
user: result.user,
|
|
});
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/logout', authRequired, async (req, res, next) => {
|
|
try {
|
|
await logoutUser({ sessionId: req.user.sessionId });
|
|
|
|
res.clearCookie(env.COOKIE_ACCESS_NAME, { path: '/' });
|
|
res.clearCookie(env.COOKIE_REFRESH_NAME, { path: '/' });
|
|
res.clearCookie(env.COOKIE_CSRF_NAME, { path: '/' });
|
|
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/refresh', async (req, res, next) => {
|
|
try {
|
|
const refreshToken = req.cookies?.[env.COOKIE_REFRESH_NAME];
|
|
|
|
if (!refreshToken) {
|
|
return res.status(401).json({
|
|
error: 'REFRESH_TOKEN_MISSING',
|
|
message: 'Refresh token is missing',
|
|
});
|
|
}
|
|
|
|
const result = await refreshUserSession({
|
|
refreshToken,
|
|
userAgent: req.get('user-agent') || null,
|
|
ipAddress: req.ip || null,
|
|
});
|
|
|
|
res.cookie(env.COOKIE_ACCESS_NAME, result.accessToken, buildAccessCookieOptions());
|
|
res.cookie(env.COOKIE_REFRESH_NAME, result.refreshToken, buildRefreshCookieOptions());
|
|
res.cookie(env.COOKIE_CSRF_NAME, result.csrfToken, buildCsrfCookieOptions());
|
|
|
|
res.json({
|
|
ok: true,
|
|
user: result.user,
|
|
});
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/me', authRequired, async (req, res) => {
|
|
try {
|
|
const dbUser = await findUserById(req.user.id);
|
|
if (!dbUser) return res.status(401).json({ error: 'USER_NOT_FOUND' });
|
|
res.json({
|
|
ok: true,
|
|
user: {
|
|
id: dbUser.id,
|
|
email: dbUser.email,
|
|
displayName: dbUser.display_name,
|
|
role: dbUser.role,
|
|
status: dbUser.status,
|
|
balance: Number(dbUser.balance || 0),
|
|
createdAt: dbUser.created_at,
|
|
sessionId: req.user.sessionId,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/balance/update', authRequired, async (req, res) => {
|
|
const delta = Number(req.body.delta);
|
|
if (!delta || isNaN(delta)) return res.status(400).json({ error: 'delta required' });
|
|
try {
|
|
const newBalance = await updateUserBalance(req.user.id, delta);
|
|
if (newBalance === null) return res.status(404).json({ error: 'User not found' });
|
|
res.json({ ok: true, balance: Number(newBalance) });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/csrf', authRequired, async (req, res) => {
|
|
res.json({
|
|
ok: true,
|
|
csrfToken: req.cookies?.[env.COOKIE_CSRF_NAME] || null,
|
|
});
|
|
});
|
|
|
|
|
|
router.post('/register', async (req, res, next) => {
|
|
try {
|
|
const { email, password, name } = req.body;
|
|
|
|
const result = await registerUser({
|
|
email,
|
|
password,
|
|
displayName: name || null,
|
|
userAgent: req.get('user-agent') || null,
|
|
ipAddress: req.ip || null,
|
|
});
|
|
|
|
res.cookie(env.COOKIE_ACCESS_NAME, result.accessToken, buildAccessCookieOptions());
|
|
res.cookie(env.COOKIE_REFRESH_NAME, result.refreshToken, buildRefreshCookieOptions());
|
|
res.cookie(env.COOKIE_CSRF_NAME, result.csrfToken, buildCsrfCookieOptions());
|
|
|
|
res.status(201).json({
|
|
ok: true,
|
|
accessToken: result.accessToken,
|
|
user: result.user,
|
|
});
|
|
} catch (err) {
|
|
if (err.statusCode === 409) {
|
|
return res.status(409).json({ ok: false, error: err.code, message: err.message });
|
|
}
|
|
if (err.statusCode === 400) {
|
|
return res.status(400).json({ ok: false, error: err.code, message: err.message });
|
|
}
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
|
|
router.post('/telegram', async (req, res, next) => {
|
|
try {
|
|
const result = await loginWithTelegram({
|
|
telegramData: req.body,
|
|
userAgent: req.get('user-agent') || null,
|
|
ipAddress: req.ip || null,
|
|
});
|
|
|
|
res.cookie(env.COOKIE_ACCESS_NAME, result.accessToken, buildAccessCookieOptions());
|
|
res.cookie(env.COOKIE_REFRESH_NAME, result.refreshToken, buildRefreshCookieOptions());
|
|
res.cookie(env.COOKIE_CSRF_NAME, result.csrfToken, buildCsrfCookieOptions());
|
|
|
|
res.status(200).json({ ok: true, accessToken: result.accessToken, user: result.user });
|
|
} catch (err) {
|
|
const status = err.statusCode || 500;
|
|
if (status < 500) {
|
|
return res.status(status).json({ ok: false, error: err.code, message: err.message });
|
|
}
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
|
|
router.get('/telegram-widget', (req, res) => {
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
res.send('<!DOCTYPE html>\n<html lang="ru"><head><meta charset="utf-8"/><title>Telegram Auth</title><style>*{margin:0;padding:0}body{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;background:#1a1a2e;font-family:sans-serif;color:#fff}h2{font-size:24px;font-weight:700;margin-bottom:6px}p{font-size:13px;color:#aaa;margin-bottom:28px}.wrap{min-height:48px;display:flex;align-items:center}</style></head><body><h2>One Click</h2><p>Войдите через Telegram</p><div class="wrap" id="tg"></div><script>window.onTelegramAuth=function(u){if(window.opener){window.opener.postMessage({type:"telegram_auth",data:u},"*");}window.close();};</script><script async src="https://telegram.org/js/telegram-widget.js?22" data-telegram-login="One_Click_Auth_bot" data-size="large" data-radius="12" data-onauth="onTelegramAuth(user)" data-request-access="write"></script></body></html>');
|
|
});
|
|
export default router; |