summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/README.md2
-rw-r--r--examples/createable/README.md (renamed from examples/externalable/README.md)6
-rw-r--r--examples/createable/content/song.go (renamed from examples/externalable/content/song.go)23
-rw-r--r--examples/deleteable/README.md31
-rw-r--r--examples/deleteable/content/song.go116
-rw-r--r--examples/updateable/README.md4
-rw-r--r--examples/updateable/content/song.go21
-rw-r--r--system/admin/handlers.go6
-rw-r--r--system/api/create.go (renamed from system/api/external.go)48
-rw-r--r--system/api/delete.go140
-rw-r--r--system/api/handlers.go4
-rw-r--r--system/api/server.go4
-rw-r--r--system/api/update.go29
-rw-r--r--system/db/content.go23
-rw-r--r--system/item/item.go37
15 files changed, 404 insertions, 90 deletions
diff --git a/examples/README.md b/examples/README.md
index f575db7..cd070b1 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -10,6 +10,6 @@ feature, it would be very helpful to include an example with a concrete use-case
for this directory.
### Table of Contents
-1. [Add content via HTTP API using the `api.Externalable` interface](https://github.com/ponzu-cms/ponzu/tree/master/examples/externalable)
+1. [Add content via HTTP API using the `api.Createable` interface](https://github.com/ponzu-cms/ponzu/tree/master/examples/createable)
2. [Update content via HTTP API using the `api.Updateable` interface](https://github.com/ponzu-cms/ponzu/tree/master/examples/updateable)
diff --git a/examples/externalable/README.md b/examples/createable/README.md
index 025e7fb..44ba03c 100644
--- a/examples/externalable/README.md
+++ b/examples/createable/README.md
@@ -1,8 +1,8 @@
-# Externalable
+# Createable
This example shows how to enable outside clients to submit content to your CMS.
All content submitted must be done through a POST request encoded as `multipart/form-data`
-to the API endpoint `/api/content/external?type=<Type>`
+to the API endpoint `/api/content/create?type=<Type>`
## Song example
Imagine an app that lets users add Spotify music to a global playlist, and you need them
@@ -23,7 +23,7 @@ See the file `content/song.go` and read the comments to understand the various
methods needed to satisfy required interfaces for this kind of activity.
### Overview
-1. Implement `api.Externalable` with the `Accept(http.ResponseWriter, *http.Request)` method to allow outside POST requests
+1. Implement `api.Createable` with the `Create(http.ResponseWriter, *http.Request)` method to allow outside POST requests
2. Implement `editor.Mergeable` with the `Approve(http.ResponseWriter, *http.Request)` method so you can control the Approval / Rejection of submitted content OR
3. Implement `api.Trustable` with the `AutoApprove(http.ResponseWriter, *http.Request)` method to bypass `Approve` and auto-approve and publish submitted content
diff --git a/examples/externalable/content/song.go b/examples/createable/content/song.go
index f2b1b73..7473e91 100644
--- a/examples/externalable/content/song.go
+++ b/examples/createable/content/song.go
@@ -8,6 +8,7 @@ import (
"github.com/ponzu-cms/ponzu/management/editor"
"github.com/ponzu-cms/ponzu/system/admin/user"
+ "github.com/ponzu-cms/ponzu/system/api"
"github.com/ponzu-cms/ponzu/system/item"
)
@@ -78,10 +79,10 @@ func init() {
// String defines the display name of a Song in the CMS list-view
func (s *Song) String() string { return s.Title }
-// Accept implements api.Externalable, and allows external POST requests from clients
+// Create implements api.Createable, and allows external POST requests from clients
// to add content as long as the request contains the json tag names of the Song
// struct fields, and is multipart encoded
-func (s *Song) Accept(res http.ResponseWriter, req *http.Request) error {
+func (s *Song) Create(res http.ResponseWriter, req *http.Request) error {
// do form data validation for required fields
required := []string{
"title",
@@ -101,31 +102,29 @@ func (s *Song) Accept(res http.ResponseWriter, req *http.Request) error {
return nil
}
-// BeforeAccept is only called if the Song type implements api.Externalable
-// It is called before Accept, and returning an error will cancel the request
+// BeforeAPICreate is only called if the Song type implements api.Createable
+// It is called before Create, and returning an error will cancel the request
// causing the system to reject the data sent in the POST
-func (s *Song) BeforeAccept(res http.ResponseWriter, req *http.Request) error {
+func (s *Song) BeforeAPICreate(res http.ResponseWriter, req *http.Request) error {
// do initial user authentication here on the request, checking for a
// token or cookie, or that certain form fields are set and valid
// for example, this will check if the request was made by a CMS admin user:
if !user.IsValid(req) {
- addr := req.RemoteAddr
- err := fmt.Errorf("request rejected, invalid user. IP: %s", addr)
- return err
+ return api.ErrNoAuth
}
// you could then to data validation on the request post form, or do it in
- // the Accept method, which is called after BeforeAccept
+ // the Create method, which is called after BeforeAPICreate
return nil
}
-// AfterAccept is called after Accept, and is useful for logging or triggering
+// AfterAPICreate is called after Create, and is useful for logging or triggering
// notifications, etc. after the data is saved to the database, etc.
// The request has a context containing the databse 'target' affected by the
// request. Ex. Song__pending:3 or Song:8 depending if Song implements api.Trustable
-func (s *Song) AfterAccept(res http.ResponseWriter, req *http.Request) error {
+func (s *Song) AfterAPICreate(res http.ResponseWriter, req *http.Request) error {
addr := req.RemoteAddr
log.Println("Song sent by:", addr, "titled:", req.PostFormValue("title"))
@@ -149,7 +148,7 @@ func (s *Song) Approve(res http.ResponseWriter, req *http.Request) error {
*/
// AutoApprove implements api.Trustable, and will automatically approve content
-// that has been submitted by an external client via api.Externalable. Be careful
+// that has been submitted by an external client via api.Createable. Be careful
// when using AutoApprove, because content will immediately be available through
// your public content API. If the Trustable interface is satisfied, the AfterApprove
// method is bypassed. The
diff --git a/examples/deleteable/README.md b/examples/deleteable/README.md
new file mode 100644
index 0000000..06f4188
--- /dev/null
+++ b/examples/deleteable/README.md
@@ -0,0 +1,31 @@
+# Updateable
+
+This example shows how to enable outside clients to update content to your CMS.
+All content submitted must be done through a POST request encoded as `multipart/form-data`
+to the API endpoint `/api/content/update?type=<Type>&id=<id>`
+
+## Song example
+Imagine an app that lets users add Spotify music to a global playlist, and you need them
+to supply songs in the format:
+```go
+type Song struct {
+ item.Item
+
+ Title string `json:"title"`
+ Artist string `json:"artist"`
+ Rating int `json:"rating"`
+ Opinion string `json:"opinion"`
+ SpotifyURL string `json:"spotify_url"`
+}
+```
+
+See the file `content/song.go` and read the comments to understand the various
+methods needed to satisfy required interfaces for this kind of activity.
+
+### Overview
+1. Implement `api.Updateable` with the `Update(http.ResponseWriter, *http.Request)` method to allow outside POST requests.
+2. Consistent with the createable example, authentication can be validated in `BeforeAPIUpdate(http.ResponseWriter, *http.Request)`
+
+There are various validation and request checks shown in this example as well.
+Please feel free to modify and submit a PR for updates or bug fixes!
+
diff --git a/examples/deleteable/content/song.go b/examples/deleteable/content/song.go
new file mode 100644
index 0000000..b33bad0
--- /dev/null
+++ b/examples/deleteable/content/song.go
@@ -0,0 +1,116 @@
+package content
+
+import (
+ "fmt"
+ "log"
+
+ "net/http"
+
+ "github.com/ponzu-cms/ponzu/management/editor"
+ "github.com/ponzu-cms/ponzu/system/admin/user"
+ "github.com/ponzu-cms/ponzu/system/api"
+ "github.com/ponzu-cms/ponzu/system/item"
+)
+
+type Song struct {
+ item.Item
+
+ Title string `json:"title"`
+ Artist string `json:"artist"`
+ Rating int `json:"rating"`
+ Opinion string `json:"opinion"`
+ SpotifyURL string `json:"spotify_url"`
+}
+
+// MarshalEditor writes a buffer of html to edit a Song within the CMS
+// and implements editor.Editable
+func (s *Song) MarshalEditor() ([]byte, error) {
+ view, err := editor.Form(s,
+ // Take note that the first argument to these Input-like functions
+ // is the string version of each Song field, and must follow
+ // this pattern for auto-decoding and auto-encoding reasons:
+ editor.Field{
+ View: editor.Input("Title", s, map[string]string{
+ "label": "Title",
+ "type": "text",
+ "placeholder": "Enter the Title here",
+ }),
+ },
+ editor.Field{
+ View: editor.Input("Artist", s, map[string]string{
+ "label": "Artist",
+ "type": "text",
+ "placeholder": "Enter the Artist here",
+ }),
+ },
+ editor.Field{
+ View: editor.Input("Rating", s, map[string]string{
+ "label": "Rating",
+ "type": "text",
+ "placeholder": "Enter the Rating here",
+ }),
+ },
+ editor.Field{
+ View: editor.Richtext("Opinion", s, map[string]string{
+ "label": "Opinion",
+ "placeholder": "Enter the Opinion here",
+ }),
+ },
+ editor.Field{
+ View: editor.Input("SpotifyURL", s, map[string]string{
+ "label": "SpotifyURL",
+ "type": "text",
+ "placeholder": "Enter the SpotifyURL here",
+ }),
+ },
+ )
+
+ if err != nil {
+ return nil, fmt.Errorf("Failed to render Song editor view: %s", err.Error())
+ }
+
+ return view, nil
+}
+
+func init() {
+ item.Types["Song"] = func() interface{} { return new(Song) }
+}
+
+// String defines the display name of a Song in the CMS list-view
+func (s *Song) String() string { return s.Title }
+
+// BeforeAPIDelete is only called if the Song type implements api.Deleteable
+// It is called before Delete, and returning an error will cancel the request
+// causing the system to reject the data sent in the POST
+func (s *Song) BeforeAPIDelete(res http.ResponseWriter, req *http.Request) error {
+ // do initial user authentication here on the request, checking for a
+ // token or cookie, or that certain form fields are set and valid
+
+ // for example, this will check if the request was made by a CMS admin user:
+ if !user.IsValid(req) {
+ return api.ErrNoAuth
+ }
+
+ // you could then to data validation on the request post form, or do it in
+ // the Delete method, which is called after BeforeAPIDelete
+
+ return nil
+}
+
+// Delete is called after BeforeAPIDelete and implements api.Deleteable. All
+// other delete-based hooks are only called if this is implemented.
+func (s *Song) Delete(res http.ResponseWriter, req *http.Request) error {
+ // See BeforeAPIDelete above, how we have checked the request for some
+ // form of auth. This could be done here instead, but if it is done once
+ // above, it means the request is valid here too.
+ return nil
+}
+
+// AfterAPIDelete is called after Delete, and is useful for logging or triggering
+// notifications, etc. after the data is deleted frm the database, etc.
+func (s *Song) AfterAPIDelete(res http.ResponseWriter, req *http.Request) error {
+ addr := req.RemoteAddr
+ log.Println("Song deleted by:", addr, "id:", req.URL.Query().Get("id"))
+
+ return nil
+}
diff --git a/examples/updateable/README.md b/examples/updateable/README.md
index 1cc50f8..06f4188 100644
--- a/examples/updateable/README.md
+++ b/examples/updateable/README.md
@@ -23,8 +23,8 @@ See the file `content/song.go` and read the comments to understand the various
methods needed to satisfy required interfaces for this kind of activity.
### Overview
-1. Implement `api.Updateable` with the `AcceptUpdate(http.ResponseWriter, *http.Request)` method to allow outside POST requests.
-2. Consistent with the externalable example, authentication can be validated in `BeforeAcceptUdate(http.ResponseWriter, *http.Request)`
+1. Implement `api.Updateable` with the `Update(http.ResponseWriter, *http.Request)` method to allow outside POST requests.
+2. Consistent with the createable example, authentication can be validated in `BeforeAPIUpdate(http.ResponseWriter, *http.Request)`
There are various validation and request checks shown in this example as well.
Please feel free to modify and submit a PR for updates or bug fixes!
diff --git a/examples/updateable/content/song.go b/examples/updateable/content/song.go
index 1ebe232..a2dc7f8 100644
--- a/examples/updateable/content/song.go
+++ b/examples/updateable/content/song.go
@@ -9,6 +9,7 @@ import (
"github.com/ponzu-cms/ponzu/management/editor"
"github.com/ponzu-cms/ponzu/system/admin/user"
+ "github.com/ponzu-cms/ponzu/system/api"
"github.com/ponzu-cms/ponzu/system/item"
)
@@ -79,31 +80,29 @@ func init() {
// String defines the display name of a Song in the CMS list-view
func (s *Song) String() string { return s.Title }
-// BeforeAcceptUpdate is only called if the Song type implements api.Updateable
-// It is called before AcceptUpdate, and returning an error will cancel the request
+// BeforeAPIUpdate is only called if the Song type implements api.Updateable
+// It is called before Update, and returning an error will cancel the request
// causing the system to reject the data sent in the POST
-func (s *Song) BeforeAcceptUpdate(res http.ResponseWriter, req *http.Request) error {
+func (s *Song) BeforeAPIUpdate(res http.ResponseWriter, req *http.Request) error {
// do initial user authentication here on the request, checking for a
// token or cookie, or that certain form fields are set and valid
// for example, this will check if the request was made by a CMS admin user:
if !user.IsValid(req) {
- addr := req.RemoteAddr
- err := fmt.Errorf("request rejected, invalid user. IP: %s", addr)
- return err
+ return api.ErrNoAuth
}
// you could then to data validation on the request post form, or do it in
- // the Accept method, which is called after BeforeAccept
+ // the Update method, which is called after BeforeAPIUpdate
return nil
}
-// AcceptUpdate is called after BeforeAccept and is where you may influence the
+// Update is called after BeforeAPIUpdate and is where you may influence the
// merge process. For example, maybe you don't want an empty string for the Title
// or Artist field to be accepted by the update request. Updates will always merge
// with existing values, but by default will accept zero value as an update if sent.
-func (s *Song) AcceptUpdate(res http.ResponseWriter, req *http.Request) error {
+func (s *Song) Update(res http.ResponseWriter, req *http.Request) error {
addr := req.RemoteAddr
log.Println("Song update sent by:", addr, "id:", req.URL.Query().Get("id"))
@@ -129,11 +128,11 @@ func (s *Song) AcceptUpdate(res http.ResponseWriter, req *http.Request) error {
return nil
}
-// AfterAcceptUpdate is called after AcceptUpdate, and is useful for logging or triggering
+// AfterAPIUpdate is called after Update, and is useful for logging or triggering
// notifications, etc. after the data is saved to the database, etc.
// The request has a context containing the databse 'target' affected by the
// request.
-func (s *Song) AfterAcceptUpdate(res http.ResponseWriter, req *http.Request) error {
+func (s *Song) AfterAPIUpdate(res http.ResponseWriter, req *http.Request) error {
addr := req.RemoteAddr
log.Println("Song updated by:", addr, "id:", req.URL.Query().Get("id"))
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
index 4bb5521..a585faa 100644
--- a/system/admin/handlers.go
+++ b/system/admin/handlers.go
@@ -866,7 +866,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) {
}
var hasExt bool
- _, ok = pt.(api.Externalable)
+ _, ok = pt.(api.Createable)
if ok {
hasExt = true
}
@@ -1409,7 +1409,7 @@ func approveContentHandler(res http.ResponseWriter, req *http.Request) {
}
if pendingID != "" {
- err = db.DeleteContent(req.FormValue("type")+":"+pendingID, req.Form)
+ err = db.DeleteContent(req.FormValue("type") + ":" + pendingID)
if err != nil {
log.Println("Failed to remove content after approval:", err)
}
@@ -1757,7 +1757,7 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
return
}
- err = db.DeleteContent(t+":"+id, req.Form)
+ err = db.DeleteContent(t + ":" + id)
if err != nil {
log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
diff --git a/system/api/external.go b/system/api/create.go
index 7f13917..3328bd6 100644
--- a/system/api/external.go
+++ b/system/api/create.go
@@ -14,20 +14,20 @@ import (
"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
+// Createable accepts or rejects external POST requests to endpoints such as:
+// /api/content/create?type=Review
+type Createable interface {
+ // Create enables external clients to submit content of a specific type
+ Create(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
+// as an Createable 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) {
+func createContentHandler(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
res.WriteHeader(http.StatusMethodNotAllowed)
return
@@ -35,7 +35,7 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
if err != nil {
- log.Println("[External] error:", err)
+ log.Println("[Create] error:", err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -48,16 +48,16 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
p, found := item.Types[t]
if !found {
- log.Println("[External] attempt to submit unknown type:", t, "from:", req.RemoteAddr)
+ log.Println("[Create] attempt to submit unknown type:", t, "from:", req.RemoteAddr)
res.WriteHeader(http.StatusNotFound)
return
}
post := p()
- ext, ok := post.(Externalable)
+ ext, ok := post.(Createable)
if !ok {
- log.Println("[External] rejected non-externalable type:", t, "from:", req.RemoteAddr)
+ log.Println("[Create] rejected non-createable type:", t, "from:", req.RemoteAddr)
res.WriteHeader(http.StatusBadRequest)
return
}
@@ -126,26 +126,26 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
hook, ok := post.(item.Hookable)
if !ok {
- log.Println("[External] error: Type", t, "does not implement item.Hookable or embed item.Item.")
+ log.Println("[Create] error: Type", t, "does not implement item.Hookable or embed item.Item.")
res.WriteHeader(http.StatusBadRequest)
return
}
- err = hook.BeforeAccept(res, req)
+ err = hook.BeforeAPICreate(res, req)
if err != nil {
- log.Println("[External] error calling BeforeAccept:", err)
+ log.Println("[Create] error calling BeforeAccept:", err)
return
}
- err = ext.Accept(res, req)
+ err = ext.Create(res, req)
if err != nil {
- log.Println("[External] error calling Accept:", err)
+ log.Println("[Create] error calling Accept:", err)
return
}
err = hook.BeforeSave(res, req)
if err != nil {
- log.Println("[External] error calling BeforeSave:", err)
+ log.Println("[Create] error calling BeforeSave:", err)
return
}
@@ -160,7 +160,7 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
if ok {
err := trusted.AutoApprove(res, req)
if err != nil {
- log.Println("[External] error calling AutoApprove:", err)
+ log.Println("[Create] error calling AutoApprove:", err)
return
}
} else {
@@ -169,7 +169,7 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
id, err := db.SetContent(t+spec+":-1", req.PostForm)
if err != nil {
- log.Println("[External] error calling SetContent:", err)
+ log.Println("[Create] error calling SetContent:", err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -180,13 +180,13 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
err = hook.AfterSave(res, req)
if err != nil {
- log.Println("[External] error calling AfterSave:", err)
+ log.Println("[Create] error calling AfterSave:", err)
return
}
- err = hook.AfterAccept(res, req)
+ err = hook.AfterAPICreate(res, req)
if err != nil {
- log.Println("[External] error calling AfterAccept:", err)
+ log.Println("[Create] error calling AfterAccept:", err)
return
}
@@ -215,7 +215,7 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
j, err := json.Marshal(resp)
if err != nil {
- log.Println("[External] error marshalling response to JSON:", err)
+ log.Println("[Create] error marshalling response to JSON:", err)
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -223,7 +223,7 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "application/json")
_, err = res.Write(j)
if err != nil {
- log.Println("[External] error writing response:", err)
+ log.Println("[Create] error writing response:", err)
return
}
diff --git a/system/api/delete.go b/system/api/delete.go
new file mode 100644
index 0000000..36f2b1b
--- /dev/null
+++ b/system/api/delete.go
@@ -0,0 +1,140 @@
+package api
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+
+ "github.com/ponzu-cms/ponzu/system/db"
+ "github.com/ponzu-cms/ponzu/system/item"
+)
+
+// Deleteable accepts or rejects update POST requests to endpoints such as:
+// /api/content/delete?type=Review&id=1
+type Deleteable interface {
+ // Delete enables external clients to delete content of a specific type
+ Delete(http.ResponseWriter, *http.Request) error
+}
+
+func deleteContentHandler(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("[Delete] 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("[Delete] attempt to delete content of unknown type:", t, "from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ id := req.URL.Query().Get("id")
+ if !db.IsValidID(id) {
+ log.Println("[Delete] attempt to delete content with missing or invalid id from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ post := p()
+
+ ext, ok := post.(Deleteable)
+ if !ok {
+ log.Println("[Delete] rejected non-deleteable type:", t, "from:", req.RemoteAddr)
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ hook, ok := post.(item.Hookable)
+ if !ok {
+ log.Println("[Delete] error: Type", t, "does not implement item.Hookable or embed item.Item.")
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ err = hook.BeforeAPIDelete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling BeforeAPIDelete:", err)
+ if err == ErrNoAuth {
+ // BeforeAPIDelete can check user.IsValid(req) for auth
+ res.WriteHeader(http.StatusUnauthorized)
+ }
+ return
+ }
+
+ err = ext.Delete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling Delete:", err)
+ if err == ErrNoAuth {
+ // Delete can check user.IsValid(req) or other forms of validation for auth
+ res.WriteHeader(http.StatusUnauthorized)
+ }
+ return
+ }
+
+ err = hook.BeforeDelete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling BeforeSave:", err)
+ return
+ }
+
+ err = db.DeleteContent(t + ":" + id)
+ if err != nil {
+ log.Println("[Delete] error calling DeleteContent:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ err = hook.AfterDelete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling AfterDelete:", err)
+ return
+ }
+
+ err = hook.AfterAPIDelete(res, req)
+ if err != nil {
+ log.Println("[Delete] error calling AfterAPIDelete:", err)
+ return
+ }
+
+ // create JSON response to send data back to client
+ var data = map[string]interface{}{
+ "id": id,
+ "status": "deleted",
+ "type": t,
+ }
+
+ resp := map[string]interface{}{
+ "data": []map[string]interface{}{
+ data,
+ },
+ }
+
+ j, err := json.Marshal(resp)
+ if err != nil {
+ log.Println("[Delete] 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("[Delete] 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/server.go b/system/api/server.go
index 6a848dd..c568877 100644
--- a/system/api/server.go
+++ b/system/api/server.go
@@ -8,7 +8,9 @@ func Run() {
http.HandleFunc("/api/content", Record(CORS(Gzip(contentHandler))))
- http.HandleFunc("/api/content/external", Record(CORS(externalContentHandler)))
+ http.HandleFunc("/api/content/create", Record(CORS(createContentHandler)))
http.HandleFunc("/api/content/update", Record(CORS(updateContentHandler)))
+
+ http.HandleFunc("/api/content/delete", Record(CORS(deleteContentHandler)))
}
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/db/content.go b/system/db/content.go
index dd93c60..8a91828 100644
--- a/system/db/content.go
+++ b/system/db/content.go
@@ -228,11 +228,25 @@ func insert(ns string, data url.Values) (int, error) {
// DeleteContent removes an item from the database. Deleting a non-existent item
// will return a nil error.
-func DeleteContent(target string, data url.Values) error {
+func DeleteContent(target string) error {
t := strings.Split(target, ":")
ns, id := t[0], t[1]
- err := store.Update(func(tx *bolt.Tx) error {
+ b, err := Content(target)
+ if err != nil {
+ return err
+ }
+
+ // get content slug to delete from __contentIndex if it exists
+ // this way content added later can use slugs even if previously
+ // deleted content had used one
+ var itm item.Item
+ err = json.Unmarshal(b, &itm)
+ if err != nil {
+ return err
+ }
+
+ err = store.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(ns))
if b == nil {
return bolt.ErrBucketNotFound
@@ -244,14 +258,13 @@ func DeleteContent(target string, data url.Values) error {
}
// if content has a slug, also delete it from __contentIndex
- slug := data.Get("slug")
- if slug != "" {
+ if itm.Slug != "" {
ci := tx.Bucket([]byte("__contentIndex"))
if ci == nil {
return bolt.ErrBucketNotFound
}
- err := ci.Delete([]byte(slug))
+ err := ci.Delete([]byte(itm.Slug))
if err != nil {
return err
}
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
}