From 687fbd7e65996961773d8ba594a28a5f404aae4d Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Tue, 19 Nov 2024 07:26:37 -0600 Subject: [PATCH] Some initial stuff --- internal/backend/{darkstorm.go => main.go} | 0 internal/blog-old/README.md | 189 ++++++++++ internal/blog-old/author.go | 164 +++++++++ internal/blog-old/blog.go | 387 +++++++++++++++++++++ internal/blog-old/main.go | 63 ++++ internal/blog-old/portfolio.go | 135 +++++++ internal/blog/README.md | 190 +--------- internal/blog/author.go | 156 --------- internal/blog/blog-list.go | 8 + internal/blog/blog.go | 373 -------------------- internal/blog/edit-page.go | 9 + internal/blog/main.go | 76 ++-- internal/blog/portfolio.go | 122 ------- internal/blog/templates.go | 152 ++++++++ 14 files changed, 1135 insertions(+), 889 deletions(-) rename internal/backend/{darkstorm.go => main.go} (100%) create mode 100644 internal/blog-old/README.md create mode 100644 internal/blog-old/author.go create mode 100644 internal/blog-old/blog.go create mode 100644 internal/blog-old/main.go create mode 100644 internal/blog-old/portfolio.go create mode 100644 internal/blog/blog-list.go create mode 100644 internal/blog/edit-page.go create mode 100644 internal/blog/templates.go 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

%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 = "

%v

" + 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: %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: + +

` + portfolioSelectorOption = "" +) + +func (p Portfolio) FullHTMX(ctx context.Context, blogApp *BlogApp, selectedTech string) string { + aboutMe := "

About Me

" + if me, err := blogApp.AboutMe(ctx); err != nil { + aboutMe += "Error getting info about me :(" + } else { + aboutMe += me.HTML() + } + aboutMe += "

My Projects

" + 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

%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 = "

%v

" - 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: %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: - -

` - portfolioSelectorOption = "" -) - -func (p Portfolio) FullHTMX(ctx context.Context, blogApp *BlogApp, selectedTech string) string { - aboutMe := "

About Me

" - if me, err := blogApp.AboutMe(ctx); err != nil { - aboutMe += "Error getting info about me :(" - } else { - aboutMe += me.HTML() - } - aboutMe += "

My Projects

" - 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 = `

+ + +

+
{{.Editor}}
` + +type blogPageStruct struct { + Selected string + Editor string + Blogs []BlogList +} + +const blogForm = ` +
+ +

+ + + +

+ + + +
{{.Result}}
+

+ + +

+

` + +type blogFormStruct struct { + Blog Blog + Result string +} + +const portfolioPage = `

+ + +

+
{{.Editor}}
` + +type portfolioPageStruct struct { + Selected string + Editor string + Projects []PortfolioProject +} + +// TODO: Add Languages to editor +const portfolioForm = `
+ + + + + + +
{{.Result}}
+

+ + +

+

` + +type portfolioFormStruct struct { + Project PortfolioProject + Result string +} + +const authorPage = `

+ + +

+
{{.Editor}}
` + +type authorPageStruct struct { + Selected string + Editor string + Authors []Author +} + +const authorForm = `
+ + + + +
{{.Result}}
+

+ + +

+

` + +type authorFormStruct struct { + Author Author + Result string +}