diff --git a/admin-app/app.go b/admin-app/app.go index ec23aa6..a92947c 100644 --- a/admin-app/app.go +++ b/admin-app/app.go @@ -1,14 +1,9 @@ package admin_app import ( - "encoding/json" - "net/http" - "strconv" - "github.com/gin-gonic/gin" "github.com/matheusgomes28/urchin/common" "github.com/matheusgomes28/urchin/database" - "github.com/rs/zerolog/log" ) type PostBinding struct { @@ -32,150 +27,6 @@ type DeletePostRequest struct { Id int `json:"id"` } -func getPostHandler(database database.Database) func(*gin.Context) { - return func(c *gin.Context) { - // localhost:8080/post/{id} - var post_binding PostBinding - if err := c.ShouldBindUri(&post_binding); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "could not get post id", - "msg": err.Error(), - }) - return - } - - post_id, err := strconv.Atoi(post_binding.Id) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid post id type", - "msg": err.Error(), - }) - return - } - - post, err := database.GetPost(post_id) - if err != nil { - log.Warn().Msgf("could not get post from DB: %v", err) - c.JSON(http.StatusNotFound, gin.H{ - "error": "post id not found", - "msg": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": post.Id, - "title": post.Title, - "excerpt": post.Excerpt, - "content": post.Content, - }) - } -} - -func postPostHandler(database database.Database) func(*gin.Context) { - return func(c *gin.Context) { - var add_post_request AddPostRequest - decoder := json.NewDecoder(c.Request.Body) - err := decoder.Decode(&add_post_request) - - if err != nil { - log.Warn().Msgf("could not get post from DB: %v", err) - c.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid request body", - "msg": err.Error(), - }) - return - } - - id, err := database.AddPost( - add_post_request.Title, - add_post_request.Excerpt, - add_post_request.Content, - ) - if err != nil { - log.Error().Msgf("failed to add post: %v", err) - c.JSON(http.StatusBadRequest, gin.H{ - "error": "could not add post", - "msg": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": id, - }) - } -} - -func putPostHandler(database database.Database) func(*gin.Context) { - return func(c *gin.Context) { - var change_post_request ChangePostRequest - decoder := json.NewDecoder(c.Request.Body) - decoder.DisallowUnknownFields() - - err := decoder.Decode(&change_post_request) - if err != nil { - log.Warn().Msgf("could not get post from DB: %v", err) - c.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid request body", - "msg": err.Error(), - }) - return - } - - err = database.ChangePost( - change_post_request.Id, - change_post_request.Title, - change_post_request.Excerpt, - change_post_request.Content, - ) - if err != nil { - log.Error().Msgf("failed to change post: %v", err) - c.JSON(http.StatusBadRequest, gin.H{ - "error": "could not change post", - "msg": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": change_post_request.Id, - }) - } -} - -func deletePostHandler(database database.Database) func(*gin.Context) { - return func(c *gin.Context) { - var delete_post_request DeletePostRequest - decoder := json.NewDecoder(c.Request.Body) - decoder.DisallowUnknownFields() - - err := decoder.Decode(&delete_post_request) - if err != nil { - log.Warn().Msgf("could not delete post: %v", err) - c.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid request body", - "msg": err.Error(), - }) - return - } - - err = database.DeletePost(delete_post_request.Id) - if err != nil { - log.Error().Msgf("failed to delete post: %v", err) - c.JSON(http.StatusBadRequest, gin.H{ - "error": "could not delete post", - "msg": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": delete_post_request.Id, - }) - } -} - func SetupRoutes(app_settings common.AppSettings, database database.Database) *gin.Engine { r := gin.Default() @@ -185,5 +36,11 @@ func SetupRoutes(app_settings common.AppSettings, database database.Database) *g r.POST("/posts", postPostHandler(database)) r.PUT("/posts", putPostHandler(database)) r.DELETE("/posts", deletePostHandler(database)) + + // CRUD images + // r.GET("/images/:id", getImageHandler(&database)) + r.POST("/images", postImageHandler(app_settings, database)) + // r.DELETE("/images", deleteImageHandler(&database)) + return r } diff --git a/admin-app/image.go b/admin-app/image.go new file mode 100644 index 0000000..ee9ecf8 --- /dev/null +++ b/admin-app/image.go @@ -0,0 +1,101 @@ +package admin_app + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "slices" + + "github.com/fossoreslp/go-uuid-v4" + "github.com/gin-gonic/gin" + "github.com/matheusgomes28/urchin/common" + "github.com/matheusgomes28/urchin/database" + "github.com/rs/zerolog/log" +) + +type AddImageRequest struct { + Alt string `json:"alt"` +} + +// TODO : need these endpoints +// r.GET("/images/:id", getImageHandler(&database)) +// r.POST("/images", postImageHandler(&database)) +// r.DELETE("/images", deleteImageHandler(&database)) +// func getImageHandler(database *database.Database) func(*gin.Context) { +// return func(c *gin.Context) { +// // Get the image from database +// } +// } + +func postImageHandler(app_settings common.AppSettings, database database.Database) func(*gin.Context) { + return func(c *gin.Context) { + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10*1000000) + form, err := c.MultipartForm() + if err != nil { + log.Error().Msgf("could not create multipart form: %v", err) + return + } + + alt_text_array := form.Value["alt"] + alt_text := "unknown" + if len(alt_text_array) > 0 { + alt_text = alt_text_array[0] + } + + // Begin saving the file to the filesystem + file_array := form.File["file"] + if len(file_array) == 0 { + log.Error().Msgf("could not get the file array: %v", err) + return + } + file := file_array[0] + if file == nil { + log.Error().Msgf("could not upload file: %v", err) + return + } + allowed_types := []string{"image/jpeg", "image/png", "image/gif"} + if file_content_type := file.Header.Get("content-type"); !slices.Contains(allowed_types, file_content_type) { + log.Error().Msgf("file type not supported") + return + } + + uuid, err := uuid.New() + if err != nil { + log.Error().Msgf("could not create the UUID: %v", err) + return + } + + allowed_extensions := []string{"jpeg", "jpg", "png"} + ext := filepath.Ext(file.Filename) + // check ext is supported + if ext == "" && slices.Contains(allowed_extensions, ext) { + log.Error().Msgf("file extension is not supported %v", err) + return + } + + filename := fmt.Sprintf("%s.%s", uuid.String(), ext) + image_path := filepath.Join(app_settings.ImageDirectory, filename) + err = c.SaveUploadedFile(file, image_path) + if err != nil { + log.Error().Msgf("could not save file: %v", err) + return + } + // End saving to filesystem + + // Save metadata into the DB + err = database.AddImage(uuid.String(), file.Filename, alt_text) + if err != nil { + log.Error().Msgf("could not add image metadata to db: %v", err) + err := os.Remove(image_path) + if err != nil { + log.Error().Msgf("could not remove image: %v", err) + } + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": uuid.String(), + }) + } +} diff --git a/admin-app/post.go b/admin-app/post.go new file mode 100644 index 0000000..487e36f --- /dev/null +++ b/admin-app/post.go @@ -0,0 +1,155 @@ +package admin_app + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/matheusgomes28/urchin/database" + "github.com/rs/zerolog/log" +) + +func getPostHandler(database database.Database) func(*gin.Context) { + return func(c *gin.Context) { + // localhost:8080/post/{id} + var post_binding PostBinding + if err := c.ShouldBindUri(&post_binding); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "could not get post id", + "msg": err.Error(), + }) + return + } + + post_id, err := strconv.Atoi(post_binding.Id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid post id type", + "msg": err.Error(), + }) + return + } + + post, err := database.GetPost(post_id) + if err != nil { + log.Warn().Msgf("could not get post from DB: %v", err) + c.JSON(http.StatusNotFound, gin.H{ + "error": "post id not found", + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": post.Id, + "title": post.Title, + "excerpt": post.Excerpt, + "content": post.Content, + }) + } +} + +func postPostHandler(database database.Database) func(*gin.Context) { + return func(c *gin.Context) { + var add_post_request AddPostRequest + decoder := json.NewDecoder(c.Request.Body) + err := decoder.Decode(&add_post_request) + + if err != nil { + log.Warn().Msgf("could not get post from DB: %v", err) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid request body", + "msg": err.Error(), + }) + return + } + + id, err := database.AddPost( + add_post_request.Title, + add_post_request.Excerpt, + add_post_request.Content, + ) + if err != nil { + log.Error().Msgf("failed to add post: %v", err) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "could not add post", + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": id, + }) + } +} + +func putPostHandler(database database.Database) func(*gin.Context) { + return func(c *gin.Context) { + var change_post_request ChangePostRequest + decoder := json.NewDecoder(c.Request.Body) + decoder.DisallowUnknownFields() + + err := decoder.Decode(&change_post_request) + if err != nil { + log.Warn().Msgf("could not get post from DB: %v", err) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid request body", + "msg": err.Error(), + }) + return + } + + err = database.ChangePost( + change_post_request.Id, + change_post_request.Title, + change_post_request.Excerpt, + change_post_request.Content, + ) + if err != nil { + log.Error().Msgf("failed to change post: %v", err) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "could not change post", + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": change_post_request.Id, + }) + } +} + +func deletePostHandler(database database.Database) func(*gin.Context) { + return func(c *gin.Context) { + var delete_post_request DeletePostRequest + decoder := json.NewDecoder(c.Request.Body) + decoder.DisallowUnknownFields() + + err := decoder.Decode(&delete_post_request) + if err != nil { + log.Warn().Msgf("could not delete post: %v", err) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid request body", + "msg": err.Error(), + }) + return + } + + err = database.DeletePost(delete_post_request.Id) + if err != nil { + log.Error().Msgf("failed to delete post: %v", err) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "could not delete post", + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": delete_post_request.Id, + }) + } +} diff --git a/cmd/urchin/index_test.go b/cmd/urchin/index_test.go index b8ea617..da165cb 100644 --- a/cmd/urchin/index_test.go +++ b/cmd/urchin/index_test.go @@ -41,6 +41,10 @@ func (db DatabaseMock) DeletePost(id int) error { return fmt.Errorf("not implemented") } +func (db DatabaseMock) AddImage(string, string, string) error { + return fmt.Errorf("not implemented") +} + func TestIndexPing(t *testing.T) { app_settings := common.AppSettings{ DatabaseAddress: "localhost", diff --git a/common/app_settings.go b/common/app_settings.go index 2aaa6ed..965e2f8 100644 --- a/common/app_settings.go +++ b/common/app_settings.go @@ -15,6 +15,7 @@ type AppSettings struct { DatabasePassword string `toml:"database_password"` DatabaseName string `toml:"database_name"` WebserverPort int `toml:"webserver_port"` + ImageDirectory string `toml:"image_dir"` } func LoadSettings() (AppSettings, error) { @@ -59,6 +60,11 @@ func LoadSettings() (AppSettings, error) { return AppSettings{}, fmt.Errorf("URCHIN_WEBSERVER_PORT is not valid: %v", err) } + image_directory := os.Getenv("URCHIN_IMAGE_DIRECTORY") + if len(image_directory) == 0 { + return AppSettings{}, fmt.Errorf("URCHIN_IMAGE_DIRECTORY is not defined\n") + } + return AppSettings{ DatabaseUser: database_user, DatabasePassword: database_password, @@ -66,6 +72,7 @@ func LoadSettings() (AppSettings, error) { DatabasePort: database_port, DatabaseName: database_name, WebserverPort: webserver_port, + ImageDirectory: image_directory, }, nil } diff --git a/database/database.go b/database/database.go index 755daad..79b8ed2 100644 --- a/database/database.go +++ b/database/database.go @@ -16,6 +16,7 @@ type Database interface { AddPost(title string, excerpt string, content string) (int, error) ChangePost(id int, title string, excerpt string, content string) error DeletePost(id int) error + AddImage(uuid string, name string, alt string) error } type SqlDatabase struct { @@ -27,14 +28,15 @@ type SqlDatabase struct { // / This function gets all the posts from the current // / database connection. -func (db SqlDatabase) GetPosts() ([]common.Post, error) { +func (db SqlDatabase) GetPosts() (all_posts []common.Post, err error) { rows, err := db.Connection.Query("SELECT title, excerpt, id FROM posts;") if err != nil { return make([]common.Post, 0), err } - defer rows.Close() + defer func() { + err = errors.Join(rows.Close()) + }() - all_posts := make([]common.Post, 0) for rows.Next() { var post common.Post if err = rows.Scan(&post.Title, &post.Excerpt, &post.Id); err != nil { @@ -48,15 +50,16 @@ func (db SqlDatabase) GetPosts() ([]common.Post, error) { // / This function gets a post from the database // / with the given ID. -func (db SqlDatabase) GetPost(post_id int) (common.Post, error) { +func (db SqlDatabase) GetPost(post_id int) (post common.Post, err error) { rows, err := db.Connection.Query("SELECT title, content FROM posts WHERE id=?;", post_id) if err != nil { return common.Post{}, err } - defer rows.Close() + defer func() { + err = errors.Join(rows.Close()) + }() rows.Next() - var post common.Post if err = rows.Scan(&post.Title, &post.Content); err != nil { return common.Post{}, err } @@ -134,6 +137,42 @@ func (db SqlDatabase) DeletePost(id int) error { return nil } +// AddImage will add the image metadata to the +// database. +// name - file name saved to the disk +// alt - the alternative text +// returns (uuid, nil) if succeeded, ("", err) otherwise +func (db SqlDatabase) AddImage(uuid string, name string, alt string) (err error) { + tx, err := db.Connection.Begin() + if err != nil { + return err + } + defer func() { + err = errors.Join(tx.Rollback()) + }() + + log.Info().Msgf("adding stuff to the DB") + if name == "" { + return fmt.Errorf("cannot have empty name") + } + + if alt == "" { + return fmt.Errorf("cannot have empty alt text") + } + + query := "INSERT INTO images(uuid, name, alt) VALUES(?, ?, ?);" + _, err = tx.Exec(query, uuid, name, alt) + if err != nil { + return err + } + + if err = tx.Commit(); err != nil { + return err + } + + return nil +} + func MakeSqlConnection(user string, password string, address string, port int, database string) (SqlDatabase, error) { /// Checking the DB connection /// TODO : let user specify the DB diff --git a/go.mod b/go.mod index b820d55..d297d74 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,19 @@ module github.com/matheusgomes28/urchin go 1.22.0 require ( + github.com/BurntSushi/toml v1.3.2 github.com/a-h/templ v0.2.543 + github.com/fossoreslp/go-uuid-v4 v1.0.0 github.com/gin-gonic/gin v1.9.1 github.com/go-sql-driver/mysql v1.7.1 github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 + github.com/pelletier/go-toml/v2 v2.0.8 github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.8.4 github.com/zutto/shardedmap v0.0.0-20180201164343-415202d0910e ) require ( - github.com/BurntSushi/toml v1.3.2 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -30,7 +32,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect diff --git a/go.sum b/go.sum index 2eebcad..b65b1ae 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fossoreslp/go-uuid-v4 v1.0.0 h1:HZDPsCNilzw1/PJ1iIRoLr7CREoROz1/b5RXr3OIUzY= +github.com/fossoreslp/go-uuid-v4 v1.0.0/go.mod h1:jylOsYkbypEni3z7dfRPUyHvdHphkU82RjBawuWkMaw= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= diff --git a/migrations/20240205195122_add_images_table.sql b/migrations/20240205195122_add_images_table.sql new file mode 100644 index 0000000..9a70343 --- /dev/null +++ b/migrations/20240205195122_add_images_table.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE images ( + uuid VARCHAR(36) DEFAULT(UUID()) PRIMARY KEY, + name TEXT NOT NULL, + alt TEXT NOT NULL +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE images; +-- +goose StatementEnd diff --git a/urchin_config.toml b/urchin_config.toml index be27741..06f6a96 100644 --- a/urchin_config.toml +++ b/urchin_config.toml @@ -1,5 +1,5 @@ # Address to the MariaDB database -database_address = "localhost" +database_address = "mariadb" # User to access datbaase database_user = "root" @@ -8,7 +8,7 @@ database_user = "root" database_password = "root" # port -database_port = 3006 +database_port = 3306 # name of database where urchin's # migrations was installed diff --git a/views/contact-failure.templ b/views/contact-failure.templ index 399592a..8153799 100644 --- a/views/contact-failure.templ +++ b/views/contact-failure.templ @@ -3,4 +3,4 @@ package views templ MakeContactFailure(email string, err string) {
Your message could not be sent. Error: { err }
-} \ No newline at end of file +} diff --git a/views/contact-success.templ b/views/contact-success.templ index ccc52f6..60507f3 100644 --- a/views/contact-success.templ +++ b/views/contact-success.templ @@ -3,4 +3,4 @@ package views templ MakeContactSuccess(email string, name string) {Thank you { name }. The message was successfully sent.
-} \ No newline at end of file +}