07 - Criando a interface do usuário
Criar o arquivo app/admin/users/page.tsx
// app/admin/users/page.tsx
import { supabaseServer } from "@/lib/supabaseServer";
import { supabaseAdmin } from "@/lib/supabaseAdmin";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
/**
* Server action: cria usuário + (opcional) vincula a um grupo
*/
async function createUserAction(formData: FormData) {
"use server";
const email = String(formData.get("email") ?? "").trim();
const password = String(formData.get("password") ?? "").trim();
const groupId = String(formData.get("group_id") ?? "");
if (!email || !password) {
console.error("Email e senha são obrigatórios");
return;
}
// Cria o usuário no Supabase Auth
const { data, error } = await supabaseAdmin.auth.admin.createUser({
email,
password,
email_confirm: true,
});
if (error || !data.user) {
console.error("Erro ao criar usuário:", error?.message);
return;
}
// Se veio um grupo, já vincula o usuário a ele
if (groupId) {
const { error: relError } = await supabaseAdmin
.from("app_group_users")
.insert({
group_id: groupId,
user_id: data.user.id,
});
if (relError) {
console.error("Erro ao vincular grupo:", relError.message);
}
}
revalidatePath("/admin/users");
}
/**
* Server action: adiciona um grupo a um usuário existente
*/
async function addUserToGroupAction(formData: FormData) {
"use server";
const userId = String(formData.get("user_id") ?? "");
const groupId = String(formData.get("group_id") ?? "");
if (!userId || !groupId) return;
const { error } = await supabaseAdmin
.from("app_group_users")
.insert({ user_id: userId, group_id: groupId });
if (error) {
console.error("Erro ao vincular grupo a usuário:", error.message);
}
revalidatePath("/admin/users");
}
export default async function AdminUsersPage() {
const supabase = await supabaseServer();
// 1) Garante que está logado
const { data: authData } = await supabase.auth.getUser();
const user = authData.user;
if (!user) {
redirect("/login");
}
// 2) Restrição de acesso por e-mail (só você / admins)
const allowedEmails = ["seu-email@dominio.com"]; // AJUSTE AQUI
if (!allowedEmails.includes(user.email ?? "")) {
redirect("/");
}
// 3) Carrega grupos
const { data: groups, error: groupsError } = await supabaseAdmin
.from("app_groups")
.select("id, name, slug")
.order("name", { ascending: true });
if (groupsError) {
console.error("Erro carregando grupos:", groupsError.message);
}
// 4) Carrega relações usuário × grupo
const { data: groupUsers, error: gusError } = await supabaseAdmin
.from("app_group_users")
.select("group_id, user_id");
if (gusError) {
console.error("Erro carregando app_group_users:", gusError.message);
}
// 5) Carrega usuários do Supabase Auth (primeira página, até 1000)
const { data: usersPage, error: usersError } =
await supabaseAdmin.auth.admin.listUsers({
perPage: 1000,
});
if (usersError) {
console.error("Erro listando usuários:", usersError.message);
}
const users = usersPage?.users ?? [];
// Map rápido: groupId -> group
const groupMap = new Map(
(groups ?? []).map((g) => [g.id, g] as const)
);
// Map: userId -> grupos[]
const userGroupsMap = new Map<
string,
{ id: string; name: string; slug: string }[]
>();
(groupUsers ?? []).forEach((rel) => {
const g = groupMap.get(rel.group_id);
if (!g) return;
if (!userGroupsMap.has(rel.user_id)) {
userGroupsMap.set(rel.user_id, []);
}
userGroupsMap.get(rel.user_id)!.push(g);
});
return (
<main className="max-w-5xl mx-auto p-8 space-y-10">
<header className="flex items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold">Administração de Usuários</h1>
<p className="text-sm text-gray-600">
Criar usuários, atribuir grupos e visualizar as permissões atuais.
</p>
</div>
</header>
{/* Formulário de criação de usuário */}
<section className="border rounded-lg p-4 space-y-4">
<h2 className="font-semibold text-lg">Criar novo usuário</h2>
<form action={createUserAction} className="grid gap-3 md:grid-cols-3">
<div className="flex flex-col gap-1 md:col-span-1">
<label className="text-xs font-medium">E-mail</label>
<input
name="email"
type="email"
required
className="border rounded px-2 py-1 text-sm"
placeholder="usuario@empresa.com"
/>
</div>
<div className="flex flex-col gap-1 md:col-span-1">
<label className="text-xs font-medium">Senha inicial</label>
<input
name="password"
type="password"
required
className="border rounded px-2 py-1 text-sm"
placeholder="Senha temporária"
/>
</div>
<div className="flex flex-col gap-1 md:col-span-1">
<label className="text-xs font-medium">
Grupo inicial (opcional)
</label>
<select
name="group_id"
className="border rounded px-2 py-1 text-sm"
defaultValue=""
>
<option value="">(nenhum)</option>
{(groups ?? []).map((g) => (
<option key={g.id} value={g.id}>
{g.name} ({g.slug})
</option>
))}
</select>
</div>
<div className="md:col-span-3">
<button
type="submit"
className="mt-2 inline-flex items-center px-4 py-1.5 rounded bg-blue-600 text-white text-sm font-medium"
>
Criar usuário
</button>
</div>
</form>
</section>
{/* Lista de usuários */}
<section className="border rounded-lg p-4 space-y-4">
<h2 className="font-semibold text-lg">Usuários cadastrados</h2>
{users.length === 0 ? (
<p className="text-sm text-gray-500">Nenhum usuário encontrado.</p>
) : (
<div className="space-y-3">
{users.map((u) => {
const userGroups = userGroupsMap.get(u.id) ?? [];
return (
<div
key={u.id}
className="border rounded-md p-3 flex flex-col gap-3"
>
<div className="flex flex-wrap justify-between gap-2">
<div>
<p className="font-medium text-sm">
{u.email ?? "(sem e-mail)"}
</p>
<p className="text-[11px] text-gray-500">
ID: {u.id}
</p>
</div>
<div className="text-[11px] text-gray-500 text-right">
<div>
Criado em:{" "}
{u.created_at
? new Date(u.created_at).toLocaleString()
: "-"}
</div>
<div>Status: {u.user_metadata?.banned ? "Banido" : "Ativo"}</div>
</div>
</div>
<div className="flex flex-col gap-2">
<div>
<span className="text-xs font-semibold">
Grupos atuais:
</span>
{userGroups.length === 0 ? (
<span className="text-xs text-gray-500 ml-2">
(nenhum)
</span>
) : (
<div className="mt-1 flex flex-wrap gap-1">
{userGroups.map((g) => (
<span
key={g.id}
className="px-2 py-0.5 rounded bg-gray-200 text-[11px]"
>
{g.name} ({g.slug})
</span>
))}
</div>
)}
</div>
{/* Form para adicionar grupo a usuário existente */}
<form
action={addUserToGroupAction}
className="flex flex-wrap items-center gap-2"
>
<input type="hidden" name="user_id" value={u.id} />
<select
name="group_id"
className="border rounded px-2 py-1 text-xs"
defaultValue=""
>
<option value="">Adicionar a grupo...</option>
{(groups ?? []).map((g) => (
<option key={g.id} value={g.id}>
{g.name} ({g.slug})
</option>
))}
</select>
<button
type="submit"
className="px-3 py-1 rounded bg-slate-700 text-white text-xs"
>
Adicionar grupo
</button>
</form>
</div>
</div>
);
})}
</div>
)}
</section>
</main>
);
}