diff --git a/api/app/v1/core/submit_setter.go b/api/app/v1/core/submit_setter.go new file mode 100644 index 0000000..231c704 --- /dev/null +++ b/api/app/v1/core/submit_setter.go @@ -0,0 +1,81 @@ +package core + +import ( + "bytes" + "io" + "io/ioutil" + "jokes-bapak2-api/app/v1/models" + "jokes-bapak2-api/app/v1/utils" + "mime/multipart" + "net/http" + "net/url" + "os" + + "github.com/gojek/heimdall/v7/httpclient" + "github.com/pquerna/ffjson/ffjson" +) + +// UploadImage process the image from the user to be uploaded to the cloud storage. +// Returns the image URL. +func UploadImage(client *httpclient.Client, image io.Reader) (string, error) { + hostURL := os.Getenv("IMAGE_API_URL") + fileName, err := utils.RandomString(10) + if err != nil { + return "", err + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + fw, err := writer.CreateFormField("image") + if err != nil { + return "", err + } + + _, err = io.Copy(fw, image) + if err != nil { + return "", err + } + + err = writer.Close() + if err != nil { + return "", err + } + + headers := http.Header{ + "Content-Type": []string{writer.FormDataContentType()}, + "User-Agent": []string{"JokesBapak2 API"}, + "Accept": []string{"application/json"}, + } + + requestURL, err := url.Parse(hostURL) + if err != nil { + return "", err + } + + params := url.Values{} + params.Add("key", os.Getenv("IMAGE_API_KEY")) + params.Add("name", fileName) + + requestURL.RawQuery = params.Encode() + + res, err := client.Post(requestURL.String(), bytes.NewReader(body.Bytes()), headers) + if err != nil { + return "", err + } + + defer res.Body.Close() + + responseBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", err + } + + var data models.ImageAPI + err = ffjson.Unmarshal(responseBody, &data) + if err != nil { + return "", err + } + + return data.Data.URL, nil +} diff --git a/api/app/v1/core/submit_validation.go b/api/app/v1/core/submit_validation.go new file mode 100644 index 0000000..a1e23af --- /dev/null +++ b/api/app/v1/core/submit_validation.go @@ -0,0 +1,25 @@ +package core + +import ( + "regexp" + "strings" +) + +func ValidateAuthor(author string) bool { + if len(author) > 200 { + return false + } + + split := strings.Split(author, " ") + if strings.HasPrefix(split[0], "<") && strings.HasSuffix(split[0], ">") { + return false + } + if !strings.HasPrefix(split[len(split)-1], "<") && !strings.HasSuffix(split[len(split)-1], ">") { + return false + } + + email := strings.Replace(split[len(split)-1], "<", "", 1) + email = strings.Replace(email, ">", "", 1) + pattern := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + return pattern.MatchString(email) +} diff --git a/api/app/v1/handler/builder.go b/api/app/v1/handler/builder.go index 59cb394..6121335 100644 --- a/api/app/v1/handler/builder.go +++ b/api/app/v1/handler/builder.go @@ -9,8 +9,8 @@ import ( "github.com/gojek/heimdall/v7/httpclient" ) -var psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) -var db = database.New() -var redis = cache.New() -var memory = cache.InMemory() -var client = httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second)) +var Psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) +var Db = database.New() +var Redis = cache.New() +var Memory = cache.InMemory() +var Client = httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second)) diff --git a/api/app/v1/handler/health.go b/api/app/v1/handler/health.go deleted file mode 100644 index 8f1942f..0000000 --- a/api/app/v1/handler/health.go +++ /dev/null @@ -1,30 +0,0 @@ -package handler - -import ( - "context" - "jokes-bapak2-api/app/v1/models" - - "github.com/gofiber/fiber/v2" -) - -func Health(c *fiber.Ctx) error { - // Ping REDIS database - err := redis.Ping(context.Background()).Err() - if err != nil { - return c. - Status(fiber.StatusServiceUnavailable). - JSON(models.Error{ - Error: "REDIS: " + err.Error(), - }) - } - - _, err = db.Query(context.Background(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1") - if err != nil { - return c. - Status(fiber.StatusServiceUnavailable). - JSON(models.Error{ - Error: "POSTGRESQL: " + err.Error(), - }) - } - return c.SendStatus(fiber.StatusOK) -} diff --git a/api/app/v1/handler/health/health_test.go b/api/app/v1/handler/health/health_test.go index d4e21dd..9e5f325 100644 --- a/api/app/v1/handler/health/health_test.go +++ b/api/app/v1/handler/health/health_test.go @@ -12,6 +12,20 @@ import ( "github.com/stretchr/testify/assert" ) +var db = database.New() +var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1} + +func cleanup() { + _, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") + if err != nil { + panic(err) + } + _, err = db.Query(context.Background(), "DROP TABLE \"administrators\"") + if err != nil { + panic(err) + } +} + func TestHealth(t *testing.T) { err := database.Setup() if err != nil { diff --git a/api/app/v1/handler/health_test.go b/api/app/v1/handler/health_test.go deleted file mode 100644 index f6cca84..0000000 --- a/api/app/v1/handler/health_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestHealth(t *testing.T) { - err := database.Setup() - if err != nil { - t.Fatal(err) - } - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", "not the real one", time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - _, err = db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(cleanup) - - app := v1.New() - - t.Run("Health - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/health", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "health") - assert.Equalf(t, 200, res.StatusCode, "health") - assert.NotEqualf(t, 0, res.ContentLength, "health") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "health") - }) -} diff --git a/api/app/v1/handler/joke_add.go b/api/app/v1/handler/joke_add.go deleted file mode 100644 index 208ef53..0000000 --- a/api/app/v1/handler/joke_add.go +++ /dev/null @@ -1,54 +0,0 @@ -package handler - -import ( - "context" - - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - - "github.com/gofiber/fiber/v2" -) - -func AddNewJoke(c *fiber.Ctx) error { - var body models.Joke - err := c.BodyParser(&body) - if err != nil { - return err - } - - // Check link validity - valid, err := core.CheckImageValidity(client, body.Link) - if err != nil { - return err - } - - if !valid { - return c.Status(fiber.StatusBadRequest).JSON(models.Error{ - Error: "URL provided is not a valid image", - }) - } - - sql, args, err := psql.Insert("jokesbapak2").Columns("link", "creator").Values(body.Link, c.Locals("userID")).ToSql() - if err != nil { - return err - } - - // TODO: Implement solution if the link provided already exists. - _, err = db.Query(context.Background(), sql, args...) - if err != nil { - return err - } - - err = core.SetAllJSONJoke(db, memory) - if err != nil { - return err - } - err = core.SetTotalJoke(db, memory) - if err != nil { - return err - } - - return c.Status(fiber.StatusCreated).JSON(models.ResponseJoke{ - Link: body.Link, - }) -} diff --git a/api/app/v1/handler/joke_add_test.go b/api/app/v1/handler/joke_add_test.go deleted file mode 100644 index 760ab38..0000000 --- a/api/app/v1/handler/joke_add_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - "net/http" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestAddNewJoke(t *testing.T) { - // t.SkipNow() - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(cleanup) - - app := v1.New() - - t.Run("Add - should return 201", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PUT", "/", reqBody) - req.Header.Set("content-type", "application/json") - req.Header.Add("accept", "application/json") - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke add") - assert.Equalf(t, 201, res.StatusCode, "joke add") - assert.NotEqualf(t, 0, res.ContentLength, "joke add") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke add") - assert.Equalf(t, "{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\"}", string(body), "joke add") - }) - - t.Run("Add - should not be a valid image", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://google.com/\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PUT", "/", reqBody) - req.Header.Set("content-type", "application/json") - req.Header.Add("accept", "application/json") - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke add") - assert.Equalf(t, 400, res.StatusCode, "joke add") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke add") - assert.Equalf(t, "{\"error\":\"URL provided is not a valid image\"}", string(body), "joke add") - }) - -} diff --git a/api/app/v1/handler/joke_delete.go b/api/app/v1/handler/joke_delete.go deleted file mode 100644 index e7e2d74..0000000 --- a/api/app/v1/handler/joke_delete.go +++ /dev/null @@ -1,59 +0,0 @@ -package handler - -import ( - "context" - "strconv" - - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - - "github.com/Masterminds/squirrel" - "github.com/gofiber/fiber/v2" -) - -func DeleteJoke(c *fiber.Ctx) error { - id, err := strconv.Atoi(c.Params("id")) - if err != nil { - return err - } - - // Check if the joke exists - sql, args, err := psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() - if err != nil { - return err - } - - var jokeID int - err = db.QueryRow(context.Background(), sql, args...).Scan(&jokeID) - if err != nil { - return err - } - - if jokeID == id { - sql, args, err = psql.Delete("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() - if err != nil { - return err - } - - _, err = db.Query(context.Background(), sql, args...) - if err != nil { - return err - } - - err = core.SetAllJSONJoke(db, memory) - if err != nil { - return err - } - err = core.SetTotalJoke(db, memory) - if err != nil { - return err - } - - return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ - Message: "specified joke id has been deleted", - }) - } - return c.Status(fiber.StatusNotAcceptable).JSON(models.Error{ - Error: "specified joke id does not exists", - }) -} diff --git a/api/app/v1/handler/joke_delete_test.go b/api/app/v1/handler/joke_delete_test.go deleted file mode 100644 index 5e4d9ec..0000000 --- a/api/app/v1/handler/joke_delete_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - "net/http" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestDeleteJoke(t *testing.T) { - // TODO: Remove this line below, make this test works - t.SkipNow() - - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - _, err = db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(cleanup) - - app := v1.New() - - t.Run("Delete - should return 200", func(t *testing.T) { - reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("DELETE", "/id/1", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke delete") - assert.Equalf(t, 200, res.StatusCode, "joke delete") - assert.NotEqualf(t, 0, res.ContentLength, "joke delete") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke delete") - assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke delete") - }) - - t.Run("Delete - id doesn't exists", func(t *testing.T) { - reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("DELETE", "/id/100", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke delete") - assert.Equalf(t, 406, res.StatusCode, "joke delete") - assert.NotEqualf(t, 0, res.ContentLength, "joke delete") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke delete") - assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke delete") - }) -} diff --git a/api/app/v1/handler/joke_get.go b/api/app/v1/handler/joke_get.go deleted file mode 100644 index 463b028..0000000 --- a/api/app/v1/handler/joke_get.go +++ /dev/null @@ -1,150 +0,0 @@ -package handler - -import ( - "context" - "io/ioutil" - "strconv" - "time" - - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - "jokes-bapak2-api/app/v1/utils" - - "github.com/gofiber/fiber/v2" -) - -func TodayJoke(c *fiber.Ctx) error { - // check from redis if today's joke already exists - // send the joke if exists - // get a new joke if it's not, then send it. - var joke models.Today - err := redis.MGet(context.Background(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke) - if err != nil { - return err - } - - eq, err := utils.IsToday(joke.Date) - if err != nil { - return err - } - - if eq { - c.Set("Content-Type", joke.ContentType) - return c.Status(fiber.StatusOK).Send([]byte(joke.Image)) - } else { - var link string - err := db.QueryRow(context.Background(), "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link) - if err != nil { - return err - } - - response, err := client.Get(link, nil) - if err != nil { - return err - } - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - - now := time.Now().UTC().Format(time.RFC3339) - err = redis.MSet(context.Background(), map[string]interface{}{ - "today:link": link, - "today:date": now, - "today:image": string(data), - "today:contentType": response.Header.Get("content-type"), - }).Err() - if err != nil { - return err - } - - c.Set("Content-Type", response.Header.Get("content-type")) - return c.Status(fiber.StatusOK).Send(data) - } - -} - -func SingleJoke(c *fiber.Ctx) error { - checkCache, err := core.CheckJokesCache(memory) - if err != nil { - return err - } - - if !checkCache { - jokes, err := core.GetAllJSONJokes(db) - if err != nil { - return err - } - err = memory.Set("jokes", jokes) - if err != nil { - return err - } - } - - link, err := core.GetRandomJokeFromCache(memory) - if err != nil { - return err - } - - // Get image data - response, err := client.Get(link, nil) - if err != nil { - return err - } - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - - c.Set("Content-Type", response.Header.Get("content-type")) - return c.Status(fiber.StatusOK).Send(data) - -} - -func JokeByID(c *fiber.Ctx) error { - checkCache, err := core.CheckJokesCache(memory) - if err != nil { - return err - } - - if !checkCache { - jokes, err := core.GetAllJSONJokes(db) - if err != nil { - return err - } - err = memory.Set("jokes", jokes) - if err != nil { - return err - } - } - - id, err := strconv.Atoi(c.Params("id")) - if err != nil { - return err - } - - link, err := core.GetCachedJokeByID(memory, id) - if err != nil { - return err - } - - if link == "" { - return c.Status(fiber.StatusNotFound).Send([]byte("Requested ID was not found.")) - } - - // Get image data - response, err := client.Get(link, nil) - if err != nil { - return err - } - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - - c.Set("Content-Type", response.Header.Get("content-type")) - return c.Status(fiber.StatusOK).Send(data) -} diff --git a/api/app/v1/handler/joke_get_test.go b/api/app/v1/handler/joke_get_test.go deleted file mode 100644 index 0f491b6..0000000 --- a/api/app/v1/handler/joke_get_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - "net/http" - "testing" - "time" - - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - - _ "github.com/joho/godotenv/autoload" - "github.com/stretchr/testify/assert" -) - -var db = database.New() -var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1} - -func cleanup() { - _, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") - if err != nil { - panic(err) - } - _, err = db.Query(context.Background(), "DROP TABLE \"administrators\"") - if err != nil { - panic(err) - } -} - -/// Need to find some workaround for this test -func TestJokeGet(t *testing.T) { - err := database.Setup() - if err != nil { - t.Fatal(err) - } - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", "not the real one", time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - _, err = db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(cleanup) - - app := v1.New() - - t.Run("TodayJoke - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/today", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "today joke") - assert.Equalf(t, 200, res.StatusCode, "today joke") - assert.NotEqualf(t, 0, res.ContentLength, "today joke") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "today joke") - }) - - t.Run("SingleJoke - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "single joke") - assert.Equalf(t, 200, res.StatusCode, "single joke") - assert.NotEqualf(t, 0, res.ContentLength, "single joke") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "single joke") - }) - - t.Run("JokeByID - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/id/1", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke by id") - assert.Equalf(t, 200, res.StatusCode, "joke by id") - assert.NotEqualf(t, 0, res.ContentLength, "joke by id") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke by id") - }) - - t.Run("JokeByID - should return 404", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/id/300", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke by id") - assert.Equalf(t, 404, res.StatusCode, "joke by id") - assert.NotEqualf(t, 0, res.ContentLength, "joke by id") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke by id") - assert.Equalf(t, "Requested ID was not found.", string(body), "joke by id") - }) -} diff --git a/api/app/v1/handler/joke_total.go b/api/app/v1/handler/joke_total.go deleted file mode 100644 index 9e473d7..0000000 --- a/api/app/v1/handler/joke_total.go +++ /dev/null @@ -1,38 +0,0 @@ -package handler - -import ( - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - "strconv" - - "github.com/gofiber/fiber/v2" -) - -func TotalJokes(c *fiber.Ctx) error { - checkTotal, err := core.CheckTotalJokesCache(memory) - if err != nil { - return err - } - - if !checkTotal { - err = core.SetTotalJoke(db, memory) - if err != nil { - return err - } - } - - total, err := memory.Get("total") - - if err != nil { - if err.Error() == "Entry not found" { - return c.Status(fiber.StatusInternalServerError).JSON(models.Error{ - Error: "no data found", - }) - } - return err - } - - return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ - Message: strconv.Itoa(int(total[0])), - }) -} diff --git a/api/app/v1/handler/joke_total_test.go b/api/app/v1/handler/joke_total_test.go deleted file mode 100644 index 3b4744a..0000000 --- a/api/app/v1/handler/joke_total_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestTotalJokes(t *testing.T) { - err := database.Setup() - if err != nil { - t.Fatal(err) - } - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", "not the real one", time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - _, err = db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(cleanup) - - app := v1.New() - - t.Run("Total - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/total", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke total") - assert.Equalf(t, 200, res.StatusCode, "joke total") - assert.NotEqualf(t, 0, res.ContentLength, "joke total") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke total") - // FIXME: This should be "message": "3", not one. I don't know what's wrong as it's 1 AM. - assert.Equalf(t, "{\"message\":\"1\"}", string(body), "joke total") - }) -} diff --git a/api/app/v1/handler/joke_update.go b/api/app/v1/handler/joke_update.go deleted file mode 100644 index 00e3853..0000000 --- a/api/app/v1/handler/joke_update.go +++ /dev/null @@ -1,74 +0,0 @@ -package handler - -import ( - "context" - - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - - "github.com/Masterminds/squirrel" - "github.com/gofiber/fiber/v2" -) - -func UpdateJoke(c *fiber.Ctx) error { - id := c.Params("id") - // Check if the joke exists - sql, args, err := psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() - if err != nil { - return err - } - - var jokeID string - err = db.QueryRow(context.Background(), sql, args...).Scan(&jokeID) - if err != nil && err != models.ErrNoRows { - return err - } - - if jokeID == id { - body := new(models.Joke) - err = c.BodyParser(&body) - if err != nil { - return err - } - - // Check link validity - valid, err := core.CheckImageValidity(client, body.Link) - if err != nil { - return err - } - - if !valid { - return c.Status(fiber.StatusBadRequest).JSON(models.Error{ - Error: "URL provided is not a valid image", - }) - } - - sql, args, err = psql.Update("jokesbapak2").Set("link", body.Link).Set("creator", c.Locals("userID")).ToSql() - if err != nil { - return err - } - - _, err = db.Query(context.Background(), sql, args...) - if err != nil { - return err - } - - err = core.SetAllJSONJoke(db, memory) - if err != nil { - return err - } - err = core.SetTotalJoke(db, memory) - if err != nil { - return err - } - - return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ - Message: "specified joke id has been updated", - Link: body.Link, - }) - } - - return c.Status(fiber.StatusNotAcceptable).JSON(models.Error{ - Error: "specified joke id does not exists", - }) -} diff --git a/api/app/v1/handler/joke_update_test.go b/api/app/v1/handler/joke_update_test.go deleted file mode 100644 index 85eade7..0000000 --- a/api/app/v1/handler/joke_update_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - "net/http" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestUpdateJoke(t *testing.T) { - t.SkipNow() - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - _, err = db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(cleanup) - - app := v1.New() - - t.Run("Update - should return 200", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PATCH", "/id/1", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke update") - assert.Equalf(t, 200, res.StatusCode, "joke update") - assert.NotEqualf(t, 0, res.ContentLength, "joke update") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke update") - assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke update") - }) - - t.Run("Update - id doesn't exists", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PATCH", "/id/100", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke update") - assert.Equalf(t, 406, res.StatusCode, "joke update") - assert.NotEqualf(t, 0, res.ContentLength, "joke update") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke update") - assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke update") - }) -} diff --git a/api/app/v1/handler/submit/submit_add.go b/api/app/v1/handler/submit/submit_add.go new file mode 100644 index 0000000..ae3d267 --- /dev/null +++ b/api/app/v1/handler/submit/submit_add.go @@ -0,0 +1,101 @@ +package submit + +import ( + "context" + "jokes-bapak2-api/app/v1/core" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + "strings" + "time" + + "github.com/georgysavva/scany/pgxscan" + "github.com/gofiber/fiber/v2" +) + +func SubmitJoke(c *fiber.Ctx) error { + var body models.Submission + err := c.BodyParser(&body) + if err != nil { + return err + } + + // Image and/or Link should not be empty + if body.Image == "" && body.Link == "" { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "a link or an image should be supplied in a form of multipart/form-data", + }) + } + + // Author should be supplied + if body.Author == "" { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "an author key consisting on the format \"yourname \" must be supplied", + }) + } else { + // Validate format + valid := core.ValidateAuthor(body.Author) + if !valid { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "please stick to the format of \"yourname \" and within 200 characters", + }) + } + } + + var url string + + // Check link validity if link was provided + if body.Link != "" { + valid, err := core.CheckImageValidity(handler.Client, body.Link) + if err != nil { + return err + } + if !valid { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "URL provided is not a valid image", + }) + } + + url = body.Link + } + + // If image was provided + if body.Image != "" { + image := strings.NewReader(body.Image) + + url, err = core.UploadImage(handler.Client, image) + if err != nil { + return err + } + } + + now := time.Now().UTC().Format(time.RFC3339) + + sql, args, err := handler.Psql. + Insert("submission"). + Columns("link", "created_at", "author"). + Values(url, now, body.Author). + Suffix("RETURNING id,created_at,link,author,status"). + ToSql() + if err != nil { + return err + } + + var submission []models.Submission + result, err := handler.Db.Query(context.Background(), sql, args...) + if err != nil { + return err + } + defer result.Close() + + err = pgxscan.ScanAll(&submission, result) + if err != nil { + return err + } + + return c. + Status(fiber.StatusOK). + JSON(models.ResponseSubmission{ + Message: "Joke submitted. Please wait for a few days for admin to approve your submission.", + Data: submission[0], + }) +} diff --git a/api/app/v1/handler/submit/submit_get.go b/api/app/v1/handler/submit/submit_get.go new file mode 100644 index 0000000..f3a3fa7 --- /dev/null +++ b/api/app/v1/handler/submit/submit_get.go @@ -0,0 +1,104 @@ +package submit + +import ( + "bytes" + "context" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + "log" + "strconv" + + "github.com/aldy505/bob" + "github.com/georgysavva/scany/pgxscan" + "github.com/gofiber/fiber/v2" +) + +func GetSubmission(c *fiber.Ctx) error { + query := new(models.SubmissionQuery) + err := c.QueryParser(query) + if err != nil { + return err + } + + var limit int + var offset int + var approved bool + + if query.Limit != "" { + limit, err = strconv.Atoi(query.Limit) + if err != nil { + return err + } + } + if query.Page != "" { + page, err := strconv.Atoi(query.Page) + if err != nil { + return err + } + offset = (page - 1) * 20 + } + + if query.Approved != "" { + approved, err = strconv.ParseBool(query.Approved) + if err != nil { + return err + } + } + + var status int + + if approved { + status = 1 + } else { + status = 0 + } + + var sql string + var args []interface{} + + var sqlQuery *bytes.Buffer = &bytes.Buffer{} + sqlQuery.WriteString("SELECT * FROM submission WHERE TRUE") + + if query.Author != "" { + sqlQuery.WriteString(" AND author = ?") + args = append(args, query.Author) + } + + if query.Approved != "" { + sqlQuery.WriteString(" AND status = ?") + args = append(args, status) + } + + if limit > 0 { + sqlQuery.WriteString(" LIMIT " + strconv.Itoa(limit)) + } else { + sqlQuery.WriteString(" LIMIT 20") + } + + if query.Page != "" { + sqlQuery.WriteString(" OFFSET " + strconv.Itoa(offset)) + } + + sql = bob.ReplacePlaceholder(sqlQuery.String(), bob.Dollar) + + var submissions []models.Submission + results, err := handler.Db.Query(context.Background(), sql, args...) + if err != nil { + log.Println(err) + return err + } + + defer results.Close() + + err = pgxscan.ScanAll(&submissions, results) + if err != nil { + return err + } + + return c. + Status(fiber.StatusOK). + JSON(fiber.Map{ + "count": len(submissions), + "jokes": submissions, + }) +} diff --git a/api/app/v1/models/errors.go b/api/app/v1/models/errors.go new file mode 100644 index 0000000..f7060e0 --- /dev/null +++ b/api/app/v1/models/errors.go @@ -0,0 +1,14 @@ +package models + +import "errors" + +var ErrNoRows = errors.New("no rows in result set") +var ErrConnDone = errors.New("connection is already closed") +var ErrTxDone = errors.New("transaction has already been committed or rolled back") + +var ErrNotFound = errors.New("record not found") +var ErrEmpty = errors.New("record is empty") + +type Error struct { + Error string `json:"error"` +} diff --git a/api/app/v1/models/general.go b/api/app/v1/models/general.go new file mode 100644 index 0000000..11eae1e --- /dev/null +++ b/api/app/v1/models/general.go @@ -0,0 +1,8 @@ +package models + +type Auth struct { + ID int `json:"id" form:"id" db:"id"` + Key string `json:"key" form:"key" db:"key"` + Token string `json:"token" form:"token" db:"token"` + LastUsed string `json:"last_used" form:"last_used" db:"last_used"` +} diff --git a/api/app/v1/models/request.go b/api/app/v1/models/joke.go similarity index 59% rename from api/app/v1/models/request.go rename to api/app/v1/models/joke.go index 6b24ac0..f2b66c4 100644 --- a/api/app/v1/models/request.go +++ b/api/app/v1/models/joke.go @@ -6,15 +6,13 @@ type Joke struct { Creator int `json:"creator" form:"creator" db:"creator"` } -type Auth struct { - ID int `json:"id" form:"id" db:"id"` - Key string `json:"key" form:"key" db:"key"` - Token string `json:"token" form:"token" db:"token"` - LastUsed string `json:"last_used" form:"last_used" db:"last_used"` -} - type Today struct { Date string `redis:"today:date"` Image string `redis:"today:image"` ContentType string `redis:"today:contentType"` } + +type ResponseJoke struct { + Link string `json:"link,omitempty"` + Message string `json:"message,omitempty"` +} diff --git a/api/app/v1/models/response.go b/api/app/v1/models/response.go deleted file mode 100644 index 590536c..0000000 --- a/api/app/v1/models/response.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -type Error struct { - Error string `json:"error"` -} - -type ResponseJoke struct { - Link string `json:"link,omitempty"` - Message string `json:"message,omitempty"` -} diff --git a/api/app/v1/models/submit.go b/api/app/v1/models/submit.go new file mode 100644 index 0000000..ded4eef --- /dev/null +++ b/api/app/v1/models/submit.go @@ -0,0 +1,37 @@ +package models + +type Submission struct { + ID int `json:"id,omitempty" db:"id"` + Link string `json:"link" form:"link" db:"link"` + Image string `json:"image,omitempty" form:"image"` + CreatedAt string `json:"created_at" db:"created_at"` + Author string `json:"author" form:"author" db:"author"` + Status int `json:"status" db:"status"` +} + +type SubmissionQuery struct { + Author string `query:"author"` + Limit string `query:"limit"` + Page string `query:"page"` + Approved string `query:"approved"` +} + +type ResponseSubmission struct { + ID string `json:"id,omitempty"` + Message string `json:"message,omitempty"` + Data Submission `json:"data,omitempty"` +} + +type ImageAPI struct { + Data ImageAPIData `json:"data"` + Success bool `json:"success"` + Status int `json:"status"` +} + +type ImageAPIData struct { + ID string `json:"id"` + Title string `json:"title"` + URLViewer string `json:"url_viewer"` + URL string `json:"url"` + DisplayURL string `json:"display_url"` +} diff --git a/api/app/v1/routes/submit.go b/api/app/v1/routes/submit.go new file mode 100644 index 0000000..6084980 --- /dev/null +++ b/api/app/v1/routes/submit.go @@ -0,0 +1,17 @@ +package routes + +import ( + "jokes-bapak2-api/app/v1/handler/submit" + + "github.com/gofiber/fiber/v2" +) + +func Submit(app *fiber.App) *fiber.App { + // Get pending submitted joke + app.Get("/submit", submit.GetSubmission) + + // Add a joke + app.Post("/submit", submit.SubmitJoke) + + return app +}