summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve Manuel <nilslice@gmail.com>2016-10-06 03:14:10 -0700
committerSteve Manuel <nilslice@gmail.com>2016-10-06 03:14:10 -0700
commitc0ba07669d8403f428ec250e3f3da74844c6c587 (patch)
tree94313747d9abea02b2f3b01c06308225d9db5e4a
parent698173a6176762f966be0abd1dc77b85e482a03a (diff)
adding authentication & token-based persistence for users, init setup for first-use, pulling out some handlers into separate file for readability and navigation
-rw-r--r--system/admin/admin.go160
-rw-r--r--system/admin/auth.go11
-rw-r--r--system/admin/config.go5
-rw-r--r--system/admin/config/config.go89
-rw-r--r--system/admin/handlers.go127
-rw-r--r--system/admin/server.go185
-rw-r--r--system/admin/static/dashboard/css/admin.css44
-rw-r--r--system/admin/user/auth.go106
-rw-r--r--system/api/server.go8
-rw-r--r--system/db/query.go207
10 files changed, 680 insertions, 262 deletions
diff --git a/system/admin/admin.go b/system/admin/admin.go
index 410c9f4..1c04bce 100644
--- a/system/admin/admin.go
+++ b/system/admin/admin.go
@@ -7,12 +7,13 @@ import (
"html/template"
"github.com/nilslice/cms/content"
+ "github.com/nilslice/cms/system/db"
)
-const adminHTML = `<!doctype html>
+var startAdminHTML = `<!doctype html>
<html lang="en">
<head>
- <title>CMS</title>
+ <title>{{ .Logo }}</title>
<script type="text/javascript" src="/admin/static/common/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="/admin/static/common/js/util.js"></script>
<script type="text/javascript" src="/admin/static/dashboard/js/materialize.min.js"></script>
@@ -32,7 +33,7 @@ const adminHTML = `<!doctype html>
<div class="navbar-fixed">
<nav class="grey darken-2">
<div class="nav-wrapper">
- <a class="brand-logo" href="/admin">CMS</a>
+ <a class="brand-logo" href="/admin">{{ .Logo }}</a>
<ul class="right">
<li><a href="/admin/logout">Logout</a></li>
@@ -41,8 +42,9 @@ const adminHTML = `<!doctype html>
</nav>
</div>
- <div class="admin-ui row">
-
+ <div class="admin-ui row">`
+
+var mainAdminHTML = `
<div class="left-nav col s3">
<div class="card">
<ul class="card-content collection">
@@ -57,6 +59,7 @@ const adminHTML = `<!doctype html>
<div class="card-title">System</div>
<div class="row collection-item">
<li><a class="col s12" href="/admin/configure"><i class="tiny left material-icons">settings</i>Configuration</a></li>
+ <li><a class="col s12" href="/admin/configure/users"><i class="tiny left material-icons">supervisor_account</i>Users</a></li>
</div>
</ul>
</div>
@@ -65,26 +68,167 @@ const adminHTML = `<!doctype html>
<div class="subview col s9">
{{ .Subview }}
</div>
- {{ end }}
+ {{ end }}`
+
+var endAdminHTML = `
</div>
</body>
</html>`
type admin struct {
+ Logo string
Types map[string]func() interface{}
Subview template.HTML
}
// Admin ...
func Admin(view []byte) ([]byte, error) {
+ cfg, err := db.Config("name")
+ if err != nil {
+ return nil, err
+ }
+
+ if cfg == nil {
+ cfg = []byte("")
+ }
+
a := admin{
+ Logo: string(cfg),
Types: content.Types,
Subview: template.HTML(view),
}
buf := &bytes.Buffer{}
- tmpl := template.Must(template.New("admin").Parse(adminHTML))
- err := tmpl.Execute(buf, a)
+ html := startAdminHTML + mainAdminHTML + endAdminHTML
+ tmpl := template.Must(template.New("admin").Parse(html))
+ err = tmpl.Execute(buf, a)
+ if err != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+var initAdminHTML = `
+<div class="init col s5">
+<div class="card">
+<div class="card-content">
+ <div class="card-title">Welcome!</div>
+ <blockquote>You need to initialize your system by filling out the form below. All of
+ this information can be updated later on, but you will not be able to start
+ without first completing this step.</blockquote>
+ <form method="post" action="/admin/init" class="row">
+ <div>Configuration</div>
+ <div class="input-field col s12">
+ <input placeholder="Enter the name of your site (interal use only)" class="validate required" type="text" id="name" name="name"/>
+ <label for="name" class="active">Site Name</label>
+ </div>
+ <div class="input-field col s12">
+ <input placeholder="Used for acquiring SSL certificate (e.g. www.example.com or example.com)" class="validate" type="text" id="domain" name="domain"/>
+ <label for="domain" class="active">Domain</label>
+ </div>
+ <div>Admin Details</div>
+ <div class="input-field col s12">
+ <input placeholder="Your email address e.g. you@example.com" class="validate required" type="email" id="email" name="email"/>
+ <label for="email" class="active">Email</label>
+ </div>
+ <div class="input-field col s12">
+ <input placeholder="Enter a strong password" class="validate required" type="password" id="password" name="password"/>
+ <label for="password" class="active">Password</label>
+ </div>
+ <button class="btn waves-effect waves-light right">Start</button>
+ </form>
+</div>
+</div>
+</div>
+<script>
+ $(function() {
+ $('.nav-wrapper ul.right').hide();
+
+ var logo = $('a.brand-logo');
+ var name = $('input#name');
+
+ name.on('change', function(e) {
+ logo.text(e.target.value);
+ });
+ });
+</script>
+`
+
+// Init ...
+func Init() ([]byte, error) {
+ html := startAdminHTML + initAdminHTML + endAdminHTML
+
+ cfg, err := db.Config("name")
+ if err != nil {
+ return nil, err
+ }
+
+ if cfg == nil {
+ cfg = []byte("")
+ }
+
+ a := admin{
+ Logo: string(cfg),
+ }
+
+ buf := &bytes.Buffer{}
+ tmpl := template.Must(template.New("init").Parse(html))
+ err = tmpl.Execute(buf, a)
+ if err != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+var loginAdminHTML = `
+<div class="init col s5">
+<div class="card">
+<div class="card-content">
+ <div class="card-title">Welcome!</div>
+ <blockquote>Please log in to the system using your email address and password.</blockquote>
+ <form method="post" action="/admin/login" class="row">
+ <div class="input-field col s12">
+ <input placeholder="Enter your email address e.g. you@example.com" class="validate required" type="email" id="email" name="email"/>
+ <label for="email" class="active">Email</label>
+ </div>
+ <div class="input-field col s12">
+ <input placeholder="Enter your password" class="validate required" type="password" id="password" name="password"/>
+ <label for="password" class="active">Password</label>
+ </div>
+ <button class="btn waves-effect waves-light right">Log in</button>
+ </form>
+</div>
+</div>
+</div>
+<script>
+ $(function() {
+ $('.nav-wrapper ul.right').hide();
+ });
+</script>
+`
+
+// Login ...
+func Login() ([]byte, error) {
+ html := startAdminHTML + loginAdminHTML + endAdminHTML
+
+ cfg, err := db.Config("name")
+ if err != nil {
+ return nil, err
+ }
+
+ if cfg == nil {
+ cfg = []byte("")
+ }
+
+ a := admin{
+ Logo: string(cfg),
+ }
+
+ buf := &bytes.Buffer{}
+ tmpl := template.Must(template.New("login").Parse(html))
+ err = tmpl.Execute(buf, a)
if err != nil {
return nil, err
}
diff --git a/system/admin/auth.go b/system/admin/auth.go
deleted file mode 100644
index 153a31a..0000000
--- a/system/admin/auth.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package admin
-
-// Session ...
-type Session struct {
- User
- token string
-}
-
-// User ...
-type User struct {
-}
diff --git a/system/admin/config.go b/system/admin/config.go
deleted file mode 100644
index e067299..0000000
--- a/system/admin/config.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package admin
-
-// Config represents the confirgurable options of the system
-type Config struct {
-}
diff --git a/system/admin/config/config.go b/system/admin/config/config.go
new file mode 100644
index 0000000..791510e
--- /dev/null
+++ b/system/admin/config/config.go
@@ -0,0 +1,89 @@
+package config
+
+import (
+ "github.com/nilslice/cms/content"
+ "github.com/nilslice/cms/management/editor"
+)
+
+//Config represents the confirgurable options of the system
+type Config struct {
+ content.Item
+ editor editor.Editor
+
+ Name string `json:"name"`
+ Domain string `json:"domain"`
+ ClientSecret string `json:"client_secret"`
+}
+
+// SetContentID partially implements editor.Editable
+func (c *Config) SetContentID(id int) { c.ID = id }
+
+// ContentID partially implements editor.Editable
+func (c *Config) ContentID() int { return c.ID }
+
+// ContentName partially implements editor.Editable
+func (c *Config) ContentName() string { return c.Name }
+
+// SetSlug partially implements editor.Editable
+func (c *Config) SetSlug(slug string) { c.Slug = slug }
+
+// Editor partially implements editor.Editable
+func (c *Config) Editor() *editor.Editor { return &c.editor }
+
+// MarshalEditor writes a buffer of html to edit a Post and partially implements editor.Editable
+func (c *Config) MarshalEditor() ([]byte, error) {
+ view, err := editor.Form(c,
+ editor.Field{
+ View: editor.Input("Name", c, map[string]string{
+ "label": "Site Name",
+ "placeholder": "Add a name to this site (internal use only)",
+ }),
+ },
+ editor.Field{
+ View: editor.Input("Domain", c, map[string]string{
+ "label": "Domain Name (required for SSL certificate)",
+ "placeholder": "e.g. www.example.com or example.com",
+ }),
+ },
+ editor.Field{
+ View: editor.Input("ClientSecret", c, map[string]string{
+ "label": "Client Secret (used to validate requests)",
+ "disabled": "true",
+ }),
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ open := []byte(`<div class="card"><form action="/admin/configure" method="post">`)
+ close := []byte(`</form></div>`)
+ script := []byte(`
+ <script>
+ $(function() {
+ // hide default fields & labels unecessary for the config
+ var fields = $('.default-fields');
+ fields.css('position', 'relative');
+ fields.find('input:not([type=submit])').remove();
+ fields.find('label').remove();
+ fields.find('button').css({
+ position: 'absolute',
+ top: '-10px',
+ right: '0px'
+ });
+
+ // adjust layout of td so save button is in same location as usual
+ fields.find('td').css('float', 'right');
+
+ // stop some fixed config settings from being modified
+ fields.find('input[name=client_secret]').attr('name', '');
+ });
+ </script>
+ `)
+
+ view = append(open, view...)
+ view = append(view, close...)
+ view = append(view, script...)
+
+ return view, nil
+}
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
new file mode 100644
index 0000000..9ff39c3
--- /dev/null
+++ b/system/admin/handlers.go
@@ -0,0 +1,127 @@
+package admin
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/nilslice/cms/system/admin/user"
+ "github.com/nilslice/cms/system/db"
+ "github.com/nilslice/jwt"
+)
+
+func adminHandler(res http.ResponseWriter, req *http.Request) {
+ view, err := Admin(nil)
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "text/html")
+ res.Write(view)
+}
+
+func loginHandler(res http.ResponseWriter, req *http.Request) {
+ if !db.SystemInitComplete() {
+ redir := req.URL.Scheme + req.URL.Host + "/admin/init"
+ http.Redirect(res, req, redir, http.StatusFound)
+ return
+ }
+
+ switch req.Method {
+ case http.MethodGet:
+ if user.IsValid(req) {
+ http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin", http.StatusFound)
+ return
+ }
+
+ view, err := Login()
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "text/html")
+ res.Write(view)
+
+ case http.MethodPost:
+ if user.IsValid(req) {
+ http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin", http.StatusFound)
+ return
+ }
+
+ err := req.ParseForm()
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ fmt.Println(req.FormValue("email"))
+ fmt.Println(req.FormValue("password"))
+
+ // check email & password
+ j, err := db.User(req.FormValue("email"))
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ if j == nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusBadRequest)
+ fmt.Println("j == nil")
+ return
+ }
+
+ usr := &user.User{}
+ err = json.Unmarshal(j, usr)
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ if !user.IsUser(usr, req.FormValue("password")) {
+ res.WriteHeader(http.StatusBadRequest)
+ fmt.Println("!IsUser")
+ return
+ }
+ // create new token
+ week := time.Now().Add(time.Hour * 24 * 7)
+ claims := map[string]interface{}{
+ "exp": week,
+ "user": usr.Email,
+ }
+ token, err := jwt.New(claims)
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // add it to cookie +1 week expiration
+ http.SetCookie(res, &http.Cookie{
+ Name: "_token",
+ Value: token,
+ Expires: week,
+ })
+
+ http.Redirect(res, req, strings.TrimSuffix(req.URL.String(), "/login"), http.StatusFound)
+ }
+}
+
+func logoutHandler(res http.ResponseWriter, req *http.Request) {
+ http.SetCookie(res, &http.Cookie{
+ Name: "_token",
+ Expires: time.Unix(0, 0),
+ Value: "",
+ })
+
+ http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin/login", http.StatusFound)
+}
diff --git a/system/admin/server.go b/system/admin/server.go
index 900f47d..92c86c3 100644
--- a/system/admin/server.go
+++ b/system/admin/server.go
@@ -2,6 +2,7 @@ package admin
import (
"bytes"
+ "encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -16,22 +17,15 @@ import (
"github.com/nilslice/cms/content"
"github.com/nilslice/cms/management/editor"
"github.com/nilslice/cms/management/manager"
+ "github.com/nilslice/cms/system/admin/config"
+ "github.com/nilslice/cms/system/admin/user"
"github.com/nilslice/cms/system/db"
+ "github.com/nilslice/jwt"
)
// Run adds Handlers to default http listener for Admin
func Run() {
- http.HandleFunc("/admin", func(res http.ResponseWriter, req *http.Request) {
- adminView, err := Admin(nil)
- if err != nil {
- fmt.Println(err)
- res.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- res.Header().Set("Content-Type", "text/html")
- res.Write(adminView)
- })
+ http.HandleFunc("/admin", user.Auth(adminHandler))
http.HandleFunc("/admin/static/", func(res http.ResponseWriter, req *http.Request) {
path := req.URL.Path
@@ -49,17 +43,151 @@ func Run() {
http.ServeFile(res, req, filepath.Join(filePathParts...))
})
- http.HandleFunc("/admin/configure", func(res http.ResponseWriter, req *http.Request) {
- adminView, err := Admin(nil)
- if err != nil {
- fmt.Println(err)
- res.WriteHeader(http.StatusInternalServerError)
+ http.HandleFunc("/admin/init", func(res http.ResponseWriter, req *http.Request) {
+ if db.SystemInitComplete() {
+ http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin", http.StatusFound)
return
}
- res.Header().Set("Content-Type", "text/html")
- res.Write(adminView)
+ switch req.Method {
+ case http.MethodGet:
+ view, err := Init()
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ res.Header().Set("Content-Type", "text/html")
+ res.Write(view)
+ case http.MethodPost:
+ err := req.ParseForm()
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // get the site name from post to encode and use as secret
+ name := []byte(req.FormValue("name"))
+ secret := base64.StdEncoding.EncodeToString(name)
+ req.Form.Set("client_secret", secret)
+
+ err = db.SetConfig(req.Form)
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ email := req.FormValue("email")
+ password := req.FormValue("password")
+ usr := user.NewUser(email, password)
+
+ _, err = db.SetUser(usr)
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // add _token cookie for login persistence
+ week := time.Now().Add(time.Hour * 24 * 7)
+ claims := map[string]interface{}{
+ "exp": week.Unix(),
+ "user": usr.Email,
+ }
+
+ jwt.Secret([]byte(secret))
+ token, err := jwt.New(claims)
+
+ http.SetCookie(res, &http.Cookie{
+ Name: "_token",
+ Value: token,
+ Expires: week,
+ })
+
+ redir := strings.TrimSuffix(req.URL.String(), "/init")
+ http.Redirect(res, req, redir, http.StatusFound)
+
+ default:
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ }
+ })
+
+ http.HandleFunc("/admin/login", loginHandler)
+ http.HandleFunc("/admin/logout", logoutHandler)
+
+ http.HandleFunc("/admin/configure", func(res http.ResponseWriter, req *http.Request) {
+ switch req.Method {
+ case http.MethodGet:
+ data, err := db.ConfigAll()
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ c := &config.Config{}
+
+ err = json.Unmarshal(data, c)
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ cfg, err := c.MarshalEditor()
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ adminView, err := Admin(cfg)
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "text/html")
+ res.Write(adminView)
+
+ case http.MethodPost:
+ err := req.ParseForm()
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ err = db.SetConfig(req.Form)
+ if err != nil {
+ fmt.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(res, req, req.URL.String(), http.StatusFound)
+
+ default:
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ }
+
+ })
+
+ http.HandleFunc("/admin/configure/users", func(res http.ResponseWriter, req *http.Request) {
+ switch req.Method {
+ case http.MethodGet:
+ // list all users and delete buttons
+
+ case http.MethodPost:
+ // create new user
+
+ default:
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ }
})
http.HandleFunc("/admin/posts", func(res http.ResponseWriter, req *http.Request) {
@@ -69,20 +197,20 @@ func Run() {
res.WriteHeader(http.StatusBadRequest)
}
- posts := db.GetAll(t)
+ posts := db.ContentAll(t)
b := &bytes.Buffer{}
p := content.Types[t]().(editor.Editable)
- html := `<div class="col s9">
- <div class="card">
- <ul class="card-content collection posts">
- <div class="card-title">` + t + ` Items</div>`
+ html := `<div class="col s9 card">
+ <div class="card-content row">
+ <div class="card-title">` + t + ` Items</div>
+ <ul class="posts">`
for i := range posts {
json.Unmarshal(posts[i], &p)
- post := `<div class="row collection-item"><li class="col s12 collection-item"><a href="/admin/edit?type=` +
+ post := `<li class="col s12"><a href="/admin/edit?type=` +
t + `&id=` + fmt.Sprintf("%d", p.ContentID()) +
- `">` + p.ContentName() + `</a></li></div>`
+ `">` + p.ContentName() + `</a></li>`
b.Write([]byte(post))
}
@@ -116,7 +244,7 @@ func Run() {
post := contentType()
if i != "" {
- data, err := db.Get(t + ":" + i)
+ data, err := db.Content(t + ":" + i)
if err != nil {
fmt.Println(err)
res.WriteHeader(http.StatusInternalServerError)
@@ -208,7 +336,7 @@ func Run() {
req.PostForm.Del(discardKey)
}
- id, err := db.Set(t+":"+cid, req.PostForm)
+ id, err := db.SetContent(t+":"+cid, req.PostForm)
if err != nil {
fmt.Println(err)
res.WriteHeader(http.StatusInternalServerError)
@@ -221,6 +349,9 @@ func Run() {
sid := fmt.Sprintf("%d", id)
desURL := scheme + host + path + "?type=" + t + "&id=" + sid
http.Redirect(res, req, desURL, http.StatusFound)
+
+ default:
+ res.WriteHeader(http.StatusMethodNotAllowed)
}
})
diff --git a/system/admin/static/dashboard/css/admin.css b/system/admin/static/dashboard/css/admin.css
index cf8a84b..413edd5 100644
--- a/system/admin/static/dashboard/css/admin.css
+++ b/system/admin/static/dashboard/css/admin.css
@@ -8,16 +8,60 @@
margin: auto;
}
+.nav-wrapper a:hover {
+ color: inherit !important;
+}
+
.admin-ui {
width: 95%;
max-width: 1300px;
margin: 1% auto;
}
+.init {
+ float: none !important;
+ margin: auto !important;
+}
+
.manager {
margin-bottom: 2%;
}
+.left-nav .row li a {
+ padding: 10px 0;
+ transition: all 0.3s ease;
+}
+
+.left-nav .card-title {
+ margin-top: 10px !important;
+}
+
+.left-nav .card-title:first-child {
+ margin-top: 0px !important;
+}
+
+ul.posts li {
+ display: block;
+ margin: 0 0 20px 0;
+ padding: 0 0 20px 0 !important;
+ border-bottom: solid 1px #e0e0e0;
+}
+
+ul.posts li:last-child {
+ border-bottom: none;
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+li a {
+ transition: all 0.3s ease;
+}
+
+li a:hover {
+ color: #333;
+ transition: all 0.3s ease;
+}
+
a.new-post {
margin: 0.5rem 0 1rem 0.75rem;
}
diff --git a/system/admin/user/auth.go b/system/admin/user/auth.go
new file mode 100644
index 0000000..36f5e40
--- /dev/null
+++ b/system/admin/user/auth.go
@@ -0,0 +1,106 @@
+package user
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+
+ "github.com/nilslice/jwt"
+ "github.com/nilslice/rand"
+ "golang.org/x/crypto/bcrypt"
+)
+
+// User defines a admin user in the system
+type User struct {
+ ID int `json:"id"`
+ Email string `json:"email"`
+ Hash string `json:"hash"`
+ Salt string `json:"salt"`
+}
+
+// NewUser creates a user
+func NewUser(email, password string) *User {
+ salt := salt128()
+ hash := encryptPassword([]byte(password), salt)
+
+ user := &User{
+ Email: email,
+ Hash: string(hash),
+ Salt: base64.StdEncoding.EncodeToString(salt),
+ }
+
+ return user
+}
+
+// Auth is HTTP middleware to ensure the request has proper token credentials
+func Auth(next http.HandlerFunc) http.HandlerFunc {
+ return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+ redir := req.URL.Scheme + req.URL.Host + "/admin/login"
+
+ if IsValid(req) {
+ next.ServeHTTP(res, req)
+ } else {
+ http.Redirect(res, req, redir, http.StatusFound)
+ }
+ })
+}
+
+// IsValid checks if the user request is authenticated
+func IsValid(req *http.Request) bool {
+ // check if token exists in cookie
+ cookie, err := req.Cookie("_token")
+ if err != nil {
+ return false
+ }
+ // validate it and allow or redirect request
+ token := cookie.Value
+ return jwt.Passes(token)
+}
+
+// IsUser checks for consistency in email/pass combination
+func IsUser(usr *User, password string) bool {
+ fmt.Println(usr, password)
+ salt, err := base64.StdEncoding.DecodeString(usr.Salt)
+ if err != nil {
+ return false
+ }
+
+ err = comparePassword([]byte(usr.Hash), []byte(password), salt)
+ if err != nil {
+ return false
+ }
+
+ return true
+}
+
+// The following functions are from github.com/sluu99/um -----------------------
+
+// salt128 generates 128 bits of random data.
+func salt128() []byte {
+ x := make([]byte, 16)
+ rand.Read(x)
+ return x
+}
+
+// makePassword makes the actual password from the plain password and salt
+func makePassword(plainPw, salt []byte) []byte {
+ password := make([]byte, 0, len(plainPw)+len(salt))
+ password = append(password, salt...)
+ password = append(password, plainPw...)
+ return password
+}
+
+// encryptPassword uses bcrypt to encrypt a password and salt combination.
+// It returns the encrypted password in hex form.
+func encryptPassword(plainPw, salt []byte) []byte {
+ hash, _ := bcrypt.GenerateFromPassword(makePassword(plainPw, salt), 10)
+ return hash
+}
+
+// comparePassword compares a hash with the plain password and the salt.
+// The function returns nil on success or an error on failure.
+func comparePassword(hash, plainPw, salt []byte) error {
+ return bcrypt.CompareHashAndPassword(hash, makePassword(plainPw, salt))
+}
+
+// End code from github.com/sluu99/um ------------------------------------------
diff --git a/system/api/server.go b/system/api/server.go
index 5de10a2..1fb9f02 100644
--- a/system/api/server.go
+++ b/system/api/server.go
@@ -40,7 +40,7 @@ func Run() {
return
}
- posts := db.GetAll(t)
+ posts := db.ContentAll(t)
var all = []json.RawMessage{}
for _, post := range posts {
all = append(all, post)
@@ -66,7 +66,7 @@ func Run() {
return
}
- post, err := db.Get(t + ":" + id)
+ post, err := db.Content(t + ":" + id)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
@@ -123,9 +123,9 @@ func toJSON(data []string) ([]byte, error) {
func wrapJSON(json []byte) []byte {
var buf = &bytes.Buffer{}
- buf.Write([]byte("{data:"))
+ buf.Write([]byte(`{"data":`))
buf.Write(json)
- buf.Write([]byte("}"))
+ buf.Write([]byte(`}`))
return buf.Bytes()
}
diff --git a/system/db/query.go b/system/db/query.go
deleted file mode 100644
index 480946f..0000000
--- a/system/db/query.go
+++ /dev/null
@@ -1,207 +0,0 @@
-package db
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "log"
- "net/url"
- "strconv"
- "strings"
-
- "github.com/boltdb/bolt"
- "github.com/gorilla/schema"
- "github.com/nilslice/cms/content"
- "github.com/nilslice/cms/management/editor"
- "github.com/nilslice/cms/management/manager"
-)
-
-var store *bolt.DB
-
-// Init creates a db connection and initializes db with required info
-func Init() {
- var err error
- store, err = bolt.Open("store.db", 0666, nil)
- if err != nil {
- log.Fatal(err)
- }
-
- // initialize db with all content type buckets
- store.Update(func(tx *bolt.Tx) error {
- for t := range content.Types {
- _, err := tx.CreateBucketIfNotExists([]byte(t))
- if err != nil {
- return err
- }
- }
-
- return nil
- })
-
-}
-
-// Set inserts or updates values in the database.
-// The `target` argument is a string made up of namespace:id (string:int)
-func Set(target string, data url.Values) (int, error) {
- t := strings.Split(target, ":")
- ns, id := t[0], t[1]
-
- // check if content id == -1 (indicating new post).
- // if so, run an insert which will assign the next auto incremented int.
- // this is done because boltdb begins its bucket auto increment value at 0,
- // which is the zero-value of an int in the Item struct field for ID.
- // this is a problem when the original first post (with auto ID = 0) gets
- // overwritten by any new post, originally having no ID, defauting to 0.
- if id == "-1" {
- return insert(ns, data)
- }
-
- return update(ns, id, data)
-}
-
-func update(ns, id string, data url.Values) (int, error) {
- cid, err := strconv.Atoi(id)
- if err != nil {
- return 0, err
- }
-
- err = store.Update(func(tx *bolt.Tx) error {
- b, err := tx.CreateBucketIfNotExists([]byte(ns))
- if err != nil {
- return err
- }
-
- j, err := toJSON(ns, data)
- if err != nil {
- return err
- }
-
- err = b.Put([]byte(fmt.Sprintf("%d", cid)), j)
- if err != nil {
- return err
- }
-
- return nil
- })
- if err != nil {
- return 0, nil
- }
-
- return cid, nil
-}
-
-func insert(ns string, data url.Values) (int, error) {
- var effectedID int
- err := store.Update(func(tx *bolt.Tx) error {
- b, err := tx.CreateBucketIfNotExists([]byte(ns))
- if err != nil {
- return err
- }
-
- // get the next available ID and convert to string
- // also set effectedID to int of ID
- id, err := b.NextSequence()
- if err != nil {
- return err
- }
- cid := strconv.FormatUint(id, 10)
- effectedID, err = strconv.Atoi(cid)
- if err != nil {
- return err
- }
- data.Add("id", cid)
-
- j, err := toJSON(ns, data)
- if err != nil {
- return err
- }
-
- err = b.Put([]byte(cid), j)
- if err != nil {
- return err
- }
-
- return nil
- })
- if err != nil {
- return 0, err
- }
-
- return effectedID, nil
-}
-
-func toJSON(ns string, data url.Values) ([]byte, error) {
- // find the content type and decode values into it
- t, ok := content.Types[ns]
- if !ok {
- return nil, fmt.Errorf(content.ErrTypeNotRegistered, ns)
- }
- post := t()
-
- dec := schema.NewDecoder()
- dec.SetAliasTag("json") // allows simpler struct tagging when creating a content type
- dec.IgnoreUnknownKeys(true) // will skip over form values submitted, but not in struct
- err := dec.Decode(post, data)
- if err != nil {
- return nil, err
- }
-
- slug, err := manager.Slug(post.(editor.Editable))
- if err != nil {
- return nil, err
- }
- post.(editor.Editable).SetSlug(slug)
-
- // marshall content struct to json for db storage
- j, err := json.Marshal(post)
- if err != nil {
- return nil, err
- }
-
- return j, nil
-}
-
-// Get retrives one item from the database. Non-existent values will return an empty []byte
-// The `target` argument is a string made up of namespace:id (string:int)
-func Get(target string) ([]byte, error) {
- t := strings.Split(target, ":")
- ns, id := t[0], t[1]
-
- val := &bytes.Buffer{}
- err := store.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(ns))
- _, err := val.Write(b.Get([]byte(id)))
- if err != nil {
- fmt.Println(err)
- return err
- }
-
- return nil
- })
- if err != nil {
- return nil, err
- }
-
- return val.Bytes(), nil
-}
-
-// GetAll retrives all items from the database within the provided namespace
-func GetAll(namespace string) [][]byte {
- var posts [][]byte
- store.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(namespace))
-
- len := b.Stats().KeyN
- posts = make([][]byte, 0, len)
-
- b.ForEach(func(k, v []byte) error {
- posts = append(posts, v)
-
- return nil
- })
-
- return nil
- })
-
- return posts
-}