aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.golangci.yml2
-rw-r--r--cmd/server/app.go18
-rw-r--r--cmd/server/auth.go108
-rw-r--r--cmd/server/dashboard.go5
-rw-r--r--cmd/server/edit.go90
-rw-r--r--cmd/server/feed.go2
-rw-r--r--cmd/server/handlers.go7
-rw-r--r--cmd/server/main.go7
-rw-r--r--cmd/server/middleware.go142
-rw-r--r--go.mod2
-rw-r--r--go.sum10
-rw-r--r--page/page.go58
-rw-r--r--page/render.go137
-rw-r--r--users/main.go43
14 files changed, 589 insertions, 42 deletions
diff --git a/.golangci.yml b/.golangci.yml
index 319e4af..2201c93 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -27,7 +27,7 @@ linters:
- godot
- godox
- gofmt
- - gofumpt
+ # - gofumpt
- goheader
# - goimports
- golint
diff --git a/cmd/server/app.go b/cmd/server/app.go
index 4f62a09..d4977ab 100644
--- a/cmd/server/app.go
+++ b/cmd/server/app.go
@@ -16,11 +16,15 @@ type App struct {
redisPool *redis.Pool
RedisKey string
- ReIndexPath string
- StaticDirectory string
- BaseTemplate string
- DocumentSplit string
- Suffix string
+ ReIndexPath string
+ StaticDirectory string
+ BaseTemplate string
+ TemplateDirectory string
+ DocumentSplit string
+ Suffix string
+
+ // Related to user authentication
+ auth *Auth
// Related to the Atom feed
Title string
@@ -59,6 +63,10 @@ func loadConf(fn string) (*App, error) {
page.BaseTemplate = app.BaseTemplate
}
+ if app.TemplateDirectory != "" {
+ page.TemplateDirectory = app.TemplateDirectory
+ }
+
if app.DocumentSplit != "" {
page.DocumentSplit = app.DocumentSplit
}
diff --git a/cmd/server/auth.go b/cmd/server/auth.go
new file mode 100644
index 0000000..caade97
--- /dev/null
+++ b/cmd/server/auth.go
@@ -0,0 +1,108 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+
+ "riedstra.dev/mitch/go-website/users"
+)
+
+type Auth struct {
+ Users []*users.SiteUser `json:"Users"`
+ // How long are JWTs valid?
+ LoginHours int `json:"LoginHours"`
+ // JWT secret
+ TokenKey string `json:"TokenKey"`
+ // Are cookies HTTP only?
+ HTTPOnly bool `json:"HTTPOnly"`
+ // Do they require HTTPs?
+ Secure bool `json:"SecureCookie"`
+ // See https://pkg.go.dev/net/http#SameSite
+ // You probably want this set to 3
+ SameSiteStrict http.SameSite `json:"SameSiteStrict"`
+}
+
+func GenTokenKey() string {
+ r := make([]byte, 16) // 128 bits
+ _, err := rand.Read(r)
+
+ if err != nil {
+ // Not my favorite thing, but I consider this to be a
+ // critical issue
+ panic(fmt.Errorf("reading random bytes: %w", err))
+ }
+
+ return base64.RawURLEncoding.EncodeToString(r)
+}
+
+func (a *App) ReadAuth(fn string) error { //nolint
+ auth := &Auth{
+ Users: []*users.SiteUser{
+ {
+ Username: "admin",
+ Password: "admin",
+ },
+ },
+ LoginHours: 1,
+ TokenKey: GenTokenKey(),
+ HTTPOnly: true,
+ Secure: true,
+ SameSiteStrict: http.SameSiteStrictMode,
+ }
+
+ var dec *json.Decoder
+
+ fh, err := os.Open(fn)
+ if err != nil {
+ if strings.Contains(err.Error(), "no such file") {
+ goto write
+ }
+
+ return fmt.Errorf("opening %s: %w", fn, err)
+ }
+
+ dec = json.NewDecoder(fh)
+ dec.DisallowUnknownFields()
+
+ err = dec.Decode(auth)
+ if err != nil {
+ return fmt.Errorf("decoding file %s: %w", fn, err)
+ }
+
+ err = fh.Close()
+ if err != nil {
+ return fmt.Errorf("closing file %s: %w", fn, err)
+ }
+
+ for _, u := range auth.Users {
+ err = u.SetPasswordHashIfNecessary()
+ if err != nil {
+ return fmt.Errorf("setting password hash for: %s: %w",
+ u.Username, err)
+ }
+ }
+
+write:
+ fh, err = os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+
+ if err != nil {
+ return fmt.Errorf("opening file %s: %w", fn, err)
+ }
+
+ enc := json.NewEncoder(fh)
+ enc.SetIndent("", " ")
+
+ err = enc.Encode(auth)
+ if err != nil {
+ return fmt.Errorf("encoding to file %s: %w", fn, err)
+ }
+
+ a.auth = auth
+
+ return nil
+}
diff --git a/cmd/server/dashboard.go b/cmd/server/dashboard.go
new file mode 100644
index 0000000..a07e722
--- /dev/null
+++ b/cmd/server/dashboard.go
@@ -0,0 +1,5 @@
+package main
+
+// func (a *App) DashboardHandler(w http.ResponseWriter, r *http.Request) {
+// page.Render("
+// }
diff --git a/cmd/server/edit.go b/cmd/server/edit.go
new file mode 100644
index 0000000..730e6e9
--- /dev/null
+++ b/cmd/server/edit.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+ "bytes"
+ "html"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "riedstra.dev/mitch/go-website/page"
+)
+
+func (a *App) EditPage(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ page.Render(w, r, page.TemplateDirectory+"/4xx", map[string]interface{}{
+ "Title": "Method Not allowed",
+ "Description": "Method not allowed",
+ }, http.StatusMethodNotAllowed)
+
+ return
+ }
+
+ p := r.URL.Path
+
+ p = filepath.Clean(p)
+
+ fh, err := os.Open("./" + p + page.Suffix)
+ if err != nil {
+ log.Printf("opening page: %s", err)
+ a.Err500Default(w, r)
+
+ return
+ }
+
+ b, err := io.ReadAll(fh)
+ if err != nil {
+ log.Printf("opening page: %s", err)
+ a.Err500Default(w, r)
+
+ return
+ }
+
+ page.Render(w, r, page.TemplateDirectory+"/edit", map[string]interface{}{
+ "Page": p,
+ "Content": html.EscapeString(string(b)),
+ }, http.StatusOK)
+}
+
+func (a *App) SaveEditPage(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ a.Err500Default(w, r)
+
+ return
+ }
+
+ p := r.URL.Path
+ content := r.FormValue("content")
+
+ content = html.UnescapeString(content)
+ p = filepath.Clean(p)
+
+ fn := "./" + p + page.Suffix
+
+ fh, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ log.Printf("opening file %s for writing: %s", fn, err)
+ a.Err500Default(w, r)
+
+ return
+ }
+
+ c := bytes.ReplaceAll([]byte(content), []byte{'\r'}, []byte{})
+
+ _, err = fh.Write(c)
+ if err != nil {
+ log.Printf("opening file %s for writing: %s", fn, err)
+ a.Err500Default(w, r)
+
+ return
+ }
+
+ http.Redirect(w, r, "/"+r.URL.Path, http.StatusFound)
+
+ err = a.ClearRedis() // Clear out the cache if any
+ if err != nil {
+ log.Printf("after editing %s: %s", fn, err)
+ }
+}
diff --git a/cmd/server/feed.go b/cmd/server/feed.go
index 880eeb4..212e5da 100644
--- a/cmd/server/feed.go
+++ b/cmd/server/feed.go
@@ -203,7 +203,7 @@ func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { //nolint:fun
}
entry := Entry{
- Title: p.Title,
+ Title: p.Title(),
Updated: &p.Date.Time,
Links: []Link{{Href: strings.Join([]string{a.SiteURL, p.Path()}, "/")}},
}
diff --git a/cmd/server/handlers.go b/cmd/server/handlers.go
index 205d462..1711ae3 100644
--- a/cmd/server/handlers.go
+++ b/cmd/server/handlers.go
@@ -15,6 +15,13 @@ func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rtr.HandleFunc(a.ReIndexPath, a.RebuildIndexHandler)
rtr.PathPrefix("/static/").Handler(a.StaticHandler())
+ rtr.HandleFunc("/login", a.LoginHandler)
+ rtr.Handle("/logout", a.RequiresLogin(http.HandlerFunc(a.LogoutHandler)))
+ rtr.PathPrefix("/edit/").Handler(
+ a.RequiresLogin(http.StripPrefix("/edit/", http.HandlerFunc(a.EditPage)))).Methods("GET")
+ rtr.PathPrefix("/edit/").Handler(
+ a.RequiresLogin(http.StripPrefix("/edit/", http.HandlerFunc(a.SaveEditPage)))).Methods("POST")
+
if a.redisPool != nil {
rtr.PathPrefix(fmt.Sprintf("/%s/{tag}", a.FeedPrefix)).Handler(
rediscache.HandleWithParams(a.redisPool, a.RedisKey,
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 912cba0..ee8cf6f 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -28,6 +28,8 @@ func main() { //nolint:funlen
directory := fl.String("d", ".", "Directory to serve.")
version := fl.Bool("v", false, "Print the version then exit")
confFn := fl.String("c", "conf.yml", "Location for the config file")
+ authConfFn := fl.String("ac", "auth.json",
+ "Location for authorization configuration file")
verbose := fl.Bool("V", false, "Be more verbose ( dump config, etc ) ")
fl.StringVar(&page.TimeFormat, "T", page.TimeFormat,
"Set the page time format, be careful with this")
@@ -61,6 +63,11 @@ func main() { //nolint:funlen
app = &App{}
}
+ err = app.ReadAuth(*authConfFn)
+ if err != nil {
+ log.Println(err)
+ }
+
if app.ReIndexPath == "" || *indexPath != defaultIndexPath {
app.ReIndexPath = *indexPath
}
diff --git a/cmd/server/middleware.go b/cmd/server/middleware.go
new file mode 100644
index 0000000..5e4bf26
--- /dev/null
+++ b/cmd/server/middleware.go
@@ -0,0 +1,142 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "time"
+
+ jwt "github.com/dgrijalva/jwt-go"
+ "riedstra.dev/mitch/go-website/page"
+ "riedstra.dev/mitch/go-website/users"
+)
+
+func (a *App) Err5xx(w http.ResponseWriter, r *http.Request,
+ statusCode int, title, desc string) {
+ page.Render5xx(w, r, map[string]interface{}{
+ "Error": title,
+ "Description": desc,
+ }, statusCode)
+}
+
+func (a *App) Err500Default(w http.ResponseWriter, r *http.Request) {
+ a.Err5xx(w, r, http.StatusInternalServerError, "Internal server error",
+ "Internal server error.")
+}
+
+func (a *App) LogoutHandler(w http.ResponseWriter, r *http.Request) {
+ http.SetCookie(w, &http.Cookie{
+ Name: "Auth",
+ HttpOnly: a.auth.HTTPOnly,
+ SameSite: a.auth.SameSiteStrict,
+ Secure: a.auth.Secure,
+ Value: "logout",
+ Expires: time.Now().Add(time.Second * 15), //nolint
+ })
+
+ http.Redirect(w, r, "/", http.StatusFound)
+}
+
+func (a *App) LoginHandler(w http.ResponseWriter, r *http.Request) { //nolint
+ if r.Method == "GET" {
+ page.RenderForPath(w, r, "login")
+
+ return
+ }
+
+ if r.Method != "POST" {
+ a.Err500Default(w, r)
+
+ return
+ }
+
+ log.Printf("made it")
+
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+
+ var (
+ err error = nil
+ u *users.SiteUser
+ found = false
+ )
+
+ for _, u = range a.auth.Users {
+ if u.Username == username {
+ err = u.CheckPassword(password)
+ found = true
+ }
+ }
+
+ if err != nil || !found {
+ page.Render(w, r, "login", map[string]interface{}{
+ "Error": "Invalid username or password",
+ "Username": username,
+ }, http.StatusUnauthorized)
+
+ return
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS512, &jwt.StandardClaims{
+ ExpiresAt: time.Now().Add(
+ time.Hour * time.Duration(a.auth.LoginHours)).Unix(),
+ Id: u.Username,
+ })
+
+ ss, err := token.SignedString([]byte(a.auth.TokenKey))
+ if err != nil {
+ log.Println("login: encountered while setting up JWT: ", err)
+ a.Err500Default(w, r)
+
+ return
+ }
+
+ log.Println(ss)
+
+ http.SetCookie(w, &http.Cookie{
+ Name: "Auth",
+ HttpOnly: a.auth.HTTPOnly,
+ SameSite: a.auth.SameSiteStrict,
+ Secure: a.auth.Secure,
+ Value: ss,
+ })
+
+ http.Redirect(w, r, "/login", http.StatusFound)
+}
+
+func (a *App) RequiresLogin(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ mustLoginResp := func() {
+ page.Render(w, r, "login", map[string]interface{}{
+ "Error": "You must login to view this page",
+ }, http.StatusUnauthorized)
+ }
+
+ c, err := r.Cookie("Auth")
+ if err != nil {
+ mustLoginResp()
+
+ return
+ }
+
+ token, err := jwt.Parse(c.Value,
+ func(token *jwt.Token) (interface{}, error) {
+ return []byte(a.auth.TokenKey), nil
+ },
+ )
+
+ if err != nil {
+ log.Printf("Unauthorized request %s %s", r.Method, r.URL.Path)
+ mustLoginResp()
+
+ return
+ }
+
+ if !token.Valid {
+ mustLoginResp()
+
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/go.mod b/go.mod
index 444615b..bf98b60 100644
--- a/go.mod
+++ b/go.mod
@@ -3,12 +3,14 @@ module riedstra.dev/mitch/go-website
go 1.13
require (
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/gomodule/redigo v1.8.5 // indirect
github.com/gorilla/mux v1.8.0
github.com/kr/pretty v0.1.0 // indirect
github.com/russross/blackfriday v2.0.0+incompatible
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
+ golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
diff --git a/go.sum b/go.sum
index 9503c39..f6dae73 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc=
@@ -25,11 +27,19 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
diff --git a/page/page.go b/page/page.go
index b2cc3e1..adb2362 100644
--- a/page/page.go
+++ b/page/page.go
@@ -46,11 +46,11 @@ import (
// The exported fields can be filled in the yaml at the top of a page and
// utilized within.
type Page struct {
- path string
- Title string
- Description string
- AuthorName string
- AuthorEmail string
+ path string
+ DefaultTitle string `yaml:"title"`
+ DefaultDescription string `yaml:"description"`
+ AuthorName string
+ AuthorEmail string
// Tags to apply to the page in question. Useful for Index()
Tags map[string]interface{}
Date *PageTime
@@ -76,8 +76,14 @@ var CacheIndex = true
// backend.
var FileSystem = os.DirFS(".")
+// TemplateDirectory is the parent directory which templates are stored,
+// usually the BaseTemplate is stored here as well, though it does not
+// have to be. Currently this is used for the 4xx and 5xx pages. ( 4xx.md
+// and 5xx.md respectively.
+var TemplateDirectory = "tpl"
+
// BaseTemplate can be adjusted to change the base template used in rendering.
-var BaseTemplate = "inc/base.html"
+var BaseTemplate = "tpl/base.md"
// Suffix is applied to all pages for reading off of the disk.
var Suffix = ".md"
@@ -107,6 +113,46 @@ func (p Page) Path() string {
return filepath.ToSlash(p.path)
}
+// Title returns `DefaultTitle` unless `.Vars.Title` is
+// set, then it returns that instead.
+func (p Page) Title() string {
+ if p.Vars == nil {
+ return p.DefaultTitle
+ }
+
+ t, ok := p.Vars["Title"]
+ if !ok {
+ return p.DefaultTitle
+ }
+
+ nt, ok := t.(string)
+ if !ok {
+ return p.DefaultTitle
+ }
+
+ return nt
+}
+
+// Description returns `DefaultDescription` unless `.Vars.Description` is
+// set, then it returns that instead.
+func (p Page) Description() string {
+ if p.Vars == nil {
+ return p.DefaultDescription
+ }
+
+ t, ok := p.Vars["Description"]
+ if !ok {
+ return p.DefaultDescription
+ }
+
+ nt, ok := t.(string)
+ if !ok {
+ return p.DefaultDescription
+ }
+
+ return nt
+}
+
// Global is specifically for use inside of a page markdown file or
// in a base template. This simply returns the package Global variable.
func (p *Page) Global() interface{} {
diff --git a/page/render.go b/page/render.go
index 6cbabf9..d35ba38 100644
--- a/page/render.go
+++ b/page/render.go
@@ -1,20 +1,104 @@
package page
import (
+ "bytes"
"net/http"
"path/filepath"
"strings"
)
-// Render is a lower level option, allowing you to specify local
-// variables and the status code in which to return.
-func Render(w http.ResponseWriter, r *http.Request,
- path string, vars map[string]interface{}, statusCode int) {
+func getURLPath(r *http.Request) string {
u := r.URL.Path
if u == "/" {
u = "/index"
}
+ return u
+}
+
+// Render5xx is automatically called if any render fails,
+// additionally you can call it for your own uses. It will
+// try to use the 5xx template but will fall back to a plain
+// page if that fails.
+func Render5xx(w http.ResponseWriter, r *http.Request,
+ vars map[string]interface{}, statusCode int) {
+ u := getURLPath(r)
+
+ Logger.Printf("%s %s %d %s",
+ r.RemoteAddr,
+ r.Method,
+ statusCode,
+ u)
+
+ p := NewPage(TemplateDirectory + "/5xx")
+
+ buf := &bytes.Buffer{}
+
+ err := p.Render(buf)
+ if err != nil {
+ Logger.Printf("%s %s path: %s while trying 5xx: %s",
+ r.RemoteAddr,
+ r.Method,
+ u,
+ err)
+ http.Error(w, "Internal server error", statusCode)
+
+ return
+ }
+
+ w.WriteHeader(statusCode)
+
+ _, _ = w.Write(buf.Bytes())
+
+ Logger.Printf("%s %s %d %s", r.RemoteAddr, r.Method, statusCode, u)
+}
+
+// Render4xx is mostly used to render a 404 page, pulls from 404 template.
+// automatically called by Render if a page is missing.
+func Render4xx(w http.ResponseWriter, r *http.Request,
+ vars map[string]interface{}, statusCode int) {
+ u := getURLPath(r)
+
+ Logger.Printf("%s %s %d %s",
+ r.RemoteAddr,
+ r.Method,
+ statusCode,
+ u)
+
+ p := NewPage(TemplateDirectory + "/4xx")
+
+ buf := &bytes.Buffer{}
+
+ err := p.Render(buf)
+ if err != nil {
+ Logger.Printf("%s %s path: %s while trying 404: %s",
+ r.RemoteAddr,
+ r.Method,
+ u,
+ err)
+ Render5xx(w, r, vars, http.StatusInternalServerError)
+
+ return
+ }
+
+ w.WriteHeader(statusCode)
+
+ _, err = w.Write(buf.Bytes())
+ if err != nil {
+ Render5xx(w, r, nil, http.StatusInternalServerError)
+
+ return
+ }
+
+ Logger.Printf("%s %s %d %s", r.RemoteAddr, r.Method, statusCode, u)
+}
+
+// Render is a lower level option, allowing you to specify local
+// variables and the status code in which to return.
+func Render(w http.ResponseWriter, r *http.Request,
+ path string, vars map[string]interface{}, statusCode int) {
+ u := getURLPath(r)
+
u = filepath.Join(".", u)
// Sepcifically use the specified path for the page
@@ -24,42 +108,37 @@ func Render(w http.ResponseWriter, r *http.Request,
p.Vars = vars
}
- err := p.Render(w)
+ buf := &bytes.Buffer{}
+
+ err := p.Render(buf)
if err != nil {
if strings.HasSuffix(err.Error(), "no such file or directory") {
- Logger.Printf("%s %s %d %s",
- r.RemoteAddr,
- r.Method,
- http.StatusNotFound,
- u)
+ Render4xx(w, r, vars, http.StatusNotFound)
- p = NewPage("404")
-
- w.WriteHeader(http.StatusNotFound)
+ return
+ }
- err := p.Render(w)
- if err != nil {
- Logger.Printf("%s %s path: %s while trying 404: %s",
- r.RemoteAddr,
- r.Method,
- u,
- err)
- http.Error(w, "Internal server error",
- http.StatusInternalServerError)
+ Logger.Printf("%s %s path: %s rendering encountered: %s",
+ r.RemoteAddr,
+ r.Method,
+ u,
+ err)
+ Render5xx(w, r, vars, http.StatusInternalServerError)
- return
- }
+ return
+ }
- return
- }
+ w.WriteHeader(statusCode)
- Logger.Printf("%s %s path: %s encountered: %s",
+ _, err = w.Write(buf.Bytes())
+ if err != nil {
+ Logger.Printf("%s %s %d %s: while writing buf: %s",
r.RemoteAddr,
r.Method,
+ statusCode,
u,
err)
- http.Error(w, "Internal server error",
- http.StatusInternalServerError)
+ Render5xx(w, r, nil, http.StatusInternalServerError)
return
}
diff --git a/users/main.go b/users/main.go
new file mode 100644
index 0000000..edb306f
--- /dev/null
+++ b/users/main.go
@@ -0,0 +1,43 @@
+package users
+
+import (
+ "fmt"
+
+ "golang.org/x/crypto/bcrypt"
+)
+
+type SiteUser struct {
+ Username string `yaml:"username"`
+ PasswordHash string `yaml:"password-hash"`
+ Password string `yaml:"password"`
+}
+
+func (su *SiteUser) SetPasswordHashIfNecessary() error {
+ if su.Password == "" {
+ return nil
+ }
+
+ res, err := bcrypt.GenerateFromPassword([]byte(su.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return fmt.Errorf("SetPasswordHashIfNecessary: %w", err)
+ }
+
+ su.Password = ""
+ su.PasswordHash = string(res)
+
+ return nil
+}
+
+func (su *SiteUser) CheckPassword(pass string) error {
+ err := su.SetPasswordHashIfNecessary()
+ if err != nil {
+ return err
+ }
+
+ err = bcrypt.CompareHashAndPassword([]byte(su.PasswordHash), []byte(pass))
+ if err != nil {
+ return fmt.Errorf("CheckPassword: %w", err)
+ }
+
+ return nil
+}