├── static
├── favicons
│ ├── favicon.ico
│ ├── blankfavicon.png
│ ├── mstile-70x70.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── mstile-144x144.png
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── mstile-310x310.png
│ ├── apple-touch-icon.png
│ ├── favicon-160x160.png
│ ├── favicon-196x196.png
│ ├── apple-touch-icon-57x57.png
│ ├── apple-touch-icon-60x60.png
│ ├── apple-touch-icon-72x72.png
│ ├── apple-touch-icon-76x76.png
│ ├── apple-touch-icon-114x114.png
│ ├── apple-touch-icon-120x120.png
│ ├── apple-touch-icon-144x144.png
│ ├── apple-touch-icon-152x152.png
│ ├── apple-touch-icon-precomposed.png
│ └── browserconfig.xml
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.woff2
├── css
│ ├── global.css
│ └── bootstrap-theme.min.css
└── js
│ ├── global.js
│ ├── underscore-min.js
│ ├── underscore-min.map
│ └── bootstrap.min.js
├── template
├── blank.tmpl
├── partial
│ ├── footer.tmpl
│ └── menu.tmpl
├── index
│ ├── anon.tmpl
│ └── auth.tmpl
├── about
│ └── about.tmpl
├── notepad
│ ├── create.tmpl
│ ├── update.tmpl
│ └── read.tmpl
├── login
│ └── login.tmpl
├── register
│ └── register.tmpl
└── base.tmpl
├── vendor
└── app
│ ├── controller
│ ├── about.go
│ ├── static.go
│ ├── index.go
│ ├── error.go
│ ├── register.go
│ ├── login.go
│ └── notepad.go
│ ├── shared
│ ├── view
│ │ ├── plugin
│ │ │ ├── noescape.go
│ │ │ ├── prettytime.go
│ │ │ └── taghelper.go
│ │ └── view.go
│ ├── jsonconfig
│ │ └── jsonconfig.go
│ ├── passhash
│ │ ├── passhash.go
│ │ └── passhash_test.go
│ ├── recaptcha
│ │ └── recaptcha.go
│ ├── session
│ │ └── session.go
│ ├── email
│ │ └── email.go
│ ├── server
│ │ └── server.go
│ └── database
│ │ └── database.go
│ ├── route
│ ├── middleware
│ │ ├── logrequest
│ │ │ └── logrequest.go
│ │ ├── acl
│ │ │ └── acl.go
│ │ ├── httprouterwrapper
│ │ │ └── httprouterwrapper.go
│ │ └── pprofhandler
│ │ │ └── pprofhandler.go
│ └── route.go
│ └── model
│ ├── model.go
│ ├── user.go
│ └── note.go
├── LICENSE
├── config
├── config.json
└── mysql.sql
├── gowebapp.go
└── README.md
/static/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/favicon.ico
--------------------------------------------------------------------------------
/static/favicons/blankfavicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/blankfavicon.png
--------------------------------------------------------------------------------
/static/favicons/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/mstile-70x70.png
--------------------------------------------------------------------------------
/static/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/static/favicons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/favicon-96x96.png
--------------------------------------------------------------------------------
/static/favicons/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/mstile-144x144.png
--------------------------------------------------------------------------------
/static/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/static/favicons/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/mstile-310x150.png
--------------------------------------------------------------------------------
/static/favicons/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/mstile-310x310.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/favicons/favicon-160x160.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/favicon-160x160.png
--------------------------------------------------------------------------------
/static/favicons/favicon-196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/favicon-196x196.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/static/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/static/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/static/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/static/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephspurrier/gowebapp/HEAD/static/favicons/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/template/blank.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}Blank Template{{end}}
2 | {{define "head"}}{{end}}
3 | {{define "content"}}
4 | This is a blank template.
5 | {{end}}
6 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/template/partial/footer.tmpl:
--------------------------------------------------------------------------------
1 | {{define "footer"}}
2 |
6 | {{end}}
--------------------------------------------------------------------------------
/vendor/app/controller/about.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 |
6 | "app/shared/view"
7 | )
8 |
9 | // AboutGET displays the About page
10 | func AboutGET(w http.ResponseWriter, r *http.Request) {
11 | // Display the view
12 | v := view.New(r)
13 | v.Name = "about/about"
14 | v.Render(w)
15 | }
16 |
--------------------------------------------------------------------------------
/template/index/anon.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}Go Web App{{end}}
2 | {{define "head"}}{{end}}
3 | {{define "content"}}
4 |
5 |
8 |
Click {{LINK "login" "here"}} to login.
9 | {{template "footer" .}}
10 |
11 | {{end}}
12 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/template/partial/menu.tmpl:
--------------------------------------------------------------------------------
1 | {{if eq .AuthLevel "auth"}}
2 |
3 |
7 |
8 | {{else}}
9 |
10 |
13 |
14 | {{end}}
--------------------------------------------------------------------------------
/vendor/app/controller/static.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | )
7 |
8 | // Static maps static files
9 | func Static(w http.ResponseWriter, r *http.Request) {
10 | // Disable listing directories
11 | if strings.HasSuffix(r.URL.Path, "/") {
12 | Error404(w, r)
13 | return
14 | }
15 | http.ServeFile(w, r, r.URL.Path[1:])
16 | }
17 |
--------------------------------------------------------------------------------
/template/index/auth.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}Go Web App{{end}}
2 | {{define "head"}}{{end}}
3 | {{define "content"}}
4 |
5 |
8 |
You have arrived. Click {{LINK "notepad" "here"}} to view your notepad.
9 | {{template "footer" .}}
10 |
11 | {{end}}
12 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/vendor/app/shared/view/plugin/noescape.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "html/template"
5 | )
6 |
7 | // NoEscape returns a template.FuncMap
8 | // * NOESCAPE prevents escaping variable
9 | func NoEscape() template.FuncMap {
10 | f := make(template.FuncMap)
11 |
12 | f["NOESCAPE"] = func(name string) template.HTML {
13 | return template.HTML(name)
14 | }
15 |
16 | return f
17 | }
18 |
--------------------------------------------------------------------------------
/template/about/about.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}About{{end}}
2 | {{define "head"}}{{end}}
3 | {{define "content"}}
4 |
5 |
8 |
This project demonstrates how to structure and build a website using
9 | the Go language without a framework.
10 | {{template "footer" .}}
11 |
12 | {{end}}
13 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/vendor/app/shared/view/plugin/prettytime.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "html/template"
5 | "time"
6 | )
7 |
8 | // PrettyTime returns a template.FuncMap
9 | // * PRETTYTIME outputs a nice time format
10 | func PrettyTime() template.FuncMap {
11 | f := make(template.FuncMap)
12 |
13 | f["PRETTYTIME"] = func(t time.Time) string {
14 | return t.Format("3:04 PM 01/02/2006")
15 | }
16 |
17 | return f
18 | }
19 |
--------------------------------------------------------------------------------
/vendor/app/route/middleware/logrequest/logrequest.go:
--------------------------------------------------------------------------------
1 | package logrequest
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | // Handler will log the HTTP requests
10 | func Handler(next http.Handler) http.Handler {
11 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12 | fmt.Println(time.Now().Format("2006-01-02 03:04:05 PM"), r.RemoteAddr, r.Method, r.URL)
13 | next.ServeHTTP(w, r)
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/static/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #da532c
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/static/css/global.css:
--------------------------------------------------------------------------------
1 | /* Global Styles */
2 | body {
3 | font-family: "Open Sans", Helvetica, Arial, sans-serif !important;
4 | }
5 |
6 | h1, h2, h3, h4, h5, h6 {
7 | font-family: "Open Sans", Helvetica, Arial, sans-serif;
8 | }
9 |
10 | .page-header {
11 | border-bottom: 0;
12 | margin-top: 0;
13 | }
14 |
15 | /* Notification Styles */
16 | #flash-container {
17 | position: fixed;
18 | bottom: 0;
19 | right: 0;
20 | z-index: 100;
21 | margin: 0;
22 | }
23 |
24 | .alert-box-fixed {
25 | margin: 0 15px 15px 0;
26 | }
--------------------------------------------------------------------------------
/vendor/app/controller/index.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 |
6 | "app/shared/session"
7 | "app/shared/view"
8 | )
9 |
10 | // IndexGET displays the home page
11 | func IndexGET(w http.ResponseWriter, r *http.Request) {
12 | // Get session
13 | session := session.Instance(r)
14 |
15 | if session.Values["id"] != nil {
16 | // Display the view
17 | v := view.New(r)
18 | v.Name = "index/auth"
19 | v.Vars["first_name"] = session.Values["first_name"]
20 | v.Render(w)
21 | } else {
22 | // Display the view
23 | v := view.New(r)
24 | v.Name = "index/anon"
25 | v.Render(w)
26 | return
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/vendor/app/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "database/sql"
5 | "errors"
6 |
7 | "gopkg.in/mgo.v2"
8 | )
9 |
10 | var (
11 | // ErrCode is a config or an internal error
12 | ErrCode = errors.New("Case statement in code is not correct.")
13 | // ErrNoResult is a not results error
14 | ErrNoResult = errors.New("Result not found.")
15 | // ErrUnavailable is a database not available error
16 | ErrUnavailable = errors.New("Database is unavailable.")
17 | // ErrUnauthorized is a permissions violation
18 | ErrUnauthorized = errors.New("User does not have permission to perform this operation.")
19 | )
20 |
21 | // standardizeErrors returns the same error regardless of the database used
22 | func standardizeError(err error) error {
23 | if err == sql.ErrNoRows || err == mgo.ErrNotFound {
24 | return ErrNoResult
25 | }
26 |
27 | return err
28 | }
29 |
--------------------------------------------------------------------------------
/vendor/app/controller/error.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | )
7 |
8 | // Error404 handles 404 - Page Not Found
9 | func Error404(w http.ResponseWriter, r *http.Request) {
10 | w.WriteHeader(http.StatusNotFound)
11 | fmt.Fprint(w, "Not Found 404")
12 | }
13 |
14 | // Error500 handles 500 - Internal Server Error
15 | func Error500(w http.ResponseWriter, r *http.Request) {
16 | w.WriteHeader(http.StatusInternalServerError)
17 | fmt.Fprint(w, "Internal Server Error 500")
18 | }
19 |
20 | // InvalidToken handles CSRF attacks
21 | func InvalidToken(w http.ResponseWriter, r *http.Request) {
22 | w.Header().Set("Content-Type", "text/html")
23 | w.WriteHeader(http.StatusForbidden)
24 | fmt.Fprint(w, `Your token expired, click here to try again.`)
25 | }
26 |
--------------------------------------------------------------------------------
/vendor/app/shared/jsonconfig/jsonconfig.go:
--------------------------------------------------------------------------------
1 | package jsonconfig
2 |
3 | import (
4 | "io"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "path/filepath"
9 | )
10 |
11 | // Parser must implement ParseJSON
12 | type Parser interface {
13 | ParseJSON([]byte) error
14 | }
15 |
16 | // Load the JSON config file
17 | func Load(configFile string, p Parser) {
18 | var err error
19 | var absPath string
20 | var input = io.ReadCloser(os.Stdin)
21 | if absPath, err = filepath.Abs(configFile); err != nil {
22 | log.Fatalln(err)
23 | }
24 |
25 | if input, err = os.Open(absPath); err != nil {
26 | log.Fatalln(err)
27 | }
28 |
29 | // Read the config file
30 | jsonBytes, err := ioutil.ReadAll(input)
31 | input.Close()
32 | if err != nil {
33 | log.Fatalln(err)
34 | }
35 |
36 | // Parse the config
37 | if err := p.ParseJSON(jsonBytes); err != nil {
38 | log.Fatalln("Could not parse %q: %v", configFile, err)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/template/notepad/create.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}Add Note{{end}}
2 | {{define "head"}}{{end}}
3 | {{define "content"}}
4 |
5 |
6 |
9 |
10 |
25 |
26 | {{template "footer" .}}
27 |
28 |
29 | {{end}}
30 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/template/notepad/update.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}Edit Note{{end}}
2 | {{define "head"}}{{end}}
3 | {{define "content"}}
4 |
5 |
28 |
29 | {{end}}
30 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/template/login/login.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}Login{{end}}
2 | {{define "head"}}{{end}}
3 | {{define "content"}}
4 |
5 |
6 |
9 |
24 |
25 |
26 | {{LINK "register" "Create a new account."}}
27 |
28 |
29 | {{template "footer" .}}
30 |
31 |
32 | {{end}}
33 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/vendor/app/route/middleware/acl/acl.go:
--------------------------------------------------------------------------------
1 | package acl
2 |
3 | import (
4 | "net/http"
5 |
6 | "app/shared/session"
7 | )
8 |
9 | // DisallowAuth does not allow authenticated users to access the page
10 | func DisallowAuth(h http.Handler) http.Handler {
11 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12 | // Get session
13 | sess := session.Instance(r)
14 |
15 | // If user is authenticated, don't allow them to access the page
16 | if sess.Values["id"] != nil {
17 | http.Redirect(w, r, "/", http.StatusFound)
18 | return
19 | }
20 |
21 | h.ServeHTTP(w, r)
22 | })
23 | }
24 |
25 | // DisallowAnon does not allow anonymous users to access the page
26 | func DisallowAnon(h http.Handler) http.Handler {
27 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28 | // Get session
29 | sess := session.Instance(r)
30 |
31 | // If user is not authenticated, don't allow them to access the page
32 | if sess.Values["id"] == nil {
33 | http.Redirect(w, r, "/", http.StatusFound)
34 | return
35 | }
36 |
37 | h.ServeHTTP(w, r)
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Joseph Spurrier
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/vendor/app/route/middleware/httprouterwrapper/httprouterwrapper.go:
--------------------------------------------------------------------------------
1 | // Package httprouterwrapper allows the use of http.HandlerFunc compatible funcs with julienschmidt/httprouter
2 | package httprouterwrapper
3 |
4 | // Source: http://nicolasmerouze.com/guide-routers-golang/
5 |
6 | import (
7 | "net/http"
8 |
9 | "github.com/gorilla/context"
10 | "github.com/julienschmidt/httprouter"
11 | )
12 |
13 | // HandlerFunc accepts the name of a function so you don't have to wrap it with http.HandlerFunc
14 | // Example: r.GET("/", httprouterwrapper.HandlerFunc(controller.Index))
15 | func HandlerFunc(h http.HandlerFunc) httprouter.Handle {
16 | return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
17 | context.Set(r, "params", p)
18 | h.ServeHTTP(w, r)
19 | }
20 | }
21 |
22 | // Handler accepts a handler to make it compatible with http.HandlerFunc
23 | // Example: r.GET("/", httprouterwrapper.Handler(http.HandlerFunc(controller.Index)))
24 | func Handler(h http.Handler) httprouter.Handle {
25 | return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
26 | context.Set(r, "params", p)
27 | h.ServeHTTP(w, r)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/vendor/app/shared/passhash/passhash.go:
--------------------------------------------------------------------------------
1 | package passhash
2 |
3 | import (
4 | "golang.org/x/crypto/bcrypt"
5 | )
6 |
7 | // HashString returns a hashed string and an error
8 | func HashString(password string) (string, error) {
9 | key, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
10 | if err != nil {
11 | return "", err
12 | }
13 |
14 | return string(key), nil
15 | }
16 |
17 | // HashBytes returns a hashed byte array and an error
18 | func HashBytes(password []byte) ([]byte, error) {
19 | key, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
20 | if err != nil {
21 | return nil, err
22 | }
23 |
24 | return key, nil
25 | }
26 |
27 | // MatchString returns true if the hash matches the password
28 | func MatchString(hash, password string) bool {
29 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
30 | if err == nil {
31 | return true
32 | }
33 |
34 | return false
35 | }
36 |
37 | // MatchBytes returns true if the hash matches the password
38 | func MatchBytes(hash, password []byte) bool {
39 | err := bcrypt.CompareHashAndPassword(hash, []byte(password))
40 | if err == nil {
41 | return true
42 | }
43 |
44 | return false
45 | }
46 |
--------------------------------------------------------------------------------
/vendor/app/shared/view/plugin/taghelper.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "html/template"
5 | "log"
6 |
7 | "app/shared/view"
8 | )
9 |
10 | // TagHelper returns a template.FuncMap
11 | // * JS returns JavaScript tag
12 | // * CSS returns stylesheet tag
13 | // * LINK returns hyperlink tag
14 | func TagHelper(v view.View) template.FuncMap {
15 | f := make(template.FuncMap)
16 |
17 | f["JS"] = func(s string) template.HTML {
18 | path, err := v.AssetTimePath(s)
19 |
20 | if err != nil {
21 | log.Println("JS Error:", err)
22 | return template.HTML("")
23 | }
24 |
25 | return template.HTML(``)
26 | }
27 |
28 | f["CSS"] = func(s string) template.HTML {
29 | path, err := v.AssetTimePath(s)
30 |
31 | if err != nil {
32 | log.Println("CSS Error:", err)
33 | return template.HTML("")
34 | }
35 |
36 | return template.HTML(``)
37 | }
38 |
39 | f["LINK"] = func(path, name string) template.HTML {
40 | return template.HTML(`` + name + ``)
41 | }
42 |
43 | return f
44 | }
45 |
--------------------------------------------------------------------------------
/vendor/app/shared/recaptcha/recaptcha.go:
--------------------------------------------------------------------------------
1 | package recaptcha
2 |
3 | import (
4 | "html/template"
5 | "net/http"
6 |
7 | "github.com/haisum/recaptcha"
8 | )
9 |
10 | var (
11 | recap Info
12 | )
13 |
14 | // Info has the details for the Google reCAPTCHA
15 | type Info struct {
16 | Enabled bool
17 | Secret string
18 | SiteKey string
19 | }
20 |
21 | // Configure adds the settings for Google reCAPTCHA
22 | func Configure(c Info) {
23 | recap = c
24 | }
25 |
26 | // ReadConfig returns the settings for Google reCAPTCHA
27 | func ReadConfig() Info {
28 | return recap
29 | }
30 |
31 | // Verified returns whether the Google reCAPTCHA was verified or not
32 | func Verified(r *http.Request) bool {
33 | if !recap.Enabled {
34 | return true
35 | }
36 |
37 | // Check the reCaptcha
38 | re := recaptcha.R{
39 | Secret: recap.Secret,
40 | }
41 | return re.Verify(*r)
42 | }
43 |
44 | // Plugin returns a map of functions that are usable in templates
45 | func Plugin() template.FuncMap {
46 | f := make(template.FuncMap)
47 |
48 | f["RECAPTCHA_SITEKEY"] = func() template.HTML {
49 | if ReadConfig().Enabled {
50 | return template.HTML(ReadConfig().SiteKey)
51 | }
52 |
53 | return template.HTML("")
54 | }
55 |
56 | return f
57 | }
58 |
--------------------------------------------------------------------------------
/template/notepad/read.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}Notepad{{end}}
2 | {{define "head"}}{{end}}
3 | {{define "content"}}
4 |
5 |
8 |
9 |
10 | Add Note
11 |
12 |
13 |
14 | {{range $n := .notes}}
15 |
16 |
17 |
{{.Content}}
18 |
26 |
{{.UpdatedAt | PRETTYTIME}}
27 |
28 |
29 | {{end}}
30 |
31 | {{template "footer" .}}
32 |
33 | {{end}}
34 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/vendor/app/shared/passhash/passhash_test.go:
--------------------------------------------------------------------------------
1 | package passhash
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestStringString(t *testing.T) {
8 | plainText := "This is a test."
9 |
10 | hash, err := HashString(plainText)
11 |
12 | if err != nil {
13 | t.Error(err)
14 | }
15 |
16 | if !MatchString(hash, plainText) {
17 | t.Error("Password does not match")
18 | }
19 | }
20 |
21 | func TestByteByte(t *testing.T) {
22 | plainText := []byte("This is a test.")
23 |
24 | hash, err := HashBytes(plainText)
25 |
26 | if err != nil {
27 | t.Error(err)
28 | }
29 |
30 | if !MatchBytes(hash, plainText) {
31 | t.Error("Password does not match")
32 | }
33 | }
34 |
35 | func TestStringByte(t *testing.T) {
36 | plainText := "This is a test."
37 |
38 | hash, err := HashString(plainText)
39 |
40 | if err != nil {
41 | t.Error(err)
42 | }
43 |
44 | if !MatchBytes([]byte(hash), []byte(plainText)) {
45 | t.Error("Password does not match")
46 | }
47 | }
48 |
49 | func TestByteString(t *testing.T) {
50 | plainText := []byte("This is a test.")
51 |
52 | hash, err := HashBytes(plainText)
53 |
54 | if err != nil {
55 | t.Error(err)
56 | }
57 |
58 | if !MatchString(string(hash), string(plainText)) {
59 | t.Error("Password does not match")
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/vendor/app/route/middleware/pprofhandler/pprofhandler.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Prometheus Authors
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | //
6 | // http://www.apache.org/licenses/LICENSE-2.0
7 | //
8 | // Unless required by applicable law or agreed to in writing, software
9 | // distributed under the License is distributed on an "AS IS" BASIS,
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 |
14 | package pprofhandler
15 |
16 | import (
17 | "net/http"
18 | "net/http/pprof"
19 |
20 | "github.com/gorilla/context"
21 | "github.com/julienschmidt/httprouter"
22 | )
23 |
24 | // Handler routes the pprof pages using httprouter
25 | func Handler(w http.ResponseWriter, r *http.Request) {
26 |
27 | p := context.Get(r, "params").(httprouter.Params)
28 |
29 | switch p.ByName("pprof") {
30 | case "/cmdline":
31 | pprof.Cmdline(w, r)
32 | case "/profile":
33 | pprof.Profile(w, r)
34 | case "/symbol":
35 | pprof.Symbol(w, r)
36 | default:
37 | pprof.Index(w, r)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/vendor/app/shared/session/session.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gorilla/sessions"
7 | )
8 |
9 | var (
10 | // Store is the cookie store
11 | Store *sessions.CookieStore
12 | // Name is the session name
13 | Name string
14 | )
15 |
16 | // Session stores session level information
17 | type Session struct {
18 | Options sessions.Options `json:"Options"` // Pulled from: http://www.gorillatoolkit.org/pkg/sessions#Options
19 | Name string `json:"Name"` // Name for: http://www.gorillatoolkit.org/pkg/sessions#CookieStore.Get
20 | SecretKey string `json:"SecretKey"` // Key for: http://www.gorillatoolkit.org/pkg/sessions#CookieStore.New
21 | }
22 |
23 | // Configure the session cookie store
24 | func Configure(s Session) {
25 | Store = sessions.NewCookieStore([]byte(s.SecretKey))
26 | Store.Options = &s.Options
27 | Name = s.Name
28 | }
29 |
30 | // Instance returns a new session, never returns an error
31 | func Instance(r *http.Request) *sessions.Session {
32 | session, _ := Store.Get(r, Name)
33 | return session
34 | }
35 |
36 | // Empty deletes all the current session values
37 | func Empty(sess *sessions.Session) {
38 | // Clear out all stored values in the cookie
39 | for k := range sess.Values {
40 | delete(sess.Values, k)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/config/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "Database": {
3 | "Type": "Bolt",
4 | "Bolt": {
5 | "Path": "gowebapp.db"
6 | },
7 | "MongoDB": {
8 | "URL": "127.0.0.1",
9 | "Database": "gowebapp"
10 | },
11 | "MySQL": {
12 | "Username": "root",
13 | "Password": "",
14 | "Name": "gowebapp",
15 | "Hostname": "127.0.0.1",
16 | "Port": 3306,
17 | "Parameter": "?parseTime=true"
18 | }
19 | },
20 | "Email": {
21 | "Username": "",
22 | "Password": "",
23 | "Hostname": "",
24 | "Port": 25,
25 | "From": ""
26 | },
27 | "Recaptcha": {
28 | "Enabled": false,
29 | "Secret": "",
30 | "SiteKey": ""
31 | },
32 | "Server": {
33 | "Hostname": "",
34 | "UseHTTP": true,
35 | "UseHTTPS": false,
36 | "HTTPPort": 80,
37 | "HTTPSPort": 443,
38 | "CertFile": "tls/server.crt",
39 | "KeyFile": "tls/server.key"
40 | },
41 | "Session": {
42 | "SecretKey": "@r4B?EThaSEh_drudR7P_hub=s#s2Pah",
43 | "Name": "gosess",
44 | "Options": {
45 | "Path": "/",
46 | "Domain": "",
47 | "MaxAge": 28800,
48 | "Secure": false,
49 | "HttpOnly": true
50 | }
51 | },
52 | "Template": {
53 | "Root": "base",
54 | "Children": [
55 | "partial/menu",
56 | "partial/footer"
57 | ]
58 | },
59 | "View": {
60 | "BaseURI": "/",
61 | "Extension": "tmpl",
62 | "Folder": "template",
63 | "Name": "blank",
64 | "Caching": true
65 | }
66 | }
--------------------------------------------------------------------------------
/vendor/app/shared/email/email.go:
--------------------------------------------------------------------------------
1 | package email
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "net/smtp"
7 | )
8 |
9 | var (
10 | e SMTPInfo
11 | )
12 |
13 | // SMTPInfo is the details for the SMTP server
14 | type SMTPInfo struct {
15 | Username string
16 | Password string
17 | Hostname string
18 | Port int
19 | From string
20 | }
21 |
22 | // Configure adds the settings for the SMTP server
23 | func Configure(c SMTPInfo) {
24 | e = c
25 | }
26 |
27 | // ReadConfig returns the SMTP information
28 | func ReadConfig() SMTPInfo {
29 | return e
30 | }
31 |
32 | // SendEmail sends an email
33 | func SendEmail(to, subject, body string) error {
34 | auth := smtp.PlainAuth("", e.Username, e.Password, e.Hostname)
35 |
36 | header := make(map[string]string)
37 | header["From"] = e.From
38 | header["To"] = to
39 | header["Subject"] = subject
40 | header["MIME-Version"] = "1.0"
41 | header["Content-Type"] = `text/plain; charset="utf-8"`
42 | header["Content-Transfer-Encoding"] = "base64"
43 |
44 | message := ""
45 | for k, v := range header {
46 | message += fmt.Sprintf("%s: %s\r\n", k, v)
47 | }
48 | message += "\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
49 |
50 | // Send the email
51 | err := smtp.SendMail(
52 | fmt.Sprintf("%s:%d", e.Hostname, e.Port),
53 | auth,
54 | e.From,
55 | []string{to},
56 | []byte(message),
57 | )
58 |
59 | return err
60 | }
61 |
--------------------------------------------------------------------------------
/static/js/global.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | //$(document).foundation();
3 |
4 | // Hide any messages after a few seconds
5 | hideFlash();
6 | });
7 |
8 | function hideFlash(rnum)
9 | {
10 | if (!rnum) rnum = '0';
11 |
12 | _.delay(function() {
13 | $('.alert-box-fixed' + rnum).fadeOut(300, function() {
14 | $(this).css({"visibility":"hidden",display:'block'}).slideUp();
15 |
16 | var that = this;
17 |
18 | _.delay(function() { that.remove(); }, 400);
19 | });
20 | }, 4000);
21 | }
22 |
23 | function showFlash(obj)
24 | {
25 | $('#flash-container').html();
26 | $(obj).each(function(i, v) {
27 | var rnum = _.random(0, 100000);
28 | var message = ''
30 | + ''
31 | + v.message + '
';
32 | $('#flash-container').prepend(message);
33 | hideFlash(rnum);
34 | });
35 | }
36 |
37 | function flashError(message) {
38 | var flash = [{Class: "alert-danger", Message: message}];
39 | showFlash(flash);
40 | }
41 |
42 | function flashSuccess(message) {
43 | var flash = [{Class: "alert-success", Message: message}];
44 | showFlash(flash);
45 | }
46 |
47 | function flashNotice(message) {
48 | var flash = [{Class: "alert-info", Message: message}];
49 | showFlash(flash);
50 | }
51 |
52 | function flashWarning(message) {
53 | var flash = [{Class: "alert-warning", Message: message}];
54 | showFlash(flash);
55 | }
--------------------------------------------------------------------------------
/template/register/register.tmpl:
--------------------------------------------------------------------------------
1 | {{define "title"}}Create an Account{{end}}
2 | {{define "head"}}{{JS "//www.google.com/recaptcha/api.js"}}{{end}}
3 | {{define "content"}}
4 |
5 |
6 |
9 |
41 |
42 | {{template "footer" .}}
43 |
44 |
45 | {{end}}
46 | {{define "foot"}}{{end}}
--------------------------------------------------------------------------------
/vendor/app/shared/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "time"
8 | )
9 |
10 | // Server stores the hostname and port number
11 | type Server struct {
12 | Hostname string `json:"Hostname"` // Server name
13 | UseHTTP bool `json:"UseHTTP"` // Listen on HTTP
14 | UseHTTPS bool `json:"UseHTTPS"` // Listen on HTTPS
15 | HTTPPort int `json:"HTTPPort"` // HTTP port
16 | HTTPSPort int `json:"HTTPSPort"` // HTTPS port
17 | CertFile string `json:"CertFile"` // HTTPS certificate
18 | KeyFile string `json:"KeyFile"` // HTTPS private key
19 | }
20 |
21 | // Run starts the HTTP and/or HTTPS listener
22 | func Run(httpHandlers http.Handler, httpsHandlers http.Handler, s Server) {
23 | if s.UseHTTP && s.UseHTTPS {
24 | go func() {
25 | startHTTPS(httpsHandlers, s)
26 | }()
27 |
28 | startHTTP(httpHandlers, s)
29 | } else if s.UseHTTP {
30 | startHTTP(httpHandlers, s)
31 | } else if s.UseHTTPS {
32 | startHTTPS(httpsHandlers, s)
33 | } else {
34 | log.Println("Config file does not specify a listener to start")
35 | }
36 | }
37 |
38 | // startHTTP starts the HTTP listener
39 | func startHTTP(handlers http.Handler, s Server) {
40 | fmt.Println(time.Now().Format("2006-01-02 03:04:05 PM"), "Running HTTP "+httpAddress(s))
41 |
42 | // Start the HTTP listener
43 | log.Fatal(http.ListenAndServe(httpAddress(s), handlers))
44 | }
45 |
46 | // startHTTPs starts the HTTPS listener
47 | func startHTTPS(handlers http.Handler, s Server) {
48 | fmt.Println(time.Now().Format("2006-01-02 03:04:05 PM"), "Running HTTPS "+httpsAddress(s))
49 |
50 | // Start the HTTPS listener
51 | log.Fatal(http.ListenAndServeTLS(httpsAddress(s), s.CertFile, s.KeyFile, handlers))
52 | }
53 |
54 | // httpAddress returns the HTTP address
55 | func httpAddress(s Server) string {
56 | return s.Hostname + ":" + fmt.Sprintf("%d", s.HTTPPort)
57 | }
58 |
59 | // httpsAddress returns the HTTPS address
60 | func httpsAddress(s Server) string {
61 | return s.Hostname + ":" + fmt.Sprintf("%d", s.HTTPSPort)
62 | }
63 |
--------------------------------------------------------------------------------
/gowebapp.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "os"
7 | "runtime"
8 |
9 | "app/route"
10 | "app/shared/database"
11 | "app/shared/email"
12 | "app/shared/jsonconfig"
13 | "app/shared/recaptcha"
14 | "app/shared/server"
15 | "app/shared/session"
16 | "app/shared/view"
17 | "app/shared/view/plugin"
18 | )
19 |
20 | // *****************************************************************************
21 | // Application Logic
22 | // *****************************************************************************
23 |
24 | func init() {
25 | // Verbose logging with file name and line number
26 | log.SetFlags(log.Lshortfile)
27 |
28 | // Use all CPU cores
29 | runtime.GOMAXPROCS(runtime.NumCPU())
30 | }
31 |
32 | func main() {
33 | // Load the configuration file
34 | jsonconfig.Load("config"+string(os.PathSeparator)+"config.json", config)
35 |
36 | // Configure the session cookie store
37 | session.Configure(config.Session)
38 |
39 | // Connect to database
40 | database.Connect(config.Database)
41 |
42 | // Configure the Google reCAPTCHA prior to loading view plugins
43 | recaptcha.Configure(config.Recaptcha)
44 |
45 | // Setup the views
46 | view.Configure(config.View)
47 | view.LoadTemplates(config.Template.Root, config.Template.Children)
48 | view.LoadPlugins(
49 | plugin.TagHelper(config.View),
50 | plugin.NoEscape(),
51 | plugin.PrettyTime(),
52 | recaptcha.Plugin())
53 |
54 | // Start the listener
55 | server.Run(route.LoadHTTP(), route.LoadHTTPS(), config.Server)
56 | }
57 |
58 | // *****************************************************************************
59 | // Application Settings
60 | // *****************************************************************************
61 |
62 | // config the settings variable
63 | var config = &configuration{}
64 |
65 | // configuration contains the application settings
66 | type configuration struct {
67 | Database database.Info `json:"Database"`
68 | Email email.SMTPInfo `json:"Email"`
69 | Recaptcha recaptcha.Info `json:"Recaptcha"`
70 | Server server.Server `json:"Server"`
71 | Session session.Session `json:"Session"`
72 | Template view.Template `json:"Template"`
73 | View view.View `json:"View"`
74 | }
75 |
76 | // ParseJSON unmarshals bytes to structs
77 | func (c *configuration) ParseJSON(b []byte) error {
78 | return json.Unmarshal(b, &c)
79 | }
80 |
--------------------------------------------------------------------------------
/config/mysql.sql:
--------------------------------------------------------------------------------
1 | /* *****************************************************************************
2 | // Setup the preferences
3 | // ****************************************************************************/
4 | SET NAMES utf8 COLLATE 'utf8_unicode_ci';
5 | SET foreign_key_checks = 1;
6 | SET time_zone = '+00:00';
7 | SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
8 | SET default_storage_engine = InnoDB;
9 | SET CHARACTER SET utf8;
10 |
11 | /* *****************************************************************************
12 | // Remove old database
13 | // ****************************************************************************/
14 | DROP DATABASE IF EXISTS gowebapp;
15 |
16 | /* *****************************************************************************
17 | // Create new database
18 | // ****************************************************************************/
19 | CREATE DATABASE gowebapp DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci;
20 | USE gowebapp;
21 |
22 | /* *****************************************************************************
23 | // Create the tables
24 | // ****************************************************************************/
25 | CREATE TABLE user_status (
26 | id TINYINT(1) UNSIGNED NOT NULL AUTO_INCREMENT,
27 |
28 | status VARCHAR(25) NOT NULL,
29 |
30 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
31 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
32 | deleted TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
33 |
34 | PRIMARY KEY (id)
35 | );
36 |
37 | CREATE TABLE user (
38 | id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
39 |
40 | first_name VARCHAR(50) NOT NULL,
41 | last_name VARCHAR(50) NOT NULL,
42 | email VARCHAR(100) NOT NULL,
43 | password CHAR(60) NOT NULL,
44 |
45 | status_id TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
46 |
47 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
48 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
49 | deleted TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
50 |
51 | UNIQUE KEY (email),
52 | CONSTRAINT `f_user_status` FOREIGN KEY (`status_id`) REFERENCES `user_status` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
53 |
54 | PRIMARY KEY (id)
55 | );
56 |
57 | INSERT INTO `user_status` (`id`, `status`, `created_at`, `updated_at`, `deleted`) VALUES
58 | (1, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0),
59 | (2, 'inactive', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0);
60 |
61 | CREATE TABLE note (
62 | id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
63 |
64 | content TEXT NOT NULL,
65 |
66 | user_id INT(10) UNSIGNED NOT NULL,
67 |
68 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
69 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
70 | deleted TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
71 |
72 | CONSTRAINT `f_note_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
73 |
74 | PRIMARY KEY (id)
75 | );
--------------------------------------------------------------------------------
/vendor/app/controller/register.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "app/model"
8 | "app/shared/passhash"
9 | "app/shared/recaptcha"
10 | "app/shared/session"
11 | "app/shared/view"
12 |
13 | "github.com/josephspurrier/csrfbanana"
14 | )
15 |
16 | // RegisterGET displays the register page
17 | func RegisterGET(w http.ResponseWriter, r *http.Request) {
18 | // Get session
19 | sess := session.Instance(r)
20 |
21 | // Display the view
22 | v := view.New(r)
23 | v.Name = "register/register"
24 | v.Vars["token"] = csrfbanana.Token(w, r, sess)
25 | // Refill any form fields
26 | view.Repopulate([]string{"first_name", "last_name", "email"}, r.Form, v.Vars)
27 | v.Render(w)
28 | }
29 |
30 | // RegisterPOST handles the registration form submission
31 | func RegisterPOST(w http.ResponseWriter, r *http.Request) {
32 | // Get session
33 | sess := session.Instance(r)
34 |
35 | // Prevent brute force login attempts by not hitting MySQL and pretending like it was invalid :-)
36 | if sess.Values["register_attempt"] != nil && sess.Values["register_attempt"].(int) >= 5 {
37 | log.Println("Brute force register prevented")
38 | http.Redirect(w, r, "/register", http.StatusFound)
39 | return
40 | }
41 |
42 | // Validate with required fields
43 | if validate, missingField := view.Validate(r, []string{"first_name", "last_name", "email", "password"}); !validate {
44 | sess.AddFlash(view.Flash{"Field missing: " + missingField, view.FlashError})
45 | sess.Save(r, w)
46 | RegisterGET(w, r)
47 | return
48 | }
49 |
50 | // Validate with Google reCAPTCHA
51 | if !recaptcha.Verified(r) {
52 | sess.AddFlash(view.Flash{"reCAPTCHA invalid!", view.FlashError})
53 | sess.Save(r, w)
54 | RegisterGET(w, r)
55 | return
56 | }
57 |
58 | // Get form values
59 | firstName := r.FormValue("first_name")
60 | lastName := r.FormValue("last_name")
61 | email := r.FormValue("email")
62 | password, errp := passhash.HashString(r.FormValue("password"))
63 |
64 | // If password hashing failed
65 | if errp != nil {
66 | log.Println(errp)
67 | sess.AddFlash(view.Flash{"An error occurred on the server. Please try again later.", view.FlashError})
68 | sess.Save(r, w)
69 | http.Redirect(w, r, "/register", http.StatusFound)
70 | return
71 | }
72 |
73 | // Get database result
74 | _, err := model.UserByEmail(email)
75 |
76 | if err == model.ErrNoResult { // If success (no user exists with that email)
77 | ex := model.UserCreate(firstName, lastName, email, password)
78 | // Will only error if there is a problem with the query
79 | if ex != nil {
80 | log.Println(ex)
81 | sess.AddFlash(view.Flash{"An error occurred on the server. Please try again later.", view.FlashError})
82 | sess.Save(r, w)
83 | } else {
84 | sess.AddFlash(view.Flash{"Account created successfully for: " + email, view.FlashSuccess})
85 | sess.Save(r, w)
86 | http.Redirect(w, r, "/login", http.StatusFound)
87 | return
88 | }
89 | } else if err != nil { // Catch all other errors
90 | log.Println(err)
91 | sess.AddFlash(view.Flash{"An error occurred on the server. Please try again later.", view.FlashError})
92 | sess.Save(r, w)
93 | } else { // Else the user already exists
94 | sess.AddFlash(view.Flash{"Account already exists for: " + email, view.FlashError})
95 | sess.Save(r, w)
96 | }
97 |
98 | // Display the page
99 | RegisterGET(w, r)
100 | }
101 |
--------------------------------------------------------------------------------
/vendor/app/controller/login.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 |
8 | "app/model"
9 | "app/shared/passhash"
10 | "app/shared/session"
11 | "app/shared/view"
12 |
13 | "github.com/gorilla/sessions"
14 | "github.com/josephspurrier/csrfbanana"
15 | )
16 |
17 | const (
18 | // Name of the session variable that tracks login attempts
19 | sessLoginAttempt = "login_attempt"
20 | )
21 |
22 | // loginAttempt increments the number of login attempts in sessions variable
23 | func loginAttempt(sess *sessions.Session) {
24 | // Log the attempt
25 | if sess.Values[sessLoginAttempt] == nil {
26 | sess.Values[sessLoginAttempt] = 1
27 | } else {
28 | sess.Values[sessLoginAttempt] = sess.Values[sessLoginAttempt].(int) + 1
29 | }
30 | }
31 |
32 | // LoginGET displays the login page
33 | func LoginGET(w http.ResponseWriter, r *http.Request) {
34 | // Get session
35 | sess := session.Instance(r)
36 |
37 | // Display the view
38 | v := view.New(r)
39 | v.Name = "login/login"
40 | v.Vars["token"] = csrfbanana.Token(w, r, sess)
41 | // Refill any form fields
42 | view.Repopulate([]string{"email"}, r.Form, v.Vars)
43 | v.Render(w)
44 | }
45 |
46 | // LoginPOST handles the login form submission
47 | func LoginPOST(w http.ResponseWriter, r *http.Request) {
48 | // Get session
49 | sess := session.Instance(r)
50 |
51 | // Prevent brute force login attempts by not hitting MySQL and pretending like it was invalid :-)
52 | if sess.Values[sessLoginAttempt] != nil && sess.Values[sessLoginAttempt].(int) >= 5 {
53 | log.Println("Brute force login prevented")
54 | sess.AddFlash(view.Flash{"Sorry, no brute force :-)", view.FlashNotice})
55 | sess.Save(r, w)
56 | LoginGET(w, r)
57 | return
58 | }
59 |
60 | // Validate with required fields
61 | if validate, missingField := view.Validate(r, []string{"email", "password"}); !validate {
62 | sess.AddFlash(view.Flash{"Field missing: " + missingField, view.FlashError})
63 | sess.Save(r, w)
64 | LoginGET(w, r)
65 | return
66 | }
67 |
68 | // Form values
69 | email := r.FormValue("email")
70 | password := r.FormValue("password")
71 |
72 | // Get database result
73 | result, err := model.UserByEmail(email)
74 |
75 | // Determine if user exists
76 | if err == model.ErrNoResult {
77 | loginAttempt(sess)
78 | sess.AddFlash(view.Flash{"Password is incorrect - Attempt: " + fmt.Sprintf("%v", sess.Values[sessLoginAttempt]), view.FlashWarning})
79 | sess.Save(r, w)
80 | } else if err != nil {
81 | // Display error message
82 | log.Println(err)
83 | sess.AddFlash(view.Flash{"There was an error. Please try again later.", view.FlashError})
84 | sess.Save(r, w)
85 | } else if passhash.MatchString(result.Password, password) {
86 | if result.StatusID != 1 {
87 | // User inactive and display inactive message
88 | sess.AddFlash(view.Flash{"Account is inactive so login is disabled.", view.FlashNotice})
89 | sess.Save(r, w)
90 | } else {
91 | // Login successfully
92 | session.Empty(sess)
93 | sess.AddFlash(view.Flash{"Login successful!", view.FlashSuccess})
94 | sess.Values["id"] = result.UserID()
95 | sess.Values["email"] = email
96 | sess.Values["first_name"] = result.FirstName
97 | sess.Save(r, w)
98 | http.Redirect(w, r, "/", http.StatusFound)
99 | return
100 | }
101 | } else {
102 | loginAttempt(sess)
103 | sess.AddFlash(view.Flash{"Password is incorrect - Attempt: " + fmt.Sprintf("%v", sess.Values[sessLoginAttempt]), view.FlashWarning})
104 | sess.Save(r, w)
105 | }
106 |
107 | // Show the login page again
108 | LoginGET(w, r)
109 | }
110 |
111 | // LogoutGET clears the session and logs the user out
112 | func LogoutGET(w http.ResponseWriter, r *http.Request) {
113 | // Get session
114 | sess := session.Instance(r)
115 |
116 | // If user is authenticated
117 | if sess.Values["id"] != nil {
118 | session.Empty(sess)
119 | sess.AddFlash(view.Flash{"Goodbye!", view.FlashNotice})
120 | sess.Save(r, w)
121 | }
122 |
123 | http.Redirect(w, r, "/", http.StatusFound)
124 | }
125 |
--------------------------------------------------------------------------------
/template/base.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{template "title" .}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{CSS "static/css/bootstrap.min.css"}}
33 | {{CSS "//fonts.googleapis.com/css?family=Open+Sans:300,400,bold,italic"}}
34 | {{CSS "static/css/global.css"}}
35 |
36 |
37 |
38 |
42 |
43 | {{template "head" .}}
44 |
45 |
46 |
47 |
63 |
64 |
65 |
66 | {{range $fm := .flashes}}
67 |
68 |
69 | {{.Message}}
70 |
71 | {{end}}
72 |
73 |
74 | {{template "content" .}}
75 |
76 | {{JS "static/js/jquery1.11.0.min.js"}}
77 | {{JS "static/js/underscore-min.js"}}
78 | {{JS "static/js/bootstrap.min.js"}}
79 | {{JS "static/js/global.js"}}
80 |
81 | {{template "foot" .}}
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/vendor/app/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "app/shared/database"
8 |
9 | "gopkg.in/mgo.v2/bson"
10 | )
11 |
12 | // *****************************************************************************
13 | // User
14 | // *****************************************************************************
15 |
16 | // User table contains the information for each user
17 | type User struct {
18 | ObjectID bson.ObjectId `bson:"_id"`
19 | ID uint32 `db:"id" bson:"id,omitempty"` // Don't use Id, use UserID() instead for consistency with MongoDB
20 | FirstName string `db:"first_name" bson:"first_name"`
21 | LastName string `db:"last_name" bson:"last_name"`
22 | Email string `db:"email" bson:"email"`
23 | Password string `db:"password" bson:"password"`
24 | StatusID uint8 `db:"status_id" bson:"status_id"`
25 | CreatedAt time.Time `db:"created_at" bson:"created_at"`
26 | UpdatedAt time.Time `db:"updated_at" bson:"updated_at"`
27 | Deleted uint8 `db:"deleted" bson:"deleted"`
28 | }
29 |
30 | // UserStatus table contains every possible user status (active/inactive)
31 | type UserStatus struct {
32 | ID uint8 `db:"id" bson:"id"`
33 | Status string `db:"status" bson:"status"`
34 | CreatedAt time.Time `db:"created_at" bson:"created_at"`
35 | UpdatedAt time.Time `db:"updated_at" bson:"updated_at"`
36 | Deleted uint8 `db:"deleted" bson:"deleted"`
37 | }
38 |
39 | // UserID returns the user id
40 | func (u *User) UserID() string {
41 | r := ""
42 |
43 | switch database.ReadConfig().Type {
44 | case database.TypeMySQL:
45 | r = fmt.Sprintf("%v", u.ID)
46 | case database.TypeMongoDB:
47 | r = u.ObjectID.Hex()
48 | case database.TypeBolt:
49 | r = u.ObjectID.Hex()
50 | }
51 |
52 | return r
53 | }
54 |
55 | // UserByEmail gets user information from email
56 | func UserByEmail(email string) (User, error) {
57 | var err error
58 |
59 | result := User{}
60 |
61 | switch database.ReadConfig().Type {
62 | case database.TypeMySQL:
63 | err = database.SQL.Get(&result, "SELECT id, password, status_id, first_name FROM user WHERE email = ? LIMIT 1", email)
64 | case database.TypeMongoDB:
65 | if database.CheckConnection() {
66 | session := database.Mongo.Copy()
67 | defer session.Close()
68 | c := session.DB(database.ReadConfig().MongoDB.Database).C("user")
69 | err = c.Find(bson.M{"email": email}).One(&result)
70 | } else {
71 | err = ErrUnavailable
72 | }
73 | case database.TypeBolt:
74 | err = database.View("user", email, &result)
75 | if err != nil {
76 | err = ErrNoResult
77 | }
78 | default:
79 | err = ErrCode
80 | }
81 |
82 | return result, standardizeError(err)
83 | }
84 |
85 | // UserCreate creates user
86 | func UserCreate(firstName, lastName, email, password string) error {
87 | var err error
88 |
89 | now := time.Now()
90 |
91 | switch database.ReadConfig().Type {
92 | case database.TypeMySQL:
93 | _, err = database.SQL.Exec("INSERT INTO user (first_name, last_name, email, password) VALUES (?,?,?,?)", firstName,
94 | lastName, email, password)
95 | case database.TypeMongoDB:
96 | if database.CheckConnection() {
97 | session := database.Mongo.Copy()
98 | defer session.Close()
99 | c := session.DB(database.ReadConfig().MongoDB.Database).C("user")
100 |
101 | user := &User{
102 | ObjectID: bson.NewObjectId(),
103 | FirstName: firstName,
104 | LastName: lastName,
105 | Email: email,
106 | Password: password,
107 | StatusID: 1,
108 | CreatedAt: now,
109 | UpdatedAt: now,
110 | Deleted: 0,
111 | }
112 | err = c.Insert(user)
113 | } else {
114 | err = ErrUnavailable
115 | }
116 | case database.TypeBolt:
117 | user := &User{
118 | ObjectID: bson.NewObjectId(),
119 | FirstName: firstName,
120 | LastName: lastName,
121 | Email: email,
122 | Password: password,
123 | StatusID: 1,
124 | CreatedAt: now,
125 | UpdatedAt: now,
126 | Deleted: 0,
127 | }
128 |
129 | err = database.Update("user", user.Email, &user)
130 | default:
131 | err = ErrCode
132 | }
133 |
134 | return standardizeError(err)
135 | }
136 |
--------------------------------------------------------------------------------
/vendor/app/route/route.go:
--------------------------------------------------------------------------------
1 | package route
2 |
3 | import (
4 | "net/http"
5 |
6 | "app/controller"
7 | "app/route/middleware/acl"
8 | hr "app/route/middleware/httprouterwrapper"
9 | "app/route/middleware/logrequest"
10 | "app/route/middleware/pprofhandler"
11 | "app/shared/session"
12 |
13 | "github.com/gorilla/context"
14 | "github.com/josephspurrier/csrfbanana"
15 | "github.com/julienschmidt/httprouter"
16 | "github.com/justinas/alice"
17 | )
18 |
19 | // Load returns the routes and middleware
20 | func Load() http.Handler {
21 | return middleware(routes())
22 | }
23 |
24 | // LoadHTTPS returns the HTTP routes and middleware
25 | func LoadHTTPS() http.Handler {
26 | return middleware(routes())
27 | }
28 |
29 | // LoadHTTP returns the HTTPS routes and middleware
30 | func LoadHTTP() http.Handler {
31 | return middleware(routes())
32 |
33 | // Uncomment this and comment out the line above to always redirect to HTTPS
34 | //return http.HandlerFunc(redirectToHTTPS)
35 | }
36 |
37 | // Optional method to make it easy to redirect from HTTP to HTTPS
38 | func redirectToHTTPS(w http.ResponseWriter, req *http.Request) {
39 | http.Redirect(w, req, "https://"+req.Host, http.StatusMovedPermanently)
40 | }
41 |
42 | // *****************************************************************************
43 | // Routes
44 | // *****************************************************************************
45 |
46 | func routes() *httprouter.Router {
47 | r := httprouter.New()
48 |
49 | // Set 404 handler
50 | r.NotFound = alice.
51 | New().
52 | ThenFunc(controller.Error404)
53 |
54 | // Serve static files, no directory browsing
55 | r.GET("/static/*filepath", hr.Handler(alice.
56 | New().
57 | ThenFunc(controller.Static)))
58 |
59 | // Home page
60 | r.GET("/", hr.Handler(alice.
61 | New().
62 | ThenFunc(controller.IndexGET)))
63 |
64 | // Login
65 | r.GET("/login", hr.Handler(alice.
66 | New(acl.DisallowAuth).
67 | ThenFunc(controller.LoginGET)))
68 | r.POST("/login", hr.Handler(alice.
69 | New(acl.DisallowAuth).
70 | ThenFunc(controller.LoginPOST)))
71 | r.GET("/logout", hr.Handler(alice.
72 | New().
73 | ThenFunc(controller.LogoutGET)))
74 |
75 | // Register
76 | r.GET("/register", hr.Handler(alice.
77 | New(acl.DisallowAuth).
78 | ThenFunc(controller.RegisterGET)))
79 | r.POST("/register", hr.Handler(alice.
80 | New(acl.DisallowAuth).
81 | ThenFunc(controller.RegisterPOST)))
82 |
83 | // About
84 | r.GET("/about", hr.Handler(alice.
85 | New().
86 | ThenFunc(controller.AboutGET)))
87 |
88 | // Notepad
89 | r.GET("/notepad", hr.Handler(alice.
90 | New(acl.DisallowAnon).
91 | ThenFunc(controller.NotepadReadGET)))
92 | r.GET("/notepad/create", hr.Handler(alice.
93 | New(acl.DisallowAnon).
94 | ThenFunc(controller.NotepadCreateGET)))
95 | r.POST("/notepad/create", hr.Handler(alice.
96 | New(acl.DisallowAnon).
97 | ThenFunc(controller.NotepadCreatePOST)))
98 | r.GET("/notepad/update/:id", hr.Handler(alice.
99 | New(acl.DisallowAnon).
100 | ThenFunc(controller.NotepadUpdateGET)))
101 | r.POST("/notepad/update/:id", hr.Handler(alice.
102 | New(acl.DisallowAnon).
103 | ThenFunc(controller.NotepadUpdatePOST)))
104 | r.GET("/notepad/delete/:id", hr.Handler(alice.
105 | New(acl.DisallowAnon).
106 | ThenFunc(controller.NotepadDeleteGET)))
107 |
108 | // Enable Pprof
109 | r.GET("/debug/pprof/*pprof", hr.Handler(alice.
110 | New(acl.DisallowAnon).
111 | ThenFunc(pprofhandler.Handler)))
112 |
113 | return r
114 | }
115 |
116 | // *****************************************************************************
117 | // Middleware
118 | // *****************************************************************************
119 |
120 | func middleware(h http.Handler) http.Handler {
121 | // Prevents CSRF and Double Submits
122 | cs := csrfbanana.New(h, session.Store, session.Name)
123 | cs.FailureHandler(http.HandlerFunc(controller.InvalidToken))
124 | cs.ClearAfterUsage(true)
125 | cs.ExcludeRegexPaths([]string{"/static(.*)"})
126 | csrfbanana.TokenLength = 32
127 | csrfbanana.TokenName = "token"
128 | csrfbanana.SingleToken = false
129 | h = cs
130 |
131 | // Log every request
132 | h = logrequest.Handler(h)
133 |
134 | // Clear handler for Gorilla Context
135 | h = context.ClearHandler(h)
136 |
137 | return h
138 | }
139 |
--------------------------------------------------------------------------------
/vendor/app/shared/database/database.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "time"
8 |
9 | "github.com/boltdb/bolt"
10 | _ "github.com/go-sql-driver/mysql" // MySQL driver
11 | "github.com/jmoiron/sqlx"
12 | "gopkg.in/mgo.v2"
13 | )
14 |
15 | var (
16 | // BoltDB wrapper
17 | BoltDB *bolt.DB
18 | // Mongo wrapper
19 | Mongo *mgo.Session
20 | // SQL wrapper
21 | SQL *sqlx.DB
22 | // Database info
23 | databases Info
24 | )
25 |
26 | // Type is the type of database from a Type* constant
27 | type Type string
28 |
29 | const (
30 | // TypeBolt is BoltDB
31 | TypeBolt Type = "Bolt"
32 | // TypeMongoDB is MongoDB
33 | TypeMongoDB Type = "MongoDB"
34 | // TypeMySQL is MySQL
35 | TypeMySQL Type = "MySQL"
36 | )
37 |
38 | // Info contains the database configurations
39 | type Info struct {
40 | // Database type
41 | Type Type
42 | // MySQL info if used
43 | MySQL MySQLInfo
44 | // Bolt info if used
45 | Bolt BoltInfo
46 | // MongoDB info if used
47 | MongoDB MongoDBInfo
48 | }
49 |
50 | // MySQLInfo is the details for the database connection
51 | type MySQLInfo struct {
52 | Username string
53 | Password string
54 | Name string
55 | Hostname string
56 | Port int
57 | Parameter string
58 | }
59 |
60 | // BoltInfo is the details for the database connection
61 | type BoltInfo struct {
62 | Path string
63 | }
64 |
65 | // MongoDBInfo is the details for the database connection
66 | type MongoDBInfo struct {
67 | URL string
68 | Database string
69 | }
70 |
71 | // DSN returns the Data Source Name
72 | func DSN(ci MySQLInfo) string {
73 | // Example: root:@tcp(localhost:3306)/test
74 | return ci.Username +
75 | ":" +
76 | ci.Password +
77 | "@tcp(" +
78 | ci.Hostname +
79 | ":" +
80 | fmt.Sprintf("%d", ci.Port) +
81 | ")/" +
82 | ci.Name + ci.Parameter
83 | }
84 |
85 | // Connect to the database
86 | func Connect(d Info) {
87 | var err error
88 |
89 | // Store the config
90 | databases = d
91 |
92 | switch d.Type {
93 | case TypeMySQL:
94 | // Connect to MySQL
95 | if SQL, err = sqlx.Connect("mysql", DSN(d.MySQL)); err != nil {
96 | log.Println("SQL Driver Error", err)
97 | }
98 |
99 | // Check if is alive
100 | if err = SQL.Ping(); err != nil {
101 | log.Println("Database Error", err)
102 | }
103 | case TypeBolt:
104 | // Connect to Bolt
105 | if BoltDB, err = bolt.Open(d.Bolt.Path, 0600, nil); err != nil {
106 | log.Println("Bolt Driver Error", err)
107 | }
108 | case TypeMongoDB:
109 | // Connect to MongoDB
110 | if Mongo, err = mgo.DialWithTimeout(d.MongoDB.URL, 5*time.Second); err != nil {
111 | log.Println("MongoDB Driver Error", err)
112 | return
113 | }
114 |
115 | // Prevents these errors: read tcp 127.0.0.1:27017: i/o timeout
116 | Mongo.SetSocketTimeout(1 * time.Second)
117 |
118 | // Check if is alive
119 | if err = Mongo.Ping(); err != nil {
120 | log.Println("Database Error", err)
121 | }
122 | default:
123 | log.Println("No registered database in config")
124 | }
125 | }
126 |
127 | // Update makes a modification to Bolt
128 | func Update(bucketName string, key string, dataStruct interface{}) error {
129 | err := BoltDB.Update(func(tx *bolt.Tx) error {
130 | // Create the bucket
131 | bucket, e := tx.CreateBucketIfNotExists([]byte(bucketName))
132 | if e != nil {
133 | return e
134 | }
135 |
136 | // Encode the record
137 | encodedRecord, e := json.Marshal(dataStruct)
138 | if e != nil {
139 | return e
140 | }
141 |
142 | // Store the record
143 | if e = bucket.Put([]byte(key), encodedRecord); e != nil {
144 | return e
145 | }
146 | return nil
147 | })
148 | return err
149 | }
150 |
151 | // View retrieves a record in Bolt
152 | func View(bucketName string, key string, dataStruct interface{}) error {
153 | err := BoltDB.View(func(tx *bolt.Tx) error {
154 | // Get the bucket
155 | b := tx.Bucket([]byte(bucketName))
156 | if b == nil {
157 | return bolt.ErrBucketNotFound
158 | }
159 |
160 | // Retrieve the record
161 | v := b.Get([]byte(key))
162 | if len(v) < 1 {
163 | return bolt.ErrInvalid
164 | }
165 |
166 | // Decode the record
167 | e := json.Unmarshal(v, &dataStruct)
168 | if e != nil {
169 | return e
170 | }
171 |
172 | return nil
173 | })
174 |
175 | return err
176 | }
177 |
178 | // Delete removes a record from Bolt
179 | func Delete(bucketName string, key string) error {
180 | err := BoltDB.Update(func(tx *bolt.Tx) error {
181 | // Get the bucket
182 | b := tx.Bucket([]byte(bucketName))
183 | if b == nil {
184 | return bolt.ErrBucketNotFound
185 | }
186 |
187 | return b.Delete([]byte(key))
188 | })
189 | return err
190 | }
191 |
192 | // CheckConnection returns true if MongoDB is available
193 | func CheckConnection() bool {
194 | if Mongo == nil {
195 | Connect(databases)
196 | }
197 |
198 | if Mongo != nil {
199 | return true
200 | }
201 |
202 | return false
203 | }
204 |
205 | // ReadConfig returns the database information
206 | func ReadConfig() Info {
207 | return databases
208 | }
209 |
--------------------------------------------------------------------------------
/vendor/app/controller/notepad.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 |
8 | "app/model"
9 | "app/shared/session"
10 | "app/shared/view"
11 |
12 | "github.com/gorilla/context"
13 | "github.com/josephspurrier/csrfbanana"
14 | "github.com/julienschmidt/httprouter"
15 | )
16 |
17 | // NotepadReadGET displays the notes in the notepad
18 | func NotepadReadGET(w http.ResponseWriter, r *http.Request) {
19 | // Get session
20 | sess := session.Instance(r)
21 |
22 | userID := fmt.Sprintf("%s", sess.Values["id"])
23 |
24 | notes, err := model.NotesByUserID(userID)
25 | if err != nil {
26 | log.Println(err)
27 | notes = []model.Note{}
28 | }
29 |
30 | // Display the view
31 | v := view.New(r)
32 | v.Name = "notepad/read"
33 | v.Vars["first_name"] = sess.Values["first_name"]
34 | v.Vars["notes"] = notes
35 | v.Render(w)
36 | }
37 |
38 | // NotepadCreateGET displays the note creation page
39 | func NotepadCreateGET(w http.ResponseWriter, r *http.Request) {
40 | // Get session
41 | sess := session.Instance(r)
42 |
43 | // Display the view
44 | v := view.New(r)
45 | v.Name = "notepad/create"
46 | v.Vars["token"] = csrfbanana.Token(w, r, sess)
47 | v.Render(w)
48 | }
49 |
50 | // NotepadCreatePOST handles the note creation form submission
51 | func NotepadCreatePOST(w http.ResponseWriter, r *http.Request) {
52 | // Get session
53 | sess := session.Instance(r)
54 |
55 | // Validate with required fields
56 | if validate, missingField := view.Validate(r, []string{"note"}); !validate {
57 | sess.AddFlash(view.Flash{"Field missing: " + missingField, view.FlashError})
58 | sess.Save(r, w)
59 | NotepadCreateGET(w, r)
60 | return
61 | }
62 |
63 | // Get form values
64 | content := r.FormValue("note")
65 |
66 | userID := fmt.Sprintf("%s", sess.Values["id"])
67 |
68 | // Get database result
69 | err := model.NoteCreate(content, userID)
70 | // Will only error if there is a problem with the query
71 | if err != nil {
72 | log.Println(err)
73 | sess.AddFlash(view.Flash{"An error occurred on the server. Please try again later.", view.FlashError})
74 | sess.Save(r, w)
75 | } else {
76 | sess.AddFlash(view.Flash{"Note added!", view.FlashSuccess})
77 | sess.Save(r, w)
78 | http.Redirect(w, r, "/notepad", http.StatusFound)
79 | return
80 | }
81 |
82 | // Display the same page
83 | NotepadCreateGET(w, r)
84 | }
85 |
86 | // NotepadUpdateGET displays the note update page
87 | func NotepadUpdateGET(w http.ResponseWriter, r *http.Request) {
88 | // Get session
89 | sess := session.Instance(r)
90 |
91 | // Get the note id
92 | var params httprouter.Params
93 | params = context.Get(r, "params").(httprouter.Params)
94 | noteID := params.ByName("id")
95 |
96 | userID := fmt.Sprintf("%s", sess.Values["id"])
97 |
98 | // Get the note
99 | note, err := model.NoteByID(userID, noteID)
100 | if err != nil { // If the note doesn't exist
101 | log.Println(err)
102 | sess.AddFlash(view.Flash{"An error occurred on the server. Please try again later.", view.FlashError})
103 | sess.Save(r, w)
104 | http.Redirect(w, r, "/notepad", http.StatusFound)
105 | return
106 | }
107 |
108 | // Display the view
109 | v := view.New(r)
110 | v.Name = "notepad/update"
111 | v.Vars["token"] = csrfbanana.Token(w, r, sess)
112 | v.Vars["note"] = note.Content
113 | v.Render(w)
114 | }
115 |
116 | // NotepadUpdatePOST handles the note update form submission
117 | func NotepadUpdatePOST(w http.ResponseWriter, r *http.Request) {
118 | // Get session
119 | sess := session.Instance(r)
120 |
121 | // Validate with required fields
122 | if validate, missingField := view.Validate(r, []string{"note"}); !validate {
123 | sess.AddFlash(view.Flash{"Field missing: " + missingField, view.FlashError})
124 | sess.Save(r, w)
125 | NotepadUpdateGET(w, r)
126 | return
127 | }
128 |
129 | // Get form values
130 | content := r.FormValue("note")
131 |
132 | userID := fmt.Sprintf("%s", sess.Values["id"])
133 |
134 | var params httprouter.Params
135 | params = context.Get(r, "params").(httprouter.Params)
136 | noteID := params.ByName("id")
137 |
138 | // Get database result
139 | err := model.NoteUpdate(content, userID, noteID)
140 | // Will only error if there is a problem with the query
141 | if err != nil {
142 | log.Println(err)
143 | sess.AddFlash(view.Flash{"An error occurred on the server. Please try again later.", view.FlashError})
144 | sess.Save(r, w)
145 | } else {
146 | sess.AddFlash(view.Flash{"Note updated!", view.FlashSuccess})
147 | sess.Save(r, w)
148 | http.Redirect(w, r, "/notepad", http.StatusFound)
149 | return
150 | }
151 |
152 | // Display the same page
153 | NotepadUpdateGET(w, r)
154 | }
155 |
156 | // NotepadDeleteGET handles the note deletion
157 | func NotepadDeleteGET(w http.ResponseWriter, r *http.Request) {
158 | // Get session
159 | sess := session.Instance(r)
160 |
161 | userID := fmt.Sprintf("%s", sess.Values["id"])
162 |
163 | var params httprouter.Params
164 | params = context.Get(r, "params").(httprouter.Params)
165 | noteID := params.ByName("id")
166 |
167 | // Get database result
168 | err := model.NoteDelete(userID, noteID)
169 | // Will only error if there is a problem with the query
170 | if err != nil {
171 | log.Println(err)
172 | sess.AddFlash(view.Flash{"An error occurred on the server. Please try again later.", view.FlashError})
173 | sess.Save(r, w)
174 | } else {
175 | sess.AddFlash(view.Flash{"Note deleted!", view.FlashSuccess})
176 | sess.Save(r, w)
177 | }
178 |
179 | http.Redirect(w, r, "/notepad", http.StatusFound)
180 | return
181 | }
182 |
--------------------------------------------------------------------------------
/vendor/app/model/note.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "time"
9 |
10 | "app/shared/database"
11 |
12 | "github.com/boltdb/bolt"
13 | "gopkg.in/mgo.v2/bson"
14 | )
15 |
16 | // *****************************************************************************
17 | // Note
18 | // *****************************************************************************
19 |
20 | // Note table contains the information for each note
21 | type Note struct {
22 | ObjectID bson.ObjectId `bson:"_id"`
23 | ID uint32 `db:"id" bson:"id,omitempty"` // Don't use Id, use NoteID() instead for consistency with MongoDB
24 | Content string `db:"content" bson:"content"`
25 | UserID bson.ObjectId `bson:"user_id"`
26 | UID uint32 `db:"user_id" bson:"userid,omitempty"`
27 | CreatedAt time.Time `db:"created_at" bson:"created_at"`
28 | UpdatedAt time.Time `db:"updated_at" bson:"updated_at"`
29 | Deleted uint8 `db:"deleted" bson:"deleted"`
30 | }
31 |
32 | // NoteID returns the note id
33 | func (u *Note) NoteID() string {
34 | r := ""
35 |
36 | switch database.ReadConfig().Type {
37 | case database.TypeMySQL:
38 | r = fmt.Sprintf("%v", u.ID)
39 | case database.TypeMongoDB:
40 | r = u.ObjectID.Hex()
41 | case database.TypeBolt:
42 | r = u.ObjectID.Hex()
43 | }
44 |
45 | return r
46 | }
47 |
48 | // NoteByID gets note by ID
49 | func NoteByID(userID string, noteID string) (Note, error) {
50 | var err error
51 |
52 | result := Note{}
53 |
54 | switch database.ReadConfig().Type {
55 | case database.TypeMySQL:
56 | err = database.SQL.Get(&result, "SELECT id, content, user_id, created_at, updated_at, deleted FROM note WHERE id = ? AND user_id = ? LIMIT 1", noteID, userID)
57 | case database.TypeMongoDB:
58 | if database.CheckConnection() {
59 | // Create a copy of mongo
60 | session := database.Mongo.Copy()
61 | defer session.Close()
62 | c := session.DB(database.ReadConfig().MongoDB.Database).C("note")
63 |
64 | // Validate the object id
65 | if bson.IsObjectIdHex(noteID) {
66 | err = c.FindId(bson.ObjectIdHex(noteID)).One(&result)
67 | if result.UserID != bson.ObjectIdHex(userID) {
68 | result = Note{}
69 | err = ErrUnauthorized
70 | }
71 | } else {
72 | err = ErrNoResult
73 | }
74 | } else {
75 | err = ErrUnavailable
76 | }
77 | case database.TypeBolt:
78 | err = database.View("note", userID+noteID, &result)
79 | if err != nil {
80 | err = ErrNoResult
81 | }
82 | if result.UserID != bson.ObjectIdHex(userID) {
83 | result = Note{}
84 | err = ErrUnauthorized
85 | }
86 | default:
87 | err = ErrCode
88 | }
89 |
90 | return result, standardizeError(err)
91 | }
92 |
93 | // NotesByUserID gets all notes for a user
94 | func NotesByUserID(userID string) ([]Note, error) {
95 | var err error
96 |
97 | var result []Note
98 |
99 | switch database.ReadConfig().Type {
100 | case database.TypeMySQL:
101 | err = database.SQL.Select(&result, "SELECT id, content, user_id, created_at, updated_at, deleted FROM note WHERE user_id = ?", userID)
102 | case database.TypeMongoDB:
103 | if database.CheckConnection() {
104 | // Create a copy of mongo
105 | session := database.Mongo.Copy()
106 | defer session.Close()
107 | c := session.DB(database.ReadConfig().MongoDB.Database).C("note")
108 |
109 | // Validate the object id
110 | if bson.IsObjectIdHex(userID) {
111 | err = c.Find(bson.M{"user_id": bson.ObjectIdHex(userID)}).All(&result)
112 | } else {
113 | err = ErrNoResult
114 | }
115 | } else {
116 | err = ErrUnavailable
117 | }
118 | case database.TypeBolt:
119 | // View retrieves a record set in Bolt
120 | err = database.BoltDB.View(func(tx *bolt.Tx) error {
121 | // Get the bucket
122 | b := tx.Bucket([]byte("note"))
123 | if b == nil {
124 | return bolt.ErrBucketNotFound
125 | }
126 |
127 | // Get the iterator
128 | c := b.Cursor()
129 |
130 | prefix := []byte(userID)
131 | for k, v := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, v = c.Next() {
132 | var single Note
133 |
134 | // Decode the record
135 | err := json.Unmarshal(v, &single)
136 | if err != nil {
137 | log.Println(err)
138 | continue
139 | }
140 |
141 | result = append(result, single)
142 | }
143 |
144 | return nil
145 | })
146 | default:
147 | err = ErrCode
148 | }
149 |
150 | return result, standardizeError(err)
151 | }
152 |
153 | // NoteCreate creates a note
154 | func NoteCreate(content string, userID string) error {
155 | var err error
156 |
157 | now := time.Now()
158 |
159 | switch database.ReadConfig().Type {
160 | case database.TypeMySQL:
161 | _, err = database.SQL.Exec("INSERT INTO note (content, user_id) VALUES (?,?)", content, userID)
162 | case database.TypeMongoDB:
163 | if database.CheckConnection() {
164 | // Create a copy of mongo
165 | session := database.Mongo.Copy()
166 | defer session.Close()
167 | c := session.DB(database.ReadConfig().MongoDB.Database).C("note")
168 |
169 | note := &Note{
170 | ObjectID: bson.NewObjectId(),
171 | Content: content,
172 | UserID: bson.ObjectIdHex(userID),
173 | CreatedAt: now,
174 | UpdatedAt: now,
175 | Deleted: 0,
176 | }
177 | err = c.Insert(note)
178 | } else {
179 | err = ErrUnavailable
180 | }
181 | case database.TypeBolt:
182 | note := &Note{
183 | ObjectID: bson.NewObjectId(),
184 | Content: content,
185 | UserID: bson.ObjectIdHex(userID),
186 | CreatedAt: now,
187 | UpdatedAt: now,
188 | Deleted: 0,
189 | }
190 |
191 | err = database.Update("note", userID+note.ObjectID.Hex(), ¬e)
192 | default:
193 | err = ErrCode
194 | }
195 |
196 | return standardizeError(err)
197 | }
198 |
199 | // NoteUpdate updates a note
200 | func NoteUpdate(content string, userID string, noteID string) error {
201 | var err error
202 |
203 | now := time.Now()
204 |
205 | switch database.ReadConfig().Type {
206 | case database.TypeMySQL:
207 | _, err = database.SQL.Exec("UPDATE note SET content=? WHERE id = ? AND user_id = ? LIMIT 1", content, noteID, userID)
208 | case database.TypeMongoDB:
209 | if database.CheckConnection() {
210 | // Create a copy of mongo
211 | session := database.Mongo.Copy()
212 | defer session.Close()
213 | c := session.DB(database.ReadConfig().MongoDB.Database).C("note")
214 | var note Note
215 | note, err = NoteByID(userID, noteID)
216 | if err == nil {
217 | // Confirm the owner is attempting to modify the note
218 | if note.UserID.Hex() == userID {
219 | note.UpdatedAt = now
220 | note.Content = content
221 | err = c.UpdateId(bson.ObjectIdHex(noteID), ¬e)
222 | } else {
223 | err = ErrUnauthorized
224 | }
225 | }
226 | } else {
227 | err = ErrUnavailable
228 | }
229 | case database.TypeBolt:
230 | var note Note
231 | note, err = NoteByID(userID, noteID)
232 | if err == nil {
233 | // Confirm the owner is attempting to modify the note
234 | if note.UserID.Hex() == userID {
235 | note.UpdatedAt = now
236 | note.Content = content
237 | err = database.Update("note", userID+note.ObjectID.Hex(), ¬e)
238 | } else {
239 | err = ErrUnauthorized
240 | }
241 | }
242 | default:
243 | err = ErrCode
244 | }
245 |
246 | return standardizeError(err)
247 | }
248 |
249 | // NoteDelete deletes a note
250 | func NoteDelete(userID string, noteID string) error {
251 | var err error
252 |
253 | switch database.ReadConfig().Type {
254 | case database.TypeMySQL:
255 | _, err = database.SQL.Exec("DELETE FROM note WHERE id = ? AND user_id = ?", noteID, userID)
256 | case database.TypeMongoDB:
257 | if database.CheckConnection() {
258 | // Create a copy of mongo
259 | session := database.Mongo.Copy()
260 | defer session.Close()
261 | c := session.DB(database.ReadConfig().MongoDB.Database).C("note")
262 |
263 | var note Note
264 | note, err = NoteByID(userID, noteID)
265 | if err == nil {
266 | // Confirm the owner is attempting to modify the note
267 | if note.UserID.Hex() == userID {
268 | err = c.RemoveId(bson.ObjectIdHex(noteID))
269 | } else {
270 | err = ErrUnauthorized
271 | }
272 | }
273 | } else {
274 | err = ErrUnavailable
275 | }
276 | case database.TypeBolt:
277 | var note Note
278 | note, err = NoteByID(userID, noteID)
279 | if err == nil {
280 | // Confirm the owner is attempting to modify the note
281 | if note.UserID.Hex() == userID {
282 | err = database.Delete("note", userID+note.ObjectID.Hex())
283 | } else {
284 | err = ErrUnauthorized
285 | }
286 | }
287 | default:
288 | err = ErrCode
289 | }
290 |
291 | return standardizeError(err)
292 | }
293 |
--------------------------------------------------------------------------------
/vendor/app/shared/view/view.go:
--------------------------------------------------------------------------------
1 | package view
2 |
3 | import (
4 | "encoding/gob"
5 | "encoding/json"
6 | "fmt"
7 | "html/template"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "path/filepath"
12 | "strings"
13 | "sync"
14 |
15 | "app/shared/session"
16 | )
17 |
18 | func init() {
19 | // Magic goes here to allow serializing maps in securecookie
20 | // http://golang.org/pkg/encoding/gob/#Register
21 | // Source: http://stackoverflow.com/questions/21934730/gob-type-not-registered-for-interface-mapstringinterface
22 | gob.Register(Flash{})
23 | }
24 |
25 | var (
26 | // FlashError is a bootstrap class
27 | FlashError = "alert-danger"
28 | // FlashSuccess is a bootstrap class
29 | FlashSuccess = "alert-success"
30 | // FlashNotice is a bootstrap class
31 | FlashNotice = "alert-info"
32 | // FlashWarning is a bootstrap class
33 | FlashWarning = "alert-warning"
34 |
35 | childTemplates []string
36 | rootTemplate string
37 | templateCollection = make(map[string]*template.Template)
38 | pluginCollection = make(template.FuncMap)
39 | mutex sync.RWMutex
40 | mutexPlugins sync.RWMutex
41 | sessionName string
42 | viewInfo View
43 | )
44 |
45 | // Template root and children
46 | type Template struct {
47 | Root string `json:"Root"`
48 | Children []string `json:"Children"`
49 | }
50 |
51 | // View attributes
52 | type View struct {
53 | BaseURI string
54 | Extension string
55 | Folder string
56 | Name string
57 | Caching bool
58 | Vars map[string]interface{}
59 | request *http.Request
60 | }
61 |
62 | // Flash Message
63 | type Flash struct {
64 | Message string
65 | Class string
66 | }
67 |
68 | // Configure sets the view information
69 | func Configure(vi View) {
70 | viewInfo = vi
71 | }
72 |
73 | // ReadConfig returns the configuration
74 | func ReadConfig() View {
75 | return viewInfo
76 | }
77 |
78 | // LoadTemplates will set the root and child templates
79 | func LoadTemplates(rootTemp string, childTemps []string) {
80 | rootTemplate = rootTemp
81 | childTemplates = childTemps
82 | }
83 |
84 | // LoadPlugins will combine all template.FuncMaps into one map and then set the
85 | // plugins for the templates
86 | // If a func already exists, it is rewritten, there is no error
87 | func LoadPlugins(fms ...template.FuncMap) {
88 | // Final FuncMap
89 | fm := make(template.FuncMap)
90 |
91 | // Loop through the maps
92 | for _, m := range fms {
93 | // Loop through each key and value
94 | for k, v := range m {
95 | fm[k] = v
96 | }
97 | }
98 |
99 | // Load the plugins
100 | mutexPlugins.Lock()
101 | pluginCollection = fm
102 | mutexPlugins.Unlock()
103 | }
104 |
105 | // PrependBaseURI prepends the base URI to the string
106 | func (v *View) PrependBaseURI(s string) string {
107 | return v.BaseURI + s
108 | }
109 |
110 | // New returns a new view
111 | func New(req *http.Request) *View {
112 | v := &View{}
113 | v.Vars = make(map[string]interface{})
114 | v.Vars["AuthLevel"] = "anon"
115 |
116 | v.BaseURI = viewInfo.BaseURI
117 | v.Extension = viewInfo.Extension
118 | v.Folder = viewInfo.Folder
119 | v.Name = viewInfo.Name
120 |
121 | // Make sure BaseURI is available in the templates
122 | v.Vars["BaseURI"] = v.BaseURI
123 |
124 | // This is required for the view to access the request
125 | v.request = req
126 |
127 | // Get session
128 | sess := session.Instance(v.request)
129 |
130 | // Set the AuthLevel to auth if the user is logged in
131 | if sess.Values["id"] != nil {
132 | v.Vars["AuthLevel"] = "auth"
133 | }
134 |
135 | return v
136 | }
137 |
138 | // AssetTimePath returns a URL with the proper base uri and timestamp appended.
139 | // Works for CSS and JS assets
140 | // Determines if local or on the web
141 | func (v *View) AssetTimePath(s string) (string, error) {
142 | if strings.HasPrefix(s, "//") {
143 | return s, nil
144 | }
145 |
146 | s = strings.TrimLeft(s, "/")
147 | abs, err := filepath.Abs(s)
148 |
149 | if err != nil {
150 | return "", err
151 | }
152 |
153 | time, err2 := FileTime(abs)
154 | if err2 != nil {
155 | return "", err2
156 | }
157 |
158 | return v.PrependBaseURI(s + "?" + time), nil
159 | }
160 |
161 | // RenderSingle renders a template to the writer
162 | func (v *View) RenderSingle(w http.ResponseWriter) {
163 |
164 | // Get the template collection from cache
165 | /*mutex.RLock()
166 | tc, ok := templateCollection[v.Name]
167 | mutex.RUnlock()*/
168 |
169 | // Get the plugin collection
170 | mutexPlugins.RLock()
171 | pc := pluginCollection
172 | mutexPlugins.RUnlock()
173 |
174 | templateList := []string{v.Name}
175 |
176 | // List of template names
177 | /*templateList := make([]string, 0)
178 | templateList = append(templateList, rootTemplate)
179 | templateList = append(templateList, v.Name)
180 | templateList = append(templateList, childTemplates...)*/
181 |
182 | // Loop through each template and test the full path
183 | for i, name := range templateList {
184 | // Get the absolute path of the root template
185 | path, err := filepath.Abs(v.Folder + string(os.PathSeparator) + name + "." + v.Extension)
186 | if err != nil {
187 | http.Error(w, "Template Path Error: "+err.Error(), http.StatusInternalServerError)
188 | return
189 | }
190 | templateList[i] = path
191 | }
192 |
193 | // Determine if there is an error in the template syntax
194 | templates, err := template.New(v.Name).Funcs(pc).ParseFiles(templateList...)
195 |
196 | if err != nil {
197 | http.Error(w, "Template Parse Error: "+err.Error(), http.StatusInternalServerError)
198 | return
199 | }
200 |
201 | // Cache the template collection
202 | /*mutex.Lock()
203 | templateCollection[v.Name] = templates
204 | mutex.Unlock()*/
205 |
206 | // Save the template collection
207 | tc := templates
208 |
209 | // Get session
210 | sess := session.Instance(v.request)
211 |
212 | // Get the flashes for the template
213 | if flashes := sess.Flashes(); len(flashes) > 0 {
214 | v.Vars["flashes"] = make([]Flash, len(flashes))
215 | for i, f := range flashes {
216 | switch f.(type) {
217 | case Flash:
218 | v.Vars["flashes"].([]Flash)[i] = f.(Flash)
219 | default:
220 | v.Vars["flashes"].([]Flash)[i] = Flash{f.(string), "alert-box"}
221 | }
222 |
223 | }
224 | sess.Save(v.request, w)
225 | }
226 |
227 | // Display the content to the screen
228 | err = tc.Funcs(pc).ExecuteTemplate(w, v.Name+"."+v.Extension, v.Vars)
229 |
230 | if err != nil {
231 | http.Error(w, "Template File Error: "+err.Error(), http.StatusInternalServerError)
232 | }
233 | }
234 |
235 | // Render renders a template to the writer
236 | func (v *View) Render(w http.ResponseWriter) {
237 |
238 | // Get the template collection from cache
239 | mutex.RLock()
240 | tc, ok := templateCollection[v.Name]
241 | mutex.RUnlock()
242 |
243 | // Get the plugin collection
244 | mutexPlugins.RLock()
245 | pc := pluginCollection
246 | mutexPlugins.RUnlock()
247 |
248 | // If the template collection is not cached or caching is disabled
249 | if !ok || !viewInfo.Caching {
250 |
251 | // List of template names
252 | var templateList []string
253 | templateList = append(templateList, rootTemplate)
254 | templateList = append(templateList, v.Name)
255 | templateList = append(templateList, childTemplates...)
256 |
257 | // Loop through each template and test the full path
258 | for i, name := range templateList {
259 | // Get the absolute path of the root template
260 | path, err := filepath.Abs(v.Folder + string(os.PathSeparator) + name + "." + v.Extension)
261 | if err != nil {
262 | http.Error(w, "Template Path Error: "+err.Error(), http.StatusInternalServerError)
263 | return
264 | }
265 | templateList[i] = path
266 | }
267 |
268 | // Determine if there is an error in the template syntax
269 | templates, err := template.New(v.Name).Funcs(pc).ParseFiles(templateList...)
270 |
271 | if err != nil {
272 | http.Error(w, "Template Parse Error: "+err.Error(), http.StatusInternalServerError)
273 | return
274 | }
275 |
276 | // Cache the template collection
277 | mutex.Lock()
278 | templateCollection[v.Name] = templates
279 | mutex.Unlock()
280 |
281 | // Save the template collection
282 | tc = templates
283 | }
284 |
285 | // Get session
286 | sess := session.Instance(v.request)
287 |
288 | // Get the flashes for the template
289 | if flashes := sess.Flashes(); len(flashes) > 0 {
290 | v.Vars["flashes"] = make([]Flash, len(flashes))
291 | for i, f := range flashes {
292 | switch f.(type) {
293 | case Flash:
294 | v.Vars["flashes"].([]Flash)[i] = f.(Flash)
295 | default:
296 | v.Vars["flashes"].([]Flash)[i] = Flash{f.(string), "alert-box"}
297 | }
298 |
299 | }
300 | sess.Save(v.request, w)
301 | }
302 |
303 | // Display the content to the screen
304 | err := tc.Funcs(pc).ExecuteTemplate(w, rootTemplate+"."+v.Extension, v.Vars)
305 |
306 | if err != nil {
307 | http.Error(w, "Template File Error: "+err.Error(), http.StatusInternalServerError)
308 | }
309 | }
310 |
311 | // Validate returns true if all the required form values are passed
312 | func Validate(req *http.Request, required []string) (bool, string) {
313 | for _, v := range required {
314 | if req.FormValue(v) == "" {
315 | return false, v
316 | }
317 | }
318 |
319 | return true, ""
320 | }
321 |
322 | // SendFlashes allows retrieval of flash messages for using with Ajax
323 | func (v *View) SendFlashes(w http.ResponseWriter) {
324 | // Get session
325 | sess := session.Instance(v.request)
326 |
327 | flashes := peekFlashes(w, v.request)
328 | sess.Save(v.request, w)
329 |
330 | js, err := json.Marshal(flashes)
331 | if err != nil {
332 | http.Error(w, "JSON Error: "+err.Error(), http.StatusInternalServerError)
333 | return
334 | }
335 |
336 | w.Header().Set("Content-Type", "application/json")
337 | w.Write(js)
338 | }
339 |
340 | func peekFlashes(w http.ResponseWriter, r *http.Request) []Flash {
341 | // Get session
342 | sess := session.Instance(r)
343 |
344 | var v []Flash
345 |
346 | // Get the flashes for the template
347 | if flashes := sess.Flashes(); len(flashes) > 0 {
348 | v = make([]Flash, len(flashes))
349 | for i, f := range flashes {
350 | switch f.(type) {
351 | case Flash:
352 | v[i] = f.(Flash)
353 | default:
354 | v[i] = Flash{f.(string), "alert-box"}
355 | }
356 |
357 | }
358 | }
359 |
360 | return v
361 | }
362 |
363 | // Repopulate updates the dst map so the form fields can be refilled
364 | func Repopulate(list []string, src url.Values, dst map[string]interface{}) {
365 | for _, v := range list {
366 | dst[v] = src.Get(v)
367 | }
368 | }
369 |
370 | // FileTime returns the modification time of the file
371 | func FileTime(name string) (string, error) {
372 | fi, err := os.Stat(name)
373 | if err != nil {
374 | return "", err
375 | }
376 | mtime := fi.ModTime().Unix()
377 | return fmt.Sprintf("%v", mtime), nil
378 | }
379 |
--------------------------------------------------------------------------------
/static/js/underscore-min.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.7.0
2 | // http://underscorejs.org
3 | // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4 | // Underscore may be freely distributed under the MIT license.
5 | (function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this);
6 | //# sourceMappingURL=underscore-min.map
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GoWebApp
2 |
3 | [](https://goreportcard.com/report/github.com/josephspurrier/gowebapp)
4 | [](https://godoc.org/github.com/josephspurrier/gowebapp)
5 |
6 | Basic MVC Web Application in Go
7 |
8 | #### I recommend you use Blue Jay which is the latest version of this project: [https://github.com/blue-jay/blueprint](https://github.com/blue-jay/blueprint).
9 |
10 | This project demonstrates how to structure and build a website using the Go language without a framework. There is a blog article you can read at [http://www.josephspurrier.com/go-web-app-example/](http://www.josephspurrier.com/go-web-app-example/). There is a full application I built with an earlier version of the project at [https://github.com/verifiedninja/webapp](https://github.com/verifiedninja/webapp). There is an API version of this project at [https://github.com/josephspurrier/gowebapi](https://github.com/josephspurrier/gowebapi).
11 |
12 | To download, run the following command:
13 |
14 | ~~~
15 | go get github.com/josephspurrier/gowebapp
16 | ~~~
17 |
18 | If you are on Go 1.5, you need to set GOVENDOREXPERIMENT to 1. If you are on Go 1.4 or earlier, the code will not work because it uses the vendor folder.
19 |
20 | ### Other Branches
21 |
22 | - 2021-02-17: There is a branch using Go modules (requires Go 1.13) available here: https://github.com/josephspurrier/gowebapp/tree/modules.
23 | - 2021-05-23: There is a branch using Go embedded assets (requires Go 1.16) available here: https://github.com/josephspurrier/gowebapp/tree/embedded-templates.
24 |
25 | ## Quick Start with Bolt
26 |
27 | The gowebapp.db file will be created once you start the application.
28 |
29 | Build and run from the root directory. Open your web browser to: http://localhost. You should see the welcome page.
30 |
31 | Navigate to the login page, and then to the register page. Create a new user and you should be able to login. That's it.
32 |
33 | ## Quick Start with MongoDB
34 |
35 | Start MongoDB.
36 |
37 | Open config/config.json and edit the Database section so the connection information matches your MongoDB instance. Also, change Type from Bolt to MongoDB.
38 |
39 | Build and run from the root directory. Open your web browser to: http://localhost. You should see the welcome page.
40 |
41 | Navigate to the login page, and then to the register page. Create a new user and you should be able to login. That's it.
42 |
43 | ## Quick Start with MySQL
44 |
45 | Start MySQL and import config/mysql.sql to create the database and tables.
46 |
47 | Open config/config.json and edit the Database section so the connection information matches your MySQL instance. Also, change Type from Bolt to MySQL.
48 |
49 | Build and run from the root directory. Open your web browser to: http://localhost. You should see the welcome page.
50 |
51 | Navigate to the login page, and then to the register page. Create a new user and you should be able to login. That's it.
52 |
53 | ## Overview
54 |
55 | The web app has a public home page, authenticated home page, login page, register page,
56 | about page, and a simple notepad to demonstrate the CRUD operations.
57 |
58 | The entrypoint for the web app is gowebapp.go. The file loads the application settings,
59 | starts the session, connects to the database, sets up the templates, loads
60 | the routes, attaches the middleware, and starts the web server.
61 |
62 | The front end is built using Bootstrap with a few small changes to fonts and spacing. The flash
63 | messages are customized so they show up at the bottom right of the screen.
64 |
65 | All of the error and warning messages should be either displayed either to the
66 | user or in the console. Informational messages are displayed to the user via
67 | flash messages that disappear after 4 seconds. The flash messages are controlled
68 | by JavaScript in the static folder.
69 |
70 | ## Structure
71 |
72 | Recently, the folder structure changed. After looking at all the forks
73 | and reusing my project in different places, I decided to move the Go code to the
74 | **app** folder inside the **vendor** folder so the github path is not littered
75 | throughout the many imports. I did not want to use relative paths so the vendor
76 | folder seemed like the best option.
77 |
78 | The project is organized into the following folders:
79 |
80 | ~~~
81 | config - application settings and database schema
82 | static - location of statically served files like CSS and JS
83 | template - HTML templates
84 |
85 | vendor/app/controller - page logic organized by HTTP methods (GET, POST)
86 | vendor/app/shared - packages for templates, MySQL, cryptography, sessions, and json
87 | vendor/app/model - database queries
88 | vendor/app/route - route information and middleware
89 | ~~~
90 |
91 | There are a few external packages:
92 |
93 | ~~~
94 | github.com/gorilla/context - registry for global request variables
95 | github.com/gorilla/sessions - cookie and filesystem sessions
96 | github.com/go-sql-driver/mysql - MySQL driver
97 | github.com/haisum/recaptcha - Google reCAPTCHA support
98 | github.com/jmoiron/sqlx - MySQL general purpose extensions
99 | github.com/josephspurrier/csrfbanana - CSRF protection for gorilla sessions
100 | github.com/julienschmidt/httprouter - high performance HTTP request router
101 | github.com/justinas/alice - middleware chaining
102 | github.com/mattn/go-sqlite3 - SQLite driver
103 | golang.org/x/crypto/bcrypt - password hashing algorithm
104 | ~~~
105 |
106 | The templates are organized into folders under the **template** folder:
107 |
108 | ~~~
109 | about/about.tmpl - quick info about the app
110 | index/anon.tmpl - public home page
111 | index/auth.tmpl - home page once you login
112 | login/login.tmpl - login page
113 | notepad/create.tmpl - create note
114 | notepad/read.tmpl - read a note
115 | notepad/update.tmpl - update a note
116 | partial/footer.tmpl - footer
117 | partial/menu.tmpl - menu at the top of all the pages
118 | register/register.tmpl - register page
119 | base.tmpl - base template for all the pages
120 | ~~~
121 |
122 | ## Templates
123 |
124 | There are a few template funcs that are available to make working with the templates
125 | and static files easier:
126 |
127 | ~~~ html
128 |
129 | {{CSS "static/css/normalize3.0.0.min.css"}}
130 | parses to
131 |
132 |
133 |
134 | {{JS "static/js/jquery1.11.0.min.js"}}
135 | parses to
136 |
137 |
138 |
139 | {{LINK "register" "Create a new account."}}
140 | parses to
141 | Create a new account.
142 |
143 |
144 | {{.SomeVariable | NOESCAPE}}
145 |
146 |
147 | {{.SomeTime | PRETTYTIME}}
148 | parses to format
149 | 3:04 PM 01/02/2006
150 | ~~~
151 |
152 | There are a few variables you can use in templates as well:
153 |
154 | ~~~ html
155 |
156 | {{if eq .AuthLevel "auth"}}
157 | You are logged in.
158 | {{else}}
159 | You are not logged in.
160 | {{end}}
161 |
162 |
163 | About
164 |
165 |
166 |
167 | ~~~
168 |
169 | It's also easy to add template-specific code before the closing and