4 Commits f7ec7fc4f2 ... 3bb36d170d

Tác giả SHA1 Thông báo Ngày
  aeth 3bb36d170d result check 2 tháng trước cách đây
  aeth a0c79e4e9d tests are not working 2 tháng trước cách đây
  aeth a51b189c4c no longer making duplicates. need to organize all my methods more. will do later 2 tháng trước cách đây
  aeth 4b4237ce8a its a mess but i have the client binary working. adding validation to the endpoints that add headers, table items, assets, etc. will continue later 2 tháng trước cách đây

+ 1 - 1
Makefile

@@ -16,7 +16,7 @@ format:
 	go fmt ./...
 
 test:
-	go test ./...
+	go test -v ./...
 
 
 coverage:

+ 39 - 11
cmd/keiji-ctl/keiji-ctl.go

@@ -7,10 +7,12 @@ import (
 	"fmt"
 	"io"
 	"log"
+	"mime/multipart"
 	"net/http"
 	"net/url"
 	"os"
 	"path"
+	"path/filepath"
 
 	"git.aetherial.dev/aeth/keiji/pkg/auth"
 	"git.aetherial.dev/aeth/keiji/pkg/controller"
@@ -18,6 +20,37 @@ import (
 	_ "github.com/mattn/go-sqlite3"
 )
 
+func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	body := &bytes.Buffer{}
+	writer := multipart.NewWriter(body)
+	part, err := writer.CreateFormFile(paramName, filepath.Base(path))
+	if err != nil {
+		return nil, err
+	}
+	_, err = io.Copy(part, file)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for key, val := range params {
+		_ = writer.WriteField(key, val)
+	}
+	err = writer.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest("POST", uri, body)
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+	return req, err
+}
+
 // authenticate and get the cookie needed to make updates
 func authenticate(url, username, password string) *http.Cookie {
 	client := http.Client{}
@@ -116,21 +149,16 @@ func main() {
 
 	case "nav":
 		fmt.Println(string(pngFile))
-		b, err := os.ReadFile(pngFile)
+		_, fileName := path.Split(pngFile)
+		extraParams := map[string]string{
+			"link":     fileName,
+			"redirect": redirect,
+		}
+		req, err := newfileUploadRequest(address+"/admin/navbar", extraParams, "file", pngFile)
 		if err != nil {
 			log.Fatal(err)
 		}
-		_, fileName := path.Split(pngFile)
-		fmt.Println(fileName)
-		item := storage.NavBarItem{
-			Link:     fileName,
-			Redirect: redirect,
-			Png:      b,
-		}
-		data, _ := json.Marshal(item)
-		req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/admin/navbar", address), bytes.NewReader(data))
 		req.AddCookie(prepareCookie(address))
-		req.Header.Add("Content-Type", "application/json")
 		resp, err := client.Do(req)
 		if err != nil {
 			fmt.Println("There was an error performing the desired request: ", err.Error())

+ 1 - 0
cmd/keiji/keiji.go

@@ -68,6 +68,7 @@ func main() {
 		"login",
 		"admin",
 		"blogpost_editor",
+		"navbar_editor",
 		"post_options",
 		"unhandled_error",
 		"upload",

+ 72 - 25
pkg/controller/admin_handlers.go

@@ -2,6 +2,7 @@ package controller
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -128,32 +129,53 @@ func (c *Controller) AddMenuItem(ctx *gin.Context) {
 @Router /admin/navbar
 */
 func (c *Controller) AddNavbarItem(ctx *gin.Context) {
+	file, err := ctx.FormFile("file")
+	if err != nil {
+		ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
+		return
+	}
+	fh, err := file.Open()
+	if err != nil {
+		ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
+		return
+	}
+	fb := make([]byte, file.Size)
+	var output bytes.Buffer
+	for {
+		n, err := fh.Read(fb)
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
+			return
+		}
+		output.Write(fb[:n])
+	}
 
 	var item storage.NavBarItem
-	err := ctx.ShouldBind(&item)
+	item = storage.NavBarItem{
+		Link: file.Filename,
+		Png:  fb,
+	}
+	err = ctx.ShouldBind(&item)
 	if err != nil {
-		ctx.JSON(400, map[string]string{
-			"Error": err.Error(),
-		})
+		ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
 		return
 	}
 	err = c.database.AddNavbarItem(item)
 	if err != nil {
-		ctx.JSON(400, map[string]string{
-			"Error": err.Error(),
-		})
+		ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
 		return
 	}
 
-	err = c.database.AddAsset(item.Link, item.Png)
+	err = c.database.AddAsset(item.File.Filename, fb)
 	if err != nil {
-		ctx.JSON(400, map[string]string{
-			"Error": err.Error(),
-		})
+		ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
 		return
 	}
 
-	ctx.Data(200, "text", []byte("navbar item added."))
+	ctx.HTML(200, "upload_status", gin.H{"UpdateMessage": "Addition Successful!", "Color": "green"})
 }
 
 /*
@@ -163,20 +185,17 @@ func (c *Controller) AddNavbarItem(ctx *gin.Context) {
 @Router /admin/navbar
 */
 func (c *Controller) RemoveNavbarItem(ctx *gin.Context) {
-	itemName := ctx.Param("item_name")
-	err := c.database.RemoveNavbarItem(storage.Identifier(itemName))
+	itemName, ok := ctx.Params.Get("item_name")
+	if !ok {
+		ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": errors.New("No navbar name passed."), "Color": "red"})
+		return
+	}
+	err := c.database.DeleteNavbarItem(storage.Identifier(itemName))
 	if err != nil {
-		ctx.JSON(400, map[string]string{
-			"name":   itemName,
-			"status": "FAIL",
-			"Error":  err.Error(),
-		})
+		ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
 		return
 	}
-	ctx.JSON(200, map[string]string{
-		"name":   itemName,
-		"status": "SUCCESS",
-	})
+	ctx.HTML(200, "upload_status", gin.H{"UpdateMessage": "Deleted '" + itemName + "' Successfully!", "Color": "green"})
 
 }
 
@@ -184,10 +203,10 @@ func (c *Controller) ServeNavbarModify(ctx *gin.Context) {
 	navbar := c.database.GetNavBarLinks()
 	tableData := storage.AdminPage{Tables: map[string][]storage.TableData{}}
 	for i := range navbar {
-		tableData.Tables[storage.Topics[i]] = append(tableData.Tables[storage.Topics[i]],
+		tableData.Tables["items"] = append(tableData.Tables["items"],
 			storage.TableData{
 				DisplayName: navbar[i].Link,
-				Link:        fmt.Sprintf("/admin/options/%s", navbar[i].Link),
+				Link:        fmt.Sprintf("/admin/navbar/options/%s", navbar[i].Link),
 			},
 		)
 	}
@@ -337,6 +356,20 @@ func (c *Controller) ServeNewBlogPage(ctx *gin.Context) {
 	})
 }
 
+/*
+Serving the new blogpost page. Serves the editor with the method to POST a new document
+*/
+func (c *Controller) ServeNewNavbarItem(ctx *gin.Context) {
+
+	ctx.HTML(200, "navbar_editor", gin.H{
+		"navigation": gin.H{
+			"menu":    c.database.GetDropdownElements(),
+			"headers": c.database.GetNavBarLinks(),
+		},
+		"Post": true,
+	})
+}
+
 /*
 Reciever for the ServeNewBlogPage UI screen. Adds a new document to the database
 */
@@ -409,6 +442,20 @@ func (c *Controller) SaveFile(ctx *gin.Context) {
 	ctx.HTML(200, "upload_status", gin.H{"UpdateMessage": "Update Successful!", "Color": "green"})
 }
 
+// Serve the document deletion template
+func (c *Controller) NavbarOptions(ctx *gin.Context) {
+	id, found := ctx.Params.Get("id")
+	if !found {
+		ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": "No ID selected!", "Color": "red"})
+		return
+	}
+
+	ctx.HTML(200, "post_options", gin.H{
+		"Link": fmt.Sprintf("/admin/navbar/%s", id),
+	})
+
+}
+
 // Serve the document deletion template
 func (c *Controller) PostOptions(ctx *gin.Context) {
 	id, found := ctx.Params.Get("id")

+ 2 - 0
pkg/routes/register.go

@@ -43,6 +43,8 @@ func Register(e *gin.Engine, domain string, database storage.DocumentIO, files f
 	priv.PATCH("/posts", c.UpdateBlogPost)
 	priv.DELETE("/posts/:id", c.DeleteDocument)
 	priv.DELETE("/navbar/:item_name", c.RemoveNavbarItem)
+	priv.GET("/navbar/new", c.ServeNewNavbarItem)
+	priv.GET("/navbar/options/:id", c.NavbarOptions)
 	priv.GET("/navbar/all", c.ServeNavbarModify)
 
 }

+ 97 - 5
pkg/storage/storage.go

@@ -57,9 +57,10 @@ type LinkPair struct {
 }
 
 type NavBarItem struct {
-	Png      []byte `json:"png"`
-	Link     string `json:"link"`
-	Redirect string `json:"redirect"`
+	Png      []byte                `json:"png"`
+	File     *multipart.FileHeader `form:"file"`
+	Link     string                `json:"link" form:"link"`
+	Redirect string                `json:"redirect" form:"redirect"`
 }
 
 type Asset struct {
@@ -224,6 +225,71 @@ func (s *SQLiteRepo) GetDropdownElements() []LinkPair {
 
 }
 
+/*
+Retrieve a dropdown element by its text name on the UI
+*/
+func (s *SQLiteRepo) GetMenuItemByName(link, text string) (LinkPair, bool) {
+	rows := s.db.QueryRow("SELECT * FROM menu WHERE link = ? AND text = ?", link, text)
+	var item LinkPair
+	var id int
+	if err := rows.Scan(&id, &item.Link, &item.Text); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return item, false
+		}
+	}
+	log.Printf("%+v\n", item)
+	return item, true
+
+}
+
+// get Admin table entry by its display name, link, and category.
+func (s *SQLiteRepo) GetAdminTableEntry(displayName, link, category string) (TableData, bool) {
+	rows := s.db.QueryRow("SELECT * FROM admin WHERE display_name = ? AND link = ? AND category = ?", displayName, link, category)
+	var item TableData
+	var id int
+	if err := rows.Scan(&id, &item.DisplayName, &item.Link, &category); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return item, false
+		}
+		log.Fatal(err)
+	}
+	return item, true
+
+}
+
+// get navbar entry.
+func (s *SQLiteRepo) GetNavbarLink(link, redirect string) (NavBarItem, bool) {
+	rows := s.db.QueryRow("SELECT * FROM navbar WHERE link = ? AND redirect = ?", link, redirect)
+	var item NavBarItem
+	var id int
+	if err := rows.Scan(&id, &item.Png, &item.Link, &item.Redirect); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return item, false
+		}
+		log.Fatal(err)
+	}
+	return item, true
+
+}
+
+// get an asset from the store
+func (s *SQLiteRepo) GetAsset(name string) (Asset, bool) {
+	rows := s.db.QueryRow("SELECT * FROM assets WHERE name = ?", name)
+	var item Asset
+	var id int
+	if err := rows.Scan(&id, &item.Name, &item.Data); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return item, false
+		}
+		log.Fatal(err)
+	}
+	return item, true
+
+}
+
+/*
+Get all nav bar items. Returns a list of NavBarItem structs with the png data, the file name, and the redirect location of the icon
+
 /*
 Get all nav bar items. Returns a list of NavBarItem structs with the png data, the file name, and the redirect location of the icon
 */
@@ -446,6 +512,11 @@ func (s *SQLiteRepo) AddMenuItem(item LinkPair) error {
 	if err != nil {
 		return err
 	}
+	_, found := s.GetMenuItemByName(item.Link, item.Text)
+	if found {
+		tx.Rollback()
+		return ErrDuplicate
+	}
 	stmt, _ := tx.Prepare("INSERT INTO menu(link, text) VALUES (?,?)")
 	_, err = stmt.Exec(item.Link, item.Text)
 	if err != nil {
@@ -467,6 +538,11 @@ func (s *SQLiteRepo) AddNavbarItem(item NavBarItem) error {
 	if err != nil {
 		return err
 	}
+	_, found := s.GetNavbarLink(item.Link, item.Redirect)
+	if found {
+		tx.Rollback()
+		return ErrDuplicate
+	}
 	stmt, err := tx.Prepare("INSERT INTO navbar(png, link, redirect) VALUES (?,?,?)")
 	if err != nil {
 		tx.Rollback()
@@ -493,6 +569,11 @@ func (s *SQLiteRepo) AddAsset(name string, data []byte) error {
 	if err != nil {
 		return err
 	}
+	_, found := s.GetAsset(name)
+	if found {
+		tx.Rollback()
+		return ErrDuplicate
+	}
 	stmt, _ := tx.Prepare("INSERT INTO assets(name, data) VALUES (?,?)")
 	_, err = stmt.Exec(name, data)
 	if err != nil {
@@ -536,6 +617,11 @@ func (s *SQLiteRepo) AddAdminTableEntry(item TableData, category string) error {
 	if err != nil {
 		return err
 	}
+	_, found := s.GetAdminTableEntry(item.DisplayName, item.Link, category)
+	if found {
+		tx.Rollback()
+		return ErrDuplicate
+	}
 	stmt, _ := tx.Prepare("INSERT INTO admin (display_name, link, category) VALUES (?,?,?)")
 	_, err = stmt.Exec(item.DisplayName, item.Link, category)
 	if err != nil {
@@ -579,12 +665,18 @@ func (s *SQLiteRepo) DeleteNavbarItem(id Identifier) error {
 	if err != nil {
 		return err
 	}
-	stmt, _ := tx.Prepare("DELETE FROM navbar WHERE redirect=?")
-	_, err = stmt.Exec(id)
+	stmt, _ := tx.Prepare("DELETE FROM navbar WHERE link=?")
+	result, err := stmt.Exec(id)
 	if err != nil {
 		tx.Rollback()
 		return err
 	}
+	rowsAffected, _ := result.RowsAffected()
+	if rowsAffected < 1 {
+		return ErrNotExists
+
+	}
+
 	tx.Commit()
 	return nil
 

+ 8 - 13
pkg/storage/storage_test.go

@@ -40,7 +40,7 @@ const badAdminTable = `
 	);
 	`
 
-var unpopulatedTables = []string{badPostsTable, badImagesTable, badMenuItemsTable, badMenuItemsTable, badAssetTable, badAdminTable}
+var unpopulatedTables = []string{badPostsTable, badImagesTable, badNavbarItemsTable, badMenuItemsTable, badAssetTable, badAdminTable}
 
 /*
 creates in memory db and SQLiteRepo struct
@@ -548,7 +548,7 @@ func TestAddMenuItem(t *testing.T) {
 		input []LinkPair
 		err   error
 	}
-	testDb, db := newTestDb(t.TempDir(), true)
+	testDb, _ := newTestDb(t.TempDir(), true)
 	for _, tc := range []testcase{
 		{
 			input: []LinkPair{
@@ -563,19 +563,14 @@ func TestAddMenuItem(t *testing.T) {
 		for i := range tc.input {
 			err := testDb.AddMenuItem(tc.input[i])
 			if err != nil {
+				// assert.Equal(expected, actual)
 				assert.Equal(t, tc.err, err)
 			}
-			rows, err := db.Query("SELECT * FROM menu")
-			var got []LinkPair
-			defer rows.Close()
-			for rows.Next() {
-				var id int
-				var item LinkPair
-				err = rows.Scan(&id, &item.Link, &item.Text)
-				if err != nil {
-					log.Fatal(err)
-				}
-				got = append(got, item)
+			rows := testDb.db.QueryRow("SELECT * FROM menu WHERE link = ? AND text = ?", tc.input[i].Link, tc.input[i].Text)
+			var got LinkPair
+			var id int
+			if err := rows.Scan(&id, &got.Link, &got.Text); err != nil {
+				t.Errorf("failed: %s", err.Error())
 			}
 			assert.Equal(t, tc.input, got)
 		}

+ 48 - 0
pkg/webpages/html/navbar_editor.html

@@ -0,0 +1,48 @@
+
+{{ define "patch_navbar_editor" }}
+<form hx-encoding='multipart/form-data' hx-patch='/admin/navbar'
+                 _='on htmx:xhr:progress(loaded, total) set #progress.value to (loaded/total)*100'>
+{{ end }}
+
+{{ define "post_navbar_editor" }}
+<form hx-encoding='multipart/form-data' hx-post='/admin/navbar'
+                 _='on htmx:xhr:progress(loaded, total) set #progress.value to (loaded/total)*100'>
+{{ end }}
+
+
+{{ define "navbar_editor" }}
+<!DOCTYPE html>
+<html lang="en">
+    <div class="container-fluid p-2 position-relative"
+        style="width: 80vw; max-width: 80%; background-color: rgb(22, 22, 22);">
+        <div class="container">
+            <div class="row">
+                <div class="col-sm"></div>
+                <div class="container position-relative">
+                    <a style="color: white; height: fit-content; font-size: xx-large; font-weight: bold; font-family: monospace;">Editing/Making navbar item</a>
+                    <div class="col m-5">
+                        {{ if $.Post }}
+                            {{ template "post_navbar_editor" }}
+                        {{ else }}
+                            {{ template "patch_navbar_editor" }}
+                        {{ end }}
+                            <div class="row"
+                                style="background-color: rgb(22, 22, 22); color: white; height: fit-content; font-size: larger; font-family: monospace;">
+                                <a>Redirect to:</a>
+                                <textarea name="redirect"
+                                    style="background-color: rgb(73, 73, 73); color: white;">{{ .Redirect }}</textarea>
+                            </div>
+                            <div class="row container p-2 m-2">
+                                <a>Add PNG:</a>
+                                <input type='file' name='file'>
+                            </div>
+                            <button type="submit">Send</button><div id="response"></div>
+                        </form>
+                    </div>
+                </div>
+                <div class="col-sm"></div>
+            </div>
+        </div>
+    </div>
+</html>
+{{ end }}

+ 2 - 1
scripts/seed.py

@@ -15,7 +15,8 @@ URL = os.getenv("SITE_URL")
 admin_table = {
         "new": {
             "blog post": "/admin/posts",
-            "digital media": "/admin/upload"
+            "digital media": "/admin/upload",
+            "navbar item": "/admin/navbar/new"
             },
         "modify": {
             "blog post": "/admin/posts/all",