import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { toLocalDateTimeString } from "@/lib/datetime"; export interface Perfil { id: string; email: string; nome: string; aprovado: boolean; created_at: string; } export interface SessaoAtual { userId: string; nome: string; admin: boolean; perfil: Perfil; } interface PerfilRow { id: string; email: string; nome: string; aprovado: boolean; created_at: Date | string; } const loginSchema = z.object({ nome: z.string().min(3), senha: z.string().min(6), }); function toPerfil(row: PerfilRow): Perfil { return { id: row.id, email: row.email, nome: row.nome, aprovado: Boolean(row.aprovado), created_at: toLocalDateTimeString(row.created_at), }; } async function getSessionUserId(): Promise { const { getSessionUserId: getServerSessionUserId } = await import("@/lib/auth.server"); return await getServerSessionUserId(); } async function fetchSessionProfile(userId: string): Promise { const [{ getPool, sql }] = await Promise.all([import("@/lib/db.server")]); const pool = await getPool(); const result = await pool .request() .input("userId", sql.UniqueIdentifier, userId) .query(` SELECT u.id, u.email, u.nome, u.aprovado, u.created_at, CAST(CASE WHEN EXISTS ( SELECT 1 FROM slalog.usuarios_papeis up WHERE up.user_id = u.id AND up.role = 'admin' ) THEN 1 ELSE 0 END AS bit) AS admin FROM slalog.usuarios u WHERE u.id = @userId AND u.ativo = 1 `); const row = result.recordset[0]; if (!row) return null; const perfil = toPerfil(row); return { userId: perfil.id, nome: perfil.nome, admin: Boolean(row.admin), perfil, }; } async function requireSession(): Promise { const userId = await getSessionUserId(); if (!userId) throw new Error("Não autenticado"); const sessao = await fetchSessionProfile(userId); if (!sessao) throw new Error("Sessão inválida"); return sessao; } async function requireAdminSession(): Promise { const sessao = await requireSession(); if (!sessao.admin) throw new Error("Apenas administradores podem executar esta ação."); return sessao; } async function requireApprovedSession(): Promise { const sessao = await requireSession(); if (!sessao.perfil.aprovado) throw new Error("Usuário aguardando aprovação."); return sessao; } export function normalizarNome(nome: string): string { return nome .trim() .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9._-]/g, ""); } export function emailDeNome(nome: string): string { return `${normalizarNome(nome)}@coleta.local`; } export const obterSessaoAtual = createServerFn({ method: "GET" }).handler(async () => { const userId = await getSessionUserId(); if (!userId) return null; return await fetchSessionProfile(userId); }); export const entrar = createServerFn({ method: "POST" }) .validator(loginSchema) .handler(async ({ data }) => { const nome = normalizarNome(data.nome); const [{ getPool, sql }, bcrypt] = await Promise.all([ import("@/lib/db.server"), import("bcryptjs"), ]); const pool = await getPool(); const result = await pool .request() .input("nome", sql.NVarChar(150), nome) .query<(PerfilRow & { senha_hash: string; admin: boolean; ativo: boolean })>(` SELECT u.id, u.email, u.nome, u.aprovado, u.ativo, u.created_at, u.senha_hash, CAST(CASE WHEN EXISTS ( SELECT 1 FROM slalog.usuarios_papeis up WHERE up.user_id = u.id AND up.role = 'admin' ) THEN 1 ELSE 0 END AS bit) AS admin FROM slalog.usuarios u WHERE LOWER(u.nome) = LOWER(@nome) `); const row = result.recordset[0]; if (!row || !row.ativo) throw new Error("Nome de usuário ou senha incorretos."); const ok = await bcrypt.compare(data.senha, row.senha_hash); if (!ok) throw new Error("Nome de usuário ou senha incorretos."); const { setSessionUserId } = await import("@/lib/auth.server"); await setSessionUserId(row.id); const perfil = toPerfil(row); return { userId: row.id, nome: row.nome, admin: Boolean(row.admin), perfil, } satisfies SessaoAtual; }); export const criarConta = createServerFn({ method: "POST" }) .validator(loginSchema) .handler(async ({ data }) => { const nome = normalizarNome(data.nome); const email = emailDeNome(nome); const [{ getPool, sql }, bcrypt] = await Promise.all([ import("@/lib/db.server"), import("bcryptjs"), ]); const pool = await getPool(); const existente = await pool .request() .input("nome", sql.NVarChar(150), nome) .query<{ id: string }>(` SELECT TOP 1 id FROM slalog.usuarios WHERE LOWER(nome) = LOWER(@nome) `); if (existente.recordset[0]) throw new Error("Este nome de usuário já existe. Faça login."); const primeiro = await pool.request().query<{ total: number }>(` SELECT COUNT(1) AS total FROM slalog.usuarios `); const senhaHash = await bcrypt.hash(data.senha, 10); const aprovado = (primeiro.recordset[0]?.total ?? 0) === 0; const criado = await pool .request() .input("id", sql.UniqueIdentifier, crypto.randomUUID()) .input("email", sql.NVarChar(255), email) .input("nome", sql.NVarChar(150), nome) .input("aprovado", sql.Bit, aprovado) .input("senhaHash", sql.NVarChar(255), senhaHash) .query(` INSERT INTO slalog.usuarios ( id, email, nome, aprovado, ativo, senha_hash, created_at, updated_at ) OUTPUT inserted.id, inserted.email, inserted.nome, inserted.aprovado, inserted.created_at VALUES ( @id, @email, @nome, @aprovado, 1, @senhaHash, SYSDATETIME(), SYSDATETIME() ) `); if (aprovado) { await pool .request() .input("id", sql.UniqueIdentifier, crypto.randomUUID()) .input("userId", sql.UniqueIdentifier, criado.recordset[0].id) .input("role", sql.NVarChar(20), "admin") .query(` INSERT INTO slalog.usuarios_papeis (id, user_id, role, created_at) VALUES (@id, @userId, @role, SYSDATETIME()) `); } return { perfil: toPerfil(criado.recordset[0]), aprovado, }; }); export const sair = createServerFn({ method: "POST" }).handler(async () => { const { clearAuthSession } = await import("@/lib/auth.server"); await clearAuthSession(); return { success: true }; }); export const listarPerfis = createServerFn({ method: "GET" }).handler(async () => { await requireAdminSession(); const [{ getPool }] = await Promise.all([import("@/lib/db.server")]); const pool = await getPool(); const result = await pool.query(` SELECT id, email, nome, aprovado, created_at FROM slalog.usuarios WHERE ativo = 1 ORDER BY created_at DESC `); return result.recordset.map(toPerfil); }); export const definirAprovacao = createServerFn({ method: "POST" }) .validator(z.object({ id: z.string().uuid(), aprovado: z.boolean() })) .handler(async ({ data }) => { await requireAdminSession(); const [{ getPool, sql }] = await Promise.all([import("@/lib/db.server")]); const pool = await getPool(); await pool .request() .input("id", sql.UniqueIdentifier, data.id) .input("aprovado", sql.Bit, data.aprovado) .query(` UPDATE slalog.usuarios SET aprovado = @aprovado, updated_at = SYSDATETIME() WHERE id = @id `); }); export async function obterSessaoAutenticada(): Promise { return await requireSession(); } export async function obterSessaoAprovada(): Promise { return await requireApprovedSession(); }