Added backend

This commit is contained in:
2022-02-06 22:51:36 +01:00
parent 77599e26f8
commit 431cadaeea
35 changed files with 2422 additions and 0 deletions

48
backend/api/api.go Normal file
View File

@@ -0,0 +1,48 @@
package api
import (
"encoding/json"
"net/http"
)
var (
DatabaseError = ApiError{Message: "internal database error", Code: http.StatusInternalServerError}
InvalidJson = ApiError{Message: "invalid json", Code: http.StatusUnprocessableEntity}
)
type ApiError struct {
Message string `json:"message"`
Code int `json:"-"`
}
func (ae ApiError) Send(w http.ResponseWriter) error {
w.WriteHeader(ae.Code)
return json.NewEncoder(w).Encode(ae)
}
var sessions = map[string]int{}
type article struct {
ID int `json:"-"`
Title string `json:"title"`
Summary string `json:"summary"`
Image string `json:"image"`
Created int64 `json:"created"`
Modified int64 `json:"modified"`
Link string `json:"link"`
Markdown string `json:"markdown"`
Html string `json:"html"`
}
func authorizedSession(r *http.Request) (int, bool) {
cookie, err := r.Cookie("session_id")
if err != nil {
return 0, false
}
for sessionId, authorId := range sessions {
if sessionId == cookie.Value {
return authorId, true
}
}
return 0, false
}

136
backend/api/api_test.go Normal file
View File

@@ -0,0 +1,136 @@
package api
import (
"TheAdversary/database"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
url2 "net/url"
"os"
"path/filepath"
"reflect"
"testing"
)
type testInformation struct {
Method string
Header map[string]string
Cookie map[string]string
Body interface{}
Query map[string]interface{}
ResultBody interface{}
ResultCookie []string
Code int
AfterExec func(*testInformation)
}
func initTestDatabase(name string) error {
path := filepath.Join(os.TempDir(), name)
if _, err := os.Stat(path); !os.IsNotExist(err) {
if err = os.Remove(path); err != nil {
return err
}
}
db, err := database.NewSqlite3Connection(path)
if err != nil {
return err
}
database.SetGlobDB(db)
declaration, _ := os.ReadFile("../database.sql")
db.Exec(string(declaration))
return nil
}
func initSession() string {
sessid := sessionId()
sessions[sessid] = 1
return sessid
}
func checkTestInformation(t *testing.T, url string, information []testInformation) {
for i, information := range information {
var body io.Reader
if information.Body != nil {
if b, ok := information.Body.([]byte); ok {
body = bytes.NewReader(b)
} else {
buf, _ := json.Marshal(information.Body)
body = bytes.NewReader(buf)
}
}
query := url2.Values{}
if information.Query != nil {
for key, value := range information.Query {
query.Add(key, fmt.Sprintf("%v", value))
}
}
req, _ := http.NewRequest(information.Method, fmt.Sprintf("%s?%s", url, query.Encode()), body)
if information.Cookie != nil {
for name, value := range information.Cookie {
req.AddCookie(&http.Cookie{
Name: name,
Value: value,
})
}
}
if information.Header != nil {
for name, value := range information.Header {
req.Header.Set(name, value)
}
}
resp, _ := http.DefaultClient.Do(req)
if information.AfterExec != nil {
information.AfterExec(&information)
}
if resp.StatusCode != information.Code {
t.Errorf("Test %d sent invalid status code: expected %d, got %d", i+1, information.Code, resp.StatusCode)
}
if resp.Body != nil {
var respBody, informationBody interface{}
json.NewDecoder(resp.Body).Decode(&respBody)
resp.Body.Close()
tmpInformationBytes, _ := json.Marshal(information.ResultBody)
json.Unmarshal(tmpInformationBytes, &informationBody)
if !reflect.DeepEqual(respBody, informationBody) {
respBytes, _ := json.Marshal(respBody)
informationBytes, _ := json.Marshal(informationBody)
// for some reason the maps are sometimes not matched as equal.
// this is an additional checks if the map bytes are equal
if !bytes.Equal(respBytes, informationBytes) {
t.Errorf("Test %d sent invalid response body: expected %s, got %s", i+1, informationBytes, respBytes)
}
}
}
if information.ResultCookie != nil {
for _, cookie := range information.ResultCookie {
var found bool
for _, respCookie := range resp.Cookies() {
if cookie == respCookie.Name {
found = true
break
}
}
if !found {
t.Errorf("Test %d sent invalid cookies: expected %s, got none", i+1, cookie)
}
}
}
}
}

308
backend/api/article.go Normal file
View File

@@ -0,0 +1,308 @@
package api
import (
"TheAdversary/config"
"TheAdversary/database"
"TheAdversary/schema"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/gomarkdown/markdown"
"go.uber.org/zap"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
)
func Article(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
articleGet(w, r)
case http.MethodPost:
articlePost(w, r)
case http.MethodDelete:
articleDelete(w, r)
case http.MethodPatch:
articlePatch(w, r)
}
}
type getResponse 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 articleGet(w http.ResponseWriter, r *http.Request) {
authorId, ok := authorizedSession(r)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
rawId := r.URL.Query().Get("id")
if rawId == "" {
ApiError{Message: "no id was given", Code: http.StatusBadRequest}.Send(w)
return
}
id, err := strconv.Atoi(rawId)
if err != nil {
ApiError{"invalid 'id' parameter", http.StatusUnprocessableEntity}.Send(w)
return
}
if !database.Exists(database.GetDB().Table("article_author"), "author_id=?", authorId) {
w.WriteHeader(http.StatusUnauthorized)
return
}
var article database.Article
if database.GetDB().Table("article").First(&article, id).RowsAffected == 0 {
ApiError{Message: "no such id", Code: http.StatusNotFound}.Send(w)
return
}
resp := getResponse{
Title: article.Title,
Summary: article.Summary,
Image: article.Image,
Link: article.Link,
Content: base64.StdEncoding.EncodeToString([]byte(article.Markdown)),
}
database.GetDB().Table("article_author").Select("author_id").Where("article_id", article.Id).Find(&resp.Authors)
database.GetDB().Table("article_tag").Select("tag").Where("article_id", article.Id).Find(&resp.Tags)
json.NewEncoder(w).Encode(resp)
}
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(&notFound)
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: 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)
if len(payload.Tags) > 0 {
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{}
}
articleSummary.Link = path.Join(config.SubPath, "article", url.PathEscape(articleSummary.Link))
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(articleSummary)
}
type editPayload struct {
Id int `json:"id"`
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 articlePatch(w http.ResponseWriter, r *http.Request) {
authorId, ok := authorizedSession(r)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
var payload editPayload
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
}
updates := map[string]interface{}{}
var authorUpdates, tagUpdates []map[string]interface{}
a := article{
ID: payload.Id,
Modified: time.Now().Unix(),
}
if payload.Title != nil {
updates["title"] = *payload.Title
}
if payload.Summary != nil {
updates["summary"] = *payload.Summary
}
if payload.Authors != nil {
var notFound []string
database.GetDB().Select("* FROM ? EXCEPT ?", *payload.Authors, database.GetDB().Table("author").Select("id")).Find(&notFound)
if len(notFound) > 0 {
ApiError{fmt.Sprintf("no authors with the id(s) %s were found", strings.Join(notFound, ", ")), http.StatusUnprocessableEntity}.Send(w)
return
}
for _, author := range append([]int{authorId}, *payload.Authors...) {
authorUpdates = append(authorUpdates, map[string]interface{}{
"article_id": payload.Id,
"author_id": author,
})
}
}
if payload.Tags != nil {
for _, tag := range *payload.Tags {
tagUpdates = append(tagUpdates, map[string]interface{}{
"article_id": payload.Id,
"tag": tag,
})
}
}
if payload.Image != nil {
updates["image"] = *payload.Image
}
if payload.Link != nil {
updates["link"] = *payload.Link
}
if payload.Content != nil {
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
}
a.Markdown = string(rawMarkdown)
a.Html = string(markdown.ToHTML(rawMarkdown, nil, nil))
updates["markdown"] = string(rawMarkdown)
updates["html"] = string(markdown.ToHTML(rawMarkdown, nil, nil))
}
if len(updates) > 0 {
updates["modified"] = time.Now().Unix()
database.GetDB().Table("article").Where("id = ?", payload.Id).Updates(&updates)
if authorUpdates != nil {
database.GetDB().Table("article_author").Where("article_id = ?", payload.Id).Delete(nil)
database.GetDB().Table("article_author").Create(&authorUpdates)
}
if tagUpdates != nil {
database.GetDB().Table("article_tag").Where("article_id = ?", payload.Id).Delete(nil)
database.GetDB().Table("article_tag").Create(&tagUpdates)
}
}
var articleSummary schema.ArticleSummary
database.GetDB().Table("article").Find(&articleSummary, payload.Id)
database.GetDB().Table("author").Where("id IN (?)", database.GetDB().Table("article_author").Select("author_id").Where("article_id = ?", payload.Id)).Find(&articleSummary.Authors)
if payload.Tags != nil {
articleSummary.Tags = *payload.Tags
} else {
articleSummary.Tags = []string{}
}
articleSummary.Link = path.Join(config.SubPath, "article", url.PathEscape(articleSummary.Link))
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)
}

319
backend/api/article_test.go Normal file
View File

@@ -0,0 +1,319 @@
package api
import (
"TheAdversary/database"
"TheAdversary/schema"
"encoding/base64"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestArticleGet(t *testing.T) {
if err := initTestDatabase("upload_get_test.sqlite3"); err != nil {
t.Fatal(err)
}
database.GetDB().Table("article").Create([]map[string]interface{}{
{
"title": "Get test",
"summary": "",
"created": time.Now().Unix(),
"link": "get-test",
"markdown": "Testing ._.",
"html": "<p>Testing ._.<p>",
},
})
database.GetDB().Table("author").Create([]map[string]interface{}{
{
"name": "admin",
"password": "",
"information": "admin",
},
})
database.GetDB().Table("article_author").Create([]map[string]interface{}{
{
"article_id": 1,
"author_id": 1,
},
})
server := httptest.NewServer(http.HandlerFunc(articleGet))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodGet,
Code: http.StatusUnauthorized,
},
{
Method: http.MethodGet,
Cookie: map[string]string{
"session_id": initSession(),
},
ResultBody: map[string]interface{}{
"message": "no id was given",
},
Code: http.StatusBadRequest,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"id": 1,
},
Cookie: map[string]string{
"session_id": initSession(),
},
ResultBody: getResponse{
Title: "Get test",
Authors: []int{1},
Tags: []string{},
Link: "get-test",
Content: base64.StdEncoding.EncodeToString([]byte("Testing ._.")),
},
Code: http.StatusOK,
},
})
}
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": "upload-test",
"markdown": "Oh god i have to test all this, what am i doing with my life",
"html": "<p>Oh god i have to test all this, what am i doing with my life<p>",
},
})
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": "test-article",
"markdown": "Just a simple test article",
"html": "<p>Just a simple test article<p>",
},
})
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": "test",
"markdown": "# Title",
"html": "<h1>Title</h1>",
},
{
"title": "owo",
"created": time.Now().Unix(),
"link": "owo",
"markdown": "owo",
"html": "<p>owo<p>",
},
})
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,
},
})
}

140
backend/api/assets.go Normal file
View File

@@ -0,0 +1,140 @@
package api
import (
"TheAdversary/config"
"TheAdversary/database"
"TheAdversary/schema"
"encoding/json"
"gorm.io/gorm/clause"
"io"
"net/http"
"net/url"
"path"
"strconv"
)
var assetsPayload struct {
ArticleId int `json:"article_id"`
Content string `json:"content"`
}
func Assets(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)
}
}
func assetsGet(w http.ResponseWriter, r *http.Request) {
_, ok := authorizedSession(r)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
query := r.URL.Query()
request := database.GetDB().Table("assets")
if query.Has("q") {
request.Where("LOWER(name) LIKE ?", "%"+query.Get("q")+"%")
}
limit := 20
if query.Has("limit") {
var err error
limit, err = strconv.Atoi(query.Get("limit"))
if err != nil {
ApiError{"invalid 'limit' parameter", http.StatusUnprocessableEntity}.Send(w)
return
}
}
request.Limit(limit)
var assets []schema.Asset
request.Find(&assets)
for _, asset := range assets {
asset.Link = path.Join(config.SubPath, "assets", asset.Link)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(&assets)
}
func assetsPost(w http.ResponseWriter, r *http.Request) {
_, ok := authorizedSession(r)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
file, header, err := r.FormFile("file")
if err != nil {
if err == http.ErrMissingFile {
ApiError{Message: "file is missing", Code: http.StatusUnprocessableEntity}.Send(w)
} else {
ApiError{Message: "could not parse file" + err.Error(), Code: http.StatusInternalServerError}.Send(w)
}
return
}
defer file.Close()
var name string
if name = r.FormValue("name"); name == "" {
name = header.Filename
}
rawData, err := io.ReadAll(file)
if err != nil {
ApiError{Message: "failed to read file", Code: http.StatusInternalServerError}.Send(w)
return
}
tmpDatabaseSchema := struct {
Id int
Name string
Data []byte
Link string
}{Name: name, Data: rawData, Link: url.PathEscape(name)}
if database.GetDB().Table("assets").Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).Create(&tmpDatabaseSchema).RowsAffected == 0 {
w.WriteHeader(http.StatusConflict)
} else {
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(schema.Asset{
Id: tmpDatabaseSchema.Id,
Name: tmpDatabaseSchema.Name,
Link: path.Join(config.SubPath, "assets", tmpDatabaseSchema.Link),
})
}
}
type assetsDeletePayload struct {
Id int `json:"id"`
}
func assetsDelete(w http.ResponseWriter, r *http.Request) {
_, ok := authorizedSession(r)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
var payload assetsDeletePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
InvalidJson.Send(w)
return
}
if database.GetDB().Table("assets").Delete(schema.Asset{}, payload.Id).RowsAffected == 0 {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusOK)
}
}

183
backend/api/assets_test.go Normal file
View File

@@ -0,0 +1,183 @@
package api
import (
"TheAdversary/database"
"TheAdversary/schema"
"bytes"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
)
func TestAssetsGet(t *testing.T) {
if err := initTestDatabase("assets_get_test.sqlite3"); err != nil {
t.Fatal(err)
}
database.GetDB().Table("assets").Create([]map[string]interface{}{
{
"name": "linux",
"data": "this should be an image of tux :3",
"link": "assets/linux",
},
{
"name": "get test",
"data": "",
"link": "assets/get-test",
},
})
server := httptest.NewServer(http.HandlerFunc(assetsGet))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodGet,
Code: http.StatusUnauthorized,
},
{
Method: http.MethodGet,
Cookie: map[string]string{
"session_id": initSession(),
},
Query: map[string]interface{}{
"q": "linux",
},
ResultBody: []schema.Asset{
{
Id: 1,
Name: "linux",
Link: "assets/linux",
},
},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Cookie: map[string]string{
"session_id": initSession(),
},
Query: map[string]interface{}{
"limit": 1,
},
ResultBody: []schema.Asset{
{
Id: 1,
Name: "linux",
Link: "assets/linux",
},
},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Cookie: map[string]string{
"session_id": initSession(),
},
ResultBody: []schema.Asset{
{
Id: 1,
Name: "linux",
Link: "assets/linux",
},
{
Id: 2,
Name: "get test",
Link: "assets/get-test",
},
},
Code: http.StatusOK,
},
})
}
func TestAssetsPost(t *testing.T) {
if err := initTestDatabase("assets_post_test.sqlite3"); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
mw.SetBoundary("test")
formFile, _ := mw.CreateFormFile("file", "srfwsr")
formFile.Write([]byte("just a test file"))
mw.WriteField("name", "test")
mw.Close()
fmt.Println(buf.String())
server := httptest.NewServer(http.HandlerFunc(assetsPost))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodPost,
Code: http.StatusUnauthorized,
},
{
Method: http.MethodPost,
Header: map[string]string{
"Content-Type": "multipart/form-data; boundary=test",
},
Cookie: map[string]string{
"session_id": initSession(),
},
Body: buf.Bytes(),
ResultBody: schema.Asset{
Id: 1,
Name: "test",
Link: "assets/test",
},
Code: http.StatusCreated,
},
{
Method: http.MethodPost,
Header: map[string]string{
"Content-Type": "multipart/form-data; boundary=test",
},
Cookie: map[string]string{
"session_id": initSession(),
},
Body: buf.Bytes(),
Code: http.StatusConflict,
},
})
}
func TestAssetsDelete(t *testing.T) {
if err := initTestDatabase("assets_delete_test.sqlite3"); err != nil {
t.Fatal(err)
}
database.GetDB().Table("assets").Create(map[string]interface{}{
"name": "example",
"data": "just a normal string",
"link": "assets/example",
})
server := httptest.NewServer(http.HandlerFunc(assetsDelete))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodDelete,
Code: http.StatusUnauthorized,
},
{
Method: http.MethodDelete,
Cookie: map[string]string{
"session_id": initSession(),
},
Body: assetsDeletePayload{
Id: 1,
},
Code: http.StatusOK,
},
{
Method: http.MethodDelete,
Cookie: map[string]string{
"session_id": initSession(),
},
Body: assetsDeletePayload{
Id: 69,
},
Code: http.StatusNotFound,
},
})
}

32
backend/api/authors.go Normal file
View File

@@ -0,0 +1,32 @@
package api
import (
"TheAdversary/database"
"TheAdversary/schema"
"encoding/json"
"net/http"
"strconv"
)
func Authors(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
request := database.GetDB().Table("author")
if query.Has("name") {
request.Where("name LIKE ?", "%"+query.Get("name")+"%")
}
if query.Has("limit") {
limit, err := strconv.Atoi(query.Get("limit"))
if err != nil {
ApiError{"invalid 'limit' parameter", http.StatusUnprocessableEntity}.Send(w)
return
}
request.Limit(limit)
}
var authors []schema.Author
request.Find(&authors)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(authors)
}

View File

@@ -0,0 +1,55 @@
package api
import (
"TheAdversary/database"
"net/http"
"net/http/httptest"
"testing"
)
func TestAuthors(t *testing.T) {
if err := initTestDatabase("authors_test.sqlite3"); err != nil {
t.Fatal(err)
}
database.GetDB().Table("author").Create([]map[string]interface{}{
{
"name": "test",
"password": "",
"information": "test information",
},
})
server := httptest.NewServer(http.HandlerFunc(Authors))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodGet,
ResultBody: []map[string]interface{}{
{
"id": 1,
"name": "test",
"information": "test information",
},
},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"name": "abc",
},
ResultBody: []interface{}{},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"limit": "notanumber",
},
ResultBody: map[string]interface{}{
"message": "invalid 'limit' parameter",
},
Code: http.StatusUnprocessableEntity,
},
})
}

50
backend/api/login.go Normal file
View File

@@ -0,0 +1,50 @@
package api
import (
"TheAdversary/database"
"TheAdversary/schema"
"encoding/json"
"golang.org/x/crypto/bcrypt"
"math/rand"
"net/http"
)
type loginPayload struct {
Username string `json:"username"`
Password string `json:"password"`
}
func Login(w http.ResponseWriter, r *http.Request) {
var payload loginPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
InvalidJson.Send(w)
return
}
var author schema.Author
database.GetDB().Table("author").Select("id", "password").Where("name = ?", payload.Username).Take(&author)
if author.Id == 0 || bcrypt.CompareHashAndPassword([]byte(author.Password), []byte(payload.Password)) != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
sessionID := sessionId()
sessions[sessionID] = author.Id
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
})
w.WriteHeader(http.StatusOK)
}
func sessionId() string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
s := make([]rune, 32)
for i := range s {
s[i] = letters[rand.Intn(len(letters))]
}
return string(s)
}

51
backend/api/login_test.go Normal file
View File

@@ -0,0 +1,51 @@
package api
import (
"TheAdversary/database"
"golang.org/x/crypto/bcrypt"
"net/http"
"net/http/httptest"
"testing"
)
func TestLogin(t *testing.T) {
if err := initTestDatabase("login_test.sqlite3"); err != nil {
t.Fatal(err)
}
password, _ := bcrypt.GenerateFromPassword([]byte("super secure password"), bcrypt.DefaultCost)
database.GetDB().Table("author").Create([]map[string]interface{}{
{
"name": "owner",
"password": password,
"information": "owner of the best blog in the world",
},
})
server := httptest.NewServer(http.HandlerFunc(Login))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodPost,
Body: loginPayload{
Username: "owner",
Password: "super secure password",
},
ResultCookie: []string{"session_id"},
Code: http.StatusOK,
},
{
Method: http.MethodPost,
Body: loginPayload{
Username: "not a user",
},
Code: http.StatusUnauthorized,
},
{
Method: http.MethodPost,
Body: loginPayload{
Username: "test",
},
Code: http.StatusUnauthorized,
},
})
}

44
backend/api/recent.go Normal file
View File

@@ -0,0 +1,44 @@
package api
import (
"TheAdversary/config"
"TheAdversary/database"
"TheAdversary/schema"
"encoding/json"
"net/http"
"path"
"strconv"
)
func Recent(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
request := database.GetDB().Table("article")
limit := 20
if query.Has("limit") {
var err error
limit, err = strconv.Atoi(query.Get("limit"))
if err != nil {
ApiError{"invalid 'limit' parameter", http.StatusUnprocessableEntity}.Send(w)
return
} else if limit > 100 {
ApiError{"'limit' parameter must not be over 100", http.StatusUnprocessableEntity}.Send(w)
return
}
}
request.Limit(limit)
var articleSummaries []schema.ArticleSummary
request.Find(&articleSummaries)
for i, summary := range articleSummaries {
database.GetDB().Table("author").Where("id IN (?)", database.GetDB().Table("article_author").Select("author_id").Where("article_id = ?", summary.Id)).Find(&summary.Authors)
summary.Tags = []string{}
database.GetDB().Table("article_tag").Select("tag").Where("article_id = ?", summary.Id).Find(&summary.Tags)
summary.Link = path.Join(config.SubPath, "article", summary.Link)
articleSummaries[i] = summary
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(articleSummaries)
}

114
backend/api/recent_test.go Normal file
View File

@@ -0,0 +1,114 @@
package api
import (
"TheAdversary/database"
"TheAdversary/schema"
"net/http"
"net/http/httptest"
"path"
"testing"
"time"
)
func TestRecent(t *testing.T) {
if err := initTestDatabase("recent_test.sqlite3"); err != nil {
t.Fatal(err)
}
authors := []schema.Author{
{
Id: 1,
Name: "user",
Password: "",
},
}
articles := []schema.ArticleSummary{
{
Id: 1,
Title: "test",
Summary: "test summary",
Authors: authors,
Tags: []string{},
Created: time.Unix(0, 0).Unix(),
Modified: time.Now().Unix(),
Link: "article/test",
},
{
Id: 2,
Title: "Recent Article",
Summary: "This article is recent",
Authors: authors,
Tags: []string{},
Created: time.Now().Unix(),
Link: "article/recent",
},
}
database.GetDB().Table("article").Create([]map[string]interface{}{
{
"title": articles[0].Title,
"summary": articles[0].Summary,
"created": articles[0].Created,
"modified": articles[0].Modified,
"link": path.Base(articles[0].Link),
"markdown": "# Title",
"html": "<h1>Title</h1>",
},
{
"title": articles[1].Title,
"summary": articles[1].Summary,
"created": articles[1].Created,
"link": path.Base(articles[1].Link),
"markdown": "This is the most recent article",
"html": "<p>This is the most recent article</p>",
},
})
database.GetDB().Table("author").Create(authors)
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(Recent))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodGet,
ResultBody: articles,
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"limit": 10,
},
ResultBody: articles,
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"limit": 1001,
},
ResultBody: map[string]interface{}{
"message": "'limit' parameter must not be over 100",
},
Code: http.StatusUnprocessableEntity,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"limit": "notanumber",
},
ResultBody: map[string]interface{}{
"message": "invalid 'limit' parameter",
},
Code: http.StatusUnprocessableEntity,
},
})
}

81
backend/api/search.go Normal file
View File

@@ -0,0 +1,81 @@
package api
import (
"TheAdversary/config"
"TheAdversary/database"
"TheAdversary/schema"
"encoding/json"
"net/http"
"path"
"strconv"
"strings"
)
func Search(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
request := database.GetDB().Table("article")
if query.Has("q") {
request.Where("LOWER(title) LIKE ?", "%"+query.Get("q")+"%")
}
if query.Has("from") {
from, err := strconv.ParseInt(query.Get("from"), 10, 64)
if err != nil {
ApiError{"invalid 'from' parameter", http.StatusUnprocessableEntity}.Send(w)
return
}
request.Where("created >= ?", from).Or("modified >= ?", from)
}
if query.Has("to") {
to, err := strconv.ParseInt(query.Get("to"), 10, 64)
if err != nil {
ApiError{"invalid 'to' parameter", http.StatusUnprocessableEntity}.Send(w)
return
}
request.Where("created <= ?", to).Or("modified <= ?", to)
}
if query.Has("authors") {
var authorIds []int
if err := json.NewDecoder(strings.NewReader(query.Get("authors"))).Decode(&authorIds); err != nil {
ApiError{"could not parse 'authors' parameter as array of integers / numbers", http.StatusUnprocessableEntity}.Send(w)
return
}
request.Where("id IN (?)", database.GetDB().Table("article_author").Select("article_id").Where("author_id IN (?)", authorIds))
}
if query.Has("tags") {
var tags []string
if err := json.NewDecoder(strings.NewReader(query.Get("tags"))).Decode(&tags); err != nil {
ApiError{"could not parse 'tags' parameter as array of strings", http.StatusUnprocessableEntity}.Send(w)
return
}
authorRequest := database.GetDB().Table("article_tag").Select("article_id").Where("tag IN ?", tags)
request.Where("id IN (?)", authorRequest)
}
limit := 20
if query.Has("limit") {
var err error
limit, err = strconv.Atoi(query.Get("limit"))
if err != nil {
ApiError{"invalid 'limit' parameter", http.StatusUnprocessableEntity}.Send(w)
return
} else if limit > 100 {
ApiError{"'limit' parameter must not be over 100", http.StatusUnprocessableEntity}.Send(w)
return
}
}
request.Limit(limit)
var articleSummaries []schema.ArticleSummary
request.Find(&articleSummaries)
for i, summary := range articleSummaries {
database.GetDB().Table("author").Where("id IN (?)", database.GetDB().Table("article_author").Select("author_id").Where("article_id = ?", summary.Id)).Find(&summary.Authors)
summary.Tags = []string{}
database.GetDB().Table("article_tag").Select("tag").Where("article_id = ?", summary.Id).Find(&summary.Tags)
summary.Link = path.Join(config.SubPath, "article", summary.Link)
articleSummaries[i] = summary
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(articleSummaries)
}

231
backend/api/search_test.go Normal file
View File

@@ -0,0 +1,231 @@
package api
import (
"TheAdversary/database"
"TheAdversary/schema"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestSearch(t *testing.T) {
if err := initTestDatabase("search_test.sqlite3"); err != nil {
t.Fatal(err)
}
now := time.Now()
database.GetDB().Table("article").Create([]map[string]interface{}{
{
"title": "First article",
"created": now.Unix(),
"link": "first-article",
"markdown": "This is my first article",
"html": "<p>This is my first article</p>",
},
{
"title": "test",
"summary": "test summary",
"image": "https://upload.wikimedia.org/wikipedia/commons/0/05/Go_Logo_Blue.svg",
"created": now.Unix(),
"modified": now.Add(24 * time.Hour).Unix(),
"link": "test",
"markdown": "# Title",
"html": "<h1>Title</h1>",
},
{
"title": "owo",
"created": now.Unix(),
"modified": now.Add(12 * time.Hour).Unix(),
"link": "owo",
"markdown": "owo",
"html": "<p>owo<p>",
},
})
database.GetDB().Table("author").Create([]map[string]interface{}{
{
"name": "test",
"password": "",
},
{
"name": "hacr",
"password": "1234567890",
},
})
database.GetDB().Table("article_tag").Create([]map[string]interface{}{
{
"article_id": 1,
"tag": "example",
},
})
database.GetDB().Table("article_author").Create([]map[string]interface{}{
{
"article_id": 1,
"author_id": 1,
},
{
"article_id": 2,
"author_id": 1,
},
{
"article_id": 3,
"author_id": 2,
},
})
server := httptest.NewServer(http.HandlerFunc(Search))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodGet,
Query: map[string]interface{}{
"q": "first",
},
ResultBody: []schema.ArticleSummary{
{
Id: 1,
Title: "First article",
Created: now.Unix(),
Authors: []schema.Author{
{
Id: 1,
Name: "test",
},
},
Tags: []string{
"example",
},
Link: "article/first-article",
},
},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"from": now.Add(1 * time.Hour).Unix(),
"to": now.Add(10 * time.Hour).Unix(),
},
ResultBody: []schema.ArticleSummary{
{
Id: 2,
Title: "test",
Summary: "test summary",
Authors: []schema.Author{
{
Id: 1,
Name: "test",
},
},
Image: "https://upload.wikimedia.org/wikipedia/commons/0/05/Go_Logo_Blue.svg",
Created: now.Unix(),
Modified: now.Add(24 * time.Hour).Unix(),
Tags: []string{},
Link: "article/test",
},
{
Id: 3,
Title: "owo",
Authors: []schema.Author{
{
Id: 2,
Name: "hacr",
},
},
Created: now.Unix(),
Modified: now.Add(12 * time.Hour).Unix(),
Tags: []string{},
Link: "article/owo",
},
},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"authors": []int{2},
},
ResultBody: []schema.ArticleSummary{
{
Id: 3,
Title: "owo",
Authors: []schema.Author{
{
Id: 2,
Name: "hacr",
},
},
Created: now.Unix(),
Modified: now.Add(12 * time.Hour).Unix(),
Tags: []string{},
Link: "article/owo",
},
},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"tags": []string{"\"example\""},
},
ResultBody: []schema.ArticleSummary{
{
Id: 1,
Title: "First article",
Authors: []schema.Author{
{
Id: 1,
Name: "test",
},
},
Created: now.Unix(),
Tags: []string{
"example",
},
Link: "article/first-article",
},
},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"limit": 2,
},
ResultBody: []schema.ArticleSummary{
{
Id: 1,
Title: "First article",
Authors: []schema.Author{
{
Id: 1,
Name: "test",
},
},
Created: now.Unix(),
Tags: []string{
"example",
},
Link: "article/first-article",
},
{
Id: 2,
Title: "test",
Summary: "test summary",
Authors: []schema.Author{
{
Id: 1,
Name: "test",
},
},
Image: "https://upload.wikimedia.org/wikipedia/commons/0/05/Go_Logo_Blue.svg",
Created: now.Unix(),
Modified: now.Add(24 * time.Hour).Unix(),
Tags: []string{},
Link: "article/test",
},
},
Code: http.StatusOK,
},
})
}

31
backend/api/tags.go Normal file
View File

@@ -0,0 +1,31 @@
package api
import (
"TheAdversary/database"
"encoding/json"
"net/http"
"strconv"
)
func Tags(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
request := database.GetDB().Table("article_tag")
if query.Has("name") {
request.Where("tag LIKE ?", "%"+query.Get("name")+"%")
}
if query.Has("limit") {
limit, err := strconv.Atoi(query.Get("limit"))
if err != nil {
ApiError{"invalid 'limit' parameter", http.StatusUnprocessableEntity}.Send(w)
return
}
request.Limit(limit)
}
tags := make([]string, 0)
request.Distinct("tag").Find(&tags)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(tags)
}

81
backend/api/tags_test.go Normal file
View File

@@ -0,0 +1,81 @@
package api
import (
"TheAdversary/database"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestTags(t *testing.T) {
if err := initTestDatabase("tags_test.sqlite3"); err != nil {
t.Fatal(err)
}
tags := []string{
"test",
"linux",
}
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": "<p>Oh god i have to test all this, what am i doing with my life<p>",
},
})
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,
},
})
database.GetDB().Table("article_tag").Create([]map[string]interface{}{
{
"article_id": 1,
"tag": "test",
},
{
"article_id": 1,
"tag": "linux",
},
})
server := httptest.NewServer(http.HandlerFunc(Tags))
checkTestInformation(t, server.URL, []testInformation{
{
Method: http.MethodGet,
ResultBody: tags,
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"name": "abc",
},
ResultBody: []interface{}{},
Code: http.StatusOK,
},
{
Method: http.MethodGet,
Query: map[string]interface{}{
"limit": "notanumber",
},
ResultBody: map[string]interface{}{
"message": "invalid 'limit' parameter",
},
Code: http.StatusUnprocessableEntity,
},
})
}