diff --git a/internal/backend/darkstorm.go b/internal/backend/main.go
similarity index 100%
rename from internal/backend/darkstorm.go
rename to internal/backend/main.go
diff --git a/internal/blog-old/README.md b/internal/blog-old/README.md
new file mode 100644
index 0000000..bf5728b
--- /dev/null
+++ b/internal/blog-old/README.md
@@ -0,0 +1,189 @@
+# Blog module
+
+A simple blog module for darkstorm-backend.
+
+## Requests
+
+### Author info
+
+#### Get author info
+
+> GET /author/{authorID}
+
+```json
+{
+ id: "authorID",
+ name: "author name",
+ about: "about",
+ picurl: "picture URL"
+}
+```
+
+#### Update author info
+
+> POST /author/{authorID}
+
+Must have a auth token for a user with the `"blog": "admin"` permission.
+
+```json
+{
+ name: "author name",
+ about: "about",
+ picurl: "picture url"
+}
+```
+
+#### Add Author info
+
+> POST /author
+
+Must have a auth token for a user with the `"blog": "admin"` permission.
+
+```json
+{
+ name: "author name",
+ about: "about",
+ picurl: "picture URL"
+}
+```
+
+### Blog
+
+#### Specific blog
+
+> GET /blog/{blogID}
+
+Return:
+
+```json
+{
+ id: "blogID",
+ staticPage: false, // static pages don't show up alongside other blog pages.
+ createTime: 0, // creation time in Unix format
+ updateTime: 0, // last update time in Unix format
+ author: "authorID",
+ favicon: "favicon url",
+ title: "blog title",
+ blog: "blog", // blog will have been converted to HTML
+}
+```
+
+#### Create blog
+
+Request:
+
+> POST /blog
+
+Must have a auth token for a user with the `"blog": "admin"` permission.
+
+```json
+{
+ favicon: "favicon url",
+ title: "blog title",
+ blog: "blog", // blog will have been converted to HTML
+}
+```
+
+Return:
+
+```json
+{
+ id: "blogID"
+}
+```
+
+#### Update blog
+
+Request:
+
+> POST /blog/{blogID}
+
+Must have a auth token for a user with the `"blog": "admin"` permission.
+
+```json
+{
+ favicon: "new icon",
+ title: "new title",
+ blog: "new blog content"
+}
+```
+
+#### Latest blogs
+
+> GET /blog?page=0
+
+Will return up to 5 blogs. `page` query is optional (implies 0 if not set).
+
+Return:
+
+```json
+{
+ num: 1, // Number of returned results, returns up to 5 results
+ blogs: [
+ {
+ id: "blogID",
+ createTime: 0, // creation time in Unix format
+ updateTime: 0, // last update time in Unix format
+ author: "authorID",
+ favicon: "favicon url",
+ title: "blog title",
+ blog: "blog", // blog will have been converted to HTML
+ }
+ ...
+ ]
+}
+```
+
+#### Blog List
+
+> GET /blog/list?page=0
+
+Will return up to 50 IDs. `page` query is optional (implies 0 if not set).
+
+Return:
+
+```json
+{
+ num: 1, // Number of returned results, returns up to 50 results
+ blogList: [
+ {
+ id: "blogID",
+ createTime: 0, // Unix format
+ },
+ {
+ id: "blogID",
+ createTime: 0, // Unix format
+ },
+ ...
+ ]
+}
+```
+
+### Portfolio
+
+#### Get Projects
+
+> GET: /portfolio?lang=go
+
+Return:
+
+```json
+[
+ {
+ title: "Darkstorm Server",
+ order: 0,
+ repository: "https://github.com/CalebQ42/darkstorm-server",
+ description: "The backend that runs runs my website and APIs",
+ technologies: [ // May be empty
+ "MongoDB",
+ "RESTful API"
+ ],
+ language: [
+ {
+ language: "go",
+ dates: "September 2021"
+ }
+ ]
+ }
+]
+```
diff --git a/internal/blog-old/author.go b/internal/blog-old/author.go
new file mode 100644
index 0000000..b553190
--- /dev/null
+++ b/internal/blog-old/author.go
@@ -0,0 +1,164 @@
+package blog
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/CalebQ42/darkstorm-server/internal/backend"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+const authorInfo = `
+
+
+ %v %v
+
`
+
+type Author struct {
+ ID string `json:"id" bson:"_id"`
+ Name string `json:"name" bson:"name"`
+ About string `json:"about" bson:"about"`
+ PicURL string `json:"picurl" bson:"picurl"`
+}
+
+func (a Author) HTML() string {
+ return fmt.Sprintf(authorInfo, a.PicURL, a.Name+"'s profile picture", a.Name, a.About)
+}
+
+func (b *BlogApp) AboutMe(ctx context.Context) (*Author, error) {
+ res := b.authCol.FindOne(ctx, bson.M{"_id": "caleb_gardner"})
+ if res.Err() != nil {
+ log.Println("error getting about me:", res.Err())
+ if res.Err() == mongo.ErrNoDocuments {
+ return nil, backend.ErrNotFound
+ }
+ return nil, res.Err()
+ }
+ var aboutMe Author
+ err := res.Decode(&aboutMe)
+ if err != nil {
+ log.Println("error decoding about me:", res)
+ return nil, err
+ }
+ return &aboutMe, nil
+}
+
+func (b *BlogApp) reqAuthorInfo(w http.ResponseWriter, r *http.Request) {
+ res := b.authCol.FindOne(r.Context(), r.PathValue("authorID"))
+ if res.Err() == mongo.ErrNoDocuments {
+ backend.ReturnError(w, http.StatusNotFound, "notFound", "Author with ID "+r.PathValue("authorID")+" not found")
+ return
+ } else if res.Err() != nil {
+ log.Println("error getting author info:", res.Err())
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ return
+ }
+ var auth Author
+ err := res.Decode(&auth)
+ if err != nil {
+ log.Println("error decoding author info:", err)
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ return
+ }
+ json.NewEncoder(w).Encode(auth)
+}
+
+func (b *BlogApp) addAuthorInfo(w http.ResponseWriter, r *http.Request) {
+ hdr, err := b.back.VerifyHeader(w, r, "blogManagement", false)
+ if hdr == nil {
+ if err != nil {
+ log.Println("request key parsing error:", err)
+ }
+ return
+ } else if hdr.Key.AppID != "blog" {
+ backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application is unauthorized")
+ return
+ }
+ if hdr.User == nil || hdr.User.Perm["blog"] != "admin" {
+ backend.ReturnError(w, http.StatusUnauthorized, "unauthorized", "Application is unauthorized")
+ return
+ }
+ var newAuth Author
+ err = json.NewDecoder(r.Body).Decode(&newAuth)
+ r.Body.Close()
+ if err != nil {
+ backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Invalid request")
+ return
+ }
+ for i := 1; ; i++ {
+ newID := strings.ReplaceAll(newAuth.Name, " ", "-")
+ if i != 1 {
+ newID += strconv.Itoa(i)
+ }
+ collisionCheck := b.authCol.FindOne(r.Context(), bson.M{"name": newAuth.Name})
+ if collisionCheck.Err() == mongo.ErrNoDocuments {
+ newAuth.ID = newID
+ break
+ } else if collisionCheck.Err() != nil {
+ log.Println("error checking for new author ID collisions:", err)
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ return
+ }
+ }
+ _, err = b.authCol.InsertOne(r.Context(), newAuth)
+ if err != nil {
+ log.Println("error inserting new author:", err)
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ return
+ }
+ w.WriteHeader(http.StatusCreated)
+}
+
+func (b *BlogApp) updateAuthorInfo(w http.ResponseWriter, r *http.Request) {
+ hdr, err := b.back.VerifyHeader(w, r, "blogManagement", false)
+ if hdr == nil {
+ if err != nil {
+ log.Println("request key parsing error:", err)
+ }
+ return
+ } else if hdr.Key.AppID != "blog" {
+ backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application is unauthorized")
+ return
+ }
+ if hdr.User == nil || hdr.User.Perm["blog"] != "admin" {
+ backend.ReturnError(w, http.StatusUnauthorized, "unauthorized", "Application is unauthorized")
+ return
+ }
+ var rawUpd map[string]string
+ err = json.NewDecoder(r.Body).Decode(&rawUpd)
+ r.Body.Close()
+ if err != nil {
+ backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Invalid request")
+ return
+ }
+ actlUpd := make(map[string]string)
+ if rawUpd["name"] != "" {
+ actlUpd["name"] = rawUpd["name"]
+ }
+ if rawUpd["about"] != "" {
+ actlUpd["about"] = rawUpd["about"]
+ }
+ if rawUpd["picurl"] != "" {
+ actlUpd["picurl"] = rawUpd["picurl"]
+ }
+ res, err := b.authCol.UpdateByID(r.Context(), r.PathValue("authorID"), actlUpd)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ backend.ReturnError(w, http.StatusNotFound, "notFound", "Blog with ID "+r.PathValue("blogID")+" not found")
+ } else {
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ }
+ return
+ }
+ if res.MatchedCount == 0 {
+ backend.ReturnError(w, http.StatusNotFound, "notFound", "Blog with ID "+r.PathValue("blogID")+" not found")
+ return
+ }
+ w.WriteHeader(http.StatusCreated)
+}
diff --git a/internal/blog-old/blog.go b/internal/blog-old/blog.go
new file mode 100644
index 0000000..6bd8e04
--- /dev/null
+++ b/internal/blog-old/blog.go
@@ -0,0 +1,387 @@
+package blog
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/CalebQ42/darkstorm-server/internal/backend"
+ "github.com/google/uuid"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ blogTitle = ""
+ blogAuthor = "By %v "
+ blogCreate = "Written on: %v "
+ blogMain = "%v
"
+)
+
+type Blog struct {
+ ID string `json:"id" bson:"_id"`
+ Author string `json:"author" bson:"author"`
+ Favicon string `json:"favicon" bson:"favicon"`
+ Title string `json:"title" bson:"title"`
+ RawBlog string `json:"blog" bson:"blog"`
+ HTMLBlog string `json:"-" bson:"-"`
+ StaticPage bool `json:"staticPage" bson:"staticPage"`
+ Draft bool `json:"draft" bson:"draft"`
+ CreateTime int64 `json:"createTime" bson:"createTime"`
+ UpdateTime int64 `json:"updateTime" bson:"updateTime"`
+}
+
+func (b *Blog) HTMX(blogApp *BlogApp, ctx context.Context) string {
+ if b.StaticPage {
+ return b.RawBlog
+ }
+ out := fmt.Sprintf(blogTitle, b.ID, b.ID, b.Title)
+ auth, err := blogApp.GetAuthor(ctx, b)
+ if err == nil {
+ out += fmt.Sprintf(blogAuthor, auth.Name)
+ } else {
+ out += fmt.Sprintf(blogAuthor, "unknown")
+ }
+ cTime := time.Unix(b.CreateTime, 0).Format(time.DateOnly)
+ if b.UpdateTime > b.CreateTime {
+ out += fmt.Sprintf(blogCreate, cTime+"; Last updated on: "+time.Unix(b.UpdateTime, 0).Format(time.DateOnly))
+ } else {
+ out += fmt.Sprintf(blogCreate, cTime)
+ }
+ out += fmt.Sprintf(blogMain, b.HTMLBlog)
+ if err == nil {
+ out += "About the author: " + auth.HTML()
+ }
+ return out
+}
+
+func (b *BlogApp) ConvertBlog(blog *Blog) {
+ if !blog.StaticPage {
+ blog.HTMLBlog = b.conv.HTMLConvert(blog.RawBlog)
+ }
+}
+
+func (b *BlogApp) GetAuthor(ctx context.Context, blog *Blog) (*Author, error) {
+ res := b.authCol.FindOne(ctx, bson.M{"_id": blog.Author})
+ if res.Err() != nil {
+ if res.Err() == mongo.ErrNoDocuments {
+ return nil, backend.ErrNotFound
+ }
+ return nil, res.Err()
+ }
+ var author Author
+ err := res.Decode(&author)
+ return &author, err
+}
+
+func (b *BlogApp) Blog(ctx context.Context, ID string) (*Blog, error) {
+ b.cacheMutex.RLock()
+ blog, has := b.blogCache[ID]
+ b.cacheMutex.RUnlock()
+ if has {
+ return &blog, nil
+ }
+ res := b.blogCol.FindOne(ctx, bson.M{"_id": ID, "draft": false})
+ if res.Err() != nil {
+ if res.Err() == mongo.ErrNoDocuments {
+ return nil, backend.ErrNotFound
+ }
+ return nil, res.Err()
+ }
+ err := res.Decode(&blog)
+ if err != nil {
+ return nil, err
+ }
+ b.ConvertBlog(&blog)
+ b.cacheMutex.Lock()
+ b.blogCache[ID] = blog
+ b.cacheMutex.Unlock()
+ go b.CleanCache(ID)
+ return &blog, nil
+}
+
+func (b *BlogApp) AnyBlog(ctx context.Context, ID string) (*Blog, error) {
+ res := b.blogCol.FindOne(ctx, bson.M{"_id": ID})
+ if res.Err() != nil {
+ if res.Err() == mongo.ErrNoDocuments {
+ return nil, backend.ErrNotFound
+ }
+ return nil, res.Err()
+ }
+ var blog Blog
+ err := res.Decode(&blog)
+ if err != nil {
+ return nil, err
+ }
+ b.ConvertBlog(&blog)
+ return &blog, nil
+}
+
+func (b *BlogApp) Contains(ctx context.Context, ID string) bool {
+ res := b.blogCol.FindOne(ctx, bson.M{"_id": ID})
+ return res.Err() == nil
+}
+
+func (b *BlogApp) CleanCache(ID string) {
+ time.Sleep(5 * time.Minute)
+ b.cacheMutex.Lock()
+ delete(b.blogCache, ID)
+ b.cacheMutex.Unlock()
+}
+
+func (b *BlogApp) reqBlog(w http.ResponseWriter, r *http.Request) {
+ blogID := r.PathValue("blogID")
+ if blogID == "" {
+ backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Must provide a blogID")
+ return
+ }
+ blog, err := b.Blog(r.Context(), blogID)
+ if err != nil {
+ if err == backend.ErrNotFound {
+ backend.ReturnError(w, http.StatusNotFound, "notFound", "Not blog found with the given ID")
+ return
+ }
+ log.Println("error getting blog:", err)
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
+ return
+ }
+ if r.Header.Get("Hx-Request") == "true" {
+ w.Write([]byte(blog.HTMX(b, r.Context())))
+ } else {
+ json.NewEncoder(w).Encode(blog)
+ }
+}
+
+func (b *BlogApp) createBlog(w http.ResponseWriter, r *http.Request) {
+ hdr, err := b.back.VerifyHeader(w, r, "blogManagement", false)
+ if hdr == nil {
+ if err != nil {
+ log.Println("request key parsing error:", err)
+ }
+ return
+ } else if hdr.Key.AppID != "blog" {
+ backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application is unauthorized")
+ return
+ }
+ if hdr.User == nil || hdr.User.Perm["blog"] != "admin" {
+ backend.ReturnError(w, http.StatusUnauthorized, "unauthorized", "Application is unauthorized")
+ return
+ }
+ var newBlog Blog
+ err = json.NewDecoder(r.Body).Decode(&newBlog)
+ r.Body.Close()
+ if err != nil {
+ backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request")
+ return
+ }
+ id, err := uuid.NewV7()
+ if err != nil {
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ return
+ }
+ tim := time.Now().Unix()
+ newBlog.ID = id.String()
+ newBlog.CreateTime = tim
+ newBlog.UpdateTime = tim
+ newBlog.Author = hdr.User.Username
+ _, err = b.blogCol.InsertOne(r.Context(), newBlog)
+ if err != nil {
+ log.Println("error when inserting new blog:", err)
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ return
+ }
+ w.WriteHeader(http.StatusCreated)
+
+}
+
+func (b *BlogApp) updateBlog(w http.ResponseWriter, r *http.Request) {
+ hdr, err := b.back.VerifyHeader(w, r, "blogManagement", false)
+ if hdr == nil {
+ if err != nil {
+ log.Println("request key parsing error:", err)
+ }
+ return
+ } else if hdr.Key.AppID != "blog" {
+ backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application is unauthorized")
+ return
+ }
+ if hdr.User == nil || hdr.User.Perm["blog"] != "admin" {
+ backend.ReturnError(w, http.StatusUnauthorized, "unauthorized", "Application is unauthorized")
+ return
+ }
+ if r.PathValue("blogID") == "" {
+ backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request")
+ return
+ }
+ var reqUpdRaw map[string]string
+ err = json.NewDecoder(r.Body).Decode(&reqUpdRaw)
+ r.Body.Close()
+ if err != nil {
+ backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request")
+ return
+ }
+ reqUpd := bson.M{}
+ if fav, ok := reqUpdRaw["favicon"]; ok && fav != "" {
+ reqUpd["favicon"] = fav
+ }
+ if titl, ok := reqUpdRaw["title"]; ok && titl != "" {
+ reqUpd["title"] = titl
+ }
+ if blog, ok := reqUpdRaw["blog"]; ok && blog != "" {
+ reqUpd["blog"] = blog
+ }
+ reqUpd["updateTime"] = time.Now().Unix()
+ res, err := b.blogCol.UpdateByID(r.Context(), r.PathValue("blogID"), reqUpd)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ backend.ReturnError(w, http.StatusNotFound, "notFound", "Blog with ID "+r.PathValue("blogID")+" not found")
+ } else {
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ }
+ return
+ }
+ if res.MatchedCount == 0 {
+ backend.ReturnError(w, http.StatusNotFound, "notFound", "Blog with ID "+r.PathValue("blogID")+" not found")
+ return
+ }
+ w.WriteHeader(http.StatusCreated)
+}
+
+func (b *BlogApp) InsertBlog(ctx context.Context, blog Blog) error {
+ _, err := b.blogCol.InsertOne(ctx, blog)
+ return err
+}
+
+func (b *BlogApp) UpdateBlog(ctx context.Context, ID string, updates bson.M) error {
+ _, err := b.blogCol.UpdateByID(ctx, ID, bson.M{"$set": updates})
+ return err
+}
+
+func (b *BlogApp) LatestBlogs(ctx context.Context, page int64) ([]*Blog, error) {
+ res, err := b.blogCol.Find(ctx, bson.M{"staticPage": false, "draft": false}, options.Find().
+ SetSort(bson.M{"createTime": -1}).
+ SetLimit(5).
+ SetSkip(page*5))
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil, backend.ErrNotFound
+ }
+ return nil, err
+ }
+ var out []*Blog
+ err = res.All(ctx, &out)
+ if err != nil {
+ return nil, err
+ }
+ for i := range out {
+ b.ConvertBlog(out[i])
+ }
+ return out, nil
+}
+
+func (b *BlogApp) reqLatestBlogs(w http.ResponseWriter, r *http.Request) {
+ var page int
+ var err error
+ pagQuery := r.URL.Query().Get("page")
+ if pagQuery != "" {
+ page, err = strconv.Atoi(pagQuery)
+ if err != nil {
+ page = 0
+ }
+ }
+ blogs, err := b.LatestBlogs(r.Context(), int64(page))
+ if err != nil && err != backend.ErrNotFound {
+ log.Println("error getting latest blogs:", err)
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "internal error")
+ return
+ }
+ var ret struct {
+ Blogs []*Blog `json:"blogs"`
+ Num int `json:"num"`
+ }
+ ret.Num = len(blogs)
+ ret.Blogs = blogs
+ json.NewEncoder(w).Encode(ret)
+}
+
+type BlogListResult struct {
+ ID string `json:"id" bson:"_id"`
+ Title string `json:"title" bson:"title"`
+ CreateTime int `json:"createTime" bson:"createTime"`
+}
+
+func (b BlogListResult) HTMX() string {
+ return "" + b.Title + " "
+}
+
+func (b *BlogApp) BlogList(ctx context.Context, page int64) ([]BlogListResult, error) {
+ res, err := b.blogCol.Find(ctx, bson.M{"staticPage": false, "draft": false}, options.Find().
+ SetProjection(bson.M{"_id": 1, "createTime": 1, "title": 1}).
+ SetSort(bson.M{"createTime": -1}).
+ SetLimit(50).
+ SetSkip(page*50))
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil, backend.ErrNotFound
+ }
+ return nil, err
+ }
+ var out []BlogListResult
+ err = res.All(ctx, &out)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (b *BlogApp) AllBlogsList(ctx context.Context) ([]BlogListResult, error) {
+ res, err := b.blogCol.Find(ctx, bson.M{}, options.Find().
+ SetProjection(bson.M{"_id": 1, "createTime": 1, "title": 1}).
+ SetSort(bson.M{"createTime": -1}))
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil, backend.ErrNotFound
+ }
+ return nil, err
+ }
+ var out []BlogListResult
+ err = res.All(ctx, &out)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (b *BlogApp) reqBlogList(w http.ResponseWriter, r *http.Request) {
+ var page int
+ var err error
+ pagQuery := r.URL.Query().Get("page")
+ if pagQuery != "" {
+ page, err = strconv.Atoi(pagQuery)
+ if err != nil {
+ page = 0
+ }
+ }
+ blogList, err := b.BlogList(r.Context(), int64(page))
+ if err != nil && err != backend.ErrNotFound {
+ log.Println("error getting blog list:", err)
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "internal error")
+ return
+ }
+ var ret struct {
+ BlogList []BlogListResult `json:"blogList"`
+ Num int `json:"num"`
+ }
+ ret.Num = len(blogList)
+ ret.BlogList = blogList
+ json.NewEncoder(w).Encode(ret)
+}
diff --git a/internal/blog-old/main.go b/internal/blog-old/main.go
new file mode 100644
index 0000000..068e856
--- /dev/null
+++ b/internal/blog-old/main.go
@@ -0,0 +1,63 @@
+package blog
+
+import (
+ "net/http"
+ "sync"
+
+ "github.com/CalebQ42/bbConvert"
+ "github.com/CalebQ42/darkstorm-server/internal/backend"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+type BlogApp struct {
+ back *backend.Backend
+ blogCol *mongo.Collection
+ authCol *mongo.Collection
+ portfolioCol *mongo.Collection
+ conv bbConvert.ComboConverter
+
+ cacheMutex *sync.RWMutex
+ blogCache map[string]Blog
+}
+
+func NewBlogApp(db *mongo.Database) *BlogApp {
+ out := &BlogApp{
+ blogCol: db.Collection("blog"),
+ authCol: db.Collection("author"),
+ portfolioCol: db.Collection("portfolio"),
+ conv: bbConvert.NewComboConverter(),
+ cacheMutex: &sync.RWMutex{},
+ blogCache: make(map[string]Blog),
+ }
+ return out
+}
+
+func (b *BlogApp) AppID() string {
+ return "blog"
+}
+
+func (b *BlogApp) CountTable() backend.CountTable {
+ return nil
+}
+
+func (b *BlogApp) CrashTable() backend.CrashTable {
+ return nil
+}
+
+func (b *BlogApp) AddBackend(back *backend.Backend) {
+ b.back = back
+}
+
+func (b *BlogApp) Extension(mux *http.ServeMux) {
+ mux.HandleFunc("GET /blog", b.reqLatestBlogs)
+ mux.HandleFunc("GET /blog/list", b.reqBlogList)
+ mux.HandleFunc("GET /blog/{blogID}", b.reqBlog)
+ mux.HandleFunc("POST /blog", b.createBlog)
+ mux.HandleFunc("POST /blog/{blogID}", b.updateBlog)
+
+ mux.HandleFunc("GET /blog/author/{authorID}", b.reqAuthorInfo)
+ mux.HandleFunc("POST /blog/author", b.addAuthorInfo)
+ mux.HandleFunc("POST /blog/author/{authorID}", b.updateAuthorInfo)
+
+ mux.HandleFunc("GET /blog/portfolio", b.reqPortfolio)
+}
diff --git a/internal/blog-old/portfolio.go b/internal/blog-old/portfolio.go
new file mode 100644
index 0000000..f9b0be8
--- /dev/null
+++ b/internal/blog-old/portfolio.go
@@ -0,0 +1,135 @@
+package blog
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "slices"
+ "strings"
+
+ "github.com/CalebQ42/darkstorm-server/internal/backend"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ portfolioTitle = "%v "
+ portfolioLink = "%v "
+ portfolioLanguage = "
%v : %v
"
+ portfolioTech = "Tech: %v
"
+ portfolioDesc = "%v
"
+)
+
+type PortfolioProject struct {
+ Title string `json:"_id" bson:"_id"`
+ Order int `json:"order" bson:"order"`
+ Repository string `json:"repository" bson:"repository"`
+ Description string `json:"description" bson:"description"`
+ Technologies []string `json:"technologies" bson:"technologies"`
+ Languages []struct {
+ Language string `json:"language" bson:"language"`
+ Dates string `json:"dates" bson:"dates"`
+ } `json:"language" bson:"language"`
+}
+
+func (p PortfolioProject) HTMX() string {
+ out := fmt.Sprintf(portfolioTitle, p.Title)
+ out += fmt.Sprintf(portfolioLink, p.Repository, p.Repository)
+ for _, l := range p.Languages {
+ out += fmt.Sprintf(portfolioLanguage, l.Language, l.Dates)
+ }
+ out += fmt.Sprintf(portfolioTech, strings.Join(p.Technologies, ", "))
+ out += fmt.Sprintf(portfolioDesc, p.Description)
+ return out
+}
+
+type Portfolio []PortfolioProject
+
+const (
+ portfolioSelector = `
+Tech Filter:
+
+ %v
+
+
`
+ portfolioSelectorOption = "%v "
+)
+
+func (p Portfolio) FullHTMX(ctx context.Context, blogApp *BlogApp, selectedTech string) string {
+ aboutMe := ""
+ if me, err := blogApp.AboutMe(ctx); err != nil {
+ aboutMe += "Error getting info about me :("
+ } else {
+ aboutMe += me.HTML()
+ }
+ aboutMe += ""
+ tech := make(map[string]struct{})
+ for i := range p {
+ for _, t := range p[i].Technologies {
+ tech[t] = struct{}{}
+ }
+ }
+ techKeys := make([]string, 0, len(tech))
+ for k := range tech {
+ techKeys = append(techKeys, k)
+ }
+ slices.Sort(techKeys)
+ var out string
+ if selectedTech == "" {
+ out = fmt.Sprintf(portfolioSelectorOption, "", " selected=true", "All")
+ } else {
+ out = fmt.Sprintf(portfolioSelectorOption, "", "", "All")
+ }
+ for _, k := range techKeys {
+ if selectedTech == strings.ToLower(k) {
+ out += fmt.Sprintf(portfolioSelectorOption, k, " selected=true", k)
+ } else {
+ out += fmt.Sprintf(portfolioSelectorOption, k, "", k)
+ }
+ }
+ return aboutMe + fmt.Sprintf(portfolioSelector, out) + "" + p.HTMX() + "
"
+}
+
+func (p Portfolio) HTMX() string {
+ out := ""
+ for _, proj := range p {
+ out += proj.HTMX()
+ }
+ return out
+}
+
+func (b *BlogApp) Projects(ctx context.Context, techFilter string) (Portfolio, error) {
+ filter := bson.M{}
+ if techFilter != "" {
+ filter = bson.M{"technologies": techFilter}
+ }
+ res, err := b.portfolioCol.Find(ctx, filter, options.Find().SetSort(bson.M{"order": 1}))
+ if err != nil {
+ return nil, err
+ }
+ var out []PortfolioProject
+ err = res.All(ctx, &out)
+ return out, err
+}
+
+func (b *BlogApp) reqPortfolio(w http.ResponseWriter, r *http.Request) {
+ folio, err := b.Projects(r.Context(), r.URL.Query().Get("tech"))
+ if err != nil {
+ backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
+ return
+ }
+ if r.Header.Get("Hx-Request") == "true" {
+ if r.URL.Query().Has("tech") {
+ w.Write([]byte(folio.HTMX()))
+ } else {
+ w.Write([]byte(folio.FullHTMX(r.Context(), b, r.URL.Query().Get("tech"))))
+ }
+ } else {
+ json.NewEncoder(w).Encode(folio)
+ }
+}
diff --git a/internal/blog/README.md b/internal/blog/README.md
index bf5728b..dca2caa 100644
--- a/internal/blog/README.md
+++ b/internal/blog/README.md
@@ -1,189 +1,3 @@
-# Blog module
+# Blogs
-A simple blog module for darkstorm-backend.
-
-## Requests
-
-### Author info
-
-#### Get author info
-
-> GET /author/{authorID}
-
-```json
-{
- id: "authorID",
- name: "author name",
- about: "about",
- picurl: "picture URL"
-}
-```
-
-#### Update author info
-
-> POST /author/{authorID}
-
-Must have a auth token for a user with the `"blog": "admin"` permission.
-
-```json
-{
- name: "author name",
- about: "about",
- picurl: "picture url"
-}
-```
-
-#### Add Author info
-
-> POST /author
-
-Must have a auth token for a user with the `"blog": "admin"` permission.
-
-```json
-{
- name: "author name",
- about: "about",
- picurl: "picture URL"
-}
-```
-
-### Blog
-
-#### Specific blog
-
-> GET /blog/{blogID}
-
-Return:
-
-```json
-{
- id: "blogID",
- staticPage: false, // static pages don't show up alongside other blog pages.
- createTime: 0, // creation time in Unix format
- updateTime: 0, // last update time in Unix format
- author: "authorID",
- favicon: "favicon url",
- title: "blog title",
- blog: "blog", // blog will have been converted to HTML
-}
-```
-
-#### Create blog
-
-Request:
-
-> POST /blog
-
-Must have a auth token for a user with the `"blog": "admin"` permission.
-
-```json
-{
- favicon: "favicon url",
- title: "blog title",
- blog: "blog", // blog will have been converted to HTML
-}
-```
-
-Return:
-
-```json
-{
- id: "blogID"
-}
-```
-
-#### Update blog
-
-Request:
-
-> POST /blog/{blogID}
-
-Must have a auth token for a user with the `"blog": "admin"` permission.
-
-```json
-{
- favicon: "new icon",
- title: "new title",
- blog: "new blog content"
-}
-```
-
-#### Latest blogs
-
-> GET /blog?page=0
-
-Will return up to 5 blogs. `page` query is optional (implies 0 if not set).
-
-Return:
-
-```json
-{
- num: 1, // Number of returned results, returns up to 5 results
- blogs: [
- {
- id: "blogID",
- createTime: 0, // creation time in Unix format
- updateTime: 0, // last update time in Unix format
- author: "authorID",
- favicon: "favicon url",
- title: "blog title",
- blog: "blog", // blog will have been converted to HTML
- }
- ...
- ]
-}
-```
-
-#### Blog List
-
-> GET /blog/list?page=0
-
-Will return up to 50 IDs. `page` query is optional (implies 0 if not set).
-
-Return:
-
-```json
-{
- num: 1, // Number of returned results, returns up to 50 results
- blogList: [
- {
- id: "blogID",
- createTime: 0, // Unix format
- },
- {
- id: "blogID",
- createTime: 0, // Unix format
- },
- ...
- ]
-}
-```
-
-### Portfolio
-
-#### Get Projects
-
-> GET: /portfolio?lang=go
-
-Return:
-
-```json
-[
- {
- title: "Darkstorm Server",
- order: 0,
- repository: "https://github.com/CalebQ42/darkstorm-server",
- description: "The backend that runs runs my website and APIs",
- technologies: [ // May be empty
- "MongoDB",
- "RESTful API"
- ],
- language: [
- {
- language: "go",
- dates: "September 2021"
- }
- ]
- }
-]
-```
+An HTMX powered blog system and editor.
diff --git a/internal/blog/author.go b/internal/blog/author.go
index b553190..8905a81 100644
--- a/internal/blog/author.go
+++ b/internal/blog/author.go
@@ -1,164 +1,8 @@
package blog
-import (
- "context"
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "strconv"
- "strings"
-
- "github.com/CalebQ42/darkstorm-server/internal/backend"
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo"
-)
-
-const authorInfo = `
-
-
- %v %v
-
`
-
type Author struct {
ID string `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`
About string `json:"about" bson:"about"`
PicURL string `json:"picurl" bson:"picurl"`
}
-
-func (a Author) HTML() string {
- return fmt.Sprintf(authorInfo, a.PicURL, a.Name+"'s profile picture", a.Name, a.About)
-}
-
-func (b *BlogApp) AboutMe(ctx context.Context) (*Author, error) {
- res := b.authCol.FindOne(ctx, bson.M{"_id": "caleb_gardner"})
- if res.Err() != nil {
- log.Println("error getting about me:", res.Err())
- if res.Err() == mongo.ErrNoDocuments {
- return nil, backend.ErrNotFound
- }
- return nil, res.Err()
- }
- var aboutMe Author
- err := res.Decode(&aboutMe)
- if err != nil {
- log.Println("error decoding about me:", res)
- return nil, err
- }
- return &aboutMe, nil
-}
-
-func (b *BlogApp) reqAuthorInfo(w http.ResponseWriter, r *http.Request) {
- res := b.authCol.FindOne(r.Context(), r.PathValue("authorID"))
- if res.Err() == mongo.ErrNoDocuments {
- backend.ReturnError(w, http.StatusNotFound, "notFound", "Author with ID "+r.PathValue("authorID")+" not found")
- return
- } else if res.Err() != nil {
- log.Println("error getting author info:", res.Err())
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- return
- }
- var auth Author
- err := res.Decode(&auth)
- if err != nil {
- log.Println("error decoding author info:", err)
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- return
- }
- json.NewEncoder(w).Encode(auth)
-}
-
-func (b *BlogApp) addAuthorInfo(w http.ResponseWriter, r *http.Request) {
- hdr, err := b.back.VerifyHeader(w, r, "blogManagement", false)
- if hdr == nil {
- if err != nil {
- log.Println("request key parsing error:", err)
- }
- return
- } else if hdr.Key.AppID != "blog" {
- backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application is unauthorized")
- return
- }
- if hdr.User == nil || hdr.User.Perm["blog"] != "admin" {
- backend.ReturnError(w, http.StatusUnauthorized, "unauthorized", "Application is unauthorized")
- return
- }
- var newAuth Author
- err = json.NewDecoder(r.Body).Decode(&newAuth)
- r.Body.Close()
- if err != nil {
- backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Invalid request")
- return
- }
- for i := 1; ; i++ {
- newID := strings.ReplaceAll(newAuth.Name, " ", "-")
- if i != 1 {
- newID += strconv.Itoa(i)
- }
- collisionCheck := b.authCol.FindOne(r.Context(), bson.M{"name": newAuth.Name})
- if collisionCheck.Err() == mongo.ErrNoDocuments {
- newAuth.ID = newID
- break
- } else if collisionCheck.Err() != nil {
- log.Println("error checking for new author ID collisions:", err)
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- return
- }
- }
- _, err = b.authCol.InsertOne(r.Context(), newAuth)
- if err != nil {
- log.Println("error inserting new author:", err)
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- return
- }
- w.WriteHeader(http.StatusCreated)
-}
-
-func (b *BlogApp) updateAuthorInfo(w http.ResponseWriter, r *http.Request) {
- hdr, err := b.back.VerifyHeader(w, r, "blogManagement", false)
- if hdr == nil {
- if err != nil {
- log.Println("request key parsing error:", err)
- }
- return
- } else if hdr.Key.AppID != "blog" {
- backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application is unauthorized")
- return
- }
- if hdr.User == nil || hdr.User.Perm["blog"] != "admin" {
- backend.ReturnError(w, http.StatusUnauthorized, "unauthorized", "Application is unauthorized")
- return
- }
- var rawUpd map[string]string
- err = json.NewDecoder(r.Body).Decode(&rawUpd)
- r.Body.Close()
- if err != nil {
- backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Invalid request")
- return
- }
- actlUpd := make(map[string]string)
- if rawUpd["name"] != "" {
- actlUpd["name"] = rawUpd["name"]
- }
- if rawUpd["about"] != "" {
- actlUpd["about"] = rawUpd["about"]
- }
- if rawUpd["picurl"] != "" {
- actlUpd["picurl"] = rawUpd["picurl"]
- }
- res, err := b.authCol.UpdateByID(r.Context(), r.PathValue("authorID"), actlUpd)
- if err != nil {
- if err == mongo.ErrNoDocuments {
- backend.ReturnError(w, http.StatusNotFound, "notFound", "Blog with ID "+r.PathValue("blogID")+" not found")
- } else {
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- }
- return
- }
- if res.MatchedCount == 0 {
- backend.ReturnError(w, http.StatusNotFound, "notFound", "Blog with ID "+r.PathValue("blogID")+" not found")
- return
- }
- w.WriteHeader(http.StatusCreated)
-}
diff --git a/internal/blog/blog-list.go b/internal/blog/blog-list.go
new file mode 100644
index 0000000..243168e
--- /dev/null
+++ b/internal/blog/blog-list.go
@@ -0,0 +1,8 @@
+package blog
+
+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"`
+}
diff --git a/internal/blog/blog.go b/internal/blog/blog.go
index 6bd8e04..7241c3d 100644
--- a/internal/blog/blog.go
+++ b/internal/blog/blog.go
@@ -1,28 +1,5 @@
package blog
-import (
- "context"
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "strconv"
- "time"
-
- "github.com/CalebQ42/darkstorm-server/internal/backend"
- "github.com/google/uuid"
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo"
- "go.mongodb.org/mongo-driver/mongo/options"
-)
-
-const (
- blogTitle = ""
- blogAuthor = "By %v "
- blogCreate = "Written on: %v "
- blogMain = "%v
"
-)
-
type Blog struct {
ID string `json:"id" bson:"_id"`
Author string `json:"author" bson:"author"`
@@ -35,353 +12,3 @@ type Blog struct {
CreateTime int64 `json:"createTime" bson:"createTime"`
UpdateTime int64 `json:"updateTime" bson:"updateTime"`
}
-
-func (b *Blog) HTMX(blogApp *BlogApp, ctx context.Context) string {
- if b.StaticPage {
- return b.RawBlog
- }
- out := fmt.Sprintf(blogTitle, b.ID, b.ID, b.Title)
- auth, err := blogApp.GetAuthor(ctx, b)
- if err == nil {
- out += fmt.Sprintf(blogAuthor, auth.Name)
- } else {
- out += fmt.Sprintf(blogAuthor, "unknown")
- }
- cTime := time.Unix(b.CreateTime, 0).Format(time.DateOnly)
- if b.UpdateTime > b.CreateTime {
- out += fmt.Sprintf(blogCreate, cTime+"; Last updated on: "+time.Unix(b.UpdateTime, 0).Format(time.DateOnly))
- } else {
- out += fmt.Sprintf(blogCreate, cTime)
- }
- out += fmt.Sprintf(blogMain, b.HTMLBlog)
- if err == nil {
- out += "About the author: " + auth.HTML()
- }
- return out
-}
-
-func (b *BlogApp) ConvertBlog(blog *Blog) {
- if !blog.StaticPage {
- blog.HTMLBlog = b.conv.HTMLConvert(blog.RawBlog)
- }
-}
-
-func (b *BlogApp) GetAuthor(ctx context.Context, blog *Blog) (*Author, error) {
- res := b.authCol.FindOne(ctx, bson.M{"_id": blog.Author})
- if res.Err() != nil {
- if res.Err() == mongo.ErrNoDocuments {
- return nil, backend.ErrNotFound
- }
- return nil, res.Err()
- }
- var author Author
- err := res.Decode(&author)
- return &author, err
-}
-
-func (b *BlogApp) Blog(ctx context.Context, ID string) (*Blog, error) {
- b.cacheMutex.RLock()
- blog, has := b.blogCache[ID]
- b.cacheMutex.RUnlock()
- if has {
- return &blog, nil
- }
- res := b.blogCol.FindOne(ctx, bson.M{"_id": ID, "draft": false})
- if res.Err() != nil {
- if res.Err() == mongo.ErrNoDocuments {
- return nil, backend.ErrNotFound
- }
- return nil, res.Err()
- }
- err := res.Decode(&blog)
- if err != nil {
- return nil, err
- }
- b.ConvertBlog(&blog)
- b.cacheMutex.Lock()
- b.blogCache[ID] = blog
- b.cacheMutex.Unlock()
- go b.CleanCache(ID)
- return &blog, nil
-}
-
-func (b *BlogApp) AnyBlog(ctx context.Context, ID string) (*Blog, error) {
- res := b.blogCol.FindOne(ctx, bson.M{"_id": ID})
- if res.Err() != nil {
- if res.Err() == mongo.ErrNoDocuments {
- return nil, backend.ErrNotFound
- }
- return nil, res.Err()
- }
- var blog Blog
- err := res.Decode(&blog)
- if err != nil {
- return nil, err
- }
- b.ConvertBlog(&blog)
- return &blog, nil
-}
-
-func (b *BlogApp) Contains(ctx context.Context, ID string) bool {
- res := b.blogCol.FindOne(ctx, bson.M{"_id": ID})
- return res.Err() == nil
-}
-
-func (b *BlogApp) CleanCache(ID string) {
- time.Sleep(5 * time.Minute)
- b.cacheMutex.Lock()
- delete(b.blogCache, ID)
- b.cacheMutex.Unlock()
-}
-
-func (b *BlogApp) reqBlog(w http.ResponseWriter, r *http.Request) {
- blogID := r.PathValue("blogID")
- if blogID == "" {
- backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Must provide a blogID")
- return
- }
- blog, err := b.Blog(r.Context(), blogID)
- if err != nil {
- if err == backend.ErrNotFound {
- backend.ReturnError(w, http.StatusNotFound, "notFound", "Not blog found with the given ID")
- return
- }
- log.Println("error getting blog:", err)
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
- return
- }
- if r.Header.Get("Hx-Request") == "true" {
- w.Write([]byte(blog.HTMX(b, r.Context())))
- } else {
- json.NewEncoder(w).Encode(blog)
- }
-}
-
-func (b *BlogApp) createBlog(w http.ResponseWriter, r *http.Request) {
- hdr, err := b.back.VerifyHeader(w, r, "blogManagement", false)
- if hdr == nil {
- if err != nil {
- log.Println("request key parsing error:", err)
- }
- return
- } else if hdr.Key.AppID != "blog" {
- backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application is unauthorized")
- return
- }
- if hdr.User == nil || hdr.User.Perm["blog"] != "admin" {
- backend.ReturnError(w, http.StatusUnauthorized, "unauthorized", "Application is unauthorized")
- return
- }
- var newBlog Blog
- err = json.NewDecoder(r.Body).Decode(&newBlog)
- r.Body.Close()
- if err != nil {
- backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request")
- return
- }
- id, err := uuid.NewV7()
- if err != nil {
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- return
- }
- tim := time.Now().Unix()
- newBlog.ID = id.String()
- newBlog.CreateTime = tim
- newBlog.UpdateTime = tim
- newBlog.Author = hdr.User.Username
- _, err = b.blogCol.InsertOne(r.Context(), newBlog)
- if err != nil {
- log.Println("error when inserting new blog:", err)
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- return
- }
- w.WriteHeader(http.StatusCreated)
-
-}
-
-func (b *BlogApp) updateBlog(w http.ResponseWriter, r *http.Request) {
- hdr, err := b.back.VerifyHeader(w, r, "blogManagement", false)
- if hdr == nil {
- if err != nil {
- log.Println("request key parsing error:", err)
- }
- return
- } else if hdr.Key.AppID != "blog" {
- backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application is unauthorized")
- return
- }
- if hdr.User == nil || hdr.User.Perm["blog"] != "admin" {
- backend.ReturnError(w, http.StatusUnauthorized, "unauthorized", "Application is unauthorized")
- return
- }
- if r.PathValue("blogID") == "" {
- backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request")
- return
- }
- var reqUpdRaw map[string]string
- err = json.NewDecoder(r.Body).Decode(&reqUpdRaw)
- r.Body.Close()
- if err != nil {
- backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request")
- return
- }
- reqUpd := bson.M{}
- if fav, ok := reqUpdRaw["favicon"]; ok && fav != "" {
- reqUpd["favicon"] = fav
- }
- if titl, ok := reqUpdRaw["title"]; ok && titl != "" {
- reqUpd["title"] = titl
- }
- if blog, ok := reqUpdRaw["blog"]; ok && blog != "" {
- reqUpd["blog"] = blog
- }
- reqUpd["updateTime"] = time.Now().Unix()
- res, err := b.blogCol.UpdateByID(r.Context(), r.PathValue("blogID"), reqUpd)
- if err != nil {
- if err == mongo.ErrNoDocuments {
- backend.ReturnError(w, http.StatusNotFound, "notFound", "Blog with ID "+r.PathValue("blogID")+" not found")
- } else {
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- }
- return
- }
- if res.MatchedCount == 0 {
- backend.ReturnError(w, http.StatusNotFound, "notFound", "Blog with ID "+r.PathValue("blogID")+" not found")
- return
- }
- w.WriteHeader(http.StatusCreated)
-}
-
-func (b *BlogApp) InsertBlog(ctx context.Context, blog Blog) error {
- _, err := b.blogCol.InsertOne(ctx, blog)
- return err
-}
-
-func (b *BlogApp) UpdateBlog(ctx context.Context, ID string, updates bson.M) error {
- _, err := b.blogCol.UpdateByID(ctx, ID, bson.M{"$set": updates})
- return err
-}
-
-func (b *BlogApp) LatestBlogs(ctx context.Context, page int64) ([]*Blog, error) {
- res, err := b.blogCol.Find(ctx, bson.M{"staticPage": false, "draft": false}, options.Find().
- SetSort(bson.M{"createTime": -1}).
- SetLimit(5).
- SetSkip(page*5))
- if err != nil {
- if err == mongo.ErrNoDocuments {
- return nil, backend.ErrNotFound
- }
- return nil, err
- }
- var out []*Blog
- err = res.All(ctx, &out)
- if err != nil {
- return nil, err
- }
- for i := range out {
- b.ConvertBlog(out[i])
- }
- return out, nil
-}
-
-func (b *BlogApp) reqLatestBlogs(w http.ResponseWriter, r *http.Request) {
- var page int
- var err error
- pagQuery := r.URL.Query().Get("page")
- if pagQuery != "" {
- page, err = strconv.Atoi(pagQuery)
- if err != nil {
- page = 0
- }
- }
- blogs, err := b.LatestBlogs(r.Context(), int64(page))
- if err != nil && err != backend.ErrNotFound {
- log.Println("error getting latest blogs:", err)
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "internal error")
- return
- }
- var ret struct {
- Blogs []*Blog `json:"blogs"`
- Num int `json:"num"`
- }
- ret.Num = len(blogs)
- ret.Blogs = blogs
- json.NewEncoder(w).Encode(ret)
-}
-
-type BlogListResult struct {
- ID string `json:"id" bson:"_id"`
- Title string `json:"title" bson:"title"`
- CreateTime int `json:"createTime" bson:"createTime"`
-}
-
-func (b BlogListResult) HTMX() string {
- return "" + b.Title + " "
-}
-
-func (b *BlogApp) BlogList(ctx context.Context, page int64) ([]BlogListResult, error) {
- res, err := b.blogCol.Find(ctx, bson.M{"staticPage": false, "draft": false}, options.Find().
- SetProjection(bson.M{"_id": 1, "createTime": 1, "title": 1}).
- SetSort(bson.M{"createTime": -1}).
- SetLimit(50).
- SetSkip(page*50))
- if err != nil {
- if err == mongo.ErrNoDocuments {
- return nil, backend.ErrNotFound
- }
- return nil, err
- }
- var out []BlogListResult
- err = res.All(ctx, &out)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
-func (b *BlogApp) AllBlogsList(ctx context.Context) ([]BlogListResult, error) {
- res, err := b.blogCol.Find(ctx, bson.M{}, options.Find().
- SetProjection(bson.M{"_id": 1, "createTime": 1, "title": 1}).
- SetSort(bson.M{"createTime": -1}))
- if err != nil {
- if err == mongo.ErrNoDocuments {
- return nil, backend.ErrNotFound
- }
- return nil, err
- }
- var out []BlogListResult
- err = res.All(ctx, &out)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
-func (b *BlogApp) reqBlogList(w http.ResponseWriter, r *http.Request) {
- var page int
- var err error
- pagQuery := r.URL.Query().Get("page")
- if pagQuery != "" {
- page, err = strconv.Atoi(pagQuery)
- if err != nil {
- page = 0
- }
- }
- blogList, err := b.BlogList(r.Context(), int64(page))
- if err != nil && err != backend.ErrNotFound {
- log.Println("error getting blog list:", err)
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "internal error")
- return
- }
- var ret struct {
- BlogList []BlogListResult `json:"blogList"`
- Num int `json:"num"`
- }
- ret.Num = len(blogList)
- ret.BlogList = blogList
- json.NewEncoder(w).Encode(ret)
-}
diff --git a/internal/blog/edit-page.go b/internal/blog/edit-page.go
new file mode 100644
index 0000000..2efa55d
--- /dev/null
+++ b/internal/blog/edit-page.go
@@ -0,0 +1,9 @@
+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/main.go b/internal/blog/main.go
index 068e856..2663f5f 100644
--- a/internal/blog/main.go
+++ b/internal/blog/main.go
@@ -1,63 +1,39 @@
package blog
import (
+ "log"
"net/http"
"sync"
-
- "github.com/CalebQ42/bbConvert"
- "github.com/CalebQ42/darkstorm-server/internal/backend"
- "go.mongodb.org/mongo-driver/mongo"
)
-type BlogApp struct {
- back *backend.Backend
- blogCol *mongo.Collection
- authCol *mongo.Collection
- portfolioCol *mongo.Collection
- conv bbConvert.ComboConverter
+type HTMXReturner func(http.ResponseWriter, *http.Request) (string, error)
- cacheMutex *sync.RWMutex
- blogCache map[string]Blog
+type Backend struct {
+ cacheMutex sync.RWMutex
+ cache map[string]string
}
-func NewBlogApp(db *mongo.Database) *BlogApp {
- out := &BlogApp{
- blogCol: db.Collection("blog"),
- authCol: db.Collection("author"),
- portfolioCol: db.Collection("portfolio"),
- conv: bbConvert.NewComboConverter(),
- cacheMutex: &sync.RWMutex{},
- blogCache: make(map[string]Blog),
- }
- return out
+func (b *Backend) AddToMux(mux *http.ServeMux) {
+ mux.HandleFunc("GET /editor/{page}", b.editorPage)
}
-func (b *BlogApp) AppID() string {
- return "blog"
-}
-
-func (b *BlogApp) CountTable() backend.CountTable {
- return nil
-}
-
-func (b *BlogApp) CrashTable() backend.CrashTable {
- return nil
-}
-
-func (b *BlogApp) AddBackend(back *backend.Backend) {
- b.back = back
-}
-
-func (b *BlogApp) Extension(mux *http.ServeMux) {
- mux.HandleFunc("GET /blog", b.reqLatestBlogs)
- mux.HandleFunc("GET /blog/list", b.reqBlogList)
- mux.HandleFunc("GET /blog/{blogID}", b.reqBlog)
- mux.HandleFunc("POST /blog", b.createBlog)
- mux.HandleFunc("POST /blog/{blogID}", b.updateBlog)
-
- mux.HandleFunc("GET /blog/author/{authorID}", b.reqAuthorInfo)
- mux.HandleFunc("POST /blog/author", b.addAuthorInfo)
- mux.HandleFunc("POST /blog/author/{authorID}", b.updateAuthorInfo)
-
- mux.HandleFunc("GET /blog/portfolio", b.reqPortfolio)
+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
+ }
+ 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
+ }
+ w.Write([]byte(res))
+ })
}
diff --git a/internal/blog/portfolio.go b/internal/blog/portfolio.go
index f9b0be8..9d1a174 100644
--- a/internal/blog/portfolio.go
+++ b/internal/blog/portfolio.go
@@ -1,26 +1,5 @@
package blog
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "slices"
- "strings"
-
- "github.com/CalebQ42/darkstorm-server/internal/backend"
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo/options"
-)
-
-const (
- portfolioTitle = "%v "
- portfolioLink = "%v "
- portfolioLanguage = "
%v : %v
"
- portfolioTech = "Tech: %v
"
- portfolioDesc = "%v
"
-)
-
type PortfolioProject struct {
Title string `json:"_id" bson:"_id"`
Order int `json:"order" bson:"order"`
@@ -32,104 +11,3 @@ type PortfolioProject struct {
Dates string `json:"dates" bson:"dates"`
} `json:"language" bson:"language"`
}
-
-func (p PortfolioProject) HTMX() string {
- out := fmt.Sprintf(portfolioTitle, p.Title)
- out += fmt.Sprintf(portfolioLink, p.Repository, p.Repository)
- for _, l := range p.Languages {
- out += fmt.Sprintf(portfolioLanguage, l.Language, l.Dates)
- }
- out += fmt.Sprintf(portfolioTech, strings.Join(p.Technologies, ", "))
- out += fmt.Sprintf(portfolioDesc, p.Description)
- return out
-}
-
-type Portfolio []PortfolioProject
-
-const (
- portfolioSelector = `
-Tech Filter:
-
- %v
-
-
`
- portfolioSelectorOption = "%v "
-)
-
-func (p Portfolio) FullHTMX(ctx context.Context, blogApp *BlogApp, selectedTech string) string {
- aboutMe := ""
- if me, err := blogApp.AboutMe(ctx); err != nil {
- aboutMe += "Error getting info about me :("
- } else {
- aboutMe += me.HTML()
- }
- aboutMe += ""
- tech := make(map[string]struct{})
- for i := range p {
- for _, t := range p[i].Technologies {
- tech[t] = struct{}{}
- }
- }
- techKeys := make([]string, 0, len(tech))
- for k := range tech {
- techKeys = append(techKeys, k)
- }
- slices.Sort(techKeys)
- var out string
- if selectedTech == "" {
- out = fmt.Sprintf(portfolioSelectorOption, "", " selected=true", "All")
- } else {
- out = fmt.Sprintf(portfolioSelectorOption, "", "", "All")
- }
- for _, k := range techKeys {
- if selectedTech == strings.ToLower(k) {
- out += fmt.Sprintf(portfolioSelectorOption, k, " selected=true", k)
- } else {
- out += fmt.Sprintf(portfolioSelectorOption, k, "", k)
- }
- }
- return aboutMe + fmt.Sprintf(portfolioSelector, out) + "" + p.HTMX() + "
"
-}
-
-func (p Portfolio) HTMX() string {
- out := ""
- for _, proj := range p {
- out += proj.HTMX()
- }
- return out
-}
-
-func (b *BlogApp) Projects(ctx context.Context, techFilter string) (Portfolio, error) {
- filter := bson.M{}
- if techFilter != "" {
- filter = bson.M{"technologies": techFilter}
- }
- res, err := b.portfolioCol.Find(ctx, filter, options.Find().SetSort(bson.M{"order": 1}))
- if err != nil {
- return nil, err
- }
- var out []PortfolioProject
- err = res.All(ctx, &out)
- return out, err
-}
-
-func (b *BlogApp) reqPortfolio(w http.ResponseWriter, r *http.Request) {
- folio, err := b.Projects(r.Context(), r.URL.Query().Get("tech"))
- if err != nil {
- backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
- return
- }
- if r.Header.Get("Hx-Request") == "true" {
- if r.URL.Query().Has("tech") {
- w.Write([]byte(folio.HTMX()))
- } else {
- w.Write([]byte(folio.FullHTMX(r.Context(), b, r.URL.Query().Get("tech"))))
- }
- } else {
- json.NewEncoder(w).Encode(folio)
- }
-}
diff --git a/internal/blog/templates.go b/internal/blog/templates.go
new file mode 100644
index 0000000..aba121b
--- /dev/null
+++ b/internal/blog/templates.go
@@ -0,0 +1,152 @@
+package blog
+
+const editor = `
+
+ Blogs
+ Portfolio
+ Author
+
+{{ .Page }}
`
+
+type editorStruct struct {
+ SelectedPage string
+ Page string
+}
+
+const blogPage = `
+ Blog:
+
+ New Blog
+ {{ range $blog := .Blogs }}
+ {{.Title}}{{if .Draft}} (Draft){{end}}
+ {{end}}
+
+
+{{.Editor}}
`
+
+type blogPageStruct struct {
+ Selected string
+ Editor string
+ Blogs []BlogList
+}
+
+const blogForm = `
+`
+
+type blogFormStruct struct {
+ Blog Blog
+ Result string
+}
+
+const portfolioPage = `
+ Project:
+
+ New Project
+ {{ range $project := .Projects }}
+ {{.Title}}
+ {{end}}
+
+
+{{.Editor}}
`
+
+type portfolioPageStruct struct {
+ Selected string
+ Editor string
+ Projects []PortfolioProject
+}
+
+// TODO: Add Languages to editor
+const portfolioForm = `
+
+ Title
+
+ Technologies
+
+ {{.Project.Description}}
+ {{.Result}}
+
+ {{if eq .Project.Title ""}}Create{{else}}Update{{end}}
+
+ Cancel
+
+
+
`
+
+type portfolioFormStruct struct {
+ Project PortfolioProject
+ Result string
+}
+
+const authorPage = `
+ Author:
+
+ New Author
+ {{ range $author := .Authors }}
+ {{.Name}}
+ {{end}}
+
+
+{{.Editor}}
`
+
+type authorPageStruct struct {
+ Selected string
+ Editor string
+ Authors []Author
+}
+
+const authorForm = `
+
+ Name
+
+ {{.Author.About}}
+ {{.Result}}
+
+ {{if eq .Author.ID ""}}Create{{else}}Update{{end}}
+
+ Cancel
+
+
+
`
+
+type authorFormStruct struct {
+ Author Author
+ Result string
+}