diff --git a/internal/blog/blog-list.go b/internal/blog/blog-list.go index 243168e..a1c0619 100644 --- a/internal/blog/blog-list.go +++ b/internal/blog/blog-list.go @@ -1,8 +1,42 @@ package blog +import ( + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + type BlogList struct { ID string `json:"id" bson:"_id"` Title string `json:"title" bson:"title"` Draft bool `json:"draft" bson:"draft"` CreateTime int64 `json:"createTime" bson:"createTime"` } + +func (b *Backend) FullBlogList(ctx context.Context) ([]BlogList, error) { + res, err := b.blogCol.Find(ctx, bson.M{}, options.Find(). + SetProjection(bson.M{"_id": 1, "createTime": 1, "title": 1, "draft": 1}). + SetSort(bson.M{"createTime": -1})) + if err != nil { + return nil, err + } + var list []BlogList + err = res.All(ctx, &list) + return list, err +} + +func (b *Backend) BlogList(ctx context.Context) ([]BlogList, error) { + res, err := b.blogCol.Find(ctx, bson.M{ + "draft": false, + "staticPage": false, + }, options.Find(). + SetProjection(bson.M{"_id": 1, "createTime": 1, "title": 1, "draft": 1}). + SetSort(bson.M{"createTime": -1})) + if err != nil { + return nil, err + } + var list []BlogList + err = res.All(ctx, &list) + return list, err +} diff --git a/internal/blog/blog.go b/internal/blog/blog.go index 7241c3d..bced496 100644 --- a/internal/blog/blog.go +++ b/internal/blog/blog.go @@ -1,5 +1,17 @@ package blog +import ( + "bytes" + "context" + "log" + "net/http" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + type Blog struct { ID string `json:"id" bson:"_id"` Author string `json:"author" bson:"author"` @@ -12,3 +24,202 @@ type Blog struct { CreateTime int64 `json:"createTime" bson:"createTime"` UpdateTime int64 `json:"updateTime" bson:"updateTime"` } + +func (b *Backend) GetBlog(ctx context.Context, ID string, allowDraft bool) (Blog, error) { + filter := bson.M{"_id": ID} + if !allowDraft { + filter["draft"] = false + } + res := b.blogCol.FindOne(ctx, filter) + if res.Err() != nil { + return Blog{}, res.Err() + } + var out Blog + err := res.Decode(&out) + return out, err +} + +func (b *Backend) postBlogReq(w http.ResponseWriter, r *http.Request) { + usr := b.verifyEditorCookie(r) + if usr == nil { + redirect(w, r, "/login") + return + } + if usr.Perm["blog"] != "admin" && usr.Perm["blog"] != "writer" { + w.Write([]byte("

Sorry, but you aren't authorized to do this action.

")) + return + } + err := r.ParseForm() + if err != nil { + w.Write([]byte("

Error decoding form

")) + return + } + newBlog := Blog{ + ID: r.FormValue("id"), + Title: r.FormValue("title"), + RawBlog: r.FormValue("blog"), + StaticPage: r.FormValue("staticPage") == "on", + Draft: r.FormValue("draft") == "on", + UpdateTime: time.Now().Unix(), + } + if newBlog.Title == "" || newBlog.RawBlog == "" { + w.Write([]byte("

Title and blog contents are required

")) + return + } + if newBlog.ID == "" { + newBlog.ID = strings.ToLower(strings.ReplaceAll(newBlog.Title, " ", "-")) + newBlog.CreateTime = newBlog.UpdateTime + newBlog.Author = usr.Username + _, err = b.blogCol.InsertOne(r.Context(), newBlog) + if mongo.IsDuplicateKeyError(err) { + w.Write([]byte("

Title already exists

")) + return + } else if err != nil { + log.Println("error inserting document") + w.Write([]byte("

Server error inserting document

")) + return + } + b.blogSuccessFullPageReplace(r.Context(), w, newBlog) + return + } + res := b.blogCol.FindOne(r.Context(), bson.M{"_id": newBlog.ID}) + if res.Err() == mongo.ErrNoDocuments { + w.Write([]byte("

Error finding old document

")) + return + } else if res.Err() != nil { + log.Println("error getting old blog for update:", res.Err()) + w.Write([]byte("

Server error!

")) + return + } + var oldBlog Blog + err = res.Decode(&oldBlog) + if err != nil { + log.Println("error decoding old blog for update:", res.Err()) + w.Write([]byte("

Server error!

")) + return + } + newBlog.CreateTime = oldBlog.CreateTime + if oldBlog.Title != newBlog.Title { + res = b.blogCol.FindOne(r.Context(), bson.M{"_id": strings.ToLower(strings.ReplaceAll(newBlog.Title, " ", "-"))}) + if res.Err() == nil { + w.Write([]byte("

Title already exists

")) + return + } else if res.Err() != mongo.ErrNoDocuments { + log.Println("error checking for title existance:", res.Err()) + w.Write([]byte("

Server error!

")) + return + } + res = b.blogCol.FindOneAndDelete(r.Context(), bson.M{"_id": oldBlog.ID}) + if res.Err() != nil { + log.Println("error deleting old blog:", res.Err()) + w.Write([]byte("

Server error!

")) + return + } + newBlog.ID = strings.ToLower(strings.ReplaceAll(newBlog.Title, " ", "-")) + _, err = b.blogCol.InsertOne(r.Context(), newBlog) + if err != nil { + log.Println("error inserting document") + w.Write([]byte("

Server error inserting document

")) + return + } + b.blogSuccessFullPageReplace(r.Context(), w, newBlog) + return + } + _, err = b.blogCol.UpdateByID(r.Context(), newBlog.ID, bson.M{ + "updateTime": newBlog.UpdateTime, + "blog": newBlog.RawBlog, + "staticPage": newBlog.StaticPage, + "draft": newBlog.Draft, + }) + if err != nil { + log.Println("error updating blog:", err) + w.Write([]byte("

Server error inserting document

")) + } else { + w.Write([]byte("

Successfully updated

")) + } +} + +func (b *Backend) blogSuccessFullPageReplace(ctx context.Context, w http.ResponseWriter, blog Blog) { + form, err := b.BlogEditForm(blog) + if err != nil { + log.Println("error with blogForm template:", err) + w.Write([]byte("

Success, but error reloading page

")) + return + } + out, err := b.BlogEditPage(ctx, blog.ID, form) + if err != nil { + log.Println("error with getting blog list:", err) + w.Write([]byte("

Success, but error reloading page

")) + return + } + w.Header().Set("Hx-Retarget", "#editPage") + w.Write([]byte(out)) +} + +func (b *Backend) BlogEditForm(blog Blog) (string, error) { + form := new(bytes.Buffer) + err := b.tmpl.ExecuteTemplate(form, "blogForm", blogFormStruct{ + Blog: blog, + Result: "

Success!!

", + }) + return form.String(), err +} + +func (b *Backend) blogFormReq(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Hx-Request") != "true" { + redirect(w, r, "/editor") + return + } + blogID := r.URL.Query().Get("blog") + var blog Blog + if blogID != "" { + var err error + blog, err = b.GetBlog(r.Context(), blogID, true) + if err != nil { + log.Println("error getting blog:", err) + w.Write([]byte("

Server error!

")) + return + } + } + form, err := b.BlogEditForm(blog) + if err != nil { + log.Println("error using blogForm template:", err) + w.Write([]byte("

Server error!

")) + return + } + w.Write([]byte(form)) +} + +func (b *Backend) BlogEditPage(ctx context.Context, selectedID, editor string) (string, error) { + blogs, err := b.FullBlogList(ctx) + if err != nil { + return "", err + } + out := new(bytes.Buffer) + err = b.tmpl.ExecuteTemplate(out, "blogPage", blogPageStruct{ + Selected: selectedID, + Editor: editor, + Blogs: blogs, + }) + return out.String(), err +} + +func (b *Backend) blogPageReq(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Hx-Request") != "true" { + redirect(w, r, "/editor") + return + } + form, err := b.BlogEditForm(Blog{}) + if err != nil { + log.Println("error using blogForm template:", err) + w.Write([]byte("

Server error!

")) + return + } + page, err := b.BlogEditPage(r.Context(), "", form) + if err != nil { + log.Println("error using blogPage template:", err) + w.Write([]byte("

Server error!

")) + return + } + w.Write([]byte(page)) +} diff --git a/internal/blog/edit-page.go b/internal/blog/edit-page.go deleted file mode 100644 index 2efa55d..0000000 --- a/internal/blog/edit-page.go +++ /dev/null @@ -1,9 +0,0 @@ -package blog - -import "net/http" - - -func (b *Backend) editorPage(w http.ResponseWriter, r *http.Request) { - pag := r.PathValue("page") - if -} diff --git a/internal/blog/editor.go b/internal/blog/editor.go new file mode 100644 index 0000000..5cc05ea --- /dev/null +++ b/internal/blog/editor.go @@ -0,0 +1,47 @@ +package blog + +import ( + "bytes" + "log" + "net/http" +) + +func (b *Backend) editorReq(w http.ResponseWriter, r *http.Request) { + blogForm := new(bytes.Buffer) + err := b.tmpl.ExecuteTemplate(blogForm, "blogForm", blogFormStruct{ + Blog: Blog{}, + }) + if err != nil { + log.Println("error using blogForm:", err) + b.wrapper(w, r, "error", "

Server error

") + return + } + blogs, err := b.FullBlogList(r.Context()) + if err != nil { + log.Println("error getting blog list:", err) + b.wrapper(w, r, "error", "

Server error

") + return + } + blogPage := new(bytes.Buffer) + err = b.tmpl.ExecuteTemplate(blogPage, "blogPage", blogPageStruct{ + Selected: "", + Editor: blogForm.String(), + Blogs: blogs, + }) + if err != nil { + log.Println("error using blogPage:", err) + b.wrapper(w, r, "error", "

Server error

") + return + } + out := new(bytes.Buffer) + err = b.tmpl.ExecuteTemplate(out, "editor", editorStruct{ + SelectedPage: "", + Page: blogPage.String(), + }) + if err != nil { + log.Println("error using editor:", err) + b.wrapper(w, r, "error", "

Server error

") + return + } + b.wrapper(w, r, "Editor", out.String()) +} diff --git a/internal/blog/main.go b/internal/blog/main.go index 2663f5f..8c6ce68 100644 --- a/internal/blog/main.go +++ b/internal/blog/main.go @@ -4,36 +4,78 @@ import ( "log" "net/http" "sync" + "text/template" + + "github.com/CalebQ42/darkstorm-server/internal/backend" + "go.mongodb.org/mongo-driver/mongo" ) -type HTMXReturner func(http.ResponseWriter, *http.Request) (string, error) +type WrapperFunc func(w http.ResponseWriter, r *http.Request, title, content string) type Backend struct { + blogCol *mongo.Collection + authCol *mongo.Collection + projCol *mongo.Collection + + tmpl *template.Template + wrapper WrapperFunc + + back *backend.Backend + cacheMutex sync.RWMutex cache map[string]string } -func (b *Backend) AddToMux(mux *http.ServeMux) { - mux.HandleFunc("GET /editor/{page}", b.editorPage) +func New(c *mongo.Client, back *backend.Backend, wrapper WrapperFunc) (*Backend, error) { + var b = &Backend{ + blogCol: c.Database("blog").Collection("blog"), + authCol: c.Database("blog").Collection("blog"), + projCol: c.Database("blog").Collection("blog"), + wrapper: wrapper, + back: back, + cache: make(map[string]string), + } + return b, b.parseTemplates() } -func (b *Backend) cacheMiddleware(h HTMXReturner) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b.cacheMutex.RLock() - if pag, ok := b.cache[r.URL.EscapedPath()]; ok { - w.Write([]byte(pag)) - b.cacheMutex.RUnlock() - return +func (b *Backend) RegisterToMux(mux *http.ServeMux) { + mux.HandleFunc("GET /editor", b.editorReq) + + mux.HandleFunc("GET /editor/blog", b.blogPageReq) + mux.HandleFunc("GET /editor/blog/edit", b.blogFormReq) + mux.HandleFunc("POST /editor/blog/post", b.postBlogReq) + + // mux.HandleFunc("GET /editor/portfolio", b.portfolioPageReq) + // mux.HandleFunc("GET /editor/portfolio/edit", b.portfolioFormReq) + // mux.HandleFunc("POST /editor/portfolio/post", b.postPortfolioReq) + + // mux.HandleFunc("GET /editor/author", b.authorPageReq) + // mux.HandleFunc("GET /editor/author/edit", b.authorFormReq) + // mux.HandleFunc("POST /editor/author/post", b.postAuthorReq) +} + +func (b *Backend) verifyEditorCookie(r *http.Request) *backend.User { + authCookie, err := r.Cookie("blogAuthToken") + if err != nil { + if err != http.ErrNoCookie { + log.Println("error getting auth cookie:", err) } - b.cacheMutex.RUnlock() - b.cacheMutex.Lock() - defer b.cacheMutex.Unlock() - res, err := h(w, r) - if err != nil { - log.Printf("error getting %v: %v", r.URL.EscapedPath(), err) - } else { - b.cache[r.URL.EscapedPath()] = res + return nil + } + usr, err := b.back.VerifyUser(r.Context(), authCookie.Value) + if err != nil { + if err != backend.ErrTokenUnauthorized { + log.Println("error authorizing JWT token:", err) } - w.Write([]byte(res)) - }) + return nil + } + return usr +} + +func redirect(w http.ResponseWriter, r *http.Request, path string) { + if r.Header.Get("HX-Request") == "true" { + w.Header().Set("HX-Location", `{"path": "`+path+`", "target":"#content"}`) + return + } + http.Redirect(w, r, "https://darkstorm.tech"+path, http.StatusFound) } diff --git a/internal/blog/templates.go b/internal/blog/templates.go index aba121b..a832dac 100644 --- a/internal/blog/templates.go +++ b/internal/blog/templates.go @@ -1,8 +1,12 @@ package blog +import ( + "text/template" +) + const editor = `

- Blogs + Blogs Portfolio Author

@@ -33,6 +37,7 @@ type blogPageStruct struct { Blogs []BlogList } +// TODO: Add delete const blogForm = `
@@ -150,3 +155,33 @@ type authorFormStruct struct { Author Author Result string } + +func (b *Backend) parseTemplates() error { + var err error + b.tmpl, err = template.New("editor").Parse(editor) + if err != nil { + return err + } + b.tmpl, err = template.New("blogPage").Parse(blogPage) + if err != nil { + return err + } + b.tmpl, err = template.New("blogForm").Parse(blogForm) + if err != nil { + return err + } + b.tmpl, err = template.New("portfolioPage").Parse(portfolioPage) + if err != nil { + return err + } + b.tmpl, err = template.New("portfolioForm").Parse(portfolioForm) + if err != nil { + return err + } + b.tmpl, err = template.New("authorPage").Parse(authorPage) + if err != nil { + return err + } + b.tmpl, err = template.New("authorForm").Parse(authorForm) + return err +}