diff --git a/configs/config.yml b/configs/config.yml new file mode 100644 index 0000000..ce84bd4 --- /dev/null +++ b/configs/config.yml @@ -0,0 +1,6 @@ +port: "8013" +db: + host: "mysql-pl-wa10.joinserver.xyz" + port: "3306" + username: "u45925_HjjJCz8EVf" + dbname: "s45925_tentula" \ No newline at end of file diff --git a/pkg/handler/api.go b/pkg/handler/api.go new file mode 100644 index 0000000..148bc48 --- /dev/null +++ b/pkg/handler/api.go @@ -0,0 +1,286 @@ +package handler + +import ( + "esforum" + "esforum/pkg/util" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +type threadRequest struct { + Title string `json:"title" binding:"required,min=3,max=100"` +} + +type postRequest struct { + Title string `json:"title" binding:"required,min=3,max=100"` + Content string `json:"content" binding:"required,min=5"` + Thread int64 `json:"thread" binding:"required"` +} + +// thread godoc +// @Summary Create new thread (only for admins) +// @Tags thread +// @Produce json +// @Param thread body handler.threadRequest true "Thread data in JSON" +// @Param Authorization header string true "JWT Bearer token (authorization)" +// @Success 200 {object} esforum.Thread "created thread" +// @Router /api/thread [post] +func (h *Handler) create(c *gin.Context) { + var input threadRequest + + if !util.MatchRole(c, "admin") { + util.NewErrorResponse(c, http.StatusForbidden, "no perms") + return + } + + if err := c.BindJSON(&input); err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, err.Error()) + return + } + + thread, err := h.repo.CreateThread(input.Title) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, thread) +} + +// thread/delete godoc +// @Summary Delete thread by id (only for admins) +// @Tags thread +// @Produce json +// @Param id path int true "id of thread" +// @Param Authorization header string true "JWT Bearer token (authorization)" +// @Success 200 +// @Router /api/thread/{id} [delete] +func (h *Handler) delete(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, "invalid id format") + return + } + + _, err = h.repo.GetThread(id) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, err.Error()) + return + } + + current, exist := c.Get(userCtx) + if !exist { + util.NewErrorResponse(c, http.StatusUnauthorized, "user not found in context") + return + } + + currentUser, err := h.services.AuthorizeById(current.(int64)) + if (err != nil) || (currentUser.Access != "admin") { + util.NewErrorResponse(c, http.StatusForbidden, "no permissions") + return + } + + err = h.repo.DeleteThread(id) + if err != nil { + util.NewErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} + +// createPost godoc +// @Summary Create new post +// @Tags post +// @Produce json +// @Param post body handler.postRequest true "Post data in JSON" +// @Param Authorization header string true "JWT Bearer token (authorization)" +// @Success 200 {object} esforum.Post "created post" +// @Router /api/post [post] +func (h *Handler) createPost(c *gin.Context) { + var input postRequest + + if err := c.BindJSON(&input); err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, err.Error()) + return + } + + // Проверяем существование треда + _, err := h.repo.GetThread(input.Thread) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, "thread does not exist") + return + } + + post, err := h.repo.CreatePost(input.Title, input.Content, input.Thread) + if err != nil { + util.NewErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + c.JSON(http.StatusOK, post) +} + +// deletePost godoc +// @Summary Delete post by id +// @Tags post +// @Produce json +// @Param id path int true "id of post" +// @Param Authorization header string true "JWT Bearer token (authorization)" +// @Success 200 +// @Router /api/post/{id} [delete] +func (h *Handler) deletePost(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, "invalid id format") + return + } + + post, err := h.repo.GetPost(id) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, err.Error()) + return + } + + // Только админ может удалять посты + if !util.MatchRole(c, "admin") { + util.NewErrorResponse(c, http.StatusForbidden, "no permissions") + return + } + + err = h.repo.DeletePost(id) + if err != nil { + util.NewErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "ok", "deleted_post_id": post.Id}) +} + +// getThread godoc +// @Summary Get thread by id +// @Tags thread +// @Produce json +// @Param id path int true "id of thread" +// @Success 200 {object} esforum.Thread +// @Router /api/guest/thread/{id} [get] +func (h *Handler) getThread(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, "invalid id format") + return + } + + thread, err := h.repo.GetThread(id) + if err != nil { + util.NewErrorResponse(c, http.StatusNotFound, "thread not found") + return + } + + c.JSON(http.StatusOK, thread) +} + +// getAllThreads godoc +// @Summary Get all threads +// @Tags thread +// @Produce json +// @Success 200 {array} esforum.Thread +// @Router /api/guest/thread/all [get] +func (h *Handler) getAllThreads(c *gin.Context) { + // Получаем параметры для пагинации + startParam := c.DefaultQuery("start", "0") + limitParam := c.DefaultQuery("limit", "20") + + start, err := strconv.Atoi(startParam) + if err != nil || start < 0 { + start = 0 + } + + limit, err := strconv.Atoi(limitParam) + if err != nil || limit < 1 || limit > 100 { + limit = 20 + } + + // Получаем ID всех тредов с учетом пагинации + threadIds := h.repo.GetThreads(start, limit) + + // Получаем общее количество тредов + totalThreads := h.repo.GetThreadsCount() + + // Получаем информацию о каждом треде + threads := make([]esforum.Thread, 0, len(threadIds)) + for _, threadId := range threadIds { + thread, err := h.repo.GetThread(threadId) + if err == nil { + threads = append(threads, *thread) + } + } + + c.JSON(http.StatusOK, gin.H{ + "threads": threads, + "total": totalThreads, + "start": start, + "limit": limit, + }) +} + +// getThreadPosts godoc +// @Summary Get posts in thread +// @Tags post +// @Produce json +// @Param id path int true "id of thread" +// @Success 200 {array} esforum.Post +// @Router /api/guest/thread/{id}/posts [get] +func (h *Handler) getThreadPosts(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, "invalid id format") + return + } + + // Проверяем существование треда + _, err = h.repo.GetThread(id) + if err != nil { + util.NewErrorResponse(c, http.StatusNotFound, "thread not found") + return + } + + // Получаем все посты треда + // По умолчанию первые 20 постов + postIds := h.repo.FindPostsByThread(id, 0, 20) + + posts := make([]esforum.Post, 0, len(postIds)) + for _, postId := range postIds { + post, err := h.repo.GetPost(postId) + if err == nil { + posts = append(posts, *post) + } + } + + c.JSON(http.StatusOK, posts) +} + +// getPost godoc +// @Summary Get post by id +// @Tags post +// @Produce json +// @Param id path int true "id of post" +// @Success 200 {object} esforum.Post +// @Router /api/guest/post/{id} [get] +func (h *Handler) getPost(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + util.NewErrorResponse(c, http.StatusBadRequest, "invalid id format") + return + } + + post, err := h.repo.GetPost(id) + if err != nil { + util.NewErrorResponse(c, http.StatusNotFound, "post not found") + return + } + + c.JSON(http.StatusOK, post) +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go new file mode 100644 index 0000000..71cc1c6 --- /dev/null +++ b/pkg/handler/handler.go @@ -0,0 +1,56 @@ +package handler + +import ( + "esforum/docs" + "esforum/pkg/repository" + "esforum/pkg/service" + + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +type Handler struct { + services *service.Service + repo *repository.Repository +} + +func NewHandler(services *service.Service, repo *repository.Repository) *Handler { + return &Handler{services: services, repo: repo} +} + +// @title EasyStartup Forum +// @version 1.0 +// @description Forum service for EasyStartup. +// +// @contact.name API Support +// @contact.email red.tea.dev@gmail.com +// +// @host forum.easystartup.su +// @BasePath /api +// + +func (h *Handler) InitRoutes() *gin.Engine { + router := gin.New() + docs.SwaggerInfo.BasePath = "/" + + guest := router.Group("/api/guest") + { + guest.GET("thread/:id", h.getThread) + guest.GET("thread/all", h.getAllThreads) + guest.GET("thread/:id/posts", h.getThreadPosts) + guest.GET("post/:id", h.getPost) + } + + api := router.Group("/api", h.userIdentity) + { + api.POST("thread", h.create) + api.DELETE("thread/:id", h.delete) + + api.POST("post", h.createPost) + api.DELETE("post/:id", h.deletePost) + } + + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + return router +} diff --git a/pkg/handler/middleware.go b/pkg/handler/middleware.go new file mode 100644 index 0000000..6a7a00a --- /dev/null +++ b/pkg/handler/middleware.go @@ -0,0 +1,30 @@ +package handler + +import ( + "esforum/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + authorizationHeader = "Authorization" + userCtx = "UserInfo" + userHeader = "UserHeader" +) + +func (h *Handler) userIdentity(c *gin.Context) { + header := c.GetHeader(authorizationHeader) + if header == "" { + util.NewErrorResponse(c, http.StatusUnauthorized, "empty auth header") + return + } + + user, err := h.services.Authorization.Authorize(header) + if err != nil { + util.NewErrorResponse(c, http.StatusUnauthorized, err.Error()) + } + + c.Set(userCtx, user.Id) + c.Set(userHeader, header) +} diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go new file mode 100644 index 0000000..bfce402 --- /dev/null +++ b/pkg/repository/repository.go @@ -0,0 +1,37 @@ +package repository + +import ( + "esforum" + + "gorm.io/gorm" +) + +type Post interface { + CreatePost(title string, content string, thread int64) (*esforum.Post, error) + GetPost(id int64) (*esforum.Post, error) + DeletePost(id int64) error + GetPosts(start, limit int) []int64 + FindPostsByThread(thread int64, start, limit int) []int64 + GetPostsCount() int64 +} + +type Thread interface { + CreateThread(title string) (*esforum.Thread, error) + GetThread(id int64) (*esforum.Thread, error) + FindThread(title string) (*esforum.Thread, error) + DeleteThread(id int64) error + GetThreads(start, limit int) []int64 + GetThreadsCount() int64 +} + +type Repository struct { + Post + Thread +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{ + Post: NewPostSchema(db), + Thread: NewThreadSchema(db), + } +}