Files
SLA_SEPARACAO/src/lib/auth.ts
T
2026-08-06 11:13:33 -03:00

302 lines
8.2 KiB
TypeScript

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<string | null> {
const { getSessionUserId: getServerSessionUserId } = await import("@/lib/auth.server");
return await getServerSessionUserId();
}
async function fetchSessionProfile(userId: string): Promise<SessaoAtual | null> {
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<PerfilRow & { admin: boolean }>(`
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<SessaoAtual> {
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<SessaoAtual> {
const sessao = await requireSession();
if (!sessao.admin) throw new Error("Apenas administradores podem executar esta ação.");
return sessao;
}
async function requireApprovedSession(): Promise<SessaoAtual> {
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<PerfilRow>(`
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<PerfilRow>(`
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<SessaoAtual> {
return await requireSession();
}
export async function obterSessaoAprovada(): Promise<SessaoAtual> {
return await requireApprovedSession();
}