diff options
author | Steve <nilslice@gmail.com> | 2016-12-06 15:24:36 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-12-06 15:24:36 -0800 |
commit | f39c1519ab382a343c05163f00f38c83bff3583d (patch) | |
tree | 254f75834f2cb787179f7880b0063e667d8ad234 /system/admin | |
parent | 5527117e706114c1188afaa10188d96170874047 (diff) | |
parent | 64050ef8065bccdef0aab1748040995c637fe9ed (diff) |
Merge pull request #19 from bosssauce/ponzu-dev
[core] Added account recovery process and content pagination in admin UI
Diffstat (limited to 'system/admin')
-rw-r--r-- | system/admin/admin.go | 9 | ||||
-rw-r--r-- | system/admin/config/config.go | 4 | ||||
-rw-r--r-- | system/admin/handlers.go | 269 | ||||
-rw-r--r-- | system/admin/server.go | 6 |
4 files changed, 197 insertions, 91 deletions
diff --git a/system/admin/admin.go b/system/admin/admin.go index e0689b3..9ddff84 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -57,7 +57,7 @@ var mainAdminHTML = ` {{ range $t, $f := .Types }} <div class="row collection-item"> - <li><a class="col s12" href="/admin/posts?type={{ $t }}"><i class="tiny left material-icons">playlist_add</i>{{ $t }}</a></li> + <li><a class="col s12" href="/admin/contents?type={{ $t }}"><i class="tiny left material-icons">playlist_add</i>{{ $t }}</a></li> </div> {{ end }} @@ -205,7 +205,7 @@ var loginAdminHTML = ` </div> <div class="input-field col s12"> <input placeholder="Enter your password" class="validate required" type="password" id="password" name="password"/> - <a href="/admin/recover" class="right">Forgot password?</a> + <a href="/admin/recover">Forgot password?</a> <label for="password" class="active">Password</label> </div> <button class="btn waves-effect waves-light right">Log in</button> @@ -253,12 +253,13 @@ var forgotPasswordHTML = ` <div class="card-content"> <div class="card-title">Account Recovery</div> <blockquote>Please enter the email for your account and a recovery message will be sent to you at this address. Check your spam folder in case the message was flagged.</blockquote> - <form method="post" action="/admin/recover" class="row"> + <form method="post" action="/admin/recover" class="row" enctype="multipart/form-data"> <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> + <a href="/admin/recover/key">Already have a recovery key?</a> <button class="btn waves-effect waves-light right">Send Recovery Email</button> </form> </div> @@ -304,7 +305,7 @@ var recoveryKeyHTML = ` <div class="card-content"> <div class="card-title">Account Recovery</div> <blockquote>Please check for your recovery key inside an email sent to the address you provided. Check your spam folder in case the message was flagged.</blockquote> - <form method="post" action="/admin/recover/key" class="row"> + <form method="post" action="/admin/recover/key" class="row" enctype="multipart/form-data"> <div class="input-field col s12"> <input placeholder="Enter your recovery key" class="validate required" type="text" id="key" name="key"/> <label for="key" class="active">Recovery Key</label> diff --git a/system/admin/config/config.go b/system/admin/config/config.go index 0a7103e..b898b49 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -18,8 +18,8 @@ type Config struct { CacheInvalidate []string `json:"cache"` } -// ContentName partially implements editor.Editable -func (c *Config) ContentName() string { return c.Name } +// String partially implements content.Identifiable and overrides Item's String() +func (c *Config) String() string { return c.Name } // Editor partially implements editor.Editable func (c *Config) Editor() *editor.Editor { return &c.editor } diff --git a/system/admin/handlers.go b/system/admin/handlers.go index 7b8bfae..c91db79 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -556,35 +556,20 @@ func forgotPasswordHandler(res http.ResponseWriter, req *http.Request) { email := strings.ToLower(req.FormValue("email")) if email == "" { res.WriteHeader(http.StatusBadRequest) - errView, err := Error400() - if err != nil { - return - } - - res.Write(errView) + log.Println("Failed account recovery. No email address submitted.") return } _, err = db.User(email) if err == db.ErrNoUserExists { res.WriteHeader(http.StatusBadRequest) - errView, err := Error400() - if err != nil { - return - } - - res.Write(errView) + log.Println("No user exists.", err) return } if err != db.ErrNoUserExists && err != nil { res.WriteHeader(http.StatusInternalServerError) - errView, err := Error500() - if err != nil { - return - } - - res.Write(errView) + log.Println("Error:", err) return } @@ -592,54 +577,48 @@ func forgotPasswordHandler(res http.ResponseWriter, req *http.Request) { key, err := db.SetRecoveryKey(email) if err != nil { res.WriteHeader(http.StatusInternalServerError) - errView, err := Error500() - if err != nil { - return - } + log.Println("Failed to set account recovery key.", err) + return + } - res.Write(errView) + domain, err := db.Config("domain") + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + log.Println("Failed to get domain from configuration.", err) return } - domain := db.ConfigCache("domain") body := fmt.Sprintf(` - There has been an account recovery request made for the user with email: - %s +There has been an account recovery request made for the user with email: +%s + +To recover your account, please go to http://%s/admin/recover/key and enter +this email address along with the following secret key: - To recover your account, please go to http://%s/admin/recover/key and enter - this email address along with the following secret key: - - %s +%s - If you did not make the request, ignore this message and your password - will remain as-is. +If you did not make the request, ignore this message and your password +will remain as-is. - Thank you, - Ponzu CMS at %s +Thank you, +Ponzu CMS at %s + +`, email, domain, key, domain) - `, email, domain, key, domain) msg := emailer.Message{ To: email, - From: fmt.Sprintf("Ponzu CMS <ponzu-cms@%s", domain), + From: fmt.Sprintf("ponzu@%s", domain), Subject: fmt.Sprintf("Account Recovery [%s]", domain), Body: body, } - /* + go func() { err = msg.Send() if err != nil { - res.WriteHeader(http.StatusInternalServerError) - errView, err := Error500() - if err != nil { - return - } - - res.Write(errView) - return + log.Println("Failed to send message to:", msg.To, "about", msg.Subject, "Error:", err) } - */ - fmt.Println(msg) + }() // redirect to /admin/recover/key and send email with key and URL http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin/recover/key", http.StatusFound) @@ -662,33 +641,90 @@ func recoveryKeyHandler(res http.ResponseWriter, req *http.Request) { view, err := RecoveryKey() if err != nil { res.WriteHeader(http.StatusInternalServerError) - errView, err := Error500() - if err != nil { - return - } - - res.Write(errView) return } res.Write(view) case http.MethodPost: + err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB + if err != nil { + log.Println("Error parsing recovery key form:", err) + + res.WriteHeader(http.StatusInternalServerError) + res.Write([]byte("Error, please go back and try again.")) + return + } // check for email & key match + email := strings.ToLower(req.FormValue("email")) + key := req.FormValue("key") + + var actual string + if actual, err = db.RecoveryKey(email); err != nil || actual == "" { + log.Println("Error getting recovery key from database:", err) + + res.WriteHeader(http.StatusInternalServerError) + res.Write([]byte("Error, please go back and try again.")) + return + } + + if key != actual { + log.Println("Bad recovery key submitted:", key) + log.Println("Actual:", actual) + + res.WriteHeader(http.StatusBadRequest) + res.Write([]byte("Error, please go back and try again.")) + return + } // set user with new password + password := req.FormValue("password") + usr := &user.User{} + u, err := db.User(email) + if err != nil { + log.Println("Error finding user by email:", email, err) - // redirect to /admin/login + res.WriteHeader(http.StatusInternalServerError) + res.Write([]byte("Error, please go back and try again.")) + return + } - default: - res.WriteHeader(http.StatusMethodNotAllowed) - errView, err := Error405() + if u == nil { + log.Println("No user found with email:", email) + + res.WriteHeader(http.StatusBadRequest) + res.Write([]byte("Error, please go back and try again.")) + return + } + + err = json.Unmarshal(u, usr) if err != nil { + log.Println("Error decoding user from database:", err) + + res.WriteHeader(http.StatusInternalServerError) + res.Write([]byte("Error, please go back and try again.")) return } - res.Write(errView) + update := user.NewUser(email, password) + update.ID = usr.ID + + err = db.UpdateUser(usr, update) + if err != nil { + log.Println("Error updating user:", err) + + res.WriteHeader(http.StatusInternalServerError) + res.Write([]byte("Error, please go back and try again.")) + return + } + + // redirect to /admin/login + redir := req.URL.Scheme + req.URL.Host + "/admin/login" + http.Redirect(res, req, redir, http.StatusFound) + + default: + res.WriteHeader(http.StatusMethodNotAllowed) return } } @@ -697,7 +733,7 @@ func recoveryEditHandler(res http.ResponseWriter, req *http.Request) { } -func postsHandler(res http.ResponseWriter, req *http.Request) { +func contentsHandler(res http.ResponseWriter, req *http.Request) { q := req.URL.Query() t := q.Get("type") if t == "" { @@ -787,7 +823,7 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { Order: order, } - posts := db.Query(t+"_sorted", opts) + total, posts := db.Query(t+"__sorted", opts) b := &bytes.Buffer{} html := `<div class="col s9 card"> @@ -824,20 +860,25 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { </script> </div> </div> - <form class="col s4" action="/admin/posts/search" method="get"> + <form class="col s4" action="/admin/contents/search" method="get"> <div class="input-field post-search inline"> <label class="active">Search:</label> <i class="right material-icons search-icon">search</i> <input class="search" name="q" type="text" placeholder="Within all ` + t + ` fields" class="search"/> <input type="hidden" name="type" value="` + t + `" /> + <input type="hidden" name="status" value="` + status + `" /> </div> </form> </div>` if hasExt { if status == "" { - q.Add("status", "public") + q.Set("status", "public") } + // always start from top of results when changing public/pending + q.Del("count") + q.Del("offset") + q.Set("status", "public") publicURL := req.URL.Path + "?" + q.Encode() @@ -868,8 +909,8 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { } case "pending": - // get _pending posts of type t from the db - posts = db.Query(t+"_pending", opts) + // get __pending posts of type t from the db + _, posts = db.Query(t+"__pending", opts) html += `<div class="row externalable"> <span class="description">Status:</span> @@ -911,7 +952,56 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { html += `<ul class="posts row">` - b.Write([]byte(`</ul></div></div>`)) + b.Write([]byte(`</ul>`)) + + statusDisabled := "disabled" + prevStatus := "" + nextStatus := "" + // total may be less than 10 (default count), so reset count to match total + if total < count { + count = total + } + // nothing previous to current list + if offset == 0 { + prevStatus = statusDisabled + } + // nothing after current list + if (offset+1)*count >= total { + nextStatus = statusDisabled + } + + // set up pagination values + urlFmt := req.URL.Path + "?count=%d&offset=%d&&order=%s&status=%s&type=%s" + prevURL := fmt.Sprintf(urlFmt, count, offset-1, order, status, t) + nextURL := fmt.Sprintf(urlFmt, count, offset+1, order, status, t) + start := 1 + count*offset + end := start + count - 1 + + if total < end { + end = total + } + + pagination := fmt.Sprintf(` + <ul class="pagination row"> + <li class="col s2 waves-effect %s"><a href="%s"><i class="material-icons">chevron_left</i></a></li> + <li class="col s8">%d to %d of %d</li> + <li class="col s2 waves-effect %s"><a href="%s"><i class="material-icons">chevron_right</i></a></li> + </ul> + `, prevStatus, prevURL, start, end, total, nextStatus, nextURL) + + // show indicator that a collection of items will be listed implicitly, but + // that none are created yet + if total < 1 { + pagination = ` + <ul class="pagination row"> + <li class="col s2 waves-effect disabled"><a href="#"><i class="material-icons">chevron_left</i></a></li> + <li class="col s8">0 to 0 of 0</li> + <li class="col s2 waves-effect disabled"><a href="#"><i class="material-icons">chevron_right</i></a></li> + </ul> + ` + } + + b.Write([]byte(pagination + `</div></div>`)) script := ` <script> @@ -923,6 +1013,13 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { } }); }); + + // disable link from being clicked if parent is 'disabled' + $(function() { + $('ul.pagination li.disabled a').on('click', function(e) { + e.preventDefault(); + }); + }); </script> ` @@ -942,7 +1039,7 @@ 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. -// specifier is passed to append a name to a namespace like _pending +// specifier is passed to append a name to a namespace like __pending func adminPostListItem(e editor.Editable, typeName, status string) []byte { s, ok := e.(editor.Sortable) if !ok { @@ -970,12 +1067,12 @@ func adminPostListItem(e editor.Editable, typeName, status string) []byte { case "public", "": status = "" default: - status = "_" + status + status = "__" + status } post := ` <li class="col s12"> - <a href="/admin/edit?type=` + typeName + `&status=` + strings.TrimPrefix(status, "_") + `&id=` + cid + `">` + e.ContentName() + `</a> + <a href="/admin/edit?type=` + typeName + `&status=` + strings.TrimPrefix(status, "__") + `&id=` + cid + `">` + i.String() + `</a> <span class="post-detail">Updated: ` + updatedTime + `</span> <span class="publish-date right">` + publishTime + `</span> @@ -989,7 +1086,7 @@ func adminPostListItem(e editor.Editable, typeName, status string) []byte { return []byte(post) } -func approvePostHandler(res http.ResponseWriter, req *http.Request) { +func approveContentHandler(res http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { res.WriteHeader(http.StatusMethodNotAllowed) errView, err := Error405() @@ -1014,8 +1111,8 @@ func approvePostHandler(res http.ResponseWriter, req *http.Request) { } t := req.FormValue("type") - if strings.Contains(t, "_") { - t = strings.Split(t, "_")[0] + if strings.Contains(t, "__") { + t = strings.Split(t, "__")[0] } post := content.Types[t]() @@ -1160,7 +1257,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) { if i != "" { if status == "pending" { - t = t + "_pending" + t = t + "__pending" } data, err := db.Content(t + ":" + i) @@ -1275,7 +1372,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) { } for name, urlPath := range urlPaths { - req.PostForm.Add(name, urlPath) + req.PostForm.Set(name, urlPath) } // check for any multi-value fields (ex. checkbox fields) @@ -1299,8 +1396,8 @@ func editHandler(res http.ResponseWriter, req *http.Request) { req.PostForm.Del(discardKey) } - if strings.Contains(t, "_") { - t = strings.Split(t, "_")[0] + if strings.Contains(t, "__") { + t = strings.Split(t, "__")[0] } p, ok := content.Types[t] @@ -1409,8 +1506,8 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) { } // catch specifier suffix from delete form value - if strings.Contains(t, "_") { - spec := strings.Split(t, "_") + if strings.Contains(t, "__") { + spec := strings.Split(t, "__") ct = spec[0] } @@ -1506,7 +1603,7 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) { } redir := strings.TrimSuffix(req.URL.Scheme+req.URL.Host+req.URL.Path, "/edit/delete") - redir = redir + "/posts?type=" + ct + redir = redir + "/contents?type=" + ct http.Redirect(res, req, redir, http.StatusFound) } @@ -1531,13 +1628,19 @@ func searchHandler(res http.ResponseWriter, req *http.Request) { q := req.URL.Query() t := q.Get("type") search := q.Get("q") + status := q.Get("status") + var specifier string if t == "" || search == "" { http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin", http.StatusFound) return } - posts := db.ContentAll(t) + if status == "pending" { + specifier = "__" + status + } + + posts := db.ContentAll(t + specifier) b := &bytes.Buffer{} p := content.Types[t]().(editor.Editable) @@ -1545,11 +1648,13 @@ func searchHandler(res http.ResponseWriter, req *http.Request) { <div class="card-content"> <div class="row"> <div class="card-title col s7">` + t + ` Results</div> - <form class="col s5" action="/admin/posts/search" method="get"> + <form class="col s4" action="/admin/contents/search" method="get"> <div class="input-field post-search inline"> + <label class="active">Search:</label> <i class="right material-icons search-icon">search</i> - <input class="search" name="q" type="text" placeholder="Search for ` + t + ` content" class="search"/> + <input class="search" name="q" type="text" placeholder="Within all ` + t + ` fields" class="search"/> <input type="hidden" name="type" value="` + t + `" /> + <input type="hidden" name="status" value="` + status + `" /> </div> </form> </div> @@ -1572,7 +1677,7 @@ func searchHandler(res http.ResponseWriter, req *http.Request) { continue } - post := adminPostListItem(p, t, "") + post := adminPostListItem(p, t, status) b.Write([]byte(post)) } diff --git a/system/admin/server.go b/system/admin/server.go index 75b48f6..f80a750 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -27,12 +27,12 @@ func Run() { 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/contents", user.Auth(contentsHandler)) + http.HandleFunc("/admin/contents/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/approve", user.Auth(approveContentHandler)) http.HandleFunc("/admin/edit/upload", user.Auth(editUploadHandler)) pwd, err := os.Getwd() |