diff options
author | Steve Manuel <nilslice@gmail.com> | 2017-03-15 11:01:28 -0700 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2017-03-15 11:01:28 -0700 |
commit | 07fe1b15899fa6439e587984d6183371f9a6877c (patch) | |
tree | f1191c86ce03fbc36ea03539e9fbd27d143ec72d | |
parent | 0eaddb8ae0b29937f7514cc83c63e0aa3c377850 (diff) |
changing API for external client interaction. Externalable -> Createable, +Deleteable, changing Hookable interface methods to conform to pattern: BeforeAPI$ACTION, etc.
-rw-r--r-- | system/api/external.go | 230 | ||||
-rw-r--r-- | system/api/handlers.go | 4 | ||||
-rw-r--r-- | system/api/update.go | 29 | ||||
-rw-r--r-- | system/item/item.go | 37 |
4 files changed, 42 insertions, 258 deletions
diff --git a/system/api/external.go b/system/api/external.go deleted file mode 100644 index 7f13917..0000000 --- a/system/api/external.go +++ /dev/null @@ -1,230 +0,0 @@ -package api - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "strings" - "time" - - "github.com/ponzu-cms/ponzu/system/admin/upload" - "github.com/ponzu-cms/ponzu/system/db" - "github.com/ponzu-cms/ponzu/system/item" -) - -// Externalable accepts or rejects external POST requests to endpoints such as: -// /api/content/external?type=Review -type Externalable interface { - // Accept allows external content submissions of a specific type - Accept(http.ResponseWriter, *http.Request) error -} - -// Trustable allows external content to be auto-approved, meaning content sent -// as an Externalable will be stored in the public content bucket -type Trustable interface { - AutoApprove(http.ResponseWriter, *http.Request) error -} - -func externalContentHandler(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 := item.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 - } - - ts := fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) - 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.Set(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} - fieldOrderValue := make(map[string]map[string][]string) - ordVal := make(map[string][]string) - for k, v := range req.PostForm { - if strings.Contains(k, ".") { - fo := strings.Split(k, ".") - - // put the order and the field value into map - field := string(fo[0]) - order := string(fo[1]) - fieldOrderValue[field] = ordVal - - // orderValue is 0:[?type=Thing&id=1] - orderValue := fieldOrderValue[field] - orderValue[order] = v - fieldOrderValue[field] = orderValue - - // discard the post form value with name.N - req.PostForm.Del(k) - } - - } - - // add/set the key & value to the post form in order - for f, ov := range fieldOrderValue { - for i := 0; i < len(ov); i++ { - position := fmt.Sprintf("%d", i) - fieldValue := ov[position] - - if req.PostForm.Get(f) == "" { - for i, fv := range fieldValue { - if i == 0 { - req.PostForm.Set(f, fv) - } else { - req.PostForm.Add(f, fv) - } - } - } else { - for _, fv := range fieldValue { - req.PostForm.Add(f, fv) - } - } - } - } - - hook, ok := post.(item.Hookable) - if !ok { - log.Println("[External] error: Type", t, "does not implement item.Hookable or embed item.Item.") - res.WriteHeader(http.StatusBadRequest) - return - } - - err = hook.BeforeAccept(res, req) - if err != nil { - log.Println("[External] error calling BeforeAccept:", err) - return - } - - err = ext.Accept(res, req) - if err != nil { - log.Println("[External] error calling Accept:", err) - return - } - - err = hook.BeforeSave(res, req) - if err != nil { - log.Println("[External] error calling BeforeSave:", err) - return - } - - // set specifier for db bucket in case content is/isn't Trustable - var spec string - - // check if the content is Trustable should be auto-approved, if so the - // content is immediately added to the public content API. If not, then it - // is added to a "pending" list, only visible to Admins in the CMS and only - // if the type implements editor.Mergable - trusted, ok := post.(Trustable) - if ok { - err := trusted.AutoApprove(res, req) - if err != nil { - log.Println("[External] error calling AutoApprove:", err) - return - } - } else { - spec = "__pending" - } - - id, err := db.SetContent(t+spec+":-1", req.PostForm) - if err != nil { - log.Println("[External] error calling SetContent:", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - // set the target in the context so user can get saved value from db in hook - ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%d", t, id)) - req = req.WithContext(ctx) - - err = hook.AfterSave(res, req) - if err != nil { - log.Println("[External] error calling AfterSave:", err) - return - } - - err = hook.AfterAccept(res, req) - if err != nil { - log.Println("[External] error calling AfterAccept:", err) - return - } - - // create JSON response to send data back to client - var data map[string]interface{} - if spec != "" { - spec = strings.TrimPrefix(spec, "__") - data = map[string]interface{}{ - "status": spec, - "type": t, - } - } else { - spec = "public" - data = map[string]interface{}{ - "id": id, - "status": spec, - "type": t, - } - } - - resp := map[string]interface{}{ - "data": []map[string]interface{}{ - data, - }, - } - - j, err := json.Marshal(resp) - if err != nil { - log.Println("[External] error marshalling response to JSON:", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - res.Header().Set("Content-Type", "application/json") - _, err = res.Write(j) - if err != nil { - log.Println("[External] error writing response:", err) - return - } - -} diff --git a/system/api/handlers.go b/system/api/handlers.go index 4a9eaff..83bbe43 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "log" "net/http" "strconv" @@ -11,6 +12,9 @@ import ( "github.com/ponzu-cms/ponzu/system/item" ) +// ErrNoAuth should be used to report failed auth requests +var ErrNoAuth = errors.New("Auth failed for request") + // deprecating from API, but going to provide code here in case someone wants it func typesHandler(res http.ResponseWriter, req *http.Request) { var types = []string{} diff --git a/system/api/update.go b/system/api/update.go index 3a92a84..f7f7346 100644 --- a/system/api/update.go +++ b/system/api/update.go @@ -3,7 +3,6 @@ package api import ( "context" "encoding/json" - "errors" "fmt" "log" "net/http" @@ -15,13 +14,11 @@ import ( "github.com/ponzu-cms/ponzu/system/item" ) -var ErrNoAuth = errors.New("Auth failed for update request.") - // Updateable accepts or rejects update POST requests to endpoints such as: // /api/content/update?type=Review&id=1 type Updateable interface { - // AcceptUpdate allows external content update submissions of a specific type - AcceptUpdate(http.ResponseWriter, *http.Request) error + // Update enabled external clients to update content of a specific type + Update(http.ResponseWriter, *http.Request) error } func updateContentHandler(res http.ResponseWriter, req *http.Request) { @@ -45,14 +42,14 @@ func updateContentHandler(res http.ResponseWriter, req *http.Request) { p, found := item.Types[t] if !found { - log.Println("[Update] attempt to submit unknown type:", t, "from:", req.RemoteAddr) + log.Println("[Update] attempt to update content unknown type:", t, "from:", req.RemoteAddr) res.WriteHeader(http.StatusNotFound) return } id := req.URL.Query().Get("id") if !db.IsValidID(id) { - log.Println("[Update] attempt to submit update with missing or invalid id from:", req.RemoteAddr) + log.Println("[Update] attempt to update content with missing or invalid id from:", req.RemoteAddr) res.WriteHeader(http.StatusBadRequest) return } @@ -135,21 +132,21 @@ func updateContentHandler(res http.ResponseWriter, req *http.Request) { return } - err = hook.BeforeAcceptUpdate(res, req) + err = hook.BeforeAPIUpdate(res, req) if err != nil { - log.Println("[Update] error calling BeforeAcceptUpdate:", err) + log.Println("[Update] error calling BeforeAPIUpdate:", err) if err == ErrNoAuth { - // BeforeAcceptUpdate can check user.IsValid(req) for auth + // BeforeAPIUpdate can check user.IsValid(req) for auth res.WriteHeader(http.StatusUnauthorized) } return } - err = ext.AcceptUpdate(res, req) + err = ext.Update(res, req) if err != nil { - log.Println("[Update] error calling AcceptUpdate:", err) + log.Println("[Update] error calling Update:", err) if err == ErrNoAuth { - // AcceptUpdate can check user.IsValid(req) for auth + // Update can check user.IsValid(req) or other forms of validation for auth res.WriteHeader(http.StatusUnauthorized) } return @@ -172,7 +169,7 @@ func updateContentHandler(res http.ResponseWriter, req *http.Request) { } // set the target in the context so user can get saved value from db in hook - ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%d", t, id)) + ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%s", t, id)) req = req.WithContext(ctx) err = hook.AfterSave(res, req) @@ -181,9 +178,9 @@ func updateContentHandler(res http.ResponseWriter, req *http.Request) { return } - err = hook.AfterAcceptUpdate(res, req) + err = hook.AfterAPIUpdate(res, req) if err != nil { - log.Println("[Update] error calling AfterAcceptUpdate:", err) + log.Println("[Update] error calling AfterAPIUpdate:", err) return } diff --git a/system/item/item.go b/system/item/item.go index 286842b..f6e8f99 100644 --- a/system/item/item.go +++ b/system/item/item.go @@ -42,11 +42,14 @@ type Sortable interface { // to the different lifecycles/events a struct may encounter. Item implements // Hookable with no-ops so our user can override only whichever ones necessary. type Hookable interface { - BeforeAcceptUpdate(http.ResponseWriter, *http.Request) error - AfterAcceptUpdate(http.ResponseWriter, *http.Request) error + BeforeAPICreate(http.ResponseWriter, *http.Request) error + AfterAPICreate(http.ResponseWriter, *http.Request) error - BeforeAccept(http.ResponseWriter, *http.Request) error - AfterAccept(http.ResponseWriter, *http.Request) error + BeforeAPIUpdate(http.ResponseWriter, *http.Request) error + AfterAPIUpdate(http.ResponseWriter, *http.Request) error + + BeforeAPIDelete(http.ResponseWriter, *http.Request) error + AfterAPIDelete(http.ResponseWriter, *http.Request) error BeforeSave(http.ResponseWriter, *http.Request) error AfterSave(http.ResponseWriter, *http.Request) error @@ -135,23 +138,33 @@ func (i Item) String() string { return fmt.Sprintf("Item ID: %s", i.UniqueID()) } -// BeforeAcceptUpdate is a no-op to ensure structs which embed Item implement Hookable -func (i Item) BeforeAcceptUpdate(res http.ResponseWriter, req *http.Request) error { +// BeforeAPICreate is a no-op to ensure structs which embed Item implement Hookable +func (i Item) BeforeAPICreate(res http.ResponseWriter, req *http.Request) error { + return nil +} + +// AfterAPICreate is a no-op to ensure structs which embed Item implement Hookable +func (i Item) AfterAPICreate(res http.ResponseWriter, req *http.Request) error { + return nil +} + +// BeforeAPIUpdate is a no-op to ensure structs which embed Item implement Hookable +func (i Item) BeforeAPIUpdate(res http.ResponseWriter, req *http.Request) error { return nil } -// AfterAcceptUpdate is a no-op to ensure structs which embed Item implement Hookable -func (i Item) AfterAcceptUpdate(res http.ResponseWriter, req *http.Request) error { +// AfterAPIUpdate is a no-op to ensure structs which embed Item implement Hookable +func (i Item) AfterAPIUpdate(res http.ResponseWriter, req *http.Request) error { return nil } -// BeforeAccept is a no-op to ensure structs which embed Item implement Hookable -func (i Item) BeforeAccept(res http.ResponseWriter, req *http.Request) error { +// BeforeAPIDelete is a no-op to ensure structs which embed Item implement Hookable +func (i Item) BeforeAPIDelete(res http.ResponseWriter, req *http.Request) error { return nil } -// AfterAccept is a no-op to ensure structs which embed Item implement Hookable -func (i Item) AfterAccept(res http.ResponseWriter, req *http.Request) error { +// AfterAPIDelete is a no-op to ensure structs which embed Item implement Hookable +func (i Item) AfterAPIDelete(res http.ResponseWriter, req *http.Request) error { return nil } |