diff options
author | Steve Manuel <nilslice@gmail.com> | 2016-10-06 03:14:10 -0700 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2016-10-06 03:14:10 -0700 |
commit | c0ba07669d8403f428ec250e3f3da74844c6c587 (patch) | |
tree | 94313747d9abea02b2f3b01c06308225d9db5e4a | |
parent | 698173a6176762f966be0abd1dc77b85e482a03a (diff) |
adding authentication & token-based persistence for users, init setup for first-use, pulling out some handlers into separate file for readability and navigation
-rw-r--r-- | system/admin/admin.go | 160 | ||||
-rw-r--r-- | system/admin/auth.go | 11 | ||||
-rw-r--r-- | system/admin/config.go | 5 | ||||
-rw-r--r-- | system/admin/config/config.go | 89 | ||||
-rw-r--r-- | system/admin/handlers.go | 127 | ||||
-rw-r--r-- | system/admin/server.go | 185 | ||||
-rw-r--r-- | system/admin/static/dashboard/css/admin.css | 44 | ||||
-rw-r--r-- | system/admin/user/auth.go | 106 | ||||
-rw-r--r-- | system/api/server.go | 8 | ||||
-rw-r--r-- | system/db/query.go | 207 |
10 files changed, 680 insertions, 262 deletions
diff --git a/system/admin/admin.go b/system/admin/admin.go index 410c9f4..1c04bce 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -7,12 +7,13 @@ import ( "html/template" "github.com/nilslice/cms/content" + "github.com/nilslice/cms/system/db" ) -const adminHTML = `<!doctype html> +var startAdminHTML = `<!doctype html> <html lang="en"> <head> - <title>CMS</title> + <title>{{ .Logo }}</title> <script type="text/javascript" src="/admin/static/common/js/jquery-2.1.4.min.js"></script> <script type="text/javascript" src="/admin/static/common/js/util.js"></script> <script type="text/javascript" src="/admin/static/dashboard/js/materialize.min.js"></script> @@ -32,7 +33,7 @@ const adminHTML = `<!doctype html> <div class="navbar-fixed"> <nav class="grey darken-2"> <div class="nav-wrapper"> - <a class="brand-logo" href="/admin">CMS</a> + <a class="brand-logo" href="/admin">{{ .Logo }}</a> <ul class="right"> <li><a href="/admin/logout">Logout</a></li> @@ -41,8 +42,9 @@ const adminHTML = `<!doctype html> </nav> </div> - <div class="admin-ui row"> - + <div class="admin-ui row">` + +var mainAdminHTML = ` <div class="left-nav col s3"> <div class="card"> <ul class="card-content collection"> @@ -57,6 +59,7 @@ const adminHTML = `<!doctype html> <div class="card-title">System</div> <div class="row collection-item"> <li><a class="col s12" href="/admin/configure"><i class="tiny left material-icons">settings</i>Configuration</a></li> + <li><a class="col s12" href="/admin/configure/users"><i class="tiny left material-icons">supervisor_account</i>Users</a></li> </div> </ul> </div> @@ -65,26 +68,167 @@ const adminHTML = `<!doctype html> <div class="subview col s9"> {{ .Subview }} </div> - {{ end }} + {{ end }}` + +var endAdminHTML = ` </div> </body> </html>` type admin struct { + Logo string Types map[string]func() interface{} Subview template.HTML } // Admin ... func Admin(view []byte) ([]byte, error) { + cfg, err := db.Config("name") + if err != nil { + return nil, err + } + + if cfg == nil { + cfg = []byte("") + } + a := admin{ + Logo: string(cfg), Types: content.Types, Subview: template.HTML(view), } buf := &bytes.Buffer{} - tmpl := template.Must(template.New("admin").Parse(adminHTML)) - err := tmpl.Execute(buf, a) + html := startAdminHTML + mainAdminHTML + endAdminHTML + tmpl := template.Must(template.New("admin").Parse(html)) + err = tmpl.Execute(buf, a) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var initAdminHTML = ` +<div class="init col s5"> +<div class="card"> +<div class="card-content"> + <div class="card-title">Welcome!</div> + <blockquote>You need to initialize your system by filling out the form below. All of + this information can be updated later on, but you will not be able to start + without first completing this step.</blockquote> + <form method="post" action="/admin/init" class="row"> + <div>Configuration</div> + <div class="input-field col s12"> + <input placeholder="Enter the name of your site (interal use only)" class="validate required" type="text" id="name" name="name"/> + <label for="name" class="active">Site Name</label> + </div> + <div class="input-field col s12"> + <input placeholder="Used for acquiring SSL certificate (e.g. www.example.com or example.com)" class="validate" type="text" id="domain" name="domain"/> + <label for="domain" class="active">Domain</label> + </div> + <div>Admin Details</div> + <div class="input-field col s12"> + <input placeholder="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 a strong password" class="validate required" type="password" id="password" name="password"/> + <label for="password" class="active">Password</label> + </div> + <button class="btn waves-effect waves-light right">Start</button> + </form> +</div> +</div> +</div> +<script> + $(function() { + $('.nav-wrapper ul.right').hide(); + + var logo = $('a.brand-logo'); + var name = $('input#name'); + + name.on('change', function(e) { + logo.text(e.target.value); + }); + }); +</script> +` + +// Init ... +func Init() ([]byte, error) { + html := startAdminHTML + initAdminHTML + 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("init").Parse(html)) + err = tmpl.Execute(buf, a) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var loginAdminHTML = ` +<div class="init col s5"> +<div class="card"> +<div class="card-content"> + <div class="card-title">Welcome!</div> + <blockquote>Please log in to the system using your email address and password.</blockquote> + <form method="post" action="/admin/login" 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> + <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> + </div> + <button class="btn waves-effect waves-light right">Log in</button> + </form> +</div> +</div> +</div> +<script> + $(function() { + $('.nav-wrapper ul.right').hide(); + }); +</script> +` + +// Login ... +func Login() ([]byte, error) { + html := startAdminHTML + loginAdminHTML + 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("login").Parse(html)) + err = tmpl.Execute(buf, a) if err != nil { return nil, err } diff --git a/system/admin/auth.go b/system/admin/auth.go deleted file mode 100644 index 153a31a..0000000 --- a/system/admin/auth.go +++ /dev/null @@ -1,11 +0,0 @@ -package admin - -// Session ... -type Session struct { - User - token string -} - -// User ... -type User struct { -} diff --git a/system/admin/config.go b/system/admin/config.go deleted file mode 100644 index e067299..0000000 --- a/system/admin/config.go +++ /dev/null @@ -1,5 +0,0 @@ -package admin - -// Config represents the confirgurable options of the system -type Config struct { -} diff --git a/system/admin/config/config.go b/system/admin/config/config.go new file mode 100644 index 0000000..791510e --- /dev/null +++ b/system/admin/config/config.go @@ -0,0 +1,89 @@ +package config + +import ( + "github.com/nilslice/cms/content" + "github.com/nilslice/cms/management/editor" +) + +//Config represents the confirgurable options of the system +type Config struct { + content.Item + editor editor.Editor + + Name string `json:"name"` + Domain string `json:"domain"` + ClientSecret string `json:"client_secret"` +} + +// SetContentID partially implements editor.Editable +func (c *Config) SetContentID(id int) { c.ID = id } + +// ContentID partially implements editor.Editable +func (c *Config) ContentID() int { return c.ID } + +// ContentName partially implements editor.Editable +func (c *Config) ContentName() string { return c.Name } + +// SetSlug partially implements editor.Editable +func (c *Config) SetSlug(slug string) { c.Slug = slug } + +// Editor partially implements editor.Editable +func (c *Config) Editor() *editor.Editor { return &c.editor } + +// MarshalEditor writes a buffer of html to edit a Post and partially implements editor.Editable +func (c *Config) MarshalEditor() ([]byte, error) { + view, err := editor.Form(c, + editor.Field{ + View: editor.Input("Name", c, map[string]string{ + "label": "Site Name", + "placeholder": "Add a name to this site (internal use only)", + }), + }, + editor.Field{ + View: editor.Input("Domain", c, map[string]string{ + "label": "Domain Name (required for SSL certificate)", + "placeholder": "e.g. www.example.com or example.com", + }), + }, + editor.Field{ + View: editor.Input("ClientSecret", c, map[string]string{ + "label": "Client Secret (used to validate requests)", + "disabled": "true", + }), + }, + ) + if err != nil { + return nil, err + } + + open := []byte(`<div class="card"><form action="/admin/configure" method="post">`) + close := []byte(`</form></div>`) + script := []byte(` + <script> + $(function() { + // hide default fields & labels unecessary for the config + var fields = $('.default-fields'); + fields.css('position', 'relative'); + fields.find('input:not([type=submit])').remove(); + fields.find('label').remove(); + fields.find('button').css({ + position: 'absolute', + top: '-10px', + right: '0px' + }); + + // adjust layout of td so save button is in same location as usual + fields.find('td').css('float', 'right'); + + // stop some fixed config settings from being modified + fields.find('input[name=client_secret]').attr('name', ''); + }); + </script> + `) + + view = append(open, view...) + view = append(view, close...) + view = append(view, script...) + + return view, nil +} diff --git a/system/admin/handlers.go b/system/admin/handlers.go new file mode 100644 index 0000000..9ff39c3 --- /dev/null +++ b/system/admin/handlers.go @@ -0,0 +1,127 @@ +package admin + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/nilslice/cms/system/admin/user" + "github.com/nilslice/cms/system/db" + "github.com/nilslice/jwt" +) + +func adminHandler(res http.ResponseWriter, req *http.Request) { + view, err := Admin(nil) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + res.Header().Set("Content-Type", "text/html") + res.Write(view) +} + +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 { + fmt.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 { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + fmt.Println(req.FormValue("email")) + fmt.Println(req.FormValue("password")) + + // check email & password + j, err := db.User(req.FormValue("email")) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + if j == nil { + fmt.Println(err) + res.WriteHeader(http.StatusBadRequest) + fmt.Println("j == nil") + return + } + + usr := &user.User{} + err = json.Unmarshal(j, usr) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + if !user.IsUser(usr, req.FormValue("password")) { + res.WriteHeader(http.StatusBadRequest) + fmt.Println("!IsUser") + 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 { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + // add it to cookie +1 week expiration + http.SetCookie(res, &http.Cookie{ + Name: "_token", + Value: token, + Expires: week, + }) + + 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: "", + }) + + http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin/login", http.StatusFound) +} diff --git a/system/admin/server.go b/system/admin/server.go index 900f47d..92c86c3 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -2,6 +2,7 @@ package admin import ( "bytes" + "encoding/base64" "encoding/json" "fmt" "io" @@ -16,22 +17,15 @@ import ( "github.com/nilslice/cms/content" "github.com/nilslice/cms/management/editor" "github.com/nilslice/cms/management/manager" + "github.com/nilslice/cms/system/admin/config" + "github.com/nilslice/cms/system/admin/user" "github.com/nilslice/cms/system/db" + "github.com/nilslice/jwt" ) // Run adds Handlers to default http listener for Admin func Run() { - http.HandleFunc("/admin", func(res http.ResponseWriter, req *http.Request) { - adminView, err := Admin(nil) - if err != nil { - fmt.Println(err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - res.Header().Set("Content-Type", "text/html") - res.Write(adminView) - }) + http.HandleFunc("/admin", user.Auth(adminHandler)) http.HandleFunc("/admin/static/", func(res http.ResponseWriter, req *http.Request) { path := req.URL.Path @@ -49,17 +43,151 @@ func Run() { http.ServeFile(res, req, filepath.Join(filePathParts...)) }) - http.HandleFunc("/admin/configure", func(res http.ResponseWriter, req *http.Request) { - adminView, err := Admin(nil) - if err != nil { - fmt.Println(err) - res.WriteHeader(http.StatusInternalServerError) + http.HandleFunc("/admin/init", func(res http.ResponseWriter, req *http.Request) { + if db.SystemInitComplete() { + http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin", http.StatusFound) return } - res.Header().Set("Content-Type", "text/html") - res.Write(adminView) + switch req.Method { + case http.MethodGet: + view, err := Init() + if err != nil { + fmt.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 { + fmt.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) + + err = db.SetConfig(req.Form) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + email := req.FormValue("email") + password := req.FormValue("password") + usr := user.NewUser(email, password) + + _, err = db.SetUser(usr) + if err != nil { + fmt.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, + }) + + redir := strings.TrimSuffix(req.URL.String(), "/init") + http.Redirect(res, req, redir, http.StatusFound) + + default: + res.WriteHeader(http.StatusMethodNotAllowed) + } + }) + + http.HandleFunc("/admin/login", loginHandler) + http.HandleFunc("/admin/logout", logoutHandler) + + http.HandleFunc("/admin/configure", func(res http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + data, err := db.ConfigAll() + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + c := &config.Config{} + + err = json.Unmarshal(data, c) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + cfg, err := c.MarshalEditor() + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + adminView, err := Admin(cfg) + if err != nil { + fmt.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 { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + err = db.SetConfig(req.Form) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + http.Redirect(res, req, req.URL.String(), http.StatusFound) + + default: + res.WriteHeader(http.StatusMethodNotAllowed) + } + + }) + + http.HandleFunc("/admin/configure/users", func(res http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + // list all users and delete buttons + + case http.MethodPost: + // create new user + + default: + res.WriteHeader(http.StatusMethodNotAllowed) + } }) http.HandleFunc("/admin/posts", func(res http.ResponseWriter, req *http.Request) { @@ -69,20 +197,20 @@ func Run() { res.WriteHeader(http.StatusBadRequest) } - posts := db.GetAll(t) + posts := db.ContentAll(t) b := &bytes.Buffer{} p := content.Types[t]().(editor.Editable) - html := `<div class="col s9"> - <div class="card"> - <ul class="card-content collection posts"> - <div class="card-title">` + t + ` Items</div>` + html := `<div class="col s9 card"> + <div class="card-content row"> + <div class="card-title">` + t + ` Items</div> + <ul class="posts">` for i := range posts { json.Unmarshal(posts[i], &p) - post := `<div class="row collection-item"><li class="col s12 collection-item"><a href="/admin/edit?type=` + + post := `<li class="col s12"><a href="/admin/edit?type=` + t + `&id=` + fmt.Sprintf("%d", p.ContentID()) + - `">` + p.ContentName() + `</a></li></div>` + `">` + p.ContentName() + `</a></li>` b.Write([]byte(post)) } @@ -116,7 +244,7 @@ func Run() { post := contentType() if i != "" { - data, err := db.Get(t + ":" + i) + data, err := db.Content(t + ":" + i) if err != nil { fmt.Println(err) res.WriteHeader(http.StatusInternalServerError) @@ -208,7 +336,7 @@ func Run() { req.PostForm.Del(discardKey) } - id, err := db.Set(t+":"+cid, req.PostForm) + id, err := db.SetContent(t+":"+cid, req.PostForm) if err != nil { fmt.Println(err) res.WriteHeader(http.StatusInternalServerError) @@ -221,6 +349,9 @@ func Run() { sid := fmt.Sprintf("%d", id) desURL := scheme + host + path + "?type=" + t + "&id=" + sid http.Redirect(res, req, desURL, http.StatusFound) + + default: + res.WriteHeader(http.StatusMethodNotAllowed) } }) diff --git a/system/admin/static/dashboard/css/admin.css b/system/admin/static/dashboard/css/admin.css index cf8a84b..413edd5 100644 --- a/system/admin/static/dashboard/css/admin.css +++ b/system/admin/static/dashboard/css/admin.css @@ -8,16 +8,60 @@ margin: auto; } +.nav-wrapper a:hover { + color: inherit !important; +} + .admin-ui { width: 95%; max-width: 1300px; margin: 1% auto; } +.init { + float: none !important; + margin: auto !important; +} + .manager { margin-bottom: 2%; } +.left-nav .row li a { + padding: 10px 0; + transition: all 0.3s ease; +} + +.left-nav .card-title { + margin-top: 10px !important; +} + +.left-nav .card-title:first-child { + margin-top: 0px !important; +} + +ul.posts li { + display: block; + margin: 0 0 20px 0; + padding: 0 0 20px 0 !important; + border-bottom: solid 1px #e0e0e0; +} + +ul.posts li:last-child { + border-bottom: none; + margin: 0 !important; + padding: 0 !important; +} + +li a { + transition: all 0.3s ease; +} + +li a:hover { + color: #333; + transition: all 0.3s ease; +} + a.new-post { margin: 0.5rem 0 1rem 0.75rem; } diff --git a/system/admin/user/auth.go b/system/admin/user/auth.go new file mode 100644 index 0000000..36f5e40 --- /dev/null +++ b/system/admin/user/auth.go @@ -0,0 +1,106 @@ +package user + +import ( + "encoding/base64" + "fmt" + "net/http" + + "github.com/nilslice/jwt" + "github.com/nilslice/rand" + "golang.org/x/crypto/bcrypt" +) + +// User defines a admin user in the system +type User struct { + ID int `json:"id"` + Email string `json:"email"` + Hash string `json:"hash"` + Salt string `json:"salt"` +} + +// NewUser creates a user +func NewUser(email, password string) *User { + salt := salt128() + hash := encryptPassword([]byte(password), salt) + + user := &User{ + Email: email, + Hash: string(hash), + Salt: base64.StdEncoding.EncodeToString(salt), + } + + return user +} + +// Auth is HTTP middleware to ensure the request has proper token credentials +func Auth(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + redir := req.URL.Scheme + req.URL.Host + "/admin/login" + + if IsValid(req) { + next.ServeHTTP(res, req) + } else { + http.Redirect(res, req, redir, http.StatusFound) + } + }) +} + +// IsValid checks if the user request is authenticated +func IsValid(req *http.Request) bool { + // check if token exists in cookie + cookie, err := req.Cookie("_token") + if err != nil { + return false + } + // validate it and allow or redirect request + token := cookie.Value + return jwt.Passes(token) +} + +// IsUser checks for consistency in email/pass combination +func IsUser(usr *User, password string) bool { + fmt.Println(usr, password) + salt, err := base64.StdEncoding.DecodeString(usr.Salt) + if err != nil { + return false + } + + err = comparePassword([]byte(usr.Hash), []byte(password), salt) + if err != nil { + return false + } + + return true +} + +// The following functions are from github.com/sluu99/um ----------------------- + +// salt128 generates 128 bits of random data. +func salt128() []byte { + x := make([]byte, 16) + rand.Read(x) + return x +} + +// makePassword makes the actual password from the plain password and salt +func makePassword(plainPw, salt []byte) []byte { + password := make([]byte, 0, len(plainPw)+len(salt)) + password = append(password, salt...) + password = append(password, plainPw...) + return password +} + +// encryptPassword uses bcrypt to encrypt a password and salt combination. +// It returns the encrypted password in hex form. +func encryptPassword(plainPw, salt []byte) []byte { + hash, _ := bcrypt.GenerateFromPassword(makePassword(plainPw, salt), 10) + return hash +} + +// comparePassword compares a hash with the plain password and the salt. +// The function returns nil on success or an error on failure. +func comparePassword(hash, plainPw, salt []byte) error { + return bcrypt.CompareHashAndPassword(hash, makePassword(plainPw, salt)) +} + +// End code from github.com/sluu99/um ------------------------------------------ diff --git a/system/api/server.go b/system/api/server.go index 5de10a2..1fb9f02 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -40,7 +40,7 @@ func Run() { return } - posts := db.GetAll(t) + posts := db.ContentAll(t) var all = []json.RawMessage{} for _, post := range posts { all = append(all, post) @@ -66,7 +66,7 @@ func Run() { return } - post, err := db.Get(t + ":" + id) + post, err := db.Content(t + ":" + id) if err != nil { res.WriteHeader(http.StatusInternalServerError) return @@ -123,9 +123,9 @@ func toJSON(data []string) ([]byte, error) { func wrapJSON(json []byte) []byte { var buf = &bytes.Buffer{} - buf.Write([]byte("{data:")) + buf.Write([]byte(`{"data":`)) buf.Write(json) - buf.Write([]byte("}")) + buf.Write([]byte(`}`)) return buf.Bytes() } diff --git a/system/db/query.go b/system/db/query.go deleted file mode 100644 index 480946f..0000000 --- a/system/db/query.go +++ /dev/null @@ -1,207 +0,0 @@ -package db - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "net/url" - "strconv" - "strings" - - "github.com/boltdb/bolt" - "github.com/gorilla/schema" - "github.com/nilslice/cms/content" - "github.com/nilslice/cms/management/editor" - "github.com/nilslice/cms/management/manager" -) - -var store *bolt.DB - -// Init creates a db connection and initializes db with required info -func Init() { - var err error - store, err = bolt.Open("store.db", 0666, nil) - if err != nil { - log.Fatal(err) - } - - // initialize db with all content type buckets - store.Update(func(tx *bolt.Tx) error { - for t := range content.Types { - _, err := tx.CreateBucketIfNotExists([]byte(t)) - if err != nil { - return err - } - } - - return nil - }) - -} - -// Set inserts or updates values in the database. -// The `target` argument is a string made up of namespace:id (string:int) -func Set(target string, data url.Values) (int, error) { - t := strings.Split(target, ":") - ns, id := t[0], t[1] - - // check if content id == -1 (indicating new post). - // if so, run an insert which will assign the next auto incremented int. - // this is done because boltdb begins its bucket auto increment value at 0, - // which is the zero-value of an int in the Item struct field for ID. - // this is a problem when the original first post (with auto ID = 0) gets - // overwritten by any new post, originally having no ID, defauting to 0. - if id == "-1" { - return insert(ns, data) - } - - return update(ns, id, data) -} - -func update(ns, id string, data url.Values) (int, error) { - cid, err := strconv.Atoi(id) - if err != nil { - return 0, err - } - - err = store.Update(func(tx *bolt.Tx) error { - b, err := tx.CreateBucketIfNotExists([]byte(ns)) - if err != nil { - return err - } - - j, err := toJSON(ns, data) - if err != nil { - return err - } - - err = b.Put([]byte(fmt.Sprintf("%d", cid)), j) - if err != nil { - return err - } - - return nil - }) - if err != nil { - return 0, nil - } - - return cid, nil -} - -func insert(ns string, data url.Values) (int, error) { - var effectedID int - err := store.Update(func(tx *bolt.Tx) error { - b, err := tx.CreateBucketIfNotExists([]byte(ns)) - if err != nil { - return err - } - - // get the next available ID and convert to string - // also set effectedID to int of ID - id, err := b.NextSequence() - if err != nil { - return err - } - cid := strconv.FormatUint(id, 10) - effectedID, err = strconv.Atoi(cid) - if err != nil { - return err - } - data.Add("id", cid) - - j, err := toJSON(ns, data) - if err != nil { - return err - } - - err = b.Put([]byte(cid), j) - if err != nil { - return err - } - - return nil - }) - if err != nil { - return 0, err - } - - return effectedID, nil -} - -func toJSON(ns string, data url.Values) ([]byte, error) { - // find the content type and decode values into it - t, ok := content.Types[ns] - if !ok { - return nil, fmt.Errorf(content.ErrTypeNotRegistered, ns) - } - post := t() - - dec := schema.NewDecoder() - dec.SetAliasTag("json") // allows simpler struct tagging when creating a content type - dec.IgnoreUnknownKeys(true) // will skip over form values submitted, but not in struct - err := dec.Decode(post, data) - if err != nil { - return nil, err - } - - slug, err := manager.Slug(post.(editor.Editable)) - if err != nil { - return nil, err - } - post.(editor.Editable).SetSlug(slug) - - // marshall content struct to json for db storage - j, err := json.Marshal(post) - if err != nil { - return nil, err - } - - return j, nil -} - -// Get retrives one item from the database. Non-existent values will return an empty []byte -// The `target` argument is a string made up of namespace:id (string:int) -func Get(target string) ([]byte, error) { - t := strings.Split(target, ":") - ns, id := t[0], t[1] - - val := &bytes.Buffer{} - err := store.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(ns)) - _, err := val.Write(b.Get([]byte(id))) - if err != nil { - fmt.Println(err) - return err - } - - return nil - }) - if err != nil { - return nil, err - } - - return val.Bytes(), nil -} - -// GetAll retrives all items from the database within the provided namespace -func GetAll(namespace string) [][]byte { - var posts [][]byte - store.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(namespace)) - - len := b.Stats().KeyN - posts = make([][]byte, 0, len) - - b.ForEach(func(k, v []byte) error { - posts = append(posts, v) - - return nil - }) - - return nil - }) - - return posts -} |