package admin import ( "context" "strconv" "time" "git.dmitriygnatenko.ru/dima/go-common/logger" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v4" helper "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helpers/i18n" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helpers/request" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/mapper" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/models" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/handler" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/services/i18n" ) type ( CacheService interface { Delete(key string) } TransactionManager interface { ReadCommitted(context.Context, func(ctx context.Context) error) error } ArticleRepository interface { GetAll(ctx context.Context) ([]models.Article, error) GetByURL(ctx context.Context, url string, lang models.Language) (*models.Article, error) GetByID(ctx context.Context, id uint64) (*models.Article, error) Add(ctx context.Context, m models.Article) (uint64, error) Update(ctx context.Context, m models.Article) error Delete(ctx context.Context, id uint64) error } TagRepository interface { GetAll(ctx context.Context) ([]models.Tag, error) IsUsed(ctx context.Context, id uint64) (bool, error) GetByArticleID(ctx context.Context, id uint64) ([]models.Tag, error) GetByURL(ctx context.Context, tag string) (*models.Tag, error) GetByID(ctx context.Context, id uint64) (*models.Tag, error) Add(ctx context.Context, m models.Tag) error Update(ctx context.Context, m models.Tag) error Delete(ctx context.Context, id uint64) error } ArticleTagRepository interface { Add(ctx context.Context, id uint64, tagIDs []uint64) error Delete(ctx context.Context, id uint64, tagIDs []uint64) error DeleteByArticleID(ctx context.Context, id uint64) error } AuthService interface { GeneratePasswordHash(password string) (string, error) IsCorrectPassword(password string, hash string) bool GenerateToken(user models.User) (string, error) GetClaims(fctx *fiber.Ctx) jwt.MapClaims } ConfigService interface { JWTCookie() string JWTLifeTime() time.Duration } UserRepository interface { Get(ctx context.Context, username string) (*models.User, error) Add(ctx context.Context, username string, password string) (uint64, error) UpdatePassword(ctx context.Context, id uint64, newPassword string) error } ) func ArticleHandler(articleRepository ArticleRepository) fiber.Handler { return func(ctx *fiber.Ctx) error { lang := mapper.LanguageFromContext(ctx) articles, err := articleRepository.GetAll(ctx.Context()) if err != nil { logger.Error(ctx.Context(), err.Error()) return err } return ctx.Render("admin/article", fiber.Map{ "articles": mapper.ToArticlesList(lang, articles), "section": "article", }, "admin/_layout") } } func AddArticleHandler( tm TransactionManager, articleRepository ArticleRepository, tagRepository TagRepository, articleTagRepository ArticleTagRepository, ) fiber.Handler { return func(fctx *fiber.Ctx) error { ctx := fctx.Context() lang := mapper.LanguageFromContext(fctx) var validate = validator.New() validateErrors := make(map[string]string) trans, err := helper.GetDefaultTranslator(validate) if err != nil { logger.Error(ctx, err.Error()) return err } form := models.ArticleForm{ ActiveTags: make(map[uint64]bool), } tags, err := tagRepository.GetAll(ctx) if err != nil { logger.Error(ctx, err.Error()) return err } tagsDTO := mapper.ToTagsList(tags) if fctx.Method() == fiber.MethodPost { if err = fctx.BodyParser(&form); err != nil { logger.Info(ctx, err.Error()) return err } if err = validate.Struct(form); err != nil { validateErrors = helper.FormatValidateErrors(err, trans) } if res, _ := articleRepository.GetByURL(ctx, form.URL, models.LangRu); res != nil { validateErrors["ArticleForm.URL"] = i18n.T(lang, "admin_err_article_exists") } tagIDs := make([]uint64, 0, len(form.Tags)) for i := range form.Tags { tagID, tagErr := strconv.ParseUint(form.Tags[i], 10, 64) if tagErr != nil { logger.Error(ctx, tagErr.Error()) return tagErr } tagIDs = append(tagIDs, tagID) } for i := range tagIDs { form.ActiveTags[tagIDs[i]] = true } if len(validateErrors) == 0 { articleModel, err := mapper.ToArticleModel(form) if err != nil { logger.Error(ctx, err.Error()) return err } err = tm.ReadCommitted(ctx, func(ctx context.Context) error { articleID, txErr := articleRepository.Add(ctx, *articleModel) if txErr != nil { return txErr } if len(form.Tags) > 0 { if txErr = articleTagRepository.Add(ctx, articleID, tagIDs); txErr != nil { return txErr } } return nil }) if err != nil { logger.Error(ctx, err.Error()) return err } return fctx.Redirect("/admin") } } return fctx.Render("admin/article_edit", fiber.Map{ "form": form, "errors": validateErrors, "tags": tagsDTO, "section": "article", "title": i18n.T(lang, "admin_add_article_title"), "show_apply": false, }, "admin/_layout") } } func EditArticleHandler( tm TransactionManager, articleRepository ArticleRepository, tagRepository TagRepository, articleTagRepository ArticleTagRepository, cacheService CacheService, ) fiber.Handler { return func(fctx *fiber.Ctx) error { ctx := fctx.Context() lang := mapper.LanguageFromContext(fctx) var validate = validator.New() validateErrors := make(map[string]string) trans, err := helper.GetDefaultTranslator(validate) if err != nil { logger.Error(ctx, err.Error()) return err } id, err := request.ConvertToUint64(fctx, "id") if err != nil { logger.Error(ctx, err.Error()) return err } article, err := articleRepository.GetByID(ctx, id) if err != nil { logger.Error(ctx, err.Error()) return err } if article == nil { return fiber.ErrNotFound } articleTags, err := tagRepository.GetByArticleID(ctx, id) if err != nil { logger.Error(ctx, err.Error()) return err } tags, err := tagRepository.GetAll(ctx) if err != nil { logger.Error(ctx, err.Error()) return err } tagsDTO := mapper.ToTagsList(tags) var form *models.ArticleForm if fctx.Method() == fiber.MethodGet { form = mapper.ToArticleForm(*article, articleTags) } else if fctx.Method() == fiber.MethodPost { form = &models.ArticleForm{ ID: id, ActiveTags: make(map[uint64]bool), } if err = fctx.BodyParser(form); err != nil { logger.Info(ctx, err.Error()) return err } if err = validate.Struct(*form); err != nil { validateErrors = helper.FormatValidateErrors(err, trans) } if res, _ := articleRepository.GetByURL(ctx, form.URL, models.LangRu); res != nil { if res.ID != id { validateErrors["ArticleForm.URL"] = i18n.T(lang, "admin_err_article_exists") } } tagIDs := make([]uint64, 0, len(form.Tags)) for i := range form.Tags { tagID, tagErr := strconv.ParseUint(form.Tags[i], 10, 64) if tagErr != nil { return tagErr } tagIDs = append(tagIDs, tagID) } for i := range tagIDs { form.ActiveTags[tagIDs[i]] = true } if len(validateErrors) == 0 { articleModel, err := mapper.ToArticleModel(*form) if err != nil { logger.Error(ctx, err.Error()) return err } var tagsToAdd, tagsToDelete []uint64 oldTagsMap := make(map[int]struct{}, len(articleTags)) for i := range articleTags { oldTagsMap[int(articleTags[i].ID)] = struct{}{} if _, ok := form.ActiveTags[articleTags[i].ID]; !ok { tagsToDelete = append(tagsToDelete, articleTags[i].ID) } } for i := range tagIDs { if _, ok := oldTagsMap[int(tagIDs[i])]; !ok { tagsToAdd = append(tagsToAdd, tagIDs[i]) } } err = tm.ReadCommitted(ctx, func(ctx context.Context) error { txErr := articleRepository.Update(ctx, *articleModel) if txErr != nil { return txErr } if len(tagsToAdd) > 0 { if txErr = articleTagRepository.Add(ctx, id, tagsToAdd); txErr != nil { return txErr } } if len(tagsToDelete) > 0 { if txErr = articleTagRepository.Delete(ctx, id, tagsToDelete); txErr != nil { return txErr } } return nil }) if err != nil { logger.Error(ctx, err.Error()) return err } cacheService.Delete(handler.ArticleCacheKey + string(i18n.Ru) + article.URL) if fctx.FormValue("action", "save") == "save" { return fctx.Redirect("/admin") } } } return fctx.Render("admin/article_edit", fiber.Map{ "form": form, "errors": validateErrors, "tags": tagsDTO, "show_apply": true, "section": "article", "title": i18n.T(lang, "admin_edit_article_title"), }, "admin/_layout") } } func DeleteArticleHandler( tm TransactionManager, articleRepository ArticleRepository, articleTagRepository ArticleTagRepository, cacheService CacheService, ) fiber.Handler { return func(fctx *fiber.Ctx) error { ctx := fctx.Context() id, err := request.ConvertToUint64(fctx, "id") if err != nil { logger.Error(ctx, err.Error()) return err } article, err := articleRepository.GetByID(ctx, id) if err != nil { logger.Error(ctx, err.Error()) return err } if fctx.Method() == fiber.MethodPost { err = tm.ReadCommitted(ctx, func(ctx context.Context) error { txErr := articleTagRepository.DeleteByArticleID(ctx, id) if txErr != nil { return txErr } txErr = articleRepository.Delete(ctx, id) if txErr != nil { return txErr } return nil }) if err != nil { logger.Error(ctx, err.Error()) return err } cacheService.Delete(handler.ArticleCacheKey + string(i18n.Ru) + article.URL) return fctx.Redirect("/admin") } return fctx.Render("admin/article_delete", fiber.Map{ "article": article.Title, "section": "article", }, "admin/_layout") } }