article.go 8.2 KB

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