diff options
Diffstat (limited to 'system')
-rw-r--r-- | system/admin/admin.go | 115 | ||||
-rw-r--r-- | system/admin/handlers.go | 182 | ||||
-rw-r--r-- | system/admin/server.go | 4 | ||||
-rw-r--r-- | system/db/user.go | 55 |
4 files changed, 352 insertions, 4 deletions
diff --git a/system/admin/admin.go b/system/admin/admin.go index 8c13582..e0689b3 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -205,7 +205,8 @@ var loginAdminHTML = ` </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> + <a href="/admin/recover" class="right">Forgot password?</a> + <label for="password" class="active">Password</label> </div> <button class="btn waves-effect waves-light right">Log in</button> </form> @@ -246,6 +247,118 @@ func Login() ([]byte, error) { return buf.Bytes(), nil } +var forgotPasswordHTML = ` +<div class="init col s5"> +<div class="card"> +<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"> + <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> + + <button class="btn waves-effect waves-light right">Send Recovery Email</button> + </form> +</div> +</div> +</div> +<script> + $(function() { + $('.nav-wrapper ul.right').hide(); + }); +</script> +` + +// ForgotPassword ... +func ForgotPassword() ([]byte, error) { + html := startAdminHTML + forgotPasswordHTML + 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("forgotPassword").Parse(html)) + err = tmpl.Execute(buf, a) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var recoveryKeyHTML = ` +<div class="init col s5"> +<div class="card"> +<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"> + <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> + </div> + + <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">New Password</label> + </div> + + <button class="btn waves-effect waves-light right">Update Account</button> + </form> +</div> +</div> +</div> +<script> + $(function() { + $('.nav-wrapper ul.right').hide(); + }); +</script> +` + +// RecoveryKey ... +func RecoveryKey() ([]byte, error) { + html := startAdminHTML + recoveryKeyHTML + 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("recoveryKey").Parse(html)) + err = tmpl.Execute(buf, a) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + // UsersList ... func UsersList(req *http.Request) ([]byte, error) { html := ` diff --git a/system/admin/handlers.go b/system/admin/handlers.go index fe04601..998db6f 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -21,6 +21,7 @@ import ( "github.com/bosssauce/ponzu/system/db" "github.com/gorilla/schema" + emailer "github.com/nilslice/email" "github.com/nilslice/jwt" ) @@ -521,12 +522,187 @@ func logoutHandler(res http.ResponseWriter, req *http.Request) { http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin/login", http.StatusFound) } +func forgotPasswordHandler(res http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + view, err := ForgotPassword() + 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 { + res.WriteHeader(http.StatusBadRequest) + errView, err := Error400() + if err != nil { + return + } + + res.Write(errView) + return + } + + // check email for user, if no user return Error + email := strings.ToLower(req.FormValue("email")) + if email == "" { + res.WriteHeader(http.StatusBadRequest) + errView, err := Error400() + if err != nil { + return + } + + res.Write(errView) + return + } + + err, u := db.User(email) + if err.Error() == db.ErrNoUserExists { + res.WriteHeader(http.StatusBadRequest) + errView, err := Error400() + if err != nil { + return + } + + res.Write(errView) + return + } + + if err.Error() != db.ErrNoUserExists && err != nil { + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + + // create temporary key to verify user + key, err := db.SetRecoveryKey(email) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + + domain := db.ConfigCache("domain") + body := fmt.Sprintf(` + 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: + + %s + + If you did not make the request, ignore this message and your password + will remain as-is. + + + Thank you, + Ponzu CMS at %s + + `, email, domain, key, domain) + msg := emailer.Message{ + To: email, + From: fmt.Sprintf("Ponzu CMS <ponzu-cms@%s", domain), + Subject: fmt.Sprintf("Account Recovery [%s]", domain), + Body: body, + } + + /* + err = msg.Send() + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + */ + 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) + + default: + res.WriteHeader(http.StatusMethodNotAllowed) + errView, err := Error405() + if err != nil { + return + } + + res.Write(errView) + return + } +} + +func recoveryKeyHandler(res http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + 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: + + // check for email & key match + + // set user with new password + + // redirect to /admin/login + + default: + res.WriteHeader(http.StatusMethodNotAllowed) + errView, err := Error405() + if err != nil { + return + } + + res.Write(errView) + return + } +} + +func recoveryEditHandler(res http.ResponseWriter, req *http.Request) { + +} + func postsHandler(res http.ResponseWriter, req *http.Request) { q := req.URL.Query() t := q.Get("type") if t == "" { res.WriteHeader(http.StatusBadRequest) - errView, err := Error405() + errView, err := Error400() if err != nil { return } @@ -837,9 +1013,9 @@ func approvePostHandler(res http.ResponseWriter, req *http.Request) { } // check if we have a Mergeable - m, ok := post.(content.Mergeable) + m, ok := post.(editor.Mergeable) if !ok { - log.Println("Content type", t, "must implement api.Mergable before it can bee approved.") + log.Println("Content type", t, "must implement editor.Mergable before it can bee approved.") res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { diff --git a/system/admin/server.go b/system/admin/server.go index 5d93d84..ef2ae4b 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -18,6 +18,10 @@ func Run() { http.HandleFunc("/admin/login", loginHandler) http.HandleFunc("/admin/logout", logoutHandler) + http.HandleFunc("/admin/recover", forgotPasswordHandler) + http.HandleFunc("/admin/recover/key", recoveryKeyHandler) + http.HandleFunc("/admin/recover/edit", recoveryEditHandler) + http.HandleFunc("/admin/configure", user.Auth(configHandler)) http.HandleFunc("/admin/configure/users", user.Auth(configUsersHandler)) http.HandleFunc("/admin/configure/users/edit", user.Auth(configUsersEditHandler)) diff --git a/system/db/user.go b/system/db/user.go index d2dc3a9..3386dc2 100644 --- a/system/db/user.go +++ b/system/db/user.go @@ -11,6 +11,7 @@ import ( "github.com/boltdb/bolt" "github.com/nilslice/jwt" + "github.com/nilslice/rand" ) // ErrUserExists is used for the db to report to admin user of existing user @@ -135,6 +136,10 @@ func User(email string) ([]byte, error) { return nil, err } + if val.Bytes() == nil { + return nil, ErrNoUserExists + } + return val.Bytes(), nil } @@ -184,3 +189,53 @@ func CurrentUser(req *http.Request) ([]byte, error) { return usr, nil } + +// SetRecoveryKey generates and saves a random secret key to verify an email +// address submitted in order to recover/reset an account password +func SetRecoveryKey(email string) (string, error) { + key := fmt.Sprintf("%d", rand.Int63()) + + err := store.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte("_recoveryKeys")) + if err != nil { + return err + } + + err = b.Put([]byte(email), []byte(key)) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return "", err + } + + return key, nil +} + +// RecoveryKey generates and saves a random secret key to verify an email +// address submitted in order to recover/reset an account password +func RecoveryKey(email string) (string, error) { + key := &bytes.Buffer{} + + err := store.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("_recoveryKeys")) + if b == nil { + return errors.New("No database found for checking keys.") + } + + _, err := key.Write(b.Get([]byte("email"))) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return "", err + } + + return key.String(), nil +} |