article.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. package admin
  2. import (
  3. "context"
  4. "strconv"
  5. "time"
  6. "git.dmitriygnatenko.ru/dima/go-common/logger"
  7. "github.com/go-playground/validator/v10"
  8. "github.com/gofiber/fiber/v2"
  9. "github.com/golang-jwt/jwt/v4"
  10. "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helpers"
  11. "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/mapper"
  12. "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/models"
  13. "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/handler"
  14. )
  15. type (
  16. CacheService interface {
  17. Delete(key string)
  18. }
  19. ArticleRepository interface {
  20. GetAll(ctx context.Context) ([]models.Article, error)
  21. GetByURL(ctx context.Context, url string) (*models.Article, error)
  22. GetByID(ctx context.Context, ID uint64) (*models.Article, error)
  23. Add(ctx context.Context, m models.Article) (uint64, error)
  24. Update(ctx context.Context, m models.Article) error
  25. Delete(ctx context.Context, ID uint64) error
  26. }
  27. TagRepository interface {
  28. GetAll(ctx context.Context) ([]models.Tag, error)
  29. IsUsed(ctx context.Context, ID uint64) (bool, error)
  30. GetByArticleID(ctx context.Context, ID uint64) ([]models.Tag, error)
  31. GetByURL(ctx context.Context, tag string) (*models.Tag, error)
  32. GetByID(ctx context.Context, ID uint64) (*models.Tag, error)
  33. Add(ctx context.Context, m models.Tag) error
  34. Update(ctx context.Context, m models.Tag) error
  35. Delete(ctx context.Context, ID uint64) error
  36. }
  37. ArticleTagRepository interface {
  38. Add(ctx context.Context, articleID uint64, tagIDs []uint64) error
  39. Delete(ctx context.Context, articleID uint64, tagIDs []uint64) error
  40. DeleteByArticleID(ctx context.Context, articleID uint64) error
  41. }
  42. AuthService interface {
  43. GeneratePasswordHash(password string) (string, error)
  44. IsCorrectPassword(password string, hash string) bool
  45. GenerateToken(user models.User) (string, error)
  46. GetClaims(fctx *fiber.Ctx) jwt.MapClaims
  47. }
  48. EnvService interface {
  49. JwtCookie() string
  50. JwtLifeTime() time.Duration
  51. }
  52. UserRepository interface {
  53. Get(ctx context.Context, username string) (*models.User, error)
  54. Add(ctx context.Context, username string, password string) (int, error)
  55. UpdatePassword(ctx context.Context, id uint64, newPassword string) error
  56. }
  57. )
  58. const errArticleExists = "Статья с данным URL уже существует"
  59. func ArticleHandler(articleRepository ArticleRepository) fiber.Handler {
  60. return func(ctx *fiber.Ctx) error {
  61. articles, err := articleRepository.GetAll(ctx.Context())
  62. if err != nil {
  63. logger.Error(ctx.Context(), err.Error())
  64. return err
  65. }
  66. return ctx.Render("admin/article", fiber.Map{
  67. "articles": mapper.ToArticleDTOList(articles),
  68. "section": "article",
  69. }, "admin/_layout")
  70. }
  71. }
  72. func AddArticleHandler(
  73. articleRepository ArticleRepository,
  74. tagRepository TagRepository,
  75. articleTagRepository ArticleTagRepository,
  76. ) fiber.Handler {
  77. return func(fctx *fiber.Ctx) error {
  78. ctx := fctx.Context()
  79. var validate = validator.New()
  80. validateErrors := make(map[string]string)
  81. trans, err := helpers.GetDefaultTranslator(validate)
  82. if err != nil {
  83. logger.Error(ctx, err.Error())
  84. return err
  85. }
  86. form := models.ArticleForm{
  87. ActiveTags: make(map[uint64]bool),
  88. }
  89. tags, err := tagRepository.GetAll(ctx)
  90. if err != nil {
  91. logger.Error(ctx, err.Error())
  92. return err
  93. }
  94. tagsDTO := mapper.ToTagDTOList(tags)
  95. if fctx.Method() == fiber.MethodPost {
  96. if err = fctx.BodyParser(&form); err != nil {
  97. logger.Error(ctx, err.Error())
  98. return err
  99. }
  100. if err = validate.Struct(form); err != nil {
  101. validateErrors = helpers.FormatValidateErrors(err, trans)
  102. }
  103. if res, _ := articleRepository.GetByURL(ctx, form.URL); res != nil {
  104. validateErrors["ArticleForm.URL"] = errArticleExists
  105. }
  106. tagIDs := make([]uint64, 0, len(form.Tags))
  107. for i := range form.Tags {
  108. tagID, tagErr := strconv.ParseUint(form.Tags[i], 10, 64)
  109. if tagErr != nil {
  110. logger.Error(ctx, tagErr.Error())
  111. return tagErr
  112. }
  113. tagIDs = append(tagIDs, tagID)
  114. }
  115. for i := range tagIDs {
  116. form.ActiveTags[tagIDs[i]] = true
  117. }
  118. if len(validateErrors) == 0 {
  119. articleModel, err := mapper.ToArticle(form)
  120. if err != nil {
  121. logger.Error(ctx, err.Error())
  122. return err
  123. }
  124. articleID, articleErr := articleRepository.Add(ctx, *articleModel)
  125. if articleErr != nil {
  126. logger.Error(ctx, articleErr.Error())
  127. return articleErr
  128. }
  129. if len(form.Tags) > 0 {
  130. if err = articleTagRepository.Add(ctx, articleID, tagIDs); err != nil {
  131. logger.Error(ctx, err.Error())
  132. return err
  133. }
  134. }
  135. return fctx.Redirect("/admin")
  136. }
  137. }
  138. return fctx.Render("admin/article_edit", fiber.Map{
  139. "form": form,
  140. "errors": validateErrors,
  141. "tags": tagsDTO,
  142. "section": "article",
  143. "title": "Добавление статьи",
  144. "show_apply": false,
  145. }, "admin/_layout")
  146. }
  147. }
  148. func EditArticleHandler(
  149. articleRepository ArticleRepository,
  150. tagRepository TagRepository,
  151. articleTagRepository ArticleTagRepository,
  152. cacheService CacheService,
  153. ) fiber.Handler {
  154. return func(fctx *fiber.Ctx) error {
  155. ctx := fctx.Context()
  156. var validate = validator.New()
  157. validateErrors := make(map[string]string)
  158. trans, err := helpers.GetDefaultTranslator(validate)
  159. if err != nil {
  160. return err
  161. }
  162. ID, err := strconv.Atoi(fctx.Params("id"))
  163. if err != nil {
  164. return err
  165. }
  166. article, err := articleRepository.GetByID(ctx, ID)
  167. if err != nil {
  168. return err
  169. }
  170. if article == nil {
  171. return fiber.ErrNotFound
  172. }
  173. articleTags, err := tagRepository.GetByArticleID(ctx, ID)
  174. if err != nil {
  175. return err
  176. }
  177. tags, err := tagRepository.GetAll(ctx)
  178. if err != nil {
  179. return err
  180. }
  181. tagsDTO := mapper.ToTagDTOList(tags)
  182. var form *models.ArticleForm
  183. if fctx.Method() == fiber.MethodGet {
  184. form = mapper.ToArticleForm(*article, articleTags)
  185. } else if fctx.Method() == fiber.MethodPost {
  186. form = &models.ArticleForm{
  187. ID: ID,
  188. ActiveTags: make(map[int]bool),
  189. }
  190. if err = fctx.BodyParser(form); err != nil {
  191. return err
  192. }
  193. if err = validate.Struct(*form); err != nil {
  194. validateErrors = helpers.FormatValidateErrors(err, trans)
  195. }
  196. if res, _ := articleRepository.GetByURL(ctx, form.URL); res != nil {
  197. if res.ID != ID {
  198. validateErrors["ArticleForm.URL"] = errArticleExists
  199. }
  200. }
  201. tagIDs := make([]int, 0, len(form.Tags))
  202. for i := range form.Tags {
  203. tagID, tagErr := strconv.Atoi(form.Tags[i])
  204. if tagErr != nil {
  205. return tagErr
  206. }
  207. tagIDs = append(tagIDs, tagID)
  208. }
  209. for i := range tagIDs {
  210. form.ActiveTags[tagIDs[i]] = true
  211. }
  212. if len(validateErrors) == 0 {
  213. articleModel, err := mapper.ToArticle(*form)
  214. if err != nil {
  215. return err
  216. }
  217. err = articleRepository.Update(ctx, *articleModel)
  218. if err != nil {
  219. return err
  220. }
  221. var tagsToAdd, tagsToDelete []int
  222. oldTagsMap := make(map[int]struct{}, len(articleTags))
  223. for i := range articleTags {
  224. oldTagsMap[articleTags[i].ID] = struct{}{}
  225. if _, ok := form.ActiveTags[articleTags[i].ID]; !ok {
  226. tagsToDelete = append(tagsToDelete, articleTags[i].ID)
  227. }
  228. }
  229. for i := range tagIDs {
  230. if _, ok := oldTagsMap[tagIDs[i]]; !ok {
  231. tagsToAdd = append(tagsToAdd, tagIDs[i])
  232. }
  233. }
  234. if len(tagsToAdd) > 0 {
  235. if err = articleTagRepository.Add(ctx, ID, tagsToAdd); err != nil {
  236. return err
  237. }
  238. }
  239. if len(tagsToDelete) > 0 {
  240. if err = articleTagRepository.Delete(ctx, ID, tagsToDelete); err != nil {
  241. return err
  242. }
  243. }
  244. cacheService.Delete(handler.ArticleCacheKey + article.URL)
  245. if fctx.FormValue("action", "save") == "save" {
  246. return fctx.Redirect("/admin")
  247. }
  248. }
  249. }
  250. return fctx.Render("admin/article_edit", fiber.Map{
  251. "form": form,
  252. "errors": validateErrors,
  253. "tags": tagsDTO,
  254. "show_apply": true,
  255. "section": "article",
  256. "title": "Редактирование статьи",
  257. }, "admin/_layout")
  258. }
  259. }
  260. func DeleteArticleHandler(
  261. articleRepository ArticleRepository,
  262. articleTagRepository ArticleTagRepository,
  263. cacheService CacheService,
  264. ) fiber.Handler {
  265. return func(fctx *fiber.Ctx) error {
  266. ctx := fctx.Context()
  267. ID, err := strconv.Atoi(fctx.Params("id"))
  268. if err != nil {
  269. return err
  270. }
  271. article, err := articleRepository.GetByID(ctx, ID)
  272. if err != nil {
  273. return err
  274. }
  275. if fctx.Method() == fiber.MethodPost {
  276. err = articleTagRepository.DeleteByArticleID(ctx, ID)
  277. if err != nil {
  278. return err
  279. }
  280. err = articleRepository.Delete(ctx, ID)
  281. if err != nil {
  282. return err
  283. }
  284. cacheService.Delete(handler.ArticleCacheKey + article.URL)
  285. return fctx.Redirect("/admin")
  286. }
  287. return fctx.Render("admin/article_delete", fiber.Map{
  288. "article": article.Title,
  289. "section": "article",
  290. }, "admin/_layout")
  291. }
  292. }