Aula 28 – Deploy do Sistema de Autenticação no Render.com

Aula 28 – Deploy do Sistema de Autenticação no Render.com

Voltar para página principal do site

Todas as aulas desse curso

Aula 27

Fiber

Fiber

Redes Sociais do Código Fluente:

facebook

 

 


Scarlett Finch

Scarlett Finch é uma influenciadora virtual criada com IA.

Ela é 🎤 cantora e 🎶compositora pop britânica , 24 anos de idade.

Siga a Scarlett Finch no Instagram:

facebook

 


Conecte-se comigo!

LinkedIn: Fique à vontade para me adicionar no LinkedIn.

Ao conectar-se comigo, você terá acesso a atualizações regulares sobre desenvolvimento web, insights profissionais e oportunidades de networking no setor de tecnologia.

GitHub: Siga-me no GitHub para ficar por dentro dos meus projetos mais recentes, colaborar em código aberto ou simplesmente explorar os repositórios que eu contribuo, o que pode ajudar você a aprender mais sobre programação e desenvolvimento de software.

Recursos e Afiliados

Explorando os recursos abaixo, você ajuda a apoiar nosso site.

Somos parceiros afiliados das seguintes plataformas:

  • https://heygen.com/ – Eleve a produção de seus vídeos com HeyGen! Com esta plataforma inovadora, você pode criar vídeos envolventes utilizando avatares personalizados, ideal para quem busca impactar e conectar com audiências em todo o mundo. HeyGen transforma a maneira como você cria conteúdo, oferecendo ferramentas fáceis de usar para produzir vídeos educativos, demonstrações de produtos e muito mais. Descubra o poder de comunicar através de avatares interativos e traga uma nova dimensão para seus projetos. Experimente HeyGen agora e revolucione sua forma de criar vídeos!
  • letsrecast.ai – Redefina a maneira como você consome artigos com Recast. Esta plataforma transforma artigos longos em diálogos de áudio que são informativos, divertidos e fáceis de entender. Ideal para quem está sempre em movimento ou busca uma forma mais conveniente de se manter informado. Experimente Recast agora.
  • dupdub.com – Explore o universo do marketing digital com DupDub. Esta plataforma oferece ferramentas inovadoras e soluções personalizadas para elevar a sua estratégia de marketing online. Ideal para empresas que buscam aumentar sua visibilidade e eficiência em campanhas digitais. Descubra mais sobre DupDub.
  • DeepBrain AI Studios – Revolucione a criação de conteúdo com a tecnologia de inteligência artificial da DeepBrain AI Studios. Esta plataforma avançada permite que você crie vídeos interativos e apresentações utilizando avatares digitais gerados por IA, que podem simular conversas reais e interações humanas. Perfeito para educadores, criadores de conteúdo e empresas que querem inovar em suas comunicações digitais. Explore DeepBrain AI Studios.
  • Audyo.ai – Transforme a maneira como você interage com conteúdo auditivo com Audyo.ai. Esta plataforma inovadora utiliza inteligência artificial para criar experiências de áudio personalizadas, melhorando a acessibilidade e a compreensão de informações através de podcasts, transcrições automáticas e síntese de voz avançada. Ideal para profissionais de mídia, educadores e qualquer pessoa que deseje acessar informações auditivas de maneira mais eficiente e envolvente. Descubra Audyo.ai e suas possibilidades.
  • Acoust.io – Transforme sua produção de áudio com Acoust.io. Esta plataforma inovadora fornece uma suite completa de ferramentas para criação, edição e distribuição de áudio, ideal para artistas, produtores e empresas de mídia em busca de excelência e inovação sonora. Acoust.io simplifica o processo de levar suas ideias à realidade, oferecendo soluções de alta qualidade que elevam seus projetos de áudio. Experimente Acoust.io agora e descubra um novo patamar de possibilidades para seu conteúdo sonoro.
  • Hostinger – Hospedagem web acessível e confiável. Ideal para quem busca soluções de hospedagem de sites com excelente custo-benefício e suporte ao cliente robusto. Saiba mais sobre a Hostinger.
  • Digital Ocean – Infraestrutura de nuvem para desenvolvedores. Oferece uma plataforma de nuvem confiável e escalável projetada especificamente para desenvolvedores que precisam de servidores virtuais, armazenamento e networking. Explore a Digital Ocean.
  • One.com – Soluções simples e poderosas para o seu site. Uma escolha ideal para quem busca registrar domínios, hospedar sites ou criar presença online com facilidade e eficiência. Visite One.com.

Educação e Networking

Amplie suas habilidades e sua rede participando de cursos gratuitos e comunidades de desenvolvedores:

Canais do Youtube

Explore nossos canais no YouTube para uma variedade de conteúdos educativos e de entretenimento, cada um com um foco único para enriquecer sua experiência de aprendizado e lazer.

Toti

Toti: Meu canal pessoal, onde posto clips artesanais de músicas que curto tocar, dicas de teoria musical, entre outras coisas.

Scarlett Finch

Scarlett Finch: Cantora e influenciadora criada com IA.

Lofi Music Zone Beats

Lofi Music Zone Beats: O melhor da música Lofi para estudo, trabalho e relaxamento, criando o ambiente perfeito para sua concentração.

Backing Track / Play-Along

Backing Track / Play-Along: Acompanhe faixas instrumentais para prática musical, ideal para músicos que desejam aprimorar suas habilidades.

Código Fluente

Código Fluente: Aulas gratuitas de programação, devops, IA, entre outras coisas.

Putz!

Putz!: Canal da banda Putz!, uma banda virtual, criada durante a pandemia com mais 3 amigos, Fábio, Tatá e Lula.

PIX para doações

PIX Nubank

PIX Nubank


Aula 28 – Deploy do Sistema de Autenticação no Render.com

Links da Aula:

Repositório do Frontend

Repositório do Backend

Render Dashboard

FreedDB – Banco de Dados

Senha de App do Gmail

Introdução

Esta aula é um complemento do tutorial de Fiber/React, onde vamos aprender como fazer o deploy da aplicação desenvolvida usando o render.com, uma plataforma que oferece hospedagem gratuita e simplicidade no processo de deploy.

Será um deploy simples, sem nada automatizado.

Na aula 13 do tutorial de Kubernates, no menu superior em DevOps/Kubernates, eu mostro como fazer o deploy em um cluster K8S na Digital Ocean, usando o Terraform para automatizar tudo.

Branchs

Para essa aula, criei as branchs 10-deploy do frontend e a 18-deploy do backend, para as mudanças no código dessa aula.

Para o deploy, estou usando as branchs master de cada projeto, porque elas já estão iguais as versões de deploy de cada projeto, isto é, elas já estão mergeadas, misturadas.

Antes de Começar

Melhoria da Configuração do Ambiente de Desenvolvimento

Air – Hot Reload para Go

Esse arquivo de configuração do Air não existia, ele é usada apenas em desenvolvimento local.

fiber-project/.air.toml

root = "."
tmp_dir = "tmp"

[build]
  cmd = "go build -o ./tmp/main.exe ."
  bin = "tmp/main.exe"
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor"]
  include_ext = ["go", "tpl", "tmpl", "html"]
  exclude_regex = ["_test.go"]

[screen]
  clear_on_rebuild = true

[misc]
  clean_on_exit = true

O arquivo .air.toml é usado para configurar o Air, uma ferramenta de hot reload para Go que recompila e reinicia o servidor automaticamente durante o desenvolvimento local, agilizando o processo de testes e ajustes no código.

Ele define o diretório raiz, onde os binários temporários serão gerados, os arquivos e extensões monitorados e exclui diretórios irrelevantes, como vendor e tmp.

Também limpa o terminal antes de exibir novos logs e remove arquivos temporários ao encerrar o processo.

Isso melhora a produtividade, evitando a necessidade de reiniciar manualmente o servidor a cada alteração.

Mudanças Necessárias para o Deploy (Backend)

Configuração do Servidor Backend (Fiber/Go)

Melhorias no Modelo PasswordReset

Mudanças no fiber-project/models/passwordReset.go:

fiber-project/models/passwordReset.go

// Antes
type PasswordReset struct {
	Id    uint
	Email string
	Token string
}

// Depois
type PasswordReset struct {
	ID        uint           `json:"id" gorm:"primaryKey"`
	Email     string         `json:"email" gorm:"not null;type:varchar(255)"`
	Token     string         `json:"token" gorm:"not null;type:varchar(100);uniqueIndex"`
	ExpiresAt time.Time      `json:"expires_at" gorm:"type:datetime"`
	CreatedAt time.Time      `json:"created_at" gorm:"autoCreateTime"`
	UpdatedAt time.Time      `json:"updated_at" gorm:"autoUpdateTime"`
	DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
}

O modelo foi aprimorado para incluir novos campos e funcionalidades que garantem maior robustez e compatibilidade com o banco de dados.

Agora, ele suporta expiração automática de tokens, rastreamento de criação e atualização dos registros, e permite soft deletes para auditoria, sem remover os dados fisicamente.

As tags do GORM foram configuradas para evitar valores nulos, definir limites de tamanho e garantir unicidade no banco, tornando o modelo mais alinhado às boas práticas e preparado para operações mais complexas.

Além disso, a renomeação de Id para ID segue as convenções de nomenclatura do Go, melhorando a legibilidade.

Melhorias no Main

Mudanças no main.go:

  • Adição de configuração dinâmica de porta
  • Implementação completa de CORS para produção

fiber-project/main.go

// Antes
package main

import (
	"fiber-project/database"
	"fiber-project/routes"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
)

func main() {
	database.Connect()
	app := fiber.New()
	app.Use(cors.New(cors.Config{
		AllowCredentials: true,
	}))
	routes.Setup(app)
	app.Listen(":3000")
}

// Depois
package main

import (
	"fiber-project/database"
	"fiber-project/routes"
	"os"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
)

func main() {
	database.Connect()
	app := fiber.New()

	// Configuração de CORS dinâmica
	app.Use(cors.New(cors.Config{
		AllowOriginsFunc: func(origin string) bool {
			// Permite todas as origens válidas (não vazias)
			return origin != ""
		},
		AllowMethods:     "GET,POST,PUT,DELETE",
		AllowHeaders:     "Origin, Content-Type, Accept, Authorization",
		AllowCredentials: true,
	}))

	// Configuração das rotas
	routes.Setup(app)

	// Configuração da porta
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	// Inicialização do servidor
	app.Listen(":" + port)
}

As mudanças tornam o código mais flexível para diferentes ambientes.

A configuração de CORS agora é dinâmica, permitindo origens válidas de maneira mais abrangente e incluindo métodos e cabeçalhos específicos usados em ambientes modernos de APIs.

Isso melhora a compatibilidade com sistemas externos.

A configuração da porta foi alterada para utilizar uma variável de ambiente (PORT), que é uma prática comum em plataformas como render.com.

Se a variável não estiver definida, a aplicação usa 3000 como valor padrão, garantindo compatibilidade com o ambiente local e produção.

Essas alterações refletem a necessidade de adaptar o aplicativo para deploy em diferentes serviços sem exigir alterações manuais constantes.

Melhoria de Conexão e Gerenciamento do Banco de Dados

Mudanças no fiber-project/database/connect.go:

  • Carregamento condicional do arquivo .env (desenvolvimento vs. produção)
  • Melhor tratamento de erros
  • Logs mais informativos

fiber-project/database/connect.go

// Antes
func Connect() {
    err := godotenv.Load("../.env") // Load .env file
    if err != nil {
        log.Fatalf("Error loading .env file: %v", err)
    }
    dsn := os.Getenv("DB_DSN") // Get DSN from environment variables
    if dsn == "" {
        log.Fatal("DB_DSN is not set in .env file")
    }
    connection, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("Failed to connect to database")
    }
    DB = connection
    connection.AutoMigrate(&models.User{}, &models.PasswordReset{})
    fmt.Println("Database connection successful!")
}

// Depois
func Connect() {
    if os.Getenv("ENV") != "production" {
        if err := godotenv.Load(); err != nil {
            log.Printf("Aviso: Arquivo .env não encontrado, usando variáveis de ambiente padrão.")
        }
    }
    dsn := os.Getenv("DB_DSN")
    if dsn == "" {
        log.Fatal("Erro: A variável de ambiente DB_DSN não está configurada")
    }
    connection, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Erro ao conectar ao banco de dados: %v", err)
    }
    DB = connection
    migrate(connection)
}

func migrate(connection *gorm.DB) {
    connection.Migrator().DropTable(&models.PasswordReset{})
    connection.AutoMigrate(&models.User{}, &models.PasswordReset{})
    log.Println("Migração do banco de dados realizada com sucesso!")
    // Exibe a estrutura da tabela para depuração
    var tableInfo []struct{ Field, Type, Null, Key, Default, Extra string }
    connection.Raw("DESCRIBE password_resets").Scan(&tableInfo)
    fmt.Printf("Estrutura da tabela password_resets: %+v\n", tableInfo)
}

As principais alterações foram a restrição do carregamento do .env a ambientes de desenvolvimento e a introdução de uma função migrate.

A migrate recria a tabela password_resets quando necessário, atualiza a estrutura do banco e fornece logs detalhados para depuração.

Isso garante compatibilidade entre o código e o banco mesmo após mudanças no modelo.

Melhorias nas Rotas da Aplicação

Mudanças no fiber-project/routes/routes.go:

  • Adicionada uma rota de verificação de saúde (/health) para monitorar o status do serviço e facilitar diagnósticos.
  • Introduzida uma rota para redirecionar tokens de redefinição de senha (/reset/:token) para o frontend, utilizando a variável de ambiente FRONTEND_URL para maior flexibilidade e integração.
  • Melhor tratamento de erros para cenários onde o FRONTEND_URL não está configurado, com mensagens claras e adequadas para facilitar a depuração.

Essas melhorias tornam as rotas mais completas, confiáveis e integradas, além de facilitar a manutenção e a experiência do usuário final.

fiber-project/routes/routes.go

// Antes
func Setup(app *fiber.App) {
	api := app.Group("/api")

	api.Post("/register", controllers.Register)
	api.Post("/login", controllers.Login)
	api.Get("/user", controllers.User)
	api.Post("/logout", controllers.Logout)
	api.Post("/forgot", controllers.Forgot)
	api.Post("/reset", controllers.Reset)
}

// Depois
func Setup(app *fiber.App) {
	api := app.Group("/api")

	api.Post("/register", controllers.Register)
	api.Post("/login", controllers.Login)
	api.Get("/user", controllers.User)
	api.Post("/logout", controllers.Logout)
	api.Post("/forgot", controllers.Forgot)
	api.Post("/reset", controllers.Reset)

	// Nova rota de verificação de saúde
	api.Get("/health", func(c *fiber.Ctx) error {
		return c.JSON(fiber.Map{
			"status":  "OK",
			"service": "auth-api",
			"version": "1.0.0",
		})
	})

	// Nova rota para redirecionar /reset/:token para o frontend
	app.Get("/reset/:token", func(c *fiber.Ctx) error {
		token := c.Params("token")
		frontendURL := os.Getenv("FRONTEND_URL")
		if frontendURL == "" {
			return c.Status(500).SendString("FRONTEND_URL não está configurado nas variáveis de ambiente")
		}
		return c.Redirect(frontendURL + "/reset/" + token, 302)
	})
}

As alterações no arquivo routes.go foram feitas para adicionar duas novas funcionalidades que melhoram o gerenciamento do sistema e a integração com o frontend.

Primeiramente, foi adicionada uma rota de verificação de saúde (/health).

Essa rota serve para fornecer informações básicas sobre o estado do serviço, como se ele está ativo e qual versão está rodando.

Isso é útil tanto para monitoramento automatizado quanto para diagnósticos rápidos.

Além disso, foi criada uma nova rota para redirecionar tokens de redefinição de senha (/reset/:token) para o frontend.

Essa mudança foi necessária para facilitar a integração entre o backend e o frontend, garantindo que o token gerado pelo backend seja usado corretamente na página de redefinição de senha do frontend.

Essa funcionalidade depende da variável de ambiente FRONTEND_URL, que precisa estar configurada corretamente para que o redirecionamento funcione.

Essas mudanças foram feitas para melhorar a robustez, o diagnóstico e a experiência do usuário ao navegar no sistema.

Melhoria na Gestão e Validação de Tokens de Redefinição de Senha 

Mudanças no fiber-project/services/forgot_service.go:

fiber-project/services/forgot_service.go

// IMPORTS
//antes
package services
import (
    "fiber-project/database"
    "fiber-project/models"
    "math/rand"
    "net/smtp"
    "github.com/gofiber/fiber/v2"
    "golang.org/x/crypto/bcrypt"
)

//depois
package services
import (
    "fiber-project/database"
    "fiber-project/models"
    "fmt"           // Adicionado para melhor logging
    "math/rand"
    "net/smtp"
    "os"           // Adicionado para variáveis de ambiente
    "time"         // Adicionado para manipulação de datas
    "github.com/gofiber/fiber/v2"
    "golang.org/x/crypto/bcrypt"
)

// FUNÇÃO FORGOT
//antes
func Forgot(c *fiber.Ctx) error {
    var data map[string]string
    if err := c.BodyParser(&data); err != nil {
        return err
    }
    token := RandStringRunes(12)
    passwordReset := models.PasswordReset{
        Email: data["email"],
        Token: token,
    }
    database.DB.Create(&passwordReset)
    from := "fluentcode@exemple.com"
    to := []string{
        data["email"],
    }
    message := []byte("Clique aqui para redefinir sua senha!")
    err := smtp.SendMail("localhost:1025", nil, from, to, message)
    if err != nil {
        return err
    }
    return c.JSON(fiber.Map{
        "message": "success",
    })
}

//depois
func Forgot(c *fiber.Ctx) error {
    var data map[string]string

    if err := c.BodyParser(&data); err != nil {
        fmt.Printf("Erro ao fazer parse do body em Forgot: %v\n", err)
        return c.Status(400).JSON(fiber.Map{
            "message": "Invalid request data",
        })
    }

    // Adicionada validação de email
    if data["email"] == "" {
        return c.Status(400).JSON(fiber.Map{
            "message": "Email is required",
        })
    }

    token := RandStringRunes(12)
    
    // Adicionado ExpiresAt
    passwordReset := models.PasswordReset{
        Email:     data["email"],
        Token:     token,
        ExpiresAt: time.Now().Add(1 * time.Hour),
    }
    
    // Melhor tratamento de erro
    if err := database.DB.Create(&passwordReset).Error; err != nil {
        fmt.Printf("Erro ao salvar token: %v\n", err)
        return c.Status(500).JSON(fiber.Map{
            "message": "Error saving token",
        })
    }

    // Configuração de email mais robusta usando variáveis de ambiente
    auth := smtp.PlainAuth("", os.Getenv("GMAIL_EMAIL"), os.Getenv("GMAIL_APP_PASSWORD"), "smtp.gmail.com")
    to := []string{data["email"]}
    msg := []byte("To: " + data["email"] + "\r\n" +
        "Subject: Redefina sua senha\r\n" +
        "\r\n" +
        "Use o link para redefinir sua senha: " + os.Getenv("APP_URL") + "/reset/" + token + "\r\n")

    if err := smtp.SendMail("smtp.gmail.com:587", auth, os.Getenv("GMAIL_EMAIL"), to, msg); err != nil {
        fmt.Printf("Erro ao enviar email: %v\n", err)
        return c.Status(500).JSON(fiber.Map{
            "message": "Error sending email",
        })
    }

    return c.JSON(fiber.Map{
        "message": "success",
    })
}

// FUNÇÃO RESET
//antes
func Reset(c *fiber.Ctx) error {
    var data map[string]string
    if err := c.BodyParser(&data); err != nil {
        return err
    }
    if data["password"] != data["confirm_password"] {
        c.Status(400)
        return c.JSON(fiber.Map{
            "message": "Passwords do not match!",
        })
    }
    var passwordReset = models.PasswordReset{}
    if err := database.DB.Where("token = ?", data["token"]).Last(&passwordReset); err.Error != nil {
        c.Status(400)
        return c.JSON(fiber.Map{
            "message": "Invalid token!",
        })
    }
    password, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 14)
    database.DB.Model(&models.User{}).Where("email = ?", passwordReset.Email).Update("password", password)
    return c.JSON(fiber.Map{
        "message": "success",
    })
}

//depois
func Reset(c *fiber.Ctx) error {
    var data map[string]string

    if err := c.BodyParser(&data); err != nil {
        fmt.Printf("Erro ao fazer parse do body em Reset: %v\n", err)
        return c.Status(400).JSON(fiber.Map{
            "message": "Invalid request data",
        })
    }

    // Validações mais completas
    if data["token"] == "" {
        return c.Status(400).JSON(fiber.Map{
            "message": "Token is required!",
        })
    }

    if data["password"] == "" {
        return c.Status(400).JSON(fiber.Map{
            "message": "Password is required!",
        })
    }

    if data["password"] != data["confirm_password"] {
        return c.Status(400).JSON(fiber.Map{
            "message": "Passwords do not match!",
        })
    }

    // Estrutura específica para obter apenas os dados necessários
    var resetInfo struct {
        Email     string
        Token     string
        ExpiresAt string
    }

    // Query mais eficiente
    result := database.DB.Model(&models.PasswordReset{}).
        Select("email, token, expires_at").
        Where("token = ?", data["token"]).
        First(&resetInfo)

    if result.Error != nil {
        fmt.Printf("Erro ao buscar token no banco: %v\n", result.Error)
        return c.Status(400).JSON(fiber.Map{
            "message": "Invalid token!",
        })
    }

    // Verificação de expiração do token
    expiresAt, err := parseExpirationDate(resetInfo.ExpiresAt)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "message": "Error processing token expiration",
        })
    }

    if time.Now().After(expiresAt) {
        return c.Status(400).JSON(fiber.Map{
            "message": "Token has expired",
        })
    }

    // Melhor tratamento de erro ao gerar hash
    password, err := bcrypt.GenerateFromPassword([]byte(data["password"]), 14)
    if err != nil {
        fmt.Printf("Erro ao gerar hash da senha: %v\n", err)
        return c.Status(500).JSON(fiber.Map{
            "message": "Error generating password hash",
        })
    }

    // Atualização com verificação de erro
    updateResult := database.DB.Model(&models.User{}).
        Where("email = ?", resetInfo.Email).
        Update("password", password)

    if updateResult.Error != nil {
        return c.Status(500).JSON(fiber.Map{
            "message": "Error updating password",
        })
    }

    // Remoção do token usado
    database.DB.Where("token = ?", data["token"]).Delete(&models.PasswordReset{})

    return c.JSON(fiber.Map{
        "message": "Password successfully updated!",
    })
}

// NOVAS FUNÇÕES E VARIÁVEIS ADICIONADAS
//antes
// Não existiam

//depois
// Formatos de data suportados
var formatosData = []string{
    "2006-01-02 15:04:05",
    "2006-01-02T15:04:05Z",
    "2006-01-02T15:04:05.999Z",
    "2006-01-02T15:04:05-07:00",
    time.RFC3339,
    time.RFC3339Nano,
}

// Função para parse de data
func parseExpirationDate(dateStr string) (time.Time, error) {
    fmt.Printf("Tentando parsear data: %s\n", dateStr)
    
    for _, formato := range formatosData {
        parsedTime, err := time.Parse(formato, dateStr)
        if err == nil {
            fmt.Printf("Sucesso usando formato: %s\n", formato)
            localTime := parsedTime.In(time.Local)
            return localTime, nil
        }
        fmt.Printf("Falha com formato %s: %v\n", formato, err)
    }
    
    return time.Time{}, fmt.Errorf("nenhum formato conhecido funcionou para a data '%s'", dateStr)
}

Evolução do forgot_service.go

O arquivo forgot_service.go evoluiu de uma implementação básica para uma solução mais robusta de recuperação de senha.

Originalmente, o código não tinha controle de expiração do token – após gerado, ele permanecia válido indefinidamente.

A estrutura PasswordReset só armazenava email e token, sem nenhum campo para controlar tempo de vida.

A nova versão adiciona um campo ExpiresAt que marca quando o token expira.

Isso exigiu melhorias significativas no código para lidar com datas:

  • Uma lista de formatos de data suportados foi adicionada para compatibilidade
  • Uma função parseExpirationDate foi criada para interpretar diferentes padrões de data
  • O sistema agora verifica se o token expirou antes de permitir seu uso
  • Tokens são criados com 1 hora de validade (time.Now().Add(1 * time.Hour))

O tratamento de datas foi um dos maiores desafios, levando à criação de uma solução robusta que lida com vários formatos comuns de timestamps do banco de dados.

Essas mudanças tornaram o sistema mais seguro e profissional, evitando o uso de tokens antigos que poderiam comprometer a segurança das contas.

Configuração de Variáveis de Ambiente

Desenvolvimento Local (Exemplo de Arquivo .env)

fiber-project/.env


# Environment
ENV=development

# Database
DB_DSN=root:senhamysql@/nome_do_banco?charset=utf8mb4&parseTime=True&loc=Local

# JWT
JWT_SECRET=seu-segredo-local-aqui

# Email (Gmail)
GMAIL_EMAIL=seu-email@gmail.com
GMAIL_APP_PASSWORD=sua-senha-de-aplicativo
APP_URL=http://localhost:3000   
FRONTEND_URL=http://localhost:3001

Produção (render.com)

No Render, as variáveis de ambiente do backend são configuradas através da interface web:

  • APP_URL: “https://seu-app-fiber.onrender.com”
  • DB_DSN: “usuario:senha@tcp(sql.freedb.tech:3306)/nome_banco?charset=utf8mb4&parseTime=True&loc=Local”
  • FRONTEND_URL:”https://seu-app-react.onrender.com”
  • GMAIL_EMAIL: “seu-email@gmail.com”
  • GMAIL_APP_PASSWORD: “sua-senha-de-aplicativo”
  • JWT_SECRET: “seu-segredo-muito-seguro-aqui”

Mudanças Necessárias para o Deploy (Frontend)

Melhoria na Gestão de Autenticação e Configuração do Axios

Mudanças no react-auth/src/App.tsx:

react-auth/src/App.tsx

// Antes
import React, { useState, useEffect } from "react";
import axios from "axios";

// Código original omitia a configuração global do Axios
// Não havia suporte para JWT ou controle granular de headers
useEffect(() => {
  (async () => {
    try {
      const response = await axios.get("user");
      const user = response.data;
      setUser(user); 
    } catch (e) {
      setUser(null);
    }
  })();
}, [login]);

// Depois
import React, { useState, useEffect } from "react";
import axios from "axios";
import "./App.css";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Login from "./pages/Login";
import Home from "./pages/Home";
import Register from "./pages/Register";
import Forgot from "./pages/Forgot";
import Reset from "./pages/Reset";
import Nav from "./components/Nav";
import { startHealthCheck } from "./utils/healthCheck";

// Configuração global de credenciais e baseURL do Axios adicionada
axios.defaults.withCredentials = true;

const apiBaseUrl = process.env.REACT_APP_API_URL || "http://localhost:8080/api";

// Health Check Effect
useEffect(() => {
  startHealthCheck();
}, []);

// User Authentication Effect
useEffect(() => {
  const token = localStorage.getItem("jwt");

  if (token) {
    const config = {
      headers: {
        Authorization: `Bearer ${token}`,
        Accept: "*/*",
        "Content-Type": "application/json",
      },
    };

    axios
      .get(`${apiBaseUrl}/api/user`, config)
      .then((response) => setUser(response.data))
      .catch((error) => {
        setUser(null);
        localStorage.removeItem("jwt");
      });
  }
}, [login, apiBaseUrl]);

return (
  <div className="App">
    <Router>
      <Nav user={user} setLogin={() => setLogin(false)} />
      <Routes>
        <Route path="/login" element={<Login setLogin={() => setLogin(true)} />} />
        <Route path="/register" element={<Register />} />
        <Route path="/forgot" element={<Forgot />} />
        <Route path="/reset/:token" element={<Reset />} />
        <Route path="/" element={<Home user={user} />} />
        <Route path="*" element={<Home user={user} />} />
      </Routes>
    </Router>
  </div>
);

O que mudou?

O código foi aprimorado com a adição de um efeito Health Check que utiliza a função startHealthCheck para monitorar a disponibilidade do backend e identificar falhas rapidamente.

Além disso, foi configurado o axios para usar credenciais automaticamente (withCredentials) e uma base URL dinâmica (process.env.REACT_APP_API_URL).

O efeito de autenticação agora valida o JWT armazenado no localStorage e utiliza headers personalizados para obter os dados do usuário de maneira segura, garantindo que o estado da aplicação reflita o status de autenticação corretamente.

Melhoria no Fluxo de Redefinição de Senha e Validação de Dados

Mudanças no react-auth/src/pages/Reset.tsx:

react-auth/src/pages/Reset.tsx


// Antes
await axios.post(`${process.env.REACT_APP_API_URL || 'http://localhost:3001'}/api/reset`, {
  token,
  password,
  confirm_password: confirmPassword,
});

// Depois
const apiUrl = getApiUrl();
await axios.post(`${apiUrl}/api/reset`, {
  token,
  password,
  confirm_password: confirmPassword,
}, {
  headers: { "Content-Type": "application/json" },
});
setError(""); // Limpa o erro antes de enviar
setLoading(true); // Adiciona indicador de carregamento

// Antes
if (password !== confirmPassword) {
  console.error('Passwords do not match!');
  return;
}

// Depois
if (!token) {
  setError("Invalid token. Please request a new password reset link.");
  setLoading(false);
  return;
}
if (password.length < 6) {
  setError("Password must be at least 6 characters long.");
  setLoading(false);
  return;
}
if (password !== confirmPassword) {
  setError("Passwords do not match!");
  setLoading(false);
  return;
}
// Antes
if (redirect) {
  return <Navigate to="/login" />;
}

// Depois
if (redirect) {
  return (
    <div>
      <p>Password reset successfully! Redirecting to login...</p>
      <Navigate to="/login" />
    </div>
  );
}

// Antes
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');

// Depois
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState(""); // Adiciona estado para erros
const [loading, setLoading] = useState(false); // Adiciona estado para carregamento

O que mudou?

A lógica de validação foi significativamente aprimorada para incluir verificações adicionais, como token ausente ou inválido e validação de senha com um tamanho mínimo.

Foram adicionados estados de carregamento (loading) e erro (error) para melhorar o feedback ao usuário.

Além disso, a estrutura de redirecionamento foi atualizada para incluir uma mensagem de sucesso antes de redirecionar o usuário.

Melhoria no Processo de Registro e Tratamento de Erros

Mudanças no react-auth/src/pages/Register.tsx:

react-auth/src/pages/Register.tsx

// Antes
const response = await axios.post('http://localhost:3000/api/register', {
  first_name: firstName,
  last_name: lastName,
  email: email,
  password: password,
  confirm_password: confirmPassword,
})

// Depois
const response = await axios.post(`${apiUrl}/api/register`, {
  first_name: firstName,
  last_name: lastName,
  email: email,
  password: password,
  confirm_password: confirmPassword,
}, {
  headers: {
    'Content-Type': 'application/json',
  },
  withCredentials: true,
})

// Antes
console.log(response)

// Depois
console.log('Resposta do servidor:', {
  status: response.status,
  statusText: response.statusText,
  data: response.data,
})

// Antes
// Redirecionamento manual não utilizado

// Depois
if (response.status === 200 || response.status === 201) {
  console.log('Registro bem sucedido, redirecionando...')
  navigate('/login')
}

// Antes
catch (e) {
  console.error('Failed to register:', e)
}

// Depois
catch (error: any) {
  if (error.response) {
    const errorMsg = error.response.data.message || 'Registration failed'
    console.log('Mensagem de erro:', errorMsg)
    setError(errorMsg)
  } else if (error.request) {
    setError('No response from server')
  } else {
    setError('Error during registration')
  }

  if (process.env.REACT_APP_LOG_LEVEL === 'debug') {
    console.error('Registration error:', error)
  }
}

// Antes
return (
  <form onSubmit={submit}>
    ...
  </form>
)

// Depois
return (
  <form onSubmit={submit}>
    {error && (
      <div className="alert alert-danger" role="alert">
        {error}
      </div>
    )}
    ...
  </form>
)

O que mudou?

O código foi refatorado para utilizar uma URL de API configurável através de variáveis de ambiente, permitindo maior flexibilidade entre ambientes.

A lógica de redirecionamento foi simplificada com o uso do navigate.

Também foram adicionados estados de erro para informar ao usuário problemas durante o registro e melhorias no log detalhado das respostas da API para depuração.

Melhoria no Processo de Login e Gerenciamento de Token

Mudanças no react-auth/src/pages/Login.tsx:

react-auth/src/pages/Login.tsx


// Antes
const response = await axios.post('http://localhost:3000/api/login', {
  email,
  password,
});

if (response.status === 200) {
  setLogin(true);
  setRedirect(true);
}

// Depois
const apiUrl = getApiUrl();
const response = await axios.post(`${apiUrl}/api/login`, {
  email,
  password,
});

if (response.data?.jwt) {
  localStorage.setItem('jwt', response.data.jwt);
  axios.defaults.headers.common = {
    Authorization: `Bearer ${response.data.jwt}`,
    'Content-Type': 'application/json',
    Accept: 'application/json',
  };
  setLogin(true);
  setRedirect(true);
}

O que mudou?

O gerenciamento de tokens foi melhorado, com os tokens sendo armazenados no localStorage e configurados nos headers padrão do axios para autenticação em futuras requisições.

A lógica de resposta foi refinada para lidar com diferentes cenários de erro e para limpar os dados locais em caso de falha no login.

Melhoria no Processo de Recuperação de Senha e Feedback ao Usuário

Mudanças no react-auth/src/pages/Forgot.tsx:

react-auth/src/pages/Forgot.tsx

// Antes
await axios.post('forgot', { email });
setNotify({
  show: true,
  error: false,
  message: 'Email was sent!'
});

// Depois
const apiUrl = getApiUrl();
await axios.post(`${apiUrl}/api/forgot`, { email }, {
  headers: { 'Content-Type': 'application/json' },
  withCredentials: true
});
setNotify({
  show: true,
  error: false,
  message: 'Password reset instructions have been sent to your email'
});

// Antes
const [notify, setNotify] = useState({
  show: false,
  error: false,
  message: ''
});

// Depois
const [loading, setLoading] = useState(false);
const [notify, setNotify] = useState<NotifyState>({
  show: false,
  error: false,
  message: ''
});

O que mudou?

As mudanças feitas no arquivo react-auth/src/pages/Forgot.tsx focam na melhoria da experiência do usuário durante o processo de recuperação de senha.

A atualização aprimora o tratamento de erros, o feedback visual ao usuário, e adiciona suporte a configurações globais, como o uso de uma base URL dinâmica para a API e um estado de carregamento enquanto a operação está em progresso.

Aviso no Render

Obs. Sua instância gratuita ficará inativa devido à inatividade, o que pode atrasar as solicitações em 50 segundos ou mais.

Health Check

O Health Check foi implementado para lidar com essa característica do plano gratuito do Render.com, onde o serviço “dorme” após períodos de inatividade, causando atrasos significativos (50 segundos ou mais) na primeira requisição após o período inativo.

src/utils/healthCheck.ts

const checkServiceHealth = async () => {
    try {
        const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080';
        const response = await fetch(`${apiUrl}/api/health`);
        console.log('Health check status:', response.status);
    } catch (error) {
        console.error('Health check failed:', error);
    }
};

export const startHealthCheck = () => {
    // Primeira verificação imediata
    checkServiceHealth();
    
    // Depois a cada 10 minutos
    setInterval(checkServiceHealth, 10 * 60 * 1000);
};

 

Outras Mudanças Necessarias Para o Deploy

Precisamos criar o public/_redirects.

O arquivo public/_redirects é uma abordagem comum em aplicações React para lidar com o roteamento no lado do cliente.

Criamos este arquivo porque, quando implantamos uma Single Page Application (SPA), todas as rotas precisam ser redirecionadas para o index.html, permitindo que o React Router gerencie a navegação internamente.

No nosso caso, configuramos com /* /index.html 200, indicando que qualquer rota deve servir o index.html com status 200.

Isso é especialmente importante para funcionalidades como o reset de senha, onde o usuário acessa URLs diretas como /rbeset/[token].

Sem esta configuração, o servidor tentaria encontrar um arquivo físico correspondente a essa rota, resultando em erro 404.

public/_redirects

/*    /index.html   200

Redirect and Rewrite Rules No render.com

No render.com, configuramos as Redirect and Rewrite Rules de forma similar, mas através da interface gráfica da plataforma.

Definimos a regra: /*.

Ela redireciona para o index.html, garantindo que o React Router possa gerenciar essa rotas adequadamente.

A diferença é que estas regras são processadas no nível do servidor do render.com, antes mesmo do request chegar à nossa aplicação.

Embora pareça redundante ter ambas as configurações, isso nos dá uma camada extra de segurança: se uma falhar, a outra serve como backup.

Além disso, enquanto o _redirects é processado durante o build da aplicação, as regras do render.com são processadas em tempo de execução, oferecendo mais flexibilidade para ajustes sem necessidade de novo deploy.

Esta dupla abordagem se mostra especialmente útil no nosso sistema de reset de senha, onde precisamos garantir que o token chegue corretamente ao frontend, independentemente de como o usuário acessa a URL.

O _redirects garante a funcionalidade básica, enquanto as regras do render.com nos dão controle adicional sobre como os redirecionamentos são tratados em produção.

O uso do arquivo public/_redirects junto com as Redirect and Rewrite Rules do Render não é apenas uma questão de redundância técnica, mas também uma estratégia inteligente para portabilidade.

Ao mantermos o arquivo _redirects em nosso código, estamos essencialmente documentando e preservando nossas regras de redirecionamento de forma independente da plataforma.

Isso significa que, se no futuro precisarmos migrar nossa aplicação para outro serviço de hospedagem, como Netlify, Vercel ou até mesmo um ambiente Kubernetes próprio, grande parte da configuração de rotas já estará pronta e versionada no código.

Esta portabilidade é especialmente valiosa em um cenário empresarial, onde mudanças de infraestrutura são comuns.

Por exemplo, o arquivo _redirects é nativamente suportado pelo Netlify e mesmo em plataformas que não o suportam diretamente, ele serve como documentação clara de como as rotas devem ser configuradas.

Enquanto isso, as regras configuradas no painel do Render nos garantem o funcionamento imediato na nossa infraestrutura atual.

Esta abordagem híbrida reflete uma boa prática de DevOps: nunca ficar totalmente dependente de uma única plataforma ou solução.

Mantendo nossas configurações de roteamento tanto no código (_redirects) quanto na plataforma (render.com), garantimos flexibilidade para mudanças futuras com mínimo esforço de reconfiguração.

Deploy no render.com

Primeiro passo é criar o banco de dados!

Preparação do Banco de Dados

  1. Crie uma conta no FreedDB
  2. Crie um novo banco de dados MySQL
  3. Anote as credenciais fornecidas
  4. Monte a string de conexão:
    usuario:senha@tcp(sql.freedb.tech:3306)/nome_banco?charset=utf8mb4&parseTime=True

Segundo passo, crie a senha de app gmail!

Configuração do Gmail

  1. Acesse Configurações do Google
  2. Ative a verificação em duas etapas
  3. Gere uma senha de aplicativo
    • Selecione “App: Email”
    • Escolha “Outro (Nome personalizado)”
    • Digite um nome (ex: “Auth System”)
    • Copie a senha gerada

Deploy do Backend

  1. Crie uma conta no render.com
  2. Clique em “New +” e selecione “Web Service
  3. Conecte com seu repositório GitHub
  4. Configure o serviço:
    • Name: auth-backend (ou nome de sua preferência)
    • Runtime: Go
    • Build Command: cd fiber-project && go build -o ../main .
    • Start Command: ./main
  5. Configure as variáveis de ambiente:
    
    ENV=production
    PORT=10000
    DB_DSN=sua-string-de-conexao-do-freedb
    JWT_SECRET=seu-segredo-seguro
    GMAIL_EMAIL=seu-email@gmail.com
    GMAIL_APP_PASSWORD=senha-do-app-gerada
    APP_URL=https://seu-app.onrender.com
  6. Clique em “Create Web Service

Deploy do Frontend

  1. No Render, clique em “New +” e selecione “Static Site
  2. Conecte com seu repositório do frontend
  3. Configure:
    • Name: auth-frontend (ou nome de sua preferência)
    • Build Command: npm install && npm run build
    • Publish Directory: build
  4. Configure as variáveis de ambiente:
    
    REACT_APP_API_URL=https://seu-backend.onrender.com
    
  5. Clique em “Create Static Site”

Build Command:

npm install --legacy-peer-deps && npm run build

Root DirectoryOptional: em branco

Publish Directory: build

Downgrade typescript 5.3.3 para 4.9.5

Build Command:

npm install typescript@4.9.5 --legacy-peer-deps && npm install --legacy-peer-deps && npm run build

Verificação

Após o deploy, teste:

  • Registro de usuários
  • Login
  • Recuperação de senha
  • Recebimento de emails

Encerramento

Assim concluímos nossa jornada de desenvolvimento e deploy de um sistema completo de autenticação usando Go/Fiber no backend e React/TypeScript no frontend!

Neste tutorial, você aprendeu:

  • Como estruturar um projeto profissional
  • Implementar autenticação segura
  • Configurar ambientes de desenvolvimento e produção
  • Fazer deploy gratuito no Render.com
  • Usar banco de dados gratuito (FreedDB)
  • Configurar envio de emails com Gmail
  • Seguir boas práticas de desenvolvimento

Este projeto serve como base para suas próprias aplicações, podendo ser expandido com:

  • Mais funcionalidades de segurança
  • Diferentes provedores de email
  • Melhorias na interface do usuário
  • Refatoração para melhor organização do código

Nos vemos em breve em outros tutoriais!

Valeu, 😉 \o/

About The Author
-

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>