diff --git a/api/edit.go b/api/article.go similarity index 52% rename from api/edit.go rename to api/article.go index 5d33247..debc9a1 100644 --- a/api/edit.go +++ b/api/article.go @@ -15,6 +15,95 @@ import ( "time" ) +func Article(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + assetsGet(w, r) + case http.MethodPost: + assetsPost(w, r) + case http.MethodDelete: + assetsDelete(w, r) + } +} + +type uploadPayload struct { + Title string `json:"title"` + Summary string `json:"summary"` + Authors []int `json:"authors"` + Image string `json:"image"` + Tags []string `json:"tags"` + Link string `json:"link"` + Content string `json:"content"` +} + +func articlePost(w http.ResponseWriter, r *http.Request) { + authorId, ok := authorizedSession(r) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + var payload uploadPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + InvalidJson.Send(w) + return + } + + rawMarkdown, err := base64.StdEncoding.DecodeString(payload.Content) + if err != nil { + zap.S().Warnf("Cannot decode base64") + ApiError{Message: "invalid base64 content", Code: http.StatusUnprocessableEntity}.Send(w) + return + } + + var notFound []string + database.GetDB().Select("* FROM ? EXCEPT ?", payload.Authors, database.GetDB().Table("author").Select("id")).Find(¬Found) + if len(notFound) > 0 { + ApiError{fmt.Sprintf("no authors with the id(s) %s were found", strings.Join(notFound, ", ")), http.StatusUnprocessableEntity}.Send(w) + return + } + + a := article{ + Title: payload.Title, + Summary: payload.Summary, + Image: payload.Image, + Created: time.Now().Unix(), + Modified: time.Unix(0, 0).Unix(), + Link: "/" + path.Join(config.Prefix, "article", payload.Link), + Markdown: string(rawMarkdown), + Html: string(markdown.ToHTML(rawMarkdown, nil, nil)), + } + database.GetDB().Table("article").Create(&a) + var authors []map[string]interface{} + for _, author := range append([]int{authorId}, payload.Authors...) { + authors = append(authors, map[string]interface{}{ + "article_id": a.ID, + "author_id": author, + }) + } + database.GetDB().Table("article_author").Create(&authors) + var tags []map[string]interface{} + for _, tag := range payload.Tags { + authors = append(authors, map[string]interface{}{ + "article_id": a.ID, + "tag": tag, + }) + } + database.GetDB().Table("article_tag").Create(&tags) + + var articleSummary schema.ArticleSummary + database.GetDB().Table("article").Find(&articleSummary, &a.ID) + database.GetDB().Table("author").Where("id IN (?)", database.GetDB().Table("article_author").Select("author_id").Where("article_id = ?", a.ID)).Find(&articleSummary.Authors) + if payload.Tags != nil { + articleSummary.Tags = payload.Tags + } else { + articleSummary.Tags = []string{} + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(articleSummary) +} + type editPayload struct { Id int `json:"id"` Title *string `json:"title"` @@ -26,7 +115,7 @@ type editPayload struct { Content *string `json:"content"` } -func Edit(w http.ResponseWriter, r *http.Request) { +func articlePatch(w http.ResponseWriter, r *http.Request) { authorId, ok := authorizedSession(r) if !ok { w.WriteHeader(http.StatusUnauthorized) @@ -127,3 +216,32 @@ func Edit(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(articleSummary) } + +type deletePayload struct { + Id int `json:"id"` +} + +func articleDelete(w http.ResponseWriter, r *http.Request) { + authorId, ok := authorizedSession(r) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + var payload deletePayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + InvalidJson.Send(w) + return + } + if !database.Exists(database.GetDB().Table("article"), "id = ?", payload.Id) { + w.WriteHeader(http.StatusNotFound) + return + } else if !database.Exists(database.GetDB().Table("article_author"), "article_id = ? AND author_id = ?", payload.Id, authorId) { + w.WriteHeader(http.StatusUnauthorized) + return + } + + database.GetDB().Table("article").Delete(&payload) + + w.WriteHeader(http.StatusOK) +} diff --git a/api/article_test.go b/api/article_test.go new file mode 100644 index 0000000..d5f8dbe --- /dev/null +++ b/api/article_test.go @@ -0,0 +1,254 @@ +package api + +import ( + "TheAdversary/database" + "TheAdversary/schema" + "encoding/base64" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestArticlePost(t *testing.T) { + if err := initTestDatabase("upload_post_test.sqlite3"); err != nil { + t.Fatal(err) + } + + database.GetDB().Table("article").Create([]map[string]interface{}{ + { + "title": "Upload test", + "summary": "An example article to test the upload api endpoint", + "created": time.Now().Unix(), + "link": "/article/upload-test", + "markdown": "Oh god i have to test all this, what am i doing with my life", + "html": "

Oh god i have to test all this, what am i doing with my life

", + }, + }) + database.GetDB().Table("author").Create([]map[string]interface{}{ + { + "name": "me", + "password": "", + "information": "this is my account", + }, + }) + database.GetDB().Table("article_author").Create([]map[string]interface{}{ + { + "article_id": 1, + "author_id": 1, + }, + }) + + server := httptest.NewServer(http.HandlerFunc(articlePost)) + checkTestInformation(t, server.URL, []testInformation{ + { + Method: http.MethodPost, + Code: http.StatusUnauthorized, + }, + { + Method: http.MethodPost, + Cookie: map[string]string{ + "session_id": initSession(), + }, + Body: uploadPayload{ + Title: "Testooo", + Summary: "I have no idea what to put in here", + Authors: []int{1}, + Link: "testooo", + Content: base64.StdEncoding.EncodeToString([]byte("### Testo")), + }, + ResultBody: schema.ArticleSummary{ + Id: 2, + Title: "Testooo", + Summary: "I have no idea what to put in here", + Authors: []schema.Author{ + { + Id: 1, + Name: "me", + Information: "this is my account", + }, + }, + Tags: []string{}, + Link: "/article/testooo", + }, + Code: http.StatusCreated, + AfterExec: func(information *testInformation) { + var created int64 + database.GetDB().Table("article").Select("created").Where("id = 2").Find(&created) + res := information.ResultBody.(schema.ArticleSummary) + res.Created = created + information.ResultBody = res + }, + }, + }) +} + +func TestArticlePatch(t *testing.T) { + if err := initTestDatabase("edit_test.sqlite3"); err != nil { + t.Fatal(err) + } + + database.GetDB().Table("article").Create([]map[string]interface{}{ + { + "title": "test article", + "summary": "example summary", + "created": time.Now().Unix(), + "link": "/article/test-article", + "markdown": "Just a simple test article", + "html": "

Just a simple test article

", + }, + }) + database.GetDB().Table("author").Create([]map[string]interface{}{ + { + "name": "test", + "password": "", + }, + { + "name": "admin", + "password": "123456", + "information": "im the admin", + }, + }) + database.GetDB().Table("article_author").Create([]map[string]interface{}{ + { + "article_id": 1, + "author_id": 1, + }, + }) + + server := httptest.NewServer(http.HandlerFunc(articlePatch)) + newTitle := "New title" + var created int64 + database.GetDB().Table("article").Select("created").Where("id = 1").Find(&created) + checkTestInformation(t, server.URL, []testInformation{ + { + Method: http.MethodPost, + Code: http.StatusUnauthorized, + }, + { + Method: http.MethodPost, + Cookie: map[string]string{ + "session_id": initSession(), + }, + Body: editPayload{ + Id: 69, + }, + Code: http.StatusNotFound, + }, + { + Method: http.MethodPost, + Cookie: map[string]string{ + "session_id": initSession(), + }, + Body: editPayload{ + Id: 1, + Title: &newTitle, + Authors: &[]int{ + 2, + }, + }, + ResultBody: schema.ArticleSummary{ + Id: 1, + Title: "New title", + Summary: "example summary", + Authors: []schema.Author{ + { + Id: 1, + Name: "test", + }, + { + Id: 2, + Name: "admin", + Information: "im the admin", + }, + }, + Created: created, + Tags: []string{}, + Link: "/article/test-article", + }, + AfterExec: func(information *testInformation) { + var modified int64 + database.GetDB().Table("article").Select("modified").Where("id = 1").Find(&modified) + res := information.ResultBody.(schema.ArticleSummary) + res.Modified = modified + information.ResultBody = res + }, + Code: http.StatusOK, + }, + }) +} + +func TestArticleDelete(t *testing.T) { + if err := initTestDatabase("delete_test.sqlite3"); err != nil { + t.Fatal(err) + } + + database.GetDB().Table("article").Create([]map[string]interface{}{ + { + "title": "test", + "summary": "test summary", + "image": "https://upload.wikimedia.org/wikipedia/commons/0/05/Go_Logo_Blue.svg", + "created": time.Now().Unix(), + "modified": time.Now().Unix(), + "link": "/article/test", + "markdown": "# Title", + "html": "

Title

", + }, + { + "title": "owo", + "created": time.Now().Unix(), + "link": "/article/owo", + "markdown": "owo", + "html": "

owo

", + }, + }) + database.GetDB().Table("author").Create(map[string]interface{}{ + "name": "test", + "password": "", + }) + database.GetDB().Table("article_author").Create([]map[string]interface{}{ + { + "article_id": 1, + "author_id": 1, + }, + { + "article_id": 2, + "author_id": 1, + }, + }) + + server := httptest.NewServer(http.HandlerFunc(articleDelete)) + checkTestInformation(t, server.URL, []testInformation{ + { + Method: http.MethodPost, + Code: http.StatusUnauthorized, + }, + { + Method: http.MethodPost, + Body: deletePayload{ + Id: 1, + }, + Code: http.StatusUnauthorized, + }, + { + Method: http.MethodPost, + Body: deletePayload{ + Id: 1, + }, + Cookie: map[string]string{ + "session_id": initSession(), + }, + Code: http.StatusOK, + }, + { + Method: http.MethodPost, + Body: deletePayload{ + Id: 69, + }, + Cookie: map[string]string{ + "session_id": initSession(), + }, + Code: http.StatusNotFound, + }, + }) +} diff --git a/api/delete.go b/api/delete.go deleted file mode 100644 index 85878c4..0000000 --- a/api/delete.go +++ /dev/null @@ -1,36 +0,0 @@ -package api - -import ( - "TheAdversary/database" - "encoding/json" - "net/http" -) - -type deletePayload struct { - Id int `json:"id"` -} - -func Delete(w http.ResponseWriter, r *http.Request) { - authorId, ok := authorizedSession(r) - if !ok { - w.WriteHeader(http.StatusUnauthorized) - return - } - - var payload deletePayload - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - InvalidJson.Send(w) - return - } - if !database.Exists(database.GetDB().Table("article"), "id = ?", payload.Id) { - w.WriteHeader(http.StatusNotFound) - return - } else if !database.Exists(database.GetDB().Table("article_author"), "article_id = ? AND author_id = ?", payload.Id, authorId) { - w.WriteHeader(http.StatusUnauthorized) - return - } - - database.GetDB().Table("article").Delete(&payload) - - w.WriteHeader(http.StatusOK) -} diff --git a/api/delete_test.go b/api/delete_test.go deleted file mode 100644 index c1a3921..0000000 --- a/api/delete_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package api - -import ( - "TheAdversary/database" - "net/http" - "net/http/httptest" - "testing" - "time" -) - -func TestDelete(t *testing.T) { - if err := initTestDatabase("delete_test.sqlite3"); err != nil { - t.Fatal(err) - } - - database.GetDB().Table("article").Create([]map[string]interface{}{ - { - "title": "test", - "summary": "test summary", - "image": "https://upload.wikimedia.org/wikipedia/commons/0/05/Go_Logo_Blue.svg", - "created": time.Now().Unix(), - "modified": time.Now().Unix(), - "link": "/article/test", - "markdown": "# Title", - "html": "

Title

", - }, - { - "title": "owo", - "created": time.Now().Unix(), - "link": "/article/owo", - "markdown": "owo", - "html": "

owo

", - }, - }) - database.GetDB().Table("author").Create(map[string]interface{}{ - "name": "test", - "password": "", - }) - database.GetDB().Table("article_author").Create([]map[string]interface{}{ - { - "article_id": 1, - "author_id": 1, - }, - { - "article_id": 2, - "author_id": 1, - }, - }) - - server := httptest.NewServer(http.HandlerFunc(Delete)) - checkTestInformation(t, server.URL, []testInformation{ - { - Method: http.MethodPost, - Code: http.StatusUnauthorized, - }, - { - Method: http.MethodPost, - Body: deletePayload{ - Id: 1, - }, - Code: http.StatusUnauthorized, - }, - { - Method: http.MethodPost, - Body: deletePayload{ - Id: 1, - }, - Cookie: map[string]string{ - "session_id": initSession(), - }, - Code: http.StatusOK, - }, - { - Method: http.MethodPost, - Body: deletePayload{ - Id: 69, - }, - Cookie: map[string]string{ - "session_id": initSession(), - }, - Code: http.StatusNotFound, - }, - }) -} diff --git a/api/edit_test.go b/api/edit_test.go deleted file mode 100644 index 6743333..0000000 --- a/api/edit_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package api - -import ( - "TheAdversary/database" - "TheAdversary/schema" - "net/http" - "net/http/httptest" - "testing" - "time" -) - -func TestEdit(t *testing.T) { - if err := initTestDatabase("edit_test.sqlite3"); err != nil { - t.Fatal(err) - } - - database.GetDB().Table("article").Create([]map[string]interface{}{ - { - "title": "test article", - "summary": "example summary", - "created": time.Now().Unix(), - "link": "/article/test-article", - "markdown": "Just a simple test article", - "html": "

Just a simple test article

", - }, - }) - database.GetDB().Table("author").Create([]map[string]interface{}{ - { - "name": "test", - "password": "", - }, - { - "name": "admin", - "password": "123456", - "information": "im the admin", - }, - }) - database.GetDB().Table("article_author").Create([]map[string]interface{}{ - { - "article_id": 1, - "author_id": 1, - }, - }) - - server := httptest.NewServer(http.HandlerFunc(Edit)) - newTitle := "New title" - var created int64 - database.GetDB().Table("article").Select("created").Where("id = 1").Find(&created) - checkTestInformation(t, server.URL, []testInformation{ - { - Method: http.MethodPost, - Code: http.StatusUnauthorized, - }, - { - Method: http.MethodPost, - Cookie: map[string]string{ - "session_id": initSession(), - }, - Body: editPayload{ - Id: 69, - }, - Code: http.StatusNotFound, - }, - { - Method: http.MethodPost, - Cookie: map[string]string{ - "session_id": initSession(), - }, - Body: editPayload{ - Id: 1, - Title: &newTitle, - Authors: &[]int{ - 2, - }, - }, - ResultBody: schema.ArticleSummary{ - Id: 1, - Title: "New title", - Summary: "example summary", - Authors: []schema.Author{ - { - Id: 1, - Name: "test", - }, - { - Id: 2, - Name: "admin", - Information: "im the admin", - }, - }, - Created: created, - Tags: []string{}, - Link: "/article/test-article", - }, - AfterExec: func(information *testInformation) { - var modified int64 - database.GetDB().Table("article").Select("modified").Where("id = 1").Find(&modified) - res := information.ResultBody.(schema.ArticleSummary) - res.Modified = modified - information.ResultBody = res - }, - Code: http.StatusOK, - }, - }) -} diff --git a/api/upload.go b/api/upload.go deleted file mode 100644 index 3db2a03..0000000 --- a/api/upload.go +++ /dev/null @@ -1,94 +0,0 @@ -package api - -import ( - "TheAdversary/config" - "TheAdversary/database" - "TheAdversary/schema" - "encoding/base64" - "encoding/json" - "fmt" - "github.com/gomarkdown/markdown" - "go.uber.org/zap" - "net/http" - "path" - "strings" - "time" -) - -type uploadPayload struct { - Title string `json:"title"` - Summary string `json:"summary"` - Authors []int `json:"authors"` - Image string `json:"image"` - Tags []string `json:"tags"` - Link string `json:"link"` - Content string `json:"content"` -} - -func Upload(w http.ResponseWriter, r *http.Request) { - authorId, ok := authorizedSession(r) - if !ok { - w.WriteHeader(http.StatusUnauthorized) - return - } - - var payload uploadPayload - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - InvalidJson.Send(w) - return - } - - rawMarkdown, err := base64.StdEncoding.DecodeString(payload.Content) - if err != nil { - zap.S().Warnf("Cannot decode base64") - ApiError{Message: "invalid base64 content", Code: http.StatusUnprocessableEntity}.Send(w) - return - } - - var notFound []string - database.GetDB().Select("* FROM ? EXCEPT ?", payload.Authors, database.GetDB().Table("author").Select("id")).Find(¬Found) - if len(notFound) > 0 { - ApiError{fmt.Sprintf("no authors with the id(s) %s were found", strings.Join(notFound, ", ")), http.StatusUnprocessableEntity}.Send(w) - return - } - - a := article{ - Title: payload.Title, - Summary: payload.Summary, - Image: payload.Image, - Created: time.Now().Unix(), - Modified: time.Unix(0, 0).Unix(), - Link: "/" + path.Join(config.Prefix, "article", payload.Link), - Markdown: string(rawMarkdown), - Html: string(markdown.ToHTML(rawMarkdown, nil, nil)), - } - database.GetDB().Table("article").Create(&a) - var authors []map[string]interface{} - for _, author := range append([]int{authorId}, payload.Authors...) { - authors = append(authors, map[string]interface{}{ - "article_id": a.ID, - "author_id": author, - }) - } - database.GetDB().Table("article_author").Create(&authors) - var tags []map[string]interface{} - for _, tag := range payload.Tags { - authors = append(authors, map[string]interface{}{ - "article_id": a.ID, - "tag": tag, - }) - } - database.GetDB().Table("article_tag").Create(&tags) - - var articleSummary schema.ArticleSummary - database.GetDB().Table("article").Find(&articleSummary, &a.ID) - database.GetDB().Table("author").Where("id IN (?)", database.GetDB().Table("article_author").Select("author_id").Where("article_id = ?", a.ID)).Find(&articleSummary.Authors) - if payload.Tags != nil { - articleSummary.Tags = payload.Tags - } else { - articleSummary.Tags = []string{} - } - - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(articleSummary) -} diff --git a/api/upload_test.go b/api/upload_test.go deleted file mode 100644 index 9b66749..0000000 --- a/api/upload_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package api - -import ( - "TheAdversary/database" - "TheAdversary/schema" - "encoding/base64" - "net/http" - "net/http/httptest" - "testing" - "time" -) - -func TestUpload(t *testing.T) { - if err := initTestDatabase("upload_test.sqlite3"); err != nil { - t.Fatal(err) - } - - database.GetDB().Table("article").Create([]map[string]interface{}{ - { - "title": "Upload test", - "summary": "An example article to test the upload api endpoint", - "created": time.Now().Unix(), - "link": "/article/upload-test", - "markdown": "Oh god i have to test all this, what am i doing with my life", - "html": "

Oh god i have to test all this, what am i doing with my life

", - }, - }) - database.GetDB().Table("author").Create([]map[string]interface{}{ - { - "name": "me", - "password": "", - "information": "this is my account", - }, - }) - database.GetDB().Table("article_author").Create([]map[string]interface{}{ - { - "article_id": 1, - "author_id": 1, - }, - }) - - server := httptest.NewServer(http.HandlerFunc(Upload)) - checkTestInformation(t, server.URL, []testInformation{ - { - Method: http.MethodPost, - Code: http.StatusUnauthorized, - }, - { - Method: http.MethodPost, - Cookie: map[string]string{ - "session_id": initSession(), - }, - Body: uploadPayload{ - Title: "Testooo", - Summary: "I have no idea what to put in here", - Authors: []int{1}, - Link: "testooo", - Content: base64.StdEncoding.EncodeToString([]byte("### Testo")), - }, - ResultBody: schema.ArticleSummary{ - Id: 2, - Title: "Testooo", - Summary: "I have no idea what to put in here", - Authors: []schema.Author{ - { - Id: 1, - Name: "me", - Information: "this is my account", - }, - }, - Tags: []string{}, - Link: "/article/testooo", - }, - Code: http.StatusCreated, - AfterExec: func(information *testInformation) { - var created int64 - database.GetDB().Table("article").Select("created").Where("id = 2").Find(&created) - res := information.ResultBody.(schema.ArticleSummary) - res.Created = created - information.ResultBody = res - }, - }, - }) -} diff --git a/main.go b/main.go index 9f07267..8beac98 100644 --- a/main.go +++ b/main.go @@ -23,9 +23,7 @@ func main() { r.HandleFunc("/api/recent", api.Recent).Methods(http.MethodGet) r.HandleFunc("/api/search", api.Search).Methods(http.MethodGet) - r.HandleFunc("/api/upload", api.Upload).Methods(http.MethodPost) - r.HandleFunc("/api/edit", api.Edit).Methods(http.MethodPost) - r.HandleFunc("/api/delete", api.Delete).Methods(http.MethodPost) + r.HandleFunc("/api/article", api.Article).Methods(http.MethodPost, http.MethodPatch, http.MethodDelete) r.HandleFunc("/api/assets", api.Assets).Methods(http.MethodGet, http.MethodPost, http.MethodDelete)