Essas funções tocam módulos *.server.* (sessão em cookie, conexão SQL Server) mas eram funções soltas, então o TanStack Start acusava "import denied in client environment" ao analisar o grafo de import que chega em auth.ts a partir de rotas renderizadas no cliente. Na prática já eram seguras (só chamadas de dentro de handlers createServerFn, que rodam só no servidor), mas createServerOnlyFn torna isso explícito e reconhecido pela análise estática do framework, além de lançar erro em vez de rodar se algum dia forem chamadas do lado do cliente por engano.
346 lines
9.5 KiB
TypeScript
346 lines
9.5 KiB
TypeScript
import { createServerFn, createServerOnlyFn } 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),
|
|
};
|
|
}
|
|
|
|
const getSessionUserId = createServerOnlyFn(async (): Promise<string | null> => {
|
|
const { getSessionUserId: getServerSessionUserId } = await import("@/lib/auth.server");
|
|
return await getServerSessionUserId();
|
|
});
|
|
|
|
const fetchSessionProfile = createServerOnlyFn(async (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 recuperarSenha = 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 existente = await pool
|
|
.request()
|
|
.input("nome", sql.NVarChar(150), nome)
|
|
.query<{ id: string; ativo: boolean }>(`
|
|
SELECT TOP 1 id, ativo
|
|
FROM slalog.usuarios
|
|
WHERE LOWER(nome) = LOWER(@nome)
|
|
`);
|
|
|
|
const usuario = existente.recordset[0];
|
|
if (!usuario || !usuario.ativo) {
|
|
throw new Error("Nome de usuário não encontrado.");
|
|
}
|
|
|
|
const senhaHash = await bcrypt.hash(data.senha, 10);
|
|
|
|
await pool
|
|
.request()
|
|
.input("id", sql.UniqueIdentifier, usuario.id)
|
|
.input("senhaHash", sql.NVarChar(255), senhaHash)
|
|
.query(`
|
|
UPDATE slalog.usuarios
|
|
SET senha_hash = @senhaHash,
|
|
aprovado = 0,
|
|
updated_at = SYSDATETIME()
|
|
WHERE id = @id
|
|
`);
|
|
|
|
const { clearAuthSession } = await import("@/lib/auth.server");
|
|
await clearAuthSession();
|
|
|
|
return { success: true };
|
|
});
|
|
|
|
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();
|
|
}
|