initial commit

This commit is contained in:
root
2026-05-13 14:20:41 +00:00
commit 6e178d2012
6022 changed files with 399872 additions and 0 deletions
@@ -0,0 +1,285 @@
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,
};
}