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.
368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
import { z } from "zod";
|
|
import validator from "validator";
|
|
import { isValidDocument } from "./validators/document-validator";
|
|
import sanitizeHtml from "sanitize-html";
|
|
|
|
/**
|
|
* Schemas de validação usando Zod
|
|
* Garante type-safety e validação robusta de dados de entrada
|
|
*/
|
|
|
|
// Schema para email - validação RFC 5322
|
|
export const emailSchema = z
|
|
.string()
|
|
.min(1, "Email é obrigatório")
|
|
.email("Formato de email inválido")
|
|
.refine((email) => validator.isEmail(email), {
|
|
message: "Email inválido",
|
|
})
|
|
.transform((email) => email.trim().toLowerCase());
|
|
|
|
// Schema para telefone (aceita brasileiro e internacional)
|
|
export const phoneSchema = z
|
|
.string()
|
|
.min(1, "Telefone é obrigatório")
|
|
.refine((phone) => {
|
|
// Remove todos os caracteres não numéricos (incluindo +)
|
|
// Aceita formato internacional com código de país (ex: +55119988887777)
|
|
const cleaned = phone.replace(/[^\d]/g, "");
|
|
// Aceita telefones de 8 a 15 dígitos (padrão internacional E.164)
|
|
// O código de país pode ter 1-3 dígitos, então o número total pode ser maior
|
|
// Mas para validação, aceitamos de 10 a 16 dígitos (código + número)
|
|
return cleaned.length >= 10 && cleaned.length <= 16;
|
|
}, "Telefone deve ter entre 10 e 16 dígitos (incluindo código de país)")
|
|
.transform((phone) => {
|
|
// Remove todos os caracteres não numéricos, mantendo apenas dígitos
|
|
// O formato final será: código de país + número (ex: 55119988887777)
|
|
return phone.replace(/[^\d]/g, "");
|
|
});
|
|
|
|
// Schema para telefone com código de país (para registro)
|
|
export const phoneWithCountrySchema = z.object({
|
|
dialCode: z.string().min(1, "Código de país é obrigatório"),
|
|
phoneNumber: phoneSchema,
|
|
});
|
|
|
|
// Schema para CPF/CNPJ
|
|
export const documentSchema = z
|
|
.string()
|
|
.min(1, "CPF/CNPJ é obrigatório")
|
|
.refine((doc) => isValidDocument(doc), {
|
|
message: "CPF ou CNPJ inválido",
|
|
})
|
|
.transform((doc) => doc.replace(/[^\d]/g, "")); // Normaliza removendo formatação
|
|
|
|
// Schema para nome (alfabético, sem números)
|
|
export const nameSchema = z
|
|
.string()
|
|
.min(3, "Nome deve ter no mínimo 3 caracteres")
|
|
.max(100, "Nome deve ter no máximo 100 caracteres")
|
|
.refine(
|
|
(name) => /^[A-Za-zÀ-ÿ\s'-]+$/.test(name.trim()),
|
|
"Nome deve conter apenas letras, espaços e acentos"
|
|
)
|
|
.transform((name) => name.trim());
|
|
|
|
// Schema para mensagem (sanitiza HTML e previne XSS)
|
|
export const messageSchema = z
|
|
.string()
|
|
.max(280, "Mensagem deve ter no máximo 280 caracteres")
|
|
.transform((message) => {
|
|
// Sanitiza HTML para prevenir XSS
|
|
const sanitized = sanitizeHtml(message, {
|
|
allowedTags: [], // Remove todas as tags HTML
|
|
allowedAttributes: {},
|
|
});
|
|
return sanitized.trim();
|
|
})
|
|
.optional();
|
|
|
|
// Schema para tier de doação
|
|
export const donationTierSchema = z.enum(
|
|
["coffee", "snack", "lunch", "custom"],
|
|
{
|
|
message: "Tier de doação inválido",
|
|
}
|
|
);
|
|
|
|
// Schema para valor customizado
|
|
export const customAmountSchema = z
|
|
.number()
|
|
.min(5, "Valor mínimo é R$ 5,00")
|
|
.max(100000, "Valor máximo é R$ 100.000,00");
|
|
|
|
// Schema completo para criação de doação
|
|
export const createDonationSchema = z
|
|
.discriminatedUnion("anonymous", [
|
|
// Doação anônima
|
|
z.object({
|
|
tier: donationTierSchema,
|
|
anonymous: z.literal(true),
|
|
customAmount: z.number().optional(),
|
|
}),
|
|
// Doação identificada
|
|
z.object({
|
|
tier: donationTierSchema,
|
|
anonymous: z.literal(false),
|
|
donorName: nameSchema,
|
|
donorEmail: emailSchema,
|
|
donorPhone: phoneSchema,
|
|
donorTaxId: documentSchema,
|
|
donorMessage: messageSchema,
|
|
customAmount: z.number().optional(),
|
|
}),
|
|
])
|
|
.refine(
|
|
(data) => {
|
|
// Se tier for custom, customAmount é obrigatório
|
|
if (data.tier === "custom") {
|
|
return data.customAmount !== undefined;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "customAmount é obrigatório quando tier é 'custom'",
|
|
path: ["customAmount"],
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
// Se tier for custom, customAmount deve estar no range válido
|
|
if (data.tier === "custom" && data.customAmount) {
|
|
return data.customAmount >= 5 && data.customAmount <= 100000;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "Valor deve estar entre R$ 5,00 e R$ 100.000,00",
|
|
path: ["customAmount"],
|
|
}
|
|
);
|
|
|
|
// Schema para verificação de status
|
|
export const verifyStatusSchema = z.object({
|
|
pixId: z.string().min(1, "pixId é obrigatório"),
|
|
});
|
|
|
|
// Schema para verificação de billing
|
|
export const verifyBillingSchema = z.object({
|
|
billingId: z.string().min(1, "ID da cobrança é obrigatório"),
|
|
});
|
|
|
|
// ============================================
|
|
// SCHEMAS DE AUTENTICAÇÃO E USUÁRIOS
|
|
// ============================================
|
|
|
|
// Schema para username (alfanumérico + underscore)
|
|
export const usernameSchema = z
|
|
.string()
|
|
.min(3, "Username deve ter no mínimo 3 caracteres")
|
|
.max(50, "Username deve ter no máximo 50 caracteres")
|
|
.regex(
|
|
/^[a-zA-Z0-9_]+$/,
|
|
"Username deve conter apenas letras, números e underscore"
|
|
)
|
|
.transform((username) => username.toLowerCase().trim());
|
|
|
|
// Schema para senha (complexidade mínima)
|
|
export const passwordSchema = z
|
|
.string()
|
|
.min(8, "Senha deve ter no mínimo 8 caracteres")
|
|
.refine(
|
|
(password) => {
|
|
// Pelo menos 1 maiúscula
|
|
if (!/[A-Z]/.test(password)) return false;
|
|
// Pelo menos 1 minúscula
|
|
if (!/[a-z]/.test(password)) return false;
|
|
// Pelo menos 1 número
|
|
if (!/[0-9]/.test(password)) return false;
|
|
return true;
|
|
},
|
|
{
|
|
message:
|
|
"Senha deve conter pelo menos 1 letra maiúscula, 1 letra minúscula e 1 número",
|
|
}
|
|
);
|
|
|
|
// Schema para servidor/personagem no registro
|
|
export const registerServerCharacterSchema = z.object({
|
|
serverName: z
|
|
.string()
|
|
.min(1, "Nome do servidor é obrigatório")
|
|
.max(50, "Nome do servidor deve ter no máximo 50 caracteres")
|
|
.transform((name) => name.trim()),
|
|
characterName: z
|
|
.string()
|
|
.min(3, "Nome do personagem deve ter no mínimo 3 caracteres")
|
|
.max(100, "Nome do personagem deve ter no máximo 100 caracteres")
|
|
.transform((name) => name.trim()),
|
|
isMain: z.boolean().optional().default(false),
|
|
});
|
|
|
|
// Schema para registro de usuário
|
|
export const registerSchema = z.object({
|
|
username: usernameSchema,
|
|
email: emailSchema,
|
|
password: passwordSchema,
|
|
firstName: nameSchema,
|
|
lastName: nameSchema,
|
|
phone: phoneSchema.optional(),
|
|
servers: z
|
|
.array(registerServerCharacterSchema)
|
|
.min(1, "Selecione pelo menos um servidor"),
|
|
});
|
|
|
|
// Schema para login (aceita email ou username)
|
|
export const loginSchema = z.object({
|
|
emailOrUsername: z
|
|
.string()
|
|
.min(1, "Email ou username é obrigatório")
|
|
.transform((val) => val.trim().toLowerCase()),
|
|
password: z.string().min(1, "Senha é obrigatória"),
|
|
});
|
|
|
|
// Schema para atualização de perfil
|
|
export const updateUserSchema = z.object({
|
|
firstName: nameSchema.optional(),
|
|
lastName: nameSchema.optional(),
|
|
phone: phoneSchema.optional(),
|
|
whatsappNotifications: z.boolean().optional(),
|
|
});
|
|
|
|
// Schema para personagem
|
|
export const characterSchema = z.object({
|
|
characterName: z
|
|
.string()
|
|
.min(3, "Nome do personagem deve ter no mínimo 3 caracteres")
|
|
.max(100, "Nome do personagem deve ter no máximo 100 caracteres")
|
|
.transform((name) => name.trim()),
|
|
serverName: z
|
|
.string()
|
|
.min(1, "Nome do servidor é obrigatório")
|
|
.max(50, "Nome do servidor deve ter no máximo 50 caracteres")
|
|
.transform((name) => name.trim()),
|
|
isMain: z.boolean().optional().default(false),
|
|
});
|
|
|
|
// ============================================
|
|
// SCHEMAS DE MARKETPLACE (LISTINGS E BIDS)
|
|
// ============================================
|
|
|
|
// Schema para criar anúncio (listing)
|
|
export const createListingSchema = z.object({
|
|
item_id: z.number().int().positive("ID do item deve ser positivo"),
|
|
item_name: z
|
|
.string()
|
|
.min(1, "Nome do item é obrigatório")
|
|
.max(255, "Nome do item deve ter no máximo 255 caracteres")
|
|
.transform((name) => name.trim()),
|
|
quantity: z
|
|
.number()
|
|
.int()
|
|
.positive("Quantidade deve ser maior que zero")
|
|
.default(1),
|
|
server_name: z
|
|
.string()
|
|
.min(1, "Nome do servidor é obrigatório")
|
|
.max(50, "Nome do servidor deve ter no máximo 50 caracteres")
|
|
.transform((name) => name.trim()),
|
|
price_pix: z
|
|
.number()
|
|
.positive("Preço PIX deve ser positivo")
|
|
.optional()
|
|
.nullable(),
|
|
price_tibia_coins: z
|
|
.number()
|
|
.int()
|
|
.min(0, "Preço em Tibia Coins deve ser não negativo")
|
|
.optional()
|
|
.nullable(),
|
|
price_gold_coins: z
|
|
.number()
|
|
.int()
|
|
.min(0, "Preço em Gold Coins deve ser não negativo")
|
|
.optional()
|
|
.nullable(),
|
|
is_auction: z.boolean().optional().default(false),
|
|
auction_ends_at: z
|
|
.string()
|
|
.datetime("Data de término do leilão inválida")
|
|
.optional()
|
|
.nullable()
|
|
.transform((val) => (val ? new Date(val) : null)),
|
|
description: z
|
|
.string()
|
|
.max(5000, "Descrição deve ter no máximo 5000 caracteres")
|
|
.optional()
|
|
.nullable()
|
|
.transform((val) => val?.trim() || null),
|
|
}).refine(
|
|
(data) => {
|
|
// Pelo menos um preço deve ser fornecido
|
|
return (
|
|
data.price_pix !== null ||
|
|
data.price_tibia_coins !== null ||
|
|
data.price_gold_coins !== null
|
|
);
|
|
},
|
|
{
|
|
message: "Pelo menos um método de pagamento (PIX, Tibia Coins ou Gold Coins) deve ser especificado",
|
|
path: ["price_pix"],
|
|
}
|
|
).refine(
|
|
(data) => {
|
|
// Se for leilão, deve ter data de término
|
|
if (data.is_auction && !data.auction_ends_at) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "Data de término do leilão é obrigatória quando is_auction é true",
|
|
path: ["auction_ends_at"],
|
|
}
|
|
);
|
|
|
|
// Schema para atualizar anúncio
|
|
export const updateListingSchema = z.object({
|
|
quantity: z.number().int().positive().optional(),
|
|
price_pix: z.number().positive().optional().nullable(),
|
|
price_tibia_coins: z.number().int().min(0).optional().nullable(),
|
|
price_gold_coins: z.number().int().min(0).optional().nullable(),
|
|
description: z.string().max(5000).optional().nullable(),
|
|
listing_status: z
|
|
.enum(["active", "sold", "cancelled", "in_escrow", "expired"])
|
|
.optional(),
|
|
}).refine(
|
|
(data) => {
|
|
// Se atualizando preços, pelo menos um deve ser fornecido ou manter existentes
|
|
const hasPrice =
|
|
data.price_pix !== undefined ||
|
|
data.price_tibia_coins !== undefined ||
|
|
data.price_gold_coins !== undefined;
|
|
return true; // Permite atualizar sem especificar preços (mantém existentes)
|
|
}
|
|
);
|
|
|
|
// Schema para criar lance (bid)
|
|
export const bidSchema = z.object({
|
|
listing_id: z.number().int().positive("ID do anúncio deve ser positivo"),
|
|
bid_amount: z.number().positive("Valor do lance deve ser positivo"),
|
|
bid_type: z
|
|
.enum(["pix", "tibia_coins", "gold_coins"])
|
|
.optional()
|
|
.nullable(),
|
|
});
|
|
|
|
// Type exports para uso no código
|
|
export type CreateDonationInput = z.infer<typeof createDonationSchema>;
|
|
export type VerifyStatusInput = z.infer<typeof verifyStatusSchema>;
|
|
export type VerifyBillingInput = z.infer<typeof verifyBillingSchema>;
|
|
export type RegisterInput = z.infer<typeof registerSchema>;
|
|
export type LoginInput = z.infer<typeof loginSchema>;
|
|
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
|
export type CharacterInput = z.infer<typeof characterSchema>;
|
|
export type CreateListingInput = z.infer<typeof createListingSchema>;
|
|
export type UpdateListingInput = z.infer<typeof updateListingSchema>;
|
|
export type BidInput = z.infer<typeof bidSchema>;
|