forked from jason/cpolis
		
	Instead of having entire articles in the RSS feed, items now contain just a link
This commit is contained in:
		@@ -4,6 +4,7 @@ tmp_dir = "tmp"
 | 
			
		||||
 | 
			
		||||
[build]
 | 
			
		||||
args_bin = [
 | 
			
		||||
    "-articles tmp/articles",
 | 
			
		||||
    "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
 | 
			
		||||
    "-domain localhost",
 | 
			
		||||
    "-key tmp/key.gob",
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ type Article struct {
 | 
			
		||||
	Created     time.Time
 | 
			
		||||
	Description string
 | 
			
		||||
	Content     string
 | 
			
		||||
	Link        string
 | 
			
		||||
	Published   bool
 | 
			
		||||
	Rejected    bool
 | 
			
		||||
	ID          int64
 | 
			
		||||
@@ -26,8 +27,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
			
		||||
	selectQuery := "SELECT id FROM issues WHERE published = false"
 | 
			
		||||
	insertQuery := `
 | 
			
		||||
    INSERT INTO articles
 | 
			
		||||
        (title, description, content, published, rejected, author_id, issue_id)
 | 
			
		||||
    VALUES (?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
        (title, description, content, link, published, rejected, author_id, issue_id)
 | 
			
		||||
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < TxMaxRetries; i++ {
 | 
			
		||||
@@ -45,7 +46,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			result, err := tx.Exec(insertQuery, a.Title, a.Description,
 | 
			
		||||
				a.Content, a.Published, a.Rejected, a.AuthorID, id)
 | 
			
		||||
				a.Content, a.Link, a.Published, a.Rejected, a.AuthorID, id)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
			
		||||
					log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
 | 
			
		||||
@@ -79,7 +80,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
			
		||||
 | 
			
		||||
func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
			
		||||
	query := `
 | 
			
		||||
    SELECT title, created, description, content, published, author_id
 | 
			
		||||
    SELECT title, created, description, content, link, published, author_id
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE id = ?
 | 
			
		||||
    `
 | 
			
		||||
@@ -90,7 +91,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	if err := row.Scan(&article.Title, &created, &article.Description,
 | 
			
		||||
		&article.Content, &article.Published, &article.AuthorID); err != nil {
 | 
			
		||||
		&article.Content, &article.Link, &article.Published, &article.AuthorID); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -105,7 +106,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
			
		||||
 | 
			
		||||
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
 | 
			
		||||
	query := `
 | 
			
		||||
    SELECT id, title, created, description, content, author_id, issue_id
 | 
			
		||||
    SELECT id, title, created, description, content, link, author_id, issue_id
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE published = ?
 | 
			
		||||
    AND rejected = ?
 | 
			
		||||
@@ -121,8 +122,8 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
 | 
			
		||||
		var created []byte
 | 
			
		||||
 | 
			
		||||
		if err = rows.Scan(&article.ID, &article.Title, &created,
 | 
			
		||||
			&article.Description, &article.Content, &article.AuthorID,
 | 
			
		||||
			&article.IssueID); err != nil {
 | 
			
		||||
			&article.Description, &article.Content, &article.Link,
 | 
			
		||||
			&article.AuthorID, &article.IssueID); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -143,7 +144,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, description, content, author_id
 | 
			
		||||
    SELECT id, title, created, description, content, link, author_id
 | 
			
		||||
    FROM articles
 | 
			
		||||
    WHERE issue_id = ? AND published = true
 | 
			
		||||
    `
 | 
			
		||||
@@ -177,7 +178,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
 | 
			
		||||
				var created []byte
 | 
			
		||||
 | 
			
		||||
				if err = rows.Scan(&article.ID, &article.Title, &created,
 | 
			
		||||
					&article.Description, &article.Content, &article.AuthorID); err != nil {
 | 
			
		||||
					&article.Description, &article.Content, &article.Link, &article.AuthorID); err != nil {
 | 
			
		||||
					if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
			
		||||
						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
 | 
			
		||||
					}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	ArticleDir  string
 | 
			
		||||
	DBName      string
 | 
			
		||||
	Description string
 | 
			
		||||
	Domain      string
 | 
			
		||||
@@ -28,6 +29,7 @@ type Config struct {
 | 
			
		||||
 | 
			
		||||
func newConfig() *Config {
 | 
			
		||||
	return &Config{
 | 
			
		||||
		ArticleDir:  "/var/www/cpolis/articles",
 | 
			
		||||
		DBName:      "cpolis",
 | 
			
		||||
		FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
 | 
			
		||||
		KeyFile:     "/var/www/cpolis/cpolis.key",
 | 
			
		||||
@@ -77,6 +79,7 @@ func (c *Config) handleCliArgs() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	port := 8080
 | 
			
		||||
 | 
			
		||||
	flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory")
 | 
			
		||||
	flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
 | 
			
		||||
	flag.StringVar(&c.Description, "desc", c.Description, "channel description")
 | 
			
		||||
	flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
 | 
			
		||||
@@ -92,6 +95,14 @@ func (c *Config) handleCliArgs() error {
 | 
			
		||||
	flag.IntVar(&port, "port", port, "port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	c.ArticleDir, err = filepath.Abs(c.ArticleDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error finding absolute path for articles directory: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err = os.MkdirAll(c.ArticleDir, 0755); err != nil {
 | 
			
		||||
		return fmt.Errorf("error creating articles directory: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.FirebaseKey, err = filepath.Abs(c.FirebaseKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error finding absolute path for Firebase service account key file: %v", err)
 | 
			
		||||
@@ -109,12 +120,18 @@ func (c *Config) handleCliArgs() error {
 | 
			
		||||
 | 
			
		||||
	c.PDFDir, err = filepath.Abs(c.PDFDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error finding absolute path for pdfs dir: %v", err)
 | 
			
		||||
		return fmt.Errorf("error finding absolute path for pdfs directory: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err = os.MkdirAll(c.PDFDir, 0755); err != nil {
 | 
			
		||||
		return fmt.Errorf("error creating pdfs directory: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.PicsDir, err = filepath.Abs(c.PicsDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error finding absolute path for pics dir: %v", err)
 | 
			
		||||
		return fmt.Errorf("error finding absolute path for pics directory: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err = os.MkdirAll(c.PicsDir, 0755); err != nil {
 | 
			
		||||
		return fmt.Errorf("error creating pics directory: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.Port = fmt.Sprint(":", port)
 | 
			
		||||
@@ -126,7 +143,10 @@ func (c *Config) handleCliArgs() error {
 | 
			
		||||
 | 
			
		||||
	c.WebDir, err = filepath.Abs(c.WebDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error finding absolute path for web dir: %v", err)
 | 
			
		||||
		return fmt.Errorf("error finding absolute path for web directory: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err = os.MkdirAll(c.WebDir, 0755); err != nil {
 | 
			
		||||
		return fmt.Errorf("error creating web directory: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
 | 
			
		||||
 | 
			
		||||
		channel.Items = append(channel.Items, &rss.Item{
 | 
			
		||||
			Title:       article.Title,
 | 
			
		||||
			Author:      user.FirstName + user.LastName,
 | 
			
		||||
			Author:      fmt.Sprint(user.FirstName, " ", user.LastName),
 | 
			
		||||
			PubDate:     article.Created.Format(time.RFC1123Z),
 | 
			
		||||
			Description: article.Description,
 | 
			
		||||
			Content:     &rss.Content{Value: article.Content},
 | 
			
		||||
@@ -89,14 +89,22 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
 | 
			
		||||
			return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		channel.Items = append(channel.Items, &rss.Item{
 | 
			
		||||
			Author:      fmt.Sprint(user.FirstName, user.LastName),
 | 
			
		||||
		item := &rss.Item{
 | 
			
		||||
			Author:      fmt.Sprint(user.FirstName, " ", user.LastName),
 | 
			
		||||
			Categories:  tagNames,
 | 
			
		||||
			Description: articleDescription,
 | 
			
		||||
			Link:        fmt.Sprintf("http://%s/article/serve/%d", c.Domain, article.ID),
 | 
			
		||||
			PubDate:     article.Created.Format(time.RFC1123Z),
 | 
			
		||||
			Title:       articleTitle,
 | 
			
		||||
		})
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println(article.Link, ": ", len(article.Link))
 | 
			
		||||
 | 
			
		||||
		if article.Link == "" {
 | 
			
		||||
			item.Link = fmt.Sprint("http://", c.Domain, "/article/serve/", article.ID, ".html")
 | 
			
		||||
		} else {
 | 
			
		||||
			item.Link = article.Link
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		channel.Items = append(channel.Items, item)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	feed := rss.NewFeed()
 | 
			
		||||
 
 | 
			
		||||
@@ -11,23 +11,25 @@ import (
 | 
			
		||||
 | 
			
		||||
func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if tokenIsVerified(w, r, c) {
 | 
			
		||||
			idString := r.PathValue("id")
 | 
			
		||||
			id, err := strconv.ParseInt(idString, 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
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			fmt.Fprint(w, article.Content)
 | 
			
		||||
		if !tokenIsVerified(w, r, c) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		idString := r.PathValue("id")
 | 
			
		||||
		id, err := strconv.ParseInt(idString, 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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Fprint(w, article.Content)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,15 +10,17 @@ import (
 | 
			
		||||
 | 
			
		||||
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if tokenIsVerified(w, r, c) {
 | 
			
		||||
			absFilepath, err := filepath.Abs(c.PicsDir)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic"))
 | 
			
		||||
		if !tokenIsVerified(w, r, c) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		absFilepath, err := filepath.Abs(c.PicsDir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic"))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,33 +12,37 @@ import (
 | 
			
		||||
 | 
			
		||||
func ServePDFList(c *b.Config) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if tokenIsVerified(w, r, c) {
 | 
			
		||||
			files, err := os.ReadDir(c.PDFDir)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		if !tokenIsVerified(w, r, c) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			fileNames := make([]string, 0)
 | 
			
		||||
			for _, file := range files {
 | 
			
		||||
				fileNames = append(fileNames, file.Name())
 | 
			
		||||
			}
 | 
			
		||||
		files, err := os.ReadDir(c.PDFDir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
			if err = json.NewEncoder(w).Encode(fileNames); err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		fileNames := make([]string, 0)
 | 
			
		||||
		for _, file := range files {
 | 
			
		||||
			fileNames = append(fileNames, file.Name())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
		if err = json.NewEncoder(w).Encode(fileNames); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ServePDF(c *b.Config) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if tokenIsVerified(w, r, c) {
 | 
			
		||||
			http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
 | 
			
		||||
		if !tokenIsVerified(w, r, c) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,10 @@ import (
 | 
			
		||||
 | 
			
		||||
func ServeRSS(c *b.Config) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if tokenIsVerified(w, r, c) {
 | 
			
		||||
			http.ServeFile(w, r, c.RSSFile)
 | 
			
		||||
		if !tokenIsVerified(w, r, c) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		http.ServeFile(w, r, c.RSSFile)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -347,6 +346,27 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article, err := db.GetArticle(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		content, err := b.ConvertToHTML(article.Content)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err = db.AddArticleToCurrentIssue(id); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
@@ -427,7 +447,7 @@ func ShowCurrentArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFu
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if _, err := getSession(w, r, c, s); err != nil {
 | 
			
		||||
			return
 | 
			
		||||
@@ -436,7 +456,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
		file, header, err := r.FormFile("article-image")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
@@ -451,12 +471,6 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err = os.MkdirAll(fmt.Sprint(c.PicsDir, "/"), fs.FileMode(0755)); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		img, err := os.Create(absFilepath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
@@ -471,7 +485,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		url := fmt.Sprint(c.Domain, "/pics/", filename)
 | 
			
		||||
		url := fmt.Sprint(c.Domain, "/image/serve/", filename)
 | 
			
		||||
		w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
		json.NewEncoder(w).Encode(url)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,17 @@
 | 
			
		||||
package frontend
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	b "streifling.com/jason/cpolis/cmd/backend"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -15,14 +22,141 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		session.Values["article"] = nil
 | 
			
		||||
		if err = session.Save(r, w); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if session.Values["issue-image"] == nil {
 | 
			
		||||
			err := "error: Image required"
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err, http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article := &b.Article{
 | 
			
		||||
			Title:     "Autogenerated Issue Article",
 | 
			
		||||
			Content:   r.PostFormValue("issue-content"),
 | 
			
		||||
			Link:      session.Values["issue-image"].(string),
 | 
			
		||||
			Published: true,
 | 
			
		||||
			Rejected:  false,
 | 
			
		||||
			Created:   time.Now(),
 | 
			
		||||
			AuthorID:  session.Values["id"].(int64),
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println(article.Link)
 | 
			
		||||
 | 
			
		||||
		content, err := b.ConvertToHTML(article.Content)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		article.ID, err = db.AddArticle(article)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := db.PublishLatestIssue(); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		feed, err := b.GenerateRSS(c, db)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if err = b.SaveRSS(c.RSSFile, feed); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		session.Values["issue-image"] = nil
 | 
			
		||||
		if err = session.Save(r, w); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
 | 
			
		||||
		tmpl = template.Must(tmpl, err)
 | 
			
		||||
		tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UploadIssueImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		session, err := getSession(w, r, c, s)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := r.ParseMultipartForm(10 << 20); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		file, header, err := r.FormFile("issue-image")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
 | 
			
		||||
		nameStrings := strings.Split(header.Filename, ".")
 | 
			
		||||
		extension := "." + nameStrings[len(nameStrings)-1]
 | 
			
		||||
		filename := fmt.Sprint(uuid.New(), extension)
 | 
			
		||||
		absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		img, err := os.Create(absFilepath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		defer img.Close()
 | 
			
		||||
 | 
			
		||||
		if _, err = io.Copy(img, file); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		session.Values["issue-image"] = filename
 | 
			
		||||
		if err = session.Save(r, w); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		w.WriteHeader(http.StatusOK)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		id, ok := db.GetID(userName)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusInternalServerError)
 | 
			
		||||
			http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -96,7 +96,6 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		session.Options.MaxAge = -1
 | 
			
		||||
 | 
			
		||||
		if err = session.Save(r, w); err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
 
 | 
			
		||||
@@ -198,10 +198,6 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if _, err := getSession(w, r, c, s); err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var err error
 | 
			
		||||
		htmlData := UserData{
 | 
			
		||||
			User: &b.User{
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,6 @@ func main() {
 | 
			
		||||
	mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config, store))
 | 
			
		||||
	mux.HandleFunc("GET /issue/publish", f.PublishLatestIssue(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /issue/this", f.ShowCurrentArticles(config, db, store))
 | 
			
		||||
	mux.HandleFunc("GET /logout", f.Logout(config, store))
 | 
			
		||||
	mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
 | 
			
		||||
@@ -78,7 +77,9 @@ func main() {
 | 
			
		||||
 | 
			
		||||
	mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /image/upload", f.UploadImage(config, store))
 | 
			
		||||
	mux.HandleFunc("POST /article/upload-image", f.UploadArticleImage(config, store))
 | 
			
		||||
	mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /issue/upload-image", f.UploadIssueImage(config, store))
 | 
			
		||||
	mux.HandleFunc("POST /login", f.Login(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
 | 
			
		||||
	mux.HandleFunc("POST /user/add", f.AddUser(config, db, store))
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ CREATE TABLE articles (
 | 
			
		||||
    title       VARCHAR(255)    NOT NULL,
 | 
			
		||||
    created     TIMESTAMP       DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    description TEXT            NOT NULL,
 | 
			
		||||
    content     TEXT            NOT NULL,
 | 
			
		||||
    link        VARCHAR(255),
 | 
			
		||||
    published   BOOL            NOT NULL,
 | 
			
		||||
    rejected    BOOL            NOT NULL,
 | 
			
		||||
    author_id   INT             NOT NULL,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,68 @@
 | 
			
		||||
{{define "page-content"}}
 | 
			
		||||
<h2>Aktuelle Artikel</h2>
 | 
			
		||||
<h2>Diese Ausgabe</h2>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-4">
 | 
			
		||||
    {{range .}}
 | 
			
		||||
    <div class="border px-2 py-1 rounded-md">
 | 
			
		||||
        <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
			
		||||
        <p>{{.Description}}</p>
 | 
			
		||||
<form hx-encoding="multipart/form-data">
 | 
			
		||||
    <div class="flex flex-col gap-4">
 | 
			
		||||
        <div>
 | 
			
		||||
            <h3>Aktuelle Artikel</h3>
 | 
			
		||||
            <div class="flex flex-col gap-4">
 | 
			
		||||
                {{range .}}
 | 
			
		||||
                <div class="border px-2 py-1 rounded-md">
 | 
			
		||||
                    <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
			
		||||
                    <p>{{.Description}}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                {{end}}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
            <h3>Cover</h3>
 | 
			
		||||
            <input id="image-upload" name="issue-image" type="file" required hx-post="/issue/upload-image" />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
            <h3>Über diese Ausgabe</h3>
 | 
			
		||||
            <div>
 | 
			
		||||
                <textarea id="easyMDE" placeholder="Beschreibung dieser Ausgabe"></textarea>
 | 
			
		||||
                <input id="issue-content" name="issue-content" type="hidden" />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {{end}}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="btn-area">
 | 
			
		||||
    <button class="action-btn" hx-get="/issue/publish" hx-target="#page-content">Ausgabe publizieren</button>
 | 
			
		||||
    <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
			
		||||
</div>
 | 
			
		||||
    <div class="btn-area">
 | 
			
		||||
        <button class="action-btn" hx-post="/issue/publish" hx-target="#page-content">Ausgabe publizieren</button>
 | 
			
		||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    var easyMDE = new EasyMDE({
 | 
			
		||||
        element: document.getElementById('easyMDE'),
 | 
			
		||||
        hideIcons: ['image'],
 | 
			
		||||
        imageTexts: {sbInit: ''},
 | 
			
		||||
        showIcons: ["code", "table", "upload-image"],
 | 
			
		||||
        uploadImage: true,
 | 
			
		||||
 | 
			
		||||
        imageUploadFunction: function (file, onSuccess, onError) {
 | 
			
		||||
            var formData = new FormData();
 | 
			
		||||
            formData.append('article-image', file);
 | 
			
		||||
 | 
			
		||||
            fetch('/article/upload-image', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                body: formData
 | 
			
		||||
            })
 | 
			
		||||
                .then(response => response.json())
 | 
			
		||||
                .then(data => {
 | 
			
		||||
                    onSuccess(data);
 | 
			
		||||
                })
 | 
			
		||||
                .catch(error => {
 | 
			
		||||
                    onError(error);
 | 
			
		||||
                });
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    easyMDE.codemirror.on("change", () => {
 | 
			
		||||
        document.getElementById('issue-content').value = easyMDE.value();
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{{end}}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,15 +6,17 @@
 | 
			
		||||
        <label for="article-title">Titel</label>
 | 
			
		||||
        <input name="article-title" type="text" value="{{.Title}}" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="flex flex-col">
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="article-description">Beschreibung</label>
 | 
			
		||||
        <textarea name="article-description">{{.Description}}</textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="flex flex-col">
 | 
			
		||||
        <label for="article-content">Artikel</label>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="easyMDE">Artikel</label>
 | 
			
		||||
        <textarea id="easyMDE">{{.Content}}</textarea>
 | 
			
		||||
        <input id="article-content" name="article-content" type="hidden" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <input id="article-content" name="article-content" type="hidden" />
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
        <span>Tags</span>
 | 
			
		||||
@@ -46,7 +48,7 @@
 | 
			
		||||
            var formData = new FormData();
 | 
			
		||||
            formData.append('article-image', file);
 | 
			
		||||
 | 
			
		||||
            fetch('/image/upload', {
 | 
			
		||||
            fetch('/article/upload-image', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                body: formData
 | 
			
		||||
            })
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@
 | 
			
		||||
    {{if eq . 0}}
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <h2>Administrator</h2>
 | 
			
		||||
        <div class="grid grid-cols-2 gap-4">
 | 
			
		||||
        <div class="grid grid-cols-2 gap-x-4 gap-y-2">
 | 
			
		||||
            <button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer hinzufügen</button>
 | 
			
		||||
            <button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer bearbeiten</button>
 | 
			
		||||
            <button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer löschen</button>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,15 +6,17 @@
 | 
			
		||||
        <label for="article-title">Titel</label>
 | 
			
		||||
        <input name="article-title" type="text" value="{{.Article.Title}}" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="flex flex-col">
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="article-description">Beschreibung</label>
 | 
			
		||||
        <textarea name="article-description">{{.Article.Description}}</textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="flex flex-col">
 | 
			
		||||
        <label for="article-content">Artikel</label>
 | 
			
		||||
        <textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-col gap-y-1">
 | 
			
		||||
        <label for="easyMDE">Artikel</label>
 | 
			
		||||
        <textarea id="easyMDE">{{.Article.Content}}</textarea>
 | 
			
		||||
        <input id="article-content" name="article-content" type="hidden" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <input id="article-content" name="article-content" type="hidden" />
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
        <span>Tags</span>
 | 
			
		||||
@@ -37,6 +39,8 @@
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    document.getElementById('article-content').value = easyMDE.value();
 | 
			
		||||
 | 
			
		||||
    var easyMDE = new EasyMDE({
 | 
			
		||||
        element: document.getElementById('easyMDE'),
 | 
			
		||||
        hideIcons: ['image'],
 | 
			
		||||
@@ -48,7 +52,7 @@
 | 
			
		||||
            var formData = new FormData();
 | 
			
		||||
            formData.append('article-image', file);
 | 
			
		||||
 | 
			
		||||
            fetch('/image/upload', {
 | 
			
		||||
            fetch('/article/upload-image', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                body: formData
 | 
			
		||||
            })
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user