diff --git a/backend/main.go b/backend/main.go index 8247387b..fb97b800 100644 --- a/backend/main.go +++ b/backend/main.go @@ -43,6 +43,7 @@ func main() { clerkClient := routes.SetupAuthRoutes(r, db) routes.SetupUserRoutes(r, db, clerkClient) routes.SetupETradeRoutes(r, db) + routes.SetupPostRoutes(r, db) routes.SetupOnboardingRoutes(r, db) r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/backend/src/controllers/post.go b/backend/src/controllers/post.go new file mode 100644 index 00000000..c0fb7da7 --- /dev/null +++ b/backend/src/controllers/post.go @@ -0,0 +1,186 @@ +package controllers + +import ( + "net/http" + "strconv" + + "backend/src/models" + "backend/src/services" + + "github.com/gin-gonic/gin" +) + +type PostController struct { + postService *services.PostService +} + +func NewPostController(postService *services.PostService) *PostController { + return &PostController{ + postService: postService, + } +} + +// GetAllPosts godoc +// +// @Summary Gets all posts +// @Description Returns all posts +// @ID get-all-posts +// @Tags post +// @Produce json +// @Success 200 {object} []models.Post +// @Failure 404 {string} string "Failed to fetch posts" +// @Router /api/posts/ [get] +func (pc *PostController) GetAllPosts(c *gin.Context) { + posts, err := pc.postService.GetAllPosts() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch posts"}) + return + } + + c.JSON(http.StatusOK, posts) +} + +// GetPostsByUserId godoc +// +// @Summary Gets posts by user ID +// @Description Returns all posts for a specific user by user ID +// @ID get-posts-by-user-id +// @Tags post +// @Produce json +// @Param userId path uint true "User ID" +// @Success 200 {object} []models.Post +// @Failure 404 {string} string "Failed to fetch posts" +// @Router /api/posts/user/{userId} [get] +func (pc *PostController) GetPostsByUserId(c *gin.Context) { + userId, err := strconv.ParseUint(c.Param("userId"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + posts, err := pc.postService.GetPostsByUserId(uint(userId)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Failed to fetch posts"}) + return + } + + c.JSON(http.StatusOK, posts) +} + +// CreatePost godoc +// +// @Summary Creates a post +// @Description Creates a post +// @ID create-post +// @Tags post +// @Accept json +// @Produce json +// @Param first_name body string true "First name of the post" +// @Param last_name body string true "Last name of the post" +// @Param postname body string true "Postname of the post" +// @Param email body string true "Email of the post" +// @Param password body string true "Password of the post" +// @Success 201 {object} models.Post +// @Failure 400 {string} string "Failed to create post" +// @Router /api/posts/ [post] +func (pc *PostController) CreatePost(c *gin.Context) { + var post models.Post + if err := c.ShouldBindJSON(&post); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + createdPost, err := pc.postService.CreatePost(&post) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create post"}) + return + } + + c.JSON(http.StatusCreated, createdPost) +} + +// GetPostById godoc +// +// @Summary Gets a post by id +// @Description Returns a post by id +// @ID get-post-by-id +// @Tags post +// @Produce json +// @Param id path int true "ID of the post" +// @Success 200 {object} models.Post +// @Failure 404 {string} string "Failed to fetch post" +// @Router /api/posts/{id} [get] +func (pc *PostController) GetPostById(c *gin.Context) { + id := c.Param("id") + postID, err := strconv.ParseUint(id, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"}) + return + } + + post, err := pc.postService.GetPostById(uint(postID)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Failed to fetch post"}) + return + } + + c.JSON(http.StatusOK, post) +} + +// UpdatePostById godoc +// +// @Summary Updates a post by id +// @Description Updates a post by id +// @ID update-post-by-id +// @Tags post +// @Accept json +// @Produce json +// @Param id path int true "ID of the post" +// @Param first_name body string true "First name of the post" +// @Param last_name body string true "Last name of the post" +// @Param postname body string true "Postname of the post" +// @Param email body string true "Email of the post" +// @Param password body string true "Password of the post" +// @Success 200 {object} models.Post +// @Failure 400 {string} string "Failed to update post" +// @Router /api/posts/{id} [put] +func (pc *PostController) UpdatePostById(c *gin.Context) { + id := c.Param("id") + postID, _ := strconv.ParseUint(id, 10, 32) + var post *models.Post + if err := c.ShouldBindJSON(&post); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + updatedPost, err := pc.postService.UpdatePostById(uint(postID), post) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to update post"}) + return + } + + c.JSON(http.StatusOK, updatedPost) +} + +// DeletePostById godoc +// +// @Summary Deletes a post by id +// @Description Deletes a post by id +// @ID delete-post-by-id +// @Tags post +// @Produce json +// @Param id path int true "ID of the post" +// @Success 200 {object} models.Post +// @Failure 404 {string} string "Failed to delete post" +// @Router /api/posts/{id} [delete] +func (pc *PostController) DeletePostById(c *gin.Context) { + id := c.Param("id") + postID, _ := strconv.ParseUint(id, 10, 32) + post, err := pc.postService.DeletePostById(uint(postID)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Failed to delete post"}) + return + } + + c.JSON(http.StatusOK, post) +} \ No newline at end of file diff --git a/backend/src/db/migrations/7_POST_V1.sql b/backend/src/db/migrations/7_POST_V1.sql new file mode 100644 index 00000000..09d0b106 --- /dev/null +++ b/backend/src/db/migrations/7_POST_V1.sql @@ -0,0 +1,30 @@ +DROP TABLE IF EXISTS post_type_enum; +DROP TABLE IF EXISTS posts; + +--Create post type table +CREATE TYPE post_type_enum AS ENUM ( + '1 month summary', + 'Recent trade', + 'Share comment' +); + +--Create post table +CREATE TABLE IF NOT EXISTS posts ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + post_type post_type_enum NOT NULL, + num_data FLOAT NOT NULL, + ticker_symbol VARCHAR(9), + time_stamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + comment TEXT NOT NULL, + title VARCHAR NOT NULL +); + +INSERT INTO posts (user_id, post_type, num_data, ticker_symbol, comment, title) +VALUES + (1, '1 month summary', 100, 'AAPL', 'I made a 100% return on my investment in Apple this month!', 'Apple Investment'), + (1, 'Recent trade', 200, 'TSLA', 'I just bought 200 shares of Tesla!', 'Tesla Investment'), + (1, 'Share comment', 150, 'MSFT', 'I think Microsoft is a great company to invest in!', 'Microsoft Comment'), + (1, '1 month summary', 250, 'GOOGL', 'I made a 250% return on my investment in Google this month!', 'Google Investment'), + (1, 'Recent trade', 300, 'AMZN', 'I just bought 300 shares of Amazon!', 'Amazon Investment'), + (1, 'Share comment', 400, 'FB', 'I think Facebook is a great company to invest in!', 'Facebook Comment'); \ No newline at end of file diff --git a/backend/src/models/post.go b/backend/src/models/post.go new file mode 100644 index 00000000..15536684 --- /dev/null +++ b/backend/src/models/post.go @@ -0,0 +1,27 @@ +package models + +import ( + "backend/src/types" + "time" +) + +type PostType string + +const ( + ONE_MONTH_SUMMARY PostType = "1 month summary" + RECENT_TRADE PostType = "Recent trade" + SHARE_COMMENT PostType = "Share comment" +) + +type Post struct { + types.Model + UserID uint `gorm:"not null" json:"user_id"` + User User `gorm:"foreignKey:UserID"` + PostType PostType `gorm:"type:post_type_enum;not null" json:"post_type"` + NumData float64 `gorm:"not null" json:"num_data"` + TickerSymbol string `gorm:"type:varchar(9);" json:"ticker_symbol"` + TimeStamp time.Time `gorm:"default:created_at" json:"time_stamp"` + Comment string `gorm:"not null" json:"comment"` + Title string `gorm:"not null" json:"title"` +} + \ No newline at end of file diff --git a/backend/src/routes/etrade.go b/backend/src/routes/etrade.go index c0cc2382..2fd53ad6 100644 --- a/backend/src/routes/etrade.go +++ b/backend/src/routes/etrade.go @@ -12,7 +12,6 @@ func SetupETradeRoutes(router *gin.Engine, db *gorm.DB) { etradeService := services.NewETradeService(db) etradeController := controllers.NewETradeController(etradeService) - etradeRoutes := router.Group("/etrade") { etradeRoutes.GET("/redirect/:user_id", etradeController.GetRedirectURL) diff --git a/backend/src/routes/post.go b/backend/src/routes/post.go new file mode 100644 index 00000000..0f177214 --- /dev/null +++ b/backend/src/routes/post.go @@ -0,0 +1,24 @@ +package routes + +import ( + "backend/src/controllers" + "backend/src/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func SetupPostRoutes(router *gin.Engine, db *gorm.DB) { + postService := services.NewPostService(db) + postController := controllers.NewPostController(postService) + + postRoutes := router.Group("/posts") + { + postRoutes.GET("/", postController.GetAllPosts) + postRoutes.GET("/user-posts/:userId", postController.GetPostsByUserId) + postRoutes.POST("/", postController.CreatePost) + postRoutes.GET("/:id", postController.GetPostById) + postRoutes.PUT("/:id", postController.UpdatePostById) + postRoutes.DELETE("/:id", postController.DeletePostById) + } +} diff --git a/backend/src/services/post.go b/backend/src/services/post.go new file mode 100644 index 00000000..0b174261 --- /dev/null +++ b/backend/src/services/post.go @@ -0,0 +1,73 @@ +package services + +import ( + "backend/src/models" + + "gorm.io/gorm" +) + +type PostService struct { + DB *gorm.DB +} + +func NewPostService(db *gorm.DB) *PostService { + return &PostService{ + DB: db, + } +} + +func (ps *PostService) GetAllPosts() ([]models.Post, error) { + var posts []models.Post + if err := ps.DB.Find(&posts).Error; err != nil { + return nil, err + } + return posts, nil +} + +func (ps *PostService) GetPostsByUserId(userId uint) ([]models.Post, error) { + var posts []models.Post + if err := ps.DB.Where("user_id = ?", userId).Find(&posts).Error; err != nil { + return nil, err + } + return posts, nil +} + +//TODO: Add GetPostsFromFollowedUsers once Cam is done + +func (ps *PostService) CreatePost(post *models.Post) (*models.Post, error) { + if err := ps.DB.Create(post).Error; err != nil { + return nil, err + } + return post, nil +} + +func (ps *PostService) GetPostById(id uint) (*models.Post, error) { + post := &models.Post{} + if err := ps.DB.First(post, id).Error; err != nil { + return nil, err + } + return post, nil +} + +func (ps *PostService) UpdatePostById(id uint, post *models.Post) (*models.Post, error) { + // Retrieve the existing post from the database + _, err := ps.GetPostById(id) + if err != nil { + return nil, err + } + + // Save the updated post back to the database + if err := ps.DB.Save(post).Error; err != nil { + return nil, err + } + + return post, nil +} + +func (ps *PostService) DeletePostById(id uint) (*models.Post, error) { + post := &models.Post{} + if err := ps.DB.Delete(post, id).Error; err != nil { + return nil, err + } + return post, nil +} \ No newline at end of file