sophuwu.site > myweb
added managment hander with auth
can edit index, upload media and convert to webp, delete media
sophuwu sophie@skisiel.com
Wed, 11 Dec 2024 10:32:00 +0100
commit

894a3330e701ec43a09b96721b19c237dc9bd329

parent

f5402b22680729161273f7b81cbae4a7d39840d9

M .gitignore.gitignore

@@ -2,3 +2,5 @@ build/

webhome/*.sqlite webhome/*.db webhome/media/* +webhome/*.log +webhome/userpass
M blogs.goblogs.go

@@ -28,7 +28,7 @@ title = strings.ReplaceAll(title, " ", "-")

return filepath.Join(date, url.PathEscape(title)) } -func NewBlog(title, desc, body string, date ...string) error { +func SaveBlog(title, desc, body string, date ...string) error { if len(date) == 0 { date = append(date, time.Now().Format("2006-01-02")) }
M config/config.goconfig/config.go

@@ -5,19 +5,48 @@ "bytes"

"log" "os" "path/filepath" + "time" ) var ( - ListenAddr string - WebRoot string - StaticPath string - MediaPath string - Templates string - DBPath string - Email string - Name string - URL string + ListenAddr string + WebRoot string + StaticPath string + MediaPath string + Templates string + DBPath string + Email string + Name string + URL string + passHash str + passLoadTime time.Time ) + +type str string + +func (s str) Bytes() []byte { + return []byte(s) +} +func (s str) String() string { + return string(s) + +} + +func PassHash() str { + if time.Since(passLoadTime) > 5*time.Minute { + passLoad() + } + return passHash +} + +func passLoad() { + b, err := os.ReadFile(path("userpass")) + if err != nil { + log.Fatalf("Error reading userpass: %v", err) + } + passHash = str(bytes.TrimSpace(b)) + passLoadTime = time.Now() +} func path(p string) string { return filepath.Join(WebRoot, p)

@@ -62,5 +91,5 @@ DBPath = path("data.db")

Email = mm["email"] Name = mm["name"] URL = mm["url"] - + passLoad() }
M db.godb.go

@@ -1,10 +1,13 @@

package main import ( + "encoding/json" "github.com/asdine/storm/v3" "log" + "net/http" "sophuwu.site/myweb/config" "sophuwu.site/myweb/template" + "strings" ) var DB *storm.DB

@@ -33,3 +36,75 @@

func SetPageData(page string, data template.HTMLDataMap) error { return DB.Set("pages", page, data) } + +func EditIndex(w http.ResponseWriter, r *http.Request) { + var err error + if r.Method == "GET" && r.URL.Path == "/manage/edit/" { + var d template.HTMLDataMap + d, err = GetPageData("index") + var b []byte + b, err = json.MarshalIndent(d, "", " ") + if CheckHttpErr(err, w, r, 500) { + return + } + bb := string(b) + data := template.Data("Edit index", "Edit the index page") + data.Set("Data", bb) + err = template.Use(w, r, "edit", data) + CheckHttpErr(err, w, r, 500) + return + } else if r.Method == "POST" && r.URL.Path == "/manage/edit/save" { + err = r.ParseForm() + if CheckHttpErr(err, w, r, 500) { + return + } + var bb string + bb = r.Form.Get("data") + if bb == "" { + HttpErr(w, r, 400) + return + } + var d template.HTMLDataMap + err = json.Unmarshal([]byte(bb), &d) + if CheckHttpErr(err, w, r, 400) { + return + } + err = SetPageData("index", d) + if CheckHttpErr(err, w, r, 400) { + return + } + http.Redirect(w, r, "/", 302) + return + } + HttpErr(w, r, 405) +} + +func ManagerHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/manage/" { + data := template.Data("Manage", config.URL) + data["Options"] = []struct{ Name, URL string }{ + {"Edit index", "/manage/edit/"}, + {"Upload media", "/manage/media/"}, + {"Delete media", "/manage/delete/media/"}, + {"Post blog", "/manage/blog/"}, + {"Add Animation", "/manage/animation/"}, + {"Backup", "/manage/backup/"}, + } + err := template.Use(w, r, "manage", data) + CheckHttpErr(err, w, r, 500) + return + } + if r.URL.Path == "/manage/edit/" || r.URL.Path == "/manage/edit/save" { + EditIndex(w, r) + return + } + if r.URL.Path == "/manage/media/" { + ManageMedia(w, r) + return + } + if strings.HasPrefix(r.URL.Path, "/manage/delete/media/") { + DeleteMedia(w, r) + return + } + HttpErr(w, r, 404) +}
M go.modgo.mod

@@ -5,5 +5,8 @@

require ( github.com/asdine/storm/v3 v3.2.1 go.etcd.io/bbolt v1.3.4 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 ) + +require github.com/stretchr/testify v1.7.0 // indirect
M go.sumgo.sum

@@ -4,6 +4,7 @@ github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=

github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac= github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

@@ -16,12 +17,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=

github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20191105084925-a882066a44e0 h1:QPlSTtPE2k6PZPasQUbzuK3p9JbS+vMXYVto8g/yrsg=

@@ -34,4 +38,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
M main.gomain.go

@@ -4,6 +4,7 @@ import (

"context" "errors" "fmt" + "golang.org/x/crypto/bcrypt" "golang.org/x/sys/unix" "log" "net/http"

@@ -11,6 +12,7 @@ "os"

"os/signal" "sophuwu.site/myweb/config" "sophuwu.site/myweb/template" + "strings" ) func CheckHttpErr(err error, w http.ResponseWriter, r *http.Request, code int) bool {

@@ -23,9 +25,11 @@ return false

} var HttpErrs = map[int]string{ + 400: "Bad request: the server could not understand your request. Please check the URL and try again.", 401: "Unauthorized: You must log in to access this page.", 403: "Forbidden: You do not have permission to access this page.", 404: "Not found: the requested page does not exist. Please check the URL and try again.", + 405: "Method not allowed: the requested method is not allowed on this page.", 500: "Internal server error: the server encountered an error while processing your request. Please try again later.", }

@@ -55,6 +59,7 @@ d, err := GetPageData("index")

if CheckHttpErr(err, w, r, 500) { return } + d.Set("Image", strings.TrimSuffix(config.URL, "/")+d["ImagePath"].(string)) err = template.Use(w, r, "index", d) _ = CheckHttpErr(err, w, r, 500) }

@@ -63,6 +68,25 @@ func HttpFS(path, fspath string) {

http.Handle(path, http.StripPrefix(path, http.FileServer(http.Dir(fspath)))) } +type Profile struct { + Icon string + Website string + URL string + User string +} + +func Authenticate(next http.HandlerFunc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, apass, authOK := r.BasicAuth() + if !authOK || bcrypt.CompareHashAndPassword(config.PassHash().Bytes(), []byte(user+":"+apass)) != nil { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "Unauthorized.", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) +} + func main() { OpenDB() err := template.Init(config.Templates)

@@ -74,7 +98,7 @@ http.HandleFunc("/", HttpIndex)

http.HandleFunc("/blog/", BlogHandler) HttpFS("/static/", config.StaticPath) http.HandleFunc("/media/", MediaHandler) - // HttpFS("/media/", config.MediaPath) + http.Handle("/manage/", Authenticate(ManagerHandler)) server := http.Server{Addr: config.ListenAddr, Handler: nil} go func() {
M media.gomedia.go

@@ -1,9 +1,13 @@

package main import ( + "bytes" + "fmt" "go.etcd.io/bbolt" + "io" "mime" "net/http" + "os/exec" "path/filepath" "sophuwu.site/myweb/template" "strings"

@@ -25,22 +29,28 @@ f.Name = name

f.Size = size } +func ListMedia() ([]DBFile, error) { + var list []DBFile + var t DBFile + err := DB.Bolt.View(func(tx *bbolt.Tx) error { + b := tx.Bucket([]byte("media")) + return b.ForEach(func(k, v []byte) error { + t.Set(string(k), len(v)) + if t.Valid() { + list = append(list, t) + } + return nil + }) + }) + return list, err +} + func MediaHandler(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/media/") var err error if path == "" { var list []DBFile - var t DBFile - err = DB.Bolt.View(func(tx *bbolt.Tx) error { - b := tx.Bucket([]byte("media")) - return b.ForEach(func(k, v []byte) error { - t.Set(string(k), len(v)) - if t.Valid() { - list = append(list, t) - } - return nil - }) - }) + list, err = ListMedia() if CheckHttpErr(err, w, r, 500) { return }

@@ -60,3 +70,96 @@ w.WriteHeader(200)

w.Header().Set("content-type", mime.TypeByExtension(filepath.Ext(path))) w.Write(data) } + +func AddMedia(path string, data []byte) error { + return DB.SetBytes("media", path, data) +} + +func ConvWebp(f io.Reader) (bytes.Buffer, error) { + cmd := exec.Command("convert", "-", "webp:-") + var data bytes.Buffer + cmd.Stdin = f + cmd.Stdout = &data + err := cmd.Run() + return data, err +} + +func ManageMedia(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/manage/media/" { + HttpErr(w, r, 404) + return + } + if r.Method == "GET" { + d := template.Data("Manage media", "Upload media files") + d.Set("Media", "true") + err := template.Use(w, r, "edit", d) + CheckHttpErr(err, w, r, 500) + return + } + if r.Method == "POST" { + + err := r.ParseMultipartForm(10 << 20) + if CheckHttpErr(err, w, r, 500) { + return + } + + fh, h, err := r.FormFile("file1") + if CheckHttpErr(err, w, r, 500) { + return + } + defer fh.Close() + f := io.Reader(fh) + ext := filepath.Ext(h.Filename) + ext = strings.TrimPrefix(ext, ".") + name := filepath.Base(h.Filename) + var data bytes.Buffer + if ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "gif" { + // convert to webp + name = strings.TrimSuffix(name, ext) + if !strings.HasSuffix(name, ".webp") { + name += ".webp" + } + name = strings.ReplaceAll(name, " ", "-") + name = strings.ReplaceAll(name, "..", ".") + data, err = ConvWebp(f) + } else { + _, err = data.ReadFrom(f) + } + if CheckHttpErr(err, w, r, 500) { + return + } + // add to db + err = AddMedia(name, data.Bytes()) + if CheckHttpErr(err, w, r, 500) { + return + } + http.Redirect(w, r, "/media/", 302) + } +} + +func DeleteMedia(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/manage/delete/media/") + if path == "" { + list, err := ListMedia() + if CheckHttpErr(err, w, r, 500) { + return + } + d := template.Data("Delete media", "Delete media files") + d.Set("Files", list) + d.Set("NoFiles", len(list)) + err = template.Use(w, r, "filelist", d) + CheckHttpErr(err, w, r, 500) + return + } + conf := r.URL.Query().Get("confirm") + if conf == "true" { + err := DB.Delete("media", path) + if CheckHttpErr(err, w, r, 500) { + return + } + http.Redirect(w, r, "/media/", 302) + } + w.Header().Set("content-type", "text/html") + w.WriteHeader(200) + fmt.Fprintf(w, "Are you sure you want to delete %s?<br><a href=\"/manage/delete/media/%s?confirm=true\">Yes</a><br><a href=\"/\">No</a>", path, path) +}
M webhome/static/style_dark.csswebhome/static/style_dark.css

@@ -18,7 +18,14 @@ }

button:active { box-shadow: none; } - +textarea { + background-color: #111111; + border: 2px solid #aaaaaa; +} +input { + background-color: #111111; + border: 2px solid #aaaaaa; +} button { background-color: rgba(0, 0, 0, 0.25); border: 3px solid transparent;
M webhome/static/style_main.csswebhome/static/style_main.css

@@ -126,6 +126,34 @@ border-bottom-width: 1px;

width: calc(100% + 1rem); transform: translateX(-0.5rem); } +.about-me { + flex-wrap: wrap; + display: flex; + flex-direction: row-reverse; + justify-content: center; + align-items: center; +} +.about-me > div { + @media (max-width: 800px) { + max-width: 100%; + width: 100%; + padding: 0; + margin: 10px auto; + } + @media (min-width: 800px) { + max-width: calc(100% - 240px); + width: 100%; + padding: 0; + margin: 10px auto; + } + +} +.about-me img { + width: 200px; + height: auto; + margin: 10px 10px 10px 0; + border-radius: 1em; +} button:hover { transform: translateY(-2px);
A webhome/templates/edit.html

@@ -0,0 +1,27 @@

+{{ define "edit" }} +<html lang="en"> +{{ template "head" . }} +<body> +{{ template "nav" . }} +<main> + <h2>{{ .Title }}</h2> + <p> + {{ .Description }} + </p> + <br> + {{ if .Media }} + <form action="/manage/media/" method="post" style="width: 100%;" enctype="multipart/form-data"> + <input type="file" name="file1" id="file1"> + <input type="submit"> + </form> + {{ else }} + <form action="/manage/edit/save" method="post" style="width: 100%;"> + <textarea name="data" rows="30" cols="100" style="width: 100%;">{{ .Data }}</textarea> + <br> + <input type="submit" value="Save"> + </form> + {{ end }} +</main> +</body> +</html> +{{ end }}
M webhome/templates/filelist.htmlwebhome/templates/filelist.html

@@ -13,10 +13,6 @@ {{ .NoFiles }} files found.

</p> <hr> <table> - <tr> - <td>Name</td> - <td>Size</td> - </tr> {{- range .Files -}} <tr> <td><a href="{{- .Name -}}">{{- .Name -}}</a></td>
M webhome/templates/index.htmlwebhome/templates/index.html

@@ -4,7 +4,32 @@ {{ template "head" . }}

<body> {{ template "nav" . }} <main> - {{- .HTML -}} + <h2>About Me</h2> + <div class="about-me"> + <div> + {{ range .AboutText }} + <p>{{ . }}</p> + {{ end }} + </div> + <img src="{{ .ImagePath }}" alt="Just a picture of me."> + </div> + <br> + <hr> + <h2> + Online Profiles + </h2> + {{ range .Profiles }} + <p> + <b class="icon">{{ .Icon }}</b> {{ .Website }}: <a href="{{ .URL }}">{{ .User }}</a> + </p> + {{ end }} + <br> + <hr> + <h2> + Contact + </h2> + <p> + <b class="icon">E</b> Email: <a href="mailto:{{ .Email }}">{{ .Email }}</a> </main> </body> </html>
A webhome/templates/manage.html

@@ -0,0 +1,15 @@

+{{ define "manage" }} +<html lang="en"> +{{ template "head" . }} +<body> +<main> + <h2> + {{ .Title }} {{ .Description }} + </h2> + {{ range .Options }} + <a href="{{ .URL }}">{{ .Name }}</a><br> + {{ end }} +</main> +</body> +</html> +{{ end }}