Compare commits
	
		
			90 Commits
		
	
	
		
			v0.5.0
			...
			d2b21e7405
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d2b21e7405 | |||
| 4bd255a7c4 | |||
| 2743899b65 | |||
| 065ffcdc30 | |||
| 38ef7b80d5 | |||
| e3c192359f | |||
| c777a77824 | |||
| 46532e4c85 | |||
| c183043dac | |||
| c722135a56 | |||
| 391b3bf157 | |||
| 0aa479763d | |||
| ff0e229f03 | |||
| 8ef6ff729e | |||
| e4624b8705 | |||
| 887fa863bc | |||
| 4592bdf970 | |||
| dadd610b2d | |||
| 74d71cfb6a | |||
| 4004bcb8f0 | |||
| 9e0182ed03 | |||
| e554174c28 | |||
| 8597f1b849 | |||
| b04e0e5e81 | |||
| ca7e7cddd3 | |||
| 62921c8e2a | |||
| 94431a2aa9 | |||
| c7761e2dc8 | |||
| 5b1f20c5bc | |||
| 013cddc157 | |||
| 523ff9d2db | |||
| 84960fdd44 | |||
| d0c566f8df | |||
| 0a387c1087 | |||
| 38caf02b2c | |||
| 435157a42f | |||
| 4163afb0b7 | |||
| 5e586aa49a | |||
| 6cc0fb40d7 | |||
| 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 | 
| @@ -4,13 +4,17 @@ tmp_dir = "tmp" | |||||||
|  |  | ||||||
| [build] | [build] | ||||||
| args_bin = [ | args_bin = [ | ||||||
|  |     "-articles tmp/articles", | ||||||
|  |     "-config tmp/config.toml", | ||||||
|     "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'", |     "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'", | ||||||
|     "-domain localhost", |     "-domain localhost", | ||||||
|  |     "-firebase tmp/firebase.json", | ||||||
|     "-key tmp/key.gob", |     "-key tmp/key.gob", | ||||||
|     "-link https://distrikt-ni-st.de", |     "-link https://distrikt-ni-st.de", | ||||||
|     "-log tmp/cpolis.log", |     "-log tmp/cpolis.log", | ||||||
|     "-pdfs tmp/pdfs", |     "-pdfs tmp/pdfs", | ||||||
|     "-pics tmp/pics", |     "-pics tmp/pics", | ||||||
|  |     "-port 8080", | ||||||
|     "-rss tmp/orientexpress_alle.rss", |     "-rss tmp/orientexpress_alle.rss", | ||||||
|     "-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'", |     "-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'", | ||||||
|     "-web web", |     "-web web", | ||||||
|   | |||||||
| @@ -9,15 +9,21 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type Article struct { | type Article struct { | ||||||
| 	Title       string | 	Created       time.Time | ||||||
| 	Created     time.Time | 	Title         string | ||||||
| 	Description string | 	Description   string | ||||||
| 	Content     string | 	Link          string | ||||||
| 	Published   bool | 	EncURL        string | ||||||
| 	Rejected    bool | 	EncType       string | ||||||
| 	ID          int64 | 	ID            int64 | ||||||
| 	AuthorID    int64 | 	AuthorID      int64 | ||||||
| 	IssueID     int64 | 	IssueID       int64 | ||||||
|  | 	EditedID      int64 | ||||||
|  | 	EncLength     int | ||||||
|  | 	Published     bool | ||||||
|  | 	Rejected      bool | ||||||
|  | 	IsInIssue     bool | ||||||
|  | 	AutoGenerated bool | ||||||
| } | } | ||||||
|  |  | ||||||
| func (db *DB) AddArticle(a *Article) (int64, error) { | func (db *DB) AddArticle(a *Article) (int64, error) { | ||||||
| @@ -26,8 +32,9 @@ 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, | ||||||
|     VALUES (?, ?, ?, ?, ?, ?, ?) |         rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated) | ||||||
|  |     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||||||
|     ` |     ` | ||||||
|  |  | ||||||
| 	for i := 0; i < TxMaxRetries; i++ { | 	for i := 0; i < TxMaxRetries; i++ { | ||||||
| @@ -44,8 +51,9 @@ 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, | 			result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link, | ||||||
| 				a.Content, a.Published, a.Rejected, a.AuthorID, id) | 				a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id, | ||||||
|  | 				a.EditedID, 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 +87,9 @@ 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, edited_id, is_in_issue, auto_generated | ||||||
|     FROM articles |     FROM articles | ||||||
|     WHERE id = ? |     WHERE id = ? | ||||||
|     ` |     ` | ||||||
| @@ -90,7 +100,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.EditedID, | ||||||
|  | 		&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) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -103,14 +115,15 @@ func (db *DB) GetArticle(id int64) (*Article, error) { | |||||||
| 	return article, nil | 	return article, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { | func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { | ||||||
| 	query := ` | 	query := fmt.Sprintf(` | ||||||
|     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, published, rejected, is_in_issue, auto_generated | ||||||
|     FROM articles |     FROM articles | ||||||
|     WHERE published = ? |     WHERE %s = ? | ||||||
|     AND rejected = ? |     `, attribute) | ||||||
|     ` | 	rows, err := db.Query(query, value) | ||||||
| 	rows, err := db.Query(query, published, rejected) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error querying articles: %v", err) | 		return nil, fmt.Errorf("error querying articles: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -121,12 +134,12 @@ 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.Published, | ||||||
|  | 			&article.Rejected, &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) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		article.Published = false |  | ||||||
| 		article.Created, err = time.Parse("2006-01-02 15:04:05", string(created)) | 		article.Created, err = time.Parse("2006-01-02 15:04:05", string(created)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("error parsing created: %v", err) | 			return nil, fmt.Errorf("error parsing created: %v", err) | ||||||
| @@ -143,9 +156,11 @@ 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 +192,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 +272,20 @@ 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 = ?" | ||||||
|  | 	articlesQuery := "DELETE FROM articles WHERE id = ?" | ||||||
|  |  | ||||||
|  | 	_, err := db.Exec(articlesTagsQuery, id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting article %v from DB: %v", id, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = db.Exec(articlesQuery, id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting article %v from DB: %v", id, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package backend | |||||||
| import ( | import ( | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/fs" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| @@ -11,9 +12,12 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type Config struct { | type Config struct { | ||||||
|  | 	ArticleDir  string | ||||||
|  | 	ConfigFile  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,98 +31,199 @@ 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", | 		ConfigFile:  "/etc/cpolis/config.toml", | ||||||
| 		LogFile: "/var/log/cpolis.log", | 		DBName:      "cpolis", | ||||||
| 		PDFDir:  "/var/www/cpolis/pdfs", | 		FirebaseKey: "/var/www/cpolis/serviceAccountKey.json", | ||||||
| 		PicsDir: "/var/www/cpolis/pics", | 		KeyFile:     "/var/www/cpolis/cpolis.key", | ||||||
| 		RSSFile: "/var/www/cpolis/cpolis.rss", | 		LogFile:     "/var/log/cpolis.log", | ||||||
| 		WebDir:  "/var/www/cpolis/web", | 		PDFDir:      "/var/www/cpolis/pdfs", | ||||||
|  | 		PicsDir:     "/var/www/cpolis/pics", | ||||||
|  | 		Port:        ":8080", | ||||||
|  | 		RSSFile:     "/var/www/cpolis/cpolis.rss", | ||||||
|  | 		WebDir:      "/var/www/cpolis/web", | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (c *Config) readFile() error { | func mkDir(path string, perm fs.FileMode) (string, error) { | ||||||
| 	cfgFile, err := filepath.Abs(os.Getenv("HOME") + "/.config/cpolis/config.toml") | 	var err error | ||||||
|  |  | ||||||
|  | 	stringSlice := strings.Split(path, "/") | ||||||
|  | 	name := stringSlice[len(stringSlice)-1] | ||||||
|  |  | ||||||
|  | 	path, err = filepath.Abs(path) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("error getting absolute path for config file: %v", err) | 		return "", fmt.Errorf("error finding absolute path for %v directory: %v", name, err) | ||||||
|  | 	} | ||||||
|  | 	if err = os.MkdirAll(path, perm); err != nil { | ||||||
|  | 		return "", fmt.Errorf("error creating %v directory: %v", name, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, err = os.Stat(cfgFile) | 	return path, nil | ||||||
| 	if os.IsNotExist(err) { | } | ||||||
| 		fileStrings := strings.Split(cfgFile, "/") |  | ||||||
|  |  | ||||||
| 		dir := strings.Join(fileStrings[0:len(fileStrings)-1], "/") | func mkFile(path string, filePerm, dirPerm fs.FileMode) (string, error) { | ||||||
| 		if err = os.MkdirAll(dir, 0755); err != nil { | 	var err error | ||||||
| 			return fmt.Errorf("error creating config directory: %v", err) |  | ||||||
|  | 	path, err = filepath.Abs(path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("error finding absolute path for %v: %v", path, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	stringSlice := strings.Split(path, "/") | ||||||
|  | 	_, err = os.Stat(path) | ||||||
|  | 	if os.IsNotExist(err) { | ||||||
|  | 		dir := strings.Join(stringSlice[:len(stringSlice)-1], "/") | ||||||
|  | 		if err = os.MkdirAll(dir, dirPerm); err != nil { | ||||||
|  | 			return "", fmt.Errorf("error creating %v: %v", dir, err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		fileName := fileStrings[len(fileStrings)-1] | 		fileName := stringSlice[len(stringSlice)-1] | ||||||
| 		file, err := os.Create(dir + "/" + fileName) | 		file, err := os.Create(dir + "/" + fileName) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("error creating config file: %v", err) | 			return "", fmt.Errorf("error creating %v: %v", fileName, err) | ||||||
| 		} | 		} | ||||||
| 		defer file.Close() | 		defer file.Close() | ||||||
| 		if err = file.Chmod(0644); err != nil { | 		if err = file.Chmod(filePerm); err != nil { | ||||||
| 			return fmt.Errorf("error setting permissions for config file: %v", err) | 			return "", fmt.Errorf("error setting permissions for %v: %v", fileName, err) | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		_, err = toml.DecodeFile(cfgFile, c) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("error reading config file: %v", err) |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return path, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Config) handleCliArgs() error { | ||||||
|  | 	var port int | ||||||
|  | 	var err error | ||||||
|  |  | ||||||
|  | 	flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory") | ||||||
|  | 	flag.StringVar(&c.ConfigFile, "config", c.ConfigFile, "config file") | ||||||
|  | 	flag.StringVar(&c.DBName, "db", c.DBName, "DB name") | ||||||
|  | 	flag.StringVar(&c.Description, "desc", c.Description, "channel description") | ||||||
|  | 	flag.StringVar(&c.Domain, "domain", c.Domain, "domain name") | ||||||
|  | 	flag.StringVar(&c.FirebaseKey, "firebase", c.FirebaseKey, "Firebase service account key file") | ||||||
|  | 	flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file") | ||||||
|  | 	flag.StringVar(&c.Link, "link", c.Link, "channel Link") | ||||||
|  | 	flag.StringVar(&c.LogFile, "log", c.LogFile, "log file") | ||||||
|  | 	flag.StringVar(&c.PDFDir, "pdfs", c.PDFDir, "pdf directory") | ||||||
|  | 	flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory") | ||||||
|  | 	flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file") | ||||||
|  | 	flag.StringVar(&c.Title, "title", c.Title, "channel title") | ||||||
|  | 	flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory") | ||||||
|  | 	flag.IntVar(&port, "port", port, "port") | ||||||
|  | 	flag.Parse() | ||||||
|  |  | ||||||
|  | 	if port != 0 { | ||||||
|  | 		c.Port = fmt.Sprint(":", port) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	c.ConfigFile, err = mkFile(c.ConfigFile, 0600, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error setting up file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (c *Config) handleCliArgs() error { | func (c *Config) handleFile(configFile string) error { | ||||||
|  | 	_, err := toml.DecodeFile(configFile, c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error reading config file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Config) setupConfig(cliConfig *Config) error { | ||||||
| 	var err error | 	var err error | ||||||
| 	port := 8080 | 	defaultConfig := newConfig() | ||||||
|  |  | ||||||
| 	flag.StringVar(&c.DBName, "db", c.DBName, "DB name") | 	if cliConfig.ArticleDir != defaultConfig.ArticleDir { | ||||||
| 	flag.StringVar(&c.Description, "desc", c.Description, "Channel description") | 		c.ArticleDir = cliConfig.ArticleDir | ||||||
| 	flag.StringVar(&c.Domain, "domain", c.Domain, "domain name") | 	} | ||||||
| 	flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file") | 	c.ArticleDir, err = mkDir(c.ArticleDir, 0700) | ||||||
| 	flag.StringVar(&c.Link, "link", c.Link, "Channel Link") |  | ||||||
| 	flag.StringVar(&c.LogFile, "log", c.LogFile, "log file") |  | ||||||
| 	flag.StringVar(&c.PDFDir, "pdfs", c.PDFDir, "pdf directory") |  | ||||||
| 	flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory") |  | ||||||
| 	flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file") |  | ||||||
| 	flag.StringVar(&c.Title, "title", c.Title, "Channel title") |  | ||||||
| 	flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory") |  | ||||||
| 	flag.IntVar(&port, "port", port, "port") |  | ||||||
| 	flag.Parse() |  | ||||||
|  |  | ||||||
| 	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 setting up directory: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.LogFile, err = filepath.Abs(c.LogFile) | 	if cliConfig.DBName != defaultConfig.DBName { | ||||||
| 	if err != nil { | 		c.DBName = cliConfig.DBName | ||||||
| 		return fmt.Errorf("error finding absolute path for log file: %v", err) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.PDFDir, err = filepath.Abs(c.PDFDir) | 	if cliConfig.Description != defaultConfig.Description { | ||||||
| 	if err != nil { | 		c.Description = cliConfig.Description | ||||||
| 		return fmt.Errorf("error finding absolute path for pdfs dir: %v", err) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.PicsDir, err = filepath.Abs(c.PicsDir) | 	if cliConfig.Domain != defaultConfig.Domain { | ||||||
| 	if err != nil { | 		c.Domain = cliConfig.Domain | ||||||
| 		return fmt.Errorf("error finding absolute path for pics dir: %v", err) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.Port = fmt.Sprint(":", port) | 	if cliConfig.FirebaseKey != defaultConfig.FirebaseKey { | ||||||
|  | 		c.FirebaseKey = cliConfig.FirebaseKey | ||||||
| 	c.RSSFile, err = filepath.Abs(c.RSSFile) | 	} | ||||||
|  | 	c.FirebaseKey, err = mkFile(c.FirebaseKey, 0600, 0700) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("error finding absolute path for RSS file: %v", err) | 		return fmt.Errorf("error setting up file: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.WebDir, err = filepath.Abs(c.WebDir) | 	if cliConfig.KeyFile != defaultConfig.KeyFile { | ||||||
|  | 		c.KeyFile = cliConfig.KeyFile | ||||||
|  | 	} | ||||||
|  | 	c.KeyFile, err = mkFile(c.KeyFile, 0600, 0700) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("error finding absolute path for web dir: %v", err) | 		return fmt.Errorf("error setting up file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cliConfig.Link != defaultConfig.Link { | ||||||
|  | 		c.Link = cliConfig.Link | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cliConfig.LogFile != defaultConfig.LogFile { | ||||||
|  | 		c.LogFile = cliConfig.LogFile | ||||||
|  | 	} | ||||||
|  | 	c.LogFile, err = mkFile(c.LogFile, 0600, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error setting up file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cliConfig.PDFDir != defaultConfig.PDFDir { | ||||||
|  | 		c.PDFDir = cliConfig.PDFDir | ||||||
|  | 	} | ||||||
|  | 	c.PDFDir, err = mkDir(c.PDFDir, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error setting up directory: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cliConfig.PicsDir != defaultConfig.PicsDir { | ||||||
|  | 		c.PicsDir = cliConfig.PicsDir | ||||||
|  | 	} | ||||||
|  | 	c.PicsDir, err = mkDir(c.PicsDir, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error setting up directory: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cliConfig.Port != defaultConfig.Port { | ||||||
|  | 		c.Port = cliConfig.Port | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cliConfig.RSSFile != defaultConfig.RSSFile { | ||||||
|  | 		c.RSSFile = cliConfig.RSSFile | ||||||
|  | 	} | ||||||
|  | 	c.RSSFile, err = mkFile(c.RSSFile, 0600, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error setting up file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cliConfig.Title != defaultConfig.Title { | ||||||
|  | 		c.Title = cliConfig.Title | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cliConfig.WebDir != defaultConfig.WebDir { | ||||||
|  | 		c.WebDir = cliConfig.WebDir | ||||||
|  | 	} | ||||||
|  | 	c.WebDir, err = mkDir(c.WebDir, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error setting up directory: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -126,13 +231,19 @@ func (c *Config) handleCliArgs() error { | |||||||
|  |  | ||||||
| func HandleConfig() (*Config, error) { | func HandleConfig() (*Config, error) { | ||||||
| 	config := newConfig() | 	config := newConfig() | ||||||
|  | 	cliConfig := newConfig() | ||||||
|  |  | ||||||
| 	if err := config.readFile(); err != nil { | 	if err := cliConfig.handleCliArgs(); err != nil { | ||||||
| 		return nil, fmt.Errorf("error reading config file: %v", err) | 		return nil, fmt.Errorf("error handling cli arguments: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := config.handleCliArgs(); err != nil { | 	err := config.handleFile(cliConfig.ConfigFile) | ||||||
| 		return nil, fmt.Errorf("error handling cli arguments: %v", err) | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error reading configuration file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err = config.setupConfig(cliConfig); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error setting up files: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return config, nil | 	return config, nil | ||||||
|   | |||||||
| @@ -8,16 +8,14 @@ import ( | |||||||
| 	"google.golang.org/api/option" | 	"google.golang.org/api/option" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Client struct { | 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,56 +9,15 @@ 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), | 		Items:       make([]*rss.Item, 0), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	articles, err := db.GetCertainArticles(true, false) | 	articles, err := db.GetCertainArticles("published", true) | ||||||
| 	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), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	articles, err := db.GetCertainArticles(true, false) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err) | 		return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -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() | ||||||
|   | |||||||
| @@ -10,9 +10,7 @@ import ( | |||||||
| 	"github.com/gorilla/sessions" | 	"github.com/gorilla/sessions" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type CookieStore struct { | type CookieStore struct{ sessions.CookieStore } | ||||||
| 	sessions.CookieStore |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func NewKey() ([]byte, error) { | func NewKey() ([]byte, error) { | ||||||
| 	key := make([]byte, 32) | 	key := make([]byte, 32) | ||||||
|   | |||||||
							
								
								
									
										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,6 +22,38 @@ const ( | |||||||
| 	PreviewMode | 	PreviewMode | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | type EditorHTMLData struct { | ||||||
|  | 	Selected     map[int64]bool | ||||||
|  | 	Content      string | ||||||
|  | 	Action       string | ||||||
|  | 	ActionTitle  string | ||||||
|  | 	ActionButton string | ||||||
|  | 	HTMLContent  template.HTML | ||||||
|  | 	Article      *b.Article | ||||||
|  | 	Tags         []*b.Tag | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func copyFile(src, dst string) error { | ||||||
|  | 	srcFile, err := os.Open(src) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error opening source file: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer srcFile.Close() | ||||||
|  |  | ||||||
|  | 	dstFile, err := os.Create(dst) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error opening destination file: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer dstFile.Close() | ||||||
|  |  | ||||||
|  | 	_, err = io.Copy(dstFile, srcFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error copying file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return dstFile.Sync() | ||||||
|  | } | ||||||
|  |  | ||||||
| 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) | ||||||
| @@ -30,22 +61,14 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		type editorHTMLData struct { | 		data := new(EditorHTMLData) | ||||||
| 			Title       string |  | ||||||
| 			Description string |  | ||||||
| 			Content     string |  | ||||||
| 			HTMLContent template.HTML |  | ||||||
| 			Tags        []*b.Tag |  | ||||||
| 			Mode        int |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		var data editorHTMLData |  | ||||||
| 		if session.Values["article"] == nil { | 		if session.Values["article"] == nil { | ||||||
| 			data = editorHTMLData{} | 			data = &EditorHTMLData{Article: new(b.Article)} | ||||||
| 		} else { | 		} else { | ||||||
| 			data = session.Values["article"].(editorHTMLData) | 			data = session.Values["article"].(*EditorHTMLData) | ||||||
| 		} | 		} | ||||||
| 		data.Mode = EditMode | 		// data.Mode = EditMode | ||||||
|  |  | ||||||
| 		data.Tags, err = db.GetTagList() | 		data.Tags, err = db.GetTagList() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -54,6 +77,8 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		data.Action = "submit" | ||||||
|  |  | ||||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") | 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") | ||||||
| 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data) | 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data) | ||||||
| 	} | 	} | ||||||
| @@ -74,12 +99,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) | ||||||
| @@ -89,6 +115,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"] { | ||||||
| @@ -130,11 +170,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) | ||||||
| @@ -164,22 +211,41 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func ShowUnpublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(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 { | 		if _, err := getSession(w, r, c, s); err != nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		unpublishedArticles, err := db.GetCertainArticles(false, false) | 		rejectedArticles, err := db.GetCertainArticles("rejected", true) | ||||||
| 		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 | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		articles := make([]*b.Article, 0) | ||||||
|  | 		for _, article := range rejectedArticles { | ||||||
|  | 			if article.Published { | ||||||
|  | 				articles = append(articles, article) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		unpublishedArticles, err := db.GetCertainArticles("published", false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, article := range unpublishedArticles { | ||||||
|  | 			if !article.Rejected { | ||||||
|  | 				articles = append(articles, article) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html") | 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html") | ||||||
| 		tmpl = template.Must(tmpl, err) | 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles) | ||||||
| 		tmpl.ExecuteTemplate(w, "page-content", unpublishedArticles) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -190,13 +256,12 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		type htmlData struct { | 		data := new(struct { | ||||||
| 			MyIDs            map[int64]bool | 			MyIDs            map[int64]bool | ||||||
| 			RejectedArticles []*b.Article | 			RejectedArticles []*b.Article | ||||||
| 		} | 		}) | ||||||
| 		data := new(htmlData) |  | ||||||
|  |  | ||||||
| 		data.RejectedArticles, err = db.GetCertainArticles(false, true) | 		data.RejectedArticles, err = db.GetCertainArticles("rejected", true) | ||||||
| 		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) | ||||||
| @@ -216,85 +281,12 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func ReviewUnpublishedArticle(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 |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		content, err := b.ConvertToHTML(article.Content) |  | ||||||
| 		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-published.html") |  | ||||||
| 		tmpl = template.Must(tmpl, err) |  | ||||||
| 		tmpl.ExecuteTemplate(w, "page-content", data) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | func ReviewRejectedArticle(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 { | 		if _, err := getSession(w, r, c, s); err != nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		type htmlData struct { |  | ||||||
| 			Selected map[int64]bool |  | ||||||
| 			Article  *b.Article |  | ||||||
| 			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 { | ||||||
| 			log.Println(err) | 			log.Println(err) | ||||||
| @@ -302,6 +294,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		data := new(EditorHTMLData) | ||||||
| 		data.Article, err = db.GetArticle(id) | 		data.Article, err = db.GetArticle(id) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Println(err) | 			log.Println(err) | ||||||
| @@ -309,6 +302,15 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") | ||||||
|  | 		content, err := os.ReadFile(articleAbsName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		data.Content = string(content) | ||||||
|  |  | ||||||
| 		data.Tags, err = db.GetTagList() | 		data.Tags, err = db.GetTagList() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Println(err) | 			log.Println(err) | ||||||
| @@ -327,7 +329,9 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | |||||||
| 			data.Selected[tag.ID] = true | 			data.Selected[tag.ID] = true | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html") | 		data.Action = fmt.Sprint("resubmit/", data.Article.ID) | ||||||
|  |  | ||||||
|  | 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") | ||||||
| 		tmpl = template.Must(tmpl, err) | 		tmpl = template.Must(tmpl, err) | ||||||
| 		tmpl.ExecuteTemplate(w, "page-content", data) | 		tmpl.ExecuteTemplate(w, "page-content", data) | ||||||
| 	} | 	} | ||||||
| @@ -341,13 +345,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 | ||||||
| @@ -363,7 +374,37 @@ 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) | 		if article.EditedID != 0 { | ||||||
|  | 			oldArticle, err := db.GetArticle(article.EditedID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Println(err) | ||||||
|  | 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if err = db.DeleteArticle(oldArticle.ID); err != nil { | ||||||
|  | 				log.Println(err) | ||||||
|  | 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")); err != nil { | ||||||
|  | 				log.Println(err) | ||||||
|  | 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if err = db.UpdateAttributes( | ||||||
|  | 				&b.Attribute{Table: "articles", ID: id, AttName: "link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)}, | ||||||
|  | 				&b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0}, | ||||||
|  | 			); err != nil { | ||||||
|  | 				log.Println(err) | ||||||
|  | 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		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) | ||||||
| @@ -395,9 +436,7 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err = db.UpdateAttributes( | 		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}); err != nil { | ||||||
| 			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}, |  | ||||||
| 		); err != nil { |  | ||||||
| 			log.Println(err) | 			log.Println(err) | ||||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
| 			return | 			return | ||||||
| @@ -427,7 +466,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 | ||||||
| @@ -436,7 +475,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() | ||||||
| @@ -451,12 +490,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) | ||||||
| @@ -471,8 +504,352 @@ 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, action string) http.HandlerFunc { | ||||||
|  | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		if _, err := getSession(w, r, c, s); err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		data := new(struct { | ||||||
|  | 			Action   string | ||||||
|  | 			Articles []*b.Article | ||||||
|  | 		}) | ||||||
|  | 		data.Action = action | ||||||
|  |  | ||||||
|  | 		publishedArticles, err := db.GetCertainArticles("published", true) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, article := range publishedArticles { | ||||||
|  | 			if !article.AutoGenerated { | ||||||
|  | 				data.Articles = append(data.Articles, article) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html") | ||||||
|  | 		tmpl = template.Must(tmpl, err) | ||||||
|  | 		tmpl.ExecuteTemplate(w, "page-content", data) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, button string) http.HandlerFunc { | ||||||
|  | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		if _, err := getSession(w, r, c, s); 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 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		article, err := db.GetArticle(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		data := &EditorHTMLData{ | ||||||
|  | 			Article: &b.Article{ | ||||||
|  | 				ID:        id, | ||||||
|  | 				IsInIssue: article.IsInIssue, | ||||||
|  | 			}, | ||||||
|  | 			Action:       action, | ||||||
|  | 			ActionTitle:  title, | ||||||
|  | 			ActionButton: button, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		data.Article.Title, err = b.ConvertToPlain(article.Title) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		data.Article.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") | ||||||
|  | 		content, err := os.ReadFile(articleAbsName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		data.Content, err = b.ConvertToHTML(string(content)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		data.HTMLContent = template.HTML(data.Content) | ||||||
|  |  | ||||||
|  | 		data.Tags, err = db.GetArticleTags(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/review-article.html") | ||||||
|  | 		template.Must(tmpl, err).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)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func AllowEditArticle(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 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		oldID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		fmt.Println(oldID) | ||||||
|  |  | ||||||
|  | 		oldArticle, err := db.GetArticle(oldID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		newArticle := oldArticle | ||||||
|  | 		newArticle.Published = false | ||||||
|  | 		newArticle.Rejected = true | ||||||
|  | 		newArticle.EditedID = oldArticle.ID | ||||||
|  |  | ||||||
|  | 		newID, err := db.AddArticle(newArticle) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldID, AttName: "edited_id", Value: newID}); err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err = copyFile(fmt.Sprint(c.ArticleDir, "/", oldID, ".md"), fmt.Sprint(c.ArticleDir, "/", newID, ".md")); 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)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func EditArticle(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 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		data := new(EditorHTMLData) | ||||||
|  |  | ||||||
|  | 		data.Article, err = db.GetArticle(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		data.Content = string(content) | ||||||
|  |  | ||||||
|  | 		data.Tags, err = db.GetArticleTags(data.Article.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		selectedTags, err := db.GetArticleTags(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		data.Selected = make(map[int64]bool) | ||||||
|  | 		for _, tag := range selectedTags { | ||||||
|  | 			data.Selected[tag.ID] = true | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		data.Action = fmt.Sprint("save/", data.Article.ID) | ||||||
|  |  | ||||||
|  | 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html") | ||||||
|  | 		template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func SaveArticle(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 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		article := &b.Article{ | ||||||
|  | 			Title:         r.PostFormValue("article-title"), | ||||||
|  | 			Description:   r.PostFormValue("article-description"), | ||||||
|  | 			Published:     false, | ||||||
|  | 			Rejected:      false, | ||||||
|  | 			AuthorID:      session.Values["id"].(int64), | ||||||
|  | 			IsInIssue:     r.PostFormValue("issue") == "on", | ||||||
|  | 			AutoGenerated: false, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		oldID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		fmt.Println(oldID) | ||||||
|  | 		if oldID != 0 { | ||||||
|  | 			if err = db.DeleteArticle(oldID); err != nil { | ||||||
|  | 				log.Println(err) | ||||||
|  | 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldID, ".md")); err != nil { | ||||||
|  | 				log.Println(err) | ||||||
|  | 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		article.ID, err = db.AddArticle(article) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(err) | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		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() | ||||||
|  | 		tags := make([]int64, 0) | ||||||
|  | 		for _, tag := range r.Form["tags"] { | ||||||
|  | 			tagID, err := strconv.ParseInt(tag, 10, 64) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Println(err) | ||||||
|  | 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			tags = append(tags, tagID) | ||||||
|  | 		} | ||||||
|  | 		if err = db.WriteArticleTags(article.ID, tags); 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"]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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,174 @@ 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 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		title := r.PostFormValue("issue-title") | ||||||
|  | 		if len(title) == 0 { | ||||||
|  | 			err = fmt.Errorf("error: no title for issue specified") | ||||||
|  | 			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 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		article := &b.Article{ | ||||||
|  | 			Title:         title, | ||||||
|  | 			EncURL:        fmt.Sprint(c.Domain, "/image/serve/", imgFileName), | ||||||
|  | 			EncLength:     int(imgInfo.Size()), | ||||||
|  | 			EncType:       mime.TypeByExtension(filepath.Ext(imgAbsName)), | ||||||
|  | 			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) | ||||||
|   | |||||||
| @@ -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{ | ||||||
| @@ -281,12 +277,12 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		type htmlData struct { | 		data := new(struct { | ||||||
| 			Users  map[int64]*b.User | 			Users  map[int64]*b.User | ||||||
| 			Action string | 			Action string | ||||||
| 		} | 		}) | ||||||
|  |  | ||||||
| 		data := &htmlData{Action: action} | 		data.Action = action | ||||||
| 		data.Users, err = db.GetAllUsers() | 		data.Users, err = db.GetAllUsers() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Println(err) | 			log.Println(err) | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								cmd/main.go
									
									
									
									
									
								
							| @@ -49,38 +49,49 @@ 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/allow-edit/{id}", f.AllowEditArticle(config, db, store)) | ||||||
| 	mux.HandleFunc("GET /create-user", f.CreateUser(config, store)) | 	mux.HandleFunc("GET /article/all-published/review-edit", f.ShowPublishedArticles(config, db, store, "review-edit")) | ||||||
| 	mux.HandleFunc("GET /edit-self", f.EditSelf(config, db, store)) | 	mux.HandleFunc("GET /article/all-published/delete", f.ShowPublishedArticles(config, db, store, "review-delete")) | ||||||
| 	mux.HandleFunc("GET /edit-user/{id}", f.EditUser(config, db, store)) | 	mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store)) | ||||||
| 	mux.HandleFunc("GET /delete-user/{id}", f.DeleteUser(config, db, store)) | 	mux.HandleFunc("GET /article/all-unpublished-unrejected-and-published-rejected", f.ShowUnpublishedUnrejectedAndPublishedRejectedArticles(config, db, store)) | ||||||
|  | 	mux.HandleFunc("GET /article/delete/{id}", f.DeleteArticle(config, db, store)) | ||||||
|  | 	mux.HandleFunc("GET /article/edit/{id}", f.EditArticle(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-delete/{id}", f.ReviewArticle(config, db, store, "delete", "Artikel löschen", "Löschen")) | ||||||
|  | 	mux.HandleFunc("GET /article/review-edit/{id}", f.ReviewArticle(config, db, store, "allow-edit", "Artikel bearbeiten", "Bearbeiten erlauben")) | ||||||
|  | 	mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store)) | ||||||
|  | 	mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, store, "publish", "Artikel veröffentlichen", "Veröffentlichen")) | ||||||
|  | 	mux.HandleFunc("GET /article/save/{id}", f.SaveArticle(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)) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,40 +11,46 @@ 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, | ||||||
|  |     edited_id       INT, | ||||||
|  |     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= | ||||||
|   | |||||||
| @@ -7,4 +7,5 @@ module.exports = { | |||||||
|     plugins: [ |     plugins: [ | ||||||
|         require('@tailwindcss/typography') |         require('@tailwindcss/typography') | ||||||
|     ], |     ], | ||||||
|  |     darkMode: 'selector', | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								update.sh
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								update.sh
									
									
									
									
									
								
							| @@ -6,35 +6,53 @@ CPOLIS_DIR=$EXTRACTION_DIR/cpolis | |||||||
| TAILWINDCSS_REPO_URL=https://api.github.com/repos/tailwindlabs/tailwindcss/releases/latest | TAILWINDCSS_REPO_URL=https://api.github.com/repos/tailwindlabs/tailwindcss/releases/latest | ||||||
| TMP_DIR=/tmp | TMP_DIR=/tmp | ||||||
| BIN_DIR=/usr/local/bin | BIN_DIR=/usr/local/bin | ||||||
| SYSTEMD_DIR=/lib/systemd/system | SYSTEMD_DIR=/etc/systemd/system | ||||||
|  |  | ||||||
| ! groups | grep -E 'root|wheel|sudo' && echo "You need administrative privileges for this script" && exit 1 | check_dependency() { | ||||||
|  |     if ! which $1 >/dev/null 2>&1; then | ||||||
|  |         echo "$1 needs to be installed" >&2 | ||||||
|  |         exit 1 | ||||||
|  |     fi | ||||||
|  | } | ||||||
|  |  | ||||||
| ! which curl >/dev/null 2>&1 && echo "curl needs to be installed" >&2 && exit 1 | if ! groups | grep -E 'root|wheel|sudo' >/dev/null; then | ||||||
| ! which go >/dev/null 2>&1 && echo "go needs to be installed" >&2 && exit 1 |     echo "You need administrative privileges for this script" >&2 | ||||||
| ! which jq >/dev/null 2>&1 && echo "jq needs to be installed" >&2 && exit 1 |     exit 1 | ||||||
| ! which tar >/dev/null 2>&1 && echo "tar needs to be installed" >&2 && exit 1 | fi | ||||||
| ! which xargs >/dev/null 2>&1 && echo "xargs needs to be installed" >&2 && exit 1 |  | ||||||
|  |  | ||||||
|  | check_dependency curl | ||||||
|  | check_dependency go | ||||||
|  | check_dependency jq | ||||||
|  | check_dependency tar | ||||||
|  | check_dependency xargs | ||||||
|  |  | ||||||
|  | echo '\nDownloading cpolis...' >&2 | ||||||
| rm -fr $CPOLIS_DIR/* | rm -fr $CPOLIS_DIR/* | ||||||
| latest_release=$(curl -s $CPOLIS_REPO_URL | jq -r '.[0].tag_name') | 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 | 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 | tar -xzf $TMP_DIR/cpolis.tar.gz -C $EXTRACTION_DIR | ||||||
| rm $TMP_DIR/cpolis.tar.gz | rm $TMP_DIR/cpolis.tar.gz | ||||||
|  |  | ||||||
|  | echo '\nDownloading TailwindCSS...' >&2 | ||||||
| curl -s $TAILWINDCSS_REPO_URL | | curl -s $TAILWINDCSS_REPO_URL | | ||||||
|     grep -F browser_download_url | |     grep -F browser_download_url | | ||||||
|     grep -F linux-x64 | |     grep -F linux-x64 | | ||||||
|     cut -d'"' -f4 | |     cut -d'"' -f4 | | ||||||
|     xargs -r curl -Lo $CPOLIS_DIR/tailwindcss |     xargs -r curl -Lo $CPOLIS_DIR/tailwindcss | ||||||
| chmod +x $CPOLIS_DIR/tailwindcss | chmod +x $CPOLIS_DIR/tailwindcss | ||||||
| $CPOLIS_DIR/tailwindcss -i web/static/css/input.css -o web/static/css/style.css | $CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css | ||||||
|  |  | ||||||
| go build -o $TMP_DIR/cpolis $CPOLIS_DIR/cmd/main.go | echo '\nBuilding cpolis...' >&2 | ||||||
| sudo mv $TMP_DIR/cpolis $BIN_DIR/cpolis | 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 | chmod +x $BIN_DIR/cpolis | ||||||
|  |  | ||||||
| [ ! -f $SYSTEMD_DIR ] && mkdir -p $SYSTEMD_DIR | echo '\nSetting up service...' >&2 | ||||||
| sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR | sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR | ||||||
|  | sudo chown root:root $SYSTEMD_DIR/cpolis.service | ||||||
| sudo systemctl daemon-reload | sudo systemctl daemon-reload | ||||||
| sudo systemctl is-active --quiet cpolis.service && sudo systemctl restart cpolis.service | sudo systemctl is-active --quiet cpolis.service && sudo systemctl restart cpolis.service | ||||||
|   | |||||||
| @@ -2,40 +2,71 @@ | |||||||
| @tailwind components; | @tailwind components; | ||||||
| @tailwind utilities; | @tailwind utilities; | ||||||
|  |  | ||||||
| body { |  | ||||||
|     width: 800px; |  | ||||||
|     @apply mx-auto text-slate-900; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| h2 { | h2 { | ||||||
|     @apply font-bold mb-2 text-2xl; |     @apply font-bold mb-2 text-2xl; | ||||||
| } | } | ||||||
|  |  | ||||||
| form { | h3 { | ||||||
|     @apply flex flex-col gap-y-3; |     @apply font-bold mb-2 text-xl; | ||||||
| } |  | ||||||
|  |  | ||||||
| input[type="file"] { |  | ||||||
|     @apply border rounded-md w-full; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| input[type="password"], | input[type="password"], | ||||||
| input[type="text"] { | input[type="text"] { | ||||||
|     @apply border h-8 rounded-md; |     @apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-8 rounded-md; | ||||||
| } | } | ||||||
|  |  | ||||||
| textarea { | textarea { | ||||||
|     @apply border h-32 rounded-md; |     @apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-32 rounded-md; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .btn-area-1 { | ||||||
|  |     @apply grid grid-cols-1 gap-x-4 gap-y-1 mt-4; | ||||||
| } | } | ||||||
|  |  | ||||||
| .btn-area { | .btn-area { | ||||||
|     @apply flex gap-4 mt-4; |     @apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .btn-area-3 { | ||||||
|  |     @apply grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-1 mt-4; | ||||||
| } | } | ||||||
|  |  | ||||||
| .btn { | .btn { | ||||||
|     @apply bg-slate-200 border my-2 px-3 py-2 rounded-md w-full hover:bg-slate-100; |     @apply bg-slate-200 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-900 border border-slate-200 dark:border-slate-800 my-2 px-3 py-2 rounded-md w-full; | ||||||
| } | } | ||||||
|  |  | ||||||
| .action-btn { | .action-btn { | ||||||
|     @apply bg-slate-800 border my-2 px-3 py-2 rounded-md text-slate-50 w-full hover:bg-slate-700; |     @apply bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 my-2 px-3 py-2 rounded-md text-slate-50 dark:text-slate-950 w-full; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .EasyMDEContainer .CodeMirror { | ||||||
|  |     @apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .EasyMDEContainer .cm-s-easymde .CodeMirror-cursor { | ||||||
|  |     @apply border-slate-900 dark:border-slate-100 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .EasyMDEContainer .editor-toolbar > * { | ||||||
|  |     @apply text-slate-900 dark:text-slate-100 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .EasyMDEContainer .editor-toolbar > .active, .editor-toolbar > button:hover, .editor-preview pre, .cm-s-easymde .cm-comment { | ||||||
|  |     @apply bg-slate-100 dark:bg-slate-900 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .EasyMDEContainer .CodeMirror-fullscreen { | ||||||
|  |     @apply bg-slate-50 dark:bg-slate-950 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .editor-toolbar { | ||||||
|  |     @apply border border-slate-200 dark:border-slate-800 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .editor-toolbar.fullscreen { | ||||||
|  |     @apply bg-slate-50 dark:bg-slate-950 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .editor-preview { | ||||||
|  |     @apply bg-slate-50 dark:bg-slate-950 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,9 +2,9 @@ | |||||||
| <h2>Neuer Tag</h2> | <h2>Neuer Tag</h2> | ||||||
|  |  | ||||||
| <form> | <form> | ||||||
|     <input required name="tag" placeholder="Tag eingeben" type="text" /> |     <input class="w-full" name="tag" placeholder="Tag eingeben" required 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> | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <h2>Neuer Benutzer</h2> | <h2>Neuer Benutzer</h2> | ||||||
|  |  | ||||||
| <form> | <form> | ||||||
|     <div class="grid grid-cols-3 gap-4"> |     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||||
|         <div> |         <div> | ||||||
|             <label for="username">Benutzername</label> |             <label for="username">Benutzername</label> | ||||||
|             <input class="w-full" required name="username" type="text" value="{{.UserName}}" /> |             <input class="w-full" required name="username" type="text" value="{{.UserName}}" /> | ||||||
| @@ -25,7 +25,7 @@ | |||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="flex gap-4"> |     <div class="flex flex-wrap gap-4"> | ||||||
|         <div> |         <div> | ||||||
|             <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> |             <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> | ||||||
|             <label for="author">Autor</label> |             <label for="author">Autor</label> | ||||||
| @@ -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,73 @@ | |||||||
| {{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 border-slate-200 dark:border-slate-800 px-2 py-1 rounded-md"> | ||||||
|  |                     <h1 class="font-bold text-2xl">{{.Title}}</h1> | ||||||
|  |                     <p>{{.Description}}</p> | ||||||
|  |                 </div> | ||||||
|  |                 {{end}} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div> | ||||||
|  |             <h3>Titelseite</h3> | ||||||
|  |             <div class="grid grid-cols-2 gap-4 items-center"> | ||||||
|  |                 <input class="h-full" name="issue-title" placeholder="Titel" required type="text" /> | ||||||
|  |                 <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>Ü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}} | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <h2>Profil bearbeiten</h2> | <h2>Profil bearbeiten</h2> | ||||||
|  |  | ||||||
| <form> | <form> | ||||||
|     <div class="grid grid-cols-3 gap-4"> |     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||||
|         <div> |         <div> | ||||||
|             <label for="username">Benutzername</label> |             <label for="username">Benutzername</label> | ||||||
|             <input class="w-full" name="username" type="text" value="{{.UserName}}" /> |             <input class="w-full" name="username" type="text" value="{{.UserName}}" /> | ||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2> | <h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2> | ||||||
|  |  | ||||||
| <form> | <form> | ||||||
|     <div class="grid grid-cols-3 gap-4"> |     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||||
|         <div> |         <div> | ||||||
|             <label for="username">Benutzername</label> |             <label for="username">Benutzername</label> | ||||||
|             <input class="w-full" name="username" type="text" value="{{.UserName}}" /> |             <input class="w-full" name="username" type="text" value="{{.UserName}}" /> | ||||||
| @@ -29,7 +29,7 @@ | |||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="flex gap-4"> |     <div class="flex flex-wrap gap-4"> | ||||||
|         <div> |         <div> | ||||||
|             <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> |             <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> | ||||||
|             <label for="author">Autor</label> |             <label for="author">Autor</label> | ||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -4,32 +4,41 @@ | |||||||
| <form id="edit-area"> | <form id="edit-area"> | ||||||
|     <div class="flex flex-col gap-y-1"> |     <div class="flex flex-col gap-y-1"> | ||||||
|         <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="{{.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">{{.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"> | ||||||
|  |         <label for="easyMDE">Artikel</label> | ||||||
|         <textarea id="easyMDE">{{.Content}}</textarea> |         <textarea id="easyMDE">{{.Content}}</textarea> | ||||||
|  |         <input id="article-content" name="article-content" type="hidden" value="{{.Content}}" /> | ||||||
|     </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="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" /> |                 <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected | ||||||
|                 <label for="{{.Name}}">{{.Name}}</label> |                     .ID}}checked{{end}} /> | ||||||
|  |                 <label for="tag-{{.Name}}">{{.Name}}</label> | ||||||
|             </div> |             </div> | ||||||
|             {{end}} |             {{end}} | ||||||
|         </div> |         </div> | ||||||
|     </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/{{.Action}}" | ||||||
|  |             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 +55,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 | ||||||
|             }) |             }) | ||||||
|   | |||||||
| @@ -25,8 +25,8 @@ | |||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="btn-area"> |     <div class="btn-area-1"> | ||||||
|         <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> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,44 +4,46 @@ | |||||||
|  |  | ||||||
|     {{if lt . 4}} |     {{if lt . 4}} | ||||||
|     <div class="mb-3"> |     <div class="mb-3"> | ||||||
|         <h2>Autor</h2> |         <h2>Artikel</h2> | ||||||
|         <div class="grid grid-cols-2 gap-x-4 gap-y-2"> |         <div class="grid grid-cols-1 md: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">Artikel bearbeiten</button> | ||||||
|             <a class="btn text-center" href="/rss">RSS Feed</a> |             {{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished-unrejected-and-published-rejected" | ||||||
|             <button class="btn" hx-get="/edit-self" hx-target="#page-content">Profil bearbeiten</button> |                 hx-target="#page-content">Artikel veröffentlichen</button>{{end}} | ||||||
|         </div> |             {{if lt . 2}}<button class="btn" hx-get="/article/all-published/delete" hx-target="#page-content">Artikel | ||||||
|     </div> |                 löschen</button>{{end}} | ||||||
|     {{end}} |             {{if lt . 2}}<button class="btn" hx-get="/article/all-published/review-edit" | ||||||
|  |                 hx-target="#page-content">Artikel bearbeiten lassen</button>{{end}} | ||||||
|     {{if lt . 3}} |             {{if lt . 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>{{end}} | ||||||
|     <div class="mb-3"> |  | ||||||
|         <h2>Redakteur</h2> |  | ||||||
|         <div class="grid grid-cols-2 gap-4"> |  | ||||||
|             <button class="btn" hx-get="/unpublished-articles" hx-target="#page-content"> |  | ||||||
|                 Unveröffentlichte Artikel |  | ||||||
|             </button> |  | ||||||
|             <button class="btn" hx-get="/create-tag" hx-target="#page-content">Neuer Tag</button> |  | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|     {{end}} |     {{end}} | ||||||
|  |  | ||||||
|     {{if lt . 2}} |     {{if lt . 2}} | ||||||
|     <div class="mb-3"> |     <div class="mb-3"> | ||||||
|         <h2>Herausgeber</h2> |         <h2>Ausgabe</h2> | ||||||
|         <div class="grid grid-cols-2 gap-4"> |         <div class="grid grid-cols-1 md: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> | ||||||
|  |             <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> | ||||||
|  |         {{end}} | ||||||
|     </div> |     </div> | ||||||
|     {{end}} |  | ||||||
|  |  | ||||||
|     {{if eq . 0}} |     {{if lt . 4}} | ||||||
|     <div class="mb-3"> |     <div class="mb-3"> | ||||||
|         <h2>Administrator</h2> |         <h2>Benutzer</h2> | ||||||
|         <div class="grid grid-cols-3 gap-4"> |         <div class="grid grid-cols-1 md: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/edit/self" hx-target="#page-content">Mein Profil bearbeiten</button> | ||||||
|             <button class="btn" hx-get="/show-all-users-edit" hx-target="#page-content">Benutzer bearbeiten</button> |             {{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer | ||||||
|             <button class="btn" hx-get="/show-all-users-delete" hx-target="#page-content">Benutzer löschen</button> |                 hinzufügen</button>{{end}} | ||||||
|  |             {{if eq . 0}}<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer | ||||||
|  |                 bearbeiten</button>{{end}} | ||||||
|  |             {{if eq . 0}}<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer | ||||||
|  |                 löschen</button>{{end}} | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|     {{end}} |     {{end}} | ||||||
|   | |||||||
| @@ -5,13 +5,26 @@ | |||||||
|     <meta charset="UTF-8"> |     <meta charset="UTF-8"> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> |     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|     <title>Orient Editor</title> |     <title>Orient Editor</title> | ||||||
|  |  | ||||||
|     <link href="/web/static/css/style.css" rel="stylesheet"> |     <link href="/web/static/css/style.css" rel="stylesheet"> | ||||||
|     <link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css"> |     <link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css"> | ||||||
| </head> | </head> | ||||||
|  |  | ||||||
| <body class="flex flex-col justify-between min-h-screen bg-slate-50"> | <body | ||||||
|  |     class="bg-slate-50 dark:bg-slate-950 container flex flex-col justify-between max-w-screen-lg min-h-screen mx-auto text-slate-900 dark:text-slate-100"> | ||||||
|     <header class="my-8"> |     <header class="my-8"> | ||||||
|         <h1 class="font-bold text-4xl text-center">Orient Editor</h1> |         <h1 class="font-bold text-4xl text-center">Orient Editor</h1> | ||||||
|  |  | ||||||
|  |         <div class="ml-4"> | ||||||
|  |             <label class="cursor-pointer flex items-center relative" for="theme-toggle"> | ||||||
|  |                 <div class="bg-slate-200 dark:bg-slate-800 block h-6 w-12 rounded-full"></div> | ||||||
|  |                 <div | ||||||
|  |                     class="absolute bg-slate-800 dark:bg-slate-50 dot left-1 top-1 h-4 w-4 rounded-full transform transition"> | ||||||
|  |                 </div> | ||||||
|  |                 <span class="ml-2">Dunkel</span> | ||||||
|  |             </label> | ||||||
|  |             <input type="checkbox" id="theme-toggle" class="sr-only"> | ||||||
|  |         </div> | ||||||
|     </header> |     </header> | ||||||
|  |  | ||||||
|     <main class="mx-4"> |     <main class="mx-4"> | ||||||
| @@ -20,14 +33,39 @@ | |||||||
|         </div> |         </div> | ||||||
|     </main> |     </main> | ||||||
|  |  | ||||||
|     <footer class="my-8"> |     <footer class="text-center text-gray-500 my-8"> | ||||||
|         <p class="text-center text-gray-500 dark:text-gray-400"> |         <p>© 2024 Jason Streifling. Alle Rechte vorbehalten.</p> | ||||||
|             © 2024 Jason Streifling. Alle Rechte vorbehalten. |         <p>v0.11.0 - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p> | ||||||
|         </p> |  | ||||||
|     </footer> |     </footer> | ||||||
|  |  | ||||||
|     <script src="https://unpkg.com/htmx.org@2.0.1"></script> |     <script src="https://unpkg.com/htmx.org@2.0.2"></script> | ||||||
|     <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script> |     <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script> | ||||||
|  |     <script> | ||||||
|  |         document.addEventListener('DOMContentLoaded', () => { | ||||||
|  |             const toggleSwitch = document.getElementById('theme-toggle'); | ||||||
|  |             const dot = document.querySelector('.dot'); | ||||||
|  |  | ||||||
|  |             if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | ||||||
|  |                 document.documentElement.classList.add('dark') | ||||||
|  |                 toggleSwitch.checked = true; | ||||||
|  |                 dot.classList.add('translate-x-6'); | ||||||
|  |             } else { | ||||||
|  |                 document.documentElement.classList.remove('dark') | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             toggleSwitch.addEventListener('change', () => { | ||||||
|  |                 if (toggleSwitch.checked) { | ||||||
|  |                     document.documentElement.classList.add('dark'); | ||||||
|  |                     localStorage.theme = 'dark'; | ||||||
|  |                     dot.classList.add('translate-x-6'); | ||||||
|  |                 } else { | ||||||
|  |                     document.documentElement.classList.remove('dark'); | ||||||
|  |                     localStorage.theme = 'light'; | ||||||
|  |                     dot.classList.remove('translate-x-6'); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     </script> | ||||||
| </body> | </body> | ||||||
|  |  | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -2,11 +2,13 @@ | |||||||
| <h2>Anmeldung</h2> | <h2>Anmeldung</h2> | ||||||
|  |  | ||||||
| <form> | <form> | ||||||
|     <div class="btn-area"> |     <div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-1"> | ||||||
|         <input class="w-full" name="username" placeholder="Benutzername" type="text" /> |         <input class="w-full" name="username" placeholder="Benutzername" type="text" /> | ||||||
|         <input class="w-full" name="password" placeholder="Passwort" type="password" /> |         <input class="w-full" name="password" placeholder="Passwort" type="password" /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" /> |     <div class="mt-2"> | ||||||
|  |         <input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" /> | ||||||
|  |     </div> | ||||||
| </form> | </form> | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								web/templates/published-articles.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								web/templates/published-articles.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | {{define "page-content"}} | ||||||
|  | <h2>Artikel löschen</h2> | ||||||
|  |  | ||||||
|  | <div class="flex flex-col gap-4"> | ||||||
|  |     {{range .Articles}} | ||||||
|  |     <button class="btn" hx-get="/article/{{$.Action}}/{{.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}} | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| {{define "page-content"}} | {{define "page-content"}} | ||||||
| <h2>Abgelehnte Artikel</h2> | <h2>Artikel bearbeiten</h2> | ||||||
|  |  | ||||||
| <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> | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								web/templates/review-article.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								web/templates/review-article.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | {{define "page-content"}} | ||||||
|  | <h2>{{.ActionTitle}}</h2> | ||||||
|  |  | ||||||
|  | <div> | ||||||
|  |     <span>Titel</span> | ||||||
|  |     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||||
|  |         {{.Article.Title}} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <span>Beschreibung</span> | ||||||
|  |     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||||
|  |         {{.Article.Description}} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <span>Artikel</span> | ||||||
|  |     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||||
|  |         <div class="prose text-slate-900 dark:text-slate-100"> | ||||||
|  |             {{.HTMLContent}} | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <span>Tags</span> | ||||||
|  |     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||||
|  |         {{if .Article.IsInIssue}} | ||||||
|  |         <span>Orient Express</span> | ||||||
|  |         <br> | ||||||
|  |         {{end}} | ||||||
|  |         {{range .Tags}} | ||||||
|  |         <span>{{.Name}}</span> | ||||||
|  |         <br> | ||||||
|  |         {{end}} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     {{if eq .Action "publish"}} | ||||||
|  |     <div class="btn-area-3"> | ||||||
|  |         <input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}" | ||||||
|  |             hx-target="#page-content" /> | ||||||
|  |         <input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}" | ||||||
|  |             hx-target="#page-content" /> | ||||||
|  |         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> | ||||||
|  |     </div> | ||||||
|  |     {{else}} | ||||||
|  |     <div class="btn-area"> | ||||||
|  |         <input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}" | ||||||
|  |             hx-target="#page-content" /> | ||||||
|  |         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> | ||||||
|  |     </div> | ||||||
|  |     {{end}} | ||||||
|  | </div> | ||||||
|  | {{end}} | ||||||
| @@ -1,69 +0,0 @@ | |||||||
| {{define "page-content"}} |  | ||||||
| <h2>Editor</h2> |  | ||||||
|  |  | ||||||
| <form> |  | ||||||
|     <div class="flex flex-col gap-y-1"> |  | ||||||
|         <label for="article-title">Titel</label> |  | ||||||
|         <input name="article-title" type="text" value="{{.Article.Title}}" /> |  | ||||||
|     </div> |  | ||||||
|     <div class="flex flex-col"> |  | ||||||
|         <label for="article-description">Beschreibung</label> |  | ||||||
|         <textarea name="article-description">{{.Article.Description}}</textarea> |  | ||||||
|     </div> |  | ||||||
|     <div class="flex flex-col"> |  | ||||||
|         <label for="article-content">Artikel</label> |  | ||||||
|         <textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea> |  | ||||||
|     </div> |  | ||||||
|     <input id="article-content" name="article-content" type="hidden" /> |  | ||||||
|  |  | ||||||
|     <div> |  | ||||||
|         <span>Tags</span> |  | ||||||
|         <div class="flex flex-wrap gap-x-4"> |  | ||||||
|             {{range .Tags}} |  | ||||||
|             <div> |  | ||||||
|                 <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected |  | ||||||
|                     .ID}}checked{{end}} /> |  | ||||||
|                 <label for="tag-{{.Name}}">{{.Name}}</label> |  | ||||||
|             </div> |  | ||||||
|             {{end}} |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div class="btn-area"> |  | ||||||
|         <input class="action-btn" type="submit" value="Senden" hx-post="/resubmit-article/{{.Article.ID}}" |  | ||||||
|             hx-target="#page-content" /> |  | ||||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button> |  | ||||||
|     </div> |  | ||||||
| </form> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
|     var easyMDE = new EasyMDE({ |  | ||||||
|         element: document.getElementById('easyMDE'), |  | ||||||
|         hideIcons: ['image'], |  | ||||||
|         imageTexts: {sbInit: ''}, |  | ||||||
|         showIcons: ["code", "table", "upload-image"], |  | ||||||
|         uploadImage: true, |  | ||||||
|  |  | ||||||
|         imageUploadFunction: function (file, onSuccess, onError) { |  | ||||||
|             var formData = new FormData(); |  | ||||||
|             formData.append('article-image', file); |  | ||||||
|  |  | ||||||
|             fetch('/upload-image', { |  | ||||||
|                 method: 'POST', |  | ||||||
|                 body: formData |  | ||||||
|             }) |  | ||||||
|                 .then(response => response.json()) |  | ||||||
|                 .then(data => { |  | ||||||
|                     onSuccess(data); |  | ||||||
|                 }) |  | ||||||
|                 .catch(error => { |  | ||||||
|                     onError(error); |  | ||||||
|                 }); |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     easyMDE.codemirror.on("change", () => { |  | ||||||
|         document.getElementById('article-content').value = easyMDE.value(); |  | ||||||
|     }); |  | ||||||
| </script> |  | ||||||
| {{end}} |  | ||||||
| @@ -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}} | ||||||
|   | |||||||
| @@ -1,37 +0,0 @@ | |||||||
| {{define "page-content"}} |  | ||||||
| <h2>Artikel veröffentlichen</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="Veröffentlichen" hx-get="/publish-article/{{.ID}}" |  | ||||||
|             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> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| {{end}} |  | ||||||
| @@ -1,9 +1,9 @@ | |||||||
| {{define "page-content"}} | {{define "page-content"}} | ||||||
| <h2>Unveröffentlichte Artikel</h2> | <h2>Artikel veröffentlichen</h2> | ||||||
|  |  | ||||||
| <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