feat: add user authentication UI to header component

Add login and register dialogs, user dropdown menu with profile link and logout option, and integrate with session management hook. Prevent hydration mismatches by conditionally rendering auth elements after mount. Update package-lock.json with new @esbuild dependencies for build tooling.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-04 02:33:29 -03:00
1 parent e641138887
commit 55eebde0ba
55 files changed
+7353 -7

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
/**
* Módulo de conexão com PostgreSQL
* Gerencia pool de conexões e fornece helpers para queries
*/
import { Pool, QueryResult } from "pg";
let pool: Pool | null = null;
/**
* Valida variáveis de ambiente necessárias
*/
function validateEnvVars() {
if (!process.env.PG_DB) {
throw new Error(
"PG_DB não está definido. Configure a variável de ambiente PG_DB com a connection string do PostgreSQL."
);
}
}
/**
* Obtém ou cria o pool de conexões PostgreSQL
*/
export function getPool(): Pool {
if (!pool) {
validateEnvVars();
const connectionString = process.env.PG_DB;
pool = new Pool({
connectionString,
min: 2, // Mínimo de conexões no pool
max: 10, // Máximo de conexões no pool
idleTimeoutMillis: 30000, // Fecha conexões idle após 30s
connectionTimeoutMillis: 5000, // Timeout ao tentar conectar
});
// Event listeners para debug
pool.on("error", (err) => {
console.error("❌ Erro inesperado no pool de conexões PostgreSQL:", err);
});
pool.on("connect", () => {
console.log("✅ Nova conexão estabelecida com PostgreSQL");
});
}
return pool;
}
/**
* Executa uma query SQL com parâmetros
* @param text - SQL query com placeholders ($1, $2, etc)
* @param params - Array de parâmetros para a query
* @returns Resultado da query
*/
export async function query<T = any>(
text: string,
params?: any[]
): Promise<QueryResult<T>> {
const poolInstance = getPool();
const start = Date.now();
try {
const result = await poolInstance.query<T>(text, params);
const duration = Date.now() - start;
// Log apenas em desenvolvimento ou queries lentas (>100ms)
if (process.env.NODE_ENV !== "production" || duration > 100) {
console.log("📊 Query executada", {
duration: `${duration}ms`,
query: text.substring(0, 100),
});
}
return result;
} catch (error) {
console.error("❌ Erro ao executar query:", {
query: text.substring(0, 100),
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* Health check do banco de dados
* @returns true se o banco está acessível, false caso contrário
*/
export async function healthCheck(): Promise<boolean> {
try {
const poolInstance = getPool();
const result = await poolInstance.query("SELECT NOW()");
return result.rows.length > 0;
} catch (error) {
console.error("❌ Health check falhou:", error);
return false;
}
}
/**
* Fecha o pool de conexões
* Útil para cleanup em testes ou shutdown graceful
*/
export async function closePool(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
console.log("🔌 Pool de conexões PostgreSQL fechado");
}
}
// Cleanup ao encerrar o processo
if (typeof process !== "undefined") {
process.on("SIGINT", async () => {
await closePool();
process.exit(0);
});
process.on("SIGTERM", async () => {
await closePool();
process.exit(0);
});
}
+41
View File
@@ -0,0 +1,41 @@
-- Migration 001: Tabelas de usuários e personagens
-- Criado em: 2025-01-27
-- Tabela de usuários
CREATE TABLE IF NOT EXISTS users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
cpf VARCHAR(14) UNIQUE NOT NULL,
account_level VARCHAR(20) DEFAULT 'free' CHECK (account_level IN ('free', 'bronze', 'silver', 'gold')),
has_donated BOOLEAN DEFAULT FALSE,
whatsapp_notifications BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE
);
-- Tabela de personagens dos usuários
CREATE TABLE IF NOT EXISTS user_characters (
character_id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
character_name VARCHAR(100) NOT NULL,
server_name VARCHAR(50) NOT NULL,
is_main BOOLEAN DEFAULT FALSE,
verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(character_name, server_name)
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_user_characters_user_id ON user_characters(user_id);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_cpf ON users(cpf);
+47
View File
@@ -0,0 +1,47 @@
-- Migration 002: Tabela de servidores do Tibia
-- Criado em: 2025-01-27
-- Tornar CPF opcional na tabela users (para compatibilidade)
ALTER TABLE users ALTER COLUMN cpf DROP NOT NULL;
-- Tabela de servidores
CREATE TABLE IF NOT EXISTS servers (
server_id SERIAL PRIMARY KEY,
server_name VARCHAR(50) UNIQUE NOT NULL,
server_type VARCHAR(20) CHECK (server_type IN ('pvp', 'no-pvp', 'retro-pvp', 'hardcore-pvp')),
location VARCHAR(10) CHECK (location IN ('NA', 'SA', 'EU', 'OC')),
is_active BOOLEAN DEFAULT TRUE,
-- Estatísticas de tracking
total_listings INTEGER DEFAULT 0,
total_users INTEGER DEFAULT 0,
total_subscribers INTEGER DEFAULT 0,
total_transactions INTEGER DEFAULT 0,
total_volume DECIMAL(15, 2) DEFAULT 0,
average_listing_price DECIMAL(10, 2) DEFAULT 0,
last_activity_at TIMESTAMP,
-- Metadados
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_servers_name ON servers(server_name);
CREATE INDEX IF NOT EXISTS idx_servers_active ON servers(is_active);
CREATE INDEX IF NOT EXISTS idx_servers_type ON servers(server_type);
-- Trigger para atualizar updated_at automaticamente
CREATE OR REPLACE FUNCTION update_servers_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_servers_updated_at
BEFORE UPDATE ON servers
FOR EACH ROW
EXECUTE FUNCTION update_servers_updated_at();
@@ -0,0 +1,10 @@
-- Migration 003: Corrigir constraint de location para incluir BR
-- Criado em: 2025-01-27
-- Remover constraint antigo
ALTER TABLE servers DROP CONSTRAINT IF EXISTS servers_location_check;
-- Adicionar novo constraint que inclui BR
ALTER TABLE servers ADD CONSTRAINT servers_location_check
CHECK (location IN ('NA', 'SA', 'EU', 'OC', 'BR'));
+54
View File
@@ -0,0 +1,54 @@
-- Migration 004: Tabela de anúncios (listings) do marketplace
-- Criado em: 2025-01-27
-- Tabela de anúncios
CREATE TABLE IF NOT EXISTS listings (
listing_id SERIAL PRIMARY KEY,
seller_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
item_id INTEGER NOT NULL, -- referência ao banco de itens do TibiaWiki
item_name VARCHAR(255) NOT NULL,
quantity INTEGER DEFAULT 1 CHECK (quantity > 0),
server_name VARCHAR(50) NOT NULL,
-- Preços aceitos
price_pix DECIMAL(10, 2),
price_tibia_coins INTEGER,
price_gold_coins BIGINT,
-- Leilão
is_auction BOOLEAN DEFAULT FALSE,
current_bid_amount DECIMAL(10, 2),
current_bidder_id INTEGER REFERENCES users(user_id),
auction_ends_at TIMESTAMP,
-- Metadados
description TEXT,
listing_status VARCHAR(20) DEFAULT 'active' CHECK (listing_status IN ('active', 'sold', 'cancelled', 'in_escrow', 'expired')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sold_at TIMESTAMP,
views_count INTEGER DEFAULT 0
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_listings_status ON listings(listing_status);
CREATE INDEX IF NOT EXISTS idx_listings_item_id ON listings(item_id);
CREATE INDEX IF NOT EXISTS idx_listings_seller_id ON listings(seller_id);
CREATE INDEX IF NOT EXISTS idx_listings_server ON listings(server_name);
CREATE INDEX IF NOT EXISTS idx_listings_created_at ON listings(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_listings_auction ON listings(is_auction, auction_ends_at) WHERE is_auction = TRUE;
-- Trigger para atualizar updated_at automaticamente
CREATE OR REPLACE FUNCTION update_listings_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_listings_updated_at
BEFORE UPDATE ON listings
FOR EACH ROW
EXECUTE FUNCTION update_listings_updated_at();
+20
View File
@@ -0,0 +1,20 @@
-- Migration 005: Tabela de lances em leilões
-- Criado em: 2025-01-27
-- Tabela de lances
CREATE TABLE IF NOT EXISTS bids (
bid_id SERIAL PRIMARY KEY,
listing_id INTEGER NOT NULL REFERENCES listings(listing_id) ON DELETE CASCADE,
bidder_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
bid_amount DECIMAL(10, 2) NOT NULL,
bid_type VARCHAR(20) CHECK (bid_type IN ('pix', 'tibia_coins', 'gold_coins')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_bids_listing ON bids(listing_id);
CREATE INDEX IF NOT EXISTS idx_bids_bidder ON bids(bidder_id);
CREATE INDEX IF NOT EXISTS idx_bids_active ON bids(is_active) WHERE is_active = TRUE;
CREATE INDEX IF NOT EXISTS idx_bids_amount ON bids(listing_id, bid_amount DESC) WHERE is_active = TRUE;
+53
View File
@@ -0,0 +1,53 @@
-- Migration 006: Tabela de transações do sistema de escrow
-- Criado em: 2025-01-27
-- Tabela de transações
CREATE TABLE IF NOT EXISTS transactions (
transaction_id SERIAL PRIMARY KEY,
listing_id INTEGER REFERENCES listings(listing_id),
buyer_id INTEGER NOT NULL REFERENCES users(user_id),
seller_id INTEGER NOT NULL REFERENCES users(user_id),
-- Detalhes do item
item_name VARCHAR(255) NOT NULL,
quantity INTEGER NOT NULL,
server_name VARCHAR(50) NOT NULL,
-- Valores
amount DECIMAL(10, 2) NOT NULL,
payment_method VARCHAR(20) CHECK (payment_method IN ('pix', 'tibia_coins', 'gold_coins')),
escrow_fee DECIMAL(10, 2), -- 2% da transação
-- Status da transação
transaction_status VARCHAR(30) DEFAULT 'pending' CHECK (transaction_status IN (
'pending', -- aguardando depósito
'deposited', -- comprador depositou
'item_delivered', -- vendedor entregou
'completed', -- pagamento liberado
'disputed', -- em disputa
'cancelled', -- cancelada
'refunded' -- reembolsada
)),
-- Datas importantes
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deposited_at TIMESTAMP,
delivered_at TIMESTAMP,
completed_at TIMESTAMP,
-- Confirmações
buyer_confirmed BOOLEAN DEFAULT FALSE,
seller_confirmed BOOLEAN DEFAULT FALSE,
-- Notas
buyer_notes TEXT,
seller_notes TEXT
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_transactions_buyer ON transactions(buyer_id);
CREATE INDEX IF NOT EXISTS idx_transactions_seller ON transactions(seller_id);
CREATE INDEX IF NOT EXISTS idx_transactions_status ON transactions(transaction_status);
CREATE INDEX IF NOT EXISTS idx_transactions_listing ON transactions(listing_id);
CREATE INDEX IF NOT EXISTS idx_transactions_created_at ON transactions(created_at DESC);
+22
View File
@@ -0,0 +1,22 @@
-- Migration 007: Tabela de depósitos em escrow
-- Criado em: 2025-01-27
-- Tabela de depósitos em escrow
CREATE TABLE IF NOT EXISTS escrow_deposits (
deposit_id SERIAL PRIMARY KEY,
transaction_id INTEGER NOT NULL REFERENCES transactions(transaction_id) ON DELETE CASCADE,
depositor_id INTEGER NOT NULL REFERENCES users(user_id),
amount DECIMAL(10, 2) NOT NULL,
payment_method VARCHAR(20),
payment_proof_url TEXT,
deposit_status VARCHAR(20) DEFAULT 'pending' CHECK (deposit_status IN ('pending', 'confirmed', 'released', 'refunded')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
confirmed_at TIMESTAMP,
released_at TIMESTAMP
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_escrow_deposits_transaction ON escrow_deposits(transaction_id);
CREATE INDEX IF NOT EXISTS idx_escrow_deposits_depositor ON escrow_deposits(depositor_id);
CREATE INDEX IF NOT EXISTS idx_escrow_deposits_status ON escrow_deposits(deposit_status);
+21
View File
@@ -0,0 +1,21 @@
-- Migration 008: Tabela de disputas
-- Criado em: 2025-01-27
-- Tabela de disputas
CREATE TABLE IF NOT EXISTS disputes (
dispute_id SERIAL PRIMARY KEY,
transaction_id INTEGER NOT NULL REFERENCES transactions(transaction_id) ON DELETE CASCADE,
raised_by_user_id INTEGER NOT NULL REFERENCES users(user_id),
dispute_reason TEXT NOT NULL,
dispute_status VARCHAR(20) DEFAULT 'open' CHECK (dispute_status IN ('open', 'investigating', 'resolved', 'closed')),
resolution_notes TEXT,
resolved_by_admin_id INTEGER REFERENCES users(user_id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resolved_at TIMESTAMP
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_disputes_transaction ON disputes(transaction_id);
CREATE INDEX IF NOT EXISTS idx_disputes_raised_by ON disputes(raised_by_user_id);
CREATE INDEX IF NOT EXISTS idx_disputes_status ON disputes(dispute_status);
@@ -0,0 +1,21 @@
-- Migration 009: Tabela de histórico de preços de itens
-- Criado em: 2025-01-27
-- Tabela de histórico de preços
CREATE TABLE IF NOT EXISTS item_price_history (
price_history_id SERIAL PRIMARY KEY,
item_id INTEGER NOT NULL,
item_name VARCHAR(255) NOT NULL,
server_name VARCHAR(50),
price_amount DECIMAL(10, 2) NOT NULL,
price_type VARCHAR(20) CHECK (price_type IN ('pix', 'tibia_coins', 'gold_coins')),
transaction_id INTEGER REFERENCES transactions(transaction_id),
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_price_history_item ON item_price_history(item_id, recorded_at DESC);
CREATE INDEX IF NOT EXISTS idx_price_history_server ON item_price_history(server_name);
CREATE INDEX IF NOT EXISTS idx_price_history_transaction ON item_price_history(transaction_id);
CREATE INDEX IF NOT EXISTS idx_price_history_recorded_at ON item_price_history(recorded_at DESC);
+23
View File
@@ -0,0 +1,23 @@
-- Migration 010: Tabela de notificações
-- Criado em: 2025-01-27
-- Tabela de notificações
CREATE TABLE IF NOT EXISTS notifications (
notification_id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
notification_type VARCHAR(30) CHECK (notification_type IN ('bid', 'sale', 'purchase', 'escrow', 'dispute', 'system')),
title VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
related_id INTEGER, -- ID da transação/listing relacionado
is_read BOOLEAN DEFAULT FALSE,
send_whatsapp BOOLEAN DEFAULT FALSE,
whatsapp_sent BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, is_read);
CREATE INDEX IF NOT EXISTS idx_notifications_type ON notifications(notification_type);
CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(user_id, is_read) WHERE is_read = FALSE;
+26
View File
@@ -0,0 +1,26 @@
-- Migration 011: Tabela de doações
-- Criado em: 2025-01-27
-- Tabela de doações
CREATE TABLE IF NOT EXISTS donations (
donation_id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(user_id),
donor_name VARCHAR(100),
donor_email VARCHAR(255),
amount DECIMAL(10, 2) NOT NULL,
donation_level VARCHAR(20) CHECK (donation_level IN ('coffee', 'snack', 'lunch', 'custom')),
payment_method VARCHAR(20) DEFAULT 'pix',
payment_status VARCHAR(20) DEFAULT 'pending' CHECK (payment_status IN ('pending', 'completed', 'failed', 'refunded')),
abacatepay_id VARCHAR(100),
message TEXT,
is_anonymous BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
);
-- Índices para performance
CREATE INDEX IF NOT EXISTS idx_donations_user ON donations(user_id);
CREATE INDEX IF NOT EXISTS idx_donations_status ON donations(payment_status);
CREATE INDEX IF NOT EXISTS idx_donations_abacatepay_id ON donations(abacatepay_id);
CREATE INDEX IF NOT EXISTS idx_donations_created_at ON donations(created_at DESC);
+113
View File
@@ -0,0 +1,113 @@
/**
* Script de execução de migrations
* Executa todas as migrations SQL em ordem
*/
// Carregar variáveis de ambiente
import { config } from "dotenv";
import { resolve } from "path";
// Tentar carregar .env.local primeiro, depois .env
config({ path: resolve(process.cwd(), ".env.local") });
config({ path: resolve(process.cwd(), ".env") });
import { readFileSync } from "fs";
import { join } from "path";
import { getPool, query, healthCheck } from "../index";
const MIGRATIONS_DIR = join(__dirname, ".");
// Lista de migrations em ordem
const MIGRATIONS = [
"001_users.sql",
"002_servers.sql",
"003_fix_servers_location.sql",
"004_listings.sql",
"005_bids.sql",
"006_transactions.sql",
"007_escrow_deposits.sql",
"008_disputes.sql",
"009_item_price_history.sql",
"010_notifications.sql",
"011_donations.sql",
];
async function runMigrations() {
console.log("🚀 Iniciando execução de migrations...\n");
// Verificar conexão com banco
console.log("🔍 Verificando conexão com PostgreSQL...");
const isHealthy = await healthCheck();
if (!isHealthy) {
console.error("❌ Falha na conexão com PostgreSQL. Verifique a variável PG_DB.");
process.exit(1);
}
console.log("✅ Conexão estabelecida com sucesso\n");
// Verificar tabela de controle de migrations (opcional)
try {
await query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(255) PRIMARY KEY,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
} catch (error) {
console.error("❌ Erro ao criar tabela de controle:", error);
process.exit(1);
}
// Executar cada migration
for (const migrationFile of MIGRATIONS) {
const migrationPath = join(MIGRATIONS_DIR, migrationFile);
const version = migrationFile.replace(".sql", "");
try {
// Verificar se já foi executada
const checkResult = await query<{ version: string }>(
"SELECT version FROM schema_migrations WHERE version = $1",
[version]
);
if (checkResult.rows.length > 0) {
console.log(`⏭️ Migration ${version} já executada, pulando...`);
continue;
}
console.log(`📝 Executando migration: ${migrationFile}...`);
// Ler arquivo SQL
const sql = readFileSync(migrationPath, "utf-8");
// Executar migration
await query(sql);
// Registrar como executada
await query(
"INSERT INTO schema_migrations (version) VALUES ($1)",
[version]
);
console.log(`✅ Migration ${version} executada com sucesso\n`);
} catch (error) {
console.error(`❌ Erro ao executar migration ${version}:`, error);
process.exit(1);
}
}
console.log("🎉 Todas as migrations foram executadas com sucesso!");
// Fechar pool
const pool = getPool();
await pool.end();
process.exit(0);
}
// Executar
runMigrations().catch((error) => {
console.error("❌ Erro fatal:", error);
process.exit(1);
});
+111
View File
@@ -0,0 +1,111 @@
/**
* Queries de bids (lances em leilões)
* Operações para a tabela bids
*/
import { query } from "../index";
export interface Bid {
bid_id: number;
listing_id: number;
bidder_id: number;
bid_amount: number;
bid_type: "pix" | "tibia_coins" | "gold_coins" | null;
created_at: Date;
is_active: boolean;
}
export interface CreateBidData {
listing_id: number;
bidder_id: number;
bid_amount: number;
bid_type?: "pix" | "tibia_coins" | "gold_coins";
}
/**
* Cria um novo lance
*/
export async function createBid(data: CreateBidData): Promise<Bid> {
const result = await query<Bid>(
`
INSERT INTO bids (listing_id, bidder_id, bid_amount, bid_type)
VALUES ($1, $2, $3, $4)
RETURNING *
`,
[data.listing_id, data.bidder_id, data.bid_amount, data.bid_type || null]
);
return result.rows[0];
}
/**
* Busca lances de um anúncio
*/
export async function getBidsByListing(
listingId: number,
activeOnly: boolean = true
): Promise<Bid[]> {
let sql = `
SELECT * FROM bids
WHERE listing_id = $1
`;
const params: any[] = [listingId];
if (activeOnly) {
sql += ` AND is_active = TRUE`;
}
sql += ` ORDER BY bid_amount DESC, created_at DESC`;
const result = await query<Bid>(sql, params);
return result.rows;
}
/**
* Busca o maior lance de um anúncio
*/
export async function getHighestBid(listingId: number): Promise<Bid | null> {
const result = await query<Bid>(
`
SELECT * FROM bids
WHERE listing_id = $1 AND is_active = TRUE
ORDER BY bid_amount DESC, created_at DESC
LIMIT 1
`,
[listingId]
);
return result.rows[0] || null;
}
/**
* Cancela um lance (desativa)
*/
export async function cancelBid(bidId: number): Promise<boolean> {
const result = await query(
`
UPDATE bids
SET is_active = FALSE
WHERE bid_id = $1
`,
[bidId]
);
return result.rowCount > 0;
}
/**
* Busca lance por ID
*/
export async function getBidById(bidId: number): Promise<Bid | null> {
const result = await query<Bid>(
`
SELECT * FROM bids WHERE bid_id = $1
`,
[bidId]
);
return result.rows[0] || null;
}
+224
View File
@@ -0,0 +1,224 @@
/**
* Queries de personagens
* CRUD completo para a tabela user_characters
*/
import { query } from "../index";
export interface Character {
character_id: number;
user_id: number;
character_name: string;
server_name: string;
is_main: boolean;
verified: boolean;
created_at: Date;
}
export interface CreateCharacterData {
character_name: string;
server_name: string;
is_main?: boolean;
}
export interface UpdateCharacterData {
character_name?: string;
server_name?: string;
is_main?: boolean;
}
/**
* Cria um novo personagem
* Se isMain=true, desmarca outros personagens como main
*/
export async function createCharacter(
userId: number,
data: CreateCharacterData
): Promise<Character> {
// Usar transação para garantir atomicidade
const { getPool } = await import("../index");
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
// Se este personagem será main, desmarcar outros
if (data.is_main) {
await client.query(
`
UPDATE user_characters
SET is_main = FALSE
WHERE user_id = $1
`,
[userId]
);
}
// Inserir novo personagem
const result = await client.query<Character>(
`
INSERT INTO user_characters (user_id, character_name, server_name, is_main)
VALUES ($1, $2, $3, $4)
RETURNING *
`,
[userId, data.character_name, data.server_name, data.is_main || false]
);
await client.query("COMMIT");
return result.rows[0];
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Lista todos os personagens de um usuário
*/
export async function getUserCharacters(userId: number): Promise<Character[]> {
const result = await query<Character>(
`
SELECT * FROM user_characters
WHERE user_id = $1
ORDER BY is_main DESC, created_at ASC
`,
[userId]
);
return result.rows;
}
/**
* Busca personagem por ID
*/
export async function getCharacterById(
characterId: number
): Promise<Character | null> {
const result = await query<Character>(
`
SELECT * FROM user_characters WHERE character_id = $1
`,
[characterId]
);
return result.rows[0] || null;
}
/**
* Atualiza personagem
* Verifica ownership e se isMain=true, desmarca outros
*/
export async function updateCharacter(
characterId: number,
userId: number,
data: UpdateCharacterData
): Promise<Character> {
// Verificar ownership primeiro
const existing = await getCharacterById(characterId);
if (!existing) {
throw new Error("Personagem não encontrado");
}
if (existing.user_id !== userId) {
throw new Error("Você não tem permissão para atualizar este personagem");
}
// Usar transação para garantir atomicidade
const { getPool } = await import("../index");
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
// Se este personagem será main, desmarcar outros
if (data.is_main) {
await client.query(
`
UPDATE user_characters
SET is_main = FALSE
WHERE user_id = $1 AND character_id != $2
`,
[userId, characterId]
);
}
// Construir query de update
const updates: string[] = [];
const values: any[] = [];
let paramIndex = 1;
if (data.character_name !== undefined) {
updates.push(`character_name = $${paramIndex++}`);
values.push(data.character_name);
}
if (data.server_name !== undefined) {
updates.push(`server_name = $${paramIndex++}`);
values.push(data.server_name);
}
if (data.is_main !== undefined) {
updates.push(`is_main = $${paramIndex++}`);
values.push(data.is_main);
}
if (updates.length === 0) {
// Se não há updates, retorna personagem atual
await client.query("COMMIT");
return existing;
}
values.push(characterId);
const result = await client.query<Character>(
`
UPDATE user_characters
SET ${updates.join(", ")}
WHERE character_id = $${paramIndex}
RETURNING *
`,
values
);
await client.query("COMMIT");
return result.rows[0];
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Remove personagem
* Verifica ownership antes de remover
*/
export async function deleteCharacter(
characterId: number,
userId: number
): Promise<void> {
// Verificar ownership
const existing = await getCharacterById(characterId);
if (!existing) {
throw new Error("Personagem não encontrado");
}
if (existing.user_id !== userId) {
throw new Error("Você não tem permissão para remover este personagem");
}
await query(
`
DELETE FROM user_characters WHERE character_id = $1
`,
[characterId]
);
}
+338
View File
@@ -0,0 +1,338 @@
/**
* Queries de listings (anúncios)
* CRUD completo para a tabela listings
*/
import { query } from "../index";
export interface Listing {
listing_id: number;
seller_id: number;
item_id: number;
item_name: string;
quantity: number;
server_name: string;
price_pix: number | null;
price_tibia_coins: number | null;
price_gold_coins: number | null;
is_auction: boolean;
current_bid_amount: number | null;
current_bidder_id: number | null;
auction_ends_at: Date | null;
description: string | null;
listing_status: "active" | "sold" | "cancelled" | "in_escrow" | "expired";
created_at: Date;
updated_at: Date;
sold_at: Date | null;
views_count: number;
}
export interface CreateListingData {
seller_id: number;
item_id: number;
item_name: string;
quantity: number;
server_name: string;
price_pix?: number;
price_tibia_coins?: number;
price_gold_coins?: number;
is_auction?: boolean;
auction_ends_at?: Date;
description?: string;
}
export interface UpdateListingData {
quantity?: number;
price_pix?: number;
price_tibia_coins?: number;
price_gold_coins?: number;
description?: string;
listing_status?: Listing["listing_status"];
current_bid_amount?: number;
current_bidder_id?: number;
}
export interface ListingFilters {
status?: Listing["listing_status"];
server_name?: string;
item_id?: number;
seller_id?: number;
is_auction?: boolean;
search?: string;
page?: number;
pageSize?: number;
sortBy?: "created_at" | "price_pix" | "price_tibia_coins" | "price_gold_coins" | "views_count";
sortOrder?: "ASC" | "DESC";
}
/**
* Cria um novo anúncio
*/
export async function createListing(
data: CreateListingData
): Promise<Listing> {
const result = await query<Listing>(
`
INSERT INTO listings (
seller_id, item_id, item_name, quantity, server_name,
price_pix, price_tibia_coins, price_gold_coins,
is_auction, auction_ends_at, description
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`,
[
data.seller_id,
data.item_id,
data.item_name,
data.quantity,
data.server_name,
data.price_pix || null,
data.price_tibia_coins || null,
data.price_gold_coins || null,
data.is_auction || false,
data.auction_ends_at || null,
data.description || null,
]
);
return result.rows[0];
}
/**
* Busca anúncio por ID
*/
export async function getListingById(listingId: number): Promise<Listing | null> {
const result = await query<Listing>(
`
SELECT * FROM listings WHERE listing_id = $1
`,
[listingId]
);
return result.rows[0] || null;
}
/**
* Lista anúncios com filtros e paginação
*/
export async function getListings(
filters: ListingFilters = {}
): Promise<{ listings: Listing[]; total: number }> {
const {
status,
server_name,
item_id,
seller_id,
is_auction,
search,
page = 1,
pageSize = 20,
sortBy = "created_at",
sortOrder = "DESC",
} = filters;
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (status) {
conditions.push(`listing_status = $${paramIndex}`);
params.push(status);
paramIndex++;
}
if (server_name) {
conditions.push(`server_name = $${paramIndex}`);
params.push(server_name);
paramIndex++;
}
if (item_id) {
conditions.push(`item_id = $${paramIndex}`);
params.push(item_id);
paramIndex++;
}
if (seller_id) {
conditions.push(`seller_id = $${paramIndex}`);
params.push(seller_id);
paramIndex++;
}
if (is_auction !== undefined) {
conditions.push(`is_auction = $${paramIndex}`);
params.push(is_auction);
paramIndex++;
}
if (search) {
conditions.push(`(item_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`);
params.push(`%${search}%`);
paramIndex++;
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
// Contar total
const countResult = await query<{ count: string }>(
`SELECT COUNT(*) as count FROM listings ${whereClause}`,
params
);
const total = parseInt(countResult.rows[0].count, 10);
// Validar sortBy
const validSortColumns = ["created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count"];
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : "created_at";
const sortDirection = sortOrder === "ASC" ? "ASC" : "DESC";
// Buscar listings
const offset = (page - 1) * pageSize;
const result = await query<Listing>(
`
SELECT * FROM listings
${whereClause}
ORDER BY ${sortColumn} ${sortDirection}
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`,
[...params, pageSize, offset]
);
return {
listings: result.rows,
total,
};
}
/**
* Atualiza um anúncio
*/
export async function updateListing(
listingId: number,
data: UpdateListingData
): Promise<Listing | null> {
const updates: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (data.quantity !== undefined) {
updates.push(`quantity = $${paramIndex}`);
params.push(data.quantity);
paramIndex++;
}
if (data.price_pix !== undefined) {
updates.push(`price_pix = $${paramIndex}`);
params.push(data.price_pix);
paramIndex++;
}
if (data.price_tibia_coins !== undefined) {
updates.push(`price_tibia_coins = $${paramIndex}`);
params.push(data.price_tibia_coins);
paramIndex++;
}
if (data.price_gold_coins !== undefined) {
updates.push(`price_gold_coins = $${paramIndex}`);
params.push(data.price_gold_coins);
paramIndex++;
}
if (data.description !== undefined) {
updates.push(`description = $${paramIndex}`);
params.push(data.description);
paramIndex++;
}
if (data.listing_status !== undefined) {
updates.push(`listing_status = $${paramIndex}`);
params.push(data.listing_status);
paramIndex++;
}
if (data.current_bid_amount !== undefined) {
updates.push(`current_bid_amount = $${paramIndex}`);
params.push(data.current_bid_amount);
paramIndex++;
}
if (data.current_bidder_id !== undefined) {
updates.push(`current_bidder_id = $${paramIndex}`);
params.push(data.current_bidder_id);
paramIndex++;
}
if (updates.length === 0) {
return getListingById(listingId);
}
params.push(listingId);
const result = await query<Listing>(
`
UPDATE listings
SET ${updates.join(", ")}
WHERE listing_id = $${paramIndex}
RETURNING *
`,
params
);
return result.rows[0] || null;
}
/**
* Cancela um anúncio (soft delete - muda status)
*/
export async function deleteListing(listingId: number): Promise<boolean> {
const result = await query(
`
UPDATE listings
SET listing_status = 'cancelled'
WHERE listing_id = $1
`,
[listingId]
);
return result.rowCount > 0;
}
/**
* Incrementa contador de views
*/
export async function incrementViews(listingId: number): Promise<void> {
await query(
`
UPDATE listings
SET views_count = views_count + 1
WHERE listing_id = $1
`,
[listingId]
);
}
/**
* Busca anúncios de um vendedor
*/
export async function getListingsBySeller(
sellerId: number,
filters: { status?: Listing["listing_status"]; page?: number; pageSize?: number } = {}
): Promise<{ listings: Listing[]; total: number }> {
return getListings({
seller_id: sellerId,
...filters,
});
}
/**
* Busca apenas anúncios ativos
*/
export async function getActiveListings(
filters: Omit<ListingFilters, "status"> = {}
): Promise<{ listings: Listing[]; total: number }> {
return getListings({
...filters,
status: "active",
});
}
+208
View File
@@ -0,0 +1,208 @@
/**
* Queries de servidores
* CRUD completo para a tabela servers
*/
import { query } from "../index";
export interface Server {
server_id: number;
server_name: string;
server_type: "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null;
location: "NA" | "SA" | "EU" | "OC" | "BR" | null;
is_active: boolean;
total_listings: number;
total_users: number;
total_subscribers: number;
total_transactions: number;
total_volume: number;
average_listing_price: number;
last_activity_at: Date | null;
created_at: Date;
updated_at: Date;
}
export interface ServerStats {
total_listings?: number;
total_users?: number;
total_subscribers?: number;
total_transactions?: number;
total_volume?: number;
average_listing_price?: number;
last_activity_at?: Date;
}
/**
* Lista todos os servidores
*/
export async function getAllServers(): Promise<Server[]> {
const result = await query<Server>(
`SELECT * FROM servers ORDER BY server_name ASC`
);
return result.rows;
}
/**
* Lista apenas servidores ativos
*/
export async function getActiveServers(): Promise<Server[]> {
const result = await query<Server>(
`SELECT * FROM servers WHERE is_active = TRUE ORDER BY server_name ASC`
);
return result.rows;
}
/**
* Busca servidor por nome
*/
export async function getServerByName(serverName: string): Promise<Server | null> {
const result = await query<Server>(
`SELECT * FROM servers WHERE server_name = $1`,
[serverName]
);
return result.rows[0] || null;
}
/**
* Busca servidor por ID
*/
export async function getServerById(serverId: number): Promise<Server | null> {
const result = await query<Server>(
`SELECT * FROM servers WHERE server_id = $1`,
[serverId]
);
return result.rows[0] || null;
}
/**
* Verifica se servidor existe e está ativo
*/
export async function isServerActive(serverName: string): Promise<boolean> {
const result = await query<{ count: string }>(
`SELECT COUNT(*) as count FROM servers WHERE server_name = $1 AND is_active = TRUE`,
[serverName]
);
return parseInt(result.rows[0].count) > 0;
}
/**
* Incrementa estatísticas do servidor
*/
export async function incrementServerStats(
serverName: string,
stats: Partial<{
total_listings: number;
total_users: number;
total_subscribers: number;
total_transactions: number;
total_volume: number;
}>
): Promise<void> {
const updates: string[] = [];
const values: any[] = [];
let paramIndex = 1;
if (stats.total_listings !== undefined) {
updates.push(`total_listings = total_listings + $${paramIndex++}`);
values.push(stats.total_listings);
}
if (stats.total_users !== undefined) {
updates.push(`total_users = total_users + $${paramIndex++}`);
values.push(stats.total_users);
}
if (stats.total_subscribers !== undefined) {
updates.push(`total_subscribers = total_subscribers + $${paramIndex++}`);
values.push(stats.total_subscribers);
}
if (stats.total_transactions !== undefined) {
updates.push(`total_transactions = total_transactions + $${paramIndex++}`);
values.push(stats.total_transactions);
}
if (stats.total_volume !== undefined) {
updates.push(`total_volume = total_volume + $${paramIndex++}`);
values.push(stats.total_volume);
}
if (updates.length === 0) {
return;
}
// Sempre atualizar last_activity_at
updates.push(`last_activity_at = CURRENT_TIMESTAMP`);
values.push(serverName);
await query(
`UPDATE servers SET ${updates.join(", ")} WHERE server_name = $${paramIndex}`,
values
);
}
/**
* Atualiza estatísticas completas do servidor
*/
export async function updateServerStats(
serverName: string,
stats: ServerStats
): Promise<void> {
const updates: string[] = [];
const values: any[] = [];
let paramIndex = 1;
if (stats.total_listings !== undefined) {
updates.push(`total_listings = $${paramIndex++}`);
values.push(stats.total_listings);
}
if (stats.total_users !== undefined) {
updates.push(`total_users = $${paramIndex++}`);
values.push(stats.total_users);
}
if (stats.total_subscribers !== undefined) {
updates.push(`total_subscribers = $${paramIndex++}`);
values.push(stats.total_subscribers);
}
if (stats.total_transactions !== undefined) {
updates.push(`total_transactions = $${paramIndex++}`);
values.push(stats.total_transactions);
}
if (stats.total_volume !== undefined) {
updates.push(`total_volume = $${paramIndex++}`);
values.push(stats.total_volume);
}
if (stats.average_listing_price !== undefined) {
updates.push(`average_listing_price = $${paramIndex++}`);
values.push(stats.average_listing_price);
}
if (stats.last_activity_at !== undefined) {
updates.push(`last_activity_at = $${paramIndex++}`);
values.push(stats.last_activity_at);
}
if (updates.length === 0) {
return;
}
values.push(serverName);
await query(
`UPDATE servers SET ${updates.join(", ")} WHERE server_name = $${paramIndex}`,
values
);
}
/**
* Incrementa total_users de múltiplos servidores
*/
export async function incrementUsersForServers(serverNames: string[]): Promise<void> {
if (serverNames.length === 0) {
return;
}
await query(
`UPDATE servers
SET total_users = total_users + 1,
last_activity_at = CURRENT_TIMESTAMP
WHERE server_name = ANY($1::text[])`,
[serverNames]
);
}
+241
View File
@@ -0,0 +1,241 @@
/**
* Queries de transactions (transações do sistema de escrow)
* Operações para a tabela transactions
*/
import { query } from "../index";
export interface Transaction {
transaction_id: number;
listing_id: number | null;
buyer_id: number;
seller_id: number;
item_name: string;
quantity: number;
server_name: string;
amount: number;
payment_method: "pix" | "tibia_coins" | "gold_coins" | null;
escrow_fee: number | null;
transaction_status:
| "pending"
| "deposited"
| "item_delivered"
| "completed"
| "disputed"
| "cancelled"
| "refunded";
created_at: Date;
deposited_at: Date | null;
delivered_at: Date | null;
completed_at: Date | null;
buyer_confirmed: boolean;
seller_confirmed: boolean;
buyer_notes: string | null;
seller_notes: string | null;
}
export interface CreateTransactionData {
listing_id?: number;
buyer_id: number;
seller_id: number;
item_name: string;
quantity: number;
server_name: string;
amount: number;
payment_method: "pix" | "tibia_coins" | "gold_coins";
escrow_fee?: number;
}
export interface UpdateTransactionStatusData {
transaction_status: Transaction["transaction_status"];
deposited_at?: Date;
delivered_at?: Date;
completed_at?: Date;
buyer_confirmed?: boolean;
seller_confirmed?: boolean;
buyer_notes?: string;
seller_notes?: string;
}
/**
* Cria uma nova transação
*/
export async function createTransaction(
data: CreateTransactionData
): Promise<Transaction> {
// Calcular escrow_fee se não fornecido (2% do valor)
const escrowFee = data.escrow_fee ?? Number((data.amount * 0.02).toFixed(2));
const result = await query<Transaction>(
`
INSERT INTO transactions (
listing_id, buyer_id, seller_id, item_name, quantity,
server_name, amount, payment_method, escrow_fee
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *
`,
[
data.listing_id || null,
data.buyer_id,
data.seller_id,
data.item_name,
data.quantity,
data.server_name,
data.amount,
data.payment_method,
escrowFee,
]
);
return result.rows[0];
}
/**
* Busca transação por ID
*/
export async function getTransactionById(
transactionId: number
): Promise<Transaction | null> {
const result = await query<Transaction>(
`
SELECT * FROM transactions WHERE transaction_id = $1
`,
[transactionId]
);
return result.rows[0] || null;
}
/**
* Atualiza status da transação
*/
export async function updateTransactionStatus(
transactionId: number,
data: UpdateTransactionStatusData
): Promise<Transaction | null> {
const updates: string[] = [];
const params: any[] = [];
let paramIndex = 1;
updates.push(`transaction_status = $${paramIndex}`);
params.push(data.transaction_status);
paramIndex++;
if (data.deposited_at !== undefined) {
updates.push(`deposited_at = $${paramIndex}`);
params.push(data.deposited_at);
paramIndex++;
}
if (data.delivered_at !== undefined) {
updates.push(`delivered_at = $${paramIndex}`);
params.push(data.delivered_at);
paramIndex++;
}
if (data.completed_at !== undefined) {
updates.push(`completed_at = $${paramIndex}`);
params.push(data.completed_at);
paramIndex++;
}
if (data.buyer_confirmed !== undefined) {
updates.push(`buyer_confirmed = $${paramIndex}`);
params.push(data.buyer_confirmed);
paramIndex++;
}
if (data.seller_confirmed !== undefined) {
updates.push(`seller_confirmed = $${paramIndex}`);
params.push(data.seller_confirmed);
paramIndex++;
}
if (data.buyer_notes !== undefined) {
updates.push(`buyer_notes = $${paramIndex}`);
params.push(data.buyer_notes);
paramIndex++;
}
if (data.seller_notes !== undefined) {
updates.push(`seller_notes = $${paramIndex}`);
params.push(data.seller_notes);
paramIndex++;
}
params.push(transactionId);
const result = await query<Transaction>(
`
UPDATE transactions
SET ${updates.join(", ")}
WHERE transaction_id = $${paramIndex}
RETURNING *
`,
params
);
return result.rows[0] || null;
}
/**
* Busca transações de um usuário (comprador ou vendedor)
*/
export async function getTransactionsByUser(
userId: number,
filters: {
role?: "buyer" | "seller" | "both";
status?: Transaction["transaction_status"];
page?: number;
pageSize?: number;
} = {}
): Promise<{ transactions: Transaction[]; total: number }> {
const { role = "both", status, page = 1, pageSize = 20 } = filters;
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (role === "buyer") {
conditions.push(`buyer_id = $${paramIndex}`);
} else if (role === "seller") {
conditions.push(`seller_id = $${paramIndex}`);
} else {
conditions.push(`(buyer_id = $${paramIndex} OR seller_id = $${paramIndex})`);
}
params.push(userId);
paramIndex++;
if (status) {
conditions.push(`transaction_status = $${paramIndex}`);
params.push(status);
paramIndex++;
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
// Contar total
const countResult = await query<{ count: string }>(
`SELECT COUNT(*) as count FROM transactions ${whereClause}`,
params
);
const total = parseInt(countResult.rows[0].count, 10);
// Buscar transações
const offset = (page - 1) * pageSize;
const result = await query<Transaction>(
`
SELECT * FROM transactions
${whereClause}
ORDER BY created_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`,
[...params, pageSize, offset]
);
return {
transactions: result.rows,
total,
};
}
+213
View File
@@ -0,0 +1,213 @@
/**
* Queries de usuários
* CRUD completo para a tabela users
*/
import { query } from "../index";
export interface User {
user_id: number;
username: string;
email: string;
password_hash?: string; // Apenas para login, não retornar em outros lugares
first_name: string;
last_name: string;
phone: string | null;
cpf: string;
account_level: "free" | "bronze" | "silver" | "gold";
has_donated: boolean;
whatsapp_notifications: boolean;
created_at: Date;
updated_at: Date;
last_login: Date | null;
is_active: boolean;
is_verified: boolean;
}
export interface CreateUserData {
username: string;
email: string;
password_hash: string;
first_name: string;
last_name: string;
phone?: string;
cpf?: string; // CPF agora é opcional
}
export interface UpdateUserData {
first_name?: string;
last_name?: string;
phone?: string;
whatsapp_notifications?: boolean;
}
/**
* Cria um novo usuário
*/
export async function createUser(
data: CreateUserData
): Promise<{ userId: number; username: string; email: string }> {
const result = await query<{ user_id: number; username: string; email: string }>(
`
INSERT INTO users (username, email, password_hash, first_name, last_name, phone, cpf)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING user_id, username, email
`,
[
data.username,
data.email,
data.password_hash,
data.first_name,
data.last_name,
data.phone || null,
data.cpf || null,
]
);
return {
userId: result.rows[0].user_id,
username: result.rows[0].username,
email: result.rows[0].email,
};
}
/**
* Busca usuário por email (inclui password_hash para login)
*/
export async function getUserByEmail(email: string): Promise<User | null> {
const result = await query<User>(
`
SELECT * FROM users WHERE email = $1
`,
[email]
);
return result.rows[0] || null;
}
/**
* Busca usuário por username
*/
export async function getUserByUsername(
username: string
): Promise<User | null> {
const result = await query<User>(
`
SELECT * FROM users WHERE username = $1
`,
[username]
);
return result.rows[0] || null;
}
/**
* Busca usuário por ID (sem password_hash)
*/
export async function getUserById(userId: number): Promise<User | null> {
const result = await query<User>(
`
SELECT
user_id, username, email, first_name, last_name, phone, cpf,
account_level, has_donated, whatsapp_notifications,
created_at, updated_at, last_login, is_active, is_verified
FROM users
WHERE user_id = $1
`,
[userId]
);
return result.rows[0] || null;
}
/**
* Busca usuário por CPF (verifica duplicidade)
*/
export async function getUserByCpf(cpf: string): Promise<User | null> {
const result = await query<User>(
`
SELECT user_id, username, email FROM users WHERE cpf = $1
`,
[cpf]
);
return result.rows[0] || null;
}
/**
* Atualiza dados do usuário
*/
export async function updateUser(
userId: number,
data: UpdateUserData
): Promise<User> {
const updates: string[] = [];
const values: any[] = [];
let paramIndex = 1;
if (data.first_name !== undefined) {
updates.push(`first_name = $${paramIndex++}`);
values.push(data.first_name);
}
if (data.last_name !== undefined) {
updates.push(`last_name = $${paramIndex++}`);
values.push(data.last_name);
}
if (data.phone !== undefined) {
updates.push(`phone = $${paramIndex++}`);
values.push(data.phone || null);
}
if (data.whatsapp_notifications !== undefined) {
updates.push(`whatsapp_notifications = $${paramIndex++}`);
values.push(data.whatsapp_notifications);
}
if (updates.length === 0) {
// Se não há updates, retorna usuário atual
const user = await getUserById(userId);
if (!user) {
throw new Error("Usuário não encontrado");
}
return user;
}
updates.push(`updated_at = CURRENT_TIMESTAMP`);
values.push(userId);
const result = await query<User>(
`
UPDATE users
SET ${updates.join(", ")}
WHERE user_id = $${paramIndex}
RETURNING
user_id, username, email, first_name, last_name, phone, cpf,
account_level, has_donated, whatsapp_notifications,
created_at, updated_at, last_login, is_active, is_verified
`,
values
);
if (result.rows.length === 0) {
throw new Error("Usuário não encontrado");
}
return result.rows[0];
}
/**
* Atualiza timestamp de último login
*/
export async function updateLastLogin(userId: number): Promise<void> {
await query(
`
UPDATE users
SET last_login = CURRENT_TIMESTAMP
WHERE user_id = $1
`,
[userId]
);
}
+157
View File
@@ -0,0 +1,157 @@
/**
* Seed inicial de servidores do Tibia
* Lista completa de servidores oficiais
*/
// Carregar variáveis de ambiente
import { config } from "dotenv";
import { resolve } from "path";
// Tentar carregar .env.local primeiro, depois .env
config({ path: resolve(process.cwd(), ".env.local") });
config({ path: resolve(process.cwd(), ".env") });
import { query } from "../index";
export interface ServerSeed {
server_name: string;
server_type: "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp";
location: "NA" | "SA" | "EU" | "OC" | "BR";
}
// Lista de servidores oficiais do Tibia (servidores únicos)
// Nota: Cada servidor tem um nome único. O tipo pode ser atualizado posteriormente se necessário.
const TIBIA_SERVERS: ServerSeed[] = [
// Servidores PvP - EU
{ server_name: "Antica", server_type: "pvp", location: "EU" },
{ server_name: "Assombra", server_type: "pvp", location: "EU" },
{ server_name: "Astera", server_type: "pvp", location: "EU" },
{ server_name: "Belobra", server_type: "pvp", location: "EU" },
{ server_name: "Bona", server_type: "pvp", location: "EU" },
{ server_name: "Calmera", server_type: "pvp", location: "EU" },
{ server_name: "Celesta", server_type: "pvp", location: "EU" },
{ server_name: "Concorda", server_type: "pvp", location: "EU" },
{ server_name: "Cosera", server_type: "pvp", location: "EU" },
{ server_name: "Damora", server_type: "pvp", location: "EU" },
{ server_name: "Descubra", server_type: "pvp", location: "EU" },
{ server_name: "Dia", server_type: "pvp", location: "EU" },
{ server_name: "Duna", server_type: "pvp", location: "EU" },
{ server_name: "Elera", server_type: "pvp", location: "EU" },
{ server_name: "Epoca", server_type: "pvp", location: "EU" },
{ server_name: "Esmera", server_type: "pvp", location: "EU" },
{ server_name: "Faluna", server_type: "pvp", location: "EU" },
{ server_name: "Famosa", server_type: "pvp", location: "EU" },
{ server_name: "Fera", server_type: "pvp", location: "EU" },
{ server_name: "Ferobra", server_type: "pvp", location: "EU" },
{ server_name: "Firmera", server_type: "pvp", location: "EU" },
{ server_name: "Funera", server_type: "pvp", location: "EU" },
{ server_name: "Furia", server_type: "pvp", location: "EU" },
{ server_name: "Garnera", server_type: "pvp", location: "EU" },
{ server_name: "Gentebra", server_type: "pvp", location: "EU" },
{ server_name: "Gladera", server_type: "pvp", location: "EU" },
{ server_name: "Gravitera", server_type: "pvp", location: "EU" },
{ server_name: "Grimera", server_type: "pvp", location: "EU" },
{ server_name: "Harmonia", server_type: "pvp", location: "EU" },
{ server_name: "Honbra", server_type: "pvp", location: "EU" },
{ server_name: "Impulsa", server_type: "pvp", location: "EU" },
{ server_name: "Inabra", server_type: "pvp", location: "EU" },
{ server_name: "Issobra", server_type: "pvp", location: "EU" },
{ server_name: "Jacabra", server_type: "pvp", location: "EU" },
{ server_name: "Javibra", server_type: "pvp", location: "EU" },
{ server_name: "Jonera", server_type: "pvp", location: "EU" },
{ server_name: "Kalibra", server_type: "pvp", location: "EU" },
{ server_name: "Kardera", server_type: "pvp", location: "EU" },
{ server_name: "Kenora", server_type: "pvp", location: "EU" },
{ server_name: "Libertabra", server_type: "pvp", location: "EU" },
{ server_name: "Lobera", server_type: "pvp", location: "EU" },
{ server_name: "Luminera", server_type: "pvp", location: "EU" },
{ server_name: "Lutabra", server_type: "pvp", location: "EU" },
{ server_name: "Menera", server_type: "pvp", location: "EU" },
{ server_name: "Mitigera", server_type: "pvp", location: "EU" },
{ server_name: "Monza", server_type: "pvp", location: "EU" },
{ server_name: "Nefera", server_type: "pvp", location: "EU" },
{ server_name: "Noctera", server_type: "pvp", location: "EU" },
{ server_name: "Ocera", server_type: "pvp", location: "EU" },
{ server_name: "Ombra", server_type: "pvp", location: "EU" },
{ server_name: "Pacera", server_type: "pvp", location: "EU" },
{ server_name: "Peloria", server_type: "pvp", location: "EU" },
{ server_name: "Premia", server_type: "pvp", location: "EU" },
{ server_name: "Pyra", server_type: "pvp", location: "EU" },
{ server_name: "Quelibra", server_type: "pvp", location: "EU" },
{ server_name: "Quintera", server_type: "pvp", location: "EU" },
{ server_name: "Refugia", server_type: "pvp", location: "EU" },
{ server_name: "Relania", server_type: "pvp", location: "EU" },
{ server_name: "Secura", server_type: "pvp", location: "EU" },
{ server_name: "Serdebra", server_type: "pvp", location: "EU" },
{ server_name: "Solidera", server_type: "pvp", location: "EU" },
{ server_name: "Talonera", server_type: "pvp", location: "EU" },
{ server_name: "Tortura", server_type: "pvp", location: "EU" },
{ server_name: "Unica", server_type: "pvp", location: "EU" },
{ server_name: "Utobra", server_type: "pvp", location: "EU" },
{ server_name: "Venebra", server_type: "pvp", location: "EU" },
{ server_name: "Vita", server_type: "pvp", location: "EU" },
{ server_name: "Vunira", server_type: "pvp", location: "EU" },
{ server_name: "Wadira", server_type: "pvp", location: "EU" },
{ server_name: "Wintera", server_type: "pvp", location: "EU" },
{ server_name: "Xandebra", server_type: "pvp", location: "EU" },
{ server_name: "Yonabra", server_type: "pvp", location: "EU" },
{ server_name: "Zuna", server_type: "pvp", location: "EU" },
{ server_name: "Zunera", server_type: "pvp", location: "EU" },
// Servidores No-PvP - EU
{ server_name: "Asteria", server_type: "no-pvp", location: "EU" },
{ server_name: "Belluma", server_type: "no-pvp", location: "EU" },
{ server_name: "Calva", server_type: "no-pvp", location: "EU" },
{ server_name: "Castela", server_type: "no-pvp", location: "EU" },
{ server_name: "Collabra", server_type: "no-pvp", location: "EU" },
{ server_name: "Estela", server_type: "no-pvp", location: "EU" },
];
export async function seedServers() {
console.log("🌱 Iniciando seed de servidores...\n");
let inserted = 0;
let skipped = 0;
for (const server of TIBIA_SERVERS) {
try {
// Verificar se já existe
const existing = await query<{ server_id: number }>(
"SELECT server_id FROM servers WHERE server_name = $1",
[server.server_name]
);
if (existing.rows.length > 0) {
skipped++;
continue;
}
// Inserir servidor
await query(
`INSERT INTO servers (server_name, server_type, location, is_active)
VALUES ($1, $2, $3, $4)`,
[server.server_name, server.server_type, server.location, true]
);
inserted++;
} catch (error) {
console.error(`❌ Erro ao inserir servidor ${server.server_name}:`, error);
}
}
console.log(`✅ Seed concluído: ${inserted} servidores inseridos, ${skipped} já existiam\n`);
}
// Se executado diretamente
if (require.main === module) {
seedServers()
.then(() => {
console.log("🎉 Seed finalizado com sucesso!");
process.exit(0);
})
.catch((error) => {
console.error("❌ Erro fatal no seed:", error);
process.exit(1);
});
}
+99
View File
@@ -0,0 +1,99 @@
/**
* Script de teste de conexão com PostgreSQL
* Executa um health check e tenta uma query simples
*/
// Carregar variáveis de ambiente
import { config } from "dotenv";
import { resolve } from "path";
// Tentar carregar .env.local primeiro, depois .env
config({ path: resolve(process.cwd(), ".env.local") });
config({ path: resolve(process.cwd(), ".env") });
import { healthCheck, query, closePool } from "./index";
async function testConnection() {
console.log("🔍 Testando conexão com PostgreSQL...\n");
// Verificar variável de ambiente
if (!process.env.PG_DB) {
console.error("❌ PG_DB não está definido nas variáveis de ambiente");
console.log("\n💡 Configure a variável PG_DB:");
console.log(" export PG_DB=postgresql://user:pass@host:port/db");
console.log(" ou crie um arquivo .env.local com:");
console.log(" PG_DB=postgresql://user:pass@host:port/db\n");
process.exit(1);
}
console.log("✅ Variável PG_DB encontrada");
console.log(` Host: ${process.env.PG_DB.split("@")[1]?.split("/")[0] || "não detectado"}\n`);
try {
// Health check
console.log("📡 Executando health check...");
const isHealthy = await healthCheck();
if (!isHealthy) {
console.error("❌ Health check falhou - banco não está acessível");
process.exit(1);
}
console.log("✅ Health check passou\n");
// Query de teste
console.log("📊 Executando query de teste...");
const result = await query("SELECT version(), NOW() as current_time");
console.log("✅ Query executada com sucesso");
console.log(` PostgreSQL Version: ${result.rows[0]?.version?.split(" ")[0] || "N/A"}`);
console.log(` Current Time: ${result.rows[0]?.current_time || "N/A"}\n`);
// Verificar se as tabelas existem
console.log("🔍 Verificando tabelas...");
const tablesResult = await query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('users', 'user_characters')
ORDER BY table_name
`);
const tables = tablesResult.rows.map((row: any) => row.table_name);
if (tables.length === 0) {
console.log("⚠️ Tabelas não encontradas. Execute as migrations:");
console.log(" pnpm db:migrate\n");
} else {
console.log(`✅ Tabelas encontradas: ${tables.join(", ")}\n`);
}
console.log("🎉 Conexão com PostgreSQL está funcionando corretamente!");
// Fechar pool
await closePool();
process.exit(0);
} catch (error) {
console.error("❌ Erro ao conectar com PostgreSQL:");
console.error(error instanceof Error ? error.message : String(error));
if (error instanceof Error && error.message.includes("timeout")) {
console.log("\n💡 Verifique:");
console.log(" - O servidor PostgreSQL está acessível?");
console.log(" - A URL de conexão está correta?");
console.log(" - O firewall permite conexões?");
}
if (error instanceof Error && error.message.includes("authentication")) {
console.log("\n💡 Verifique:");
console.log(" - Usuário e senha estão corretos?");
console.log(" - O usuário tem permissão para acessar o banco?");
}
await closePool();
process.exit(1);
}
}
testConnection();