note editing implemented
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
{{define "Form"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>NoteHub — Add note</title>
|
||||
<title>NoteHub — {{if .ID}}Edit{{else}}Add{{end}} note</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<link href="/style.css" rel="stylesheet" type="text/css" />
|
||||
@@ -9,13 +10,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<form action="/note" autocomplete="off" method="POST" target="_self">
|
||||
<textarea autofocus name="text"></textarea>
|
||||
<textarea autofocus name="text">{{.Text}}</textarea>
|
||||
<fieldset>
|
||||
<input class="ui-elem"
|
||||
name="plain-password"
|
||||
placeholder="Password for editing"
|
||||
type="text"
|
||||
autocomplete="off">
|
||||
<input id="id" name="id" value="{{.ID}}" type="hidden" />
|
||||
<input class="ui-elem" id="password" name="password" placeholder="Password for editing" type="text" autocomplete="off">
|
||||
<label style="margin-right: 1em">
|
||||
<input id="tos" name="tos" type="checkbox" onClick="toggleButton()">
|
||||
Accept <a href="/TOS.md">Terms of Service</a>
|
||||
@@ -33,3 +31,4 @@
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -1,8 +1,8 @@
|
||||
{{define "Stats"}}
|
||||
<h2>Statistics</h2>
|
||||
<table>
|
||||
<tr><td>Published</td><td>{{.Published}}</td></tr>
|
||||
<tr><td>Edited</td><td>{{.Edited}}</td></tr>
|
||||
<tr><td>Published</td><td>{{.Published.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr>
|
||||
<tr><td>Edited</td><td>{{.Edited.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr>
|
||||
<tr><td>Views</td><td>{{.Views}}</td></tr>
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
@@ -35,21 +35,24 @@ func main() {
|
||||
e.File("/robots.txt", "assets/public/robots.txt")
|
||||
e.File("/style.css", "assets/public/style.css")
|
||||
e.File("/index.html", "assets/public/index.html")
|
||||
e.File("/new", "assets/public/new.html")
|
||||
e.File("/", "assets/public/index.html")
|
||||
|
||||
e.GET("/TOS.md", func(c echo.Context) error {
|
||||
n, code := md2html(c, "TOS")
|
||||
return c.Render(code, "Page", n)
|
||||
})
|
||||
|
||||
e.GET("/:id", func(c echo.Context) error {
|
||||
n, code := load(c, db)
|
||||
n.prepare()
|
||||
return c.Render(code, "Note", n)
|
||||
})
|
||||
|
||||
e.GET("/:id/export", func(c echo.Context) error {
|
||||
n, code := load(c, db)
|
||||
return c.String(code, n.Text)
|
||||
})
|
||||
|
||||
e.GET("/:id/stats", func(c echo.Context) error {
|
||||
n, code := load(c, db)
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
@@ -58,6 +61,15 @@ func main() {
|
||||
return c.Render(code, "Note", n)
|
||||
})
|
||||
|
||||
e.GET("/:id/edit", func(c echo.Context) error {
|
||||
n, code := load(c, db)
|
||||
return c.Render(code, "Form", n)
|
||||
})
|
||||
|
||||
e.GET("/new", func(c echo.Context) error {
|
||||
return c.Render(http.StatusOK, "Form", nil)
|
||||
})
|
||||
|
||||
e.POST("/note", func(c echo.Context) error {
|
||||
vals, err := c.FormParams()
|
||||
if err != nil {
|
||||
@@ -73,18 +85,25 @@ func main() {
|
||||
return c.Render(code, "Note",
|
||||
errPage(code, "note length not accepted"))
|
||||
}
|
||||
n := Note{
|
||||
id := get(vals, "id")
|
||||
n := &Note{
|
||||
ID: id,
|
||||
Text: text,
|
||||
Password: get(vals, "password"),
|
||||
}
|
||||
id, err := save(c, db, &n)
|
||||
n, err = save(c, db, n)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
code := http.StatusServiceUnavailable
|
||||
if err == errorUnathorised {
|
||||
code = http.StatusUnauthorized
|
||||
} else if err == errorBadRequest {
|
||||
code = http.StatusBadRequest
|
||||
}
|
||||
return c.Render(code, "Note", errPage(code))
|
||||
}
|
||||
c.Logger().Infof("new note %q created", n.ID)
|
||||
return c.Redirect(http.StatusMovedPermanently, "/"+id)
|
||||
c.Logger().Infof("note %q saved", n.ID)
|
||||
return c.Redirect(http.StatusMovedPermanently, "/"+n.ID)
|
||||
})
|
||||
|
||||
e.Logger.Fatal(e.Start(":3000"))
|
||||
|
||||
+48
-8
@@ -2,7 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
@@ -24,12 +26,17 @@ const idLength = 5
|
||||
var (
|
||||
errorCodes = map[int]string{
|
||||
400: "Bad request",
|
||||
401: "Unauthorized",
|
||||
404: "Not found",
|
||||
412: "Precondition failed",
|
||||
503: "Service unavailable",
|
||||
}
|
||||
rexpNewLine = regexp.MustCompile("[\n\r]")
|
||||
rexpNonAlphaNum = regexp.MustCompile("[`~!@#$%^&*_|+=?;:'\",.<>{}\\/]")
|
||||
rexpNoScriptIframe = regexp.MustCompile("<.*?(iframe|script).*?>")
|
||||
|
||||
errorUnathorised = errors.New("id or password is wrong")
|
||||
errorBadRequest = errors.New("password is empty")
|
||||
)
|
||||
|
||||
type Note struct {
|
||||
@@ -50,35 +57,69 @@ func errPage(code int, details ...string) Note {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Note) render() {
|
||||
func (n *Note) prepare() {
|
||||
fstLine := rexpNewLine.Split(n.Text, -1)[0]
|
||||
maxLength := 25
|
||||
if len(fstLine) < 25 {
|
||||
maxLength = len(fstLine)
|
||||
}
|
||||
n.Text = rexpNoScriptIframe.ReplaceAllString(n.Text, "")
|
||||
n.Title = strings.TrimSpace(rexpNonAlphaNum.ReplaceAllString(fstLine[:maxLength], ""))
|
||||
n.Password = ""
|
||||
n.Content = mdTmplHTML([]byte(n.Text))
|
||||
}
|
||||
|
||||
func save(c echo.Context, db *sql.DB, n *Note) (string, error) {
|
||||
func save(c echo.Context, db *sql.DB, n *Note) (*Note, error) {
|
||||
if n.Password != "" {
|
||||
n.Password = fmt.Sprintf("%x", sha256.Sum256([]byte(n.Password)))
|
||||
}
|
||||
if n.ID == "" {
|
||||
return insert(c, db, n)
|
||||
}
|
||||
return update(c, db, n)
|
||||
}
|
||||
|
||||
func update(c echo.Context, db *sql.DB, n *Note) (*Note, error) {
|
||||
if n.Password == "" {
|
||||
return nil, errorBadRequest
|
||||
}
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
stmt, _ := tx.Prepare("update notes set (text, edited) = (?, ?) where id = ? and password = ?")
|
||||
defer stmt.Close()
|
||||
res, err := stmt.Exec(n.Text, time.Now(), n.ID, n.Password)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if rows != 1 {
|
||||
tx.Rollback()
|
||||
return nil, errorUnathorised
|
||||
}
|
||||
return n, tx.Commit()
|
||||
}
|
||||
|
||||
func insert(c echo.Context, db *sql.DB, n *Note) (*Note, error) {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stmt, _ := tx.Prepare("insert into notes(id, text, password) values(?, ?, ?)")
|
||||
defer stmt.Close()
|
||||
id := randId()
|
||||
_, err = stmt.Exec(id, n.Text, n.Password)
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") {
|
||||
tx.Rollback()
|
||||
if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") {
|
||||
c.Logger().Infof("collision on id %q", id)
|
||||
return save(c, db, n)
|
||||
}
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
return id, tx.Commit()
|
||||
n.ID = id
|
||||
return n, tx.Commit()
|
||||
}
|
||||
|
||||
func randId() string {
|
||||
@@ -119,7 +160,6 @@ func load(c echo.Context, db *sql.DB) (Note, int) {
|
||||
Published: published,
|
||||
Edited: edited,
|
||||
}
|
||||
n.render()
|
||||
return *n, http.StatusOK
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user