How does a new user gets created ?
Client
|
| POST /api/v1/signup
| Body: email, password
v
Gin Router
|
v
UserHandler (HTTP Layer)
|
|-- Bind & Validate Request (DTO + custom validator)
|
v
UserService (Business Logic)
|
|-- Hash Password
|-- Check if user exists
|
v
UserRepository (Data Access)
|
|-- INSERT INTO users
|
v
PostgreSQL
How does a user get authenticated and receive a token ?
Client
|
| POST /api/v1/signin
| Body: email, password
v
Gin Router
|
v
UserHandler
|
|-- Bind & Validate Request
|
v
UserService
|
|-- Fetch user by email
|-- Compare password hash
|
|-- JWTManager.GenerateToken(userID)
|
v
JWTManager
|
|-- Create Claims
|-- Sign token
|
v
Response
|
|-- JWT returned to client
What happens after signin ?
Client
|
| Authorization: Bearer <JWT>
v
RateLimitMiddleware
|
v
AuthMiddleware
|
|-- Validate JWT
|-- Extract user_id
|-- Inject into context
|
v
Protected Handler
|
|-- contextutil.GetUserID()
|
v
Service / Repository
api-go
|
|-------> cmd/server
| |------> main.go
|
|
|-------> internal
| |
| |------> app
| | |-----> app.go
| |
| |------> auth
| | |-----> jwt.go
| |
| |
| |------> db
| | |-----> postgres.go
| |
| |------> domain
| | |-----> errors.go
| | |
| | |-----> user.go
| | |
| | |-----> website.go
| |
| |------> dto
| | |-----> websites.go
| | |
| | |-----> auth.go // added
| |
| |
| |------> handlers
| | |-----> websites.go
| | |
| | |-----> users.go // added
| |
| |
| |------> http
| | |-----> apperror
| | | |-----------> mapper.go
| | |
| | |-----> context
| | | |-----------> context.go
| | |
| | |-----> middleware
| | | |-----------> auth.go
| | | |
| | | |-----------> rateLimit.go
| | |
| | |
| | |-----> response
| | |-----------> response.go
| |
| |
| |------> repository
| | |-----> website_repository_pg.go
| | |-----> website_repository.go
| | |-----> user_repository.go // added
| | |-----> user_repository_pg.go // added
| |
| |
| |------> routes
| | |-----> websites.go
| | |
| | |-----> health.go
| | |
| | |-----> router.go
| | |
| | |-----> user.go // added
| |
| |
| |------> service
| | |-----> websites.go
| | |
| | |-----> users.go // added
| |
| |
| |------> validation
| |-----> password.go
|
|------> migrations
| |------> 20251226095007_create_websites_table.down.sql
| |------> 20251226095007_create_websites_table.up.sql
|
|------> .env
Custom validators live in a validation package and must be registered once during app startup before routes run.
internal/validation/password.go
package validation
import (
"unicode"
"github.com/go-playground/validator/v10"
)
/*
RegisterPasswordValidator func :
- Receives Gin's validator engine
- v is the object that stores all validation rules.
- You call this func once at app startup
*/
func RegisterPasswordValidator(v *validator.Validate) {
/*
- v.RegisterValidation("password" : Registers a new rule named password
*/
v.RegisterValidation("password", func(fl validator.FieldLevel) bool {
/*
- fl : current field being validated
- Field() : Reflect value of that field
- .String() :get actual string value
*/
password := fl.Field().String()
if len(password) < 8 {
return false
}
var hasUpper, hasLower, hasSpecial, hasNumber bool
for _, ch := range password {
switch {
case unicode.IsUpper(ch):
hasUpper = true
case unicode.IsLower(ch):
hasLower = true
case unicode.IsDigit(ch):
hasNumber = true
case unicode.IsPunct(ch) || unicode.IsSymbol(ch):
hasSpecial = true
}
}
return hasUpper && hasLower && hasNumber && hasSpecial
})
}
internal/app/app.go
package app
import (
"os"
"time"
"github.com/RitikaxG/runState/apps/api-go/internal/auth"
"github.com/RitikaxG/runState/apps/api-go/internal/db"
"github.com/RitikaxG/runState/apps/api-go/internal/handlers"
"github.com/RitikaxG/runState/apps/api-go/internal/http/middleware"
"github.com/RitikaxG/runState/apps/api-go/internal/repository"
"github.com/RitikaxG/runState/apps/api-go/internal/routes"
"github.com/RitikaxG/runState/apps/api-go/internal/service"
"github.com/RitikaxG/runState/apps/api-go/internal/validation"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
func BuildServer() *gin.Engine {
r := gin.Default()
dbConn := db.NewPostgres(os.Getenv("DATABASE_URL"))
// 1. Initialise JWT Manager
jwtManager := auth.NewJWTManager(
os.Getenv("JWT_SECRET"),
24*time.Hour,
)
// Start cleanup goroutine once
middleware.StartRateLimiterCleanup()
// Apply rate limiting to all requests
r.Use(middleware.RateLimitMiddleware())
websiteRepo := repository.NewWebsiteRepository(dbConn)
websiteService := service.NewWebsiteService(websiteRepo)
websiteHandler := handlers.NewWebsiteHandler(websiteService)
routes.RegisterRouter(r, websiteHandler, jwtManager)
/*
Custom validators live in a validation package and must be registered once during app startup before routes run.
It gets Gin’s internal validator engine,
safely casts it to *validator.Validate, and registers your custom password rule.
- binding.Validator : Gin's global validator instance
- binding.Validator.Engine() : returns actual underlying validator, its return type is interface
- .(*validator.Validate) → type assertion
Is this engine actually a *validator.Validate ?
Since it returned an interface this type check is imp)
- ok : true if assertion succeeded otherwise false
*/
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
validation.RegisterPasswordValidator(v)
}
return r
}