summaryrefslogtreecommitdiff
path: root/system
diff options
context:
space:
mode:
Diffstat (limited to 'system')
-rw-r--r--system/admin/admin.go128
-rw-r--r--system/admin/config/config.go8
-rw-r--r--system/admin/handlers.go591
-rw-r--r--system/admin/server.go3
-rw-r--r--system/admin/static/common/js/util.js16
-rw-r--r--system/admin/static/dashboard/css/admin.css26
-rw-r--r--system/admin/upload/upload.go (renamed from system/admin/upload.go)5
-rw-r--r--system/api/analytics/init.go101
-rw-r--r--system/api/external.go109
-rw-r--r--system/api/handlers.go80
-rw-r--r--system/api/server.go2
-rw-r--r--system/db/content.go124
-rw-r--r--system/db/init.go18
-rw-r--r--system/db/user.go114
-rw-r--r--system/tls/enable.go79
15 files changed, 1245 insertions, 159 deletions
diff --git a/system/admin/admin.go b/system/admin/admin.go
index b375eb6..9ee86c3 100644
--- a/system/admin/admin.go
+++ b/system/admin/admin.go
@@ -4,9 +4,12 @@ package admin
import (
"bytes"
+ "encoding/json"
"html/template"
+ "net/http"
"github.com/bosssauce/ponzu/content"
+ "github.com/bosssauce/ponzu/system/admin/user"
"github.com/bosssauce/ponzu/system/db"
)
@@ -241,6 +244,129 @@ func Login() ([]byte, error) {
return buf.Bytes(), nil
}
+// UsersList ...
+func UsersList(req *http.Request) ([]byte, error) {
+ html := `
+ <div class="card user-management">
+ <div class="card-title">Edit your account:</div>
+ <form class="row" enctype="multipart/form-data" action="/admin/configure/users/edit" method="post">
+ <div class="input-feild col s9">
+ <label class="active">Email Address</label>
+ <input type="email" name="email" value="{{ .User.Email }}"/>
+ </div>
+
+ <div class="input-feild col s9">
+ <div>To approve changes, enter your password:</div>
+
+ <label class="active">Current Password</label>
+ <input type="password" name="password"/>
+ </div>
+
+ <div class="input-feild col s9">
+ <label class="active">New Password: (leave blank if no password change needed)</label>
+ <input name="new_password" type="password"/>
+ </div>
+
+ <div class="input-feild col s9">
+ <button class="btn waves-effect waves-light green right" type="submit">Save</button>
+ </div>
+ </form>
+
+ <div class="card-title">Add a new user:</div>
+ <form class="row" enctype="multipart/form-data" action="/admin/configure/users" method="post">
+ <div class="input-feild col s9">
+ <label class="active">Email Address</label>
+ <input type="email" name="email" value=""/>
+ </div>
+
+ <div class="input-feild col s9">
+ <label class="active">Password</label>
+ <input type="password" name="password"/>
+ </div>
+
+ <div class="input-feild col s9">
+ <button class="btn waves-effect waves-light green right" type="submit">Add User</button>
+ </div>
+ </form>
+
+ <div class="card-title">Remove Admin Users</div>
+ <ul class="users row">
+ {{ range .Users }}
+ <li class="col s9">
+ {{ .Email }}
+ <form enctype="multipart/form-data" class="delete-user __ponzu right" action="/admin/configure/users/delete" method="post">
+ <span>Delete</span>
+ <input type="hidden" name="email" value="{{ .Email }}"/>
+ <input type="hidden" name="id" value="{{ .ID }}"/>
+ </form>
+ </li>
+ {{ end }}
+ </ul>
+ </div>
+ `
+ script := `
+ <script>
+ $(function() {
+ var del = $('.delete-user.__ponzu span');
+ del.on('click', function(e) {
+ if (confirm("[Ponzu] Please confirm:\n\nAre you sure you want to delete this user?\nThis cannot be undone.")) {
+ $(e.target).parent().submit();
+ }
+ });
+ });
+ </script>
+ `
+ // get current user out to pass as data to execute template
+ j, err := db.CurrentUser(req)
+ if err != nil {
+ return nil, err
+ }
+
+ var usr user.User
+ err = json.Unmarshal(j, &usr)
+ if err != nil {
+ return nil, err
+ }
+
+ // get all users to list
+ jj, err := db.UserAll()
+ if err != nil {
+ return nil, err
+ }
+
+ var usrs []user.User
+ for i := range jj {
+ var u user.User
+ err = json.Unmarshal(jj[i], &u)
+ if err != nil {
+ return nil, err
+ }
+ if u.Email != usr.Email {
+ usrs = append(usrs, u)
+ }
+ }
+
+ // make buffer to execute html into then pass buffer's bytes to Admin
+ buf := &bytes.Buffer{}
+ tmpl := template.Must(template.New("users").Parse(html + script))
+ data := map[string]interface{}{
+ "User": usr,
+ "Users": usrs,
+ }
+
+ err = tmpl.Execute(buf, data)
+ if err != nil {
+ return nil, err
+ }
+
+ view, err := Admin(buf.Bytes())
+ if err != nil {
+ return nil, err
+ }
+
+ return view, nil
+}
+
var err400HTML = `
<div class="error-page e400 col s6">
<div class="card">
@@ -288,7 +414,7 @@ var err405HTML = `
<div class="card">
<div class="card-content">
<div class="card-title"><b>405</b> Error: Method Not Allowed</div>
- <blockquote>Sorry, the page you requested could not be found.</blockquote>
+ <blockquote>Sorry, the method of your request is not allowed.</blockquote>
</div>
</div>
</div>
diff --git a/system/admin/config/config.go b/system/admin/config/config.go
index c83c311..66f767d 100644
--- a/system/admin/config/config.go
+++ b/system/admin/config/config.go
@@ -12,6 +12,7 @@ type Config struct {
Name string `json:"name"`
Domain string `json:"domain"`
+ AdminEmail string `json:"admin_email"`
ClientSecret string `json:"client_secret"`
Etag string `json:"etag"`
CacheInvalidate []string `json:"-"`
@@ -48,8 +49,13 @@ func (c *Config) MarshalEditor() ([]byte, error) {
}),
},
editor.Field{
+ View: editor.Input("AdminEmail", c, map[string]string{
+ "label": "Adminstrator Email (will be notified of internal system information)",
+ }),
+ },
+ editor.Field{
View: editor.Input("ClientSecret", c, map[string]string{
- "label": "Client Secret (used to validate requests)",
+ "label": "Client Secret (used to validate requests, DO NOT SHARE)",
"disabled": "true",
}),
},
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
index de340ae..742b898 100644
--- a/system/admin/handlers.go
+++ b/system/admin/handlers.go
@@ -14,16 +14,19 @@ import (
"github.com/bosssauce/ponzu/management/editor"
"github.com/bosssauce/ponzu/management/manager"
"github.com/bosssauce/ponzu/system/admin/config"
+ "github.com/bosssauce/ponzu/system/admin/upload"
"github.com/bosssauce/ponzu/system/admin/user"
+ "github.com/bosssauce/ponzu/system/api"
"github.com/bosssauce/ponzu/system/db"
+ "github.com/gorilla/schema"
"github.com/nilslice/jwt"
)
func adminHandler(res http.ResponseWriter, req *http.Request) {
view, err := Admin(nil)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -42,7 +45,7 @@ func initHandler(res http.ResponseWriter, req *http.Request) {
case http.MethodGet:
view, err := Init()
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -52,7 +55,7 @@ func initHandler(res http.ResponseWriter, req *http.Request) {
case http.MethodPost:
err := req.ParseForm()
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -66,20 +69,22 @@ func initHandler(res http.ResponseWriter, req *http.Request) {
etag := db.NewEtag()
req.Form.Set("etag", etag)
- err = db.SetConfig(req.Form)
- if err != nil {
- fmt.Println(err)
- res.WriteHeader(http.StatusInternalServerError)
- return
- }
-
email := strings.ToLower(req.FormValue("email"))
password := req.FormValue("password")
usr := user.NewUser(email, password)
_, err = db.SetUser(usr)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // set initial user email as admin_email and make config
+ req.Form.Set("admin_email", email)
+ err = db.SetConfig(req.Form)
+ if err != nil {
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -98,6 +103,7 @@ func initHandler(res http.ResponseWriter, req *http.Request) {
Name: "_token",
Value: token,
Expires: week,
+ Path: "/",
})
redir := strings.TrimSuffix(req.URL.String(), "/init")
@@ -113,7 +119,7 @@ func configHandler(res http.ResponseWriter, req *http.Request) {
case http.MethodGet:
data, err := db.ConfigAll()
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -122,21 +128,21 @@ func configHandler(res http.ResponseWriter, req *http.Request) {
err = json.Unmarshal(data, c)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
cfg, err := c.MarshalEditor()
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
adminView, err := Admin(cfg)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -147,14 +153,14 @@ func configHandler(res http.ResponseWriter, req *http.Request) {
case http.MethodPost:
err := req.ParseForm()
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
err = db.SetConfig(req.Form)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -170,10 +176,246 @@ func configHandler(res http.ResponseWriter, req *http.Request) {
func configUsersHandler(res http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
- // list all users and delete buttons
+ view, err := UsersList(req)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ res.Write(view)
case http.MethodPost:
// create new user
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ email := strings.ToLower(req.FormValue("email"))
+ password := req.PostFormValue("password")
+
+ if email == "" || password == "" {
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ usr := user.NewUser(email, password)
+
+ _, err = db.SetUser(usr)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ http.Redirect(res, req, req.URL.String(), http.StatusFound)
+
+ default:
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ }
+}
+
+func configUsersEditHandler(res http.ResponseWriter, req *http.Request) {
+ switch req.Method {
+ case http.MethodPost:
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+
+ // check if user to be edited is current user
+ j, err := db.CurrentUser(req)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ usr := &user.User{}
+ err = json.Unmarshal(j, usr)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ // check if password matches
+ password := req.PostFormValue("password")
+
+ if !user.IsUser(usr, password) {
+ log.Println("Unexpected user/password combination for", usr.Email)
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error405()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ email := strings.ToLower(req.PostFormValue("email"))
+ newPassword := req.PostFormValue("new_password")
+ var updatedUser *user.User
+ if newPassword != "" {
+ updatedUser = user.NewUser(email, newPassword)
+ } else {
+ updatedUser = user.NewUser(email, password)
+ }
+
+ // set the ID to the same ID as current user
+ updatedUser.ID = usr.ID
+
+ // set user in db
+ err = db.UpdateUser(usr, updatedUser)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ // create new token
+ week := time.Now().Add(time.Hour * 24 * 7)
+ claims := map[string]interface{}{
+ "exp": week,
+ "user": updatedUser.Email,
+ }
+ token, err := jwt.New(claims)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ // add token to cookie +1 week expiration
+ cookie := &http.Cookie{
+ Name: "_token",
+ Value: token,
+ Expires: week,
+ Path: "/",
+ }
+ http.SetCookie(res, cookie)
+
+ // add new token cookie to the request
+ req.AddCookie(cookie)
+
+ http.Redirect(res, req, strings.TrimSuffix(req.URL.String(), "/edit"), http.StatusFound)
+
+ default:
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ }
+}
+
+func configUsersDeleteHandler(res http.ResponseWriter, req *http.Request) {
+ switch req.Method {
+ case http.MethodPost:
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+
+ // do not allow current user to delete themselves
+ j, err := db.CurrentUser(req)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ usr := &user.User{}
+ err = json.Unmarshal(j, &usr)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ email := strings.ToLower(req.PostFormValue("email"))
+
+ if usr.Email == email {
+ log.Println(err)
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error405()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ // delete existing user
+ err = db.DeleteUser(email)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ http.Redirect(res, req, strings.TrimSuffix(req.URL.String(), "/delete"), http.StatusFound)
default:
res.WriteHeader(http.StatusMethodNotAllowed)
@@ -196,7 +438,7 @@ func loginHandler(res http.ResponseWriter, req *http.Request) {
view, err := Login()
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -212,7 +454,7 @@ func loginHandler(res http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
- fmt.Println(err)
+ log.Println(err)
http.Redirect(res, req, req.URL.String(), http.StatusFound)
return
}
@@ -220,7 +462,7 @@ func loginHandler(res http.ResponseWriter, req *http.Request) {
// check email & password
j, err := db.User(strings.ToLower(req.FormValue("email")))
if err != nil {
- fmt.Println(err)
+ log.Println(err)
http.Redirect(res, req, req.URL.String(), http.StatusFound)
return
}
@@ -233,7 +475,7 @@ func loginHandler(res http.ResponseWriter, req *http.Request) {
usr := &user.User{}
err = json.Unmarshal(j, usr)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
http.Redirect(res, req, req.URL.String(), http.StatusFound)
return
}
@@ -250,7 +492,7 @@ func loginHandler(res http.ResponseWriter, req *http.Request) {
}
token, err := jwt.New(claims)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
http.Redirect(res, req, req.URL.String(), http.StatusFound)
return
}
@@ -260,6 +502,7 @@ func loginHandler(res http.ResponseWriter, req *http.Request) {
Name: "_token",
Value: token,
Expires: week,
+ Path: "/",
})
http.Redirect(res, req, strings.TrimSuffix(req.URL.String(), "/login"), http.StatusFound)
@@ -271,6 +514,7 @@ func logoutHandler(res http.ResponseWriter, req *http.Request) {
Name: "_token",
Expires: time.Unix(0, 0),
Value: "",
+ Path: "/",
})
http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin/login", http.StatusFound)
@@ -291,10 +535,25 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
}
order := strings.ToLower(q.Get("order"))
+ status := q.Get("status")
posts := db.ContentAll(t + "_sorted")
b := &bytes.Buffer{}
- p, ok := content.Types[t]().(editor.Editable)
+
+ if _, ok := content.Types[t]; !ok {
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error405()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ pt := content.Types[t]()
+
+ p, ok := pt.(editor.Editable)
if !ok {
res.WriteHeader(http.StatusInternalServerError)
errView, err := Error500()
@@ -306,6 +565,12 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
return
}
+ var hasExt bool
+ _, ok = pt.(api.Externalable)
+ if ok {
+ hasExt = true
+ }
+
html := `<div class="col s9 card">
<div class="card-content">
<div class="row">
@@ -321,21 +586,6 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
</div>
<script>
$(function() {
- var getParam = function(param) {
- var qs = window.location.search.substring(1);
- var qp = qs.split('&');
- var t = '';
-
- for (var i = 0; i < qp.length; i++) {
- var p = qp[i].split('=')
- if (p[0] === param) {
- t = p[1];
- }
- }
-
- return t;
- }
-
var sort = $('select.__ponzu.sort-order');
sort.on('change', function() {
@@ -363,39 +613,107 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
<input type="hidden" name="type" value="` + t + `" />
</div>
</form>
- </div>
- <ul class="posts row">`
+ </div>`
+ if hasExt {
+ if status == "" {
+ q.Add("status", "public")
+ }
- if order == "desc" || order == "" {
- // keep natural order of posts slice, as returned from sorted bucket
- for i := range posts {
- err := json.Unmarshal(posts[i], &p)
- if err != nil {
- log.Println("Error unmarshal json into", t, err, posts[i])
+ q.Set("status", "public")
+ publicURL := req.URL.Path + "?" + q.Encode()
+
+ q.Set("status", "pending")
+ pendingURL := req.URL.Path + "?" + q.Encode()
+
+ switch status {
+ case "public", "":
+ html += `<div class="row externalable">
+ <span class="description">Status:</span>
+ <span class="active">Public</span>
+ &nbsp;&vert;&nbsp;
+ <a href="` + pendingURL + `">Pending</a>
+ </div>`
+
+ case "pending":
+ // get _pending posts of type t from the db
+ posts = db.ContentAll(t + "_pending")
+
+ html += `<div class="row externalable">
+ <span class="description">Status:</span>
+ <a href="` + publicURL + `">Public</a>
+ &nbsp;&vert;&nbsp;
+ <span class="active">Pending</span>
+ </div>`
+ }
+
+ }
+ html += `<ul class="posts row">`
+
+ switch order {
+ case "desc", "":
+ if hasExt {
+ // reverse the order of posts slice
+ for i := len(posts) - 1; i >= 0; i-- {
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal json into", t, err, posts[i])
+
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ b.Write([]byte(post))
+ continue
+ }
- post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
- b.Write([]byte(post))
- continue
+ post := adminPostListItem(p, t, status)
+ b.Write(post)
}
+ } else {
+ // keep natural order of posts slice, as returned from sorted bucket
+ for i := range posts {
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal json into", t, err, posts[i])
+
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ b.Write([]byte(post))
+ continue
+ }
- post := adminPostListItem(p, t)
- b.Write(post)
+ post := adminPostListItem(p, t, status)
+ b.Write(post)
+ }
}
- } else if order == "asc" {
- // reverse the order of posts slice
- for i := len(posts) - 1; i >= 0; i-- {
- err := json.Unmarshal(posts[i], &p)
- if err != nil {
- log.Println("Error unmarshal json into", t, err, posts[i])
+ case "asc":
+ if hasExt {
+ // keep natural order of posts slice, as returned from sorted bucket
+ for i := range posts {
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal json into", t, err, posts[i])
+
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ b.Write([]byte(post))
+ continue
+ }
- post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
- b.Write([]byte(post))
- continue
+ post := adminPostListItem(p, t, status)
+ b.Write(post)
}
+ } else {
+ // reverse the order of posts slice
+ for i := len(posts) - 1; i >= 0; i-- {
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal json into", t, err, posts[i])
- post := adminPostListItem(p, t)
- b.Write(post)
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ b.Write([]byte(post))
+ continue
+ }
+
+ post := adminPostListItem(p, t, status)
+ b.Write(post)
+ }
}
}
@@ -419,7 +737,7 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
adminView, err := Admin([]byte(html))
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -430,7 +748,8 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
// adminPostListItem is a helper to create the li containing a post.
// p is the asserted post as an Editable, t is the Type of the post.
-func adminPostListItem(p editor.Editable, t string) []byte {
+// specifier is passed to append a name to a namespace like _pending
+func adminPostListItem(p editor.Editable, t, status string) []byte {
s, ok := p.(editor.Sortable)
if !ok {
log.Println("Content type", t, "doesn't implement editor.Sortable")
@@ -446,28 +765,127 @@ func adminPostListItem(p editor.Editable, t string) []byte {
cid := fmt.Sprintf("%d", p.ContentID())
+ if status == "public" {
+ status = ""
+ } else {
+ status = "_" + status
+ }
+
post := `
<li class="col s12">
- <a href="/admin/edit?type=` + t + `&id=` + cid + `">` + p.ContentName() + `</a>
+ <a href="/admin/edit?type=` + t + `&status=` + strings.TrimPrefix(status, "_") + `&id=` + cid + `">` + p.ContentName() + `</a>
<span class="post-detail">Updated: ` + updatedTime + `</span>
<span class="publish-date right">` + publishTime + `</span>
<form enctype="multipart/form-data" class="quick-delete-post __ponzu right" action="/admin/edit/delete" method="post">
<span>Delete</span>
<input type="hidden" name="id" value="` + cid + `" />
- <input type="hidden" name="type" value="` + t + `" />
+ <input type="hidden" name="type" value="` + t + status + `" />
</form>
</li>`
return []byte(post)
}
+func approvePostHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ errView, err := Error405()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+ if err != nil {
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ t := req.FormValue("type")
+ if strings.Contains(t, "_") {
+ t = strings.Split(t, "_")[0]
+ }
+
+ post := content.Types[t]()
+
+ // check if we have a Mergeable
+ m, ok := post.(api.Mergeable)
+ if !ok {
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error400()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ dec := schema.NewDecoder()
+ dec.IgnoreUnknownKeys(true)
+ dec.SetAliasTag("json")
+ err = dec.Decode(post, req.Form)
+ if err != nil {
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ // call its Approve method
+ err = m.Approve(req)
+ if err != nil {
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ // Store the content in the bucket t
+ id, err := db.SetContent(t+":-1", req.Form)
+ if err != nil {
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ // redirect to the new approved content's editor
+ redir := req.URL.Scheme + req.URL.Host + strings.TrimSuffix(req.URL.Path, "/approve")
+ redir += fmt.Sprintf("?type=%s&id=%d", t, id)
+ http.Redirect(res, req, redir, http.StatusFound)
+}
+
func editHandler(res http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
q := req.URL.Query()
i := q.Get("id")
t := q.Get("type")
+ status := q.Get("status")
+
contentType, ok := content.Types[t]
if !ok {
fmt.Fprintf(res, content.ErrTypeNotRegistered, t)
@@ -476,9 +894,13 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
post := contentType()
if i != "" {
+ if status == "pending" {
+ t = t + "_pending"
+ }
+
data, err := db.Content(t + ":" + i)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
errView, err := Error500()
if err != nil {
@@ -490,6 +912,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
}
if len(data) < 1 || data == nil {
+ fmt.Println(string(data))
res.WriteHeader(http.StatusNotFound)
errView, err := Error404()
if err != nil {
@@ -502,7 +925,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
err = json.Unmarshal(data, post)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
errView, err := Error500()
if err != nil {
@@ -518,7 +941,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
m, err := manager.Manage(post.(editor.Editable), t)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
errView, err := Error500()
if err != nil {
@@ -531,7 +954,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
adminView, err := Admin(m)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -542,7 +965,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
case http.MethodPost:
err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusBadRequest)
errView, err := Error405()
if err != nil {
@@ -568,9 +991,9 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
req.PostForm.Set("updated", ts)
}
- urlPaths, err := storeFileUploads(req)
+ urlPaths, err := upload.StoreFiles(req)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
errView, err := Error500()
if err != nil {
@@ -608,7 +1031,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
id, err := db.SetContent(t+":"+cid, req.PostForm)
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
errView, err := Error500()
if err != nil {
@@ -639,7 +1062,7 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
if err != nil {
- fmt.Println("req.ParseMPF")
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -654,11 +1077,17 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
err = db.DeleteContent(t + ":" + id)
if err != nil {
- fmt.Println("db.DeleteContent")
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
+ // catch specifier suffix from delete form value
+ if strings.Contains(t, "_") {
+ spec := strings.Split(t, "_")
+ t = spec[0]
+ }
+
redir := strings.TrimSuffix(req.URL.Scheme+req.URL.Host+req.URL.Path, "/edit/delete")
redir = redir + "/posts?type=" + t
http.Redirect(res, req, redir, http.StatusFound)
@@ -670,9 +1099,9 @@ func editUploadHandler(res http.ResponseWriter, req *http.Request) {
return
}
- urlPaths, err := storeFileUploads(req)
+ urlPaths, err := upload.StoreFiles(req)
if err != nil {
- fmt.Println("Couldn't store file uploads.", err)
+ log.Println("Couldn't store file uploads.", err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -726,7 +1155,7 @@ func searchHandler(res http.ResponseWriter, req *http.Request) {
continue
}
- post := adminPostListItem(p, t)
+ post := adminPostListItem(p, t, "")
b.Write([]byte(post))
}
@@ -737,7 +1166,7 @@ func searchHandler(res http.ResponseWriter, req *http.Request) {
adminView, err := Admin([]byte(html))
if err != nil {
- fmt.Println(err)
+ log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
return
}
diff --git a/system/admin/server.go b/system/admin/server.go
index b3b128d..5d93d84 100644
--- a/system/admin/server.go
+++ b/system/admin/server.go
@@ -20,12 +20,15 @@ func Run() {
http.HandleFunc("/admin/configure", user.Auth(configHandler))
http.HandleFunc("/admin/configure/users", user.Auth(configUsersHandler))
+ http.HandleFunc("/admin/configure/users/edit", user.Auth(configUsersEditHandler))
+ http.HandleFunc("/admin/configure/users/delete", user.Auth(configUsersDeleteHandler))
http.HandleFunc("/admin/posts", user.Auth(postsHandler))
http.HandleFunc("/admin/posts/search", user.Auth(searchHandler))
http.HandleFunc("/admin/edit", user.Auth(editHandler))
http.HandleFunc("/admin/edit/delete", user.Auth(deleteHandler))
+ http.HandleFunc("/admin/edit/approve", user.Auth(approvePostHandler))
http.HandleFunc("/admin/edit/upload", user.Auth(editUploadHandler))
pwd, err := os.Getwd()
diff --git a/system/admin/static/common/js/util.js b/system/admin/static/common/js/util.js
index 7f4c8ab..8d5e74b 100644
--- a/system/admin/static/common/js/util.js
+++ b/system/admin/static/common/js/util.js
@@ -67,4 +67,20 @@ function getPartialDate(unix) {
d.dd = day;
return d;
+}
+
+// Returns a part of the window URL 'search' string
+function getParam(param) {
+ var qs = window.location.search.substring(1);
+ var qp = qs.split('&');
+ var t = '';
+
+ for (var i = 0; i < qp.length; i++) {
+ var p = qp[i].split('=')
+ if (p[0] === param) {
+ t = p[1];
+ }
+ }
+
+ return t;
} \ No newline at end of file
diff --git a/system/admin/static/dashboard/css/admin.css b/system/admin/static/dashboard/css/admin.css
index e23658e..3cffc5d 100644
--- a/system/admin/static/dashboard/css/admin.css
+++ b/system/admin/static/dashboard/css/admin.css
@@ -44,7 +44,7 @@
padding: 0px !important;
}
-ul.posts li {
+ul.posts li, ul.users li {
display: block;
margin: 0 0 20px 0;
padding: 0 0 20px 0 !important;
@@ -164,15 +164,15 @@ span.post-detail {
font-style: italic;
}
-.quick-delete-post {
+.quick-delete-post, .delete-user {
display: none;
}
-li:hover .quick-delete-post {
+li:hover .quick-delete-post, li:hover .delete-user {
display: inline-block;
}
-.quick-delete-post span {
+.quick-delete-post span, .delete-user span {
cursor: pointer;
color: #F44336;
text-transform: uppercase;
@@ -181,6 +181,10 @@ li:hover .quick-delete-post {
margin-right: 20px;
}
+.user-management {
+ padding: 20px;
+}
+
/* OVERRIDE Bootstrap + Materialize conflicts */
.iso-texteditor.input-field label {
@@ -199,3 +203,17 @@ li:hover .quick-delete-post {
-o-transform: translateY(-140%);
transform: translateY(-140%);
}
+
+.chips {
+ margin-top: 10px;
+}
+
+.external.post-controls .col.input-field {
+ margin-top: 40px;
+ padding: 0;
+}
+
+.approve-details {
+ text-align: right;
+ padding: 10px 0 !important;
+} \ No newline at end of file
diff --git a/system/admin/upload.go b/system/admin/upload/upload.go
index 7f2a4fa..169bffe 100644
--- a/system/admin/upload.go
+++ b/system/admin/upload/upload.go
@@ -1,4 +1,4 @@
-package admin
+package upload
import (
"fmt"
@@ -10,7 +10,8 @@ import (
"time"
)
-func storeFileUploads(req *http.Request) (map[string]string, error) {
+// StoreFiles stores file uploads at paths like /YYYY/MM/filename.ext
+func StoreFiles(req *http.Request) (map[string]string, error) {
err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
if err != nil {
return nil, fmt.Errorf("%s", err)
diff --git a/system/api/analytics/init.go b/system/api/analytics/init.go
new file mode 100644
index 0000000..c351bed
--- /dev/null
+++ b/system/api/analytics/init.go
@@ -0,0 +1,101 @@
+// Package analytics provides the methods to run an analytics reporting system
+// for API requests which may be useful to users for measuring access and
+// possibly identifying bad actors abusing requests.
+package analytics
+
+import (
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/boltdb/bolt"
+)
+
+type apiRequest struct {
+ URL string `json:"url"`
+ Method string `json:"http_method"`
+ RemoteAddr string `json:"ip_address"`
+ Timestamp int64 `json:"timestamp"`
+ External bool `json:"external"`
+}
+
+var (
+ store *bolt.DB
+ recordChan chan apiRequest
+)
+
+// Record queues an apiRequest for metrics
+func Record(req *http.Request) {
+ external := strings.Contains(req.URL.Path, "/external/")
+
+ r := apiRequest{
+ URL: req.URL.String(),
+ Method: req.Method,
+ RemoteAddr: req.RemoteAddr,
+ Timestamp: time.Now().Unix() * 1000,
+ External: external,
+ }
+
+ // put r on buffered recordChan to take advantage of batch insertion in DB
+ recordChan <- r
+
+}
+
+// Close exports the abillity to close our db file. Should be called with defer
+// after call to Init() from the same place.
+func Close() {
+ err := store.Close()
+ if err != nil {
+ log.Println(err)
+ }
+}
+
+// Init creates a db connection, should run an initial prune of old data, and
+// sets up the queue/batching channel
+func Init() {
+ var err error
+ store, err = bolt.Open("analytics.db", 0666, nil)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ recordChan = make(chan apiRequest, 1024*128)
+
+ go serve()
+
+ err = store.Update(func(tx *bolt.Tx) error {
+
+ return nil
+ })
+ if err != nil {
+ log.Fatalln(err)
+ }
+}
+
+func serve() {
+ // make timer to notify select to batch request insert from recordChan
+ // interval: 1 minute
+ apiRequestTimer := time.NewTicker(time.Minute * 1)
+
+ // make timer to notify select to remove old analytics
+ // interval: 2 weeks
+ // TODO: enable analytics backup service to cloud
+ pruneDBTimer := time.NewTicker(time.Hour * 24 * 14)
+
+ for {
+ select {
+ case <-apiRequestTimer.C:
+ var reqs []apiRequest
+ batchSize := len(recordChan)
+
+ for i := 0; i < batchSize; i++ {
+ reqs = append(reqs, <-recordChan)
+ }
+
+ case <-pruneDBTimer.C:
+
+ default:
+ }
+ }
+}
diff --git a/system/api/external.go b/system/api/external.go
new file mode 100644
index 0000000..4a42eb5
--- /dev/null
+++ b/system/api/external.go
@@ -0,0 +1,109 @@
+package api
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/bosssauce/ponzu/content"
+ "github.com/bosssauce/ponzu/system/admin/upload"
+ "github.com/bosssauce/ponzu/system/db"
+)
+
+// Externalable accepts or rejects external POST requests to endpoints such as:
+// /external/posts?type=Review
+type Externalable interface {
+ // Accepts determines whether a type will allow external submissions
+ Accepts() bool
+}
+
+// Mergeable allows external post content to be approved and published through
+// the public-facing API
+type Mergeable interface {
+ // Approve copies an external post to the internal collection and triggers
+ // a re-sort of its content type posts
+ Approve(req *http.Request) error
+}
+
+func externalPostsHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+ if err != nil {
+ log.Println("[External] error:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ t := req.URL.Query().Get("type")
+ if t == "" {
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ p, found := content.Types[t]
+ if !found {
+ log.Println("[External] attempt to submit unknown type:", t, "from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ post := p()
+
+ ext, ok := post.(Externalable)
+ if !ok {
+ log.Println("[External] rejected non-externalable type:", t, "from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ if ext.Accepts() {
+ ts := fmt.Sprintf("%d", time.Now().Unix()*1000)
+ req.PostForm.Set("timestamp", ts)
+ req.PostForm.Set("updated", ts)
+
+ urlPaths, err := upload.StoreFiles(req)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ for name, urlPath := range urlPaths {
+ req.PostForm.Add(name, urlPath)
+ }
+
+ // check for any multi-value fields (ex. checkbox fields)
+ // and correctly format for db storage. Essentially, we need
+ // fieldX.0: value1, fieldX.1: value2 => fieldX: []string{value1, value2}
+ var discardKeys []string
+ for k, v := range req.PostForm {
+ if strings.Contains(k, ".") {
+ key := strings.Split(k, ".")[0]
+
+ if req.PostForm.Get(key) == "" {
+ req.PostForm.Set(key, v[0])
+ discardKeys = append(discardKeys, k)
+ } else {
+ req.PostForm.Add(key, v[0])
+ }
+ }
+ }
+
+ for _, discardKey := range discardKeys {
+ req.PostForm.Del(discardKey)
+ }
+
+ _, err = db.SetContent(t+"_pending:-1", req.PostForm)
+ if err != nil {
+ log.Println("[External] error:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ }
+}
diff --git a/system/api/handlers.go b/system/api/handlers.go
index 0c9139f..8356683 100644
--- a/system/api/handlers.go
+++ b/system/api/handlers.go
@@ -5,8 +5,11 @@ import (
"encoding/json"
"log"
"net/http"
+ "strconv"
+ "strings"
"github.com/bosssauce/ponzu/content"
+ "github.com/bosssauce/ponzu/system/api/analytics"
"github.com/bosssauce/ponzu/system/db"
)
@@ -28,24 +31,72 @@ func typesHandler(res http.ResponseWriter, req *http.Request) {
func postsHandler(res http.ResponseWriter, req *http.Request) {
q := req.URL.Query()
t := q.Get("type")
- // TODO: implement pagination
- // num := q.Get("num")
- // page := q.Get("page")
-
- // TODO: inplement time-based ?after=time.Time, ?before=time.Time between=time.Time|time.Time
-
if t == "" {
res.WriteHeader(http.StatusBadRequest)
return
}
- posts := db.ContentAll(t)
+ count, err := strconv.Atoi(q.Get("count")) // int: determines number of posts to return (10 default, -1 is all)
+ if err != nil {
+ if q.Get("count") == "" {
+ count = 10
+ } else {
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ }
+
+ offset, err := strconv.Atoi(q.Get("offset")) // int: multiplier of count for pagination (0 default)
+ if err != nil {
+ if q.Get("offset") == "" {
+ offset = 0
+ } else {
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ }
+
+ order := strings.ToLower(q.Get("order")) // string: sort order of posts by timestamp ASC / DESC (DESC default)
+ if order != "asc" || order == "" {
+ order = "desc"
+ }
+
+ // TODO: time-based ?after=time.Time, ?before=time.Time between=time.Time|time.Time
+
+ posts := db.ContentAll(t + "_sorted")
var all = []json.RawMessage{}
for _, post := range posts {
all = append(all, post)
}
- j, err := fmtJSON(all...)
+ var start, end int
+ switch count {
+ case -1:
+ start = 0
+ end = len(posts)
+
+ default:
+ start = count * offset
+ end = start + count
+ }
+
+ // bounds check on posts given the start & end count
+ if start > len(posts) {
+ start = len(posts) - count
+ }
+ if end > len(posts) {
+ end = len(posts)
+ }
+
+ // reverse the sorted order if ASC
+ if order == "asc" {
+ all = []json.RawMessage{}
+ for i := len(posts) - 1; i >= 0; i-- {
+ all = append(all, posts[i])
+ }
+ }
+
+ j, err := fmtJSON(all[start:end]...)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
@@ -150,6 +201,7 @@ func SendJSON(res http.ResponseWriter, j map[string]interface{}) {
data, err = json.Marshal(j)
if err != nil {
+ log.Println(err)
data, _ = json.Marshal(map[string]interface{}{
"status": "fail",
"message": err.Error(),
@@ -159,9 +211,6 @@ func SendJSON(res http.ResponseWriter, j map[string]interface{}) {
sendData(res, data, 200)
}
-// ResponseFunc ...
-type ResponseFunc func(http.ResponseWriter, *http.Request)
-
// CORS wraps a HandleFunc to response to OPTIONS requests properly
func CORS(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
@@ -173,3 +222,12 @@ func CORS(next http.HandlerFunc) http.HandlerFunc {
next.ServeHTTP(res, req)
})
}
+
+// Record wraps a HandleFunc to record API requests for analytical purposes
+func Record(next http.HandlerFunc) http.HandlerFunc {
+ return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+ go analytics.Record(req)
+
+ next.ServeHTTP(res, req)
+ })
+}
diff --git a/system/api/server.go b/system/api/server.go
index da73382..816bc21 100644
--- a/system/api/server.go
+++ b/system/api/server.go
@@ -9,4 +9,6 @@ func Run() {
http.HandleFunc("/api/posts", CORS(postsHandler))
http.HandleFunc("/api/post", CORS(postHandler))
+
+ http.HandleFunc("/api/external/posts", CORS(externalPostsHandler))
}
diff --git a/system/db/content.go b/system/db/content.go
index 2d6e8ea..bd1ee4b 100644
--- a/system/db/content.go
+++ b/system/db/content.go
@@ -38,13 +38,20 @@ func SetContent(target string, data url.Values) (int, error) {
}
func update(ns, id string, data url.Values) (int, error) {
+ var specifier string // i.e. _pending, _sorted, etc.
+ if strings.Contains(ns, "_") {
+ spec := strings.Split(ns, "_")
+ ns = spec[0]
+ specifier = "_" + spec[1]
+ }
+
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))
+ b, err := tx.CreateBucketIfNotExists([]byte(ns + specifier))
if err != nil {
return err
}
@@ -65,15 +72,24 @@ func update(ns, id string, data url.Values) (int, error) {
return 0, nil
}
- go SortContent(ns)
+ if specifier == "" {
+ go SortContent(ns)
+ }
return cid, nil
}
func insert(ns string, data url.Values) (int, error) {
var effectedID int
+ var specifier string // i.e. _pending, _sorted, etc.
+ if strings.Contains(ns, "_") {
+ spec := strings.Split(ns, "_")
+ ns = spec[0]
+ specifier = "_" + spec[1]
+ }
+
err := store.Update(func(tx *bolt.Tx) error {
- b, err := tx.CreateBucketIfNotExists([]byte(ns))
+ b, err := tx.CreateBucketIfNotExists([]byte(ns + specifier))
if err != nil {
return err
}
@@ -89,7 +105,7 @@ func insert(ns string, data url.Values) (int, error) {
if err != nil {
return err
}
- data.Add("id", cid)
+ data.Set("id", cid)
j, err := postToJSON(ns, data)
if err != nil {
@@ -107,40 +123,11 @@ func insert(ns string, data url.Values) (int, error) {
return 0, err
}
- go SortContent(ns)
-
- return effectedID, nil
-}
-
-func postToJSON(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
+ if specifier == "" {
+ go SortContent(ns)
}
- return j, nil
+ return effectedID, nil
}
// DeleteContent removes an item from the database. Deleting a non-existent item
@@ -153,7 +140,6 @@ func DeleteContent(target string) error {
tx.Bucket([]byte(ns)).Delete([]byte(id))
return nil
})
-
if err != nil {
return err
}
@@ -178,7 +164,7 @@ func Content(target string) ([]byte, error) {
b := tx.Bucket([]byte(ns))
_, err := val.Write(b.Get([]byte(id)))
if err != nil {
- fmt.Println(err)
+ log.Println(err)
return err
}
@@ -197,8 +183,12 @@ func ContentAll(namespace string) [][]byte {
store.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(namespace))
- len := b.Stats().KeyN
- posts = make([][]byte, 0, len)
+ if b == nil {
+ return nil
+ }
+
+ numKeys := b.Stats().KeyN
+ posts = make([][]byte, 0, numKeys)
b.ForEach(func(k, v []byte) error {
posts = append(posts, v)
@@ -216,10 +206,15 @@ func ContentAll(namespace string) [][]byte {
// in descending order, from most recent to least recent
// Should be called from a goroutine after SetContent is successful
func SortContent(namespace string) {
+ // only sort main content types i.e. Post
+ if strings.Contains(namespace, "_") {
+ return
+ }
+
all := ContentAll(namespace)
var posts sortablePosts
- // decode each (json) into Editable
+ // decode each (json) into type to then sort
for i := range all {
j := all[i]
post := content.Types[namespace]()
@@ -238,18 +233,14 @@ func SortContent(namespace string) {
// store in <namespace>_sorted bucket, first delete existing
err := store.Update(func(tx *bolt.Tx) error {
- err := tx.DeleteBucket([]byte(namespace + "_sorted"))
+ bname := []byte(namespace + "_sorted")
+ err := tx.DeleteBucket(bname)
if err != nil {
return err
}
- b, err := tx.CreateBucket([]byte(namespace + "_sorted"))
+ b, err := tx.CreateBucketIfNotExists(bname)
if err != nil {
- err := tx.Rollback()
- if err != nil {
- return err
- }
-
return err
}
@@ -263,11 +254,6 @@ func SortContent(namespace string) {
cid := fmt.Sprintf("%d:%d", i, posts[i].Time())
err = b.Put([]byte(cid), j)
if err != nil {
- err := tx.Rollback()
- if err != nil {
- return err
- }
-
return err
}
}
@@ -293,3 +279,35 @@ func (s sortablePosts) Less(i, j int) bool {
func (s sortablePosts) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
+
+func postToJSON(ns string, data url.Values) ([]byte, error) {
+ // find the content type and decode values into it
+ ns = strings.TrimSuffix(ns, "_external")
+ 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
+}
diff --git a/system/db/init.go b/system/db/init.go
index 1a5ed25..63804e1 100644
--- a/system/db/init.go
+++ b/system/db/init.go
@@ -13,12 +13,21 @@ import (
var store *bolt.DB
+// Close exports the abillity to close our db file. Should be called with defer
+// after call to Init() from the same place.
+func Close() {
+ err := store.Close()
+ if err != nil {
+ log.Println(err)
+ }
+}
+
// Init creates a db connection, initializes db with required info, sets secrets
func Init() {
var err error
- store, err = bolt.Open("store.db", 0666, nil)
+ store, err = bolt.Open("system.db", 0666, nil)
if err != nil {
- log.Fatal(err)
+ log.Fatalln(err)
}
err = store.Update(func(tx *bolt.Tx) error {
@@ -67,10 +76,9 @@ func Init() {
return nil
})
if err != nil {
- log.Fatal("Coudn't initialize db with buckets.", err)
+ log.Fatalln("Coudn't initialize db with buckets.", err)
}
- // sort all content into type_sorted buckets
go func() {
for t := range content.Types {
SortContent(t)
@@ -99,7 +107,7 @@ func SystemInitComplete() bool {
})
if err != nil {
complete = false
- log.Fatal(err)
+ log.Fatalln(err)
}
return complete
diff --git a/system/db/user.go b/system/db/user.go
index a3a0be3..d2dc3a9 100644
--- a/system/db/user.go
+++ b/system/db/user.go
@@ -4,15 +4,21 @@ import (
"bytes"
"encoding/json"
"errors"
+ "fmt"
+ "net/http"
"github.com/bosssauce/ponzu/system/admin/user"
"github.com/boltdb/bolt"
+ "github.com/nilslice/jwt"
)
// ErrUserExists is used for the db to report to admin user of existing user
var ErrUserExists = errors.New("Error. User exists.")
+// ErrNoUserExists is used for the db to report to admin user of non-existing user
+var ErrNoUserExists = errors.New("Error. No user exists.")
+
// SetUser sets key:value pairs in the db for user settings
func SetUser(usr *user.User) (int, error) {
err := store.Update(func(tx *bolt.Tx) error {
@@ -38,7 +44,7 @@ func SetUser(usr *user.User) (int, error) {
return err
}
- err = users.Put([]byte(usr.Email), j)
+ err = users.Put(email, j)
if err != nil {
return err
}
@@ -52,6 +58,65 @@ func SetUser(usr *user.User) (int, error) {
return usr.ID, nil
}
+// UpdateUser sets key:value pairs in the db for existing user settings
+func UpdateUser(usr, updatedUsr *user.User) error {
+ err := store.Update(func(tx *bolt.Tx) error {
+ users := tx.Bucket([]byte("_users"))
+
+ // check if user is found by email, fail if nil
+ exists := users.Get([]byte(usr.Email))
+ if exists == nil {
+ return ErrNoUserExists
+ }
+
+ // marshal User to json and put into bucket
+ j, err := json.Marshal(updatedUsr)
+ if err != nil {
+ return err
+ }
+
+ err = users.Put([]byte(updatedUsr.Email), j)
+ if err != nil {
+ return err
+ }
+
+ // if email address was changed, delete the old record of former
+ // user with original email address
+ if usr.Email != updatedUsr.Email {
+ err = users.Delete([]byte(usr.Email))
+ if err != nil {
+ return err
+ }
+
+ }
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteUser deletes a user from the db by email
+func DeleteUser(email string) error {
+ err := store.Update(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte("_users"))
+ err := b.Delete([]byte(email))
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
// User gets the user by email from the db
func User(email string) ([]byte, error) {
val := &bytes.Buffer{}
@@ -72,3 +137,50 @@ func User(email string) ([]byte, error) {
return val.Bytes(), nil
}
+
+// UserAll returns all users from the db
+func UserAll() ([][]byte, error) {
+ var users [][]byte
+ err := store.View(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte("_users"))
+ err := b.ForEach(func(k, v []byte) error {
+ users = append(users, v)
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return users, nil
+}
+
+// CurrentUser extracts the user from the request data and returns the current user from the db
+func CurrentUser(req *http.Request) ([]byte, error) {
+ if !user.IsValid(req) {
+ return nil, fmt.Errorf("Error. Invalid User.")
+ }
+
+ token, err := req.Cookie("_token")
+ if err != nil {
+ return nil, err
+ }
+
+ claims := jwt.GetClaims(token.Value)
+ email, ok := claims["user"]
+ if !ok {
+ return nil, fmt.Errorf("Error. No user data found in request token.")
+ }
+
+ usr, err := User(email.(string))
+ if err != nil {
+ return nil, err
+ }
+
+ return usr, nil
+}
diff --git a/system/tls/enable.go b/system/tls/enable.go
new file mode 100644
index 0000000..c53fac6
--- /dev/null
+++ b/system/tls/enable.go
@@ -0,0 +1,79 @@
+package tls
+
+import (
+ "crypto/tls"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/bosssauce/ponzu/system/db"
+
+ "golang.org/x/crypto/acme/autocert"
+)
+
+var m autocert.Manager
+
+// setup attempts to locate or create the cert cache directory and the certs for TLS encryption
+func setup() {
+ pwd, err := os.Getwd()
+ if err != nil {
+ log.Fatalln("Couldn't find working directory to locate or save certificates.")
+ }
+
+ cache := autocert.DirCache(filepath.Join(pwd, "system", "tls", "certs"))
+ if _, err := os.Stat(string(cache)); os.IsNotExist(err) {
+ err := os.MkdirAll(string(cache), os.ModePerm|os.ModeDir)
+ if err != nil {
+ log.Fatalln("Couldn't create cert directory at", cache)
+ }
+ }
+
+ // get host/domain and email from Config to use for TLS request to Let's encryption.
+ // we will fail fatally if either are not found since Let's Encrypt will rate-limit
+ // and sending incomplete requests is wasteful and guarenteed to fail its check
+ host, err := db.Config("domain")
+ if err != nil {
+ log.Fatalln("Error identifying host/domain during TLS set-up.", err)
+ }
+
+ if host == nil {
+ log.Fatalln("No 'domain' field set in Configuration. Please add a domain before attempting to make certificates.")
+ }
+ fmt.Println("Using", host, "as host/domain for certificate...")
+ fmt.Println("NOTE: if the host/domain is not configured properly or is unreachable, HTTPS set-up will fail.")
+
+ email, err := db.Config("admin_email")
+ if err != nil {
+ log.Fatalln("Error identifying admin email during TLS set-up.", err)
+ }
+
+ if email == nil {
+ log.Fatalln("No 'admin_email' field set in Configuration. Please add an admin email before attempting to make certificates.")
+ }
+ fmt.Println("Using", email, "as contact email for certificate...")
+
+ m = autocert.Manager{
+ Prompt: autocert.AcceptTOS,
+ Cache: cache,
+ HostPolicy: autocert.HostWhitelist(string(host)),
+ RenewBefore: time.Hour * 24 * 30,
+ Email: string(email),
+ }
+
+}
+
+// Enable runs the setup for creating or locating certificates and starts the TLS server
+func Enable() {
+ setup()
+
+ server := &http.Server{
+ Addr: ":443",
+ TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
+ }
+
+ go log.Fatalln(server.ListenAndServeTLS("", ""))
+ fmt.Println("Server listening for HTTPS requests...")
+}