Compare commits
	
		
			32 Commits
		
	
	
		
			v0.13.1
			...
			523bdb24cd
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 523bdb24cd | |||
| 376a1264f5 | |||
| 82faacb9ec | |||
| 7f85b60916 | |||
| ca43ec1a81 | |||
| 0a14545a19 | |||
| 8d41caf40a | |||
| 8fb0733908 | |||
| 81f0e46ba6 | |||
| 19da2ae60c | |||
| 2078be920e | |||
| eb8e14ff6d | |||
| 485eaaca70 | |||
| b2a8578c72 | |||
| dbddff6e55 | |||
| f663592019 | |||
| c59029cf3d | |||
| ac740cf4b8 | |||
| b7d82f15e9 | |||
| b4003c8216 | |||
| 81c046c1b0 | |||
| 1e9fdd2ab9 | |||
| 1fbc0ddcf6 | |||
| 0dd2101a08 | |||
| e95871ee70 | |||
| 083718711d | |||
| 20a12c6299 | |||
| be829e662b | |||
| 0036b42d82 | |||
| 59deb69e2f | |||
| 3aef27585a | |||
| 3aa8796537 | 
| @@ -5,6 +5,7 @@ import ( | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| @@ -13,11 +14,11 @@ type Article struct { | ||||
| 	Title         string | ||||
| 	BannerLink    string | ||||
| 	Summary       string | ||||
| 	ContentLink   string | ||||
| 	ID            int64 | ||||
| 	AuthorID      int64 | ||||
| 	CreatorID     int64 | ||||
| 	IssueID       int64 | ||||
| 	EditedID      int64 | ||||
| 	Clicks        int | ||||
| 	Published     bool | ||||
| 	Rejected      bool | ||||
| 	IsInIssue     bool | ||||
| @@ -30,7 +31,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { | ||||
| 	selectQuery := "SELECT id FROM issues WHERE published = false" | ||||
| 	insertQuery := ` | ||||
|     INSERT INTO articles | ||||
|         (title, banner_link, summary, content_link, published, rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated) | ||||
|         (title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated) | ||||
|     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||||
|     ` | ||||
|  | ||||
| @@ -48,7 +49,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { | ||||
| 				return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.ContentLink, a.Published, a.Rejected, a.AuthorID, id, a.EditedID, a.IsInIssue, a.AutoGenerated) | ||||
| 			result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.CreatorID, id, a.EditedID, 0, a.IsInIssue, a.AutoGenerated) | ||||
| 			if err != nil { | ||||
| 				if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 					log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| @@ -82,7 +83,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { | ||||
|  | ||||
| func (db *DB) GetArticle(id int64) (*Article, error) { | ||||
| 	query := ` | ||||
|     SELECT title, created, banner_link, summary, content_link, published, author_id, issue_id, edited_id, is_in_issue, auto_generated | ||||
|     SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated | ||||
|     FROM articles | ||||
|     WHERE id = ? | ||||
|     ` | ||||
| @@ -92,7 +93,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { | ||||
| 	var created []byte | ||||
| 	var err error | ||||
|  | ||||
| 	if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.Published, &article.AuthorID, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { | ||||
| 	if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.Clicks, &article.IsInIssue, &article.AutoGenerated); err != nil { | ||||
| 		return nil, fmt.Errorf("error scanning article row: %v", err) | ||||
| 	} | ||||
|  | ||||
| @@ -107,7 +108,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { | ||||
|  | ||||
| func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { | ||||
| 	query := fmt.Sprintf(` | ||||
|     SELECT id, title, created, banner_link, summary, content_link, author_id, issue_id, published, rejected, is_in_issue, auto_generated | ||||
|     SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, published, rejected, is_in_issue, auto_generated | ||||
|     FROM articles | ||||
|     WHERE %s = ? | ||||
|     `, attribute) | ||||
| @@ -121,7 +122,7 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro | ||||
| 		article := new(Article) | ||||
| 		var created []byte | ||||
|  | ||||
| 		if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.AuthorID, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { | ||||
| 		if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.CreatorID, &article.IssueID, &article.Clicks, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { | ||||
| 			return nil, fmt.Errorf("error scanning article row: %v", err) | ||||
| 		} | ||||
|  | ||||
| @@ -141,7 +142,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { | ||||
| 	txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} | ||||
| 	issueQuery := "SELECT id FROM issues WHERE published = false" | ||||
| 	articlesQuery := ` | ||||
|     SELECT id, title, created, banner_link, summary, content_link, author_id, auto_generated | ||||
|     SELECT id, title, created, banner_link, summary, clicks, auto_generated | ||||
|     FROM articles | ||||
|     WHERE issue_id = ? AND published = true AND is_in_issue = true | ||||
|     ` | ||||
| @@ -174,7 +175,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { | ||||
| 				article := new(Article) | ||||
| 				var created []byte | ||||
|  | ||||
| 				if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.AuthorID, &article.AutoGenerated); err != nil { | ||||
| 				if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Clicks, &article.AutoGenerated); err != nil { | ||||
| 					if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| 					} | ||||
| @@ -256,11 +257,23 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error { | ||||
|  | ||||
| func (db *DB) DeleteArticle(id int64) error { | ||||
| 	articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?" | ||||
| 	articlesContributorsQuery := "DELETE FROM articles_contributors WHERE article_id = ?" | ||||
| 	articlesAuthorsQuery := "DELETE FROM articles_authors 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) | ||||
| 		return fmt.Errorf("error deleting articles_tags %v from DB: %v", id, err) | ||||
| 	} | ||||
|  | ||||
| 	_, err = db.Exec(articlesContributorsQuery, id) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error deleting articles_contributors %v from DB: %v", id, err) | ||||
| 	} | ||||
|  | ||||
| 	_, err = db.Exec(articlesAuthorsQuery, id) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error deleting articles_authors %v from DB: %v", id, err) | ||||
| 	} | ||||
|  | ||||
| 	_, err = db.Exec(articlesQuery, id) | ||||
| @@ -270,3 +283,13 @@ func (db *DB) DeleteArticle(id int64) error { | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func WriteArticleToFile(c *Config, articleID int64, content []byte) error { | ||||
| 	articleAbsName := fmt.Sprint(c.ArticleDir, "/", articleID, ".md") | ||||
|  | ||||
| 	if err := os.WriteFile(articleAbsName, content, 0644); err != nil { | ||||
| 		return fmt.Errorf("error writing article %v to file: %v", articleID, err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										114
									
								
								cmd/backend/articles_authors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								cmd/backend/articles_authors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| package backend | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| ) | ||||
|  | ||||
| func (db *DB) WriteArticleAuthors(articleID int64, authorIDs []int64) error { | ||||
| 	query := "INSERT INTO articles_authors (article_id, author_id) VALUES (?, ?)" | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| 		err := func() error { | ||||
| 			tx, err := db.Begin() | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("error starting transaction: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			for _, authorID := range authorIDs { | ||||
| 				if _, err := tx.Exec(query, articleID, authorID); err != nil { | ||||
| 					if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| 					} | ||||
| 					return fmt.Errorf("error inserting into articles_authors: %v", err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if err = tx.Commit(); err != nil { | ||||
| 				return fmt.Errorf("error committing transaction: %v", err) | ||||
| 			} | ||||
| 			return nil | ||||
| 		}() | ||||
| 		if err == nil { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		log.Println(err) | ||||
| 		wait(i) | ||||
| 	} | ||||
| 	return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) | ||||
| } | ||||
|  | ||||
| func (db *DB) GetArticleAuthors(c *Config, articleID int64) ([]*User, error) { | ||||
| 	query := ` | ||||
|     SELECT u.id | ||||
|     FROM articles a | ||||
|         INNER JOIN articles_authors aa ON a.id = aa.article_id | ||||
|         INNER JOIN users u ON aa.author_id = u.id | ||||
|     WHERE a.id = ? | ||||
|     ` | ||||
| 	rows, err := db.Query(query, articleID) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error querying articles_authors: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	authors := make([]*User, 0) | ||||
| 	for rows.Next() { | ||||
| 		var authorID int64 | ||||
|  | ||||
| 		if err = rows.Scan(&authorID); err != nil { | ||||
| 			return nil, fmt.Errorf("error scanning rows: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		author, err := db.GetUser(c, authorID) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error getting user info for article author: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		authors = append(authors, author) | ||||
| 	} | ||||
|  | ||||
| 	return authors, nil | ||||
| } | ||||
|  | ||||
| func (db *DB) UpdateArticleAuthors(articleID int64, authorIDs []int64) error { | ||||
| 	deleteQuery := "DELETE FROM articles_authors WHERE article_id = ?" | ||||
| 	insertQuery := "INSERT INTO articles_authors (article_id, author_id) VALUES (?, ?)" | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| 		err := func() error { | ||||
| 			tx, err := db.Begin() | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("error starting transaction: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			if _, err := tx.Exec(deleteQuery, articleID); err != nil { | ||||
| 				if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 					log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| 				} | ||||
| 				return fmt.Errorf("error deleting entries from articles_authors before inserting new ones: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			for _, authorID := range authorIDs { | ||||
| 				if _, err := tx.Exec(insertQuery, articleID, authorID); err != nil { | ||||
| 					if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| 					} | ||||
| 					return fmt.Errorf("error inserting new entries into articles_authors: %v", err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if err = tx.Commit(); err != nil { | ||||
| 				return fmt.Errorf("error committing transaction: %v", err) | ||||
| 			} | ||||
| 			return nil | ||||
| 		}() | ||||
| 		if err == nil { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		log.Println(err) | ||||
| 		wait(i) | ||||
| 	} | ||||
| 	return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) | ||||
| } | ||||
							
								
								
									
										114
									
								
								cmd/backend/articles_contributors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								cmd/backend/articles_contributors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| package backend | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| ) | ||||
|  | ||||
| func (db *DB) WriteArticleContributors(articleID int64, contributorIDs []int64) error { | ||||
| 	query := "INSERT INTO articles_contributors (article_id, contributor_id) VALUES (?, ?)" | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| 		err := func() error { | ||||
| 			tx, err := db.Begin() | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("error starting transaction: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			for _, contributorID := range contributorIDs { | ||||
| 				if _, err := tx.Exec(query, articleID, contributorID); err != nil { | ||||
| 					if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| 					} | ||||
| 					return fmt.Errorf("error inserting into articles_contributors: %v", err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if err = tx.Commit(); err != nil { | ||||
| 				return fmt.Errorf("error committing transaction: %v", err) | ||||
| 			} | ||||
| 			return nil | ||||
| 		}() | ||||
| 		if err == nil { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		log.Println(err) | ||||
| 		wait(i) | ||||
| 	} | ||||
| 	return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) | ||||
| } | ||||
|  | ||||
| func (db *DB) GetArticleContributors(c *Config, articleID int64) ([]*User, error) { | ||||
| 	query := ` | ||||
|     SELECT u.id | ||||
|     FROM articles a | ||||
|         INNER JOIN articles_contributors ac ON a.id = ac.article_id | ||||
|         INNER JOIN users u ON ac.contributor_id = u.id | ||||
|     WHERE a.id = ? | ||||
|     ` | ||||
| 	rows, err := db.Query(query, articleID) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error querying articles_contributors: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	contributors := make([]*User, 0) | ||||
| 	for rows.Next() { | ||||
| 		var contributorID int64 | ||||
|  | ||||
| 		if err = rows.Scan(&contributorID); err != nil { | ||||
| 			return nil, fmt.Errorf("error scanning rows: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		contributor, err := db.GetUser(c, contributorID) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error getting user info for article contributor: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		contributors = append(contributors, contributor) | ||||
| 	} | ||||
|  | ||||
| 	return contributors, nil | ||||
| } | ||||
|  | ||||
| func (db *DB) UpdateArticleContributors(articleID int64, contributorIDs []int64) error { | ||||
| 	deleteQuery := "DELETE FROM articles_contributors WHERE article_id = ?" | ||||
| 	insertQuery := "INSERT INTO articles_contributors (article_id, contributor_id) VALUES (?, ?)" | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| 		err := func() error { | ||||
| 			tx, err := db.Begin() | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("error starting transaction: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			if _, err := tx.Exec(deleteQuery, articleID); err != nil { | ||||
| 				if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 					log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| 				} | ||||
| 				return fmt.Errorf("error deleting entries from articles_contributors before inserting new ones: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			for _, contributorID := range contributorIDs { | ||||
| 				if _, err := tx.Exec(insertQuery, articleID, contributorID); err != nil { | ||||
| 					if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 						log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| 					} | ||||
| 					return fmt.Errorf("error inserting new entries into articles_contributors: %v", err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if err = tx.Commit(); err != nil { | ||||
| 				return fmt.Errorf("error committing transaction: %v", err) | ||||
| 			} | ||||
| 			return nil | ||||
| 		}() | ||||
| 		if err == nil { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		log.Println(err) | ||||
| 		wait(i) | ||||
| 	} | ||||
| 	return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) | ||||
| } | ||||
| @@ -43,8 +43,8 @@ func (db *DB) GetArticleTags(articleID int64) ([]*Tag, error) { | ||||
| 	query := ` | ||||
|     SELECT t.id, t.name | ||||
|     FROM articles a | ||||
|     INNER JOIN articles_tags at ON a.id = at.article_id | ||||
|     INNER JOIN tags t ON at.tag_id = t.id | ||||
|         INNER JOIN articles_tags at ON a.id = at.article_id | ||||
|         INNER JOIN tags t ON at.tag_id = t.id | ||||
|     WHERE a.id = ? | ||||
|     ` | ||||
| 	rows, err := db.Query(query, articleID) | ||||
|   | ||||
| @@ -8,13 +8,13 @@ import ( | ||||
| 	"git.streifling.com/jason/atom" | ||||
| ) | ||||
|  | ||||
| type Feed struct{ *atom.Feed } | ||||
|  | ||||
| func GenerateAtomFeed(c *Config, db *DB) (*string, error) { | ||||
| 	feed := atom.NewFeed(c.Title) | ||||
| 	feed.ID = atom.NewID("urn:feed:1") | ||||
| 	feed.Subtitle = atom.NewText("text", c.Description) | ||||
| 	feed.AddLink(atom.NewLink(c.Link)) | ||||
|  | ||||
| 	linkID := feed.AddLink(atom.NewLink(c.Link)) | ||||
| 	feed.Links[linkID].Rel = "self" | ||||
|  | ||||
| 	feed.Generator = atom.NewGenerator("cpolis") | ||||
| 	feed.Generator.URI = "https://git.streifling.com/jason/cpolis" | ||||
| @@ -32,27 +32,52 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { | ||||
| 		} | ||||
| 		entry := atom.NewEntry(articleTitle) | ||||
| 		entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID)) | ||||
| 		entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", article.ContentLink) | ||||
| 		entry.Published = atom.NewDate(article.Created) | ||||
| 		entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", fmt.Sprint(c.Domain, "/article/serve/", article.ID)) | ||||
|  | ||||
| 		linkID := entry.AddLink(atom.NewLink(article.BannerLink)) | ||||
| 		entry.Links[linkID].Rel = "enclosure" | ||||
| 		entry.Links[linkID].Type = "image/webp" | ||||
|  | ||||
| 		user, err := db.GetUser(c, article.AuthorID) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error getting user user info for Atom feed: %v", err) | ||||
| 		} | ||||
| 		entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName)) | ||||
|  | ||||
| 		articleSummary, err := ConvertToPlain(article.Summary) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error converting description to plain text for Atom feed: %v", err) | ||||
| 		} | ||||
| 		if article.AutoGenerated { | ||||
| 			articleSummary = "auto generated" | ||||
| 			entry.Summary = atom.NewText("text", "automatically generated") | ||||
| 		} else { | ||||
| 			articleSummary, err := ConvertToPlain(article.Summary) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("error converting description to plain text for Atom feed: %v", err) | ||||
| 			} | ||||
| 			entry.Summary = atom.NewText("text", articleSummary) | ||||
| 		} | ||||
|  | ||||
| 		if len(article.BannerLink) > 0 { | ||||
| 			linkID := entry.AddLink(atom.NewLink(c.Domain + "/image/serve/" + article.BannerLink)) | ||||
| 			entry.Links[linkID].Rel = "enclosure" | ||||
| 			entry.Links[linkID].Type = "image/webp" | ||||
| 		} | ||||
|  | ||||
| 		authors, err := db.GetArticleAuthors(c, article.ID) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error getting article's authors for Atom feed: %v", err) | ||||
| 		} | ||||
| 		for _, author := range authors { | ||||
| 			user, err := db.GetUser(c, author.ID) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("error getting user info for Atom feed: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			authorID := entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName)) | ||||
| 			entry.Authors[authorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink | ||||
| 		} | ||||
|  | ||||
| 		contributors, err := db.GetArticleContributors(c, article.ID) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error getting article's contributors for Atom feed: %v", err) | ||||
| 		} | ||||
| 		for _, contributor := range contributors { | ||||
| 			user, err := db.GetUser(c, contributor.ID) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("error getting user info for Atom feed: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			contributorID := entry.AddContributor(atom.NewPerson(user.FirstName + " " + user.LastName)) | ||||
| 			entry.Contributors[contributorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink | ||||
| 		} | ||||
| 		entry.Summary = atom.NewText("text", articleSummary) | ||||
|  | ||||
| 		tags, err := db.GetArticleTags(article.ID) | ||||
| 		if err != nil { | ||||
|   | ||||
| @@ -18,7 +18,7 @@ type Config struct { | ||||
| 	DBName          string | ||||
| 	Description     string | ||||
| 	Domain          string | ||||
| 	AtomFeed        string | ||||
| 	AtomFile        string | ||||
| 	FirebaseKey     string | ||||
| 	GOBKeyFile      string | ||||
| 	Link            string | ||||
| @@ -39,7 +39,7 @@ func newConfig() *Config { | ||||
| 	return &Config{ | ||||
| 		AESKeyFile:      "/var/www/cpolis/aes.key", | ||||
| 		ArticleDir:      "/var/www/cpolis/articles", | ||||
| 		AtomFeed:        "/var/www/cpolis/cpolis.atom", | ||||
| 		AtomFile:        "/var/www/cpolis/cpolis.atom", | ||||
| 		ConfigFile:      "/etc/cpolis/config.toml", | ||||
| 		DBName:          "cpolis", | ||||
| 		FirebaseKey:     "/var/www/cpolis/serviceAccountKey.json", | ||||
| @@ -52,7 +52,7 @@ func newConfig() *Config { | ||||
| 		PDFDir:          "/var/www/cpolis/pdfs", | ||||
| 		PicsDir:         "/var/www/cpolis/pics", | ||||
| 		Port:            ":8080", | ||||
| 		Version:         "v0.13.1", | ||||
| 		Version:         "v0.14.0", | ||||
| 		WebDir:          "/var/www/cpolis/web", | ||||
| 	} | ||||
| } | ||||
| @@ -110,7 +110,7 @@ func (c *Config) handleCliArgs() error { | ||||
|  | ||||
| 	flag.StringVar(&c.AESKeyFile, "aes", c.AESKeyFile, "aes key file") | ||||
| 	flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory") | ||||
| 	flag.StringVar(&c.AtomFeed, "feed", c.AtomFeed, "atom feed file") | ||||
| 	flag.StringVar(&c.AtomFile, "feed", c.AtomFile, "atom feed file") | ||||
| 	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") | ||||
| @@ -187,10 +187,10 @@ func (c *Config) setupConfig(cliConfig *Config) error { | ||||
| 		c.Domain = "https://" + c.Domain | ||||
| 	} | ||||
|  | ||||
| 	if cliConfig.AtomFeed != defaultConfig.AtomFeed { | ||||
| 		c.AtomFeed = cliConfig.AtomFeed | ||||
| 	if cliConfig.AtomFile != defaultConfig.AtomFile { | ||||
| 		c.AtomFile = cliConfig.AtomFile | ||||
| 	} | ||||
| 	c.AtomFeed, err = mkFile(c.AtomFeed, 0644, 0744) | ||||
| 	c.AtomFile, err = mkFile(c.AtomFile, 0644, 0744) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error setting up file: %v", err) | ||||
| 	} | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package backend | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"io" | ||||
| @@ -43,20 +42,3 @@ func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, err | ||||
|  | ||||
| 	return filename, nil | ||||
| } | ||||
|  | ||||
| func ServeBase64Image(c *Config, filename string) (string, error) { | ||||
| 	file := c.PicsDir + "/" + filename | ||||
|  | ||||
| 	img, err := os.Open(file) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error opening file %v: %v", file, err) | ||||
| 	} | ||||
| 	defer img.Close() | ||||
|  | ||||
| 	imgBytes, err := io.ReadAll(img) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error turning %v into bytes: %v", file, err) | ||||
| 	} | ||||
|  | ||||
| 	return base64.StdEncoding.EncodeToString(imgBytes), nil | ||||
| } | ||||
|   | ||||
| @@ -149,11 +149,11 @@ func (db *DB) AddUser(c *Config, u *User, pass string) (int64, error) { | ||||
| 	} | ||||
|  | ||||
| 	query := ` | ||||
|     INSERT INTO users (username, password, first_name, last_name, email, role) | ||||
|     VALUES (?, ?, ?, ?, ?, ?) | ||||
|     INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role) | ||||
|     VALUES (?, ?, ?, ?, ?, ?, ?) | ||||
|     ` | ||||
|  | ||||
| 	result, err := db.Exec(query, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.Role) | ||||
| 	result, err := db.Exec(query, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.ProfilePicLink, u.Role) | ||||
| 	if err != nil { | ||||
| 		return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err) | ||||
| 	} | ||||
| @@ -253,13 +253,13 @@ func (db *DB) GetUser(c *Config, id int64) (*User, error) { | ||||
|  | ||||
| 	user := new(User) | ||||
| 	query := ` | ||||
|     SELECT id, username, first_name, last_name, email, role | ||||
|     SELECT id, username, first_name, last_name, email, profile_pic_link, role | ||||
|     FROM users | ||||
|     WHERE id = ? | ||||
|     ` | ||||
|  | ||||
| 	row := db.QueryRow(query, id) | ||||
| 	if err := row.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.Role); err != nil { | ||||
| 	if err := row.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { | ||||
| 		return nil, fmt.Errorf("error reading user information: %v", err) | ||||
| 	} | ||||
|  | ||||
| @@ -281,10 +281,10 @@ func (db *DB) GetUser(c *Config, id int64) (*User, error) { | ||||
| 	return user, nil | ||||
| } | ||||
|  | ||||
| func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, lastName, email, oldPass, newPass string) error { | ||||
| func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, lastName, email, profilePicLink, oldPass, newPass string) error { | ||||
| 	var err error | ||||
| 	tx := new(Tx) | ||||
| 	passwordEmpty := len(newPass) > 0 | ||||
| 	passwordEmpty := len(newPass) == 0 | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| 		err := func() error { | ||||
| @@ -331,6 +331,7 @@ func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "email", Value: aesEmail}, | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink}, | ||||
| 			); err != nil { | ||||
| 				if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 					log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| @@ -360,8 +361,8 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { | ||||
| 	txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} | ||||
| 	selectQuery := "SELECT COUNT(*) FROM users" | ||||
| 	insertQuery := ` | ||||
|     INSERT INTO users (username, password, first_name, last_name, email, role) | ||||
|     VALUES (?, ?, ?, ?, ?, ?) | ||||
|     INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role) | ||||
|     VALUES (?, ?, ?, ?, ?, ?, ?) | ||||
|     ` | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| @@ -416,7 +417,7 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { | ||||
| 				return 0, fmt.Errorf("error encrypting email: %v", err) | ||||
| 			} | ||||
|  | ||||
| 			result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.Role) | ||||
| 			result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.ProfilePicLink, u.Role) | ||||
| 			if err != nil { | ||||
| 				if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
| 					log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) | ||||
| @@ -447,11 +448,50 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { | ||||
| 	return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) | ||||
| } | ||||
|  | ||||
| func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) { | ||||
| func (db *DB) GetAllUsers(c *Config) ([]*User, error) { | ||||
| 	var aesFirstName, aesLastName, aesEmail string | ||||
| 	var err error | ||||
|  | ||||
| 	query := "SELECT id, username, first_name, last_name, email, role FROM users" | ||||
| 	query := "SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users" | ||||
|  | ||||
| 	rows, err := db.Query(query) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error getting all users from DB: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	users := make([]*User, 0) | ||||
| 	for rows.Next() { | ||||
| 		user := new(User) | ||||
| 		if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { | ||||
| 			return nil, fmt.Errorf("error getting user info: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		user.FirstName, err = aesDecrypt(c, aesFirstName) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error decrypting first name: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		user.LastName, err = aesDecrypt(c, aesLastName) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error decrypting last name: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		user.Email, err = aesDecrypt(c, aesEmail) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error decrypting email: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		users = append(users, user) | ||||
| 	} | ||||
|  | ||||
| 	return users, nil | ||||
| } | ||||
|  | ||||
| func (db *DB) GetAllUsersMap(c *Config) (map[int64]*User, error) { | ||||
| 	var aesFirstName, aesLastName, aesEmail string | ||||
| 	var err error | ||||
|  | ||||
| 	query := "SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users" | ||||
|  | ||||
| 	rows, err := db.Query(query) | ||||
| 	if err != nil { | ||||
| @@ -461,7 +501,7 @@ func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) { | ||||
| 	users := make(map[int64]*User, 0) | ||||
| 	for rows.Next() { | ||||
| 		user := new(User) | ||||
| 		if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.Role); err != nil { | ||||
| 		if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { | ||||
| 			return nil, fmt.Errorf("error getting user info: %v", err) | ||||
| 		} | ||||
|  | ||||
| @@ -506,10 +546,10 @@ func (tx *Tx) SetPassword(id int64, newPass string) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, lastName, email, newPass string, role int) error { | ||||
| func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, lastName, email, profilePicLink, newPass string, role int) error { | ||||
| 	var err error | ||||
| 	tx := new(Tx) | ||||
| 	passwordEmpty := len(newPass) > 0 | ||||
| 	passwordEmpty := len(newPass) == 0 | ||||
|  | ||||
| 	for i := 0; i < TxMaxRetries; i++ { | ||||
| 		err := func() error { | ||||
| @@ -556,6 +596,7 @@ func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, las | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "email", Value: aesEmail}, | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink}, | ||||
| 				&Attribute{Table: "users", ID: id, AttName: "role", Value: role}, | ||||
| 			); err != nil { | ||||
| 				if rollbackErr := tx.Rollback(); rollbackErr != nil { | ||||
|   | ||||
| @@ -50,6 +50,17 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		fmt.Fprint(w, content) | ||||
| 		article.Clicks++ | ||||
| 		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "clicks", Value: article.Clicks}); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if _, err = fmt.Fprint(w, content); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ func ServeAtomFeed(c *b.Config) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		absFilepath, err := filepath.Abs(c.AtomFeed) | ||||
| 		absFilepath, err := filepath.Abs(c.AtomFile) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
|   | ||||
| @@ -6,12 +6,15 @@ import ( | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	b "streifling.com/jason/cpolis/cmd/backend" | ||||
| 	f "streifling.com/jason/cpolis/cmd/frontend" | ||||
| ) | ||||
|  | ||||
| func ServeImage(c *b.Config) http.HandlerFunc { | ||||
| func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if !tokenIsVerified(w, r, c) { | ||||
| 			return | ||||
| 		if _, err := f.GetSession(w, r, c, s); err != nil { | ||||
| 			if !tokenIsVerified(w, r, c) { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		absFilepath, err := filepath.Abs(c.PicsDir) | ||||
|   | ||||
| @@ -18,21 +18,36 @@ const ( | ||||
| 	PreviewMode | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	None = iota | ||||
| 	Author | ||||
| 	Contributor | ||||
| ) | ||||
|  | ||||
| type ArticleUser struct { | ||||
| 	*b.User | ||||
| 	ArticleRole int | ||||
| } | ||||
|  | ||||
| type EditorHTMLData struct { | ||||
| 	Selected     map[int64]bool | ||||
| 	Content      string | ||||
| 	Action       string | ||||
| 	ActionTitle  string | ||||
| 	ActionButton string | ||||
| 	BannerImage  string | ||||
| 	Image        string | ||||
| 	HTMLContent  template.HTML | ||||
| 	Article      *b.Article | ||||
| 	Tags         []*b.Tag | ||||
| 	ArticleUsers map[string]*ArticleUser // A map is way more efficient in ReviewRejectedArticle() | ||||
| 	Creator      *ArticleUser | ||||
| 	Authors      []*b.User | ||||
| 	Contributors []*b.User | ||||
| } | ||||
|  | ||||
| func WriteArticle(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -41,11 +56,30 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| 		var data *EditorHTMLData | ||||
| 		if session.Values["article"] == nil { | ||||
| 			data = &EditorHTMLData{Action: "submit", Article: new(b.Article)} | ||||
| 			data = &EditorHTMLData{Action: "submit", Article: new(b.Article), ArticleUsers: make(map[string]*ArticleUser)} | ||||
| 		} else { | ||||
| 			data = session.Values["article"].(*EditorHTMLData) | ||||
| 		} | ||||
|  | ||||
| 		users, err := db.GetAllUsers(c) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		for _, user := range users { | ||||
| 			data.ArticleUsers[fmt.Sprint(user.LastName, user.FirstName, user.ID)] = &ArticleUser{User: user, ArticleRole: None} | ||||
| 		} | ||||
|  | ||||
| 		creator, err := db.GetUser(c, session.Values["id"].(int64)) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.Creator = data.ArticleUsers[fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)] | ||||
| 		delete(data.ArticleUsers, fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)) | ||||
|  | ||||
| 		data.Tags, err = db.GetTagList() | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -64,7 +98,7 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func SubmitArticle(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -82,26 +116,55 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			Title:         r.PostFormValue("article-title"), | ||||
| 			BannerLink:    r.PostFormValue("article-banner-url"), | ||||
| 			Summary:       r.PostFormValue("article-summary"), | ||||
| 			CreatorID:     session.Values["id"].(int64), | ||||
| 			Published:     false, | ||||
| 			Rejected:      false, | ||||
| 			AuthorID:      session.Values["id"].(int64), | ||||
| 			IsInIssue:     r.PostFormValue("issue") == "on", | ||||
| 			AutoGenerated: false, | ||||
| 			EditedID:      0, | ||||
| 		} | ||||
|  | ||||
| 		if len(article.Title) == 0 { | ||||
| 			http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
| 		if len(article.BannerLink) == 0 { | ||||
| 			http.Error(w, "Bitte ein Titelbild einfügen.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
| 		if len(article.Summary) == 0 { | ||||
| 			http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		r.ParseForm() | ||||
| 		authors := make([]int64, 0) | ||||
| 		contributors := make([]int64, 0) | ||||
|  | ||||
| 		for key, values := range r.Form { | ||||
| 			if strings.HasPrefix(key, "user-") && len(values) > 0 { | ||||
| 				id, err := strconv.ParseInt(strings.Split(key, "-")[1], 10, 64) | ||||
| 				if err != nil { | ||||
| 					log.Println(err) | ||||
| 					http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 				switch values[0] { | ||||
| 				case "author": | ||||
| 					authors = append(authors, id) | ||||
| 				case "contributor": | ||||
| 					contributors = append(contributors, id) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if r.PostFormValue("creator") == "contributor" { | ||||
| 			contributors = append(contributors, article.CreatorID) | ||||
| 		} else { | ||||
| 			authors = append(authors, article.CreatorID) | ||||
| 		} | ||||
| 		if len(authors) == 0 { | ||||
| 			http.Error(w, "Es muss mindestens einen Autor geben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		article.ID, err = db.AddArticle(article) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -114,30 +177,34 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") | ||||
| 		if err = os.WriteFile(articleAbsName, content, 0644); err != nil { | ||||
| 		if err := b.WriteArticleToFile(c, article.ID, content); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID) | ||||
| 		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); err != nil { | ||||
| 		if err = db.WriteArticleAuthors(article.ID, authors); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		if len(contributors) > 0 { | ||||
| 			if err = db.WriteArticleContributors(article.ID, contributors); 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) | ||||
| 				http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			tags = append(tags, tagID) | ||||
| 		} | ||||
| 		if err = db.WriteArticleTags(article.ID, tags); err != nil { | ||||
| @@ -161,39 +228,75 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func ResubmitArticle(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			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 := &b.Article{ | ||||
| 			Title:      r.PostFormValue("article-title"), | ||||
| 			BannerLink: r.PostFormValue("article-banner-url"), | ||||
| 			Summary:    r.PostFormValue("article-summary"), | ||||
| 			CreatorID:  session.Values["id"].(int64), | ||||
| 			IsInIssue:  r.PostFormValue("issue") == "on", | ||||
| 		} | ||||
|  | ||||
| 		title := r.PostFormValue("article-title") | ||||
| 		if len(title) == 0 { | ||||
| 		if len(article.Title) == 0 { | ||||
| 			http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		summary := r.PostFormValue("article-summary") | ||||
| 		if len(summary) == 0 { | ||||
| 		if len(article.Summary) == 0 { | ||||
| 			http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		r.ParseForm() | ||||
| 		authors := make([]int64, 0) | ||||
| 		contributors := make([]int64, 0) | ||||
|  | ||||
| 		for key, values := range r.Form { | ||||
| 			if strings.HasPrefix(key, "user-") && len(values) > 0 { | ||||
| 				id, err := strconv.ParseInt(strings.Split(key, "-")[1], 10, 64) | ||||
| 				if err != nil { | ||||
| 					log.Println(err) | ||||
| 					http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 				switch values[0] { | ||||
| 				case "author": | ||||
| 					authors = append(authors, id) | ||||
| 				case "contributor": | ||||
| 					contributors = append(contributors, id) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if r.PostFormValue("creator") == "contributor" { | ||||
| 			contributors = append(contributors, article.CreatorID) | ||||
| 		} else { | ||||
| 			authors = append(authors, article.CreatorID) | ||||
| 		} | ||||
| 		if len(authors) == 0 { | ||||
| 			http.Error(w, "Es muss mindestens einen Autor geben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		article.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		content := r.PostFormValue("article-content") | ||||
| 		if len(content) == 0 { | ||||
| 			http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		contentLink := fmt.Sprint(c.ArticleDir, "/", id, ".md") | ||||
| 		contentLink := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") | ||||
| 		if err = os.WriteFile(contentLink, []byte(content), 0644); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -201,17 +304,30 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 		} | ||||
|  | ||||
| 		if err = db.UpdateAttributes( | ||||
| 			&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, | ||||
| 			&b.Attribute{Table: "articles", ID: id, AttName: "summary", Value: summary}, | ||||
| 			&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"}, | ||||
| 			&b.Attribute{Table: "articles", ID: article.ID, AttName: "title", Value: article.Title}, | ||||
| 			&b.Attribute{Table: "articles", ID: article.ID, AttName: "banner_link", Value: article.BannerLink}, | ||||
| 			&b.Attribute{Table: "articles", ID: article.ID, AttName: "summary", Value: article.Summary}, | ||||
| 			&b.Attribute{Table: "articles", ID: article.ID, AttName: "rejected", Value: false}, | ||||
| 			&b.Attribute{Table: "articles", ID: article.ID, AttName: "is_in_issue", Value: article.IsInIssue}, | ||||
| 		); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		r.ParseForm() | ||||
| 		if err = db.UpdateArticleAuthors(article.ID, authors); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		if len(contributors) > 0 { | ||||
| 			if err = db.UpdateArticleContributors(article.ID, contributors); err != nil { | ||||
| 				log.Println(err) | ||||
| 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		tags := make([]int64, 0) | ||||
| 		for _, tag := range r.Form["tags"] { | ||||
| 			tagID, err := strconv.ParseInt(tag, 10, 64) | ||||
| @@ -222,7 +338,7 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			} | ||||
| 			tags = append(tags, tagID) | ||||
| 		} | ||||
| 		if err = db.UpdateArticleTags(id, tags); err != nil { | ||||
| 		if err = db.UpdateArticleTags(article.ID, tags); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -243,7 +359,7 @@ func ResubmitArticle(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) { | ||||
| 		if _, err := getSession(w, r, c, s); err != nil { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -287,7 +403,7 @@ func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB | ||||
|  | ||||
| func ShowRejectedArticles(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -308,7 +424,7 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF | ||||
|  | ||||
| 		data.MyIDs = make(map[int64]bool) | ||||
| 		for _, article := range data.RejectedArticles { | ||||
| 			if article.AuthorID == session.Values["id"].(int64) { | ||||
| 			if article.CreatorID == session.Values["id"].(int64) { | ||||
| 				data.MyIDs[article.ID] = true | ||||
| 			} | ||||
| 		} | ||||
| @@ -325,7 +441,8 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF | ||||
|  | ||||
| func ReviewRejectedArticle(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 { | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -346,14 +463,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		imgURL := strings.Split(data.Article.BannerLink, "/") | ||||
| 		imgFileName := imgURL[len(imgURL)-1] | ||||
| 		data.BannerImage, err = b.ServeBase64Image(c, imgFileName) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.Image = data.Article.BannerLink | ||||
|  | ||||
| 		articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") | ||||
| 		content, err := os.ReadFile(articleAbsName) | ||||
| @@ -371,6 +481,46 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data.ArticleUsers = make(map[string]*ArticleUser) | ||||
| 		users, err := db.GetAllUsers(c) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		for _, user := range users { | ||||
| 			data.ArticleUsers[fmt.Sprint(user.LastName, user.FirstName, user.ID)] = &ArticleUser{User: user, ArticleRole: None} | ||||
| 		} | ||||
|  | ||||
| 		authors, err := db.GetArticleAuthors(c, data.Article.ID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		for _, author := range authors { | ||||
| 			data.ArticleUsers[fmt.Sprint(author.LastName, author.FirstName, author.ID)].ArticleRole = Author | ||||
| 		} | ||||
|  | ||||
| 		contributors, err := db.GetArticleContributors(c, data.Article.ID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		for _, contributor := range contributors { | ||||
| 			data.ArticleUsers[fmt.Sprint(contributor.LastName, contributor.FirstName, contributor.ID)].ArticleRole = Contributor | ||||
| 		} | ||||
|  | ||||
| 		creator, err := db.GetUser(c, session.Values["id"].(int64)) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.Creator = data.ArticleUsers[fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)] | ||||
| 		delete(data.ArticleUsers, fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)) | ||||
|  | ||||
| 		selectedTags, err := db.GetArticleTags(id) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -396,7 +546,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler | ||||
|  | ||||
| func PublishArticle(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -424,9 +574,9 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 		} | ||||
|  | ||||
| 		if err = db.UpdateAttributes( | ||||
| 			&b.Attribute{Table: "articles", ID: id, AttName: "published", Value: true}, | ||||
| 			&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, | ||||
| 			&b.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")}, | ||||
| 			&b.Attribute{Table: "articles", ID: article.ID, AttName: "published", Value: true}, | ||||
| 			&b.Attribute{Table: "articles", ID: article.ID, AttName: "rejected", Value: false}, | ||||
| 			&b.Attribute{Table: "articles", ID: article.ID, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")}, | ||||
| 		); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -453,10 +603,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if err = db.UpdateAttributes( | ||||
| 				&b.Attribute{Table: "articles", ID: id, AttName: "content_link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)}, | ||||
| 				&b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0}, | ||||
| 			); err != nil { | ||||
| 			if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "edited_id", Value: 0}); err != nil { | ||||
| 				log.Println(err) | ||||
| 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 				return | ||||
| @@ -469,7 +616,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil { | ||||
| 		if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -490,7 +637,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func RejectArticle(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -525,7 +672,7 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func ShowCurrentIssue(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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -549,7 +696,7 @@ func ShowCurrentIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc | ||||
|  | ||||
| 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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -586,7 +733,7 @@ func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action strin | ||||
|  | ||||
| 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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -623,14 +770,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		imgURL := strings.Split(article.BannerLink, "/") | ||||
| 		imgFileName := imgURL[len(imgURL)-1] | ||||
| 		data.BannerImage, err = b.ServeBase64Image(c, imgFileName) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.Image = article.BannerLink | ||||
|  | ||||
| 		data.Article.Summary, err = b.ConvertToPlain(article.Summary) | ||||
| 		if err != nil { | ||||
| @@ -654,6 +794,22 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto | ||||
| 		} | ||||
| 		data.HTMLContent = template.HTML(data.Content) | ||||
|  | ||||
| 		data.Authors, err = db.GetArticleAuthors(c, data.Article.ID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		sortUsersByName(data.Authors) | ||||
|  | ||||
| 		data.Contributors, err = db.GetArticleContributors(c, data.Article.ID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		sortUsersByName(data.Contributors) | ||||
|  | ||||
| 		data.Tags, err = db.GetArticleTags(id) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -672,7 +828,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto | ||||
|  | ||||
| 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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -704,7 +860,7 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil { | ||||
| 		if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -725,7 +881,7 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| 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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -746,25 +902,59 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		newArticle := oldArticle | ||||
| 		newArticle := *oldArticle | ||||
| 		newArticle.Published = false | ||||
| 		newArticle.Rejected = true | ||||
| 		newArticle.EditedID = oldArticle.ID | ||||
|  | ||||
| 		newID, err := db.AddArticle(newArticle) | ||||
| 		newArticle.ID, 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 { | ||||
| 		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldArticle.ID, AttName: "edited_id", Value: newArticle.ID}); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err = b.CopyFile(fmt.Sprint(c.ArticleDir, "/", oldID, ".md"), fmt.Sprint(c.ArticleDir, "/", newID, ".md")); err != nil { | ||||
| 		src := fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md") | ||||
| 		dst := fmt.Sprint(c.ArticleDir, "/", newArticle.ID, ".md") | ||||
| 		if err = b.CopyFile(src, dst); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		authors, err := db.GetArticleAuthors(c, oldArticle.ID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		authorIDs := make([]int64, len(authors)) | ||||
| 		for i, author := range authors { | ||||
| 			authorIDs[i] = author.ID | ||||
| 		} | ||||
| 		if err = db.WriteArticleAuthors(newArticle.ID, authorIDs); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		contributors, err := db.GetArticleContributors(c, oldArticle.ID) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		contributorIDs := make([]int64, len(contributors)) | ||||
| 		for i, contributor := range contributors { | ||||
| 			contributorIDs[i] = contributor.ID | ||||
| 		} | ||||
| 		if err = db.WriteArticleContributors(newArticle.ID, contributorIDs); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -784,7 +974,7 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc | ||||
|  | ||||
| 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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -806,14 +996,7 @@ func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		imgURL := strings.Split(data.Article.BannerLink, "/") | ||||
| 		imgFileName := imgURL[len(imgURL)-1] | ||||
| 		data.BannerImage, err = b.ServeBase64Image(c, imgFileName) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		data.Image = data.Article.BannerLink | ||||
|  | ||||
| 		content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")) | ||||
| 		if err != nil { | ||||
|   | ||||
| @@ -9,20 +9,14 @@ import ( | ||||
| 	b "streifling.com/jason/cpolis/cmd/backend" | ||||
| ) | ||||
|  | ||||
| func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { | ||||
| func UploadEasyMDEImage(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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err := r.ParseMultipartForm(10 << 20); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		file, _, err := r.FormFile("article-image") | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -48,9 +42,9 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate string) http.HandlerFunc { | ||||
| func UploadImage(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate string) http.HandlerFunc { | ||||
| 	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 { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -75,20 +69,8 @@ func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		base64Img, err := b.ServeBase64Image(c, filename) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data := new(struct { | ||||
| 			BannerImage string | ||||
| 			URL         string | ||||
| 		}) | ||||
|  | ||||
| 		data.BannerImage = base64Img | ||||
| 		data.URL = c.Domain + "/image/serve/" + filename | ||||
| 		data := new(struct{ Image string }) | ||||
| 		data.Image = filename | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/" + htmlFile) | ||||
| 		if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil { | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import ( | ||||
|  | ||||
| func PublishLatestIssue(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -33,7 +33,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun | ||||
| 			Published:     true, | ||||
| 			Rejected:      false, | ||||
| 			Created:       time.Now(), | ||||
| 			AuthorID:      session.Values["id"].(int64), | ||||
| 			AutoGenerated: true, | ||||
| 		} | ||||
|  | ||||
| @@ -41,10 +40,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun | ||||
| 			http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
| 		if len(article.BannerLink) == 0 { | ||||
| 			http.Error(w, "Bitte ein Titelbild einfügen.", http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		article.ID, err = db.AddArticle(article) | ||||
| 		if err != nil { | ||||
| @@ -53,6 +48,22 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		authorIDs := make([]int64, 1) | ||||
| 		var ok bool | ||||
|  | ||||
| 		if authorIDs[0], ok = session.Values["id"].(int64); !ok { | ||||
| 			msg := "fälschlicherweise session.Values[\"id\"].(int64) für authorIDs[0] angenommen" | ||||
| 			log.Println(msg) | ||||
| 			http.Error(w, msg, http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err = db.WriteArticleAuthors(article.ID, authorIDs); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		content := []byte(r.PostFormValue("issue-content")) | ||||
| 		if len(content) == 0 { | ||||
| 			http.Error(w, "Bitte eine Beschreibung eingeben.", http.StatusBadRequest) | ||||
| @@ -66,19 +77,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID) | ||||
| 		if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); 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: "content_link", Value: article.ContentLink}); 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) | ||||
| @@ -97,7 +95,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 		if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil { | ||||
| 		if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
|   | ||||
| @@ -12,18 +12,12 @@ import ( | ||||
|  | ||||
| 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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			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) | ||||
|   | ||||
| @@ -26,8 +26,8 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *b.CookieStore, u *b. | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // getSession is used for verifying that the user is logged in and returns their session and an error. | ||||
| func getSession(w http.ResponseWriter, r *http.Request, c *b.Config, s *b.CookieStore) (*b.Session, error) { | ||||
| // GetSession is used for verifying that the user is logged in and returns their session and an error. | ||||
| func GetSession(w http.ResponseWriter, r *http.Request, c *b.Config, s *b.CookieStore) (*b.Session, error) { | ||||
| 	msg := "Keine gültige Session. Bitte erneut anmelden." | ||||
| 	tmpl, tmplErr := template.ParseFiles(c.WebDir+"/templates/index.html", c.WebDir+"/templates/login.html") | ||||
|  | ||||
| @@ -57,15 +57,21 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 		} | ||||
|  | ||||
| 		data := new(struct { | ||||
| 			*UserHTMLData | ||||
| 			Version string | ||||
| 			Role    int | ||||
| 		}) | ||||
| 		data.UserHTMLData = &UserHTMLData{User: new(b.User)} | ||||
| 		data.Version = c.Version | ||||
|  | ||||
| 		files := make([]string, 2) | ||||
| 		files[0] = c.WebDir + "/templates/index.html" | ||||
| 		if numRows == 0 { | ||||
| 			files[1] = c.WebDir + "/templates/first-user.html" | ||||
| 			data.Role = b.NonExistent | ||||
| 			data.Title = "Erster Benutzer (Administrator)" | ||||
| 			data.ButtonText = "Anlegen" | ||||
| 			data.URL = "/user/add-first" | ||||
|  | ||||
| 			files[1] = c.WebDir + "/templates/edit-user.html" | ||||
| 			tmpl, err := template.ParseFiles(files...) | ||||
| 			if err = template.Must(tmpl, err).Execute(w, data); err != nil { | ||||
| 				log.Println(err) | ||||
| @@ -79,6 +85,7 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 				http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if auth, ok := session.Values["authenticated"].(bool); auth && ok { | ||||
| 				data.Role = session.Values["role"].(int) | ||||
| 				files[1] = c.WebDir + "/templates/hub.html" | ||||
| @@ -89,6 +96,7 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 					return | ||||
| 				} | ||||
| 			} else { | ||||
| 				data.Role = b.Author | ||||
| 				files[1] = c.WebDir + "/templates/login.html" | ||||
| 				tmpl, err := template.ParseFiles(files...) | ||||
| 				if err = template.Must(tmpl, err).Execute(w, data); err != nil { | ||||
| @@ -142,7 +150,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		session, err := getSession(w, r, c, s) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -167,7 +175,7 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		session, err := getSession(w, r, c, s) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import ( | ||||
|  | ||||
| func CreateTag(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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -27,7 +27,7 @@ func CreateTag(c *b.Config, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func AddTag(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
|   | ||||
| @@ -5,11 +5,20 @@ import ( | ||||
| 	"html/template" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
|  | ||||
| 	b "streifling.com/jason/cpolis/cmd/backend" | ||||
| ) | ||||
|  | ||||
| type UserHTMLData struct { | ||||
| 	*b.User | ||||
| 	Title      string | ||||
| 	ButtonText string | ||||
| 	URL        string | ||||
| 	Image      string | ||||
| } | ||||
|  | ||||
| func checkUserStrings(user *b.User) (string, int, bool) { | ||||
| 	userLen := 63 // max value for utf-8 at 255 bytes | ||||
| 	nameLen := 56 // max value when aes encrypting utf-8 at up to 255 bytes | ||||
| @@ -25,16 +34,32 @@ func checkUserStrings(user *b.User) (string, int, bool) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func sortUsersByName(users []*b.User) { | ||||
| 	sort.SliceStable(users, func(i, j int) bool { | ||||
| 		if users[i].LastName == users[j].LastName { | ||||
| 			return users[i].FirstName < users[j].FirstName | ||||
| 		} | ||||
| 		return users[i].LastName < users[j].LastName | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func CreateUser(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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html") | ||||
| 		if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil { | ||||
| 		data := &UserHTMLData{ | ||||
| 			User:       &b.User{Role: b.Author}, | ||||
| 			Title:      "Neuer Benutzer", | ||||
| 			ButtonText: "Anlegen", | ||||
| 			URL:        "/user/add", | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") | ||||
| 		if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -44,7 +69,7 @@ func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func AddUser(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -52,10 +77,11 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 		} | ||||
|  | ||||
| 		user := &b.User{ | ||||
| 			UserName:  r.PostFormValue("username"), | ||||
| 			FirstName: r.PostFormValue("first-name"), | ||||
| 			LastName:  r.PostFormValue("last-name"), | ||||
| 			Email:     r.PostFormValue("email"), | ||||
| 			UserName:       r.PostFormValue("username"), | ||||
| 			FirstName:      r.PostFormValue("first-name"), | ||||
| 			LastName:       r.PostFormValue("last-name"), | ||||
| 			Email:          r.PostFormValue("email"), | ||||
| 			ProfilePicLink: r.PostFormValue("profile-pic-url"), | ||||
| 		} | ||||
| 		pass := r.PostFormValue("password") | ||||
| 		pass2 := r.PostFormValue("password2") | ||||
| @@ -122,7 +148,7 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func EditSelf(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -136,8 +162,16 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-self.html") | ||||
| 		if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil { | ||||
| 		data := &UserHTMLData{ | ||||
| 			User:       user, | ||||
| 			Title:      "Mein Profil bearbeiten", | ||||
| 			ButtonText: "Übernehmen", | ||||
| 			URL:        "/user/update/self", | ||||
| 			Image:      user.ProfilePicLink, | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") | ||||
| 		if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -147,7 +181,7 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func UpdateSelf(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -155,11 +189,12 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 		} | ||||
|  | ||||
| 		user := &b.User{ | ||||
| 			ID:        session.Values["id"].(int64), | ||||
| 			UserName:  r.PostFormValue("username"), | ||||
| 			FirstName: r.PostFormValue("first-name"), | ||||
| 			LastName:  r.PostFormValue("last-name"), | ||||
| 			Email:     r.PostFormValue("email"), | ||||
| 			ID:             session.Values["id"].(int64), | ||||
| 			UserName:       r.PostFormValue("username"), | ||||
| 			FirstName:      r.PostFormValue("first-name"), | ||||
| 			LastName:       r.PostFormValue("last-name"), | ||||
| 			Email:          r.PostFormValue("email"), | ||||
| 			ProfilePicLink: r.PostFormValue("profile-pic-url"), | ||||
| 		} | ||||
|  | ||||
| 		oldPass := r.PostFormValue("old-password") | ||||
| @@ -202,7 +237,7 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err = db.UpdateOwnUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, oldPass, newPass); err != nil { | ||||
| 		if err = db.UpdateOwnUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, user.ProfilePicLink, oldPass, newPass); err != nil { | ||||
| 			log.Println("error: user:", user.ID, err) | ||||
| 			http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -225,11 +260,12 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		var err error | ||||
| 		user := &b.User{ | ||||
| 			UserName:  r.PostFormValue("username"), | ||||
| 			FirstName: r.PostFormValue("first-name"), | ||||
| 			LastName:  r.PostFormValue("last-name"), | ||||
| 			Email:     r.PostFormValue("email"), | ||||
| 			Role:      b.Admin, | ||||
| 			UserName:       r.PostFormValue("username"), | ||||
| 			FirstName:      r.PostFormValue("first-name"), | ||||
| 			LastName:       r.PostFormValue("last-name"), | ||||
| 			Email:          r.PostFormValue("email"), | ||||
| 			ProfilePicLink: r.PostFormValue("profile-pic-url"), | ||||
| 			Role:           b.Admin, | ||||
| 		} | ||||
| 		pass := r.PostFormValue("password") | ||||
| 		pass2 := r.PostFormValue("password2") | ||||
| @@ -293,7 +329,7 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		session, err := getSession(w, r, c, s) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -306,14 +342,14 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H | ||||
| 		}) | ||||
|  | ||||
| 		data.Action = action | ||||
| 		data.Users, err = db.GetAllUsers(c) | ||||
| 		data.Users, err = db.GetAllUsersMap(c) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		delete(data.Users, session.Values["id"].(int64)) | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html") | ||||
| 		if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { | ||||
| 			log.Println(err) | ||||
| @@ -325,7 +361,7 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H | ||||
|  | ||||
| func EditUser(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 { | ||||
| 		if _, err := GetSession(w, r, c, s); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -345,8 +381,16 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data := &UserHTMLData{ | ||||
| 			User:       user, | ||||
| 			Title:      "Profil von " + user.FirstName + " " + user.LastName + " bearbeiten", | ||||
| 			ButtonText: "Übernehmen", | ||||
| 			URL:        fmt.Sprint("/user/update/", user.ID), | ||||
| 			Image:      user.ProfilePicLink, | ||||
| 		} | ||||
|  | ||||
| 		tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") | ||||
| 		if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil { | ||||
| 		if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -356,7 +400,7 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func UpdateUser(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| @@ -402,6 +446,8 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		user.ProfilePicLink = r.PostFormValue("profile-pic-url") | ||||
|  | ||||
| 		newPass := r.PostFormValue("password") | ||||
| 		newPass2 := r.PostFormValue("password2") | ||||
| 		if newPass != newPass2 { | ||||
| @@ -420,7 +466,7 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err = db.UpdateUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, newPass, user.Role); err != nil { | ||||
| 		if err = db.UpdateUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, user.ProfilePicLink, newPass, user.Role); err != nil { | ||||
| 			log.Println("error: user:", user.ID, err) | ||||
| 			http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) | ||||
| 			return | ||||
| @@ -440,7 +486,7 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { | ||||
|  | ||||
| func DeleteUser(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) | ||||
| 		session, err := GetSession(w, r, c, s) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
|   | ||||
| @@ -68,7 +68,7 @@ func main() { | ||||
| 	mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store)) | ||||
| 	mux.HandleFunc("GET /atom/serve", c.ServeAtomFeed(config)) | ||||
| 	mux.HandleFunc("GET /hub", f.ShowHub(config, db, store)) | ||||
| 	mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config)) | ||||
| 	mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config, store)) | ||||
| 	mux.HandleFunc("GET /issue/this", f.ShowCurrentIssue(config, db, store)) | ||||
| 	mux.HandleFunc("GET /logout", f.Logout(config, store)) | ||||
| 	mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config)) | ||||
| @@ -83,10 +83,10 @@ func main() { | ||||
|  | ||||
| 	mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store)) | ||||
| 	mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store)) | ||||
| 	mux.HandleFunc("POST /article/upload-banner", f.UploadBanner(config, store, "article-banner", "editor.html", "article-banner-template")) | ||||
| 	mux.HandleFunc("POST /article/upload-image", f.UploadImage(config, store)) | ||||
| 	mux.HandleFunc("POST /article/upload-banner", f.UploadImage(config, store, "article-banner", "editor.html", "article-banner-template")) | ||||
| 	mux.HandleFunc("POST /article/upload-image", f.UploadEasyMDEImage(config, store)) | ||||
| 	mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store)) | ||||
| 	mux.HandleFunc("POST /issue/upload-banner", f.UploadBanner(config, store, "issue-banner", "current-issue.html", "issue-banner-template")) | ||||
| 	mux.HandleFunc("POST /issue/upload-banner", f.UploadImage(config, store, "issue-banner", "current-issue.html", "issue-banner-template")) | ||||
| 	mux.HandleFunc("POST /login", f.Login(config, db, store)) | ||||
| 	mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store)) | ||||
| 	mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store)) | ||||
| @@ -94,6 +94,7 @@ func main() { | ||||
| 	mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store)) | ||||
| 	mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, store)) | ||||
| 	mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, store)) | ||||
| 	mux.HandleFunc("POST /user/upload-profile-pic", f.UploadImage(config, store, "upload-profile-pic", "edit-user.html", "profile-pic-template")) | ||||
|  | ||||
| 	log.Fatalln(http.ListenAndServe(config.Port, mux)) | ||||
| } | ||||
|   | ||||
| @@ -1,55 +1,73 @@ | ||||
| DROP TABLE IF EXISTS articles_tags; | ||||
| DROP TABLE IF EXISTS articles_contributors; | ||||
| DROP TABLE IF EXISTS articles_authors; | ||||
| DROP TABLE IF EXISTS tags; | ||||
| DROP TABLE IF EXISTS articles; | ||||
| DROP TABLE IF EXISTS issues; | ||||
| DROP TABLE IF EXISTS users; | ||||
|  | ||||
| CREATE TABLE users ( | ||||
|     id                  INT             AUTO_INCREMENT, | ||||
|     username            VARCHAR(255)    NOT NULL UNIQUE, | ||||
|     password            VARCHAR(60)     NOT NULL, | ||||
|     first_name          VARCHAR(255)    NOT NULL, | ||||
|     last_name           VARCHAR(255)    NOT NULL, | ||||
|     email               VARCHAR(255)    NOT NULL, | ||||
|     -- profile_pic_link    VARCHAR(255)    NOT NULL, | ||||
|     role                INT             NOT NULL, | ||||
|     id INT AUTO_INCREMENT, | ||||
|     username VARCHAR(255) NOT NULL UNIQUE, | ||||
|     password VARCHAR(60) NOT NULL, | ||||
|     first_name VARCHAR(255) NOT NULL, | ||||
|     last_name VARCHAR(255) NOT NULL, | ||||
|     email VARCHAR(255) NOT NULL, | ||||
|     profile_pic_link VARCHAR(255), | ||||
|     role INT NOT NULL, | ||||
|     PRIMARY KEY (id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE issues ( | ||||
|     id          INT     AUTO_INCREMENT, | ||||
|     published   BOOL    NOT NULL, | ||||
|     id INT AUTO_INCREMENT, | ||||
|     published BOOL NOT NULL, | ||||
|     PRIMARY KEY (id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE articles ( | ||||
|     id              INT             AUTO_INCREMENT, | ||||
|     title           VARCHAR(255)    NOT NULL, | ||||
|     created         TIMESTAMP       DEFAULT CURRENT_TIMESTAMP, | ||||
|     banner_link     VARCHAR(255)    NOT NULL, | ||||
|     summary         TEXT            NOT NULL, | ||||
|     content_link    VARCHAR(255)    NOT NULL, | ||||
|     published       BOOL            NOT NULL, | ||||
|     rejected        BOOL            NOT NULL, | ||||
|     author_id       INT             NOT NULL, | ||||
|     issue_id        INT             NOT NULL, | ||||
|     edited_id       INT, | ||||
|     is_in_issue     BOOL            NOT NULL, | ||||
|     auto_generated  BOOL            NOT NULL, | ||||
|     id INT AUTO_INCREMENT, | ||||
|     title VARCHAR(255) NOT NULL, | ||||
|     created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     banner_link VARCHAR(255), | ||||
|     summary TEXT NOT NULL, | ||||
|     published BOOL NOT NULL, | ||||
|     rejected BOOL NOT NULL, | ||||
|     creator_id INT NOT NULL, | ||||
|     issue_id INT NOT NULL, | ||||
|     edited_id INT NOT NULL, | ||||
|     clicks INT NOT NULL, | ||||
|     is_in_issue BOOL NOT NULL, | ||||
|     auto_generated BOOL NOT NULL, | ||||
|     PRIMARY KEY (id), | ||||
|     FOREIGN KEY (author_id) REFERENCES users (id), | ||||
|     FOREIGN KEY (creator_id) REFERENCES users (id), | ||||
|     FOREIGN KEY (issue_id) REFERENCES issues (id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE tags ( | ||||
|     id      INT         AUTO_INCREMENT, | ||||
|     name    VARCHAR(50) NOT NULL UNIQUE, | ||||
|     id INT AUTO_INCREMENT, | ||||
|     name VARCHAR(50) NOT NULL UNIQUE, | ||||
|     PRIMARY KEY (id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE articles_authors ( | ||||
|     article_id INT NOT NULL, | ||||
|     author_id INT NOT NULL, | ||||
|     PRIMARY KEY (article_id, author_id), | ||||
|     FOREIGN KEY (article_id) REFERENCES articles (id), | ||||
|     FOREIGN KEY (author_id) REFERENCES users (id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE articles_contributors ( | ||||
|     article_id INT NOT NULL, | ||||
|     contributor_id INT NOT NULL, | ||||
|     PRIMARY KEY (article_id, contributor_id), | ||||
|     FOREIGN KEY (article_id) REFERENCES articles (id), | ||||
|     FOREIGN KEY (contributor_id) REFERENCES users (id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE articles_tags ( | ||||
|     article_id  INT, | ||||
|     tag_id      INT, | ||||
|     article_id INT NOT NULL, | ||||
|     tag_id INT NOT NULL, | ||||
|     PRIMARY KEY (article_id, tag_id), | ||||
|     FOREIGN KEY (article_id) REFERENCES articles (id), | ||||
|     FOREIGN KEY (tag_id) REFERENCES tags (id) | ||||
|   | ||||
| @@ -24,7 +24,7 @@ textarea { | ||||
| } | ||||
|  | ||||
| .btn-area { | ||||
|     @apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4; | ||||
|     @apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1; | ||||
| } | ||||
|  | ||||
| .btn-area-3 { | ||||
| @@ -40,33 +40,33 @@ textarea { | ||||
| } | ||||
|  | ||||
| .EasyMDEContainer .CodeMirror { | ||||
|     @apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100 | ||||
|     @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 | ||||
|     @apply border-slate-900 dark:border-slate-100; | ||||
| } | ||||
|  | ||||
| .EasyMDEContainer .editor-toolbar > * { | ||||
|     @apply text-slate-900 dark:text-slate-100 | ||||
|     @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 | ||||
|     @apply bg-slate-100 dark:bg-slate-900; | ||||
| } | ||||
|  | ||||
| .EasyMDEContainer .CodeMirror-fullscreen { | ||||
|     @apply bg-slate-50 dark:bg-slate-950 | ||||
|     @apply bg-slate-50 dark:bg-slate-950; | ||||
| } | ||||
|  | ||||
| .editor-toolbar { | ||||
|     @apply border border-slate-200 dark:border-slate-800 | ||||
|     @apply border border-slate-200 dark:border-slate-800; | ||||
| } | ||||
|  | ||||
| .editor-toolbar.fullscreen { | ||||
|     @apply bg-slate-50 dark:bg-slate-950 | ||||
|     @apply bg-slate-50 dark:bg-slate-950; | ||||
| } | ||||
|  | ||||
| .editor-preview { | ||||
|     @apply bg-slate-50 dark:bg-slate-950 | ||||
|     @apply bg-slate-50 dark:bg-slate-950; | ||||
| } | ||||
|   | ||||
| @@ -1,66 +0,0 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Neuer Benutzer</h2> | ||||
|  | ||||
| <form> | ||||
|     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
|         <div> | ||||
|             <label for="username">Benutzername</label> | ||||
|             <input class="w-full" required name="username" type="text" value="{{.UserName}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="password">Passwort</label> | ||||
|             <input class="w-full" required name="password" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="password2">Passwort wiederholen</label> | ||||
|             <input class="w-full" required name="password2" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="first-name">Vorname</label> | ||||
|             <input class="w-full" required name="first-name" type="text" value="{{.FirstName}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="last-name">Nachname</label> | ||||
|             <input class="w-full" required name="last-name" type="text" value="{{.LastName}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="email">Emailadresse</label> | ||||
|             <input class="w-full" required name="email" type="text" value="{{.Email}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="email2">Emailadresse wiederholen</label> | ||||
|             <input class="w-full" required name="email2" type="text" value="{{.Email}}" /> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex flex-wrap gap-4"> | ||||
|         <div> | ||||
|             <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> | ||||
|             <label for="author">Autor</label> | ||||
|         </div> | ||||
|         <div> | ||||
|             <input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} /> | ||||
|             <label for="editor">Redakteur</label> | ||||
|         </div> | ||||
|         <div> | ||||
|             <input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} /> | ||||
|             <label for="publisher">Herausgeber</label> | ||||
|         </div> | ||||
|         <div> | ||||
|             <input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} /> | ||||
|             <label for="admin">Administrator</label> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|         <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> | ||||
|     </div> | ||||
| </form> | ||||
| {{end}} | ||||
| @@ -79,8 +79,8 @@ | ||||
| {{end}} | ||||
|  | ||||
| {{define "issue-banner-template"}} | ||||
| <div class="w-full" id="issue-banner-container"> | ||||
|     <img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image"> | ||||
|     <input id="issue-banner-url" name="issue-banner-url" type="hidden" value="{{.URL}}" /> | ||||
| <div id="issue-banner-container"> | ||||
|     <img src="/image/serve/{{.Image}}" alt="" /> | ||||
|     <input id="issue-banner-url" name="issue-banner-url" type="hidden" value="{{.Image}}" /> | ||||
| </div> | ||||
| {{end}} | ||||
|   | ||||
| @@ -1,53 +0,0 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Profil bearbeiten</h2> | ||||
|  | ||||
| <form> | ||||
|     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
|         <div> | ||||
|             <label for="username">Benutzername</label> | ||||
|             <input class="w-full" name="username" type="text" value="{{.UserName}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="first-name">Vorname</label> | ||||
|             <input class="w-full" name="first-name" type="text" value="{{.FirstName}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="last-name">Nachname</label> | ||||
|             <input class="w-full" name="last-name" type="text" value="{{.LastName}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="old-password">Altes Passwort</label> | ||||
|             <input class="w-full" name="old-password" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="password">Passwort</label> | ||||
|             <input class="w-full" name="password" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="password2">Passwort wiederholen</label> | ||||
|             <input class="w-full" name="password2" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="email">Emailadresse</label> | ||||
|             <input class="w-full" required name="email" type="text" value="{{.Email}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="email2">Emailadresse wiederholen</label> | ||||
|             <input class="w-full" required name="email2" type="text" value="{{.Email}}" /> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|         <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/self" | ||||
|             hx-target="#page-content" /> | ||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> | ||||
|     </div> | ||||
| </form> | ||||
| {{end}} | ||||
| @@ -1,67 +1,90 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2> | ||||
| <h2>{{.Title}}</h2> | ||||
|  | ||||
| <form class="flex flex-col gap-4" hx-encoding="multipart/form-data"> | ||||
|     <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | ||||
|         {{template "profile-pic-template" .}} | ||||
|  | ||||
| <form> | ||||
|     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
|         <div> | ||||
|             <label for="username">Benutzername</label> | ||||
|             <input class="w-full" name="username" type="text" value="{{.UserName}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="first-name">Vorname</label> | ||||
|             <input class="w-full" name="first-name" type="text" value="{{.FirstName}}" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="last-name">Nachname</label> | ||||
|             <input class="w-full" name="last-name" type="text" value="{{.LastName}}" /> | ||||
|             <input class="w-full" required name="username" type="text" {{if lt .Role 4}}value="{{.UserName}}" {{end}} /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="password">Passwort</label> | ||||
|             <input class="w-full" name="password" placeholder="***" type="password" /> | ||||
|             <input class="w-full" required name="password" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="password2">Passwort wiederholen</label> | ||||
|             <input class="w-full" name="password2" placeholder="***" type="password" /> | ||||
|             <input class="w-full" required name="password2" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="first-name">Vorname</label> | ||||
|             <input class="w-full" required name="first-name" type="text" {{if lt .Role 4}}value="{{.FirstName}}" | ||||
|                 {{end}} /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="last-name">Nachname</label> | ||||
|             <input class="w-full" required name="last-name" type="text" {{if lt .Role 4}}value="{{.LastName}}" | ||||
|                 {{end}} /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="email">Emailadresse</label> | ||||
|             <input class="w-full" required name="email" type="text" value="{{.Email}}" /> | ||||
|             <input class="w-full" required name="email" type="text" {{if lt .Role 4}}value="{{.Email}}" {{end}} /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="email2">Emailadresse wiederholen</label> | ||||
|             <input class="w-full" required name="email2" type="text" value="{{.Email}}" /> | ||||
|             <input class="w-full" required name="email2" type="text" {{if lt .Role 4}}value="{{.Email}}" {{end}} /> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     {{if lt .Role 4}} | ||||
|     <div class="flex flex-wrap gap-4"> | ||||
|         <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> | ||||
|         </div> | ||||
|         <div> | ||||
|             <input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} /> | ||||
|             <input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2}}checked{{end}} /> | ||||
|             <label for="editor">Redakteur</label> | ||||
|         </div> | ||||
|         <div> | ||||
|             <input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} /> | ||||
|             <input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1}}checked{{end}} /> | ||||
|             <label for="publisher">Herausgeber</label> | ||||
|         </div> | ||||
|         <div> | ||||
|             <input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} /> | ||||
|             <input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0}}checked{{end}} /> | ||||
|             <label for="admin">Administrator</label> | ||||
|         </div> | ||||
|     </div> | ||||
|     {{end}} | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|         <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/{{.ID}}" | ||||
|             hx-target="#page-content" /> | ||||
|         <input class="action-btn" type="submit" value="{{.ButtonText}}" hx-post="{{.URL}}" hx-target="#page-content" /> | ||||
|         <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> | ||||
|     </div> | ||||
| </form> | ||||
| {{end}} | ||||
|  | ||||
| {{define "profile-pic-template"}} | ||||
| <div class="flex items-center justify-center row-span-3 self-center" id="profile-pic-container"> | ||||
|     <label | ||||
|         class="bg-slate-200 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-900 border border-slate-200 dark:border-slate-800 cursor-pointer flex flex-col h-48 items-center justify-center overflow-hidden rounded-full w-48" | ||||
|         for="upload-profile-pic"> | ||||
|         {{if gt (len .Image) 0}} | ||||
|         <img src="/image/serve/{{.Image}}" alt="" /> | ||||
|         {{else}} | ||||
|         <span class="text-2xl">+</span> | ||||
|         <span>Profilbild</span> | ||||
|         {{end}} | ||||
|     </label> | ||||
|     <input class="hidden" id="upload-profile-pic" name="upload-profile-pic" type="file" | ||||
|         hx-post="/user/upload-profile-pic" hx-swap="outerHTML" hx-target="#profile-pic-container" /> | ||||
|     <input id="profile-pic-url" name="profile-pic-url" type="hidden" value="{{.Image}}" /> | ||||
| </div> | ||||
| {{end}} | ||||
|   | ||||
| @@ -1,41 +1,38 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Editor</h2> | ||||
|  | ||||
| <form id="edit-area" hx-encoding="multipart/form-data"> | ||||
| <form class="flex flex-col gap-4" id="edit-area" hx-encoding="multipart/form-data"> | ||||
|     <div class="flex flex-col gap-y-1"> | ||||
|         <div class="w-full" id="article-banner-container"> | ||||
|             <img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image"> | ||||
|             <input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.Article.BannerLink}}" /> | ||||
|         </div> | ||||
|         {{template "article-banner-template" .}} | ||||
|  | ||||
|         <div class="grid grid-cols-2 gap-4 items-center"> | ||||
|             <div class="flex flex-col"> | ||||
|                 <label for="article-title">Titel</label> | ||||
|                 <h3>Titel</h3> | ||||
|                 <input name="article-title" type="text" value="{{.Article.Title}}" /> | ||||
|             </div> | ||||
|  | ||||
|             <div class="grid grid-cols-1 items-center"> | ||||
|             <div class="flex"> | ||||
|                 <label class="btn text-center" for="article-banner">Titelbild</label> | ||||
|                 <input class="hidden" id="article-banner" name="article-banner" type="file" required | ||||
|                     hx-post="/article/upload-banner" hx-target="#article-banner-container" /> | ||||
|                     hx-post="/article/upload-banner" hx-swap="outerHTML" hx-target="#article-banner-container" /> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex flex-col gap-y-1"> | ||||
|         <label for="article-summary">Beschreibung</label> | ||||
|     <div class="flex flex-col gap-1"> | ||||
|         <h3>Beschreibung</h3> | ||||
|         <textarea name="article-summary">{{.Article.Summary}}</textarea> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex flex-col gap-y-1"> | ||||
|         <label for="easyMDE">Artikel</label> | ||||
|     <div class="flex flex-col gap-1"> | ||||
|         <h3>Artikel</h3> | ||||
|         <textarea id="easyMDE">{{.Content}}</textarea> | ||||
|         <input id="article-content" name="article-content" type="hidden" value="{{.Content}}" /> | ||||
|     </div> | ||||
|  | ||||
|     <div> | ||||
|         <span>Tags</span> | ||||
|         <div class="flex flex-wrap gap-x-4"> | ||||
|         <h3>Tags</h3> | ||||
|         <div class="flex flex-wrap gap-4"> | ||||
|             <div> | ||||
|                 <input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} /> | ||||
|                 <label for="issue">Orient Express</label> | ||||
| @@ -51,6 +48,38 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div> | ||||
|         <h3>Beteiligte</h3> | ||||
|         {{range .ArticleUsers}} | ||||
|         <div class="border border-slate-200 dark:border-slate-800 flex gap-4 px-2 py-1 rounded-md"> | ||||
|             <span>{{.FirstName}} {{.LastName}}: </span> | ||||
|  | ||||
|             <div> | ||||
|                 <input id="{{.ID}}-author" name="user-{{.ID}}" type="radio" value="author" {{if eq .ArticleRole | ||||
|                     1}}checked{{end}} /> | ||||
|                 <label for="{{.ID}}-author">Autor</label> | ||||
|             </div> | ||||
|  | ||||
|             <div> | ||||
|                 <input id="{{.ID}}-contributor" name="user-{{.ID}}" type="radio" value="contributor" {{if eq | ||||
|                     .ArticleRole 2}}checked{{end}} /> | ||||
|                 <label for="{{.ID}}-contributor">Mitwirkender</label> | ||||
|             </div> | ||||
|  | ||||
|             <div> | ||||
|                 <input id="{{.ID}}-none" name="user-{{.ID}}" type="radio" {{if eq .ArticleRole 0}}checked{{end}} /> | ||||
|                 <label for="{{.ID}}-none">Unbeteiligt</label> | ||||
|             </div> | ||||
|         </div> | ||||
|         {{end}} | ||||
|     </div> | ||||
|  | ||||
|     <div> | ||||
|         <input id="creator" name="creator" type="checkbox" value="contributor" {{if eq .Creator.ArticleRole | ||||
|             2}}checked{{end}} /> | ||||
|         <label for="creator">Ich bin nicht der Autor.</label> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area"> | ||||
|         <input class="action-btn" type="submit" value="Senden" hx-post="/article/{{.Action}}" | ||||
|             hx-target="#page-content" /> | ||||
| @@ -92,8 +121,8 @@ | ||||
| {{end}} | ||||
|  | ||||
| {{define "article-banner-template"}} | ||||
| <div class="w-full" id="article-banner-container"> | ||||
|     <img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image"> | ||||
|     <input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.URL}}" /> | ||||
| <div id="article-banner-container"> | ||||
|     <img src="/image/serve/{{.Image}}" alt="" /> | ||||
|     <input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.Image}}" /> | ||||
| </div> | ||||
| {{end}} | ||||
|   | ||||
| @@ -1,46 +0,0 @@ | ||||
| {{define "page-content"}} | ||||
| <h2>Erster Benutzer (Administrator)</h2> | ||||
|  | ||||
| <form> | ||||
|     <div class="grid grid-cols-3 gap-4"> | ||||
|         <div> | ||||
|             <label for="username">Benutzername</label> | ||||
|             <input class="w-full" required name="username" type="text" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="password">Passwort</label> | ||||
|             <input class="w-full" required name="password" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="password2">Passwort wiederholen</label> | ||||
|             <input class="w-full" required name="password2" placeholder="***" type="password" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="first-name">Vorname</label> | ||||
|             <input class="w-full" required name="first-name" type="text" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="last-name">Nachname</label> | ||||
|             <input class="w-full" required name="last-name" type="text" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="email">Emailadresse</label> | ||||
|             <input class="w-full" required name="email" type="text" /> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <label for="email2">Emailadresse wiederholen</label> | ||||
|             <input class="w-full" required name="email2" type="text" /> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-area-1"> | ||||
|         <input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add-first" hx-target="#page-content" /> | ||||
|     </div> | ||||
| </form> | ||||
| {{end}} | ||||
| @@ -42,7 +42,7 @@ | ||||
|         <p>{{.Version}} - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p> | ||||
|     </footer> | ||||
|  | ||||
|     <script src="https://unpkg.com/htmx.org@2.0.2"></script> | ||||
|     <script src="https://unpkg.com/htmx.org@2.0.3"></script> | ||||
|     <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script> | ||||
|     <script> | ||||
|         document.addEventListener('DOMContentLoaded', () => { | ||||
|   | ||||
| @@ -3,27 +3,27 @@ | ||||
|  | ||||
| <div> | ||||
|     <div class="w-full" id="article-banner-container"> | ||||
|         <img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image"> | ||||
|         <img src="/image/serve/{{.Image}}" alt="Banner Image"> | ||||
|     </div> | ||||
|  | ||||
|     <span>Titel</span> | ||||
|     <h3>Titel</h3> | ||||
|     <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> | ||||
|     <h3>Beschreibung</h3> | ||||
|     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||
|         {{.Article.Summary}} | ||||
|     </div> | ||||
|  | ||||
|     <span>Artikel</span> | ||||
|     <h3>Artikel</h3> | ||||
|     <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> | ||||
|     <h3>Tags</h3> | ||||
|     <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> | ||||
| @@ -35,6 +35,22 @@ | ||||
|         {{end}} | ||||
|     </div> | ||||
|  | ||||
|     <h3>Autoren</h3> | ||||
|     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||
|         {{range .Authors}} | ||||
|         <span>{{.FirstName}} {{.LastName}}</span> | ||||
|         <br> | ||||
|         {{end}} | ||||
|     </div> | ||||
|  | ||||
|     <h3>Mitwirkende</h3> | ||||
|     <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full"> | ||||
|         {{range .Contributors}} | ||||
|         <span>{{.FirstName}} {{.LastName}}</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}}" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user