302 lines
8.2 KiB
TypeScript
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();
|
|
}
|