Skip to content

Commit

Permalink
feat(server): add redis db and enable rate limiting for auth routes
Browse files Browse the repository at this point in the history
  • Loading branch information
jramsgz committed Apr 24, 2024
1 parent eceed45 commit be01858
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 47 deletions.
23 changes: 21 additions & 2 deletions config/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ DB_PORT=5432
# For sqlite, this is the path to the database file
DB_DATABASE=config/articpad.db

# Redis settings
# Configuring Redis is optional but highly recommended, if not configured, the application will use an in-memory store
# However, this will not work as expected in a multi-instance setup or even in a single instance setup if preforking is enabled (DEBUG=false)
# REDIS_HOST sets the Redis server host
REDIS_HOST=localhost
# REDIS_PORT sets the Redis server port
REDIS_PORT=6379
# REDIS_USERNAME sets the Redis server username
REDIS_USERNAME=
# REDIS_PASSWORD sets the Redis server password
REDIS_PASSWORD=
# REDIS_DB sets the Redis database number
REDIS_DB=0

# Mailer settings
# MAIL_HOST sets the SMTP server host
MAIL_HOST=localhost
Expand All @@ -22,15 +36,15 @@ MAIL_USERNAME=
# MAIL_PASSWORD sets the SMTP server password
MAIL_PASSWORD=
# MAIL_FROM sets the from address for emails
MAIL_FROM=ArticPad <MAIL_USERNAME>
MAIL_FROM=ArticPad
# MAIL_FORCE_TLS sets whether to force TLS or not
# By default (false), TLS is used if the server supports it but is not enforced
MAIL_FORCE_TLS=false
# ENABLE_MAIL sets whether to enable sending emails or not
# If set to false, mail verification and password reset will be disabled
ENABLE_MAIL=false

# DEBUG sets isProduction to false, it enables sending error messages
# DEBUG sets whether the app is running in production or development mode, it enables sending internal error messages
# for HTTP requests to the client and disables preforking
DEBUG=false
# LOG_LEVEL sets the log level for the application
Expand All @@ -54,3 +68,8 @@ TRUSTED_PROXIES=
TEMPLATES_DIR=templates
# LOCALES_DIR sets the directory where the language files are located
LOCALES_DIR=locales
# By default, the aplication will rate limit auth requests to 40 requests per minute per IP
# You can enable or disable this feature by setting the RATE_LIMIT_AUTH environment variable
# It is recommended to enable this feature or set a rate limit at the reverse proxy level
# (If enabled and Redis is not configured the rate limiting behavior may not work as expected)
RATE_LIMIT_AUTH=true
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ var defaults = map[string]string{
"DB_PASSWORD": "",
"DB_PORT": "5432",
"DB_DATABASE": "config/articpad.db",
"REDIS_HOST": "localhost",
"REDIS_PORT": "6379",
"REDIS_USERNAME": "",
"REDIS_PASSWORD": "",
"REDIS_DB": "0",
"MAIL_HOST": "localhost",
"MAIL_PORT": "25",
"MAIL_USER": "",
Expand All @@ -42,6 +47,7 @@ var defaults = map[string]string{
"TRUSTED_PROXIES": "",
"TEMPLATES_DIR": "templates",
"LOCALES_DIR": "locales",
"RATE_LIMIT_AUTH": "true",
}

// LoadEnv loads the .env file, this should be called before using GetString or GetInt functions
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/glebarez/sqlite v1.10.0
github.com/gofiber/fiber/v2 v2.52.1
github.com/gofiber/jwt/v3 v3.3.10
github.com/gofiber/storage/redis/v3 v3.1.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
Expand All @@ -19,6 +20,8 @@ require (

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
Expand All @@ -33,6 +36,7 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/redis/go-redis/v9 v9.3.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/tinylib/msgp v1.1.9 // indirect
Expand Down
12 changes: 11 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
Expand All @@ -16,6 +22,8 @@ github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlg
github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/jwt/v3 v3.3.10 h1:0bpWtFKaGepjwYTU4efHfy0o+matSqZwTxGMo5a+uuc=
github.com/gofiber/jwt/v3 v3.3.10/go.mod h1:GJorFVaDyfMPSK9RB8RG4NQ3s1oXKTmYaoL/ny08O1A=
github.com/gofiber/storage/redis/v3 v3.1.0 h1:URly7BB1TVz15vPy2R1Q6vBqI53cDiD6p5qKZX/hpog=
github.com/gofiber/storage/redis/v3 v3.1.0/go.mod h1:HQ2wqleiIwb0Fbssq2T3v5DBiNlDZsCihWOuVmguIbg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
Expand Down Expand Up @@ -57,6 +65,8 @@ github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
Expand All @@ -71,7 +81,7 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJ
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
Expand Down
6 changes: 3 additions & 3 deletions internal/auth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (h *AuthHandler) signInUser(c *fiber.Ctx) error {
}
}

signedToken, err := h.newJWTToken(user.ID.String(), user.Username, c.IP())
signedToken, err := newJWTToken(user.ID.String(), user.Username, c.IP())
if err != nil {
return apierror.NewApiError(fiber.StatusInternalServerError, consts.ErrCodeUnknown, err.Error())
}
Expand Down Expand Up @@ -166,7 +166,7 @@ func (h *AuthHandler) refreshToken(c *fiber.Ctx) error {
jwtData := c.Locals("user").(*jwt.Token)
claims := jwtData.Claims.(jwt.MapClaims)

signedToken, err := h.newJWTToken(claims["uid"].(string), claims["user"].(string), claims["user_ip"].(string))
signedToken, err := newJWTToken(claims["uid"].(string), claims["user"].(string), claims["user_ip"].(string))
if err != nil {
return apierror.NewApiError(fiber.StatusInternalServerError, consts.ErrCodeUnknown, err.Error())
}
Expand Down Expand Up @@ -377,7 +377,7 @@ func (h *AuthHandler) getLangCode(c *fiber.Ctx) string {
return h.i18n.ParseLanguage(c.Get("Accept-Language"))
}

func (h *AuthHandler) newJWTToken(userId string, username string, userIP string) (string, error) {
func newJWTToken(userId string, username string, userIP string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwtClaims{
userId,
username,
Expand Down
49 changes: 25 additions & 24 deletions internal/infrastructure/fiber.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package infrastructure

import (
"strings"
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
Expand All @@ -14,21 +15,17 @@ import (
"github.com/jramsgz/articpad/internal/logging"
"github.com/jramsgz/articpad/internal/misc"
"github.com/jramsgz/articpad/internal/user"
"github.com/jramsgz/articpad/pkg/i18n"
"github.com/jramsgz/articpad/pkg/mail"
"github.com/rs/zerolog"
"gorm.io/gorm"
)

// startFiberServer starts the Fiber server.
func startFiberServer(logger zerolog.Logger, db *gorm.DB, mailClient *mail.Mailer, i18n *i18n.I18n) *fiber.App {
func (a *App) startFiberServer() *fiber.App {
var trustedProxies []string
if config.GetString("TRUSTED_PROXIES") != "" {
trustedProxies = strings.Split(config.GetString("TRUSTED_PROXIES"), ",")
}
var enableProxy bool = len(trustedProxies) > 0

var isProduction bool = config.GetString("DEBUG") == "false"

app := fiber.New(fiber.Config{
Prefork: isProduction,
ServerHeader: "ArticPad Server " + config.Version,
Expand All @@ -38,8 +35,8 @@ func startFiberServer(logger zerolog.Logger, db *gorm.DB, mailClient *mail.Maile
TrustedProxies: trustedProxies,
})

app.Use(logging.Logger(logger, func(c *fiber.Ctx) bool {
return c.Path() == "/health" // skip logging for health check
app.Use(logging.Logger(a.logger, func(c *fiber.Ctx) bool {
return c.Path() == "/health"
}))
app.Use(cors.New(cors.Config{
MaxAge: 1800,
Expand All @@ -49,45 +46,49 @@ func startFiberServer(logger zerolog.Logger, db *gorm.DB, mailClient *mail.Maile
Level: compress.LevelBestSpeed, // 1
}))
app.Use(etag.New())
app.Use(limiter.New(limiter.Config{
// TODO: Make this configurable and enable it for auth routes. Also fix that every fork has its own counter by using redis or something.
Max: 100,
LimitReached: func(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusTooManyRequests, "You have exceeded the maximum number of requests. Please try again later.")
},
}))
if config.GetString("RATE_LIMIT_AUTH") == "true" {
app.Use(limiter.New(limiter.Config{
Max: 40,
Expiration: 1 * time.Minute,
LimitReached: func(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusTooManyRequests, "You have exceeded the maximum number of requests. Please try again later.")
},
Storage: func() fiber.Storage {
if a.redis != nil {
return a.redis
}
return limiter.ConfigDefault.Storage
}(),
Next: func(c *fiber.Ctx) bool {
return !strings.HasPrefix(c.Path(), "/api/v1/auth")
},
}))
}

// Create repositories.
userRepository := user.NewUserRepository(db)
userRepository := user.NewUserRepository(a.db)

// Create all of our services.
userService := user.NewUserService(userRepository)

// Prepare our endpoints for the API.
api := app.Group("/api")
apiv1 := api.Group("/v1")

misc.NewMiscHandler(apiv1)
health.NewHealthHandler(app.Group("/health"))
auth.NewAuthHandler(apiv1.Group("/auth"), userService, mailClient, i18n)
auth.NewAuthHandler(apiv1.Group("/auth"), userService, a.mail, a.i18n)
//user.NewUserHandler(apiv1.Group("/users"), userService)

// Prepare an endpoint for 'Not Found'.
api.All("*", func(c *fiber.Ctx) error {
return c.Status(404).JSON(fiber.Map{
"success": false,
"error": "Not Found",
})
})

// Serve Single Page application on "/"
// assume static file at static folder
app.Static("/", config.GetString("STATIC_DIR"), fiber.Static{
Compress: true,
MaxAge: 3600,
})

// Panic test route, this brings up an error
// TODO: Remove this route in production
app.Get("/panic", func(ctx *fiber.Ctx) error {
panic("Hi, I'm a panic error!")
Expand Down
58 changes: 46 additions & 12 deletions internal/infrastructure/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,24 @@ import (
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/storage/redis/v3"
"github.com/jramsgz/articpad/config"
"github.com/jramsgz/articpad/internal/user"
"github.com/jramsgz/articpad/pkg/i18n"
"github.com/jramsgz/articpad/pkg/mail"
"github.com/rs/zerolog"
"gorm.io/gorm"
)

type App struct {
fiber *fiber.App
logger zerolog.Logger
db *gorm.DB
mail *mail.Mailer
i18n *i18n.I18n
redis *redis.Storage
}

// Run ArticPad API & Static Server
func Run() {
if err := config.LoadEnv(); err != nil {
Expand Down Expand Up @@ -55,14 +68,37 @@ func Run() {
Port: config.GetInt("MAIL_PORT"),
Username: config.GetString("MAIL_USERNAME"),
Password: config.GetString("MAIL_PASSWORD"),
From: config.GetString("MAIL_FROM", "ArticPad <"+config.GetString("MAIL_USERNAME")+">"),
From: config.GetString("MAIL_FROM"),
ForceTLS: config.GetString("MAIL_FORCE_TLS") == "true",
})
if err != nil || mailClient == nil {
logger.Fatal().Msgf("Mail server connection error: %s", err)
}

app := startFiberServer(logger, db, mailClient, i18n)
var redisDB *redis.Storage
go func() {
defer func() {
if err := recover(); err != nil {
logger.Error().Msgf("Redis connection error: %s. Some features may not be available.", err)
}
}()
redisDB = redis.New(redis.Config{
Host: config.GetString("REDIS_HOST"),
Port: config.GetInt("REDIS_PORT"),
Username: config.GetString("REDIS_USERNAME"),
Password: config.GetString("REDIS_PASSWORD"),
Database: config.GetInt("REDIS_DB"),
})
}()

app := &App{
logger: logger,
db: db,
mail: mailClient,
i18n: i18n,
redis: redisDB,
}
app.fiber = app.startFiberServer()

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
Expand All @@ -72,15 +108,15 @@ func Run() {
<-c
serverShutdown.Add(1)
defer serverShutdown.Done()
_ = app.ShutdownWithTimeout(60 * time.Second)
_ = app.fiber.ShutdownWithTimeout(60 * time.Second)
}()

if !fiber.IsChild() {
logger.Info().Msgf("Starting ArticPad %s with isProduction: %t", config.Version, true)
logger.Info().Msgf("Starting ArticPad %s with isProduction: %t", config.Version, config.GetString("DEBUG") == "false")
logger.Info().Msgf("BuildTime: %s | Commit: %s", config.BuildTime, config.Commit)
logger.Info().Msgf("Listening on %s", config.GetString("APP_ADDR"))
}
if err := app.Listen(config.GetString("APP_ADDR")); err != nil {
if err := app.fiber.Listen(config.GetString("APP_ADDR")); err != nil {
logger.Fatal().Err(err).Msg("Error starting server")
}

Expand All @@ -89,11 +125,9 @@ func Run() {
serverShutdown.Wait()
}

// TODO: Only for main process or every process?
if true {
sqlDB, _ := db.DB()
_ = sqlDB.Close()
_ = logFile.Close()
_ = mailClient.Close()
}
sqlDB, _ := db.DB()
_ = sqlDB.Close()
_ = logFile.Close()
_ = mailClient.Close()
_ = redisDB.Close()
}
Loading

0 comments on commit be01858

Please sign in to comment.