package admin import ( "bytes" "context" "encoding/base64" "encoding/json" "fmt" "log" "net/http" "strconv" "strings" "time" "github.com/ponzu-cms/ponzu/management/editor" "github.com/ponzu-cms/ponzu/management/manager" "github.com/ponzu-cms/ponzu/system/addon" "github.com/ponzu-cms/ponzu/system/admin/config" "github.com/ponzu-cms/ponzu/system/admin/upload" "github.com/ponzu-cms/ponzu/system/admin/user" "github.com/ponzu-cms/ponzu/system/api" "github.com/ponzu-cms/ponzu/system/db" "github.com/ponzu-cms/ponzu/system/item" "github.com/tidwall/gjson" "github.com/gorilla/schema" emailer "github.com/nilslice/email" "github.com/nilslice/jwt" ) func adminHandler(res http.ResponseWriter, req *http.Request) { view, err := Dashboard() if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } res.Header().Set("Content-Type", "text/html") res.Write(view) } func initHandler(res http.ResponseWriter, req *http.Request) { if db.SystemInitComplete() { http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin", http.StatusFound) return } switch req.Method { case http.MethodGet: view, err := Init() if err != nil { log.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 { log.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) // generate an Etag to use for response caching etag := db.NewEtag() req.Form.Set("etag", etag) // create and save admin user email := strings.ToLower(req.FormValue("email")) password := req.FormValue("password") usr, err := user.New(email, password) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } _, err = db.SetUser(usr) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } // set HTTP port which should be previously added to config cache port := db.ConfigCache("http_port") req.Form.Set("http_port", port) // 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 } // 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, Path: "/", }) redir := strings.TrimSuffix(req.URL.String(), "/init") http.Redirect(res, req, redir, http.StatusFound) default: res.WriteHeader(http.StatusMethodNotAllowed) } } func configHandler(res http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: data, err := db.ConfigAll() if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } c := &config.Config{} err = json.Unmarshal(data, c) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } cfg, err := c.MarshalEditor() if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } adminView, err := Admin(cfg) if err != nil { log.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 { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } err = db.SetConfig(req.Form) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } http.Redirect(res, req, req.URL.String(), http.StatusFound) default: res.WriteHeader(http.StatusMethodNotAllowed) } } func configUsersHandler(res http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: 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, err := user.New(email, password) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } _, 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 if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } // 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, err = user.New(email, newPassword) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } } else { updatedUser, err = user.New(email, password) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } } // 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 if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } // 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) } } 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 { log.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 { log.Println(err) http.Redirect(res, req, req.URL.String(), http.StatusFound) return } // check email & password j, err := db.User(strings.ToLower(req.FormValue("email"))) if err != nil { log.Println(err) http.Redirect(res, req, req.URL.String(), http.StatusFound) return } if j == nil { http.Redirect(res, req, req.URL.String(), http.StatusFound) return } usr := &user.User{} err = json.Unmarshal(j, usr) if err != nil { log.Println(err) http.Redirect(res, req, req.URL.String(), http.StatusFound) return } if !user.IsUser(usr, req.FormValue("password")) { http.Redirect(res, req, req.URL.String(), http.StatusFound) 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 { log.Println(err) http.Redirect(res, req, req.URL.String(), http.StatusFound) return } // add it to cookie +1 week expiration http.SetCookie(res, &http.Cookie{ Name: "_token", Value: token, Expires: week, Path: "/", }) 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: "", Path: "/", }) 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 { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() 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) log.Println("Failed account recovery. No email address submitted.") return } _, err = db.User(email) if err == db.ErrNoUserExists { res.WriteHeader(http.StatusBadRequest) log.Println("No user exists.", err) return } if err != db.ErrNoUserExists && err != nil { res.WriteHeader(http.StatusInternalServerError) log.Println("Error:", err) return } // create temporary key to verify user key, err := db.SetRecoveryKey(email) if err != nil { res.WriteHeader(http.StatusInternalServerError) log.Println("Failed to set account recovery key.", err) return } domain, err := db.Config("domain") if err != nil { res.WriteHeader(http.StatusInternalServerError) log.Println("Failed to get domain from configuration.", err) return } 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@%s", domain), Subject: fmt.Sprintf("Account Recovery [%s]", domain), Body: body, } go func() { err = msg.Send() if err != nil { log.Println("Failed to send message to:", msg.To, "about", msg.Subject, "Error:", err) } }() // 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) 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) 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) res.WriteHeader(http.StatusInternalServerError) res.Write([]byte("Error, please go back and try again.")) return } 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 } update, err := user.New(email, password) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) res.Write([]byte("Error, please go back and try again.")) return } 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 } } func contentsHandler(res http.ResponseWriter, req *http.Request) { q := req.URL.Query() t := q.Get("type") if t == "" { res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { return } res.Write(errView) return } order := strings.ToLower(q.Get("order")) if order != "asc" { order = "desc" } status := q.Get("status") if _, ok := item.Types[t]; !ok { res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { return } res.Write(errView) return } pt := item.Types[t]() p, ok := pt.(editor.Editable) if !ok { res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } var hasExt bool _, ok = pt.(api.Externalable) if ok { hasExt = true } 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) errView, err := Error500() if err != nil { return } res.Write(errView) 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) errView, err := Error500() if err != nil { return } res.Write(errView) return } } opts := db.QueryOptions{ Count: count, Offset: offset, Order: order, } var specifier string if status == "public" || status == "" { specifier = "__sorted" } else if status == "pending" { specifier = "__pending" } b := &bytes.Buffer{} var total int var posts [][]byte html := `
No addons available.
`) if err != nil { log.Println("Error writing default addon html to admin view:", err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { log.Println(err) return } res.Write(errView) return } } view, err := Admin(html.Bytes()) if err != nil { log.Println("Error writing addon html to admin view:", err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { log.Println(err) return } res.Write(errView) return } res.Write(view) case http.MethodPost: 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 } id := req.PostFormValue("id") action := strings.ToLower(req.PostFormValue("action")) _, err = db.Addon(id) if err == db.ErrNoAddonExists { log.Println(err) res.WriteHeader(http.StatusNotFound) errView, err := Error404() if err != nil { return } res.Write(errView) return } if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } switch action { case "enable": err := addon.Enable(id) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } case "disable": err := addon.Disable(id) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } default: res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { return } res.Write(errView) return } http.Redirect(res, req, req.URL.String(), http.StatusFound) default: res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { log.Println(err) return } res.Write(errView) return } } func addonHandler(res http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: id := req.FormValue("id") data, err := db.Addon(id) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } _, ok := addon.Types[id] if !ok { log.Println("Addon: ", id, "is not found in addon.Types map") res.WriteHeader(http.StatusNotFound) errView, err := Error404() if err != nil { return } res.Write(errView) return } m, err := addon.Manage(data, id) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } addonView, err := Admin(m) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) return } res.Header().Set("Content-Type", "text/html") res.Write(addonView) case http.MethodPost: // save req.Form 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 } name := req.FormValue("addon_name") id := req.FormValue("addon_reverse_dns") at, ok := addon.Types[id] if !ok { log.Println("Error: addon", name, "has no record in addon.Types map at", id) res.WriteHeader(http.StatusBadRequest) errView, err := Error400() if err != nil { return } res.Write(errView) return } // if Hookable, call BeforeSave prior to saving h, ok := at().(item.Hookable) if ok { err := h.BeforeSave(req) if err != nil { log.Println(err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } } err = db.SetAddon(req.Form, at()) if err != nil { log.Println("Error saving addon:", name, err) res.WriteHeader(http.StatusInternalServerError) errView, err := Error500() if err != nil { return } res.Write(errView) return } http.Redirect(res, req, "/admin/addon?id="+id, http.StatusFound) default: res.WriteHeader(http.StatusBadRequest) errView, err := Error405() if err != nil { log.Println(err) return } res.Write(errView) return } } func adminAddonListItem(data []byte) []byte { id := gjson.GetBytes(data, "addon_reverse_dns").String() status := gjson.GetBytes(data, "addon_status").String() name := gjson.GetBytes(data, "addon_name").String() author := gjson.GetBytes(data, "addon_author").String() authorURL := gjson.GetBytes(data, "addon_author_url").String() version := gjson.GetBytes(data, "addon_version").String() var action string var buttonClass string if status != addon.StatusEnabled { action = "Enable" buttonClass = "green" } else { action = "Disable" buttonClass = "red" } a := `