package fiber
import (
"errors"
"html/template"
"strconv"
"time"
cache "git.dmitriygnatenko.ru/dima/go-common/cache/ttl_memory_cache"
"git.dmitriygnatenko.ru/dima/go-common/db"
"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/v2"
"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/middleware/language"
"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/repositories"
"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/auth"
"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/config"
"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/handler"
adminHandler "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/handler/admin"
"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n"
)
const (
appName = "dmitriygnatenko"
templatesPath = "./../../internal/templates"
staticPath = "../../web"
metricsURI = "/metrics"
)
type ServiceProvider interface {
ConfigService() *config.Service
AuthService() *auth.Service
CacheService() *cache.Cache
TransactionManager() *db.TxManager
ArticleRepository() *repositories.ArticleRepository
TagRepository() *repositories.TagRepository
ArticleTagRepository() *repositories.ArticleTagRepository
UserRepository() *repositories.UserRepository
}
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 language middleware
fiberApp.Use(language.New())
// Configure JWT auth
jwtAuth := jwt.New(getJWTConfig(sp))
// Configure Basic auth
basicAuth := basicauth.New(basicauth.Config{
Users: map[string]string{
sp.ConfigService().BasicAuthUser(): sp.ConfigService().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: int(sp.ConfigService().LoginRateLimiterMaxRequests()),
Expiration: sp.ConfigService().LoginRateLimiterExpiration(),
}), adminHandler.LoginHandler(
sp.ConfigService(),
sp.AuthService(),
sp.UserRepository(),
))
admin.All(
"/logout",
adminHandler.LogoutHandler(sp.ConfigService()),
)
admin.All(
"/user/change-password",
adminHandler.ChangePassword(
sp.AuthService(),
sp.UserRepository(),
),
)
admin.All(
"/article/add",
adminHandler.AddArticleHandler(
sp.TransactionManager(),
sp.ArticleRepository(),
sp.TagRepository(),
sp.ArticleTagRepository(),
),
)
admin.All(
"/article/edit/:id",
adminHandler.EditArticleHandler(
sp.TransactionManager(),
sp.ArticleRepository(),
sp.TagRepository(),
sp.ArticleTagRepository(),
sp.CacheService(),
),
)
admin.All(
"/article/delete/:id",
adminHandler.DeleteArticleHandler(
sp.TransactionManager(),
sp.ArticleRepository(),
sp.ArticleTagRepository(),
sp.CacheService(),
),
)
admin.Get(
"/tag",
adminHandler.TagHandler(sp.TagRepository()),
)
admin.All(
"/tag/add",
adminHandler.AddTagHandler(
sp.TagRepository(),
),
)
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),
ErrorHandler: getErrorHandler(),
}
}
// nolint
func getJWTConfig(sp ServiceProvider) jwt.Config {
return jwt.Config{
SigningKey: []byte(sp.ConfigService().JWTSecretKey()),
TokenLookup: "cookie:" + sp.ConfigService().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.ConfigService().CORSAllowOrigins(),
AllowMethods: sp.ConfigService().CORSAllowMethods(),
}
}
func getViewsEngine(sp ServiceProvider) *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(uint64(sp.ConfigService().StaticVersion()), 10)
})
return engine
}
func getErrorHandler() fiber.ErrorHandler {
return func(fctx *fiber.Ctx, err error) error {
lang := i18n.Language(fctx)
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": i18n.T(lang, "page_not_found_err_title"),
"code": fiber.StatusNotFound,
"text": i18n.T(lang, "page_not_found_err_desc"),
}
} else {
renderData = fiber.Map{
"pageTitle": i18n.T(lang, "internal_err_title"),
"code": fiber.StatusInternalServerError,
"text": i18n.T(lang, "internal_err_desc"),
}
}
renderData["headTitle"] = i18n.T(lang, "head_title")
return fctx.Render("error", renderData, "_layout")
}
}