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