Skip to content

Commit

Permalink
Merge pull request #2797 from SadikSunbul/master
Browse files Browse the repository at this point in the history
Added Email Verification
  • Loading branch information
ReneWerner87 authored Feb 10, 2025
2 parents 4922969 + 67eba52 commit ced5a09
Show file tree
Hide file tree
Showing 13 changed files with 455 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Here you can find the most **delicious** recipes to cook delicious meals using o
- [Docker + MariaDB](./docker-mariadb-clean-arch/README.md) - Dockerized MariaDB with Clean Architecture.
- [Docker + Nginx](./docker-nginx-loadbalancer/README.md) - Load balancing with Docker and Nginx.
- [Dummy JSON Proxy](./dummyjson/README.md) - Proxying dummy JSON data.
- [Email Verification](./email-verification/README.md) - Email verification service with code generation and validation.
- [Entgo ORM (MySQL)](./ent-mysql/README.md) - Using Entgo ORM with MySQL
- [Entgo Sveltekit](./entgo-sveltekit/README.md) - A full-stack Todo application built using Sveltekit, Tailwind CSS, Entgo, and SQLite.
- [Envoy External Authorization](./envoy-extauthz/README.md) - External authorization with Envoy.
Expand Down
99 changes: 99 additions & 0 deletions email-verification/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: Email Verification Service
keywords: [email, verification, smtp, golang, fiber]
description: Email verification service with code generation and validation
---

# Email Verification Service with Fiber

[![Github](https://img.shields.io/static/v1?label=&message=Github&color=2ea44f&style=for-the-badge&logo=github)](https://github.com/gofiber/recipes/tree/master/email-verification) [![StackBlitz](https://img.shields.io/static/v1?label=&message=StackBlitz&color=2ea44f&style=for-the-badge&logo=StackBlitz)](https://stackblitz.com/github/gofiber/recipes/tree/master/email-verification)

A clean architecture based email verification service that generates and validates verification codes.

## Features

- Clean Architecture implementation
- In-memory verification code storage
- SMTP email service integration
- Code generation and hashing
- Configurable code expiration
- Thread-safe operations

## Project Structure

```
email-verification/
├── api/
│ └── handlers/ # HTTP handlers
├── application/ # Application business logic
├── domain/ # Domain models and interfaces
├── infrastructure/ # External implementations
│ ├── code/ # Code generation
│ ├── email/ # SMTP service
│ └── repository/ # Data storage
└── config/ # Configuration
```

## Configuration

Update `config/config.go` with your SMTP settings:

```go
func GetConfig() *Config {
return &Config{
SMTPHost: "smtp.gmail.com",
SMTPPort: 587,
SMTPUser: "[email protected]",
SMTPPassword: "your-app-password",
CodeExpiration: time.Minute * 1,
}
}
```

## API Endpoints

| Method | URL | Description |
|--------|----------------------------|--------------------------------|
| POST | /verify/send/:email | Send verification code |
| POST | /verify/check/:email/:code | Verify the received code |

## Example Usage

1. Send verification code:
```bash
curl -X POST http://localhost:3000/verify/send/[email protected]
```

2. Verify code:
```bash
curl -X POST http://localhost:3000/verify/check/[email protected]/123456
```

## Response Examples

Success:
```json
{
"message": "Code verified successfully"
}
```

Error:
```json
{
"error": "invalid code"
}
```

## How to Run

1. Configure SMTP settings in `config/config.go`
2. Run the application:
```bash
go run main.go
```

## Dependencies

- [Fiber v2](https://github.com/gofiber/fiber)
- Go 1.21+
38 changes: 38 additions & 0 deletions email-verification/api/handlers/verification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package handlers

import (
"email-verification/application"

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

type VerificationHandler struct {
verificationService *application.VerificationService
}

func NewVerificationHandler(service *application.VerificationService) *VerificationHandler {
return &VerificationHandler{verificationService: service}
}

func (h *VerificationHandler) SendVerification(c *fiber.Ctx) error {
email := c.Params("email")
if err := h.verificationService.SendVerification(email); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.JSON(fiber.Map{"message": "Verification code sent"})
}

func (h *VerificationHandler) CheckVerification(c *fiber.Ctx) error {
email := c.Params("email")
code := c.Params("code")

if err := h.verificationService.VerifyCode(email, code); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}

return c.JSON(fiber.Map{"message": "Code verified successfully"})
}
80 changes: 80 additions & 0 deletions email-verification/application/verification_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package application

import (
"email-verification/config"
"email-verification/domain"
"fmt"
"time"
)

type VerificationService struct {
repo domain.VerificationRepository
emailService domain.EmailService
codeGen domain.CodeGenerator
codeExpiration time.Duration
}

func NewVerificationService(
repo domain.VerificationRepository,
emailService domain.EmailService,
codeGen domain.CodeGenerator,
config *config.Config,
) *VerificationService {
return &VerificationService{
repo: repo,
emailService: emailService,
codeGen: codeGen,
codeExpiration: config.CodeExpiration,
}
}

func (s *VerificationService) SendVerification(email string) error {
if email == "" {
return fmt.Errorf("email cannot be empty")
}

if _, err := s.repo.Get(email); err == nil {
return fmt.Errorf("verification already pending")
}

code, err := s.codeGen.Generate()
if err != nil {
return err
}

if err := s.emailService.SendVerificationCode(email, code); err != nil {
return err
}

verification := domain.Verification{
Code: s.codeGen.Hash(code),
Exp: time.Now().Add(s.codeExpiration),
}

return s.repo.Store(email, verification)
}

func (s *VerificationService) VerifyCode(email, code string) error {
if email == "" || code == "" {
return fmt.Errorf("email and code cannot be empty")
}

verification, err := s.repo.Get(email)
if err != nil {
return err
}

hashedCode := s.codeGen.Hash(code)
if verification.Code != hashedCode {
return fmt.Errorf("invalid code")
}

if time.Now().After(verification.Exp) {
if err := s.repo.Delete(email); err != nil {
return fmt.Errorf("failed to delete expired code: %w", err)
}
return fmt.Errorf("code expired")
}

return s.repo.Delete(email)
}
23 changes: 23 additions & 0 deletions email-verification/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import (
"time"
)

type Config struct {
SMTPHost string
SMTPPort int
SMTPUser string
SMTPPassword string
CodeExpiration time.Duration
}

func GetConfig() *Config {
return &Config{
SMTPHost: "smtp.gmail.com",
SMTPPort: 587,
SMTPUser: "[email protected]",
SMTPPassword: "bakkcmkakpfxwuef",
CodeExpiration: time.Minute * 1,
}
}
6 changes: 6 additions & 0 deletions email-verification/domain/code_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package domain

type CodeGenerator interface {
Generate() (string, error)
Hash(code string) string
}
18 changes: 18 additions & 0 deletions email-verification/domain/verification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package domain

import "time"

type Verification struct {
Code string
Exp time.Time
}

type VerificationRepository interface {
Store(email string, verification Verification) error
Get(email string) (Verification, error)
Delete(email string) error
}

type EmailService interface {
SendVerificationCode(to string, code string) error
}
22 changes: 22 additions & 0 deletions email-verification/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module email-verification

go 1.21

require (
github.com/gofiber/fiber/v2 v2.52.6
golang.org/x/crypto v0.32.0
)

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.29.0 // indirect
)
29 changes: 29 additions & 0 deletions email-verification/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
29 changes: 29 additions & 0 deletions email-verification/infrastructure/code/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package code

import (
"crypto/rand"
"encoding/hex"
"fmt"

"golang.org/x/crypto/sha3"
)

type DefaultCodeGenerator struct{}

func NewCodeGenerator() *DefaultCodeGenerator {
return &DefaultCodeGenerator{}
}

func (g *DefaultCodeGenerator) Generate() (string, error) {
b := make([]byte, 3)
if _, err := rand.Read(b); err != nil {
return "", err
}
return fmt.Sprintf("%06x", b), nil
}

func (g *DefaultCodeGenerator) Hash(code string) string {
hash := sha3.New256()
hash.Write([]byte(code))
return hex.EncodeToString(hash.Sum(nil))
}
35 changes: 35 additions & 0 deletions email-verification/infrastructure/email/smtp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package email

import (
"email-verification/config"
"fmt"
"net/smtp"
)

type SMTPService struct {
config *config.Config
}

func NewSMTPService(config *config.Config) *SMTPService {
return &SMTPService{config: config}
}

func (s *SMTPService) SendVerificationCode(to string, code string) error {
subject := "Subject: Email Verification Code \n"
body := fmt.Sprintf("Your verification code is %s", code)
message := []byte(subject + "\n" + body)

auth := smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPassword, s.config.SMTPHost)
err := smtp.SendMail(
fmt.Sprintf("%s:%d", s.config.SMTPHost, s.config.SMTPPort),
auth,
s.config.SMTPUser,
[]string{to},
message,
)
if err != nil {
return err
}

return nil
}
Loading

0 comments on commit ced5a09

Please sign in to comment.