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:
1 parent
e641138887
commit
55eebde0ba
55 files changed
+7353
-7
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user