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, }; }