Files
uno-click/bff/services/auth.service.js.bak.20260423_124500
T
2026-05-13 14:20:41 +00:00

482 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { findUserByEmailAndPassword, touchLastLogin, findUserById, createUser, userExistsByEmail } from '../repositories/user.repository.js';
import { createAuthSession, revokeSession, getAuthSession, rotateSessionTokens } from '../repositories/session.repository.js';
import {
generateRefreshToken,
generateCsrfToken,
hashToken,
signAccessToken,
verifyRefreshToken,
signRefreshToken,
} from './token.service.js';
const REFRESH_TTL_DAYS = Number(process.env.REFRESH_TTL_DAYS || 30);
function buildSessionExpiresAt() {
const dt = new Date();
dt.setDate(dt.getDate() + REFRESH_TTL_DAYS);
return dt;
}
export async function loginUser({
email,
password,
userAgent,
ipAddress,
}) {
const user = await findUserByEmailAndPassword(email, password);
if (!user) {
const err = new Error('Invalid email or password');
err.statusCode = 401;
err.code = 'INVALID_CREDENTIALS';
throw err;
}
const refreshToken = generateRefreshToken();
const csrfToken = generateCsrfToken();
const refreshTokenHash = hashToken(refreshToken);
const csrfTokenHash = hashToken(csrfToken);
const session = await createAuthSession({
userId: user.id,
refreshTokenHash,
csrfTokenHash,
userAgent,
ipAddress,
expiresAt: buildSessionExpiresAt(),
});
const accessToken = signAccessToken(user, session.id);
await touchLastLogin(user.id);
return {
user: {
id: user.id,
email: user.email,
displayName: user.display_name,
role: user.role,
status: user.status,
},
session: {
id: session.id,
expiresAt: session.expires_at,
},
accessToken,
refreshToken,
csrfToken,
};
}
export async function logoutUser({ sessionId }) {
if (!sessionId) {
return;
}
await revokeSession(sessionId);
}
export async function refreshUserSession({ refreshToken, userAgent, ipAddress }) {
let payload;
try {
payload = verifyRefreshToken(refreshToken);
} catch (err) {
const error = new Error('Invalid or expired refresh token');
error.code = 'INVALID_REFRESH_TOKEN';
error.status = 401;
throw error;
}
const session = await getAuthSession(payload.sid);
if (!session) {
const error = new Error('Session not found or expired');
error.code = 'SESSION_NOT_FOUND';
error.status = 401;
throw error;
}
const user = await findUserById(session.user_id);
if (!user) {
const error = new Error('User not found');
error.code = 'USER_NOT_FOUND';
error.status = 401;
throw error;
}
const newRefreshToken = generateRefreshToken();
const newCsrfToken = generateCsrfToken();
const newRefreshTokenHash = hashToken(newRefreshToken);
const newCsrfTokenHash = hashToken(newCsrfToken);
const updatedSession = await rotateSessionTokens(
session.id,
newRefreshTokenHash,
newCsrfTokenHash
);
if (!updatedSession) {
const error = new Error('Failed to rotate session tokens');
error.code = 'SESSION_ROTATION_FAILED';
error.status = 500;
throw error;
}
const accessToken = signAccessToken(user, session.id);
const refreshTokenJwt = signRefreshToken({
sub: user.id,
sid: session.id,
});
return {
user: {
id: user.id,
email: user.email,
displayName: user.display_name,
role: user.role,
status: user.status,
},
accessToken,
refreshToken: refreshTokenJwt,
csrfToken: newCsrfToken,
};
}
export async function registerUser({ email, password, displayName, userAgent, ipAddress }) {
const { userExistsByEmail, createUser } = await import('../repositories/user.repository.js');
if (!email || !password) {
const err = new Error('Email and password are required');
err.statusCode = 400;
err.code = 'VALIDATION_ERROR';
throw err;
}
if (password.length < 6) {
const err = new Error('Password must be at least 6 characters');
err.statusCode = 400;
err.code = 'PASSWORD_TOO_SHORT';
throw err;
}
const exists = await userExistsByEmail(email);
if (exists) {
const err = new Error('User with this email already exists');
err.statusCode = 409;
err.code = 'EMAIL_ALREADY_EXISTS';
throw err;
}
const user = await createUser({ email, password, displayName });
const refreshToken = generateRefreshToken();
const csrfToken = generateCsrfToken();
const refreshTokenHash = hashToken(refreshToken);
const csrfTokenHash = hashToken(csrfToken);
const session = await createAuthSession({
userId: user.id,
refreshTokenHash,
csrfTokenHash,
userAgent,
ipAddress,
expiresAt: buildSessionExpiresAt(),
});
const accessToken = signAccessToken(user, session.id);
return {
user: {
id: user.id,
email: user.email,
displayName: user.display_name,
role: user.role,
status: user.status,
},
session: { id: session.id, expiresAt: session.expires_at },
accessToken,
refreshToken,
csrfToken,
};
}
export async function loginWithTelegram({ telegramData, userAgent, ipAddress }) {
const crypto = await import("crypto");
const { hash, ...fields } = telegramData;
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
if (!BOT_TOKEN) throw Object.assign(new Error("Telegram not configured"), { statusCode: 500, code: "CONFIG_ERROR" });
// Verify auth_date freshness (max 1 day)
const authDate = Number(fields.auth_date);
if (!authDate || Date.now() / 1000 - authDate > 86400) {
throw Object.assign(new Error("Telegram auth expired"), { statusCode: 401, code: "AUTH_EXPIRED" });
}
// Antifraud: require Telegram username (filters out throwaway / bot accounts)
if (!fields.username || String(fields.username).trim().length < 3) {
throw Object.assign(new Error("Telegram username required"), { statusCode: 400, code: "TELEGRAM_USERNAME_REQUIRED" });
}
// Verify HMAC-SHA256 signature
const dataCheckString = Object.keys(fields)
.sort()
.filter(k => fields[k] !== undefined && fields[k] !== null)
.map(k => `${k}=${fields[k]}`)
.join("\n");
const secretKey = crypto.createHash("sha256").update(BOT_TOKEN).digest();
const hmac = crypto.createHmac("sha256", secretKey).update(dataCheckString).digest("hex");
if (hmac !== hash) {
throw Object.assign(new Error("Telegram signature invalid"), { statusCode: 401, code: "INVALID_SIGNATURE" });
}
const { findUserByTelegramId, createUserFromTelegram, countTelegramSignupsByIpLast24h, recordSignupAttempt } = await import("../repositories/user.repository.js");
const telegramId = Number(fields.id);
let user = await findUserByTelegramId(telegramId);
if (!user) {
// Antifraud: limit 3 new Telegram signups per IP per 24h
const recentCount = await countTelegramSignupsByIpLast24h(ipAddress);
if (recentCount >= 3) {
throw Object.assign(new Error("Too many signups from this IP"), { statusCode: 429, code: "SIGNUP_RATE_LIMIT" });
}
const displayName = [fields.first_name, fields.last_name].filter(Boolean).join(" ") || fields.username || null;
const email = `tg_${telegramId}@telegram.local`;
user = await createUserFromTelegram({ telegramId, displayName, email });
await recordSignupAttempt({ ipAddress, provider: 'telegram', telegramId, userAgent });
}
if (user.status !== "active") {
throw Object.assign(new Error("Account is blocked"), { statusCode: 403, code: "ACCOUNT_BLOCKED" });
}
const refreshToken = generateRefreshToken();
const csrfToken = generateCsrfToken();
const refreshTokenHash = hashToken(refreshToken);
const csrfTokenHash = hashToken(csrfToken);
const session = await createAuthSession({
userId: user.id,
refreshTokenHash,
csrfTokenHash,
userAgent,
ipAddress,
expiresAt: buildSessionExpiresAt(),
});
const accessToken = signAccessToken(user, session.id);
return {
user: {
id: user.id,
email: user.email,
displayName: user.display_name,
role: user.role,
status: user.status,
},
session: { id: session.id, expiresAt: session.expires_at },
accessToken,
refreshToken,
csrfToken,
};
}
export async function linkTelegramAccount({ userId, telegramData }) {
const crypto = await import("crypto");
const { hash, ...fields } = telegramData || {};
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
if (!BOT_TOKEN) throw Object.assign(new Error("Telegram not configured"), { statusCode: 500, code: "CONFIG_ERROR" });
const authDate = Number(fields.auth_date);
if (!authDate || Date.now() / 1000 - authDate > 86400) {
throw Object.assign(new Error("Telegram auth expired"), { statusCode: 401, code: "AUTH_EXPIRED" });
}
if (!fields.username || String(fields.username).trim().length < 3) {
throw Object.assign(new Error("Telegram username required"), { statusCode: 400, code: "TELEGRAM_USERNAME_REQUIRED" });
}
const dataCheckString = Object.keys(fields)
.sort()
.filter(k => fields[k] !== undefined && fields[k] !== null)
.map(k => `${k}=${fields[k]}`)
.join("\n");
const secretKey = crypto.createHash("sha256").update(BOT_TOKEN).digest();
const hmac = crypto.createHmac("sha256", secretKey).update(dataCheckString).digest("hex");
if (hmac !== hash) {
throw Object.assign(new Error("Telegram signature invalid"), { statusCode: 401, code: "INVALID_SIGNATURE" });
}
const { findTelegramOwner, linkTelegramToUser } = await import("../repositories/user.repository.js");
const telegramId = Number(fields.id);
const owner = await findTelegramOwner(telegramId);
if (owner && owner.id !== userId) {
throw Object.assign(new Error("Telegram already linked to another account"), { statusCode: 409, code: "TELEGRAM_ALREADY_LINKED" });
}
await linkTelegramToUser(userId, telegramId);
return { ok: true, telegramId };
}
export async function unlinkTelegramAccount(userId) {
const { unlinkTelegramFromUser } = await import("../repositories/user.repository.js");
await unlinkTelegramFromUser(userId);
return { ok: true };
}
// ─── Bot deeplink login flow ─────────────────────────────────────────────────
export async function startTelegramLoginFlow({ intent, userId, ipAddress, userAgent }) {
const crypto = await import("crypto");
const { createTgLoginToken, countTgStartsByIpLastMinutes } = await import("../repositories/tg-login.repository.js");
// Антифрод: 10 стартов с одного IP за 10 минут
const recent = await countTgStartsByIpLastMinutes(ipAddress, 10);
if (recent >= 10) {
throw Object.assign(new Error("Too many login attempts. Try again later."),
{ statusCode: 429, code: "TG_START_RATE_LIMIT" });
}
// Telegram /start payload: до 64 base64url-символов
const token = crypto.randomBytes(24).toString("base64url");
const created = await createTgLoginToken({ token, intent, userId, ipAddress, userAgent });
const botUsername = process.env.TELEGRAM_BOT_USERNAME || "One_Click_Auth_bot";
const url = `https://t.me/${botUsername}?start=${token}`;
return { token, url, expiresAt: created.expiresAt, ttlSec: created.ttlSec };
}
export async function confirmTelegramFromBot({ token, telegramUser }) {
const { findTgLoginToken, markTgTokenConfirmed, markTgTokenError } = await import("../repositories/tg-login.repository.js");
const {
findUserByTelegramId, createUserFromTelegram,
countTelegramSignupsByIpLast24h, recordSignupAttempt,
findUserById, findTelegramOwner, linkTelegramToUser,
} = await import("../repositories/user.repository.js");
const row = await findTgLoginToken(token);
if (!row) return { ok: false, code: "TOKEN_NOT_FOUND", message: "Ссылка не найдена или устарела." };
if (row.status !== "pending") return { ok: false, code: "TOKEN_NOT_PENDING", message: "Эта ссылка уже использована. Запросите новую на сайте." };
if (new Date(row.expires_at) < new Date()) {
await markTgTokenError(token, "EXPIRED");
return { ok: false, code: "EXPIRED", message: "Ссылка истекла. Запросите новую на сайте." };
}
// Антифрод: требуем username
if (!telegramUser.username || String(telegramUser.username).trim().length < 3) {
await markTgTokenError(token, "USERNAME_REQUIRED");
return { ok: false, code: "USERNAME_REQUIRED", message: "У вашего Telegram-аккаунта должно быть @username." };
}
const telegramId = Number(telegramUser.id);
if (row.intent === "link") {
if (!row.user_id) {
await markTgTokenError(token, "NO_USER");
return { ok: false, code: "NO_USER", message: "Авторизация не найдена. Войдите на сайт и попробуйте снова." };
}
const owner = await findTelegramOwner(telegramId);
if (owner && owner.id !== row.user_id) {
await markTgTokenError(token, "TELEGRAM_ALREADY_LINKED");
return { ok: false, code: "TELEGRAM_ALREADY_LINKED", message: "Этот Telegram уже привязан к другому аккаунту." };
}
await linkTelegramToUser(row.user_id, telegramId);
await markTgTokenConfirmed(token, {
telegramId,
telegramUsername: telegramUser.username,
telegramFirstName: telegramUser.first_name || null,
telegramLastName: telegramUser.last_name || null,
userId: row.user_id,
});
return { ok: true, message: "Готово! Telegram привязан. Возвращайтесь на сайт." };
}
// intent === "login"
let user = await findUserByTelegramId(telegramId);
if (!user) {
const recent = await countTelegramSignupsByIpLast24h(row.ip_address);
if (recent >= 3) {
await markTgTokenError(token, "SIGNUP_RATE_LIMIT");
return { ok: false, code: "SIGNUP_RATE_LIMIT", message: "Слишком много регистраций с этого IP. Попробуйте позже." };
}
const displayName = [telegramUser.first_name, telegramUser.last_name].filter(Boolean).join(" ") || telegramUser.username || null;
const email = `tg_${telegramId}@telegram.local`;
user = await createUserFromTelegram({ telegramId, displayName, email });
await recordSignupAttempt({ ipAddress: row.ip_address, provider: "telegram", telegramId, userAgent: row.user_agent });
}
if (user.status !== "active") {
await markTgTokenError(token, "ACCOUNT_BLOCKED");
return { ok: false, code: "ACCOUNT_BLOCKED", message: "Аккаунт заблокирован." };
}
await markTgTokenConfirmed(token, {
telegramId,
telegramUsername: telegramUser.username,
telegramFirstName: telegramUser.first_name || null,
telegramLastName: telegramUser.last_name || null,
userId: user.id,
});
return { ok: true, message: "Готово! Можете возвращаться на сайт — вы уже вошли." };
}
export async function pollTelegramLoginFlow({ token, userAgent, ipAddress }) {
const { findTgLoginToken, consumeTgToken } = await import("../repositories/tg-login.repository.js");
const { findUserById } = await import("../repositories/user.repository.js");
const row = await findTgLoginToken(token);
if (!row) return { status: "not_found" };
if (row.status === "pending") {
if (new Date(row.expires_at) < new Date()) return { status: "expired" };
return { status: "pending" };
}
if (row.status === "error") {
return { status: "error", code: row.error_code };
}
if (row.status === "consumed") {
return { status: "consumed" };
}
if (row.status !== "confirmed") {
return { status: row.status };
}
// confirmed → consume + maybe issue session
const consumed = await consumeTgToken(token);
if (!consumed) return { status: "consumed" };
if (row.intent === "link") {
return { status: "linked" };
}
// login: создаём сессию
const user = await findUserById(row.user_id);
if (!user) return { status: "error", code: "USER_NOT_FOUND" };
const refreshToken = generateRefreshToken();
const csrfToken = generateCsrfToken();
const session = await createAuthSession({
userId: user.id,
refreshTokenHash: hashToken(refreshToken),
csrfTokenHash: hashToken(csrfToken),
userAgent, ipAddress,
expiresAt: buildSessionExpiresAt(),
});
const accessToken = signAccessToken(user, session.id);
return {
status: "authenticated",
user: {
id: user.id, email: user.email, displayName: user.display_name,
role: user.role, status: user.status,
},
accessToken, refreshToken, csrfToken,
sessionExpiresAt: session.expires_at,
};
}