diff --git a/.gitignore b/.gitignore index 8d6e118..bf034f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/ pkg/bundled/ +public_html/.settings.json *.sqlite diff --git a/cmd/gophertype/main.go b/cmd/gophertype/main.go index a3f44bb..10524d0 100644 --- a/cmd/gophertype/main.go +++ b/cmd/gophertype/main.go @@ -8,6 +8,7 @@ import ( "os" "git.kirsle.net/apps/gophertype/pkg" + "git.kirsle.net/apps/gophertype/pkg/console" _ "git.kirsle.net/apps/gophertype/pkg/controllers" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/postgres" @@ -24,6 +25,7 @@ var ( var ( optDebug bool optBind string + optRoot string // Database option flags. optSQLite string @@ -38,6 +40,7 @@ var ( func init() { flag.BoolVar(&optDebug, "debug", false, "Debug level logging") 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. flag.StringVar(&optSQLite, "sqlite3", "", "Use SQLite database, default 'database.sqlite'") @@ -52,6 +55,8 @@ func init() { func main() { flag.Parse() + console.SetDebug(optDebug) + // Validate the choice of database. if optSQLite != "" { dbDriver = "sqlite3" @@ -74,7 +79,7 @@ func main() { fmt.Println("Hello world") - app := gophertype.NewSite() + app := gophertype.NewSite(optRoot) app.UseDB(dbDriver, dbPath) app.SetupRouter() app.ListenAndServe(optBind) diff --git a/go.mod b/go.mod index 3a1b71d..a744076 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gorilla/sessions v1.2.0 github.com/jinzhu/gorm v1.9.11 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/urfave/negroni v1.0.0 golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 diff --git a/go.sum b/go.sum index b9f2d14..6dcb36d 100644 --- a/go.sum +++ b/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.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= diff --git a/pkg/app.go b/pkg/app.go index 58c7b14..b55918e 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -1,10 +1,11 @@ package gophertype import ( - "log" "net/http" + "git.kirsle.net/apps/gophertype/pkg/console" "git.kirsle.net/apps/gophertype/pkg/models" + "git.kirsle.net/apps/gophertype/pkg/settings" "github.com/gorilla/mux" "github.com/jinzhu/gorm" "github.com/urfave/negroni" @@ -17,7 +18,12 @@ type Site struct { } // 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{} n := negroni.New() @@ -35,13 +41,13 @@ func (s *Site) UseDB(driver string, path string) error { return err } - log.Printf("Using database driver '%s'", driver) + console.Info("Using database driver '%s'", driver) models.UseDB(db) return nil } // ListenAndServe starts the HTTP service. 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) } diff --git a/pkg/authentication/login.go b/pkg/authentication/login.go index f2701d7..f3a98a2 100644 --- a/pkg/authentication/login.go +++ b/pkg/authentication/login.go @@ -3,9 +3,9 @@ package authentication import ( "context" "errors" - "log" "net/http" + "git.kirsle.net/apps/gophertype/pkg/console" "git.kirsle.net/apps/gophertype/pkg/models" "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["user-id"] = int(user.ID) if err := sess.Save(r, w); err != nil { - log.Printf("ERROR: Login() Session error: " + err.Error()) + console.Error("Login() Session error: " + err.Error()) } } diff --git a/pkg/console/log.go b/pkg/console/log.go new file mode 100644 index 0000000..ca875c1 --- /dev/null +++ b/pkg/console/log.go @@ -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...) +} diff --git a/pkg/controllers/authentication.go b/pkg/controllers/authentication.go index f673f38..9027d38 100644 --- a/pkg/controllers/authentication.go +++ b/pkg/controllers/authentication.go @@ -1,7 +1,6 @@ package controllers import ( - "log" "net/http" "git.kirsle.net/apps/gophertype/pkg/authentication" @@ -32,7 +31,6 @@ func init() { val.Require("password") if val.HasErrors() { v.ValidationError = val.ErrorMap() - log.Printf("validation: %+v", v.ValidationError) break } diff --git a/pkg/controllers/initial_setup.go b/pkg/controllers/initial_setup.go index b6e6093..8924c04 100644 --- a/pkg/controllers/initial_setup.go +++ b/pkg/controllers/initial_setup.go @@ -3,7 +3,6 @@ package controllers import ( "errors" "fmt" - "log" "net/http" "git.kirsle.net/apps/gophertype/pkg/constants" @@ -23,81 +22,86 @@ func init() { Middleware: []mux.MiddlewareFunc{ middleware.ExampleMiddleware, }, - Handler: func(w http.ResponseWriter, r *http.Request) { - // 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) - }, + Handler: InitialSetup, }) } + +// 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) +} diff --git a/pkg/controllers/static_files.go b/pkg/controllers/static_files.go index 305a295..a3b068f 100644 --- a/pkg/controllers/static_files.go +++ b/pkg/controllers/static_files.go @@ -1,10 +1,10 @@ package controllers import ( - "log" "net/http" "strings" + "git.kirsle.net/apps/gophertype/pkg/console" "git.kirsle.net/apps/gophertype/pkg/responses" ) @@ -13,7 +13,7 @@ import ( // templates from the builtin web root or the user root. func CatchAllHandler(w http.ResponseWriter, r *http.Request) { path := r.URL.Path - log.Printf("Wildcard path: %s", path) + console.Debug("Wildcard path: %s", path) // Resolve the target path. filepath, err := responses.ResolveFile(path) diff --git a/pkg/glue/controller.go b/pkg/glue/controller.go index 2dc73c4..58702fb 100644 --- a/pkg/glue/controller.go +++ b/pkg/glue/controller.go @@ -2,7 +2,6 @@ package glue import ( "fmt" - "log" "net/http" "sort" "sync" @@ -33,7 +32,6 @@ func Register(e Endpoint) { } registry[e.Path] = e registryLock.Unlock() - log.Printf("Register: %s", e.Path) } // GetControllers returns all the routes and handler functions. diff --git a/pkg/middleware/example.go b/pkg/middleware/example.go index 9aa60b6..f5f76bf 100644 --- a/pkg/middleware/example.go +++ b/pkg/middleware/example.go @@ -1,14 +1,15 @@ package middleware import ( - "log" "net/http" + + "git.kirsle.net/apps/gophertype/pkg/console" ) // ExampleMiddleware is a test middleware. func ExampleMiddleware(next http.Handler) http.Handler { 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) } diff --git a/pkg/models/app_settings.go b/pkg/models/app_settings.go deleted file mode 100644 index 7e53edb..0000000 --- a/pkg/models/app_settings.go +++ /dev/null @@ -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) -} diff --git a/pkg/models/models.go b/pkg/models/models.go index 11cbaf0..72e3d9b 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -8,6 +8,5 @@ var DB *gorm.DB // UseDB registers a database driver. func UseDB(db *gorm.DB) { DB = db - DB.AutoMigrate(&AppSetting{}) DB.AutoMigrate(&User{}) } diff --git a/pkg/models/users.go b/pkg/models/users.go index 3673c6e..71795b7 100644 --- a/pkg/models/users.go +++ b/pkg/models/users.go @@ -3,9 +3,9 @@ package models import ( "errors" "fmt" - "log" "strings" + "git.kirsle.net/apps/gophertype/pkg/console" "git.kirsle.net/apps/gophertype/pkg/constants" "github.com/jinzhu/gorm" "golang.org/x/crypto/bcrypt" @@ -36,7 +36,7 @@ func (u *User) Validate() error { func AuthenticateUser(email string, password string) (User, error) { user, err := GetUserByEmail(email) 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") } @@ -69,14 +69,13 @@ func (u *User) SetPassword(password string) error { } u.HashedPassword = string(hash) - fmt.Printf("Set hashed password: %s", u.HashedPassword) return nil } // VerifyPassword checks if the password matches the user's hashed password. func (u *User) VerifyPassword(password string) bool { if u.HashedPassword == "" { - fmt.Printf("ERROR: VerifyPassword: user has no HashedPassword") + console.Error("ERROR: VerifyPassword: user %s has no HashedPassword", u.Email) return false } @@ -84,7 +83,7 @@ func (u *User) VerifyPassword(password string) bool { if err == nil { return true } - log.Printf("ERROR: VerifyPassword: %s", err) + console.Error("VerifyPassword: %s", err) return false } diff --git a/pkg/responses/errors.go b/pkg/responses/errors.go index fc5cf85..5caddbb 100644 --- a/pkg/responses/errors.go +++ b/pkg/responses/errors.go @@ -1,8 +1,10 @@ package responses import ( - "log" + "fmt" "net/http" + + "git.kirsle.net/apps/gophertype/pkg/console" ) // 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) 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)) } } // NotFound returns an HTML 404 page. func NotFound(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) 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") } } // BadRequest returns a 400 Bad Request page. func BadRequest(w http.ResponseWriter, r *http.Request, vars interface{}) { - if err := RenderTemplate(w, r, "_builtin/errors/generic.gohtml", vars); err != nil { - log.Printf("responses.NotFound: failed to render a pretty error template: %s", err) + w.WriteHeader(http.StatusBadRequest) + 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") } } + +// 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") + } +} diff --git a/pkg/responses/template_functions.go b/pkg/responses/template_functions.go index bad85ec..f8eb624 100644 --- a/pkg/responses/template_functions.go +++ b/pkg/responses/template_functions.go @@ -20,7 +20,6 @@ func TemplateFuncs(r *http.Request) template.FuncMap { // CSRF returns the current CSRF token as an HTML hidden form field. func CSRF(r *http.Request) func() template.HTML { return func() template.HTML { - fmt.Println("CSRF() func called") token, _ := r.Cookie(constants.CSRFCookieName) return template.HTML(fmt.Sprintf( ``, diff --git a/pkg/responses/templates.go b/pkg/responses/templates.go index 30d1903..ed31cdc 100644 --- a/pkg/responses/templates.go +++ b/pkg/responses/templates.go @@ -1,10 +1,10 @@ package responses import ( - "fmt" "html/template" - "log" "net/http" + + "git.kirsle.net/apps/gophertype/pkg/console" ) // 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) } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - // 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)) if err != nil { - log.Printf("bundled template '%s': %s", tmpl, err) + console.Error("RenderTemplate: bundled template '%s': %s", tmpl, 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 { _, err := t.New("layout").Parse(string(layout)) if err != nil { - log.Printf("RenderTemplate(.layout.gohtml): %s", err) + console.Error("RenderTemplate(.layout.gohtml): %s", err) } } 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 { - log.Printf("RenderTemplate(%s): %s", tmpl, err) + console.Error("RenderTemplate(%s): %s", tmpl, err) } - log.Println("Done") return nil - } else { - Panic(w, http.StatusInternalServerError, err.Error()) } + Panic(w, http.StatusInternalServerError, err.Error()) return nil } diff --git a/pkg/routes.go b/pkg/routes.go index 3499884..395a65d 100644 --- a/pkg/routes.go +++ b/pkg/routes.go @@ -1,11 +1,10 @@ package gophertype import ( - "fmt" - "log" "net/http" "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/glue" "git.kirsle.net/apps/gophertype/pkg/middleware" @@ -21,8 +20,9 @@ func (s *Site) SetupRouter() error { router.Use(authentication.Middleware) router.Use(middleware.CSRF) + console.Debug("Setting up HTTP Router") for _, route := range glue.GetControllers() { - log.Printf("Register: %+v", route) + console.Debug("Register: %+v", route) if len(route.Methods) == 0 { route.Methods = []string{"GET"} } @@ -30,7 +30,7 @@ func (s *Site) SetupRouter() error { route.Methods = append(route.Methods, "HEAD") 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)) router.Handle(route.Path, handler).Methods(route.Methods...) @@ -42,11 +42,11 @@ func (s *Site) SetupRouter() error { router.PathPrefix("/").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 { tpl, err1 := route.GetPathTemplate() 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 }) diff --git a/pkg/session/sessions.go b/pkg/session/sessions.go index 471d5c4..1e5adde 100644 --- a/pkg/session/sessions.go +++ b/pkg/session/sessions.go @@ -6,8 +6,8 @@ import ( "net/http" "time" + "git.kirsle.net/apps/gophertype/pkg/console" "github.com/gorilla/sessions" - "github.com/kirsle/blog/src/types" ) // Store holds your cookie store information. @@ -15,7 +15,6 @@ var Store sessions.Store // SetSecretKey initializes a session cookie store with the secret key. func SetSecretKey(keyPairs ...[]byte) { - fmt.Printf("XXXXX SetSecretKey: %+v", keyPairs) Store = sessions.NewCookieStore(keyPairs...) } @@ -27,6 +26,9 @@ func SetSecretKey(keyPairs ...[]byte) { // context. func Middleware(next http.Handler) http.Handler { 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. ctx := context.WithValue(r.Context(), StartTimeKey, time.Now()) @@ -47,13 +49,13 @@ func Get(r *http.Request) *sessions.Session { } ctx := r.Context() - if session, ok := ctx.Value(types.SessionKey).(*sessions.Session); ok { + if session, ok := ctx.Value(SessionKey).(*sessions.Session); ok { return session } // If the session wasn't on the request, it means I broke something. - fmt.Printf( - "ERROR: Session(): didn't find session in request context! Getting it " + + console.Warn( + "Session(): didn't find session in request context! Getting it " + "from the session store instead.", ) session, _ := Store.Get(r, "session") diff --git a/pkg/settings/settings.go b/pkg/settings/settings.go index ce475e3..09d72d5 100644 --- a/pkg/settings/settings.go +++ b/pkg/settings/settings.go @@ -3,7 +3,14 @@ package settings import ( "crypto/rand" "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" ) @@ -36,7 +43,53 @@ type Spec struct { MailPassword string // 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. @@ -52,11 +105,33 @@ func Load() Spec { 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. func (s Spec) Save() error { Current = s 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. diff --git a/public_html/dummy.txt b/public_html/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/pvt-www/_builtin/errors/400.gohtml b/pvt-www/_builtin/errors/400.gohtml new file mode 100644 index 0000000..5ed00b5 --- /dev/null +++ b/pvt-www/_builtin/errors/400.gohtml @@ -0,0 +1,7 @@ +{{ define "title" }}Bad Request{{ end }} +{{ define "content" }} +

Bad Request

+ +{{ or .Message "A problem has occurred." }} + +{{ end }} diff --git a/pvt-www/_builtin/errors/403.gohtml b/pvt-www/_builtin/errors/403.gohtml new file mode 100644 index 0000000..d96abb7 --- /dev/null +++ b/pvt-www/_builtin/errors/403.gohtml @@ -0,0 +1,7 @@ +{{ define "title" }}Forbidden{{ end }} +{{ define "content" }} +

Forbidden

+ +{{ or .Message "Access denied." }} + +{{ end }}