Pretty Logger and Persistent App Settings JSON
parent
4eef81c07f
commit
0b04dad045
|
@ -1,3 +1,4 @@
|
||||||
bin/
|
bin/
|
||||||
pkg/bundled/
|
pkg/bundled/
|
||||||
|
public_html/.settings.json
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gophertype/pkg"
|
"git.kirsle.net/apps/gophertype/pkg"
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
_ "git.kirsle.net/apps/gophertype/pkg/controllers"
|
_ "git.kirsle.net/apps/gophertype/pkg/controllers"
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||||
|
@ -24,6 +25,7 @@ var (
|
||||||
var (
|
var (
|
||||||
optDebug bool
|
optDebug bool
|
||||||
optBind string
|
optBind string
|
||||||
|
optRoot string
|
||||||
|
|
||||||
// Database option flags.
|
// Database option flags.
|
||||||
optSQLite string
|
optSQLite string
|
||||||
|
@ -38,6 +40,7 @@ var (
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&optDebug, "debug", false, "Debug level logging")
|
flag.BoolVar(&optDebug, "debug", false, "Debug level logging")
|
||||||
flag.StringVar(&optBind, "bind", ":8000", "Bind address for HTTP server")
|
flag.StringVar(&optBind, "bind", ":8000", "Bind address for HTTP server")
|
||||||
|
flag.StringVar(&optRoot, "root", "./public_html", "User root for custom web pages")
|
||||||
|
|
||||||
// Database driver. Choose one.
|
// Database driver. Choose one.
|
||||||
flag.StringVar(&optSQLite, "sqlite3", "", "Use SQLite database, default 'database.sqlite'")
|
flag.StringVar(&optSQLite, "sqlite3", "", "Use SQLite database, default 'database.sqlite'")
|
||||||
|
@ -52,6 +55,8 @@ func init() {
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
console.SetDebug(optDebug)
|
||||||
|
|
||||||
// Validate the choice of database.
|
// Validate the choice of database.
|
||||||
if optSQLite != "" {
|
if optSQLite != "" {
|
||||||
dbDriver = "sqlite3"
|
dbDriver = "sqlite3"
|
||||||
|
@ -74,7 +79,7 @@ func main() {
|
||||||
|
|
||||||
fmt.Println("Hello world")
|
fmt.Println("Hello world")
|
||||||
|
|
||||||
app := gophertype.NewSite()
|
app := gophertype.NewSite(optRoot)
|
||||||
app.UseDB(dbDriver, dbPath)
|
app.UseDB(dbDriver, dbPath)
|
||||||
app.SetupRouter()
|
app.SetupRouter()
|
||||||
app.ListenAndServe(optBind)
|
app.ListenAndServe(optBind)
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/gorilla/sessions v1.2.0
|
github.com/gorilla/sessions v1.2.0
|
||||||
github.com/jinzhu/gorm v1.9.11
|
github.com/jinzhu/gorm v1.9.11
|
||||||
github.com/kirsle/blog v0.0.0-20191022175051-d78814b9c99b
|
github.com/kirsle/blog v0.0.0-20191022175051-d78814b9c99b
|
||||||
|
github.com/kirsle/golog v0.0.0-20180411020913-51290b4f9292
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
github.com/urfave/negroni v1.0.0
|
github.com/urfave/negroni v1.0.0
|
||||||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443
|
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443
|
||||||
|
|
1
go.sum
1
go.sum
|
@ -1,6 +1,7 @@
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
|
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
|
||||||
|
git.kirsle.net/go/log v0.0.0-20180505005739-e046e3e7b0b1 h1:F4cwOb7q/ZtZ4ADIhCgXlHpIahyIvtomrjCSZb4550I=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||||
|
|
14
pkg/app.go
14
pkg/app.go
|
@ -1,10 +1,11 @@
|
||||||
package gophertype
|
package gophertype
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/models"
|
"git.kirsle.net/apps/gophertype/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/settings"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
|
@ -17,7 +18,12 @@ type Site struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSite initializes the Site.
|
// NewSite initializes the Site.
|
||||||
func NewSite() *Site {
|
func NewSite(pubroot string) *Site {
|
||||||
|
// Initialize the settings.json inside the user root.
|
||||||
|
if err := settings.SetFilename(pubroot); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
site := &Site{}
|
site := &Site{}
|
||||||
|
|
||||||
n := negroni.New()
|
n := negroni.New()
|
||||||
|
@ -35,13 +41,13 @@ func (s *Site) UseDB(driver string, path string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Using database driver '%s'", driver)
|
console.Info("Using database driver '%s'", driver)
|
||||||
models.UseDB(db)
|
models.UseDB(db)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenAndServe starts the HTTP service.
|
// ListenAndServe starts the HTTP service.
|
||||||
func (s *Site) ListenAndServe(addr string) error {
|
func (s *Site) ListenAndServe(addr string) error {
|
||||||
log.Printf("Listening on %s", addr)
|
console.Info("Listening on %s", addr)
|
||||||
return http.ListenAndServe(addr, s.n)
|
return http.ListenAndServe(addr, s.n)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ package authentication
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/models"
|
"git.kirsle.net/apps/gophertype/pkg/models"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/session"
|
"git.kirsle.net/apps/gophertype/pkg/session"
|
||||||
)
|
)
|
||||||
|
@ -28,7 +28,7 @@ func Login(w http.ResponseWriter, r *http.Request, user models.User) {
|
||||||
sess.Values["logged-in"] = true
|
sess.Values["logged-in"] = true
|
||||||
sess.Values["user-id"] = int(user.ID)
|
sess.Values["user-id"] = int(user.ID)
|
||||||
if err := sess.Save(r, w); err != nil {
|
if err := sess.Save(r, w); err != nil {
|
||||||
log.Printf("ERROR: Login() Session error: " + err.Error())
|
console.Error("Login() Session error: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Package console implements a colorful logger for Gophertype.
|
||||||
|
package console
|
||||||
|
|
||||||
|
import "github.com/kirsle/golog"
|
||||||
|
|
||||||
|
var log *golog.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = golog.GetLogger("gophertype")
|
||||||
|
log.Configure(&golog.Config{
|
||||||
|
Colors: golog.ExtendedColor,
|
||||||
|
Theme: golog.DarkTheme,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDebug turns debug logging on or off.
|
||||||
|
func SetDebug(on bool) {
|
||||||
|
if on {
|
||||||
|
log.Config.Level = golog.DebugLevel
|
||||||
|
} else {
|
||||||
|
log.Config.Level = golog.InfoLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info level log.
|
||||||
|
func Info(msg string, v ...interface{}) {
|
||||||
|
log.Info(msg, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug level log.
|
||||||
|
func Debug(msg string, v ...interface{}) {
|
||||||
|
log.Debug(msg, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn level log.
|
||||||
|
func Warn(msg string, v ...interface{}) {
|
||||||
|
log.Warn(msg, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error level log.
|
||||||
|
func Error(msg string, v ...interface{}) {
|
||||||
|
log.Error(msg, v...)
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gophertype/pkg/authentication"
|
"git.kirsle.net/apps/gophertype/pkg/authentication"
|
||||||
|
@ -32,7 +31,6 @@ func init() {
|
||||||
val.Require("password")
|
val.Require("password")
|
||||||
if val.HasErrors() {
|
if val.HasErrors() {
|
||||||
v.ValidationError = val.ErrorMap()
|
v.ValidationError = val.ErrorMap()
|
||||||
log.Printf("validation: %+v", v.ValidationError)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package controllers
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gophertype/pkg/constants"
|
"git.kirsle.net/apps/gophertype/pkg/constants"
|
||||||
|
@ -23,81 +22,86 @@ func init() {
|
||||||
Middleware: []mux.MiddlewareFunc{
|
Middleware: []mux.MiddlewareFunc{
|
||||||
middleware.ExampleMiddleware,
|
middleware.ExampleMiddleware,
|
||||||
},
|
},
|
||||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
Handler: InitialSetup,
|
||||||
// See if we already have an admin account.
|
|
||||||
if _, err := models.FirstAdmin(); err == nil {
|
|
||||||
responses.Panic(w, http.StatusForbidden, "This site is already initialized.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template variables.
|
|
||||||
v := responses.NewTemplateVars(w, r)
|
|
||||||
v.SetupNeeded = false // supress the banner on this page.
|
|
||||||
|
|
||||||
// POST handler: create the admin account.
|
|
||||||
for r.Method == http.MethodPost {
|
|
||||||
form, err := forms.Parse(r)
|
|
||||||
if err != nil {
|
|
||||||
responses.Error(w, r, http.StatusBadRequest, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
v.FormValues = form.Values
|
|
||||||
|
|
||||||
// Validate form parameters.
|
|
||||||
val := form.Validator()
|
|
||||||
val.Require("email")
|
|
||||||
val.MatchEmail("email")
|
|
||||||
val.MinLength("password", 8)
|
|
||||||
val.Require("password2")
|
|
||||||
val.Equal("password", "password2")
|
|
||||||
if val.HasErrors() {
|
|
||||||
v.Error = fmt.Errorf("validation error")
|
|
||||||
v.ValidationError = val.ErrorMap()
|
|
||||||
log.Printf("validation: %+v", v.ValidationError)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
email = form.Get("email")
|
|
||||||
displayName = form.Get("name")
|
|
||||||
password = form.Get("password")
|
|
||||||
password2 = form.Get("password2")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Username and display name validation happens in CreateUser.
|
|
||||||
// Validate the passwords match here.
|
|
||||||
if len(password) < constants.PasswordMinLength {
|
|
||||||
v.Error = fmt.Errorf("your password is too short (must be %d+ characters)", constants.PasswordMinLength)
|
|
||||||
}
|
|
||||||
if password != password2 {
|
|
||||||
v.Error = errors.New("your passwords don't match")
|
|
||||||
} else {
|
|
||||||
admin := models.User{
|
|
||||||
Email: email,
|
|
||||||
Name: displayName,
|
|
||||||
IsAdmin: true,
|
|
||||||
}
|
|
||||||
admin.SetPassword(password)
|
|
||||||
|
|
||||||
if err := models.CreateUser(admin); err != nil {
|
|
||||||
v.Error = err
|
|
||||||
} else {
|
|
||||||
// Admin created! Make the default config.
|
|
||||||
cfg := settings.Load()
|
|
||||||
cfg.Initialized = true
|
|
||||||
cfg.Save()
|
|
||||||
|
|
||||||
responses.Redirect(w, r, "/login")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
responses.RenderTemplate(w, r, "_builtin/initial_setup.gohtml", v)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitialSetup at "/admin/setup"
|
||||||
|
func InitialSetup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Template variables.
|
||||||
|
v := responses.NewTemplateVars(w, r)
|
||||||
|
v.SetupNeeded = false // supress the banner on this page.
|
||||||
|
|
||||||
|
// See if we already have an admin account.
|
||||||
|
if _, err := models.FirstAdmin(); err == nil {
|
||||||
|
v.Message = "This site is already initialized."
|
||||||
|
responses.Forbidden(w, r, "This site has already been initialized.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST handler: create the admin account.
|
||||||
|
for r.Method == http.MethodPost {
|
||||||
|
form, err := forms.Parse(r)
|
||||||
|
if err != nil {
|
||||||
|
responses.Error(w, r, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v.FormValues = form.Values
|
||||||
|
|
||||||
|
// Validate form parameters.
|
||||||
|
val := form.Validator()
|
||||||
|
val.Require("email")
|
||||||
|
val.MatchEmail("email")
|
||||||
|
val.MinLength("password", 8)
|
||||||
|
val.Require("password2")
|
||||||
|
val.Equal("password", "password2")
|
||||||
|
if val.HasErrors() {
|
||||||
|
v.Error = fmt.Errorf("validation error")
|
||||||
|
v.ValidationError = val.ErrorMap()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
email = form.Get("email")
|
||||||
|
displayName = form.Get("name")
|
||||||
|
password = form.Get("password")
|
||||||
|
password2 = form.Get("password2")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Username and display name validation happens in CreateUser.
|
||||||
|
// Validate the passwords match here.
|
||||||
|
if len(password) < constants.PasswordMinLength {
|
||||||
|
v.Error = fmt.Errorf("your password is too short (must be %d+ characters)", constants.PasswordMinLength)
|
||||||
|
}
|
||||||
|
if password != password2 {
|
||||||
|
v.Error = errors.New("your passwords don't match")
|
||||||
|
} else {
|
||||||
|
admin := models.User{
|
||||||
|
Email: email,
|
||||||
|
Name: displayName,
|
||||||
|
IsAdmin: true,
|
||||||
|
}
|
||||||
|
admin.SetPassword(password)
|
||||||
|
|
||||||
|
if err := models.CreateUser(admin); err != nil {
|
||||||
|
v.Error = err
|
||||||
|
} else {
|
||||||
|
// Admin created! Make the default config.
|
||||||
|
cfg := settings.Load()
|
||||||
|
cfg.Initialized = true
|
||||||
|
if err := cfg.Save(); err != nil {
|
||||||
|
v.Error = err
|
||||||
|
} else {
|
||||||
|
responses.Redirect(w, r, "/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.RenderTemplate(w, r, "_builtin/initial_setup.gohtml", v)
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/responses"
|
"git.kirsle.net/apps/gophertype/pkg/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import (
|
||||||
// templates from the builtin web root or the user root.
|
// templates from the builtin web root or the user root.
|
||||||
func CatchAllHandler(w http.ResponseWriter, r *http.Request) {
|
func CatchAllHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
log.Printf("Wildcard path: %s", path)
|
console.Debug("Wildcard path: %s", path)
|
||||||
|
|
||||||
// Resolve the target path.
|
// Resolve the target path.
|
||||||
filepath, err := responses.ResolveFile(path)
|
filepath, err := responses.ResolveFile(path)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package glue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -33,7 +32,6 @@ func Register(e Endpoint) {
|
||||||
}
|
}
|
||||||
registry[e.Path] = e
|
registry[e.Path] = e
|
||||||
registryLock.Unlock()
|
registryLock.Unlock()
|
||||||
log.Printf("Register: %s", e.Path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetControllers returns all the routes and handler functions.
|
// GetControllers returns all the routes and handler functions.
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExampleMiddleware is a test middleware.
|
// ExampleMiddleware is a test middleware.
|
||||||
func ExampleMiddleware(next http.Handler) http.Handler {
|
func ExampleMiddleware(next http.Handler) http.Handler {
|
||||||
middleware := func(w http.ResponseWriter, r *http.Request) {
|
middleware := func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("ExampleMiddleware called on route %s", r.URL.Path)
|
console.Warn("ExampleMiddleware called on route %s", r.URL.Path)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AppSetting singleton holds the app configuration.
|
|
||||||
type AppSetting struct {
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
// Site information
|
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
Email string // primary email for notifications
|
|
||||||
NSFW bool
|
|
||||||
BaseURL string
|
|
||||||
|
|
||||||
// Blog settings
|
|
||||||
PostsPerPage int
|
|
||||||
|
|
||||||
// Mail settings
|
|
||||||
MailEnabled bool
|
|
||||||
MailSender string
|
|
||||||
MailHost string
|
|
||||||
MailPort int
|
|
||||||
MailUsername string
|
|
||||||
MailPassword string
|
|
||||||
|
|
||||||
// Security
|
|
||||||
SecretKey string `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSettings gets or creates the App Settings.
|
|
||||||
func GetSettings() AppSetting {
|
|
||||||
var s AppSetting
|
|
||||||
r := DB.First(&s)
|
|
||||||
if r.Error != nil {
|
|
||||||
s = AppSetting{
|
|
||||||
Title: "Untitled Site",
|
|
||||||
Description: "Just another web blog.",
|
|
||||||
SecretKey: MakeSecretKey(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.SecretKey == "" {
|
|
||||||
s.SecretKey = MakeSecretKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the settings to DB.
|
|
||||||
func (s AppSetting) Save() error {
|
|
||||||
if DB.NewRecord(s) {
|
|
||||||
r := DB.Create(&s)
|
|
||||||
return r.Error
|
|
||||||
}
|
|
||||||
r := DB.Save(&s)
|
|
||||||
return r.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeSecretKey generates a secret key for signing HTTP cookies.
|
|
||||||
func MakeSecretKey() string {
|
|
||||||
keyLength := 32
|
|
||||||
|
|
||||||
b := make([]byte, keyLength)
|
|
||||||
rand.Read(b)
|
|
||||||
return base64.URLEncoding.EncodeToString(b)
|
|
||||||
}
|
|
|
@ -8,6 +8,5 @@ var DB *gorm.DB
|
||||||
// UseDB registers a database driver.
|
// UseDB registers a database driver.
|
||||||
func UseDB(db *gorm.DB) {
|
func UseDB(db *gorm.DB) {
|
||||||
DB = db
|
DB = db
|
||||||
DB.AutoMigrate(&AppSetting{})
|
|
||||||
DB.AutoMigrate(&User{})
|
DB.AutoMigrate(&User{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ package models
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/constants"
|
"git.kirsle.net/apps/gophertype/pkg/constants"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
@ -36,7 +36,7 @@ func (u *User) Validate() error {
|
||||||
func AuthenticateUser(email string, password string) (User, error) {
|
func AuthenticateUser(email string, password string) (User, error) {
|
||||||
user, err := GetUserByEmail(email)
|
user, err := GetUserByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ERROR: AuthenticateUser: email %s not found: %s", email, err)
|
console.Error("AuthenticateUser: email %s not found: %s", email, err)
|
||||||
return User{}, errors.New("incorrect email or password")
|
return User{}, errors.New("incorrect email or password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,14 +69,13 @@ func (u *User) SetPassword(password string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
u.HashedPassword = string(hash)
|
u.HashedPassword = string(hash)
|
||||||
fmt.Printf("Set hashed password: %s", u.HashedPassword)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyPassword checks if the password matches the user's hashed password.
|
// VerifyPassword checks if the password matches the user's hashed password.
|
||||||
func (u *User) VerifyPassword(password string) bool {
|
func (u *User) VerifyPassword(password string) bool {
|
||||||
if u.HashedPassword == "" {
|
if u.HashedPassword == "" {
|
||||||
fmt.Printf("ERROR: VerifyPassword: user has no HashedPassword")
|
console.Error("ERROR: VerifyPassword: user %s has no HashedPassword", u.Email)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +83,7 @@ func (u *User) VerifyPassword(password string) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
log.Printf("ERROR: VerifyPassword: %s", err)
|
console.Error("VerifyPassword: %s", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Panic gives a simple error with no template or anything fancy.
|
// Panic gives a simple error with no template or anything fancy.
|
||||||
|
@ -19,23 +21,38 @@ func Error(w http.ResponseWriter, r *http.Request, code int, message string) {
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
|
|
||||||
if err := RenderTemplate(w, r, "_builtin/errors/generic.gohtml", v); err != nil {
|
if err := RenderTemplate(w, r, "_builtin/errors/generic.gohtml", v); err != nil {
|
||||||
log.Printf("responses.Error: failed to render a pretty error template: %s", err)
|
console.Error("responses.Error: failed to render a pretty error template: %s", err)
|
||||||
w.Write([]byte(message))
|
w.Write([]byte(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotFound returns an HTML 404 page.
|
// NotFound returns an HTML 404 page.
|
||||||
func NotFound(w http.ResponseWriter, r *http.Request) {
|
func NotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
if err := RenderTemplate(w, r, "_builtin/errors/404.gohtml", nil); err != nil {
|
if err := RenderTemplate(w, r, "_builtin/errors/404.gohtml", nil); err != nil {
|
||||||
log.Printf("responses.NotFound: failed to render a pretty error template: %s", err)
|
console.Error("responses.NotFound: failed to render a pretty error template: %s", err)
|
||||||
Panic(w, http.StatusNotFound, "Not Found")
|
Panic(w, http.StatusNotFound, "Not Found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BadRequest returns a 400 Bad Request page.
|
// BadRequest returns a 400 Bad Request page.
|
||||||
func BadRequest(w http.ResponseWriter, r *http.Request, vars interface{}) {
|
func BadRequest(w http.ResponseWriter, r *http.Request, vars interface{}) {
|
||||||
if err := RenderTemplate(w, r, "_builtin/errors/generic.gohtml", vars); err != nil {
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
log.Printf("responses.NotFound: failed to render a pretty error template: %s", err)
|
if err := RenderTemplate(w, r, "_builtin/errors/400.gohtml", vars); err != nil {
|
||||||
|
console.Error("responses.BadRequest: failed to render a pretty error template: %s", err)
|
||||||
Panic(w, http.StatusBadRequest, "Bad Request")
|
Panic(w, http.StatusBadRequest, "Bad Request")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forbidden returns a 403 Forbidden page.
|
||||||
|
func Forbidden(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
|
||||||
|
v := NewTemplateVars(w, r)
|
||||||
|
v.Message = fmt.Sprintf(message, args...)
|
||||||
|
|
||||||
|
if err := RenderTemplate(w, r, "_builtin/errors/403.gohtml", v); err != nil {
|
||||||
|
console.Error("responses.Forbidden: failed to render a pretty error template: %s", err)
|
||||||
|
Panic(w, http.StatusForbidden, "Forbidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
|
||||||
// CSRF returns the current CSRF token as an HTML hidden form field.
|
// CSRF returns the current CSRF token as an HTML hidden form field.
|
||||||
func CSRF(r *http.Request) func() template.HTML {
|
func CSRF(r *http.Request) func() template.HTML {
|
||||||
return func() template.HTML {
|
return func() template.HTML {
|
||||||
fmt.Println("CSRF() func called")
|
|
||||||
token, _ := r.Cookie(constants.CSRFCookieName)
|
token, _ := r.Cookie(constants.CSRFCookieName)
|
||||||
return template.HTML(fmt.Sprintf(
|
return template.HTML(fmt.Sprintf(
|
||||||
`<input type="hidden" name="%s" value="%s">`,
|
`<input type="hidden" name="%s" value="%s">`,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderTemplate renders a Go HTML template.
|
// RenderTemplate renders a Go HTML template.
|
||||||
|
@ -14,13 +14,12 @@ func RenderTemplate(w http.ResponseWriter, r *http.Request, tmpl string, vars in
|
||||||
vars = NewTemplateVars(w, r)
|
vars = NewTemplateVars(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
|
|
||||||
// Look for the built-in template.
|
// Look for the built-in template.
|
||||||
if b, err := GetFile(tmpl); err == nil {
|
b, err := GetFile(tmpl)
|
||||||
|
if err == nil {
|
||||||
t, err := template.New(tmpl).Funcs(TemplateFuncs(r)).Parse(string(b))
|
t, err := template.New(tmpl).Funcs(TemplateFuncs(r)).Parse(string(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("bundled template '%s': %s", tmpl, err)
|
console.Error("RenderTemplate: bundled template '%s': %s", tmpl, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,21 +27,19 @@ func RenderTemplate(w http.ResponseWriter, r *http.Request, tmpl string, vars in
|
||||||
if layout, err := GetFile(".layout.gohtml"); err == nil {
|
if layout, err := GetFile(".layout.gohtml"); err == nil {
|
||||||
_, err := t.New("layout").Parse(string(layout))
|
_, err := t.New("layout").Parse(string(layout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("RenderTemplate(.layout.gohtml): %s", err)
|
console.Error("RenderTemplate(.layout.gohtml): %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("RenderTemplate: .layout.gohtml not found to wrap %s", tmpl)
|
console.Error("RenderTemplate: .layout.gohtml not found to wrap %s", tmpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Render Templ: %s", tmpl)
|
console.Debug("Render Templ: %s", tmpl)
|
||||||
if err := t.ExecuteTemplate(w, "layout", vars); err != nil {
|
if err := t.ExecuteTemplate(w, "layout", vars); err != nil {
|
||||||
log.Printf("RenderTemplate(%s): %s", tmpl, err)
|
console.Error("RenderTemplate(%s): %s", tmpl, err)
|
||||||
}
|
}
|
||||||
log.Println("Done")
|
|
||||||
return nil
|
return nil
|
||||||
} else {
|
|
||||||
Panic(w, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Panic(w, http.StatusInternalServerError, err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
package gophertype
|
package gophertype
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gophertype/pkg/authentication"
|
"git.kirsle.net/apps/gophertype/pkg/authentication"
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/controllers"
|
"git.kirsle.net/apps/gophertype/pkg/controllers"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/glue"
|
"git.kirsle.net/apps/gophertype/pkg/glue"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/middleware"
|
"git.kirsle.net/apps/gophertype/pkg/middleware"
|
||||||
|
@ -21,8 +20,9 @@ func (s *Site) SetupRouter() error {
|
||||||
router.Use(authentication.Middleware)
|
router.Use(authentication.Middleware)
|
||||||
router.Use(middleware.CSRF)
|
router.Use(middleware.CSRF)
|
||||||
|
|
||||||
|
console.Debug("Setting up HTTP Router")
|
||||||
for _, route := range glue.GetControllers() {
|
for _, route := range glue.GetControllers() {
|
||||||
log.Printf("Register: %+v", route)
|
console.Debug("Register: %+v", route)
|
||||||
if len(route.Methods) == 0 {
|
if len(route.Methods) == 0 {
|
||||||
route.Methods = []string{"GET"}
|
route.Methods = []string{"GET"}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ func (s *Site) SetupRouter() error {
|
||||||
route.Methods = append(route.Methods, "HEAD")
|
route.Methods = append(route.Methods, "HEAD")
|
||||||
|
|
||||||
if len(route.Middleware) > 0 {
|
if len(route.Middleware) > 0 {
|
||||||
log.Printf("%+v has middlewares!", route)
|
console.Debug("%+v has middlewares!", route)
|
||||||
|
|
||||||
handler := route.Middleware[0](http.HandlerFunc(route.Handler))
|
handler := route.Middleware[0](http.HandlerFunc(route.Handler))
|
||||||
router.Handle(route.Path, handler).Methods(route.Methods...)
|
router.Handle(route.Path, handler).Methods(route.Methods...)
|
||||||
|
@ -42,11 +42,11 @@ func (s *Site) SetupRouter() error {
|
||||||
router.PathPrefix("/").HandlerFunc(controllers.CatchAllHandler)
|
router.PathPrefix("/").HandlerFunc(controllers.CatchAllHandler)
|
||||||
router.NotFoundHandler = http.HandlerFunc(controllers.CatchAllHandler)
|
router.NotFoundHandler = http.HandlerFunc(controllers.CatchAllHandler)
|
||||||
|
|
||||||
log.Println("Walk the mux.Router:")
|
console.Debug("Walk the mux.Router:")
|
||||||
router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||||
tpl, err1 := route.GetPathTemplate()
|
tpl, err1 := route.GetPathTemplate()
|
||||||
met, err2 := route.GetMethods()
|
met, err2 := route.GetMethods()
|
||||||
fmt.Println(tpl, err1, met, err2)
|
console.Debug("path:%s methods:%s path-err:%s met-err:%s", tpl, met, err1, err2)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/src/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store holds your cookie store information.
|
// Store holds your cookie store information.
|
||||||
|
@ -15,7 +15,6 @@ var Store sessions.Store
|
||||||
|
|
||||||
// SetSecretKey initializes a session cookie store with the secret key.
|
// SetSecretKey initializes a session cookie store with the secret key.
|
||||||
func SetSecretKey(keyPairs ...[]byte) {
|
func SetSecretKey(keyPairs ...[]byte) {
|
||||||
fmt.Printf("XXXXX SetSecretKey: %+v", keyPairs)
|
|
||||||
Store = sessions.NewCookieStore(keyPairs...)
|
Store = sessions.NewCookieStore(keyPairs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +26,9 @@ func SetSecretKey(keyPairs ...[]byte) {
|
||||||
// context.
|
// context.
|
||||||
func Middleware(next http.Handler) http.Handler {
|
func Middleware(next http.Handler) http.Handler {
|
||||||
middleware := func(w http.ResponseWriter, r *http.Request) {
|
middleware := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Set the HTML content-type header by default until overridden by a handler.
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
// Store the current datetime on the request context.
|
// Store the current datetime on the request context.
|
||||||
ctx := context.WithValue(r.Context(), StartTimeKey, time.Now())
|
ctx := context.WithValue(r.Context(), StartTimeKey, time.Now())
|
||||||
|
|
||||||
|
@ -47,13 +49,13 @@ func Get(r *http.Request) *sessions.Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
if session, ok := ctx.Value(types.SessionKey).(*sessions.Session); ok {
|
if session, ok := ctx.Value(SessionKey).(*sessions.Session); ok {
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the session wasn't on the request, it means I broke something.
|
// If the session wasn't on the request, it means I broke something.
|
||||||
fmt.Printf(
|
console.Warn(
|
||||||
"ERROR: Session(): didn't find session in request context! Getting it " +
|
"Session(): didn't find session in request context! Getting it " +
|
||||||
"from the session store instead.",
|
"from the session store instead.",
|
||||||
)
|
)
|
||||||
session, _ := Store.Get(r, "session")
|
session, _ := Store.Get(r, "session")
|
||||||
|
|
|
@ -3,7 +3,14 @@ package settings
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/console"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/session"
|
"git.kirsle.net/apps/gophertype/pkg/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,7 +43,53 @@ type Spec struct {
|
||||||
MailPassword string
|
MailPassword string
|
||||||
|
|
||||||
// Security
|
// Security
|
||||||
SecretKey string `json:"-"`
|
SecretKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename is the path where the settings.json file is saved to.
|
||||||
|
var (
|
||||||
|
configFilename = ".settings.json"
|
||||||
|
configPath string
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetFilename sets the config file path, to be inside the user root.
|
||||||
|
func SetFilename(userRoot string) error {
|
||||||
|
path := filepath.Join(userRoot, configFilename)
|
||||||
|
configPath = path
|
||||||
|
|
||||||
|
// Initialize it for the first time?
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
console.Warn("No settings.json found; initializing a new settings file at %s", path)
|
||||||
|
fh, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't initialize settings.json at %s: %s", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Current.ToJSON(fh)
|
||||||
|
fh.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the stored file instead.
|
||||||
|
fh, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't read settings.json from %s: %s", path, err)
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(fh)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't read data from settings.json at %s: %s", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, err := FromJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("settings.json parse error from %s: %s", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Current = spec
|
||||||
|
session.SetSecretKey([]byte(Current.SecretKey))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load gets or creates the App Settings.
|
// Load gets or creates the App Settings.
|
||||||
|
@ -52,11 +105,33 @@ func Load() Spec {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromJSON loads a settings JSON from bytes.
|
||||||
|
func FromJSON(data []byte) (Spec, error) {
|
||||||
|
var s Spec
|
||||||
|
err := json.Unmarshal(data, &s)
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON converts the settings spec to JSON.
|
||||||
|
func (s Spec) ToJSON(w io.Writer) error {
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", "\t")
|
||||||
|
err := enc.Encode(s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Save the settings to DB.
|
// Save the settings to DB.
|
||||||
func (s Spec) Save() error {
|
func (s Spec) Save() error {
|
||||||
Current = s
|
Current = s
|
||||||
session.SetSecretKey([]byte(s.SecretKey))
|
session.SetSecretKey([]byte(s.SecretKey))
|
||||||
return nil
|
|
||||||
|
fh, err := os.Create(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
return s.ToJSON(fh)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeSecretKey generates a secret key for signing HTTP cookies.
|
// MakeSecretKey generates a secret key for signing HTTP cookies.
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{{ define "title" }}Bad Request{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>Bad Request</h1>
|
||||||
|
|
||||||
|
{{ or .Message "A problem has occurred." }}
|
||||||
|
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{{ define "title" }}Forbidden{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>Forbidden</h1>
|
||||||
|
|
||||||
|
{{ or .Message "Access denied." }}
|
||||||
|
|
||||||
|
{{ end }}
|
Loading…
Reference in New Issue