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
+246
View File
@@ -0,0 +1,246 @@
import { pool } from '../db.js';
/**
* Проверка прав доступа пользователя к сценарию
*/
export async function getUserScenarioPermission(userId, scenarioId) {
const query = `
SELECT can_start, can_execute, can_view_results
FROM uno_bff.scenario_permissions
WHERE user_id = $1 AND scenario_id = $2
`;
const result = await pool.query(query, [userId, scenarioId]);
return result.rows[0] || null;
}
/**
* Получить сценарий по ID
*/
export async function getScenarioById(scenarioId) {
const query = `
SELECT scenario_id, name, description, status, is_public, start_workflow_key, created_at, updated_at
FROM uno_bff.scenarios
WHERE scenario_id = $1
`;
const result = await pool.query(query, [scenarioId]);
return result.rows[0] || null;
}
/**
* Получить шаг сценария по ID
*/
export async function getScenarioStep(scenarioId, stepId) {
const query = `
SELECT scenario_id, step_id, name, description, step_order, status, step_workflow_key, is_terminal, input_schema, output_schema, created_at, updated_at
FROM uno_bff.scenario_steps
WHERE scenario_id = $1 AND step_id = $2
`;
const result = await pool.query(query, [scenarioId, stepId]);
return result.rows[0] || null;
}
/**
* Получить первый шаг сценария
*/
export async function getFirstScenarioStep(scenarioId) {
const query = `
SELECT scenario_id, step_id, name, description, step_order, status, step_workflow_key, is_terminal, input_schema, output_schema
FROM uno_bff.scenario_steps
WHERE scenario_id = $1 AND status = 'active'
ORDER BY step_order ASC
LIMIT 1
`;
const result = await pool.query(query, [scenarioId]);
return result.rows[0] || null;
}
/**
* Создать запись generation
*/
export async function createGeneration({ userId, scenarioId, authSessionId, requestPayload, currentStepId }) {
const query = `
INSERT INTO uno_bff.generations (user_id, scenario_id, auth_session_id, status, request_payload, current_step_id)
VALUES ($1, $2, $3, 'running', $4, $5)
RETURNING generation_uuid, user_id, scenario_id, auth_session_id, status, current_step_id, request_payload, started_at
`;
const result = await pool.query(query, [userId, scenarioId, authSessionId, JSON.stringify(requestPayload), currentStepId]);
return result.rows[0];
}
/**
* Получить generation по UUID
*/
export async function getGenerationByUuid(generationUuid) {
const query = `
SELECT generation_uuid, user_id, scenario_id, auth_session_id, status, current_step_id, request_payload, result_payload, last_error_payload, external_run_id, started_at, finished_at
FROM uno_bff.generations
WHERE generation_uuid = $1
`;
const result = await pool.query(query, [generationUuid]);
return result.rows[0] || null;
}
/**
* Обновить generation (после выполнения шага)
*/
export async function updateGeneration(generationUuid, updates) {
const fields = [];
const values = [];
let idx = 1;
if (updates.status !== undefined) {
fields.push(`status = $${idx++}`);
values.push(updates.status);
}
if (updates.currentStepId !== undefined) {
fields.push(`current_step_id = $${idx++}`);
values.push(updates.currentStepId);
}
if (updates.resultPayload !== undefined) {
fields.push(`result_payload = $${idx++}`);
values.push(JSON.stringify(updates.resultPayload));
}
if (updates.lastErrorPayload !== undefined) {
fields.push(`last_error_payload = $${idx++}`);
values.push(JSON.stringify(updates.lastErrorPayload));
}
if (updates.externalRunId !== undefined) {
fields.push(`external_run_id = $${idx++}`);
values.push(updates.externalRunId);
}
if (updates.finishedAt !== undefined) {
fields.push(`finished_at = $${idx++}`);
values.push(updates.finishedAt);
}
if (fields.length === 0) {
return getGenerationByUuid(generationUuid);
}
values.push(generationUuid);
const query = `
UPDATE uno_bff.generations
SET ${fields.join(', ')}, updated_at = now()
WHERE generation_uuid = $${idx}
RETURNING generation_uuid, user_id, scenario_id, status, current_step_id, request_payload, result_payload, external_run_id
`;
const result = await pool.query(query, values);
return result.rows[0];
}
/**
* Обновить generation_step
* Используется n8n workflow для обновления статуса шага
*/
export async function updateGenerationStepByUuid(generationUuid, stepId, updates) {
const fields = [];
const values = [];
let idx = 1;
if (updates.status !== undefined) {
fields.push(`status = $${idx++}`);
values.push(updates.status);
}
if (updates.responsePayload !== undefined) {
fields.push(`response_payload = $${idx++}`);
values.push(JSON.stringify(updates.responsePayload));
}
if (updates.errorPayload !== undefined) {
fields.push(`error_payload = $${idx++}`);
values.push(JSON.stringify(updates.errorPayload));
}
if (updates.startedAt !== undefined) {
fields.push(`started_at = $${idx++}`);
values.push(updates.startedAt);
}
if (updates.finishedAt !== undefined) {
fields.push(`finished_at = $${idx++}`);
values.push(updates.finishedAt);
}
if (fields.length === 0) {
return getGenerationStep(generationUuid, stepId);
}
values.push(generationUuid, stepId);
const query = `
UPDATE uno_bff.generation_steps
SET ${fields.join(', ')}, updated_at = now()
WHERE generation_uuid = $${idx} AND step_id = $${idx + 1}
RETURNING id, generation_uuid, scenario_id, step_id, status, request_payload, response_payload
`;
const result = await pool.query(query, values);
return result.rows[0];
}
/**
* Получить generation_step
*/
export async function getGenerationStep(generationUuid, stepId) {
const query = `
SELECT id, generation_uuid, scenario_id, step_id, step_order, status, request_payload, response_payload, error_payload
FROM uno_bff.generation_steps
WHERE generation_uuid = $1 AND step_id = $2
`;
const result = await pool.query(query, [generationUuid, stepId]);
return result.rows[0] || null;
}
/**
* Получить generation по UUID с проверкой принадлежности пользователю
*/
export async function getGenerationByUuidForUser(generationUuid, userId) {
const query = `
SELECT g.generation_uuid, g.user_id, g.scenario_id, g.auth_session_id, g.status,
g.current_step_id, g.request_payload, g.result_payload, g.last_error_payload,
g.external_run_id, g.started_at, g.finished_at,
s.name as scenario_name
FROM uno_bff.generations g
LEFT JOIN uno_bff.scenarios s ON g.scenario_id = s.scenario_id
WHERE g.generation_uuid = $1 AND g.user_id = $2
`;
const result = await pool.query(query, [generationUuid, userId]);
return result.rows[0] || null;
}
/**
* Получить метаданные generation по UUID с проверкой принадлежности пользователю
*/
export async function getGenerationMetaByUuid(generationUuid, userId) {
const query = `
SELECT g.scenario_id, s.name as scenario_name
FROM uno_bff.generations g
LEFT JOIN uno_bff.scenarios s ON g.scenario_id = s.scenario_id
WHERE g.generation_uuid = $1 AND g.user_id = $2
`;
const result = await pool.query(query, [generationUuid, userId]);
return result.rows[0] || null;
}
/**
* Обновить generation result_payload и статус
*/
export async function updateGenerationResult(generationUuid, resultPayload, status = 'completed') {
const query = `
UPDATE uno_bff.generations
SET result_payload = $2, status = $3, finished_at = COALESCE(finished_at, now()), updated_at = now()
WHERE generation_uuid = $1
RETURNING generation_uuid, user_id, scenario_id, status, result_payload
`;
const result = await pool.query(query, [generationUuid, JSON.stringify(resultPayload), status]);
return result.rows[0] || null;
}
/**
* Получить все шаги для generation
*/
export async function getGenerationSteps(generationUuid) {
const query = `
SELECT step_id, step_order, status, request_payload, response_payload, error_payload, started_at, finished_at
FROM uno_bff.generation_steps
WHERE generation_uuid = $1
ORDER BY step_order ASC
`;
const result = await pool.query(query, [generationUuid]);
return result.rows;
}
+91
View File
@@ -0,0 +1,91 @@
import { pool } from '../db.js';
export async function createAuthSession({
userId,
refreshTokenHash,
csrfTokenHash,
userAgent,
ipAddress,
expiresAt,
}) {
const sql = `
INSERT INTO uno_bff.auth_sessions
(
user_id,
refresh_token_hash,
csrf_token_hash,
status,
user_agent,
ip_address,
expires_at,
last_seen_at
)
VALUES
(
$1, $2, $3, 'active', $4, $5, $6, now()
)
RETURNING
id,
user_id,
refresh_token_hash,
csrf_token_hash,
status,
user_agent,
ip_address,
expires_at,
last_seen_at,
revoked_at,
created_at,
updated_at
`;
const params = [
userId,
refreshTokenHash,
csrfTokenHash,
userAgent || null,
ipAddress || null,
expiresAt,
];
const { rows } = await pool.query(sql, params);
return rows[0];
}
export async function revokeSession(sessionId) {
const sql = `
UPDATE uno_bff.auth_sessions
SET
status = 'revoked',
revoked_at = now(),
updated_at = now()
WHERE id = $1
AND status = 'active'
`;
await pool.query(sql, [sessionId]);
}
export async function getAuthSession(sessionId) {
const sql = `
SELECT id, user_id, refresh_token_hash, csrf_token_hash, status, expires_at
FROM uno_bff.auth_sessions
WHERE id = $1 AND status = 'active' AND expires_at > now()
`;
const result = await pool.query(sql, [sessionId]);
return result.rows[0] || null;
}
export async function rotateSessionTokens(sessionId, newRefreshTokenHash, newCsrfTokenHash) {
const sql = `
UPDATE uno_bff.auth_sessions
SET
refresh_token_hash = $2,
csrf_token_hash = $3,
updated_at = now()
WHERE id = $1 AND status = 'active'
RETURNING id, user_id
`;
const result = await pool.query(sql, [sessionId, newRefreshTokenHash, newCsrfTokenHash]);
return result.rows[0] || null;
}
+66
View File
@@ -0,0 +1,66 @@
import { pool } from "../db.js";
const TOKEN_TTL_SEC = 600; // 10 минут
export async function createTgLoginToken({ token, intent, userId, ipAddress, userAgent }) {
const expires = new Date(Date.now() + TOKEN_TTL_SEC * 1000);
await pool.query(
`INSERT INTO uno_bff.tg_login_tokens
(token, intent, status, user_id, ip_address, user_agent, expires_at)
VALUES ($1, $2, 'pending', $3, $4, $5, $6)`,
[token, intent, userId || null, ipAddress, userAgent, expires]
);
return { token, expiresAt: expires, ttlSec: TOKEN_TTL_SEC };
}
export async function findTgLoginToken(token) {
const r = await pool.query(
`SELECT * FROM uno_bff.tg_login_tokens WHERE token = $1`, [token]
);
return r.rows[0] || null;
}
export async function markTgTokenConfirmed(token, { telegramId, telegramUsername, telegramFirstName, telegramLastName, userId }) {
await pool.query(
`UPDATE uno_bff.tg_login_tokens
SET status = 'confirmed',
telegram_id = $2,
telegram_username = $3,
telegram_first_name = $4,
telegram_last_name = $5,
user_id = COALESCE($6, user_id),
confirmed_at = now()
WHERE token = $1 AND status = 'pending' AND expires_at > now()`,
[token, telegramId, telegramUsername, telegramFirstName, telegramLastName, userId || null]
);
}
export async function markTgTokenError(token, errorCode) {
await pool.query(
`UPDATE uno_bff.tg_login_tokens
SET status = 'error', error_code = $2
WHERE token = $1 AND status = 'pending'`,
[token, errorCode]
);
}
export async function consumeTgToken(token) {
const r = await pool.query(
`UPDATE uno_bff.tg_login_tokens
SET status = 'consumed', consumed_at = now()
WHERE token = $1 AND status = 'confirmed'
RETURNING *`,
[token]
);
return r.rows[0] || null;
}
export async function countTgStartsByIpLastMinutes(ipAddress, minutes = 10) {
if (!ipAddress) return 0;
const r = await pool.query(
`SELECT count(*)::int AS c FROM uno_bff.tg_login_tokens
WHERE ip_address = $1 AND created_at > now() - ($2 || ' minutes')::interval`,
[ipAddress, String(minutes)]
);
return r.rows[0]?.c || 0;
}
+213
View File
@@ -0,0 +1,213 @@
import { pool } from '../db.js';
/**
* Repository для работы с метаданными файлов пользователей
*/
export const userFileRepository = {
/**
* Создать запись о файле
*/
async create({ userId, s3Key, originalFilename, fileSize, contentType, fileType = 'image', folder = 'images_input', generationUuid = null, generationStepId = null, status = 'uploaded', uploadId = null }) {
const query = `
INSERT INTO uno_bff.user_files (user_id, s3_key, original_filename, file_size, content_type, file_type, folder, generation_uuid, generation_step_id, status, upload_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id, user_id, s3_key, original_filename, file_size, content_type, file_type, folder, generation_uuid, generation_step_id, status, upload_id, created_at, updated_at
`;
const values = [userId, s3Key, originalFilename, fileSize, contentType, fileType, folder, generationUuid, generationStepId, status, uploadId];
const result = await pool.query(query, values);
return result.rows[0];
},
/**
* Обновить файл
*/
async update(fileId, updates) {
const allowedFields = ['status', 'upload_id', 'file_size', 'updated_at'];
const setClauses = [];
const values = [fileId];
let paramIndex = 2;
for (const [key, value] of Object.entries(updates)) {
if (allowedFields.includes(key)) {
setClauses.push(`${key} = $${paramIndex}`);
values.push(value);
paramIndex++;
}
}
if (setClauses.length === 0) {
throw new Error('No valid fields to update');
}
const query = `
UPDATE uno_bff.user_files
SET ${setClauses.join(', ')}
WHERE id = $1
RETURNING id, user_id, s3_key, original_filename, file_size, content_type, file_type, folder, status, upload_id, created_at, updated_at
`;
const result = await pool.query(query, values);
return result.rows[0];
},
/**
* Удалить файл по ID
*/
async delete(fileId) {
const query = `
DELETE FROM uno_bff.user_files
WHERE id = $1
RETURNING id
`;
const result = await pool.query(query, [fileId]);
return result.rows[0];
},
/**
* Найти файл по ID и проверить владение
*/
async findByIdAndOwner(fileId, userId) {
const query = `
SELECT id, user_id, s3_key, original_filename, file_size, content_type, file_type, folder, created_at
FROM uno_bff.user_files
WHERE id = $1 AND user_id = $2
`;
const result = await pool.query(query, [fileId, userId]);
return result.rows[0];
},
/**
* Найти файл по S3 ключу и проверить владение
*/
async findByKeyAndOwner(s3Key, userId) {
const query = `
SELECT id, user_id, s3_key, original_filename, file_size, content_type, file_type, folder, created_at
FROM uno_bff.user_files
WHERE s3_key = $1 AND user_id = $2
`;
const result = await pool.query(query, [s3Key, userId]);
return result.rows[0];
},
/**
* Получить список файлов пользователя
*/
async findByUser(userId, options = {}) {
const {
fileType = 'image',
folder = 'images_input',
limit = 50,
offset = 0,
orderBy = 'created_at',
order = 'DESC',
} = options;
const validOrderColumns = ['created_at', 'original_filename', 'file_size'];
const validOrders = ['ASC', 'DESC'];
const safeOrderBy = validOrderColumns.includes(orderBy) ? orderBy : 'created_at';
const safeOrder = validOrders.includes(order?.toUpperCase()) ? order.toUpperCase() : 'DESC';
const query = `
SELECT id, user_id, s3_key, original_filename, file_size, content_type, file_type, folder, created_at
FROM uno_bff.user_files
WHERE user_id = $1
AND ($2::VARCHAR IS NULL OR file_type = $2)
AND ($3::VARCHAR IS NULL OR folder = $3)
ORDER BY ${safeOrderBy} ${safeOrder}
LIMIT $4 OFFSET $5
`;
const values = [
userId,
fileType || null,
folder || null,
limit,
offset,
];
const result = await pool.query(query, values);
return result.rows;
},
/**
* Получить общее количество файлов пользователя
*/
async countByUser(userId, options = {}) {
const { fileType = 'image', folder = 'images_input' } = options;
const query = `
SELECT COUNT(*) as total
FROM uno_bff.user_files
WHERE user_id = $1
AND ($2::VARCHAR IS NULL OR file_type = $2)
AND ($3::VARCHAR IS NULL OR folder = $3)
`;
const values = [userId, fileType || null, folder || null];
const result = await pool.query(query, values);
return parseInt(result.rows[0].total, 10);
},
/**
* Удалить запись о файле (по ID и владельцу)
*/
async deleteByIdAndOwner(fileId, userId) {
const query = `
DELETE FROM uno_bff.user_files
WHERE id = $1 AND user_id = $2
RETURNING s3_key
`;
const result = await pool.query(query, [fileId, userId]);
return result.rows[0];
},
/**
* Удалить запись о файле по S3 ключу
*/
async deleteByKeyAndOwner(s3Key, userId) {
const query = `
DELETE FROM uno_bff.user_files
WHERE s3_key = $1 AND user_id = $2
RETURNING s3_key
`;
const result = await pool.query(query, [s3Key, userId]);
return result.rows[0];
},
/**
* Получить файлы генерации
*/
async findByGeneration(generationUuid, userId) {
const query = `
SELECT id, user_id, s3_key, original_filename, file_size, content_type, file_type, folder, generation_uuid, generation_step_id, created_at
FROM uno_bff.user_files
WHERE generation_uuid = $1 AND user_id = $2
ORDER BY created_at ASC
`;
const result = await pool.query(query, [generationUuid, userId]);
return result.rows;
},
/**
* Получить файлы шага генерации
*/
async findByGenerationStep(generationUuid, stepId, userId) {
const query = `
SELECT f.id, f.user_id, f.s3_key, f.original_filename, f.file_size, f.content_type, f.file_type, f.folder, f.generation_uuid, f.generation_step_id, f.created_at
FROM uno_bff.user_files f
INNER JOIN uno_bff.generation_steps gs ON f.generation_step_id = gs.id
WHERE f.generation_uuid = $1 AND gs.step_id = $2 AND f.user_id = $3
ORDER BY f.created_at ASC
`;
const result = await pool.query(query, [generationUuid, stepId, userId]);
return result.rows;
},
};
+105
View File
@@ -0,0 +1,105 @@
import { pool } from "../db.js";
export async function findUserByEmail(email) {
const { rows } = await pool.query(`
SELECT id, email, password_hash, display_name, role, status, balance, last_login_at, created_at, updated_at
FROM uno_bff.users WHERE lower(email) = lower($1) LIMIT 1
`, [email]);
return rows[0] || null;
}
export async function findUserByEmailAndPassword(email, password) {
const { rows } = await pool.query(`
SELECT id, email, password_hash, display_name, role, status, balance, last_login_at, created_at, updated_at
FROM uno_bff.users
WHERE lower(email) = lower($1) AND status = 'active'
AND password_hash::text = sites.crypt($2::text, password_hash::text)
LIMIT 1
`, [email, password]);
return rows[0] || null;
}
export async function findUserById(userId) {
const { rows } = await pool.query(`
SELECT id, email, display_name, role, status, balance, created_at
FROM uno_bff.users WHERE id = $1 AND status = 'active'
`, [userId]);
return rows[0] || null;
}
export async function updateUserBalance(userId, delta) {
const { rows } = await pool.query(`
UPDATE uno_bff.users
SET balance = balance + $2, updated_at = NOW()
WHERE id = $1
RETURNING balance
`, [userId, delta]);
return rows[0]?.balance ?? null;
}
export async function touchLastLogin(userId) {
await pool.query(`UPDATE uno_bff.users SET last_login_at = now(), updated_at = now() WHERE id = $1`, [userId]);
}
export async function createUser({ email, password, displayName }) {
const { rows } = await pool.query(`
INSERT INTO uno_bff.users (email, password_hash, display_name, role, status)
VALUES (lower($1), sites.crypt($2::text, sites.gen_salt('bf', 10)), $3, 'user', 'active')
RETURNING id, email, display_name, role, status, balance, created_at
`, [email, password, displayName || null]);
return rows[0];
}
export async function userExistsByEmail(email) {
const { rows } = await pool.query(`SELECT 1 FROM uno_bff.users WHERE lower(email) = lower($1) LIMIT 1`, [email]);
return rows.length > 0;
}
export async function findUserByTelegramId(telegramId) {
const { rows } = await pool.query(`SELECT * FROM uno_bff.users WHERE telegram_id = $1 AND status = 'active' LIMIT 1`, [telegramId]);
return rows[0] || null;
}
export async function linkTelegramToUser(userId, telegramId) {
const { rows } = await pool.query(`UPDATE uno_bff.users SET telegram_id = $2 WHERE id = $1 RETURNING *`, [userId, telegramId]);
return rows[0] || null;
}
export async function createUserFromTelegram({ telegramId, displayName, email }) {
const { rows } = await pool.query(
`INSERT INTO uno_bff.users (email, password_hash, display_name, telegram_id, role, status)
VALUES (lower($1), '!tg!' || sites.gen_salt('bf', 10), $2, $3, 'user', 'active')
RETURNING id, email, display_name, role, status, balance, telegram_id, created_at`,
[email, displayName || null, telegramId]
);
return rows[0];
}
export async function countTelegramSignupsByIpLast24h(ipAddress) {
if (!ipAddress) return 0;
const { rows } = await pool.query(
`SELECT COUNT(*)::int AS c FROM uno_bff.signup_attempts
WHERE ip_address = $1 AND provider = 'telegram' AND created_at > now() - interval '24 hours'`,
[ipAddress]
);
return rows[0]?.c ?? 0;
}
export async function recordSignupAttempt({ ipAddress, provider, telegramId, userAgent }) {
await pool.query(
`INSERT INTO uno_bff.signup_attempts (ip_address, provider, telegram_id, user_agent)
VALUES ($1, $2, $3, $4)`,
[ipAddress || null, provider, telegramId || null, userAgent || null]
);
}
export async function findTelegramOwner(telegramId) {
const { rows } = await pool.query(`SELECT id FROM uno_bff.users WHERE telegram_id = $1 LIMIT 1`, [telegramId]);
return rows[0] || null;
}
export async function unlinkTelegramFromUser(userId) {
const { rows } = await pool.query(`UPDATE uno_bff.users SET telegram_id = NULL, updated_at = now() WHERE id = $1 RETURNING id`, [userId]);
return rows[0] || null;
}
@@ -0,0 +1,66 @@
import { pool } from "../db.js";
export async function findUserByEmail(email) {
const { rows } = await pool.query(`
SELECT id, email, password_hash, display_name, role, status, balance, last_login_at, created_at, updated_at
FROM uno_bff.users WHERE lower(email) = lower($1) LIMIT 1
`, [email]);
return rows[0] || null;
}
export async function findUserByEmailAndPassword(email, password) {
const { rows } = await pool.query(`
SELECT id, email, password_hash, display_name, role, status, balance, last_login_at, created_at, updated_at
FROM uno_bff.users
WHERE lower(email) = lower($1) AND status = 'active'
AND password_hash::text = sites.crypt($2::text, password_hash::text)
LIMIT 1
`, [email, password]);
return rows[0] || null;
}
export async function findUserById(userId) {
const { rows } = await pool.query(`
SELECT id, email, display_name, role, status, balance, created_at
FROM uno_bff.users WHERE id = $1 AND status = 'active'
`, [userId]);
return rows[0] || null;
}
export async function updateUserBalance(userId, delta) {
const { rows } = await pool.query(`
UPDATE uno_bff.users
SET balance = balance + $2, updated_at = NOW()
WHERE id = $1
RETURNING balance
`, [userId, delta]);
return rows[0]?.balance ?? null;
}
export async function touchLastLogin(userId) {
await pool.query(`UPDATE uno_bff.users SET last_login_at = now(), updated_at = now() WHERE id = $1`, [userId]);
}
export async function createUser({ email, password, displayName }) {
const { rows } = await pool.query(`
INSERT INTO uno_bff.users (email, password_hash, display_name, role, status)
VALUES (lower($1), sites.crypt($2::text, sites.gen_salt('bf', 10)), $3, 'user', 'active')
RETURNING id, email, display_name, role, status, balance, created_at
`, [email, password, displayName || null]);
return rows[0];
}
export async function userExistsByEmail(email) {
const { rows } = await pool.query(`SELECT 1 FROM uno_bff.users WHERE lower(email) = lower($1) LIMIT 1`, [email]);
return rows.length > 0;
}
export async function findUserByTelegramId(telegramId) {
const { rows } = await pool.query(`SELECT * FROM uno_bff.users WHERE telegram_id = $1 AND status = 'active' LIMIT 1`, [telegramId]);
return rows[0] || null;
}
export async function linkTelegramToUser(userId, telegramId) {
const { rows } = await pool.query(`UPDATE uno_bff.users SET telegram_id = $2 WHERE id = $1 RETURNING *`, [userId, telegramId]);
return rows[0] || null;
}
@@ -0,0 +1,94 @@
import { pool } from "../db.js";
export async function findUserByEmail(email) {
const { rows } = await pool.query(`
SELECT id, email, password_hash, display_name, role, status, balance, last_login_at, created_at, updated_at
FROM uno_bff.users WHERE lower(email) = lower($1) LIMIT 1
`, [email]);
return rows[0] || null;
}
export async function findUserByEmailAndPassword(email, password) {
const { rows } = await pool.query(`
SELECT id, email, password_hash, display_name, role, status, balance, last_login_at, created_at, updated_at
FROM uno_bff.users
WHERE lower(email) = lower($1) AND status = 'active'
AND password_hash::text = sites.crypt($2::text, password_hash::text)
LIMIT 1
`, [email, password]);
return rows[0] || null;
}
export async function findUserById(userId) {
const { rows } = await pool.query(`
SELECT id, email, display_name, role, status, balance, created_at
FROM uno_bff.users WHERE id = $1 AND status = 'active'
`, [userId]);
return rows[0] || null;
}
export async function updateUserBalance(userId, delta) {
const { rows } = await pool.query(`
UPDATE uno_bff.users
SET balance = balance + $2, updated_at = NOW()
WHERE id = $1
RETURNING balance
`, [userId, delta]);
return rows[0]?.balance ?? null;
}
export async function touchLastLogin(userId) {
await pool.query(`UPDATE uno_bff.users SET last_login_at = now(), updated_at = now() WHERE id = $1`, [userId]);
}
export async function createUser({ email, password, displayName }) {
const { rows } = await pool.query(`
INSERT INTO uno_bff.users (email, password_hash, display_name, role, status)
VALUES (lower($1), sites.crypt($2::text, sites.gen_salt('bf', 10)), $3, 'user', 'active')
RETURNING id, email, display_name, role, status, balance, created_at
`, [email, password, displayName || null]);
return rows[0];
}
export async function userExistsByEmail(email) {
const { rows } = await pool.query(`SELECT 1 FROM uno_bff.users WHERE lower(email) = lower($1) LIMIT 1`, [email]);
return rows.length > 0;
}
export async function findUserByTelegramId(telegramId) {
const { rows } = await pool.query(`SELECT * FROM uno_bff.users WHERE telegram_id = $1 AND status = 'active' LIMIT 1`, [telegramId]);
return rows[0] || null;
}
export async function linkTelegramToUser(userId, telegramId) {
const { rows } = await pool.query(`UPDATE uno_bff.users SET telegram_id = $2 WHERE id = $1 RETURNING *`, [userId, telegramId]);
return rows[0] || null;
}
export async function createUserFromTelegram({ telegramId, displayName, email }) {
const { rows } = await pool.query(
`INSERT INTO uno_bff.users (email, password_hash, display_name, telegram_id, role, status)
VALUES (lower($1), '!tg!' || sites.gen_salt('bf', 10), $2, $3, 'user', 'active')
RETURNING id, email, display_name, role, status, balance, telegram_id, created_at`,
[email, displayName || null, telegramId]
);
return rows[0];
}
export async function countTelegramSignupsByIpLast24h(ipAddress) {
if (!ipAddress) return 0;
const { rows } = await pool.query(
`SELECT COUNT(*)::int AS c FROM uno_bff.signup_attempts
WHERE ip_address = $1 AND provider = 'telegram' AND created_at > now() - interval '24 hours'`,
[ipAddress]
);
return rows[0]?.c ?? 0;
}
export async function recordSignupAttempt({ ipAddress, provider, telegramId, userAgent }) {
await pool.query(
`INSERT INTO uno_bff.signup_attempts (ip_address, provider, telegram_id, user_agent)
VALUES ($1, $2, $3, $4)`,
[ipAddress || null, provider, telegramId || null, userAgent || null]
);
}