fiber.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. package fiber
  2. import (
  3. "errors"
  4. "html/template"
  5. "strconv"
  6. "time"
  7. cacheService "git.dmitriygnatenko.ru/dima/go-common/cache/ttl_memory_cache"
  8. "github.com/gofiber/fiber/v2"
  9. "github.com/gofiber/fiber/v2/middleware/basicauth"
  10. "github.com/gofiber/fiber/v2/middleware/cors"
  11. "github.com/gofiber/fiber/v2/middleware/limiter"
  12. "github.com/gofiber/fiber/v2/middleware/monitor"
  13. "github.com/gofiber/fiber/v2/middleware/recover"
  14. jwt "github.com/gofiber/jwt/v3"
  15. "github.com/gofiber/template/html"
  16. "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/repositories"
  17. authService "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/auth"
  18. envService "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/env"
  19. "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/handler"
  20. adminHandler "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/handler/admin"
  21. )
  22. const (
  23. appName = "dmitriygnatenko"
  24. templatesPath = "./../../internal/templates"
  25. staticPath = "../../web"
  26. metricsURI = "/metrics"
  27. loginRateLimiterMaxRequests = 10
  28. loginRateLimiterExpiration = 30 * time.Second
  29. )
  30. type (
  31. ServiceProvider interface {
  32. EnvService() *envService.Service
  33. AuthService() *authService.Service
  34. CacheService() *cacheService.Cache
  35. ArticleRepository() *repositories.ArticleRepository
  36. TagRepository() *repositories.TagRepository
  37. ArticleTagRepository() *repositories.ArticleTagRepository
  38. UserRepository() *repositories.UserRepository
  39. }
  40. EnvService interface {
  41. StaticVersion() uint64
  42. }
  43. )
  44. func Init(sp ServiceProvider) (*fiber.App, error) {
  45. fiberApp := fiber.New(getConfig(sp))
  46. // Configure web root
  47. fiberApp.Static("/", staticPath)
  48. // Configure CORS middleware
  49. fiberApp.Use(cors.New(getCORSConfig(sp)))
  50. // Configure recover middleware
  51. fiberApp.Use(recover.New())
  52. // Configure JWT auth
  53. jwtAuth := jwt.New(getJWTConfig(sp))
  54. // Configure Basic auth
  55. basicAuth := basicauth.New(basicauth.Config{
  56. Users: map[string]string{
  57. sp.EnvService().BasicAuthUser(): sp.EnvService().BasicAuthPassword(),
  58. },
  59. })
  60. // Metrics
  61. fiberApp.Get(metricsURI, basicAuth, monitor.New(getMetricsConfig()))
  62. // Public handlers
  63. fiberApp.Get(
  64. "/",
  65. handler.MainPageHandler(
  66. sp.CacheService(),
  67. sp.ArticleRepository(),
  68. ),
  69. )
  70. fiberApp.Get(
  71. "/tag/:tag", handler.TagHandler(
  72. sp.CacheService(),
  73. sp.ArticleRepository(),
  74. sp.TagRepository(),
  75. ),
  76. )
  77. fiberApp.Get(
  78. "/article/:article",
  79. handler.ArticleHandler(
  80. sp.CacheService(),
  81. sp.ArticleRepository(),
  82. sp.TagRepository(),
  83. ),
  84. )
  85. // Protected handlers
  86. admin := fiberApp.Group("/admin", jwtAuth)
  87. admin.Get(
  88. "/",
  89. adminHandler.ArticleHandler(sp.ArticleRepository()),
  90. )
  91. admin.All("/login", limiter.New(limiter.Config{
  92. Max: loginRateLimiterMaxRequests,
  93. Expiration: loginRateLimiterExpiration,
  94. }), adminHandler.LoginHandler(
  95. sp.EnvService(),
  96. sp.AuthService(),
  97. sp.UserRepository(),
  98. ))
  99. admin.All(
  100. "/logout",
  101. adminHandler.LogoutHandler(sp.EnvService()),
  102. )
  103. admin.All(
  104. "/user/change-password",
  105. adminHandler.ChangePassword(
  106. sp.AuthService(),
  107. sp.UserRepository(),
  108. ),
  109. )
  110. admin.All(
  111. "/article/add",
  112. adminHandler.AddArticleHandler(
  113. sp.ArticleRepository(),
  114. sp.TagRepository(),
  115. sp.ArticleTagRepository(),
  116. ),
  117. )
  118. admin.All(
  119. "/article/edit/:id<int>",
  120. adminHandler.EditArticleHandler(
  121. sp.ArticleRepository(),
  122. sp.TagRepository(),
  123. sp.ArticleTagRepository(),
  124. sp.CacheService(),
  125. ),
  126. )
  127. admin.All(
  128. "/article/delete/:id<int>",
  129. adminHandler.DeleteArticleHandler(
  130. sp.ArticleRepository(),
  131. sp.ArticleTagRepository(),
  132. sp.CacheService(),
  133. ),
  134. )
  135. admin.Get(
  136. "/tag",
  137. adminHandler.TagHandler(sp.TagRepository()),
  138. )
  139. admin.All(
  140. "/tag/add",
  141. adminHandler.AddTagHandler(
  142. sp.TagRepository(),
  143. sp.CacheService(),
  144. ),
  145. )
  146. admin.All(
  147. "/tag/edit/:id<int>",
  148. adminHandler.EditTagHandler(
  149. sp.TagRepository(),
  150. sp.CacheService(),
  151. ),
  152. )
  153. admin.All(
  154. "/tag/delete/:id<int>",
  155. adminHandler.DeleteTagHandler(
  156. sp.TagRepository(),
  157. sp.CacheService(),
  158. ),
  159. )
  160. return fiberApp, nil
  161. }
  162. func getConfig(sp ServiceProvider) fiber.Config {
  163. return fiber.Config{
  164. AppName: appName,
  165. DisableStartupMessage: true,
  166. Views: getViewsEngine(sp.EnvService()),
  167. ErrorHandler: getErrorHandler(),
  168. }
  169. }
  170. // nolint
  171. func getJWTConfig(sp ServiceProvider) jwt.Config {
  172. return jwt.Config{
  173. SigningKey: []byte(sp.EnvService().JwtSecretKey()),
  174. TokenLookup: "cookie:" + sp.EnvService().JwtCookie(),
  175. ErrorHandler: func(fctx *fiber.Ctx, err error) error {
  176. return fctx.Redirect("/admin/login")
  177. },
  178. Filter: func(fctx *fiber.Ctx) bool {
  179. method := fctx.Method()
  180. path := fctx.Path()
  181. if method != fiber.MethodGet && method != fiber.MethodPost &&
  182. method != fiber.MethodPut && method != fiber.MethodDelete {
  183. return true
  184. }
  185. if path == "/admin/login" {
  186. return true
  187. }
  188. return false
  189. },
  190. }
  191. }
  192. func getMetricsConfig() monitor.Config {
  193. return monitor.Config{
  194. Title: "dmitriygnatenko.ru metrics",
  195. }
  196. }
  197. func getCORSConfig(sp ServiceProvider) cors.Config {
  198. return cors.Config{
  199. AllowOrigins: sp.EnvService().AppCORSAllowOrigins(),
  200. AllowMethods: sp.EnvService().AppCORSAllowMethods(),
  201. }
  202. }
  203. func getViewsEngine(env EnvService) *html.Engine {
  204. engine := html.New(templatesPath, ".html")
  205. // nolint:gocritic
  206. engine.AddFunc("now", func() time.Time {
  207. return time.Now()
  208. })
  209. // nolint:gosec
  210. engine.AddFunc("noescape", func(str string) template.HTML {
  211. return template.HTML(str)
  212. })
  213. engine.AddFunc("gridsep", func(i, l int) bool {
  214. i++
  215. return i%3 == 0 && i != l
  216. })
  217. engine.AddFunc("version", func() string {
  218. return strconv.FormatUint(env.StaticVersion(), 64)
  219. })
  220. return engine
  221. }
  222. func getErrorHandler() fiber.ErrorHandler {
  223. return func(fctx *fiber.Ctx, err error) error {
  224. errCode := fiber.StatusInternalServerError
  225. var e *fiber.Error
  226. if errors.As(err, &e) {
  227. errCode = e.Code
  228. }
  229. var renderData fiber.Map
  230. if errCode == fiber.StatusNotFound {
  231. renderData = fiber.Map{
  232. "pageTitle": "Страница не найдена",
  233. "code": fiber.StatusNotFound,
  234. "text": "Запрашиваемая вами страница не найдена",
  235. }
  236. } else {
  237. renderData = fiber.Map{
  238. "pageTitle": "Внутренняя ошибка",
  239. "code": fiber.StatusInternalServerError,
  240. "text": "Внутренняя ошибка сервера, идем исправлять...",
  241. }
  242. }
  243. renderData["headTitle"] = "От слона к суслику - статьи про PHP, Go, алгоритмы"
  244. return fctx.Render("error", renderData, "_layout")
  245. }
  246. }