Skip to content

Commit ced5a09

Browse files
authored
Merge pull request #2797 from SadikSunbul/master
Added Email Verification
2 parents 4922969 + 67eba52 commit ced5a09

File tree

13 files changed

+455
-0
lines changed

13 files changed

+455
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Here you can find the most **delicious** recipes to cook delicious meals using o
3131
- [Docker + MariaDB](./docker-mariadb-clean-arch/README.md) - Dockerized MariaDB with Clean Architecture.
3232
- [Docker + Nginx](./docker-nginx-loadbalancer/README.md) - Load balancing with Docker and Nginx.
3333
- [Dummy JSON Proxy](./dummyjson/README.md) - Proxying dummy JSON data.
34+
- [Email Verification](./email-verification/README.md) - Email verification service with code generation and validation.
3435
- [Entgo ORM (MySQL)](./ent-mysql/README.md) - Using Entgo ORM with MySQL
3536
- [Entgo Sveltekit](./entgo-sveltekit/README.md) - A full-stack Todo application built using Sveltekit, Tailwind CSS, Entgo, and SQLite.
3637
- [Envoy External Authorization](./envoy-extauthz/README.md) - External authorization with Envoy.

email-verification/README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
title: Email Verification Service
3+
keywords: [email, verification, smtp, golang, fiber]
4+
description: Email verification service with code generation and validation
5+
---
6+
7+
# Email Verification Service with Fiber
8+
9+
[![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)
10+
11+
A clean architecture based email verification service that generates and validates verification codes.
12+
13+
## Features
14+
15+
- Clean Architecture implementation
16+
- In-memory verification code storage
17+
- SMTP email service integration
18+
- Code generation and hashing
19+
- Configurable code expiration
20+
- Thread-safe operations
21+
22+
## Project Structure
23+
24+
```
25+
email-verification/
26+
├── api/
27+
│ └── handlers/ # HTTP handlers
28+
├── application/ # Application business logic
29+
├── domain/ # Domain models and interfaces
30+
├── infrastructure/ # External implementations
31+
│ ├── code/ # Code generation
32+
│ ├── email/ # SMTP service
33+
│ └── repository/ # Data storage
34+
└── config/ # Configuration
35+
```
36+
37+
## Configuration
38+
39+
Update `config/config.go` with your SMTP settings:
40+
41+
```go
42+
func GetConfig() *Config {
43+
return &Config{
44+
SMTPHost: "smtp.gmail.com",
45+
SMTPPort: 587,
46+
SMTPUser: "[email protected]",
47+
SMTPPassword: "your-app-password",
48+
CodeExpiration: time.Minute * 1,
49+
}
50+
}
51+
```
52+
53+
## API Endpoints
54+
55+
| Method | URL | Description |
56+
|--------|----------------------------|--------------------------------|
57+
| POST | /verify/send/:email | Send verification code |
58+
| POST | /verify/check/:email/:code | Verify the received code |
59+
60+
## Example Usage
61+
62+
1. Send verification code:
63+
```bash
64+
curl -X POST http://localhost:3000/verify/send/[email protected]
65+
```
66+
67+
2. Verify code:
68+
```bash
69+
curl -X POST http://localhost:3000/verify/check/[email protected]/123456
70+
```
71+
72+
## Response Examples
73+
74+
Success:
75+
```json
76+
{
77+
"message": "Code verified successfully"
78+
}
79+
```
80+
81+
Error:
82+
```json
83+
{
84+
"error": "invalid code"
85+
}
86+
```
87+
88+
## How to Run
89+
90+
1. Configure SMTP settings in `config/config.go`
91+
2. Run the application:
92+
```bash
93+
go run main.go
94+
```
95+
96+
## Dependencies
97+
98+
- [Fiber v2](https://github.com/gofiber/fiber)
99+
- Go 1.21+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package handlers
2+
3+
import (
4+
"email-verification/application"
5+
6+
"github.com/gofiber/fiber/v2"
7+
)
8+
9+
type VerificationHandler struct {
10+
verificationService *application.VerificationService
11+
}
12+
13+
func NewVerificationHandler(service *application.VerificationService) *VerificationHandler {
14+
return &VerificationHandler{verificationService: service}
15+
}
16+
17+
func (h *VerificationHandler) SendVerification(c *fiber.Ctx) error {
18+
email := c.Params("email")
19+
if err := h.verificationService.SendVerification(email); err != nil {
20+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
21+
"error": err.Error(),
22+
})
23+
}
24+
return c.JSON(fiber.Map{"message": "Verification code sent"})
25+
}
26+
27+
func (h *VerificationHandler) CheckVerification(c *fiber.Ctx) error {
28+
email := c.Params("email")
29+
code := c.Params("code")
30+
31+
if err := h.verificationService.VerifyCode(email, code); err != nil {
32+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
33+
"error": err.Error(),
34+
})
35+
}
36+
37+
return c.JSON(fiber.Map{"message": "Code verified successfully"})
38+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package application
2+
3+
import (
4+
"email-verification/config"
5+
"email-verification/domain"
6+
"fmt"
7+
"time"
8+
)
9+
10+
type VerificationService struct {
11+
repo domain.VerificationRepository
12+
emailService domain.EmailService
13+
codeGen domain.CodeGenerator
14+
codeExpiration time.Duration
15+
}
16+
17+
func NewVerificationService(
18+
repo domain.VerificationRepository,
19+
emailService domain.EmailService,
20+
codeGen domain.CodeGenerator,
21+
config *config.Config,
22+
) *VerificationService {
23+
return &VerificationService{
24+
repo: repo,
25+
emailService: emailService,
26+
codeGen: codeGen,
27+
codeExpiration: config.CodeExpiration,
28+
}
29+
}
30+
31+
func (s *VerificationService) SendVerification(email string) error {
32+
if email == "" {
33+
return fmt.Errorf("email cannot be empty")
34+
}
35+
36+
if _, err := s.repo.Get(email); err == nil {
37+
return fmt.Errorf("verification already pending")
38+
}
39+
40+
code, err := s.codeGen.Generate()
41+
if err != nil {
42+
return err
43+
}
44+
45+
if err := s.emailService.SendVerificationCode(email, code); err != nil {
46+
return err
47+
}
48+
49+
verification := domain.Verification{
50+
Code: s.codeGen.Hash(code),
51+
Exp: time.Now().Add(s.codeExpiration),
52+
}
53+
54+
return s.repo.Store(email, verification)
55+
}
56+
57+
func (s *VerificationService) VerifyCode(email, code string) error {
58+
if email == "" || code == "" {
59+
return fmt.Errorf("email and code cannot be empty")
60+
}
61+
62+
verification, err := s.repo.Get(email)
63+
if err != nil {
64+
return err
65+
}
66+
67+
hashedCode := s.codeGen.Hash(code)
68+
if verification.Code != hashedCode {
69+
return fmt.Errorf("invalid code")
70+
}
71+
72+
if time.Now().After(verification.Exp) {
73+
if err := s.repo.Delete(email); err != nil {
74+
return fmt.Errorf("failed to delete expired code: %w", err)
75+
}
76+
return fmt.Errorf("code expired")
77+
}
78+
79+
return s.repo.Delete(email)
80+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package config
2+
3+
import (
4+
"time"
5+
)
6+
7+
type Config struct {
8+
SMTPHost string
9+
SMTPPort int
10+
SMTPUser string
11+
SMTPPassword string
12+
CodeExpiration time.Duration
13+
}
14+
15+
func GetConfig() *Config {
16+
return &Config{
17+
SMTPHost: "smtp.gmail.com",
18+
SMTPPort: 587,
19+
SMTPUser: "[email protected]",
20+
SMTPPassword: "bakkcmkakpfxwuef",
21+
CodeExpiration: time.Minute * 1,
22+
}
23+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package domain
2+
3+
type CodeGenerator interface {
4+
Generate() (string, error)
5+
Hash(code string) string
6+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package domain
2+
3+
import "time"
4+
5+
type Verification struct {
6+
Code string
7+
Exp time.Time
8+
}
9+
10+
type VerificationRepository interface {
11+
Store(email string, verification Verification) error
12+
Get(email string) (Verification, error)
13+
Delete(email string) error
14+
}
15+
16+
type EmailService interface {
17+
SendVerificationCode(to string, code string) error
18+
}

email-verification/go.mod

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module email-verification
2+
3+
go 1.21
4+
5+
require (
6+
github.com/gofiber/fiber/v2 v2.52.6
7+
golang.org/x/crypto v0.32.0
8+
)
9+
10+
require (
11+
github.com/andybalholm/brotli v1.1.0 // indirect
12+
github.com/google/uuid v1.6.0 // indirect
13+
github.com/klauspost/compress v1.17.9 // indirect
14+
github.com/mattn/go-colorable v0.1.13 // indirect
15+
github.com/mattn/go-isatty v0.0.20 // indirect
16+
github.com/mattn/go-runewidth v0.0.16 // indirect
17+
github.com/rivo/uniseg v0.2.0 // indirect
18+
github.com/valyala/bytebufferpool v1.0.0 // indirect
19+
github.com/valyala/fasthttp v1.51.0 // indirect
20+
github.com/valyala/tcplisten v1.0.0 // indirect
21+
golang.org/x/sys v0.29.0 // indirect
22+
)

email-verification/go.sum

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
2+
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
3+
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
4+
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
5+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
6+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7+
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
8+
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
9+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
10+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
11+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
12+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
13+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
14+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
15+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
16+
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
17+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
18+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
19+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
20+
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
21+
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
22+
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
23+
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
24+
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
25+
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
26+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
27+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
29+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package code
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/hex"
6+
"fmt"
7+
8+
"golang.org/x/crypto/sha3"
9+
)
10+
11+
type DefaultCodeGenerator struct{}
12+
13+
func NewCodeGenerator() *DefaultCodeGenerator {
14+
return &DefaultCodeGenerator{}
15+
}
16+
17+
func (g *DefaultCodeGenerator) Generate() (string, error) {
18+
b := make([]byte, 3)
19+
if _, err := rand.Read(b); err != nil {
20+
return "", err
21+
}
22+
return fmt.Sprintf("%06x", b), nil
23+
}
24+
25+
func (g *DefaultCodeGenerator) Hash(code string) string {
26+
hash := sha3.New256()
27+
hash.Write([]byte(code))
28+
return hex.EncodeToString(hash.Sum(nil))
29+
}

0 commit comments

Comments
 (0)