package fiber
import (
"errors"
"html/template"
"strconv"
"time"
cacheService "git.dmitriygnatenko.ru/dima/go-common/cache/ttl_memory_cache"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/monitor"
"github.com/gofiber/fiber/v2/middleware/recover"
jwt "github.com/gofiber/jwt/v3"
"github.com/gofiber/template/html"
"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/repositories"
authService "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/auth"
envService "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/env"
"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/handler"
adminHandler "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/handler/admin"
)
const (
appName = "dmitriygnatenko"
templatesPath = "./../../internal/templates"
staticPath = "../../web"
metricsURI = "/metrics"
loginRateLimiterMaxRequests = 10
loginRateLimiterExpiration = 30 * time.Second
)
type (
ServiceProvider interface {
EnvService() *envService.Service
AuthService() *authService.Service
CacheService() *cacheService.Cache
ArticleRepository() *repositories.ArticleRepository
TagRepository() *repositories.TagRepository
ArticleTagRepository() *repositories.ArticleTagRepository
UserRepository() *repositories.UserRepository
}
EnvService interface {
StaticVersion() uint64
}
)
func Init(sp ServiceProvider) (*fiber.App, error) {
fiberApp := fiber.New(getConfig(sp))
// Configure web root
fiberApp.Static("/", staticPath)
// Configure CORS middleware
fiberApp.Use(cors.New(getCORSConfig(sp)))
// Configure recover middleware
fiberApp.Use(recover.New())
// Configure JWT auth
jwtAuth := jwt.New(getJWTConfig(sp))
// Configure Basic auth
basicAuth := basicauth.New(basicauth.Config{
Users: map[string]string{
sp.EnvService().BasicAuthUser(): sp.EnvService().BasicAuthPassword(),
},
})
// Metrics
fiberApp.Get(metricsURI, basicAuth, monitor.New(getMetricsConfig()))
// Public handlers
fiberApp.Get(
"/",
handler.MainPageHandler(
sp.CacheService(),
sp.ArticleRepository(),
),
)
fiberApp.Get(
"/tag/:tag", handler.TagHandler(
sp.CacheService(),
sp.ArticleRepository(),
sp.TagRepository(),
),
)
fiberApp.Get(
"/article/:article",
handler.ArticleHandler(
sp.CacheService(),
sp.ArticleRepository(),
sp.TagRepository(),
),
)
// Protected handlers
admin := fiberApp.Group("/admin", jwtAuth)
admin.Get(
"/",
adminHandler.ArticleHandler(sp.ArticleRepository()),
)
admin.All("/login", limiter.New(limiter.Config{
Max: loginRateLimiterMaxRequests,
Expiration: loginRateLimiterExpiration,
}), adminHandler.LoginHandler(
sp.EnvService(),
sp.AuthService(),
sp.UserRepository(),
))
admin.All(
"/logout",
adminHandler.LogoutHandler(sp.EnvService()),
)
admin.All(
"/user/change-password",
adminHandler.ChangePassword(
sp.AuthService(),
sp.UserRepository(),
),
)
admin.All(
"/article/add",
adminHandler.AddArticleHandler(
sp.ArticleRepository(),
sp.TagRepository(),
sp.ArticleTagRepository(),
),
)
admin.All(
"/article/edit/:id",
adminHandler.EditArticleHandler(
sp.ArticleRepository(),
sp.TagRepository(),
sp.ArticleTagRepository(),
sp.CacheService(),
),
)
admin.All(
"/article/delete/:id",
adminHandler.DeleteArticleHandler(
sp.ArticleRepository(),
sp.ArticleTagRepository(),
sp.CacheService(),
),
)
admin.Get(
"/tag",
adminHandler.TagHandler(sp.TagRepository()),
)
admin.All(
"/tag/add",
adminHandler.AddTagHandler(
sp.TagRepository(),
sp.CacheService(),
),
)
admin.All(
"/tag/edit/:id",
adminHandler.EditTagHandler(
sp.TagRepository(),
sp.CacheService(),
),
)
admin.All(
"/tag/delete/:id",
adminHandler.DeleteTagHandler(
sp.TagRepository(),
sp.CacheService(),
),
)
return fiberApp, nil
}
func getConfig(sp ServiceProvider) fiber.Config {
return fiber.Config{
AppName: appName,
DisableStartupMessage: true,
Views: getViewsEngine(sp.EnvService()),
ErrorHandler: getErrorHandler(),
}
}
// nolint
func getJWTConfig(sp ServiceProvider) jwt.Config {
return jwt.Config{
SigningKey: []byte(sp.EnvService().JwtSecretKey()),
TokenLookup: "cookie:" + sp.EnvService().JwtCookie(),
ErrorHandler: func(fctx *fiber.Ctx, err error) error {
return fctx.Redirect("/admin/login")
},
Filter: func(fctx *fiber.Ctx) bool {
method := fctx.Method()
path := fctx.Path()
if method != fiber.MethodGet && method != fiber.MethodPost &&
method != fiber.MethodPut && method != fiber.MethodDelete {
return true
}
if path == "/admin/login" {
return true
}
return false
},
}
}
func getMetricsConfig() monitor.Config {
return monitor.Config{
Title: "dmitriygnatenko.ru metrics",
}
}
func getCORSConfig(sp ServiceProvider) cors.Config {
return cors.Config{
AllowOrigins: sp.EnvService().AppCORSAllowOrigins(),
AllowMethods: sp.EnvService().AppCORSAllowMethods(),
}
}
func getViewsEngine(env EnvService) *html.Engine {
engine := html.New(templatesPath, ".html")
// nolint:gocritic
engine.AddFunc("now", func() time.Time {
return time.Now()
})
// nolint:gosec
engine.AddFunc("noescape", func(str string) template.HTML {
return template.HTML(str)
})
engine.AddFunc("gridsep", func(i, l int) bool {
i++
return i%3 == 0 && i != l
})
engine.AddFunc("version", func() string {
return strconv.FormatUint(env.StaticVersion(), 64)
})
return engine
}
func getErrorHandler() fiber.ErrorHandler {
return func(fctx *fiber.Ctx, err error) error {
errCode := fiber.StatusInternalServerError
var e *fiber.Error
if errors.As(err, &e) {
errCode = e.Code
}
var renderData fiber.Map
if errCode == fiber.StatusNotFound {
renderData = fiber.Map{
"pageTitle": "Страница не найдена",
"code": fiber.StatusNotFound,
"text": "Запрашиваемая вами страница не найдена",
}
} else {
renderData = fiber.Map{
"pageTitle": "Внутренняя ошибка",
"code": fiber.StatusInternalServerError,
"text": "Внутренняя ошибка сервера, идем исправлять...",
}
}
renderData["headTitle"] = "От слона к суслику - статьи про PHP, Go, алгоритмы"
return fctx.Render("error", renderData, "_layout")
}
}