diff --git a/cmd/backend/articles.go b/cmd/backend/articles.go index e8c5802..23a4e0b 100644 --- a/cmd/backend/articles.go +++ b/cmd/backend/articles.go @@ -7,6 +7,8 @@ import ( "log" "os" "time" + + "github.com/google/uuid" ) type Article struct { @@ -14,6 +16,7 @@ type Article struct { Title string BannerLink string Summary string + UUID uuid.UUID ID int64 CreatorID int64 IssueID int64 @@ -31,8 +34,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) { selectQuery := "SELECT id FROM issues WHERE published = false" insertQuery := ` INSERT INTO articles - (title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated, uuid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` for i := 0; i < TxMaxRetries; i++ { @@ -49,7 +52,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) } - result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.CreatorID, id, a.EditedID, 0, a.IsInIssue, a.AutoGenerated) + result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.CreatorID, id, a.EditedID, 0, a.IsInIssue, a.AutoGenerated, a.UUID.String()) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -83,7 +86,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) GetArticle(id int64) (*Article, error) { query := ` - SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated + SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated, uuid FROM articles WHERE id = ? ` @@ -91,9 +94,10 @@ func (db *DB) GetArticle(id int64) (*Article, error) { article := new(Article) var created []byte + var uuidString string var err error - if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.Clicks, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.Clicks, &article.IsInIssue, &article.AutoGenerated, &uuidString); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -103,12 +107,17 @@ func (db *DB) GetArticle(id int64) (*Article, error) { return nil, fmt.Errorf("error parsing created: %v", err) } + article.UUID, err = uuid.Parse(uuidString) + if err != nil { + return nil, fmt.Errorf("error parsing uuid: %v", err) + } + return article, nil } func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { query := fmt.Sprintf(` - SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, published, rejected, is_in_issue, auto_generated + SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, published, rejected, is_in_issue, auto_generated, uuid FROM articles WHERE %s = ? `, attribute) @@ -121,8 +130,9 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro for rows.Next() { article := new(Article) var created []byte + var uuidString string - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.CreatorID, &article.IssueID, &article.Clicks, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.CreatorID, &article.IssueID, &article.Clicks, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated, &uuidString); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -131,6 +141,11 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro return nil, fmt.Errorf("error parsing created: %v", err) } + article.UUID, err = uuid.Parse(uuidString) + if err != nil { + return nil, fmt.Errorf("error parsing uuid: %v", err) + } + articleList = append(articleList, article) } @@ -142,7 +157,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} issueQuery := "SELECT id FROM issues WHERE published = false" articlesQuery := ` - SELECT id, title, created, banner_link, summary, clicks, auto_generated + SELECT id, title, created, banner_link, summary, clicks, auto_generated, uuid FROM articles WHERE issue_id = ? AND published = true AND is_in_issue = true ` @@ -174,8 +189,9 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { for rows.Next() { article := new(Article) var created []byte + var uuidString string - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Clicks, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Clicks, &article.AutoGenerated, &uuidString); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } @@ -190,6 +206,14 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { return nil, fmt.Errorf("error parsing created: %v", err) } + article.UUID, err = uuid.Parse(uuidString) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return nil, fmt.Errorf("error parsing uuid: %v", err) + } + articleList = append(articleList, article) } @@ -284,11 +308,11 @@ func (db *DB) DeleteArticle(id int64) error { return nil } -func WriteArticleToFile(c *Config, articleID int64, content []byte) error { - articleAbsName := fmt.Sprint(c.ArticleDir, "/", articleID, ".md") +func WriteArticleToFile(c *Config, articleUUID uuid.UUID, content []byte) error { + articleAbsName := fmt.Sprint(c.ArticleDir, "/", articleUUID, ".md") if err := os.WriteFile(articleAbsName, content, 0644); err != nil { - return fmt.Errorf("error writing article %v to file: %v", articleID, err) + return fmt.Errorf("error writing article %v to file: %v", articleUUID, err) } return nil diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index f3ec0c5..0f344c7 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -33,7 +33,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { entry := atom.NewEntry(articleTitle) entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID)) entry.Published = atom.NewDate(article.Created) - entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", fmt.Sprint(c.Domain, "/article/serve/", article.ID)) + entry.Content = atom.NewContent(atom.OutOfLine, "text/html", fmt.Sprint(c.Domain, "/article/serve/", article.UUID)) if article.AutoGenerated { entry.Summary = atom.NewText("text", "automatically generated") diff --git a/cmd/backend/docx.go b/cmd/backend/docx.go new file mode 100644 index 0000000..c44ee73 --- /dev/null +++ b/cmd/backend/docx.go @@ -0,0 +1,59 @@ +package backend + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + + "github.com/google/uuid" +) + +func ConvertToMarkdown(c *Config, filename string) ([]byte, error) { + var stderr bytes.Buffer + + articleID := uuid.New() + articleFileName := fmt.Sprint("/tmp/", articleID, ".md") + + tmpDir, err := os.MkdirTemp("/tmp", "cpolis_images") + if err != nil { + return nil, fmt.Errorf("error creating temporary directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + cmd := exec.Command("pandoc", "-s", "-f", "docx", "-t", "commonmark_x", "-o", articleFileName, "--extract-media", tmpDir, filename) // TODO: Is writing to a file necessary? + cmd.Stderr = &stderr + if err = cmd.Run(); err != nil { + return nil, fmt.Errorf("error converting docx to markdown: %v: %v", err, stderr.String()) + } + defer os.Remove(articleFileName) + + articleContent, err := os.ReadFile(articleFileName) + if err != nil { + return nil, fmt.Errorf("error reading markdown file: %v", err) + } + + imageNames, err := filepath.Glob(filepath.Join(tmpDir, "/media/*")) + if err != nil { + return nil, fmt.Errorf("error getting docx images from temporary directory: %v", err) + } + + for _, name := range imageNames { + image, err := os.Open(name) + if err != nil { + return nil, fmt.Errorf("error opening image file %v: %v", name, err) + } + defer image.Close() + + newImageName, err := SaveImage(image, c.MaxImgHeight, c.MaxImgWidth, c.PicsDir) + if err != nil { + return nil, fmt.Errorf("error saving image %v: %v", name, err) + } + + articleContent = regexp.MustCompile(name).ReplaceAll(articleContent, []byte(c.PicsDir+"/"+newImageName)) + } + + return articleContent, nil +} diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go index e313b79..f112600 100644 --- a/cmd/frontend/articles.go +++ b/cmd/frontend/articles.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/google/uuid" b "streifling.com/jason/cpolis/cmd/backend" ) @@ -109,6 +110,7 @@ func SubmitArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFun IsInIssue: r.PostFormValue("issue") == "on", AutoGenerated: false, EditedID: 0, + UUID: uuid.New(), } if len(article.Title) == 0 { @@ -164,7 +166,7 @@ func SubmitArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFun http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest) return } - if err := b.WriteArticleToFile(c, article.ID, content); err != nil { + if err := b.WriteArticleToFile(c, article.UUID, content); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -221,14 +223,26 @@ func ResubmitArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerF return } - article := &b.Article{ - Title: r.PostFormValue("article-title"), - BannerLink: r.PostFormValue("article-banner-url"), - Summary: r.PostFormValue("article-summary"), - CreatorID: session.User.ID, - IsInIssue: r.PostFormValue("issue") == "on", + id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return } + article, err := db.GetArticle(id) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + article.Title = r.PostFormValue("article-title") + article.BannerLink = r.PostFormValue("article-banner-url") + article.Summary = r.PostFormValue("article-summary") + article.CreatorID = session.User.ID + article.IsInIssue = r.PostFormValue("issue") == "on" + if len(article.Title) == 0 { http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest) return @@ -270,20 +284,13 @@ func ResubmitArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerF return } - article.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - content := r.PostFormValue("article-content") if len(content) == 0 { http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest) return } - contentLink := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") - if err = os.WriteFile(contentLink, []byte(content), 0644); err != nil { + + if err = b.WriteArticleToFile(c, article.UUID, []byte(content)); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -448,7 +455,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s map[string]*Session) http.Ha data.Image = data.Article.BannerLink - articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") + articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.UUID, ".md") content, err := os.ReadFile(articleAbsName) if err != nil { log.Println(err) @@ -579,7 +586,7 @@ func PublishArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFu return } - if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")); err != nil { + if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.UUID, ".md")); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -757,7 +764,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s map[string]*Session, action, title, return } - articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") + articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.UUID, ".md") content, err := os.ReadFile(articleAbsName) if err != nil { log.Println(err) @@ -819,13 +826,20 @@ func DeleteArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFun return } + article, err := db.GetArticle(id) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err = db.DeleteArticle(id); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", id, ".md")); err != nil { + if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", article.UUID, ".md")); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -896,8 +910,8 @@ func AllowEditArticle(c *b.Config, db *b.DB, s map[string]*Session) http.Handler return } - src := fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md") - dst := fmt.Sprint(c.ArticleDir, "/", newArticle.ID, ".md") + src := fmt.Sprint(c.ArticleDir, "/", oldArticle.UUID, ".md") + dst := fmt.Sprint(c.ArticleDir, "/", newArticle.UUID, ".md") if err = b.CopyFile(src, dst); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -973,7 +987,7 @@ func EditArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc data.Image = data.Article.BannerLink - content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")) + content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.UUID, ".md")) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cmd/frontend/docx.go b/cmd/frontend/docx.go new file mode 100644 index 0000000..9cf4712 --- /dev/null +++ b/cmd/frontend/docx.go @@ -0,0 +1,107 @@ +package frontend + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gabriel-vasile/mimetype" + "github.com/google/uuid" + b "streifling.com/jason/cpolis/cmd/backend" +) + +func UploadDocx(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session, err := ManageSession(w, r, c, s) + if err != nil { + http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized) + return + } + + file, fileHeader, err := r.FormFile("docx-upload") + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + var buf bytes.Buffer + if _, err = io.Copy(&buf, file); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + mime := mimetype.Detect(buf.Bytes()) + if !mime.Is("application/vnd.openxmlformats-officedocument.wordprocessingml.document") { + http.Error(w, "Die Datei ist kein DOCX Worddokument.", http.StatusBadRequest) + return + } + + docxFilename := fmt.Sprint(uuid.New(), ".docx") + absDocxFilepath, err := filepath.Abs("/tmp/" + docxFilename) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err = os.WriteFile(absDocxFilepath, buf.Bytes(), 0644); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer os.Remove(absDocxFilepath) + + mdString, err := b.ConvertToMarkdown(c, absDocxFilepath) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + uuidName := uuid.New() + mdFilename := fmt.Sprint(uuidName, ".md") + absMdFilepath, err := filepath.Abs(c.ArticleDir + "/" + mdFilename) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err = os.WriteFile(absMdFilepath, mdString, 0644); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + article := &b.Article{ + Created: time.Now(), + UUID: uuidName, + CreatorID: session.User.ID, + Rejected: true, + } + article.Title = fmt.Sprint(fileHeader.Filename, "-", article.UUID) + + id, err := db.AddArticle(article) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err = db.WriteArticleAuthors(id, []int64{session.User.ID}); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + } +} diff --git a/cmd/frontend/issues.go b/cmd/frontend/issues.go index f40450d..ee663af 100644 --- a/cmd/frontend/issues.go +++ b/cmd/frontend/issues.go @@ -8,6 +8,7 @@ import ( "os" "time" + "github.com/google/uuid" b "streifling.com/jason/cpolis/cmd/backend" ) @@ -26,6 +27,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s map[string]*Session) http.Handl Rejected: false, Created: time.Now(), AutoGenerated: true, + UUID: uuid.New(), } if len(article.Title) == 0 { @@ -55,7 +57,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s map[string]*Session) http.Handl return } - articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") + articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.UUID, ".md") if err = os.WriteFile(articleAbsName, content, 0644); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cmd/main.go b/cmd/main.go index 89980f1..0dcc0f0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -32,6 +32,8 @@ func main() { sessions, sessionExpiryChan := f.StartSessions() defer close(sessionExpiryChan) + go b.CleanUpImages(config) + mux := http.NewServeMux() mux.Handle("/web/static/", http.StripPrefix("/web/static/", http.FileServer(http.Dir(config.WebDir+"/static/")))) @@ -72,13 +74,14 @@ func main() { mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, sessions)) mux.HandleFunc("POST /article/upload-banner", f.UploadImage(config, sessions, "article-banner", "editor.html", "article-banner-template")) mux.HandleFunc("POST /article/upload-image", f.UploadEasyMDEImage(config, sessions)) + mux.HandleFunc("POST /docx/upload", f.UploadDocx(config, db, sessions)) mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, sessions)) mux.HandleFunc("POST /issue/upload-banner", f.UploadImage(config, sessions, "issue-banner", "current-issue.html", "issue-banner-template")) mux.HandleFunc("POST /login", f.Login(config, db, sessions, sessionExpiryChan)) mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, sessions)) mux.HandleFunc("POST /tag/add", f.AddTag(config, db, sessions)) mux.HandleFunc("POST /user/add", f.AddUser(config, db, sessions)) - mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, sessions)) + mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, sessions, sessionExpiryChan)) mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, sessions)) mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, sessions)) mux.HandleFunc("POST /user/upload-profile-pic", f.UploadImage(config, sessions, "upload-profile-pic", "edit-user.html", "profile-pic-template")) diff --git a/create_db.sql b/create_db.sql index a79e89c..f246a8d 100644 --- a/create_db.sql +++ b/create_db.sql @@ -38,6 +38,7 @@ CREATE TABLE articles ( clicks INT NOT NULL, is_in_issue BOOL NOT NULL, auto_generated BOOL NOT NULL, + uuid VARCHAR(36) NOT NULL, PRIMARY KEY (id), FOREIGN KEY (creator_id) REFERENCES users (id), FOREIGN KEY (issue_id) REFERENCES issues (id) diff --git a/go.mod b/go.mod index 15a8364..ee15862 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/BurntSushi/toml v1.4.0 github.com/chai2010/webp v1.1.1 github.com/disintegration/imaging v1.6.2 + github.com/gabriel-vasile/mimetype v1.4.8 github.com/go-sql-driver/mysql v1.8.1 github.com/google/uuid v1.6.0 github.com/microcosm-cc/bluemonday v1.0.27 diff --git a/go.sum b/go.sum index 1c91977..eb8e52d 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6 github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/web/templates/hub.html b/web/templates/hub.html index 39803b1..b96cc8c 100644 --- a/web/templates/hub.html +++ b/web/templates/hub.html @@ -7,6 +7,11 @@