Iniciando um backoffice rapidamente com AdminJS

Na vida de um dev é praticamente obrigatório ter passado por uma task em que você precisava criar um backoffice para um sistema. Seja para um cliente ou para a própria empresa, é muito comum que seja necessário criar um painel de administração para gerenciar os dados do sistema. Hoje nós vamos configurar do 0 uma aplicação com o AdminJS, uma ferramenta que cria o backoffice para você.

Esse post é um tutorial step-by-step do AdminJS. Se você tem interesse em conhecer mais a ferramenta antes de iniciar, sugiro ler o artigo AdminJS: diga adeus a templates de backoffice.

Iniciando o projeto

Primeiro de tudo precisamos criar uma pasta (mkdir setup-admins) e iniciar o projeto node (yarn init). Após isso vamos instalar o AdminJS (yarn add adminjs) e o Typescript. Você pode copiar o comando abaixo e fazer tudo isso de uma vez só:

mkdir setup-admins && cd setup-admins && yarn init -y && yarn add adminjs typescript && yarn add -D ts-node tslib

Além disso também vou instalar o Typescript, e criar o arquivo tsconfig.json com o seguinte comando:

{
  "compilerOptions": {
    "target": "ESNEXT",
    "module": "NodeNext",
    "moduleResolution": "nodenext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Também será necessário adicionar o seguinte código no package.json:

{
  "type": "module",
  "scripts": {
    "start": "ts-node-esm app.ts"
  }
}

O AdminJS suporte apenas ECMAScript modules, por isso precisamos adicionar o type: module no package.json e o ts-node-esm no scripts.

Configurando o AdminJS

Agora você vai precisar escolher entre os diversos plugins e adapters que o AdminJS suporta. Essa decisão é importante para definir o framework http e o banco de dados que você vai utilizar na aplicação.

Imagem mostrando bancos de dados, orms e frameworks suportados

Eu vou de Express e MongoDb/Mongoose.

Configurando o framework HTTP (Express)

yarn add @adminjs/express express tslib express-formidable @types/express && yarn add -D @types/express 

Agora vamos criar o arquivo app.ts na raiz do projeto e colar o seguinte código:

import AdminJS from 'adminjs'
import AdminJSExpress from '@adminjs/express'
import express from 'express'

const PORT = 3000

const start = async () => {
  const app = express()

  const admin = new AdminJS({})

  const adminRouter = AdminJSExpress.buildRouter(admin)
  app.use(admin.options.rootPath, adminRouter)

  app.listen(PORT, () => {
    console.log(`AdminJS iniciado em http://localhost:${PORT}${admin.options.rootPath}`)
  })
}

start()

Configuração do banco de dados (Mongoose)

Vamos começar a configurar o banco de dados, para isso vamos utilizar o Mongoose. Primeiro vamos instalar as dependências:

yarn add mongoose @adminjs/moongose

Vamos começar a configuração registrando o adapter do Mongoose no AdminJS. Para isso vamos adicionar o seguinte código no topo arquivo app.ts:

// outros imports [..]
import * as AdminJSMongoose from '@adminjs/mongoose'

AdminJS.registerAdapter({
  Resource: AdminJSMongoose.Resource,
  Database: AdminJSMongoose.Database,
})
// resto do código [...]

E dentro da função start vamos iniciar a conexão com o banco de dados:

const start = async () => {
  const app = express()
  // adicione a linha abaixo no código
  await mongoose.connect("<mongo db url>");

  const admin = new AdminJS({})
  // resto do código [...]
}

Lembre-se de alterar trecho <mongo db url> para a url do seu banco de dados. Se você não tiver um MongoDB configurado, você pode criar um gratuitamente no MongoDB Atlas.

Criando o primeiro CRUD

Agora que já temos o AdminJS configurado, vamos criar o primeiro CRUD. Para o tutorial vamos levar em consideração que estamos fazendo um backoffice para uma locadora de filmes. Para isso vamos criar um arquivo movie.entity.ts com a seguinte estrutura:

export interface Actor {
  name: string;
  surname: string;
}

export interface Movie {
  title: string;
  synopsis: string;
  date: Date;
  price: number;
  categories: string[];
  actors: Actor[];
}

Observe que adicionei vários tipos diferentes de dados para mostrar como o AdminJS trata cada um deles.

Além da interface também vamos criar o schema do Mongoose, onde vamos mapear todas as propriedades do nosso modelo para o banco de dados. Para isso vamos criar o arquivo movie.schema.ts com o seguinte código:

import { Schema, model } from "mongoose";
import { Actor, Movie } from "./movie.entity.js";

export const ActorSchema = new Schema<Actor>({
  name: { type: String, required: true },
  surname: { type: String, required: true },
})

export const MovieSchema = new Schema<Movie>(
  {
    title: { type: String, required: true },
    synopsis: { type: String, required: true },
    date: { type: Date, required: true },
    price: { type: Number, required: true },
    categories: { type: [String], required: true },
    actors: { type: [ActorSchema], required: true },
  },
  { timestamps: true },
)

export const MovieModel = model<Movie>('Movie', MovieSchema);

Por fim, precisamos registrar o nosso modelo no AdminJS, que fornece a interface ResourceWithOptions, que tem as opções que podemos passar para cada resource. Então vamos criar o arquivo movie.resource.ts com o seguinte código:

import { ResourceWithOptions } from "adminjs";
import { MovieModel } from "./movie.schema.js";

export const MovieResource: ResourceWithOptions = {
  resource: MovieModel,
  options: {
    navigation: {
      icon: "Film",
    },
    properties: {
      synopsis: { type: "richtext" },
    },
  },
};

Para deixar o formulário mais adequado, configurei um ícone para o menu de navegação e o tipo do campo synopsis para richtext. Você pode ver mais sobre customização de propriedades ou acessar a lista de ícones FatherIcons.

Agora só precisamos de passar nosso recurso na opções do AdminJS, então vamos adicionar o seguinte código no o arquivo app.ts:

// outros imports [...]
import AdminJS, { AdminJSOptions } from "adminjs";
import { MovieResource } from "./movie.resource.js";

//[...]
const start = async () => {
  //[...]  
  const adminOptions: AdminJSOptions = {
    resources: [MovieResource],
  }
  const admin = new AdminJS(adminOptions);
  //[...]
};

Agora podemos acessar http://localhost:3000/admin/resources/Movie/actions/new e ver o formulário do CRUD de filmes:

Imagem mostrando o CRUD de filmes

Dashboard

O AdminJS vem com um dashboard padrão que é bem voltado para o desenvolvedor. Mas como tudo no AdminJS, ele também pode ser customizado. Então vamos criar um dash bem simples mostrando a quantidade de livros cadastrados. O AdminJS utiliza o styled-components e a lib já vem instalada com o pacote @adminjs/design-system. Então vamos instalar o pacote de tipagem do styled-components:

yarn add -D @types/styled-components

Agora vamos criar o arquivo dashboard.component.tsx que será o componente React do dashboard, com o seguinte código:

import { ApiClient } from "adminjs";
import React, { useEffect, useState } from "react";
import styled from "styled-components";

type DashboardResponse = {
  moviesCount: number;
}

export const Dashboard = () => {
  const [data, setData] = useState<DashboardResponse>({ moviesCount: 0 });
  const api = new ApiClient();

  useEffect(() => {
    getData();
  }, []);

  const getData = async () => {
    const response = await api.getDashboard<DashboardResponse>();
    setData(response.data);
  };

  const Box = styled.div`
    background-color: #fff;
    border-radius: 4px;
    padding: 20px;
    margin: 20px;
    box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
    border-color: #ddd;
    max-width: 200px;
  `

  return (
    <div>
      <h1>Dashboard</h1>
      <Box>
        <h2>Filmes</h2>
        <span>{data.moviesCount}</span>
      </Box>
    </div>
  );
};

export default Dashboard;

O ApiClient é uma classe utilitária do AdminJS que facilita a comunicação com o backend.

Para o AdminJS reconhecer o componente, precisamos carregar ele utilizando o ComponentLoader, onde vamos poder centralizar os nossos componentes customizados. Crie o arquivo components.ts com o seguinte código:

import { ComponentLoader } from "adminjs";

export const componentLoader = new ComponentLoader();

export const Components = {
  Dashboard: componentLoader.add("Dashboard", "./Dashboard.component"),
};

Hora de trabalhar na função que vai trazer os dados para o frontend do Dashboard, então crie um arquivo chamado dashboard.config.ts com o seguinte código:

import { AdminJSOptions } from "adminjs";
import { Components } from "./components.js";
import { MovieModel } from "./movie.schema.js";

const dashboardHandler = async () => {
  const moviesCount = await MovieModel.countDocuments();
  return { moviesCount };
};

export const DashboardOptions: AdminJSOptions["dashboard"] = {
  component: Components.Dashboard,
  handler: dashboardHandler,
};

A função dashboardHandler é responsável por trazer os dados que serão utilizados no frontend do Dashboard.

Agora só precisamos adicionar o DashboardOptions e o componentLoader nas opções do AdminJS, então vamos adicionar o seguinte código no arquivo app.ts:

import { componentLoader } from "./components.js";
import { DashboardOptions } from "./dashboard.config.js";

const adminOptions: AdminJSOptions = {
  resources: [MovieResource],
  dashboard: DashboardOptions, // adicione essa configuração
  componentLoader, // adicione essa configuração
};

Agora podemos acessar http://localhost:3000/admin e ver o nosso dashboard:

Imagem mostrando o dashboard

Aqui tivemos uma pequena amostra do que podemos fazer com customização de componentes no AdminJS. Você pode ver mais sobre esse assunto na documentação do AdminJS.

Autenticação

Como todo backoffice que se prese, precisamos de autenticar os usuários que acessam o sistema. Então agora vamos configurar a autenticação no AdminJS. Vamos precisar adicionar as libs para gerenciar sessão e criptografar as senhas:

  yarn add @adminjs/passwords bcrypt connect-mongo express-session  && yarn add -D @types/express-session @types/express

Agora vamos criar os arquivos entity, schema e resource para o Administrador. Crie o arquivo admin.entity.ts com o seguinte código:

export interface Admin {
  name: string;
  email: string;
  hashedPassword: string;
}

Vamos utilizar o hashedPassword para armazenar a senha criptografada do usuário. Agora vamos criar o arquivo admin.schema.ts com o seguinte código:

import { model, Schema } from "mongoose";
import { Admin } from "./admin.entity.js";

export const AdminSchema = new Schema<Admin>(
  {
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    hashedPassword: { type: String, required: true },
  },
  { timestamps: true }
);

export const AdminModel = model<Admin>("Admin", AdminSchema);

Por fim, vamos criar o arquivo admin.resource.ts com o seguinte código:

import passwordsFeature from "@adminjs/passwords";
import bcrypt from 'bcrypt';
import { AdminModel } from "./admin.schema.js";
import { componentLoader } from "./components.js";

const encryptPassword = async (password: string) => {
  const passwordHashed = await bcrypt.hash(password, 10);
  return passwordHashed;
};

export const AdminResource = {
  resource: AdminModel,
  options: {
    navigation: {
      icon: "User",
    },
    properties: { hashedPassword: { isVisible: false } },
  },
  features: [
    passwordsFeature({
      properties: {
        encryptedPassword: "hashedPassword",
        password: "password",
      },
      hash: encryptPassword,
      componentLoader: componentLoader,
    }),
  ],
};

Observe que estamos utilizando o bcrypt para criptografar a senha do usuário. Além disso estamos utilizando a funcionalidade de senha do AdminJS, disponibilizada no pacote @adminjs/passwords, que faz as trativas de exibição do campo senha e faz a criptografia da senha antes de salvar no banco de dados.

Agora vamos precisar acessar o CRUD de Admin para cadastrar o primeiro usuário antes de finalizar a configuração de autenticação. Então acesse http://localhost:3000/admin/resources/Admin/actions/new e crie o primeiro usuário.

Imagem mostrando o formulário de cadastro de usuário

Agora vamos configurar a autenticação no AdminJS. Para isso vamos criar o arquivo auth.config.ts com o seguinte código:

import MongoStore from "connect-mongo";
import { AdminModel } from "./admin.schema.js";
import bcrypt from "bcrypt";

const authenticate = async (email: string, password: string) => {
  const admin = await AdminModel.findOne({ email });
  if (!admin) return null;
  const isValidPassword = await bcrypt.compare(password, admin.hashedPassword);
  if (!isValidPassword) return null;
  return admin;
};

export const AuthOptions = {
  auth: {
    authenticate: authenticate,
    cookieName: "adminjs",
    cookiePassword: "sessionsecret",
  },
  session: (sessionStore: MongoStore) => ({
    store: sessionStore,
    resave: true,
    saveUninitialized: true,
    secret: "sessionsecret",
    cookie: {
      httpOnly: process.env.NODE_ENV === "production",
      secure: process.env.NODE_ENV === "production",
    },
    name: "adminjs",
  }),
};

A função authenticate é responsável por validar o usuário e senha informados na tela de login e retornar um usuário válido em caso de sucesso. Se você precisar fazer uma lógica diferenciada de autenticação, utilizando um provedor externo da empresa por exemplo, é aqui que você deve alterar.

Agora vamos adicionar o AuthOptions nas opções do AdminJS, então vamos adicionar o seguinte código no arquivo app.ts:

import MongoStore from "connect-mongo";

// [...]

const start = async () => {
  await mongoose.connect(process.env.MONGO_DB_URL || "");
  // as alterações começam aqui
  const sessionStore = MongoStore.create({
    client: mongoose.connection.getClient(),
    collectionName: "sessions",
    stringify: false,
    autoRemove: "interval",
    autoRemoveInterval: 1,
  });
  const adminOptions: AdminJSOptions = {
    resources: [MovieResource, AdminResource],
    dashboard: DashboardOptions,
    componentLoader,
  };
  const admin = new AdminJS(adminOptions);
  const adminRouter = AdminJSExpress.buildAuthenticatedRouter(
    admin,
    AuthOptions.auth,
    null,
    AuthOptions.session(sessionStore)
  );
  // e finalizam aqui 
  // [...]
};

Agora podemos acessar http://localhost:3000/admin e você será redirecionado para a tela de login do AdminJS:

Imagem mostrando a tela de login do AdminJS

Conclusão

Projeto completo no Github

Vimos como configurar o AdminJS, criar um CRUD, customizar o dashboard e configurar a autenticação. O AdminJS é uma ferramenta muito poderosa e que pode ser utilizada para criar backoffices rapidamente e se você tiver interesse em aprender mais sobre a ferramenta você pode ler o artigo AdminJS: diga adeus a templates de backoffice.

Muito interessante o AdminJs, mas o dashboard poderia ter algum gráfico previsto no seu projeto ao final?

Tem sim. O AdminJS é totalmente customizável, da para você criar uma página e utilizar alguma lib de gráficos para te ajudar a exibir eles. Além de poder estilizar a sua maneira todo o painel. Você pode ver isso na [documentação do adminJS](https://docs.adminjs.co/faq/charts) e eu também expliquei isso no meu artigo sobre o AdminJS, da uma olhada la: [AdminJS: diga adeus aos templates de backoffice](https://caiofuzatto.com.br/post/adminjs-diga-adeus-aos-templates-de-backoffice/)