mirror of
https://github.com/dnote/dnote
synced 2026-03-15 23:15:50 +01:00
Implement MVC
This commit is contained in:
parent
6eb68d1817
commit
bb0ef20c72
20 changed files with 918 additions and 14 deletions
7
go.mod
7
go.mod
|
|
@ -8,14 +8,15 @@ require (
|
|||
github.com/aymerick/douceur v0.2.0
|
||||
github.com/dnote/actions v0.2.0
|
||||
github.com/dnote/color v1.7.0
|
||||
github.com/dnote/xgo v0.0.0-20200205013105-40be7d6d43ff // indirect
|
||||
github.com/gobuffalo/packr/v2 v2.8.1
|
||||
github.com/google/go-cmp v0.5.4
|
||||
github.com/google/go-github v17.0.0+incompatible
|
||||
github.com/google/go-querystring v1.0.0 // indirect
|
||||
github.com/google/uuid v1.1.3
|
||||
github.com/gorilla/csrf v1.6.2
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/schema v1.2.0
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/karrick/godirwalk v1.16.1 // indirect
|
||||
|
|
@ -25,16 +26,18 @@ require (
|
|||
github.com/pkg/errors v0.9.1
|
||||
github.com/radovskyb/watcher v1.0.7
|
||||
github.com/robfig/cron v1.2.0
|
||||
github.com/rogpeppe/go-internal v1.6.2 // indirect
|
||||
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
github.com/spf13/cobra v1.1.1
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect
|
||||
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 // indirect
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
|
|
|||
19
go.sum
19
go.sum
|
|
@ -82,8 +82,6 @@ github.com/dnote/actions v0.2.0 h1:P1ut2/QRKwfAzIIB374vN9A4IanU94C/payEocvngYo=
|
|||
github.com/dnote/actions v0.2.0/go.mod h1:bBIassLhppVQdbC3iaE92SHBpM1HOVe+xZoAlj9ROxw=
|
||||
github.com/dnote/color v1.7.0 h1:8/QGLQKSU8/zcWQaHbMyC1hJRkKO/Uu9M89sH76ecHE=
|
||||
github.com/dnote/color v1.7.0/go.mod h1:75UcP/TH7CNvjQ5pwDumkUS3vkPdGggy7/3fT8MlxHM=
|
||||
github.com/dnote/xgo v0.0.0-20200205013105-40be7d6d43ff h1:DJKdzouhr6u1NzuLbmSWeei9BagH3Nm4mSOzP0RMdc0=
|
||||
github.com/dnote/xgo v0.0.0-20200205013105-40be7d6d43ff/go.mod h1:ruGZjl8WThApI7BAIKV2Q/PnJoudvd6Epjc3z79jWVg=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
|
|
@ -164,12 +162,18 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
|||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/csrf v1.6.2 h1:QqQ/OWwuFp4jMKgBFAzJVW3FMULdyUW7JoM4pEWuqKg=
|
||||
github.com/gorilla/csrf v1.6.2/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
|
|
@ -264,8 +268,6 @@ github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO
|
|||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
|
|
@ -354,6 +356,8 @@ github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
|||
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0=
|
||||
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9riaI4zh2eXWKmCSDqVS/XH1MRHLKRwk=
|
||||
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
|
|
@ -493,8 +497,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -562,11 +566,14 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200308013534-11ec41452d41 h1:9Di9iYgOt9ThCipBxChBVhgNipDoE5mxO84rQV7D0FE=
|
||||
golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ type Config struct {
|
|||
DisableRegistration bool
|
||||
Port string
|
||||
DB PostgresConfig
|
||||
PageTemplateDir string
|
||||
StaticDir string
|
||||
}
|
||||
|
||||
// Load constructs and returns a new config based on the environment variables.
|
||||
|
|
@ -108,6 +110,16 @@ func (c *Config) SetOnPremise(val bool) {
|
|||
c.OnPremise = val
|
||||
}
|
||||
|
||||
// SetPageTemplateDir sets page template dir for the config
|
||||
func (c *Config) SetPageTemplateDir(d string) {
|
||||
c.PageTemplateDir = d
|
||||
}
|
||||
|
||||
// SetStaticDir sets static dir for the confi
|
||||
func (c *Config) SetStaticDir(d string) {
|
||||
c.StaticDir = d
|
||||
}
|
||||
|
||||
func validate(c Config) error {
|
||||
if _, err := url.ParseRequestURI(c.WebURL); err != nil {
|
||||
return errors.Wrapf(ErrWebURLInvalid, "provided: '%s'", c.WebURL)
|
||||
|
|
|
|||
30
pkg/server/context/user.go
Normal file
30
pkg/server/context/user.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
)
|
||||
|
||||
const (
|
||||
userKey privateKey = "user"
|
||||
)
|
||||
|
||||
type privateKey string
|
||||
|
||||
// WithUser creates a new context with the given user
|
||||
func WithUser(ctx context.Context, user *database.User) context.Context {
|
||||
return context.WithValue(ctx, userKey, user)
|
||||
}
|
||||
|
||||
// User retrieves a user from the given context. It returns a pointer to
|
||||
// a user. If the context does not contain a user, it returns nil.
|
||||
func User(ctx context.Context) *database.User {
|
||||
if temp := ctx.Value(userKey); temp != nil {
|
||||
if user, ok := temp.(*database.User); ok {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
21
pkg/server/controllers/controllers.go
Normal file
21
pkg/server/controllers/controllers.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Controllers is a group of controllers
|
||||
type Controllers struct {
|
||||
Users *Users
|
||||
}
|
||||
|
||||
// New returns a new group of controllers
|
||||
func New(cfg config.Config, db *gorm.DB, cl clock.Clock) *Controllers {
|
||||
c := Controllers{}
|
||||
|
||||
c.Users = NewUsers(cfg, db)
|
||||
|
||||
return &c
|
||||
}
|
||||
181
pkg/server/controllers/helpers.go
Normal file
181
pkg/server/controllers/helpers.go
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeForm = "application/x-www-form-urlencoded"
|
||||
contentTypeJSON = "application/json"
|
||||
)
|
||||
|
||||
func parseRequestData(r *http.Request, dst interface{}) error {
|
||||
ct := r.Header.Get("Content-Type")
|
||||
|
||||
if ct == contentTypeForm {
|
||||
if err := parseForm(r, dst); err != nil {
|
||||
return errors.Wrap(err, "parsing form")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// default to JSON
|
||||
if err := parseJSON(r, dst); err != nil {
|
||||
return errors.Wrap(err, "parsing JSON")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseForm(r *http.Request, dst interface{}) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
return parseValues(r.PostForm, dst)
|
||||
}
|
||||
|
||||
func parseURLParams(r *http.Request, dst interface{}) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
return parseValues(r.Form, dst)
|
||||
}
|
||||
|
||||
func parseValues(values url.Values, dst interface{}) error {
|
||||
dec := schema.NewDecoder()
|
||||
|
||||
// Ignore CSRF token field
|
||||
dec.IgnoreUnknownKeys(true)
|
||||
|
||||
if err := dec.Decode(dst, values); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseJSON(r *http.Request, dst interface{}) error {
|
||||
dec := json.NewDecoder(r.Body)
|
||||
|
||||
if err := dec.Decode(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCredential extracts a session key from the request from the request header. Concretely,
|
||||
// it first looks at the 'Cookie' and then the 'Authorization' header. If no credential is found,
|
||||
// it returns an empty string.
|
||||
func GetCredential(r *http.Request) (string, error) {
|
||||
ret, err := getSessionKeyFromCookie(r)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "getting session key from cookie")
|
||||
}
|
||||
if ret != "" {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
ret, err = getSessionKeyFromAuth(r)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "getting session key from Authorization header")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// getSessionKeyFromCookie reads and returns a session key from the cookie sent by the
|
||||
// request. If no session key is found, it returns an empty string
|
||||
func getSessionKeyFromCookie(r *http.Request) (string, error) {
|
||||
c, err := r.Cookie("id")
|
||||
|
||||
if err == http.ErrNoCookie {
|
||||
return "", nil
|
||||
} else if err != nil {
|
||||
return "", errors.Wrap(err, "reading cookie")
|
||||
}
|
||||
|
||||
return c.Value, nil
|
||||
}
|
||||
|
||||
// getSessionKeyFromAuth reads and returns a session key from the Authorization header
|
||||
func getSessionKeyFromAuth(r *http.Request) (string, error) {
|
||||
h := r.Header.Get("Authorization")
|
||||
if h == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
payload, err := parseAuthHeader(h)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "parsing the authorization header")
|
||||
}
|
||||
if payload.scheme != "Bearer" {
|
||||
return "", errors.New("unsupported scheme")
|
||||
}
|
||||
|
||||
return payload.credential, nil
|
||||
}
|
||||
|
||||
func parseAuthHeader(h string) (authHeader, error) {
|
||||
parts := strings.Split(h, " ")
|
||||
|
||||
if len(parts) != 2 {
|
||||
return authHeader{}, errors.New("Invalid authorization header")
|
||||
}
|
||||
|
||||
parsed := authHeader{
|
||||
scheme: parts[0],
|
||||
credential: parts[1],
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
type authHeader struct {
|
||||
scheme string
|
||||
credential string
|
||||
}
|
||||
|
||||
const (
|
||||
sessionCookieName = "id"
|
||||
sessionCookiePath = "/"
|
||||
)
|
||||
|
||||
func setSessionCookie(w http.ResponseWriter, key string, expires time.Time) {
|
||||
cookie := http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: key,
|
||||
Expires: expires,
|
||||
Path: sessionCookiePath,
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
func unsetSessionCookie(w http.ResponseWriter) {
|
||||
expires := time.Now().Add(time.Hour * -24 * 30)
|
||||
cookie := http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: "",
|
||||
Expires: expires,
|
||||
Path: sessionCookiePath,
|
||||
HttpOnly: true,
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
// SessionResponse is a response containing a session information
|
||||
type SessionResponse struct {
|
||||
Key string `json:"key"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
45
pkg/server/controllers/users.go
Normal file
45
pkg/server/controllers/users.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/dnote/dnote/pkg/server/views"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// NewUsers creates a new Users controller.
|
||||
// It panics if the necessary templates are not parsed.
|
||||
func NewUsers(cfg config.Config, db *gorm.DB) *Users {
|
||||
return &Users{
|
||||
NewView: views.NewView(cfg.PageTemplateDir, views.Config{Title: "Join", Layout: "base"}, "users/new"),
|
||||
onPremise: cfg.OnPremise,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Users is a user controller.
|
||||
type Users struct {
|
||||
NewView *views.View
|
||||
db *gorm.DB
|
||||
onPremise bool
|
||||
}
|
||||
|
||||
// New handles GET /register
|
||||
func (u *Users) New(w http.ResponseWriter, r *http.Request) {
|
||||
var form RegistrationForm
|
||||
parseURLParams(r, &form)
|
||||
u.NewView.Render(w, r, form)
|
||||
}
|
||||
|
||||
// RegistrationForm is the form data for registering
|
||||
type RegistrationForm struct {
|
||||
Email string `schema:"email"`
|
||||
Password string `schema:"password"`
|
||||
}
|
||||
|
||||
// LoginForm is the form data for log in
|
||||
type LoginForm struct {
|
||||
Email string `schema:"email" json:"email"`
|
||||
Password string `schema:"password" json:"password"`
|
||||
}
|
||||
|
|
@ -28,9 +28,11 @@ import (
|
|||
"github.com/dnote/dnote/pkg/server/api"
|
||||
"github.com/dnote/dnote/pkg/server/app"
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/dnote/dnote/pkg/server/controllers"
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/job"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/dnote/dnote/pkg/server/routes"
|
||||
"github.com/dnote/dnote/pkg/server/web"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
|
|
@ -42,6 +44,9 @@ var versionTag = "master"
|
|||
var port = flag.String("port", "3000", "port to connect to")
|
||||
var rootBox *packr.Box
|
||||
|
||||
var pageDir = flag.String("pageDir", "views", "the path to a directory containing page templates")
|
||||
var staticDir = flag.String("staticDir", "./static/", "the path to the static directory ")
|
||||
|
||||
func init() {
|
||||
rootBox = packr.New("root", "../../web/public")
|
||||
}
|
||||
|
|
@ -124,9 +129,11 @@ func runJob(a app.App) error {
|
|||
}
|
||||
|
||||
func startCmd() {
|
||||
c := config.Load()
|
||||
cfg := config.Load()
|
||||
cfg.SetPageTemplateDir(*pageDir)
|
||||
cfg.SetStaticDir(*staticDir)
|
||||
|
||||
app := initApp(c)
|
||||
app := initApp(cfg)
|
||||
defer app.DB.Close()
|
||||
|
||||
if err := database.Migrate(app.DB); err != nil {
|
||||
|
|
@ -137,13 +144,19 @@ func startCmd() {
|
|||
panic(errors.Wrap(err, "running job"))
|
||||
}
|
||||
|
||||
srv, err := initServer(app)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "initializing server"))
|
||||
cl := clock.New()
|
||||
ctl := controllers.New(cfg, app.DB, cl)
|
||||
|
||||
rc := routes.RouteConfig{
|
||||
WebRoutes: routes.NewWebRoutes(cfg, ctl, cl),
|
||||
APIRoutes: routes.NewAPIRoutes(cfg, ctl, cl),
|
||||
Controllers: ctl,
|
||||
}
|
||||
|
||||
r := routes.New(cfg, rc)
|
||||
|
||||
log.Printf("Dnote version %s is running on port %s", versionTag, *port)
|
||||
log.Fatalln(http.ListenAndServe(":"+*port, srv))
|
||||
log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%s", cfg.Port), r))
|
||||
}
|
||||
|
||||
func versionCmd() {
|
||||
|
|
|
|||
41
pkg/server/routes/logging.go
Normal file
41
pkg/server/routes/logging.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
)
|
||||
|
||||
// logResponseWriter wraps http.ResponseWriter to expose HTTP status code for logging.
|
||||
// The optional interfaces of http.ResponseWriter are lost because of the wrapping, and
|
||||
// such interfaces should be implemented if needed. (i.e. http.Pusher, http.Flusher, etc.)
|
||||
type logResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *logResponseWriter) WriteHeader(code int) {
|
||||
w.statusCode = code
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// LoggingMw is a middleware that logs incoming HTTP requests
|
||||
func LoggingMw(inner http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
lw := logResponseWriter{w, http.StatusOK}
|
||||
inner.ServeHTTP(&lw, r)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"remoteAddr": lookupIP(r),
|
||||
"uri": r.RequestURI,
|
||||
"statusCode": lw.statusCode,
|
||||
"method": r.Method,
|
||||
"duration": fmt.Sprintf("%dms", time.Since(start)/1000000),
|
||||
"userAgent": r.Header.Get("User-Agent"),
|
||||
}).Info("incoming request")
|
||||
}
|
||||
}
|
||||
37
pkg/server/routes/middleware.go
Normal file
37
pkg/server/routes/middleware.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
)
|
||||
|
||||
type middleware func(h http.Handler, c config.Config, rateLimit bool) http.Handler
|
||||
|
||||
// lookupIP returns the request's IP
|
||||
func lookupIP(r *http.Request) string {
|
||||
realIP := r.Header.Get("X-Real-IP")
|
||||
forwardedFor := r.Header.Get("X-Forwarded-For")
|
||||
|
||||
if forwardedFor != "" {
|
||||
parts := strings.Split(forwardedFor, ",")
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
if realIP != "" {
|
||||
return realIP
|
||||
}
|
||||
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
// WebMw is the middleware for the web
|
||||
func WebMw(h http.Handler, c config.Config, rateLimit bool) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
// APIMw is the middleware for the API
|
||||
func APIMw(h http.Handler, c config.Config, rateLimit bool) http.Handler {
|
||||
return h
|
||||
}
|
||||
66
pkg/server/routes/routes.go
Normal file
66
pkg/server/routes/routes.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/dnote/dnote/pkg/server/controllers"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Route represents a single route
|
||||
type Route struct {
|
||||
Method string
|
||||
Pattern string
|
||||
Handler http.Handler
|
||||
RateLimit bool
|
||||
}
|
||||
|
||||
func registerRoutes(router *mux.Router, mw middleware, c config.Config, routes []Route) {
|
||||
for _, route := range routes {
|
||||
wrappedHandler := mw(route.Handler, c, route.RateLimit)
|
||||
|
||||
router.
|
||||
Handle(route.Pattern, wrappedHandler).
|
||||
Methods(route.Method)
|
||||
}
|
||||
}
|
||||
|
||||
// NewWebRoutes returns a new web routes
|
||||
func NewWebRoutes(cfg config.Config, c *controllers.Controllers, cl clock.Clock) []Route {
|
||||
return []Route{
|
||||
{"GET", "/", http.HandlerFunc(c.Users.New), true},
|
||||
}
|
||||
}
|
||||
|
||||
// NewAPIRoutes returns a new api routes
|
||||
func NewAPIRoutes(cfg config.Config, c *controllers.Controllers, cl clock.Clock) []Route {
|
||||
return []Route{}
|
||||
}
|
||||
|
||||
// RouteConfig is the configuration for routes
|
||||
type RouteConfig struct {
|
||||
Controllers *controllers.Controllers
|
||||
WebRoutes []Route
|
||||
APIRoutes []Route
|
||||
}
|
||||
|
||||
// New creates and returns a new router
|
||||
func New(cfg config.Config, rc RouteConfig) http.Handler {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
webRouter := router.PathPrefix("/").Subrouter()
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
registerRoutes(webRouter, WebMw, cfg, rc.WebRoutes)
|
||||
registerRoutes(apiRouter, APIMw, cfg, rc.APIRoutes)
|
||||
|
||||
// static
|
||||
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir(cfg.StaticDir)))
|
||||
router.PathPrefix("/static/").Handler(staticHandler)
|
||||
|
||||
// catch-all
|
||||
// router.PathPrefix("/").HandlerFunc(rc.Controllers.Static.NotFound)
|
||||
|
||||
return LoggingMw(router)
|
||||
}
|
||||
13
pkg/server/routes/user.go
Normal file
13
pkg/server/routes/user.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func userMw(inner http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
// ctx = context.WithUser(ctx, user)
|
||||
inner.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
153
pkg/server/views/data.go
Normal file
153
pkg/server/views/data.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// AlertLvlError is an alert level for error
|
||||
AlertLvlError = "danger"
|
||||
// AlertLvlWarning is an alert level for warning
|
||||
AlertLvlWarning = "warning"
|
||||
// AlertLvlInfo is an alert level for info
|
||||
AlertLvlInfo = "info"
|
||||
// AlertLvlSuccess is an alert level for success
|
||||
AlertLvlSuccess = "success"
|
||||
|
||||
// AlertMsgGeneric is a generic message for a server error
|
||||
AlertMsgGeneric = "Something went wrong. Please try again."
|
||||
)
|
||||
|
||||
// Alert is used to render Bootstrap Alert messages in templates
|
||||
type Alert struct {
|
||||
Level string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Data is the top level structure that views expect data to come in.
|
||||
type Data struct {
|
||||
Alert *Alert
|
||||
CSRF template.HTML
|
||||
User *database.User
|
||||
Yield interface{}
|
||||
}
|
||||
|
||||
func getErrMessage(err error) string {
|
||||
if pErr, ok := err.(PublicError); ok {
|
||||
return pErr.Public()
|
||||
}
|
||||
|
||||
return AlertMsgGeneric
|
||||
}
|
||||
|
||||
// SetAlert sets alert in the given data for given error.
|
||||
func (d *Data) SetAlert(err error) {
|
||||
errC := errors.Cause(err)
|
||||
|
||||
if pErr, ok := errC.(PublicError); ok {
|
||||
d.Alert = &Alert{
|
||||
Level: AlertLvlError,
|
||||
Message: pErr.Public(),
|
||||
}
|
||||
} else {
|
||||
d.Alert = &Alert{
|
||||
Level: AlertLvlError,
|
||||
Message: AlertMsgGeneric,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AlertError returns a new error alert using the given message.
|
||||
func (d *Data) AlertError(msg string) {
|
||||
d.Alert = &Alert{
|
||||
Level: AlertLvlError,
|
||||
Message: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func persistAlert(w http.ResponseWriter, alert Alert) {
|
||||
expiresAt := time.Now().Add(5 * time.Minute)
|
||||
lvl := http.Cookie{
|
||||
Name: "alert_level",
|
||||
Value: alert.Level,
|
||||
Expires: expiresAt,
|
||||
HttpOnly: true,
|
||||
}
|
||||
msg := http.Cookie{
|
||||
Name: "alert_message",
|
||||
Value: alert.Message,
|
||||
Expires: expiresAt,
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, &lvl)
|
||||
http.SetCookie(w, &msg)
|
||||
}
|
||||
|
||||
func clearAlert(w http.ResponseWriter) {
|
||||
lvl := http.Cookie{
|
||||
Name: "alert_level",
|
||||
Value: "",
|
||||
Expires: time.Now(),
|
||||
HttpOnly: true,
|
||||
}
|
||||
msg := http.Cookie{
|
||||
Name: "alert_message",
|
||||
Value: "",
|
||||
Expires: time.Now(),
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, &lvl)
|
||||
http.SetCookie(w, &msg)
|
||||
}
|
||||
|
||||
func getAlert(r *http.Request) *Alert {
|
||||
lvl, err := r.Cookie("alert_level")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
msg, err := r.Cookie("alert_message")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
alert := Alert{
|
||||
Level: lvl.Value,
|
||||
Message: msg.Value,
|
||||
}
|
||||
return &alert
|
||||
}
|
||||
|
||||
// RedirectAlert redirects to a URL after persisting the provided alert data
|
||||
// into a cookie so that it can be displayed when the page is rendered.
|
||||
func RedirectAlert(w http.ResponseWriter, r *http.Request, urlStr string, code int, alert Alert) {
|
||||
persistAlert(w, alert)
|
||||
http.Redirect(w, r, urlStr, code)
|
||||
}
|
||||
|
||||
// PublicError is an error meant to be displayed to the public
|
||||
type PublicError interface {
|
||||
error
|
||||
Public() string
|
||||
}
|
||||
|
||||
// BadRequestError is an error for bad request
|
||||
type BadRequestError interface {
|
||||
error
|
||||
IsBadRequest() bool
|
||||
}
|
||||
|
||||
// ConflictError is an error for bad request
|
||||
type ConflictError interface {
|
||||
error
|
||||
IsConflictError() bool
|
||||
}
|
||||
|
||||
// NotFoundError is an error for bad request
|
||||
type NotFoundError interface {
|
||||
error
|
||||
IsNotFoundError() bool
|
||||
}
|
||||
8
pkg/server/views/layouts/alert.gohtml
Normal file
8
pkg/server/views/layouts/alert.gohtml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{{define "alert"}}
|
||||
{{if .}}
|
||||
<div class="alert alert-{{.Level}} alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
{{.Message}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
18
pkg/server/views/layouts/base.gohtml
Normal file
18
pkg/server/views/layouts/base.gohtml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
|
||||
{{template "css" .}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{template "header" .}}
|
||||
|
||||
{{template "alert" .Alert}}
|
||||
|
||||
{{template "yield" .Yield}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
5
pkg/server/views/layouts/css.gohtml
Normal file
5
pkg/server/views/layouts/css.gohtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{define "css"}}
|
||||
{{range css}}
|
||||
<link rel="stylesheet" href="/static/{{ . }}">
|
||||
{{end}}
|
||||
{{end}}
|
||||
7
pkg/server/views/layouts/header.gohtml
Normal file
7
pkg/server/views/layouts/header.gohtml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{{define "header"}}
|
||||
|
||||
{{if eq headerTemplate "navbar"}}
|
||||
{{ template "navbar" . }}
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
||||
32
pkg/server/views/layouts/navbar.gohtml
Normal file
32
pkg/server/views/layouts/navbar.gohtml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{{define "navbar"}}
|
||||
<header class="header-wrapper">
|
||||
<div class="container">
|
||||
<div class="header-content">
|
||||
<div class="left">
|
||||
<a href="/">NAD</a>
|
||||
|
||||
<nav>
|
||||
<ul class="list-unstyled main-nav">
|
||||
<li><a href="/new">New</a></li>
|
||||
<li><a href="/books">Books</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{if .User}}
|
||||
<li>{{template "logoutForm"}}</li>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
{{end}}
|
||||
|
||||
{{define "logoutForm"}}
|
||||
<form class="navbar-form navbar-left" action="/logout" method="POST">
|
||||
{{csrfField}}
|
||||
<button type="submit" class="btn btn-default">Log out</button>
|
||||
</form>
|
||||
{{end}}
|
||||
57
pkg/server/views/users/new.gohtml
Normal file
57
pkg/server/views/users/new.gohtml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{{define "yield"}}
|
||||
<div class="auth-page">
|
||||
<div class="container">
|
||||
<h1 class="heading">Register</h1>
|
||||
|
||||
<div class="body">
|
||||
<div class="panel">
|
||||
{{template "signupForm"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="callout">Already have an account?</div>
|
||||
<a href="/login" class="cta">
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "signupForm"}}
|
||||
<form action="/register" method="POST">
|
||||
{{csrfField}}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="input-row">
|
||||
<label for="email-input" class="label">
|
||||
Email
|
||||
<input
|
||||
id="email-input"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
class="form-control"
|
||||
value="{{.Email}}"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-row">
|
||||
<label for="password-input" class="label">
|
||||
Password
|
||||
<input
|
||||
id="password-input"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="●●●●●●●●"
|
||||
class="form-control"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-button button button-normal button-stretch button-third">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
155
pkg/server/views/view.go
Normal file
155
pkg/server/views/view.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/context"
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// templateExt is the template extension
|
||||
templateExt string = ".gohtml"
|
||||
)
|
||||
|
||||
const (
|
||||
siteTitle = "Dnote"
|
||||
)
|
||||
|
||||
// Config is a view config
|
||||
type Config struct {
|
||||
Title string
|
||||
Layout string
|
||||
HeaderTemplate string
|
||||
}
|
||||
|
||||
func (c Config) getLayout() string {
|
||||
if c.Layout == "" {
|
||||
return "base"
|
||||
}
|
||||
|
||||
return c.Layout
|
||||
}
|
||||
|
||||
// NewView returns a new view by parsing the given layout and files
|
||||
func NewView(baseDir string, c Config, files ...string) *View {
|
||||
addTemplatePath(baseDir, files)
|
||||
addTemplateExt(files)
|
||||
files = append(files, layoutFiles(baseDir)...)
|
||||
|
||||
t, err := template.New(c.Title).Funcs(template.FuncMap{
|
||||
"csrfField": func() (template.HTML, error) {
|
||||
return "", errors.New("csrfField is not implemented")
|
||||
},
|
||||
"css": func() []string {
|
||||
return strings.Split("", ",")
|
||||
},
|
||||
"title": func() string {
|
||||
if c.Title != "" {
|
||||
return fmt.Sprintf("%s | %s", c.Title, siteTitle)
|
||||
}
|
||||
|
||||
return siteTitle
|
||||
},
|
||||
"headerTemplate": func() string {
|
||||
return c.HeaderTemplate
|
||||
},
|
||||
}).ParseFiles(files...)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "instantiating view."))
|
||||
}
|
||||
|
||||
return &View{
|
||||
Template: t,
|
||||
Layout: c.getLayout(),
|
||||
}
|
||||
}
|
||||
|
||||
// View holds the information about a view
|
||||
type View struct {
|
||||
Template *template.Template
|
||||
Layout string
|
||||
}
|
||||
|
||||
func (v *View) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
v.Render(w, r, nil)
|
||||
}
|
||||
|
||||
// Render is used to render the view with the predefined layout.
|
||||
func (v *View) Render(w http.ResponseWriter, r *http.Request, data interface{}) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
var vd Data
|
||||
switch d := data.(type) {
|
||||
case Data:
|
||||
vd = d
|
||||
// do nothing
|
||||
default:
|
||||
vd = Data{
|
||||
Yield: data,
|
||||
}
|
||||
}
|
||||
|
||||
if alert := getAlert(r); alert != nil {
|
||||
vd.Alert = alert
|
||||
clearAlert(w)
|
||||
}
|
||||
|
||||
vd.User = context.User(r.Context())
|
||||
|
||||
var buf bytes.Buffer
|
||||
csrfField := csrf.TemplateField(r)
|
||||
tpl := v.Template.Funcs(template.FuncMap{
|
||||
"csrfField": func() template.HTML {
|
||||
return csrfField
|
||||
},
|
||||
})
|
||||
|
||||
if err := tpl.ExecuteTemplate(&buf, v.Layout, vd); err != nil {
|
||||
log.ErrorWrap(err, fmt.Sprintf("executing a template '%s'", v.Template.Name()))
|
||||
http.Error(w, AlertMsgGeneric, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
io.Copy(w, &buf)
|
||||
}
|
||||
|
||||
// layoutFiles returns a slice of strings representing
|
||||
// the layout files used in our application.
|
||||
func layoutFiles(baseDir string) []string {
|
||||
pattern := fmt.Sprintf("%s/layouts/*%s", baseDir, templateExt)
|
||||
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// addTemplatePath takes in a slice of strings
|
||||
// representing file paths for templates.
|
||||
func addTemplatePath(baseDir string, files []string) {
|
||||
for i, f := range files {
|
||||
files[i] = fmt.Sprintf("%s/%s", baseDir, f)
|
||||
}
|
||||
}
|
||||
|
||||
// addTemplateExt takes in a slice of strings
|
||||
// representing file paths for templates and it appends
|
||||
// the templateExt extension to each string in the slice
|
||||
//
|
||||
// Eg the input {"home"} would result in the output
|
||||
// {"home.gohtml"} if templateExt == ".gohtml"
|
||||
func addTemplateExt(files []string) {
|
||||
for i, f := range files {
|
||||
files[i] = f + templateExt
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue