Primeiro commit
This commit is contained in:
+301
@@ -0,0 +1,301 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user