Compare commits

...

31 Commits

Author SHA1 Message Date
57953b2cfd Fix htmx version 2025-01-24 20:09:58 +01:00
a93603eac0 Use generic /bin/sh in docker-start.sh 2025-01-24 20:09:47 +01:00
1b72f05add Fix golang and tailwind version in Dockerfile 2025-01-24 20:09:23 +01:00
2c6b15bc6d Make docker-start.sh executable 2025-01-24 18:52:00 +01:00
cafb158323 Bug fix 2025-01-24 18:40:58 +01:00
40fbb93732 Add docker-start.sh 2025-01-24 18:28:29 +01:00
b120341d78 Correct docker-compose.yml 2025-01-24 18:28:18 +01:00
095576a234 Add tailwindcss to Dockerfile 2025-01-24 18:13:13 +01:00
9199f202be Check atom feed for nil returns 2025-01-24 17:42:05 +01:00
3d08cc7612 Make background image check way more efficient 2025-01-20 20:25:50 +01:00
a7b6fb9705 Correct probably last occurance of manually set path delimeter 2025-01-19 20:41:48 +01:00
2f4d5d4c7c Correct comment 2025-01-19 20:35:35 +01:00
5b417ef87d Change ValidateSession() to SessionIsActive() which only returns a bool 2025-01-19 20:34:12 +01:00
04283d5917 Restore old way of managing session while keeping slimmed down version 2025-01-19 20:29:10 +01:00
d882daeb01 Correct command line flag for ImgDir 2025-01-19 20:15:56 +01:00
f99358729c Create ValidateSession() to not have unwanted side effects when validating session 2025-01-19 20:10:51 +01:00
7b04149a28 Rename PicsDir to ImgDir 2025-01-19 20:10:06 +01:00
43c1cb6d9a Use proper filepaths for docker-compose 2025-01-19 20:08:58 +01:00
9feb16a8d8 Change filepaths to use filepath.Join() where possible 2025-01-19 20:07:32 +01:00
6885dfbb38 Add the ability to run cpolis from docker 2025-01-19 15:17:38 +01:00
86a16629fd Change htmx version to @latest 2025-01-19 10:16:07 +01:00
6d402aca32 Update copyright notice 2025-01-19 10:13:18 +01:00
0af4a70aed Include proper image cleanup 2025-01-19 10:04:16 +01:00
cc8693ffaf Serve article clicks via uuid 2025-01-19 09:34:03 +01:00
b8fd81a86d Use proper filepaths 2025-01-19 09:31:19 +01:00
62e75eaea8 Use the correct tmpDir 2025-01-19 09:00:02 +01:00
5d251c3659 Make all files and directories in the config absolute 2025-01-19 08:59:36 +01:00
5552e6d2eb Bug fix 2025-01-18 18:18:03 +01:00
77a64d5179 Bug fix 2025-01-17 19:37:44 +01:00
fb842d203e Fix bug 2025-01-17 19:35:01 +01:00
9c0c7361a0 Serve articles via uuid 2025-01-17 19:07:55 +01:00
24 changed files with 327 additions and 198 deletions

View File

@ -12,11 +12,11 @@ args_bin = [
"-domain localhost", "-domain localhost",
"-feed tmp/cpolis.atom", "-feed tmp/cpolis.atom",
"-firebase tmp/firebase.json", "-firebase tmp/firebase.json",
"-images tmp/pics",
"-img-width 256", "-img-width 256",
"-link https://distrikt-ni-st.de", "-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log", "-log tmp/cpolis.log",
"-pdfs tmp/pdfs", "-pdfs tmp/pdfs",
"-pics tmp/pics",
"-port 8080", "-port 8080",
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'", "-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
"-web web", "-web web",

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM golang:1.23
RUN apt update
RUN apt install -y pandoc
WORKDIR /var/www/cpolis
COPY . .
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.15/tailwindcss-linux-x64
RUN chmod +x tailwindcss-linux-x64
RUN mv tailwindcss-linux-x64 tailwindcss
RUN ./tailwindcss -i ./web/static/css/input.css -o ./web/static/css/style.css
RUN go build -o cpolis cmd/main.go
CMD ["./cpolis"]

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -115,6 +116,31 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
return article, nil return article, nil
} }
func (db *DB) GetArticleByUUID(u uuid.UUID) (*Article, error) {
query := `
SELECT id, title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated
FROM articles
WHERE uuid = ?
`
row := db.QueryRow(query, u.String())
article := new(Article)
var created []byte
var err error
if err := row.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.Clicks, &article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}
article.UUID = u
article.Created, err = time.Parse("2006-01-02 15:04:05", string(created))
if err != nil {
return nil, fmt.Errorf("error parsing created: %v", err)
}
return article, nil
}
func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) {
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, published, rejected, is_in_issue, auto_generated, uuid SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, published, rejected, is_in_issue, auto_generated, uuid
@ -309,9 +335,9 @@ func (db *DB) DeleteArticle(id int64) error {
} }
func WriteArticleToFile(c *Config, articleUUID uuid.UUID, content []byte) error { func WriteArticleToFile(c *Config, articleUUID uuid.UUID, content []byte) error {
articleAbsName := fmt.Sprint(c.ArticleDir, "/", articleUUID, ".md") articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(articleUUID, ".md"))
if err := os.WriteFile(articleAbsName, content, 0644); err != nil { if err := os.WriteFile(articlePath, content, 0644); err != nil {
return fmt.Errorf("error writing article %v to file: %v", articleUUID, err) return fmt.Errorf("error writing article %v to file: %v", articleUUID, err)
} }

View File

@ -1,6 +1,7 @@
package backend package backend
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -12,6 +13,9 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
feed := atom.NewFeed(c.Title) feed := atom.NewFeed(c.Title)
feed.ID = atom.NewID("urn:feed:1") feed.ID = atom.NewID("urn:feed:1")
feed.Subtitle = atom.NewText("text", c.Description) feed.Subtitle = atom.NewText("text", c.Description)
if feed.Subtitle == nil {
return nil, errors.New("feed subtitle was not created")
}
linkID := feed.AddLink(atom.NewLink(c.Link)) linkID := feed.AddLink(atom.NewLink(c.Link))
feed.Links[linkID].Rel = "self" feed.Links[linkID].Rel = "self"
@ -34,15 +38,24 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID)) entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID))
entry.Published = atom.NewDate(article.Created) entry.Published = atom.NewDate(article.Created)
entry.Content = atom.NewContent(atom.OutOfLine, "text/html", fmt.Sprint(c.Domain, "/article/serve/", article.UUID)) entry.Content = atom.NewContent(atom.OutOfLine, "text/html", fmt.Sprint(c.Domain, "/article/serve/", article.UUID))
if entry.Content == nil {
return nil, errors.New("entry content was not created")
}
if article.AutoGenerated { if article.AutoGenerated {
entry.Summary = atom.NewText("text", "automatically generated") entry.Summary = atom.NewText("text", "automatically generated")
if entry.Summary == nil {
return nil, errors.New("entry summary was not created")
}
} else { } else {
articleSummary, err := ConvertToPlain(article.Summary) articleSummary, err := ConvertToPlain(article.Summary)
if err != nil { if err != nil {
return nil, fmt.Errorf("error converting description to plain text for Atom feed: %v", err) return nil, fmt.Errorf("error converting description to plain text for Atom feed: %v", err)
} }
entry.Summary = atom.NewText("text", articleSummary) entry.Summary = atom.NewText("text", articleSummary)
if entry.Summary == nil {
return nil, errors.New("entry summary was not created")
}
} }
if len(article.BannerLink) > 0 { if len(article.BannerLink) > 0 {

View File

@ -20,10 +20,10 @@ type Config struct {
Description string Description string
Domain string Domain string
FirebaseKey string FirebaseKey string
ImgDir string
Link string Link string
LogFile string LogFile string
PDFDir string PDFDir string
PicsDir string
Port string Port string
Title string Title string
Version string Version string
@ -43,27 +43,24 @@ func newConfig() *Config {
ConfigFile: "/etc/cpolis/config.toml", ConfigFile: "/etc/cpolis/config.toml",
CookieExpiryHours: 24 * 30, CookieExpiryHours: 24 * 30,
DBName: "cpolis", DBName: "cpolis",
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json", FirebaseKey: "/etc/cpolis/serviceAccountKey.json",
ImgDir: "/var/www/cpolis/images",
LogFile: "/var/log/cpolis.log", LogFile: "/var/log/cpolis.log",
MaxBannerHeight: 1080, MaxBannerHeight: 1080,
MaxBannerWidth: 1920, MaxBannerWidth: 1920,
MaxImgHeight: 1080, MaxImgHeight: 1080,
MaxImgWidth: 1920, MaxImgWidth: 1920,
PDFDir: "/var/www/cpolis/pdfs", PDFDir: "/var/www/cpolis/pdfs",
PicsDir: "/var/www/cpolis/pics", Port: ":1664",
Port: ":8080", Version: "v0.16.0",
Version: "v0.15.0",
WebDir: "/var/www/cpolis/web", WebDir: "/var/www/cpolis/web",
} }
} }
func mkDir(path string, perm fs.FileMode) (string, error) { func mkDir(path string, perm fs.FileMode) (string, error) {
var err error name := filepath.Base(path)
stringSlice := strings.Split(path, "/") path, err := filepath.Abs(path)
name := stringSlice[len(stringSlice)-1]
path, err = filepath.Abs(path)
if err != nil { if err != nil {
return "", fmt.Errorf("error finding absolute path for %v directory: %v", name, err) return "", fmt.Errorf("error finding absolute path for %v directory: %v", name, err)
} }
@ -82,20 +79,20 @@ func mkFile(path string, filePerm, dirPerm fs.FileMode) (string, error) {
return "", fmt.Errorf("error finding absolute path for %v: %v", path, err) return "", fmt.Errorf("error finding absolute path for %v: %v", path, err)
} }
stringSlice := strings.Split(path, "/")
_, err = os.Stat(path) _, err = os.Stat(path)
if os.IsNotExist(err) { if os.IsNotExist(err) {
dir := strings.Join(stringSlice[:len(stringSlice)-1], "/") dir := filepath.Dir(path)
if err = os.MkdirAll(dir, dirPerm); err != nil { if err = os.MkdirAll(dir, dirPerm); err != nil {
return "", fmt.Errorf("error creating %v: %v", dir, err) return "", fmt.Errorf("error creating %v: %v", dir, err)
} }
fileName := stringSlice[len(stringSlice)-1] fileName := filepath.Base(path)
file, err := os.Create(dir + "/" + fileName) file, err := os.Create(filepath.Join(dir, fileName))
if err != nil { if err != nil {
return "", fmt.Errorf("error creating %v: %v", fileName, err) return "", fmt.Errorf("error creating %v: %v", fileName, err)
} }
defer file.Close() defer file.Close()
if err = file.Chmod(filePerm); err != nil { if err = file.Chmod(filePerm); err != nil {
return "", fmt.Errorf("error setting permissions for %v: %v", fileName, err) return "", fmt.Errorf("error setting permissions for %v: %v", fileName, err)
} }
@ -116,10 +113,10 @@ func (c *Config) handleCliArgs() error {
flag.StringVar(&c.Description, "desc", c.Description, "channel description") flag.StringVar(&c.Description, "desc", c.Description, "channel description")
flag.StringVar(&c.Domain, "domain", c.Domain, "domain name") flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
flag.StringVar(&c.FirebaseKey, "firebase", c.FirebaseKey, "Firebase service account key file") flag.StringVar(&c.FirebaseKey, "firebase", c.FirebaseKey, "Firebase service account key file")
flag.StringVar(&c.ImgDir, "images", c.ImgDir, "images directory")
flag.StringVar(&c.Link, "link", c.Link, "channel Link") flag.StringVar(&c.Link, "link", c.Link, "channel Link")
flag.StringVar(&c.LogFile, "log", c.LogFile, "log file") flag.StringVar(&c.LogFile, "log", c.LogFile, "log file")
flag.StringVar(&c.PDFDir, "pdfs", c.PDFDir, "pdf directory") flag.StringVar(&c.PDFDir, "pdfs", c.PDFDir, "pdf directory")
flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory")
flag.StringVar(&c.Title, "title", c.Title, "channel title") flag.StringVar(&c.Title, "title", c.Title, "channel title")
flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory") flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory")
flag.IntVar(&c.CookieExpiryHours, "cookie-expiry-hours", c.CookieExpiryHours, "cookies expire after this amount of hours") flag.IntVar(&c.CookieExpiryHours, "cookie-expiry-hours", c.CookieExpiryHours, "cookies expire after this amount of hours")
@ -158,6 +155,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.AESKeyFile != defaultConfig.AESKeyFile { if cliConfig.AESKeyFile != defaultConfig.AESKeyFile {
c.AESKeyFile = cliConfig.AESKeyFile c.AESKeyFile = cliConfig.AESKeyFile
} }
c.AESKeyFile, err = filepath.Abs(c.AESKeyFile)
if err != nil {
return fmt.Errorf("error setting absolute filepath for AESKeyFile: %v", err)
}
c.AESKeyFile, err = mkFile(c.AESKeyFile, 0600, 0700) c.AESKeyFile, err = mkFile(c.AESKeyFile, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) return fmt.Errorf("error setting up file: %v", err)
@ -166,6 +167,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.ArticleDir != defaultConfig.ArticleDir { if cliConfig.ArticleDir != defaultConfig.ArticleDir {
c.ArticleDir = cliConfig.ArticleDir c.ArticleDir = cliConfig.ArticleDir
} }
c.ArticleDir, err = filepath.Abs(c.ArticleDir)
if err != nil {
return fmt.Errorf("error setting absolute filepath for ArticleDir: %v", err)
}
c.ArticleDir, err = mkDir(c.ArticleDir, 0700) c.ArticleDir, err = mkDir(c.ArticleDir, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error setting up directory: %v", err)
@ -174,6 +179,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.AtomFile != defaultConfig.AtomFile { if cliConfig.AtomFile != defaultConfig.AtomFile {
c.AtomFile = cliConfig.AtomFile c.AtomFile = cliConfig.AtomFile
} }
c.AtomFile, err = filepath.Abs(c.AtomFile)
if err != nil {
return fmt.Errorf("error setting absolute filepath for AtomFile: %v", err)
}
c.AtomFile, err = mkFile(c.AtomFile, 0644, 0744) c.AtomFile, err = mkFile(c.AtomFile, 0644, 0744)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) return fmt.Errorf("error setting up file: %v", err)
@ -202,11 +211,27 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.FirebaseKey != defaultConfig.FirebaseKey { if cliConfig.FirebaseKey != defaultConfig.FirebaseKey {
c.FirebaseKey = cliConfig.FirebaseKey c.FirebaseKey = cliConfig.FirebaseKey
} }
c.FirebaseKey, err = filepath.Abs(c.FirebaseKey)
if err != nil {
return fmt.Errorf("error setting absolute filepath for FirebaseKey: %v", err)
}
c.FirebaseKey, err = mkFile(c.FirebaseKey, 0600, 0700) c.FirebaseKey, err = mkFile(c.FirebaseKey, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) return fmt.Errorf("error setting up file: %v", err)
} }
if cliConfig.ImgDir != defaultConfig.ImgDir {
c.ImgDir = cliConfig.ImgDir
}
c.ImgDir, err = filepath.Abs(c.ImgDir)
if err != nil {
return fmt.Errorf("error setting absolute filepath for PicsDir: %v", err)
}
c.ImgDir, err = mkDir(c.ImgDir, 0700)
if err != nil {
return fmt.Errorf("error setting up directory: %v", err)
}
if cliConfig.Link != defaultConfig.Link { if cliConfig.Link != defaultConfig.Link {
c.Link = cliConfig.Link c.Link = cliConfig.Link
} }
@ -214,6 +239,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.LogFile != defaultConfig.LogFile { if cliConfig.LogFile != defaultConfig.LogFile {
c.LogFile = cliConfig.LogFile c.LogFile = cliConfig.LogFile
} }
c.LogFile, err = filepath.Abs(c.LogFile)
if err != nil {
return fmt.Errorf("error setting absolute filepath for LogFile: %v", err)
}
c.LogFile, err = mkFile(c.LogFile, 0600, 0700) c.LogFile, err = mkFile(c.LogFile, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) return fmt.Errorf("error setting up file: %v", err)
@ -238,15 +267,11 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.PDFDir != defaultConfig.PDFDir { if cliConfig.PDFDir != defaultConfig.PDFDir {
c.PDFDir = cliConfig.PDFDir c.PDFDir = cliConfig.PDFDir
} }
c.PDFDir, err = mkDir(c.PDFDir, 0700) c.PDFDir, err = filepath.Abs(c.PDFDir)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error setting absolute filepath for PDFDir: %v", err)
} }
c.PDFDir, err = mkDir(c.PDFDir, 0700)
if cliConfig.PicsDir != defaultConfig.PicsDir {
c.PicsDir = cliConfig.PicsDir
}
c.PicsDir, err = mkDir(c.PicsDir, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error setting up directory: %v", err)
} }
@ -262,6 +287,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.WebDir != defaultConfig.WebDir { if cliConfig.WebDir != defaultConfig.WebDir {
c.WebDir = cliConfig.WebDir c.WebDir = cliConfig.WebDir
} }
c.WebDir, err = filepath.Abs(c.WebDir)
if err != nil {
return fmt.Errorf("error setting absolute filepath for WebDir: %v", err)
}
c.WebDir, err = mkDir(c.WebDir, 0700) c.WebDir, err = mkDir(c.WebDir, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error setting up directory: %v", err)

View File

@ -14,15 +14,13 @@ import (
func ConvertToMarkdown(c *Config, filename string) ([]byte, error) { func ConvertToMarkdown(c *Config, filename string) ([]byte, error) {
var stderr bytes.Buffer var stderr bytes.Buffer
articleID := uuid.New() tmpDir, err := os.MkdirTemp(os.TempDir(), "cpolis_images")
articleFileName := fmt.Sprint("/tmp/", articleID, ".md")
tmpDir, err := os.MkdirTemp("/tmp", "cpolis_images")
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating temporary directory: %v", err) return nil, fmt.Errorf("error creating temporary directory: %v", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
articleFileName := filepath.Join(os.TempDir(), fmt.Sprint(uuid.New(), ".md"))
cmd := exec.Command("pandoc", "-s", "-f", "docx", "-t", "commonmark_x", "-o", articleFileName, "--extract-media", tmpDir, filename) // TODO: Is writing to a file necessary? 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 cmd.Stderr = &stderr
if err = cmd.Run(); err != nil { if err = cmd.Run(); err != nil {
@ -35,7 +33,7 @@ func ConvertToMarkdown(c *Config, filename string) ([]byte, error) {
return nil, fmt.Errorf("error reading markdown file: %v", err) return nil, fmt.Errorf("error reading markdown file: %v", err)
} }
imageNames, err := filepath.Glob(filepath.Join(tmpDir, "/media/*")) imageNames, err := filepath.Glob(filepath.Join(tmpDir, "media", "*"))
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting docx images from temporary directory: %v", err) return nil, fmt.Errorf("error getting docx images from temporary directory: %v", err)
} }
@ -47,12 +45,12 @@ func ConvertToMarkdown(c *Config, filename string) ([]byte, error) {
} }
defer image.Close() defer image.Close()
newImageName, err := SaveImage(image, c.MaxImgHeight, c.MaxImgWidth, c.PicsDir) newImageName, err := SaveImage(image, c.MaxImgHeight, c.MaxImgWidth, c.ImgDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("error saving image %v: %v", name, err) return nil, fmt.Errorf("error saving image %v: %v", name, err)
} }
articleContent = regexp.MustCompile(name).ReplaceAll(articleContent, []byte(c.PicsDir+"/"+newImageName)) articleContent = regexp.MustCompile(name).ReplaceAll(articleContent, []byte(c.Domain+"/image/serve/"+newImageName))
} }
return articleContent, nil return articleContent, nil

View File

@ -6,11 +6,9 @@ import (
"image" "image"
"io" "io"
"io/fs" "io/fs"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/chai2010/webp" "github.com/chai2010/webp"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
@ -19,6 +17,53 @@ import (
var ErrUnsupportedFormat error = image.ErrFormat // used internally by imaging var ErrUnsupportedFormat error = image.ErrFormat // used internally by imaging
func checkImageUsage(c *Config, db *DB, name string) (bool, error) {
imageWasFound := false
if err := filepath.Walk(c.ArticleDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("error walking articles filepath: %v", err)
}
if !info.IsDir() {
mdFile, err := os.Open(path)
if err != nil {
return fmt.Errorf("error opening article %v: %v", info.Name(), err)
}
defer mdFile.Close()
scanner := bufio.NewScanner(mdFile)
for scanner.Scan() {
if strings.Contains(scanner.Text(), name) {
imageWasFound = true
return nil
}
}
return scanner.Err()
}
return nil
}); err != nil {
return false, fmt.Errorf("error walking articles filepath: %v", err)
}
if !imageWasFound {
users, err := db.GetAllUsers(c)
if err != nil {
return false, fmt.Errorf("error getting all users: %v", err)
}
for _, user := range users {
if name == user.ProfilePicLink {
return true, nil
}
}
}
return imageWasFound, nil
}
func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, error) { func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, error) {
img, err := imaging.Decode(src, imaging.AutoOrientation(true)) img, err := imaging.Decode(src, imaging.AutoOrientation(true))
if err != nil { if err != nil {
@ -49,58 +94,32 @@ func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, err
return filename, nil return filename, nil
} }
func CleanUpImages(c *Config) { func CleanUpImages(c *Config, db *DB) error {
for { if err := filepath.Walk(c.ImgDir, func(path string, info fs.FileInfo, err error) error {
if err := filepath.Walk(c.PicsDir, func(path string, info fs.FileInfo, err error) error { if err != nil {
if err != nil { return fmt.Errorf("error walking images filepath: %v", err)
return err
}
if !info.IsDir() {
imageName := info.Name()
absImageName := path
if err = filepath.Walk(c.ArticleDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
mdFile, err := os.Open(path)
if err != nil {
return err
}
defer mdFile.Close()
scanner := bufio.NewScanner(mdFile)
imageWasFound := false
for scanner.Scan() {
if strings.Contains(scanner.Text(), imageName) {
imageWasFound = true
}
}
if !imageWasFound {
if err = os.Remove(absImageName); err != nil {
return err
}
}
return scanner.Err()
}
return nil
}); err != nil {
return err
}
}
return nil
}); err != nil {
log.Println(err)
} }
time.Sleep(time.Hour) if !info.IsDir() {
imageName := info.Name()
imagePath := path
imageWasFound, err := checkImageUsage(c, db, imageName)
if err != nil {
return fmt.Errorf("error checking image usage: %v", err)
}
if !imageWasFound {
if err = os.Remove(imagePath); err != nil {
return fmt.Errorf("error removing unused image: %v", err)
}
}
}
return nil
}); err != nil {
return fmt.Errorf("error cleaning up: %v", err)
} }
return nil
} }

View File

@ -5,8 +5,9 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"strconv" "path/filepath"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
@ -37,15 +38,15 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return return
} }
idString := r.PathValue("id") uuidString := r.PathValue("uuid")
id, err := strconv.ParseInt(idString, 10, 64) uuid, err := uuid.Parse(uuidString)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
article, err := db.GetArticle(id) article, err := db.GetArticleByUUID(uuid)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -56,8 +57,8 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(article.UUID, ".md"))
contentBytes, err := os.ReadFile(articleAbsName) contentBytes, err := os.ReadFile(articlePath)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -87,15 +88,14 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
func ServeClicks(db *b.DB) http.HandlerFunc { func ServeClicks(db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
idString := r.PathValue("id") uuid, err := uuid.Parse(r.PathValue("uuid"))
id, err := strconv.ParseInt(idString, 10, 64)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
article, err := db.GetArticle(id) article, err := db.GetArticleByUUID(uuid)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -1,9 +1,7 @@
package calls package calls
import ( import (
"log"
"net/http" "net/http"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
@ -14,13 +12,6 @@ func ServeAtomFeed(c *b.Config) http.HandlerFunc {
return return
} }
absFilepath, err := filepath.Abs(c.AtomFile) http.ServeFile(w, r, c.AtomFile)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeFile(w, r, absFilepath)
} }
} }

View File

@ -1,7 +1,6 @@
package calls package calls
import ( import (
"log"
"net/http" "net/http"
"path/filepath" "path/filepath"
@ -11,19 +10,12 @@ import (
func ServeImage(c *b.Config, s map[string]*f.Session) http.HandlerFunc { func ServeImage(c *b.Config, s map[string]*f.Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := f.ManageSession(w, r, c, s); err != nil { if !f.SessionIsActive(r, s) {
if !tokenIsVerified(w, r, c) { if !tokenIsVerified(w, r, c) {
return return
} }
} }
absFilepath, err := filepath.Abs(c.PicsDir) http.ServeFile(w, r, filepath.Join(c.ImgDir, r.PathValue("pic")))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic"))
} }
} }

View File

@ -5,6 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
@ -42,6 +43,6 @@ func ServePDF(c *b.Config) http.HandlerFunc {
return return
} }
http.ServeFile(w, r, c.PDFDir+"/"+r.PathValue("id")) http.ServeFile(w, r, filepath.Join(c.PDFDir, r.PathValue("id")))
} }
} }

View File

@ -6,6 +6,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -83,7 +84,7 @@ func WriteArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc
return return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "editor.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -205,7 +206,7 @@ func SubmitArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFun
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -340,7 +341,7 @@ func ResubmitArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerF
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -384,7 +385,7 @@ func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB
} }
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "unpublished-articles.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -420,7 +421,7 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s map[string]*Session) http.Han
} }
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rejected-articles.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "rejected-articles.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -455,8 +456,8 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s map[string]*Session) http.Ha
data.Image = data.Article.BannerLink data.Image = data.Article.BannerLink
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.UUID, ".md") articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(data.Article.UUID, ".md"))
content, err := os.ReadFile(articleAbsName) content, err := os.ReadFile(articlePath)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -524,7 +525,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s map[string]*Session) http.Ha
data.Action = fmt.Sprint("resubmit/", data.Article.ID) data.Action = fmt.Sprint("resubmit/", data.Article.ID)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "editor.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -586,7 +587,7 @@ func PublishArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFu
return return
} }
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.UUID, ".md")); err != nil { if err = os.Remove(filepath.Join(c.ArticleDir, fmt.Sprint(oldArticle.UUID, ".md"))); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -614,7 +615,7 @@ func PublishArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFu
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -648,7 +649,7 @@ func RejectArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFun
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -672,7 +673,7 @@ func ShowCurrentIssue(c *b.Config, db *b.DB, s map[string]*Session) http.Handler
return return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/current-issue.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "current-issue.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -707,7 +708,7 @@ func ShowPublishedArticles(c *b.Config, db *b.DB, s map[string]*Session, action
} }
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "published-articles.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -764,8 +765,8 @@ func ReviewArticle(c *b.Config, db *b.DB, s map[string]*Session, action, title,
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.UUID, ".md") articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(article.UUID, ".md"))
content, err := os.ReadFile(articleAbsName) content, err := os.ReadFile(articlePath)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -802,7 +803,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s map[string]*Session, action, title,
return return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/review-article.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "review-article.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -839,12 +840,19 @@ func DeleteArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFun
return return
} }
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", article.UUID, ".md")); err != nil { if err = os.Remove(filepath.Join(c.ArticleDir, fmt.Sprint(article.UUID, ".md"))); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
go func(c *b.Config, db *b.DB) {
if err = b.CleanUpImages(c, db); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}(c, db)
feed, err := b.GenerateAtomFeed(c, db) feed, err := b.GenerateAtomFeed(c, db)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -860,7 +868,7 @@ func DeleteArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFun
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -910,8 +918,8 @@ func AllowEditArticle(c *b.Config, db *b.DB, s map[string]*Session) http.Handler
return return
} }
src := fmt.Sprint(c.ArticleDir, "/", oldArticle.UUID, ".md") src := filepath.Join(c.ArticleDir, fmt.Sprint(oldArticle.UUID, ".md"))
dst := fmt.Sprint(c.ArticleDir, "/", newArticle.UUID, ".md") dst := filepath.Join(c.ArticleDir, fmt.Sprint(newArticle.UUID, ".md"))
if err = b.CopyFile(src, dst); err != nil { if err = b.CopyFile(src, dst); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -953,7 +961,7 @@ func AllowEditArticle(c *b.Config, db *b.DB, s map[string]*Session) http.Handler
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl := template.Must(template.ParseFiles(c.WebDir + "/templates/hub.html")) tmpl := template.Must(template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html")))
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -987,7 +995,7 @@ func EditArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc
data.Image = data.Article.BannerLink data.Image = data.Article.BannerLink
content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.UUID, ".md")) content, err := os.ReadFile(filepath.Join(c.ArticleDir, fmt.Sprint(data.Article.UUID, ".md")))
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -1015,7 +1023,7 @@ func EditArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc
data.Action = fmt.Sprint("save/", data.Article.ID) data.Action = fmt.Sprint("save/", data.Article.ID)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "editor.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -44,22 +44,15 @@ func UploadDocx(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return return
} }
docxFilename := fmt.Sprint(uuid.New(), ".docx") docxFilepath := filepath.Join(os.TempDir(), fmt.Sprint(uuid.New(), ".docx"))
absDocxFilepath, err := filepath.Abs("/tmp/" + docxFilename) if err = os.WriteFile(docxFilepath, buf.Bytes(), 0644); err != nil {
if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
defer os.Remove(docxFilepath)
if err = os.WriteFile(absDocxFilepath, buf.Bytes(), 0644); err != nil { mdString, err := b.ConvertToMarkdown(c, docxFilepath)
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer os.Remove(absDocxFilepath)
mdString, err := b.ConvertToMarkdown(c, absDocxFilepath)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -67,15 +60,8 @@ func UploadDocx(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
} }
uuidName := uuid.New() uuidName := uuid.New()
mdFilename := fmt.Sprint(uuidName, ".md") mdFilepath := filepath.Join(c.ArticleDir, fmt.Sprint(uuidName, ".md"))
absMdFilepath, err := filepath.Abs(c.ArticleDir + "/" + mdFilename) if err = os.WriteFile(mdFilepath, mdString, 0644); err != nil {
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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View File

@ -4,6 +4,7 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"path/filepath"
"time" "time"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
@ -24,14 +25,14 @@ func HomePage(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
data.Version = c.Version data.Version = c.Version
files := make([]string, 2) files := make([]string, 2)
files[0] = c.WebDir + "/templates/index.html" files[0] = filepath.Join(c.WebDir, "templates", "index.html")
if numRows == 0 { if numRows == 0 {
data.Role = b.NonExistent data.Role = b.NonExistent
data.Title = "Erster Benutzer (Administrator)" data.Title = "Erster Benutzer (Administrator)"
data.ButtonText = "Anlegen" data.ButtonText = "Anlegen"
data.URL = "/user/add-first" data.URL = "/user/add-first"
files[1] = c.WebDir + "/templates/edit-user.html" files[1] = filepath.Join(c.WebDir, "templates", "edit-user.html")
tmpl, err := template.ParseFiles(files...) tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil { if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err) log.Println(err)
@ -41,7 +42,7 @@ func HomePage(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
} else { } else {
cookie, err := r.Cookie("cpolis_session") cookie, err := r.Cookie("cpolis_session")
if err != nil { if err != nil {
files[1] = c.WebDir + "/templates/login.html" files[1] = filepath.Join(c.WebDir, "templates", "login.html")
tmpl, err := template.ParseFiles(files...) tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil { if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err) log.Println(err)
@ -56,7 +57,7 @@ func HomePage(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
cookie.Expires = time.Now() cookie.Expires = time.Now()
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
files[1] = c.WebDir + "/templates/login.html" files[1] = filepath.Join(c.WebDir, "templates", "login.html")
tmpl, err := template.ParseFiles(files...) tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil { if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err) log.Println(err)
@ -67,7 +68,7 @@ func HomePage(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
} }
data.Role = session.User.Role data.Role = session.User.Role
files[1] = c.WebDir + "/templates/hub.html" files[1] = filepath.Join(c.WebDir, "templates", "hub.html")
tmpl, err := template.ParseFiles(files...) tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil { if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err) log.Println(err)
@ -89,7 +90,7 @@ func ShowHub(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -5,6 +5,7 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
@ -24,7 +25,7 @@ func UploadEasyMDEImage(c *b.Config, s map[string]*Session) http.HandlerFunc {
} }
defer file.Close() defer file.Close()
filename, err := b.SaveImage(file, c.MaxImgHeight, c.MaxImgWidth, c.PicsDir+"/") filename, err := b.SaveImage(file, c.MaxImgHeight, c.MaxImgWidth, c.ImgDir)
if err != nil { if err != nil {
if err == b.ErrUnsupportedFormat { if err == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest) http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
@ -56,7 +57,7 @@ func UploadImage(c *b.Config, s map[string]*Session, fileKey, htmlFile, htmlTemp
} }
defer file.Close() defer file.Close()
filename, err := b.SaveImage(file, c.MaxBannerHeight, c.MaxBannerWidth, c.PicsDir+"/") filename, err := b.SaveImage(file, c.MaxBannerHeight, c.MaxBannerWidth, c.ImgDir)
if err != nil { if err != nil {
if err == b.ErrUnsupportedFormat { if err == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest) http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
@ -70,7 +71,7 @@ func UploadImage(c *b.Config, s map[string]*Session, fileKey, htmlFile, htmlTemp
data := new(struct{ Image string }) data := new(struct{ Image string })
data.Image = filename data.Image = filename
tmpl, err := template.ParseFiles(c.WebDir + "/templates/" + htmlFile) tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", htmlFile))
if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -6,6 +6,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -57,8 +58,8 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s map[string]*Session) http.Handl
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.UUID, ".md") articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(article.UUID, ".md"))
if err = os.WriteFile(articleAbsName, content, 0644); err != nil { if err = os.WriteFile(articlePath, content, 0644); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -91,7 +92,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s map[string]*Session) http.Handl
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)

View File

@ -44,17 +44,11 @@ func UploadPDF(c *b.Config, s map[string]*Session) http.HandlerFunc {
return return
} }
oldFilename := header.Filename oldFilename := strings.Join(strings.Split(header.Filename, ".")[:len(header.Filename)-1], ".")
oldFilename = strings.Join(strings.Split(oldFilename, ".")[:len(oldFilename)-1], ".")
filename := fmt.Sprint(oldFilename, ".", uuid.New(), ".pdf") filename := fmt.Sprint(oldFilename, ".", uuid.New(), ".pdf")
absFilepath, err := filepath.Abs(c.PDFDir + "/" + filename) filepath := filepath.Join(c.PDFDir, filename)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.WriteFile(absFilepath, file); err != nil { if err = b.WriteFile(filepath, file); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View File

@ -7,6 +7,7 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"path/filepath"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -63,11 +64,23 @@ func StartSessions() (map[string]*Session, chan string) {
return sessions, sessionExpiryChan return sessions, sessionExpiryChan
} }
// SessionIsActive is used for verifying that the user is logged in and returns
// a bool.
func SessionIsActive(r *http.Request, s map[string]*Session) bool {
cookie, err := r.Cookie("cpolis_session")
if err != nil {
return false
}
_, ok := s[cookie.Value]
return ok
}
// ManageSession is used for verifying that the user is logged in and returns // ManageSession is used for verifying that the user is logged in and returns
// their session and an error. It also handles cases where the user is not // their session and an error. It also handles cases where the user is not
// logged in. // logged in.
func ManageSession(w http.ResponseWriter, r *http.Request, c *b.Config, s map[string]*Session) (*Session, error) { func ManageSession(w http.ResponseWriter, r *http.Request, c *b.Config, s map[string]*Session) (*Session, error) {
tmpl, tmplErr := template.ParseFiles(c.WebDir+"/templates/index.html", c.WebDir+"/templates/login.html") tmpl, tmplErr := template.ParseFiles(filepath.Join(c.WebDir, "templates", "index.html"), filepath.Join(c.WebDir, "templates", "login.html"))
cookie, err := r.Cookie("cpolis_session") cookie, err := r.Cookie("cpolis_session")
if err != nil { if err != nil {
@ -124,7 +137,7 @@ func Login(c *b.Config, db *b.DB, s map[string]*Session, sessionExpiryChan chan
s[session.cookie.Value] = session s[session.cookie.Value] = session
http.SetCookie(w, session.cookie) http.SetCookie(w, session.cookie)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -135,7 +148,7 @@ func Login(c *b.Config, db *b.DB, s map[string]*Session, sessionExpiryChan chan
func Logout(c *b.Config, s map[string]*Session) http.HandlerFunc { func Logout(c *b.Config, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
tmpl, tmplErr := template.ParseFiles(c.WebDir + "/templates/login.html") tmpl, tmplErr := template.ParseFiles(filepath.Join(c.WebDir, "templates", "login.html"))
cookie, err := r.Cookie("cpolis_session") cookie, err := r.Cookie("cpolis_session")
if err != nil { if err != nil {

View File

@ -4,6 +4,7 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
@ -15,7 +16,7 @@ func CreateTag(c *b.Config, s map[string]*Session) http.HandlerFunc {
return return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "add-tag.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -42,7 +43,7 @@ func AddTag(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)

View File

@ -5,6 +5,7 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"path/filepath"
"sort" "sort"
"strconv" "strconv"
@ -57,7 +58,7 @@ func CreateUser(c *b.Config, s map[string]*Session) http.HandlerFunc {
URL: "/user/add", URL: "/user/add",
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "edit-user.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -134,7 +135,7 @@ func AddUser(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -167,7 +168,7 @@ func EditSelf(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
Image: user.ProfilePicLink, Image: user.ProfilePicLink,
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "edit-user.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -242,7 +243,7 @@ func UpdateSelf(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -312,7 +313,7 @@ func AddFirstUser(c *b.Config, db *b.DB, s map[string]*Session, sessionExpiryCha
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = user.Role data.Role = user.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -343,7 +344,7 @@ func ShowAllUsers(c *b.Config, db *b.DB, s map[string]*Session, action string) h
} }
delete(data.Users, session.User.ID) delete(data.Users, session.User.ID)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "show-all-users.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -381,7 +382,7 @@ func EditUser(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
Image: user.ProfilePicLink, Image: user.ProfilePicLink,
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "edit-user.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -466,7 +467,7 @@ func UpdateUser(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl := template.Must(template.ParseFiles(c.WebDir + "/templates/hub.html")) tmpl := template.Must(template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html")))
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -499,7 +500,7 @@ func DeleteUser(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
data := new(struct{ Role int }) data := new(struct{ Role int })
data.Role = session.User.Role data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil { if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)

View File

@ -4,6 +4,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"time"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
c "streifling.com/jason/cpolis/cmd/calls" c "streifling.com/jason/cpolis/cmd/calls"
@ -32,7 +33,14 @@ func main() {
sessions, sessionExpiryChan := f.StartSessions() sessions, sessionExpiryChan := f.StartSessions()
defer close(sessionExpiryChan) defer close(sessionExpiryChan)
// go b.CleanUpImages(config) go func(c *b.Config, db *b.DB) {
for {
if err = b.CleanUpImages(c, db); err != nil {
log.Println(err)
}
time.Sleep(time.Hour * 24)
}
}(config, db)
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/web/static/", http.StripPrefix("/web/static/", mux.Handle("/web/static/", http.StripPrefix("/web/static/",
@ -52,8 +60,8 @@ func main() {
mux.HandleFunc("GET /article/review-edit/{id}", f.ReviewArticle(config, db, sessions, "allow-edit", "Artikel bearbeiten", "Bearbeiten erlauben")) mux.HandleFunc("GET /article/review-edit/{id}", f.ReviewArticle(config, db, sessions, "allow-edit", "Artikel bearbeiten", "Bearbeiten erlauben"))
mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, sessions)) mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, sessions))
mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, sessions, "publish", "Artikel veröffentlichen", "Veröffentlichen")) mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, sessions, "publish", "Artikel veröffentlichen", "Veröffentlichen"))
mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db)) mux.HandleFunc("GET /article/serve/{uuid}", c.ServeArticle(config, db))
mux.HandleFunc("GET /article/serve/{id}/clicks", c.ServeClicks(db)) mux.HandleFunc("GET /article/serve/{uuid}/clicks", c.ServeClicks(db))
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, sessions)) mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, sessions))
mux.HandleFunc("GET /atom/serve", c.ServeAtomFeed(config)) mux.HandleFunc("GET /atom/serve", c.ServeAtomFeed(config))
mux.HandleFunc("GET /hub", f.ShowHub(config, db, sessions)) mux.HandleFunc("GET /hub", f.ShowHub(config, db, sessions))

22
docker-compose.yml Normal file
View File

@ -0,0 +1,22 @@
services:
app:
build: .
ports:
- "1664:1664"
volumes:
- /etc/cpolis/config.toml:/etc/cpolis/config.toml
- /etc/cpolis/serviceAccountKey.json:/etc/cpolis/serviceAccountKey.json
- /var/log/cpolis.log:/var/log/cpolis.log
depends_on:
- db
db:
image: mariadb:latest
environment:
MYSQL_DATABASE: cpolis
MYSQL_USER: cpolis
MYSQL_PASSWORD: ${DB_PASS}
ports:
- "3306:3306"
volumes:
- ./create_db.sql:/docker-entrypoint-initdb.d/create_db.sql

7
docker-start.sh Executable file
View File

@ -0,0 +1,7 @@
#! /bin/sh -
read -sp "Enter DB password: " DB_PASS
echo
export DB_PASS
docker-compose up -d

View File

@ -38,11 +38,11 @@
</main> </main>
<footer class="text-center text-gray-500 my-8"> <footer class="text-center text-gray-500 my-8">
<p>&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.</p> <p>&copy; 2025 Jason Streifling. Alle Rechte vorbehalten.</p>
<p>{{.Version}} - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p> <p>{{.Version}} - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p>
</footer> </footer>
<script src="https://unpkg.com/htmx.org@2.0.3"></script> <script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script> <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {