Compare commits
	
		
			69 Commits
		
	
	
		
			firebase
			...
			66b2743d3d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 66b2743d3d | |||
| 18617f1dbc | |||
| 3723b2b5e6 | |||
| 79ee20a50e | |||
| e05521591b | |||
| 0c9a79e24a | |||
| ce788bfd50 | |||
| 8147d9bef6 | |||
| 230a6278cc | |||
| da0d65d40b | |||
| 42d6e0c198 | |||
| a2d219b2c0 | |||
| e1af2979af | |||
| b02a882ed7 | |||
| f6dedc6f10 | |||
| edb448413b | |||
| cdf0a49550 | |||
| 222b791e90 | |||
| c3c0650210 | |||
| bc58b1be44 | |||
| ab6b9b9a4f | |||
| 91ef195a56 | |||
| d077f700d8 | |||
| e4589f3b84 | |||
| cd67fe6df3 | |||
| ec752b1c66 | |||
| 4663cedec5 | |||
| 3f1b18c29f | |||
| 4a11e1a497 | |||
| a33e7f9896 | |||
| 2b2ab0d428 | |||
| c52e35bf0b | |||
| cddd88d2f6 | |||
| be467521d9 | |||
| 10d8fceb77 | |||
| 46aef4f12f | |||
| 714cdd9aaf | |||
| 1b29e328cf | |||
| 5474b17ce5 | |||
| a318a265d4 | |||
| e50cb819f3 | |||
| 365d5a68a1 | |||
| b451da7e8e | |||
| 6a4a592714 | |||
| 806cfb01bd | |||
| c32e38ca10 | |||
| c2cadd1542 | |||
| 4cb2831e9a | |||
| ee31a9f8e2 | |||
| beec20cdda | |||
| 7ef957c2d7 | |||
| d7c8c7a43a | |||
| d56cdc78eb | |||
| 151d89d9f0 | |||
| 8d2944d00c | |||
| 9ddd8198ee | |||
| ff6c7a66d7 | |||
| 129c85929c | |||
| b1a6359473 | |||
| b5d979dbf8 | |||
| 1cd3edc90c | |||
| 77e8edbe16 | |||
| 0e768c9f61 | |||
| 1fcd775cc5 | |||
| 203a1ed147 | |||
| ef1914ee5c | |||
| 084b101e31 | |||
| b2db128aa9 | |||
| 081e880fb6 | 
@@ -4,6 +4,7 @@ tmp_dir = "tmp"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[build]
 | 
					[build]
 | 
				
			||||||
args_bin = [
 | 
					args_bin = [
 | 
				
			||||||
 | 
					    "-articles tmp/articles",
 | 
				
			||||||
    "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
 | 
					    "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
 | 
				
			||||||
    "-domain localhost",
 | 
					    "-domain localhost",
 | 
				
			||||||
    "-key tmp/key.gob",
 | 
					    "-key tmp/key.gob",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,15 +9,20 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Article struct {
 | 
					type Article struct {
 | 
				
			||||||
	Title       string
 | 
						Title         string
 | 
				
			||||||
	Created     time.Time
 | 
						Created       time.Time
 | 
				
			||||||
	Description string
 | 
						Description   string
 | 
				
			||||||
	Content     string
 | 
						Link          string
 | 
				
			||||||
	Published   bool
 | 
						EncURL        string
 | 
				
			||||||
	Rejected    bool
 | 
						EncLength     int
 | 
				
			||||||
	ID          int64
 | 
						EncType       string
 | 
				
			||||||
	AuthorID    int64
 | 
						Published     bool
 | 
				
			||||||
	IssueID     int64
 | 
						Rejected      bool
 | 
				
			||||||
 | 
						ID            int64
 | 
				
			||||||
 | 
						AuthorID      int64
 | 
				
			||||||
 | 
						IssueID       int64
 | 
				
			||||||
 | 
						IsInIssue     bool
 | 
				
			||||||
 | 
						AutoGenerated bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
					func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
				
			||||||
@@ -26,8 +31,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
				
			|||||||
	selectQuery := "SELECT id FROM issues WHERE published = false"
 | 
						selectQuery := "SELECT id FROM issues WHERE published = false"
 | 
				
			||||||
	insertQuery := `
 | 
						insertQuery := `
 | 
				
			||||||
    INSERT INTO articles
 | 
					    INSERT INTO articles
 | 
				
			||||||
        (title, description, content, published, rejected, author_id, issue_id)
 | 
					        (title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id, is_in_issue, auto_generated)
 | 
				
			||||||
    VALUES (?, ?, ?, ?, ?, ?, ?)
 | 
					    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
				
			||||||
    `
 | 
					    `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for i := 0; i < TxMaxRetries; i++ {
 | 
						for i := 0; i < TxMaxRetries; i++ {
 | 
				
			||||||
@@ -44,8 +49,10 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
				
			|||||||
				return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
 | 
									return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			result, err := tx.Exec(insertQuery, a.Title, a.Description,
 | 
								fmt.Println(a)
 | 
				
			||||||
				a.Content, a.Published, a.Rejected, a.AuthorID, id)
 | 
								result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link,
 | 
				
			||||||
 | 
									a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id,
 | 
				
			||||||
 | 
									a.IsInIssue, a.AutoGenerated)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
									if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
					log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
 | 
										log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
 | 
				
			||||||
@@ -79,7 +86,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
					func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
				
			||||||
	query := `
 | 
						query := `
 | 
				
			||||||
    SELECT title, created, description, content, published, author_id
 | 
					    SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id, issue_id, is_in_issue, auto_generated
 | 
				
			||||||
    FROM articles
 | 
					    FROM articles
 | 
				
			||||||
    WHERE id = ?
 | 
					    WHERE id = ?
 | 
				
			||||||
    `
 | 
					    `
 | 
				
			||||||
@@ -90,7 +97,9 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
				
			|||||||
	var err error
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := row.Scan(&article.Title, &created, &article.Description,
 | 
						if err := row.Scan(&article.Title, &created, &article.Description,
 | 
				
			||||||
		&article.Content, &article.Published, &article.AuthorID); err != nil {
 | 
							&article.Link, &article.EncURL, &article.EncLength, &article.EncType,
 | 
				
			||||||
 | 
							&article.Published, &article.AuthorID, &article.IssueID,
 | 
				
			||||||
 | 
							&article.IsInIssue, &article.AutoGenerated); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
							return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -105,7 +114,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
 | 
					func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
 | 
				
			||||||
	query := `
 | 
						query := `
 | 
				
			||||||
    SELECT id, title, created, description, content, author_id, issue_id
 | 
					    SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id, is_in_issue, auto_generated
 | 
				
			||||||
    FROM articles
 | 
					    FROM articles
 | 
				
			||||||
    WHERE published = ?
 | 
					    WHERE published = ?
 | 
				
			||||||
    AND rejected = ?
 | 
					    AND rejected = ?
 | 
				
			||||||
@@ -121,8 +130,9 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
 | 
				
			|||||||
		var created []byte
 | 
							var created []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err = rows.Scan(&article.ID, &article.Title, &created,
 | 
							if err = rows.Scan(&article.ID, &article.Title, &created,
 | 
				
			||||||
			&article.Description, &article.Content, &article.AuthorID,
 | 
								&article.Description, &article.Link, &article.EncURL, &article.EncLength,
 | 
				
			||||||
			&article.IssueID); err != nil {
 | 
								&article.EncType, &article.AuthorID, &article.IssueID,
 | 
				
			||||||
 | 
								&article.IsInIssue, &article.AutoGenerated); err != nil {
 | 
				
			||||||
			return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
								return nil, fmt.Errorf("error scanning article row: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -143,9 +153,9 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
 | 
				
			|||||||
	txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
 | 
						txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
 | 
				
			||||||
	issueQuery := "SELECT id FROM issues WHERE published = false"
 | 
						issueQuery := "SELECT id FROM issues WHERE published = false"
 | 
				
			||||||
	articlesQuery := `
 | 
						articlesQuery := `
 | 
				
			||||||
    SELECT id, title, created, description, content, author_id
 | 
					    SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, auto_generated
 | 
				
			||||||
    FROM articles
 | 
					    FROM articles
 | 
				
			||||||
    WHERE issue_id = ? AND published = true
 | 
					    WHERE issue_id = ? AND published = true AND is_in_issue = true
 | 
				
			||||||
    `
 | 
					    `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for i := 0; i < TxMaxRetries; i++ {
 | 
						for i := 0; i < TxMaxRetries; i++ {
 | 
				
			||||||
@@ -177,7 +187,8 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
 | 
				
			|||||||
				var created []byte
 | 
									var created []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if err = rows.Scan(&article.ID, &article.Title, &created,
 | 
									if err = rows.Scan(&article.ID, &article.Title, &created,
 | 
				
			||||||
					&article.Description, &article.Content, &article.AuthorID); err != nil {
 | 
										&article.Description, &article.Link, &article.EncURL, &article.EncLength,
 | 
				
			||||||
 | 
										&article.EncType, &article.AuthorID, &article.AutoGenerated); err != nil {
 | 
				
			||||||
					if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
										if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
 | 
											log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
@@ -256,3 +267,21 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
 | 
						return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (db *DB) DeleteArticle(id int64) error {
 | 
				
			||||||
 | 
						articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err := db.Exec(articlesTagsQuery, id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error deleting article %v from DB: %v", id, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						articlesQuery := "DELETE FROM articles WHERE id = ?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = db.Exec(articlesQuery, id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error deleting article %v from DB: %v", id, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,9 +11,11 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
 | 
						ArticleDir  string
 | 
				
			||||||
	DBName      string
 | 
						DBName      string
 | 
				
			||||||
	Description string
 | 
						Description string
 | 
				
			||||||
	Domain      string
 | 
						Domain      string
 | 
				
			||||||
 | 
						FirebaseKey string
 | 
				
			||||||
	KeyFile     string
 | 
						KeyFile     string
 | 
				
			||||||
	Link        string
 | 
						Link        string
 | 
				
			||||||
	LogFile     string
 | 
						LogFile     string
 | 
				
			||||||
@@ -27,13 +29,15 @@ type Config struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func newConfig() *Config {
 | 
					func newConfig() *Config {
 | 
				
			||||||
	return &Config{
 | 
						return &Config{
 | 
				
			||||||
		DBName:  "cpolis",
 | 
							ArticleDir:  "/var/www/cpolis/articles",
 | 
				
			||||||
		KeyFile: "/var/www/cpolis/cpolis.key",
 | 
							DBName:      "cpolis",
 | 
				
			||||||
		LogFile: "/var/log/cpolis.log",
 | 
							FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
 | 
				
			||||||
		PDFDir:  "/var/www/cpolis/pdfs",
 | 
							KeyFile:     "/var/www/cpolis/cpolis.key",
 | 
				
			||||||
		PicsDir: "/var/www/cpolis/pics",
 | 
							LogFile:     "/var/log/cpolis.log",
 | 
				
			||||||
		RSSFile: "/var/www/cpolis/cpolis.rss",
 | 
							PDFDir:      "/var/www/cpolis/pdfs",
 | 
				
			||||||
		WebDir:  "/var/www/cpolis/web",
 | 
							PicsDir:     "/var/www/cpolis/pics",
 | 
				
			||||||
 | 
							RSSFile:     "/var/www/cpolis/cpolis.rss",
 | 
				
			||||||
 | 
							WebDir:      "/var/www/cpolis/web",
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,20 +79,35 @@ func (c *Config) handleCliArgs() error {
 | 
				
			|||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	port := 8080
 | 
						port := 8080
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory")
 | 
				
			||||||
	flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
 | 
						flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
 | 
				
			||||||
	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.KeyFile, "key", c.KeyFile, "key file")
 | 
						flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file")
 | 
				
			||||||
	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.PicsDir, "pics", c.PicsDir, "pictures directory")
 | 
				
			||||||
	flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file")
 | 
						flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file")
 | 
				
			||||||
	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(&port, "port", port, "port")
 | 
						flag.IntVar(&port, "port", port, "port")
 | 
				
			||||||
	flag.Parse()
 | 
						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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.KeyFile, err = filepath.Abs(c.KeyFile)
 | 
						c.KeyFile, err = filepath.Abs(c.KeyFile)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("error finding absolute path for key file: %v", err)
 | 
							return fmt.Errorf("error finding absolute path for key file: %v", err)
 | 
				
			||||||
@@ -101,12 +120,18 @@ func (c *Config) handleCliArgs() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	c.PDFDir, err = filepath.Abs(c.PDFDir)
 | 
						c.PDFDir, err = filepath.Abs(c.PDFDir)
 | 
				
			||||||
	if err != nil {
 | 
						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)
 | 
						c.PicsDir, err = filepath.Abs(c.PicsDir)
 | 
				
			||||||
	if err != nil {
 | 
						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)
 | 
						c.Port = fmt.Sprint(":", port)
 | 
				
			||||||
@@ -118,7 +143,10 @@ func (c *Config) handleCliArgs() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	c.WebDir, err = filepath.Abs(c.WebDir)
 | 
						c.WebDir, err = filepath.Abs(c.WebDir)
 | 
				
			||||||
	if err != nil {
 | 
						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
 | 
						return nil
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,12 +12,12 @@ type Client struct {
 | 
				
			|||||||
	*auth.Client
 | 
						*auth.Client
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewClient() (*Client, error) {
 | 
					func NewClient(c *Config) (*Client, error) {
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	client := new(Client)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client := new(Client)
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
	opt := option.WithCredentialsFile("path/to/serviceAccountKey.json")
 | 
						opt := option.WithCredentialsFile(c.FirebaseKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	app, err := firebase.NewApp(ctx, nil, opt)
 | 
						app, err := firebase.NewApp(ctx, nil, opt)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,52 +9,11 @@ import (
 | 
				
			|||||||
	"git.streifling.com/jason/rss"
 | 
						"git.streifling.com/jason/rss"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
 | 
					func GenerateRSS(c *Config, db *DB) (*string, error) {
 | 
				
			||||||
	channel := &rss.Channel{
 | 
						channel := &rss.Channel{
 | 
				
			||||||
		Title:       title,
 | 
							Title:       c.Title,
 | 
				
			||||||
		Link:        link,
 | 
							Link:        c.Link,
 | 
				
			||||||
		Description: description,
 | 
							Description: c.Description,
 | 
				
			||||||
		Items:       make([]*rss.Item, 0),
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	articles, err := db.GetCertainArticles(true, false)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("error fetching published articles: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, article := range articles {
 | 
					 | 
				
			||||||
		tags, err := db.GetArticleTags(article.ID)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("error fetching tags for article %v: %v", article.Title, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		tagNames := make([]string, 0)
 | 
					 | 
				
			||||||
		for _, tag := range tags {
 | 
					 | 
				
			||||||
			tagNames = append(tagNames, tag.Name)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		user, err := db.GetUser(article.AuthorID)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("error finding user %v: %v", article.AuthorID, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		channel.Items = append(channel.Items, &rss.Item{
 | 
					 | 
				
			||||||
			Title:       article.Title,
 | 
					 | 
				
			||||||
			Author:      user.FirstName + user.LastName,
 | 
					 | 
				
			||||||
			PubDate:     article.Created.Format(time.RFC1123Z),
 | 
					 | 
				
			||||||
			Description: article.Description,
 | 
					 | 
				
			||||||
			Content:     &rss.Content{Value: article.Content},
 | 
					 | 
				
			||||||
			Categories:  tagNames,
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return channel, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func GenerateRSS(db *DB, title, link, desc string) (*string, error) {
 | 
					 | 
				
			||||||
	channel := &rss.Channel{
 | 
					 | 
				
			||||||
		Title:       title,
 | 
					 | 
				
			||||||
		Link:        link,
 | 
					 | 
				
			||||||
		Description: desc,
 | 
					 | 
				
			||||||
		Items:       make([]*rss.Item, 0),
 | 
							Items:       make([]*rss.Item, 0),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -72,7 +31,13 @@ func GenerateRSS(db *DB, title, link, desc string) (*string, error) {
 | 
				
			|||||||
		for _, tag := range tags {
 | 
							for _, tag := range tags {
 | 
				
			||||||
			tagNames = append(tagNames, tag.Name)
 | 
								tagNames = append(tagNames, tag.Name)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
 | 
					
 | 
				
			||||||
 | 
							if article.IsInIssue || article.AutoGenerated {
 | 
				
			||||||
 | 
								tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if article.AutoGenerated {
 | 
				
			||||||
 | 
								tagNames = append(tagNames, "autogenerated")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		user, err := db.GetUser(article.AuthorID)
 | 
							user, err := db.GetUser(article.AuthorID)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@@ -89,19 +54,25 @@ func GenerateRSS(db *DB, title, link, desc string) (*string, error) {
 | 
				
			|||||||
			return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
 | 
								return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		articleContent, err := ConvertToHTML(article.Content)
 | 
							item := &rss.Item{
 | 
				
			||||||
		if err != nil {
 | 
								Author:      fmt.Sprint(user.FirstName, " ", user.LastName),
 | 
				
			||||||
			return nil, fmt.Errorf("error converting content to HTML for RSS feed: %v", err)
 | 
								Categories:  tagNames,
 | 
				
			||||||
 | 
								Description: articleDescription,
 | 
				
			||||||
 | 
								Guid:        string(article.ID),
 | 
				
			||||||
 | 
								Link:        article.Link,
 | 
				
			||||||
 | 
								PubDate:     article.Created.Format(time.RFC1123Z),
 | 
				
			||||||
 | 
								Title:       articleTitle,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		channel.Items = append(channel.Items, &rss.Item{
 | 
							if article.AutoGenerated {
 | 
				
			||||||
			Title:       articleTitle,
 | 
								item.Enclosure = &rss.Enclosure{
 | 
				
			||||||
			Author:      user.FirstName + " " + user.LastName,
 | 
									Url:    article.EncURL,
 | 
				
			||||||
			PubDate:     article.Created.Format(time.RFC1123Z),
 | 
									Lenght: article.EncLength,
 | 
				
			||||||
			Description: articleDescription,
 | 
									Type:   article.EncType,
 | 
				
			||||||
			Content:     &rss.Content{Value: articleContent},
 | 
								}
 | 
				
			||||||
			Categories:  tagNames,
 | 
							}
 | 
				
			||||||
		})
 | 
					
 | 
				
			||||||
 | 
							channel.Items = append(channel.Items, item)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	feed := rss.NewFeed()
 | 
						feed := rss.NewFeed()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										55
									
								
								cmd/calls/articles.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								cmd/calls/articles.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					package calls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b "streifling.com/jason/cpolis/cmd/backend"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !article.Published {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
 | 
				
			||||||
 | 
							contentBytes, err := os.ReadFile(articleAbsName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							content, err := b.ConvertToHTML(string(contentBytes))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fmt.Fprint(w, content)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package frontend
 | 
					package calls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
@@ -10,7 +10,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
					func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		if _, err := getSession(w, r, c, s); err != nil {
 | 
							if !tokenIsVerified(w, r, c) {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,33 +12,37 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func ServePDFList(c *b.Config) http.HandlerFunc {
 | 
					func ServePDFList(c *b.Config) http.HandlerFunc {
 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		if tokenIsVerified(w, r) {
 | 
							if !tokenIsVerified(w, r, c) {
 | 
				
			||||||
			files, err := os.ReadDir(c.PDFDir)
 | 
								return
 | 
				
			||||||
			if err != nil {
 | 
							}
 | 
				
			||||||
				log.Println(err)
 | 
					 | 
				
			||||||
				http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			fileNames := make([]string, 0)
 | 
							files, err := os.ReadDir(c.PDFDir)
 | 
				
			||||||
			for _, file := range files {
 | 
							if err != nil {
 | 
				
			||||||
				fileNames = append(fileNames, file.Name())
 | 
								log.Println(err)
 | 
				
			||||||
			}
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			w.Header().Set("Content-Type", "application/json")
 | 
							fileNames := make([]string, 0)
 | 
				
			||||||
			if err = json.NewEncoder(w).Encode(fileNames); err != nil {
 | 
							for _, file := range files {
 | 
				
			||||||
				log.Println(err)
 | 
								fileNames = append(fileNames, file.Name())
 | 
				
			||||||
				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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ServePDF(c *b.Config) http.HandlerFunc {
 | 
					func ServePDF(c *b.Config) http.HandlerFunc {
 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		if tokenIsVerified(w, r) {
 | 
							if !tokenIsVerified(w, r, c) {
 | 
				
			||||||
			http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,8 +8,10 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func ServeRSS(c *b.Config) http.HandlerFunc {
 | 
					func ServeRSS(c *b.Config) http.HandlerFunc {
 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		if tokenIsVerified(w, r) {
 | 
							if !tokenIsVerified(w, r, c) {
 | 
				
			||||||
			http.ServeFile(w, r, c.RSSFile)
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							http.ServeFile(w, r, c.RSSFile)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// tokenIsVerified verifies that a request is authorized. It returns a bool.
 | 
					// tokenIsVerified verifies that a request is authorized. It returns a bool.
 | 
				
			||||||
func tokenIsVerified(w http.ResponseWriter, r *http.Request) bool {
 | 
					func tokenIsVerified(w http.ResponseWriter, r *http.Request, c *b.Config) bool {
 | 
				
			||||||
	idToken := r.Header.Get("Authorization")
 | 
						idToken := r.Header.Get("Authorization")
 | 
				
			||||||
	if idToken == "" {
 | 
						if idToken == "" {
 | 
				
			||||||
		log.Println("Authorization header missing")
 | 
							log.Println("Authorization header missing")
 | 
				
			||||||
@@ -16,7 +16,7 @@ func tokenIsVerified(w http.ResponseWriter, r *http.Request) bool {
 | 
				
			|||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	client, err := b.NewClient()
 | 
						client, err := b.NewClient(c)
 | 
				
			||||||
	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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,6 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/fs"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
@@ -23,25 +22,6 @@ const (
 | 
				
			|||||||
	PreviewMode
 | 
						PreviewMode
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
					 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
		session, err := getSession(w, r, c, s)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		session.Values["article"] = 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")
 | 
					 | 
				
			||||||
		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
					func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		session, err := getSession(w, r, c, s)
 | 
							session, err := getSession(w, r, c, s)
 | 
				
			||||||
@@ -93,12 +73,13 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		article := &b.Article{
 | 
							article := &b.Article{
 | 
				
			||||||
			Title:       r.PostFormValue("article-title"),
 | 
								Title:         r.PostFormValue("article-title"),
 | 
				
			||||||
			Description: r.PostFormValue("article-description"),
 | 
								Description:   r.PostFormValue("article-description"),
 | 
				
			||||||
			Content:     r.PostFormValue("article-content"),
 | 
								Published:     false,
 | 
				
			||||||
			Published:   false,
 | 
								Rejected:      false,
 | 
				
			||||||
			Rejected:    false,
 | 
								AuthorID:      session.Values["id"].(int64),
 | 
				
			||||||
			AuthorID:    session.Values["id"].(int64),
 | 
								IsInIssue:     r.PostFormValue("issue") == "on",
 | 
				
			||||||
 | 
								AutoGenerated: false,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		article.ID, err = db.AddArticle(article)
 | 
							article.ID, err = db.AddArticle(article)
 | 
				
			||||||
@@ -108,6 +89,20 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
 | 
				
			||||||
 | 
							if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
 | 
				
			||||||
 | 
							if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		r.ParseForm()
 | 
							r.ParseForm()
 | 
				
			||||||
		tags := make([]int64, 0)
 | 
							tags := make([]int64, 0)
 | 
				
			||||||
		for _, tag := range r.Form["tags"] {
 | 
							for _, tag := range r.Form["tags"] {
 | 
				
			||||||
@@ -149,11 +144,18 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
		description := r.PostFormValue("article-description")
 | 
							description := r.PostFormValue("article-description")
 | 
				
			||||||
		content := r.PostFormValue("article-content")
 | 
							content := r.PostFormValue("article-content")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							link := fmt.Sprint(c.ArticleDir, "/", id, ".md")
 | 
				
			||||||
 | 
							if err = os.WriteFile(link, []byte(content), 0644); err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err = db.UpdateAttributes(
 | 
							if err = db.UpdateAttributes(
 | 
				
			||||||
			&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
 | 
								&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
 | 
				
			||||||
			&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
 | 
								&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
 | 
				
			||||||
			&b.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
 | 
					 | 
				
			||||||
			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
 | 
								&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
 | 
				
			||||||
 | 
								&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"},
 | 
				
			||||||
		); err != nil {
 | 
							); err != nil {
 | 
				
			||||||
			log.Println(err)
 | 
								log.Println(err)
 | 
				
			||||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
@@ -241,46 +243,35 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		type htmlData struct {
 | 
							data := new(struct {
 | 
				
			||||||
			Title       string
 | 
								Article *b.Article
 | 
				
			||||||
			Description string
 | 
								Content template.HTML
 | 
				
			||||||
			Content     template.HTML
 | 
								Tags    []*b.Tag
 | 
				
			||||||
			Tags        []*b.Tag
 | 
							})
 | 
				
			||||||
			ID          int64
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		var err error
 | 
							id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
				
			||||||
		data := new(htmlData)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		data.ID, err = strconv.ParseInt(r.PathValue("id"), 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(data.ID)
 | 
							data.Article, err = db.GetArticle(id)
 | 
				
			||||||
		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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		data.Title, err = b.ConvertToPlain(article.Title)
 | 
							articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
 | 
				
			||||||
 | 
							contentBytes, err := os.ReadFile(articleAbsName)
 | 
				
			||||||
		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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		data.Description, err = b.ConvertToPlain(article.Description)
 | 
							content, err := b.ConvertToHTML(string(contentBytes))
 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Println(err)
 | 
					 | 
				
			||||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		content, err := b.ConvertToHTML(article.Content)
 | 
					 | 
				
			||||||
		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)
 | 
				
			||||||
@@ -288,7 +279,7 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		data.Content = template.HTML(content)
 | 
							data.Content = template.HTML(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		data.Tags, err = db.GetArticleTags(data.ID)
 | 
							data.Tags, err = db.GetArticleTags(data.Article.ID)
 | 
				
			||||||
		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)
 | 
				
			||||||
@@ -307,12 +298,12 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		type htmlData struct {
 | 
							data := new(struct {
 | 
				
			||||||
			Selected map[int64]bool
 | 
								Selected map[int64]bool
 | 
				
			||||||
			Article  *b.Article
 | 
								Article  *b.Article
 | 
				
			||||||
 | 
								Content  string
 | 
				
			||||||
			Tags     []*b.Tag
 | 
								Tags     []*b.Tag
 | 
				
			||||||
		}
 | 
							})
 | 
				
			||||||
		data := new(htmlData)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
							id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@@ -328,6 +319,16 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
 | 
				
			||||||
 | 
							contentBytes, err := os.ReadFile(articleAbsName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							data.Content = string(contentBytes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		data.Tags, err = db.GetTagList()
 | 
							data.Tags, err = db.GetTagList()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Println(err)
 | 
								log.Println(err)
 | 
				
			||||||
@@ -360,13 +361,20 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
							id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusBadRequest)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							article, err := db.GetArticle(id)
 | 
				
			||||||
		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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err = db.AddArticleToCurrentIssue(id); err != nil {
 | 
							if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
 | 
				
			||||||
			log.Println(err)
 | 
								log.Println(err)
 | 
				
			||||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -382,7 +390,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		feed, err := b.GenerateRSS(db, c.Title, c.Link, c.Description)
 | 
							feed, err := b.GenerateRSS(c, db)
 | 
				
			||||||
		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)
 | 
				
			||||||
@@ -446,7 +454,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) {
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		if _, err := getSession(w, r, c, s); err != nil {
 | 
							if _, err := getSession(w, r, c, s); err != nil {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -455,7 +463,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
		file, header, err := r.FormFile("article-image")
 | 
							file, header, err := r.FormFile("article-image")
 | 
				
			||||||
		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.StatusBadRequest)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		defer file.Close()
 | 
							defer file.Close()
 | 
				
			||||||
@@ -470,12 +478,6 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
			return
 | 
								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)
 | 
							img, err := os.Create(absFilepath)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Println(err)
 | 
								log.Println(err)
 | 
				
			||||||
@@ -490,8 +492,152 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		url := fmt.Sprint(c.Domain, "/pics/", filename)
 | 
							url := fmt.Sprint(c.Domain, "/image/serve/", filename)
 | 
				
			||||||
		w.Header().Set("Content-Type", "application/json")
 | 
							w.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
		json.NewEncoder(w).Encode(url)
 | 
							json.NewEncoder(w).Encode(url)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ShowPublishedArticles(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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							publishedArticles, err := db.GetCertainArticles(true, false)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							filteredArticles := make([]*b.Article, 0)
 | 
				
			||||||
 | 
							for _, article := range publishedArticles {
 | 
				
			||||||
 | 
								if !article.AutoGenerated {
 | 
				
			||||||
 | 
									filteredArticles = append(filteredArticles, article)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
 | 
				
			||||||
 | 
							tmpl = template.Must(tmpl, err)
 | 
				
			||||||
 | 
							tmpl.ExecuteTemplate(w, "page-content", filteredArticles)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ReviewArticleForDeletion(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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							type htmlData struct {
 | 
				
			||||||
 | 
								Title       string
 | 
				
			||||||
 | 
								Description string
 | 
				
			||||||
 | 
								Content     template.HTML
 | 
				
			||||||
 | 
								Tags        []*b.Tag
 | 
				
			||||||
 | 
								ID          int64
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
							data := new(htmlData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							article, err := db.GetArticle(data.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							data.Title, err = b.ConvertToPlain(article.Title)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							data.Description, err = b.ConvertToPlain(article.Description)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
 | 
				
			||||||
 | 
							contentBytes, err := os.ReadFile(articleAbsName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							content, err := b.ConvertToHTML(string(contentBytes))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							data.Content = template.HTML(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							data.Tags, err = db.GetArticleTags(data.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-deleted.html")
 | 
				
			||||||
 | 
							tmpl = template.Must(tmpl, err)
 | 
				
			||||||
 | 
							tmpl.ExecuteTemplate(w, "page-content", data)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							session, err := getSession(w, r, c, s)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = db.DeleteArticle(id); err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", id, ".md")); err != nil {
 | 
				
			||||||
 | 
								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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
 | 
				
			||||||
 | 
							tmpl = template.Must(tmpl, err)
 | 
				
			||||||
 | 
							tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,18 @@
 | 
				
			|||||||
package frontend
 | 
					package frontend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
 | 
						"mime"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
	b "streifling.com/jason/cpolis/cmd/backend"
 | 
						b "streifling.com/jason/cpolis/cmd/backend"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -15,14 +23,169 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
 | 
				
			|||||||
			return
 | 
								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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							imgFileName := session.Values["issue-image"].(string)
 | 
				
			||||||
 | 
							imgAbsName := fmt.Sprint(c.PicsDir, "/", imgFileName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							imgFile, err := os.Open(imgAbsName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer imgFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							imgInfo, err := imgFile.Stat()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							imgSize := imgInfo.Size()
 | 
				
			||||||
 | 
							mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							article := &b.Article{
 | 
				
			||||||
 | 
								Title:         r.PostFormValue("issue-title"),
 | 
				
			||||||
 | 
								EncURL:        fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
 | 
				
			||||||
 | 
								EncLength:     int(imgSize),
 | 
				
			||||||
 | 
								EncType:       mimeType,
 | 
				
			||||||
 | 
								Published:     true,
 | 
				
			||||||
 | 
								Rejected:      false,
 | 
				
			||||||
 | 
								Created:       time.Now(),
 | 
				
			||||||
 | 
								AuthorID:      session.Values["id"].(int64),
 | 
				
			||||||
 | 
								AutoGenerated: true,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							article.ID, err = db.AddArticle(article)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
 | 
				
			||||||
 | 
							if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
 | 
				
			||||||
 | 
							if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); 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 {
 | 
							if err := db.PublishLatestIssue(); err != nil {
 | 
				
			||||||
			log.Println(err)
 | 
								log.Println(err)
 | 
				
			||||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
			return
 | 
								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, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
 | 
				
			||||||
		tmpl = template.Must(tmpl, err)
 | 
							tmpl = template.Must(tmpl, err)
 | 
				
			||||||
		tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
 | 
							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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										59
									
								
								cmd/frontend/pdf.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								cmd/frontend/pdf.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					package frontend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						b "streifling.com/jason/cpolis/cmd/backend"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UploadPDF(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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := r.ParseMultipartForm(10 << 20); err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusBadRequest)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							file, _, err := r.FormFile("pdf-upload")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer file.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							filename := fmt.Sprint(uuid.New(), ".pdf")
 | 
				
			||||||
 | 
							absFilepath, err := filepath.Abs(fmt.Sprint(c.PDFDir, "/", filename))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							pdf, err := os.Create(absFilepath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println(err)
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer pdf.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if _, err = io.Copy(pdf, file); 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)
 | 
							id, ok := db.GetID(userName)
 | 
				
			||||||
		if !ok {
 | 
							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
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -96,7 +96,6 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		session.Options.MaxAge = -1
 | 
							session.Options.MaxAge = -1
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err = session.Save(r, w); err != nil {
 | 
							if err = session.Save(r, w); err != nil {
 | 
				
			||||||
			log.Println(err)
 | 
								log.Println(err)
 | 
				
			||||||
			http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
@@ -107,3 +106,22 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
 | 
				
			|||||||
		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
 | 
							template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							session, err := getSession(w, r, c, s)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							session.Values["article"] = 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")
 | 
				
			||||||
 | 
							template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
					func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		if _, err := getSession(w, r, c, s); err != nil {
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var err error
 | 
							var err error
 | 
				
			||||||
		htmlData := UserData{
 | 
							htmlData := UserData{
 | 
				
			||||||
			User: &b.User{
 | 
								User: &b.User{
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										60
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								cmd/main.go
									
									
									
									
									
								
							@@ -49,38 +49,44 @@ func main() {
 | 
				
			|||||||
		http.FileServer(http.Dir(config.WebDir+"/static/"))))
 | 
							http.FileServer(http.Dir(config.WebDir+"/static/"))))
 | 
				
			||||||
	mux.HandleFunc("/", f.HomePage(config, db, store))
 | 
						mux.HandleFunc("/", f.HomePage(config, db, store))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.HandleFunc("GET /create-tag", f.CreateTag(config, store))
 | 
						mux.HandleFunc("GET /article/all-published", f.ShowPublishedArticles(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /create-user", f.CreateUser(config, store))
 | 
						mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /edit-self", f.EditSelf(config, db, store))
 | 
						mux.HandleFunc("GET /article/all-unpublished", f.ShowUnpublishedArticles(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /edit-user/{id}", f.EditUser(config, db, store))
 | 
						mux.HandleFunc("GET /article/delete/{id}", f.DeleteArticle(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /delete-user/{id}", f.DeleteUser(config, db, store))
 | 
						mux.HandleFunc("GET /article/publish/{id}", f.PublishArticle(config, db, store))
 | 
				
			||||||
 | 
						mux.HandleFunc("GET /article/reject/{id}", f.RejectArticle(config, db, store))
 | 
				
			||||||
 | 
						mux.HandleFunc("GET /article/review-deletion/{id}", f.ReviewArticleForDeletion(config, db, store))
 | 
				
			||||||
 | 
						mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store))
 | 
				
			||||||
 | 
						mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewUnpublishedArticle(config, db, store))
 | 
				
			||||||
 | 
						mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db))
 | 
				
			||||||
 | 
						mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /hub", f.ShowHub(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/this", f.ShowCurrentArticles(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /logout", f.Logout(config, store))
 | 
						mux.HandleFunc("GET /logout", f.Logout(config, store))
 | 
				
			||||||
	mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
 | 
						mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
 | 
				
			||||||
	mux.HandleFunc("GET /pdf/{id}", c.ServePDF(config))
 | 
						mux.HandleFunc("GET /pdf/serve/{id}", c.ServePDF(config))
 | 
				
			||||||
	mux.HandleFunc("GET /pics/{pic}", f.ServeImage(config, store))
 | 
						mux.HandleFunc("GET /rss/serve", c.ServeRSS(config))
 | 
				
			||||||
	mux.HandleFunc("GET /publish-article/{id}", f.PublishArticle(config, db, store))
 | 
						mux.HandleFunc("GET /tag/create", f.CreateTag(config, store))
 | 
				
			||||||
	mux.HandleFunc("GET /publish-issue", f.PublishLatestIssue(config, db, store))
 | 
						mux.HandleFunc("GET /user/create", f.CreateUser(config, store))
 | 
				
			||||||
	mux.HandleFunc("GET /reject-article/{id}", f.RejectArticle(config, db, store))
 | 
						mux.HandleFunc("GET /user/delete/{id}", f.DeleteUser(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /rejected-articles", f.ShowRejectedArticles(config, db, store))
 | 
						mux.HandleFunc("GET /user/edit/{id}", f.EditUser(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /review-rejected-article/{id}", f.ReviewRejectedArticle(config, db, store))
 | 
						mux.HandleFunc("GET /user/edit/self", f.EditSelf(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("GET /review-unpublished-article/{id}", f.ReviewUnpublishedArticle(config, db, store))
 | 
						mux.HandleFunc("GET /user/show-all/delete", f.ShowAllUsers(config, db, store, "delete"))
 | 
				
			||||||
	mux.HandleFunc("GET /rss", c.ServeRSS(config))
 | 
						mux.HandleFunc("GET /user/show-all/edit", f.ShowAllUsers(config, db, store, "edit"))
 | 
				
			||||||
	mux.HandleFunc("GET /show-all-users-edit", f.ShowAllUsers(config, db, store, "edit-user"))
 | 
					 | 
				
			||||||
	mux.HandleFunc("GET /show-all-users-delete", f.ShowAllUsers(config, db, store, "delete-user"))
 | 
					 | 
				
			||||||
	mux.HandleFunc("GET /this-issue", f.ShowCurrentArticles(config, db, store))
 | 
					 | 
				
			||||||
	mux.HandleFunc("GET /unpublished-articles", f.ShowUnpublishedArticles(config, db, store))
 | 
					 | 
				
			||||||
	mux.HandleFunc("GET /write-article", f.WriteArticle(config, db, store))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.HandleFunc("POST /add-first-user", f.AddFirstUser(config, db, store))
 | 
						mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("POST /add-tag", f.AddTag(config, db, store))
 | 
						mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("POST /add-user", f.AddUser(config, db, 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 /login", f.Login(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("POST /resubmit-article/{id}", f.ResubmitArticle(config, db, store))
 | 
						mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store))
 | 
				
			||||||
	mux.HandleFunc("POST /submit-article", f.SubmitArticle(config, db, store))
 | 
						mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("POST /update-self", f.UpdateSelf(config, db, store))
 | 
						mux.HandleFunc("POST /user/add", f.AddUser(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("POST /update-user/{id}", f.UpdateUser(config, db, store))
 | 
						mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store))
 | 
				
			||||||
	mux.HandleFunc("POST /upload-image", f.UploadImage(config, store))
 | 
						mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, store))
 | 
				
			||||||
 | 
						mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, store))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Fatalln(http.ListenAndServe(config.Port, mux))
 | 
						log.Fatalln(http.ListenAndServe(config.Port, mux))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								cpolis.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								cpolis.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					[Unit]
 | 
				
			||||||
 | 
					Description=cpolis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Service]
 | 
				
			||||||
 | 
					ExecStart=/usr/local/bin/cpolis
 | 
				
			||||||
 | 
					Restart=on-failure
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Install]
 | 
				
			||||||
 | 
					WantedBy=default.target
 | 
				
			||||||
@@ -11,40 +11,45 @@ CREATE TABLE users (
 | 
				
			|||||||
    first_name  VARCHAR(50) NOT NULL,
 | 
					    first_name  VARCHAR(50) NOT NULL,
 | 
				
			||||||
    last_name   VARCHAR(50) NOT NULL,
 | 
					    last_name   VARCHAR(50) NOT NULL,
 | 
				
			||||||
    role        INT         NOT NULL,
 | 
					    role        INT         NOT NULL,
 | 
				
			||||||
    PRIMARY KEY(id)
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE issues (
 | 
					CREATE TABLE issues (
 | 
				
			||||||
    id          INT     AUTO_INCREMENT,
 | 
					    id          INT     AUTO_INCREMENT,
 | 
				
			||||||
    published   BOOL    NOT NULL,
 | 
					    published   BOOL    NOT NULL,
 | 
				
			||||||
    PRIMARY KEY(id)
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE articles (
 | 
					CREATE TABLE articles (
 | 
				
			||||||
    id          INT             AUTO_INCREMENT,
 | 
					    id              INT             AUTO_INCREMENT,
 | 
				
			||||||
    title       VARCHAR(255)    NOT NULL,
 | 
					    title           VARCHAR(255)    NOT NULL,
 | 
				
			||||||
    created     TIMESTAMP       DEFAULT CURRENT_TIMESTAMP,
 | 
					    created         TIMESTAMP       DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
    description TEXT            NOT NULL,
 | 
					    description     TEXT            NOT NULL,
 | 
				
			||||||
    content     TEXT            NOT NULL,
 | 
					    link            VARCHAR(255),
 | 
				
			||||||
    published   BOOL            NOT NULL,
 | 
					    enc_url         VARCHAR(255),
 | 
				
			||||||
    rejected    BOOL            NOT NULL,
 | 
					    enc_length      INT,
 | 
				
			||||||
    author_id   INT             NOT NULL,
 | 
					    enc_type        VARCHAR(255),
 | 
				
			||||||
    issue_id    INT             NOT NULL,
 | 
					    published       BOOL            NOT NULL,
 | 
				
			||||||
    PRIMARY KEY(id),
 | 
					    rejected        BOOL            NOT NULL,
 | 
				
			||||||
    FOREIGN KEY(author_id) REFERENCES users(id),
 | 
					    author_id       INT             NOT NULL,
 | 
				
			||||||
    FOREIGN KEY(issue_id) REFERENCES issues(id)
 | 
					    issue_id        INT             NOT NULL,
 | 
				
			||||||
 | 
					    is_in_issue     BOOL            NOT NULL,
 | 
				
			||||||
 | 
					    auto_generated  BOOL            NOT NULL,
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (author_id) REFERENCES users (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (issue_id) REFERENCES issues (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE tags (
 | 
					CREATE TABLE tags (
 | 
				
			||||||
    id      INT         AUTO_INCREMENT,
 | 
					    id      INT         AUTO_INCREMENT,
 | 
				
			||||||
    name    VARCHAR(50) NOT NULL UNIQUE,
 | 
					    name    VARCHAR(50) NOT NULL UNIQUE,
 | 
				
			||||||
    PRIMARY KEY(id)
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE articles_tags (
 | 
					CREATE TABLE articles_tags (
 | 
				
			||||||
    article_id  INT,
 | 
					    article_id  INT,
 | 
				
			||||||
    tag_id      INT,
 | 
					    tag_id      INT,
 | 
				
			||||||
    PRIMARY KEY(article_id, tag_id),
 | 
					    PRIMARY KEY (article_id, tag_id),
 | 
				
			||||||
    FOREIGN KEY(article_id) REFERENCES articles(id),
 | 
					    FOREIGN KEY (article_id) REFERENCES articles (id),
 | 
				
			||||||
    FOREIGN KEY(tag_id) REFERENCES tags(id)
 | 
					    FOREIGN KEY (tag_id) REFERENCES tags (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@@ -4,7 +4,7 @@ go 1.22.0
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	firebase.google.com/go/v4 v4.14.1
 | 
						firebase.google.com/go/v4 v4.14.1
 | 
				
			||||||
	git.streifling.com/jason/rss v0.1.2
 | 
						git.streifling.com/jason/rss v0.1.3
 | 
				
			||||||
	github.com/BurntSushi/toml v1.3.2
 | 
						github.com/BurntSushi/toml v1.3.2
 | 
				
			||||||
	github.com/go-sql-driver/mysql v1.7.1
 | 
						github.com/go-sql-driver/mysql v1.7.1
 | 
				
			||||||
	github.com/google/uuid v1.6.0
 | 
						github.com/google/uuid v1.6.0
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@@ -15,8 +15,8 @@ cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJah
 | 
				
			|||||||
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
 | 
					cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
 | 
				
			||||||
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
 | 
					firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
 | 
				
			||||||
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
 | 
					firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
 | 
				
			||||||
git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4=
 | 
					git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU=
 | 
				
			||||||
git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
 | 
					git.streifling.com/jason/rss v0.1.3/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
 | 
				
			||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
					github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
				
			||||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
 | 
					github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
 | 
				
			||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 | 
					github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										58
									
								
								update.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										58
									
								
								update.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					#! /bin/sh -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CPOLIS_REPO_URL="https://git.streifling.com/api/v1/repos/jason/cpolis/releases"
 | 
				
			||||||
 | 
					EXTRACTION_DIR=$HOME
 | 
				
			||||||
 | 
					CPOLIS_DIR=$EXTRACTION_DIR/cpolis
 | 
				
			||||||
 | 
					TAILWINDCSS_REPO_URL=https://api.github.com/repos/tailwindlabs/tailwindcss/releases/latest
 | 
				
			||||||
 | 
					TMP_DIR=/tmp
 | 
				
			||||||
 | 
					BIN_DIR=/usr/local/bin
 | 
				
			||||||
 | 
					SYSTEMD_DIR=/etc/systemd/system
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					check_dependency() {
 | 
				
			||||||
 | 
					    if ! which $1 >/dev/null 2>&1; then
 | 
				
			||||||
 | 
					        echo "$1 needs to be installed" >&2
 | 
				
			||||||
 | 
					        exit 1
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if ! groups | grep -E 'root|wheel|sudo' >/dev/null; then
 | 
				
			||||||
 | 
					    echo "You need administrative privileges for this script" >&2
 | 
				
			||||||
 | 
					    exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					check_dependency curl
 | 
				
			||||||
 | 
					check_dependency go
 | 
				
			||||||
 | 
					check_dependency jq
 | 
				
			||||||
 | 
					check_dependency tar
 | 
				
			||||||
 | 
					check_dependency xargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo '\nDownloading cpolis...' >&2
 | 
				
			||||||
 | 
					rm -fr $CPOLIS_DIR/*
 | 
				
			||||||
 | 
					latest_release=$(curl -s $CPOLIS_REPO_URL | jq -r '.[0].tag_name')
 | 
				
			||||||
 | 
					curl -Lo $TMP_DIR/cpolis.tar.gz https://git.streifling.com/jason/cpolis/archive/$latest_release.tar.gz
 | 
				
			||||||
 | 
					tar -xzf $TMP_DIR/cpolis.tar.gz -C $EXTRACTION_DIR
 | 
				
			||||||
 | 
					rm $TMP_DIR/cpolis.tar.gz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo '\nDownloading TailwindCSS...' >&2
 | 
				
			||||||
 | 
					curl -s $TAILWINDCSS_REPO_URL |
 | 
				
			||||||
 | 
					    grep -F browser_download_url |
 | 
				
			||||||
 | 
					    grep -F linux-x64 |
 | 
				
			||||||
 | 
					    cut -d'"' -f4 |
 | 
				
			||||||
 | 
					    xargs -r curl -Lo $CPOLIS_DIR/tailwindcss
 | 
				
			||||||
 | 
					chmod +x $CPOLIS_DIR/tailwindcss
 | 
				
			||||||
 | 
					$CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo '\nBuilding cpolis...' >&2
 | 
				
			||||||
 | 
					cd $CPOLIS_DIR
 | 
				
			||||||
 | 
					go build -o $BIN_DIR/cpolis cmd/main.go
 | 
				
			||||||
 | 
					cd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo '\nSetting up system files...' >&2
 | 
				
			||||||
 | 
					sudo chown root:root $BIN_DIR/cpolis
 | 
				
			||||||
 | 
					chmod +x $BIN_DIR/cpolis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo '\nSetting up service...' >&2
 | 
				
			||||||
 | 
					sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR
 | 
				
			||||||
 | 
					sudo chown root:root $SYSTEMD_DIR/cpolis.service
 | 
				
			||||||
 | 
					sudo systemctl daemon-reload
 | 
				
			||||||
 | 
					sudo systemctl is-active --quiet cpolis.service && sudo systemctl restart cpolis.service
 | 
				
			||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
<form>
 | 
					<form>
 | 
				
			||||||
    <input required name="tag" placeholder="Tag eingeben" type="text" />
 | 
					    <input required name="tag" placeholder="Tag eingeben" type="text" />
 | 
				
			||||||
    <div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
        <input class="action-btn" type="submit" value="Anlegen" hx-post="/add-tag" hx-target="#page-content" />
 | 
					        <input class="action-btn" type="submit" value="Anlegen" hx-post="/tag/add" hx-target="#page-content" />
 | 
				
			||||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,7 +45,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
        <input class="action-btn" type="submit" value="Anlegen" hx-post="/add-user" hx-target="#page-content" />
 | 
					        <input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add" hx-target="#page-content" />
 | 
				
			||||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,79 @@
 | 
				
			|||||||
{{define "page-content"}}
 | 
					{{define "page-content"}}
 | 
				
			||||||
<h2>Aktuelle Artikel</h2>
 | 
					<h2>Diese Ausgabe</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="flex flex-col gap-4">
 | 
					<form hx-encoding="multipart/form-data">
 | 
				
			||||||
    {{range .}}
 | 
					    <div class="flex flex-col gap-4">
 | 
				
			||||||
    <div class="border px-2 py-1 rounded-md">
 | 
					        <div>
 | 
				
			||||||
        <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
					            <h3>Aktuelle Artikel</h3>
 | 
				
			||||||
        <p>{{.Description}}</p>
 | 
					            <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>
 | 
				
			||||||
 | 
					            <div class="flex">
 | 
				
			||||||
 | 
					                <label class="btn text-center" for="image-upload">Bild hochladen</label>
 | 
				
			||||||
 | 
					                <input class="hidden" id="image-upload" name="issue-image" type="file" required
 | 
				
			||||||
 | 
					                    hx-post="/issue/upload-image" />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <h3>Titel</h3>
 | 
				
			||||||
 | 
					            <div class="flex flex-col gap-y-1">
 | 
				
			||||||
 | 
					                <input name="issue-title" type="text" />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </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>
 | 
					    </div>
 | 
				
			||||||
    {{end}}
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
    <button class="action-btn" hx-get="/publish-issue" hx-target="#page-content">Ausgabe publizieren</button>
 | 
					        <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>
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
				
			||||||
</div>
 | 
					    </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}}
 | 
					{{end}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,7 +35,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
        <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-self"
 | 
					        <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/self"
 | 
				
			||||||
            hx-target="#page-content" />
 | 
					            hx-target="#page-content" />
 | 
				
			||||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,7 +49,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
        <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-user/{{.ID}}"
 | 
					        <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/{{.ID}}"
 | 
				
			||||||
            hx-target="#page-content" />
 | 
					            hx-target="#page-content" />
 | 
				
			||||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,19 +6,26 @@
 | 
				
			|||||||
        <label for="article-title">Titel</label>
 | 
					        <label for="article-title">Titel</label>
 | 
				
			||||||
        <input name="article-title" type="text" value="{{.Title}}" />
 | 
					        <input name="article-title" type="text" value="{{.Title}}" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="flex flex-col">
 | 
					
 | 
				
			||||||
 | 
					    <div class="flex flex-col gap-y-1">
 | 
				
			||||||
        <label for="article-description">Beschreibung</label>
 | 
					        <label for="article-description">Beschreibung</label>
 | 
				
			||||||
        <textarea name="article-description">{{.Description}}</textarea>
 | 
					        <textarea name="article-description">{{.Description}}</textarea>
 | 
				
			||||||
    </div>
 | 
					    </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>
 | 
					        <textarea id="easyMDE">{{.Content}}</textarea>
 | 
				
			||||||
 | 
					        <input id="article-content" name="article-content" type="hidden" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <input id="article-content" name="article-content" type="hidden" />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <span>Tags</span>
 | 
					        <span>Tags</span>
 | 
				
			||||||
        <div class="flex flex-wrap gap-x-4">
 | 
					        <div class="flex flex-wrap gap-x-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <input id="issue" name="issue" type="checkbox" />
 | 
				
			||||||
 | 
					                <label for="issue">Orient Express</label>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            {{range .Tags}}
 | 
					            {{range .Tags}}
 | 
				
			||||||
            <div>
 | 
					            <div>
 | 
				
			||||||
                <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
 | 
					                <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
 | 
				
			||||||
@@ -29,7 +36,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
        <input class="action-btn" type="submit" value="Senden" hx-post="/submit-article" hx-target="#page-content" />
 | 
					        <input class="action-btn" type="submit" value="Senden" hx-post="/article/submit" hx-target="#page-content" />
 | 
				
			||||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
@@ -46,7 +53,7 @@
 | 
				
			|||||||
            var formData = new FormData();
 | 
					            var formData = new FormData();
 | 
				
			||||||
            formData.append('article-image', file);
 | 
					            formData.append('article-image', file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            fetch('/upload-image', {
 | 
					            fetch('/article/upload-image', {
 | 
				
			||||||
                method: 'POST',
 | 
					                method: 'POST',
 | 
				
			||||||
                body: formData
 | 
					                body: formData
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
        <input class="action-btn" type="submit" value="Anlegen" hx-post="/add-first-user" hx-target="#page-content" />
 | 
					        <input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add-first" hx-target="#page-content" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,10 +6,9 @@
 | 
				
			|||||||
    <div class="mb-3">
 | 
					    <div class="mb-3">
 | 
				
			||||||
        <h2>Autor</h2>
 | 
					        <h2>Autor</h2>
 | 
				
			||||||
        <div class="grid grid-cols-2 gap-x-4 gap-y-2">
 | 
					        <div class="grid grid-cols-2 gap-x-4 gap-y-2">
 | 
				
			||||||
            <button class="btn" hx-get="/write-article" hx-target="#page-content">Artikel schreiben</button>
 | 
					            <button class="btn" hx-get="/article/write" hx-target="#page-content">Artikel schreiben</button>
 | 
				
			||||||
            <button class="btn" hx-get="/rejected-articles" hx-target="#page-content">Abgelehnte Artikel</button>
 | 
					            <button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Abgelehnte Artikel</button>
 | 
				
			||||||
            <a class="btn text-center" href="/rss">RSS Feed</a>
 | 
					            <button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Profil bearbeiten</button>
 | 
				
			||||||
            <button class="btn" hx-get="/edit-self" hx-target="#page-content">Profil bearbeiten</button>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    {{end}}
 | 
					    {{end}}
 | 
				
			||||||
@@ -17,11 +16,11 @@
 | 
				
			|||||||
    {{if lt . 3}}
 | 
					    {{if lt . 3}}
 | 
				
			||||||
    <div class="mb-3">
 | 
					    <div class="mb-3">
 | 
				
			||||||
        <h2>Redakteur</h2>
 | 
					        <h2>Redakteur</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="/unpublished-articles" hx-target="#page-content">
 | 
					            <button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content">
 | 
				
			||||||
                Unveröffentlichte Artikel
 | 
					                Unveröffentlichte Artikel
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
            <button class="btn" hx-get="/create-tag" hx-target="#page-content">Neuer Tag</button>
 | 
					            <button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    {{end}}
 | 
					    {{end}}
 | 
				
			||||||
@@ -29,8 +28,14 @@
 | 
				
			|||||||
    {{if lt . 2}}
 | 
					    {{if lt . 2}}
 | 
				
			||||||
    <div class="mb-3">
 | 
					    <div class="mb-3">
 | 
				
			||||||
        <h2>Herausgeber</h2>
 | 
					        <h2>Herausgeber</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="/this-issue" hx-target="#page-content">Diese Ausgabe</button>
 | 
					            <button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button>
 | 
				
			||||||
 | 
					            <button class="btn" hx-get="/article/all-published" hx-target="#page-content">Artikel löschen</button>
 | 
				
			||||||
 | 
					            <form class="flex" hx-encoding="multipart/form-data">
 | 
				
			||||||
 | 
					                <label class="btn text-center" for="pdf-upload">PDF hochladen</label>
 | 
				
			||||||
 | 
					                <input accept=".pdf" class="hidden" id="pdf-upload" name="pdf-upload" type="file"
 | 
				
			||||||
 | 
					                    hx-post="/pdf/upload" />
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    {{end}}
 | 
					    {{end}}
 | 
				
			||||||
@@ -38,10 +43,10 @@
 | 
				
			|||||||
    {{if eq . 0}}
 | 
					    {{if eq . 0}}
 | 
				
			||||||
    <div class="mb-3">
 | 
					    <div class="mb-3">
 | 
				
			||||||
        <h2>Administrator</h2>
 | 
					        <h2>Administrator</h2>
 | 
				
			||||||
        <div class="grid grid-cols-3 gap-4">
 | 
					        <div class="grid grid-cols-2 gap-x-4 gap-y-2">
 | 
				
			||||||
            <button class="btn" hx-get="/create-user" hx-target="#page-content">Benutzer hinzufügen</button>
 | 
					            <button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer hinzufügen</button>
 | 
				
			||||||
            <button class="btn" hx-get="/show-all-users-edit" hx-target="#page-content">Benutzer bearbeiten</button>
 | 
					            <button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer bearbeiten</button>
 | 
				
			||||||
            <button class="btn" hx-get="/show-all-users-delete" hx-target="#page-content">Benutzer löschen</button>
 | 
					            <button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer löschen</button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    {{end}}
 | 
					    {{end}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,9 @@
 | 
				
			|||||||
        <p class="text-center text-gray-500 dark:text-gray-400">
 | 
					        <p class="text-center text-gray-500 dark:text-gray-400">
 | 
				
			||||||
            © 2024 Jason Streifling. Alle Rechte vorbehalten.
 | 
					            © 2024 Jason Streifling. Alle Rechte vorbehalten.
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
 | 
					        <p class="text-center text-gray-500 dark:text-gray-400">
 | 
				
			||||||
 | 
					            <strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler enthalten.
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
    </footer>
 | 
					    </footer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <script src="https://unpkg.com/htmx.org@2.0.1"></script>
 | 
					    <script src="https://unpkg.com/htmx.org@2.0.1"></script>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								web/templates/published-articles.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/templates/published-articles.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					{{define "page-content"}}
 | 
				
			||||||
 | 
					<h2>Artikel löschen</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="flex flex-col gap-4">
 | 
				
			||||||
 | 
					    {{range .}}
 | 
				
			||||||
 | 
					    <button class="btn" hx-get="/article/review-deletion/{{.ID}}" hx-target="#page-content">
 | 
				
			||||||
 | 
					        <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
				
			||||||
 | 
					        <p>{{.Description}}</p>
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					    {{end}}
 | 
				
			||||||
 | 
					    <button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
<div class="flex flex-col gap-4">
 | 
					<div class="flex flex-col gap-4">
 | 
				
			||||||
    {{range .RejectedArticles}}
 | 
					    {{range .RejectedArticles}}
 | 
				
			||||||
    {{if index $.MyIDs .ID}}
 | 
					    {{if index $.MyIDs .ID}}
 | 
				
			||||||
    <button class="btn" hx-get="/review-rejected-article/{{.ID}}" hx-target="#page-content">
 | 
					    <button class="btn" hx-get="/article/review-rejected/{{.ID}}" hx-target="#page-content">
 | 
				
			||||||
        <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
					        <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
				
			||||||
        <p>{{.Description}}</p>
 | 
					        <p>{{.Description}}</p>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,19 +6,26 @@
 | 
				
			|||||||
        <label for="article-title">Titel</label>
 | 
					        <label for="article-title">Titel</label>
 | 
				
			||||||
        <input name="article-title" type="text" value="{{.Article.Title}}" />
 | 
					        <input name="article-title" type="text" value="{{.Article.Title}}" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="flex flex-col">
 | 
					
 | 
				
			||||||
 | 
					    <div class="flex flex-col gap-y-1">
 | 
				
			||||||
        <label for="article-description">Beschreibung</label>
 | 
					        <label for="article-description">Beschreibung</label>
 | 
				
			||||||
        <textarea name="article-description">{{.Article.Description}}</textarea>
 | 
					        <textarea name="article-description">{{.Article.Description}}</textarea>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="flex flex-col">
 | 
					
 | 
				
			||||||
        <label for="article-content">Artikel</label>
 | 
					    <div class="flex flex-col gap-y-1">
 | 
				
			||||||
        <textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea>
 | 
					        <label for="easyMDE">Artikel</label>
 | 
				
			||||||
 | 
					        <textarea id="easyMDE">{{.Content}}</textarea>
 | 
				
			||||||
 | 
					        <input id="article-content" name="article-content" type="hidden" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <input id="article-content" name="article-content" type="hidden" />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <span>Tags</span>
 | 
					        <span>Tags</span>
 | 
				
			||||||
        <div class="flex flex-wrap gap-x-4">
 | 
					        <div class="flex flex-wrap gap-x-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
 | 
				
			||||||
 | 
					                <label for="issue">Orient Express</label>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            {{range .Tags}}
 | 
					            {{range .Tags}}
 | 
				
			||||||
            <div>
 | 
					            <div>
 | 
				
			||||||
                <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
 | 
					                <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
 | 
				
			||||||
@@ -30,13 +37,15 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
        <input class="action-btn" type="submit" value="Senden" hx-post="/resubmit-article/{{.Article.ID}}"
 | 
					        <input class="action-btn" type="submit" value="Senden" hx-post="/article/resubmit/{{.Article.ID}}"
 | 
				
			||||||
            hx-target="#page-content" />
 | 
					            hx-target="#page-content" />
 | 
				
			||||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					    document.getElementById('article-content').value = easyMDE.value();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var easyMDE = new EasyMDE({
 | 
					    var easyMDE = new EasyMDE({
 | 
				
			||||||
        element: document.getElementById('easyMDE'),
 | 
					        element: document.getElementById('easyMDE'),
 | 
				
			||||||
        hideIcons: ['image'],
 | 
					        hideIcons: ['image'],
 | 
				
			||||||
@@ -48,7 +57,7 @@
 | 
				
			|||||||
            var formData = new FormData();
 | 
					            var formData = new FormData();
 | 
				
			||||||
            formData.append('article-image', file);
 | 
					            formData.append('article-image', file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            fetch('/upload-image', {
 | 
					            fetch('/article/upload-image', {
 | 
				
			||||||
                method: 'POST',
 | 
					                method: 'POST',
 | 
				
			||||||
                body: formData
 | 
					                body: formData
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<div class="flex flex-col gap-4">
 | 
					<div class="flex flex-col gap-4">
 | 
				
			||||||
    {{range .Users}}
 | 
					    {{range .Users}}
 | 
				
			||||||
    <button class="btn" hx-get="/{{$.Action}}/{{.ID}}" hx-target="#page-content">
 | 
					    <button class="btn" hx-get="/user/{{$.Action}}/{{.ID}}" hx-target="#page-content">
 | 
				
			||||||
        <h1 class="font-bold text-2xl">
 | 
					        <h1 class="font-bold text-2xl">
 | 
				
			||||||
            {{.UserName}}
 | 
					            {{.UserName}}
 | 
				
			||||||
            ({{if eq .Role 0}}
 | 
					            ({{if eq .Role 0}}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										36
									
								
								web/templates/to-be-deleted.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/templates/to-be-deleted.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					{{define "page-content"}}
 | 
				
			||||||
 | 
					<h2>Artikel löschen</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
					    <span>Titel</span>
 | 
				
			||||||
 | 
					    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
				
			||||||
 | 
					        {{.Title}}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span>Beschreibung</span>
 | 
				
			||||||
 | 
					    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
				
			||||||
 | 
					        {{.Description}}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span>Artikel</span>
 | 
				
			||||||
 | 
					    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
				
			||||||
 | 
					        <div class="prose">
 | 
				
			||||||
 | 
					            {{.Content}}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <span>Tags</span>
 | 
				
			||||||
 | 
					    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
				
			||||||
 | 
					        {{range .Tags}}
 | 
				
			||||||
 | 
					        {{.Name}}
 | 
				
			||||||
 | 
					        <br>
 | 
				
			||||||
 | 
					        {{end}}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="btn-area">
 | 
				
			||||||
 | 
					        <input class="action-btn" type="submit" value="Löschen" hx-get="/article/delete/{{.ID}}"
 | 
				
			||||||
 | 
					            hx-target="#page-content" />
 | 
				
			||||||
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
@@ -4,12 +4,12 @@
 | 
				
			|||||||
<div>
 | 
					<div>
 | 
				
			||||||
    <span>Titel</span>
 | 
					    <span>Titel</span>
 | 
				
			||||||
    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
					    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
				
			||||||
        {{.Title}}
 | 
					        {{.Article.Title}}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <span>Beschreibung</span>
 | 
					    <span>Beschreibung</span>
 | 
				
			||||||
    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
					    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
				
			||||||
        {{.Description}}
 | 
					        {{.Article.Description}}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <span>Artikel</span>
 | 
					    <span>Artikel</span>
 | 
				
			||||||
@@ -21,16 +21,21 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <span>Tags</span>
 | 
					    <span>Tags</span>
 | 
				
			||||||
    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
					    <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
 | 
				
			||||||
 | 
					        {{if .Article.IsInIssue}}
 | 
				
			||||||
 | 
					        <span>Orient Express</span>
 | 
				
			||||||
 | 
					        <br>
 | 
				
			||||||
 | 
					        {{end}}
 | 
				
			||||||
        {{range .Tags}}
 | 
					        {{range .Tags}}
 | 
				
			||||||
        {{.Name}}
 | 
					        <span>{{.Name}}</span>
 | 
				
			||||||
        <br>
 | 
					        <br>
 | 
				
			||||||
        {{end}}
 | 
					        {{end}}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-area">
 | 
					    <div class="btn-area">
 | 
				
			||||||
        <input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/publish-article/{{.ID}}"
 | 
					        <input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.Article.ID}}"
 | 
				
			||||||
 | 
					            hx-target="#page-content" />
 | 
				
			||||||
 | 
					        <input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}"
 | 
				
			||||||
            hx-target="#page-content" />
 | 
					            hx-target="#page-content" />
 | 
				
			||||||
        <input class="btn" type="submit" value="Ablehnen" hx-get="/reject-article/{{.ID}}" hx-target="#page-content" />
 | 
					 | 
				
			||||||
        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
					        <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<div class="flex flex-col gap-4">
 | 
					<div class="flex flex-col gap-4">
 | 
				
			||||||
    {{range .}}
 | 
					    {{range .}}
 | 
				
			||||||
    <button class="btn" hx-get="/review-unpublished-article/{{.ID}}" hx-target="#page-content">
 | 
					    <button class="btn" hx-get="/article/review-unpublished/{{.ID}}" hx-target="#page-content">
 | 
				
			||||||
        <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
					        <h1 class="font-bold text-2xl">{{.Title}}</h1>
 | 
				
			||||||
        <p>{{.Description}}</p>
 | 
					        <p>{{.Description}}</p>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user