Compare commits

..

46 Commits

Author SHA1 Message Date
370ef205a9 Merge branch 'devel' 2024-12-27 21:36:02 +01:00
544dddc893 Add a way to get info about clicks 2024-12-27 11:09:36 +01:00
523bdb24cd Cleanup 2024-12-27 10:53:06 +01:00
376a1264f5 Add further support for clicks counter 2024-12-27 10:52:57 +01:00
82faacb9ec Let the article deletion logic happen entirely in the backend 2024-12-27 10:40:34 +01:00
7f85b60916 Add backend support for clicks counter 2024-12-27 10:37:19 +01:00
ca43ec1a81 Add support for multiple authors and contributors 2024-12-27 10:30:15 +01:00
0a14545a19 Bug fix 2024-12-27 07:20:24 +01:00
8d41caf40a Make issue compatible with multiple authors 2024-12-01 10:04:42 +01:00
8fb0733908 Make atom feed compatible with multiple authors 2024-12-01 09:59:39 +01:00
81f0e46ba6 Correct spelling mistake 2024-11-02 16:26:02 +01:00
19da2ae60c Add articles_authors.go and articles_contributors.go 2024-11-02 16:25:47 +01:00
2078be920e Change articles.go to not include an author 2024-11-02 16:25:15 +01:00
eb8e14ff6d Adapt DB layout to multiple authors and contributors 2024-11-02 16:24:39 +01:00
485eaaca70 Delete fmt.Println() uses 2024-11-01 16:32:42 +01:00
b2a8578c72 Add profile pic and correct usage of banner link 2024-11-01 16:31:47 +01:00
dbddff6e55 Delete content link from everywhere since it is only a combination of already saved info 2024-11-01 16:26:37 +01:00
f663592019 Changed version 2024-11-01 16:22:50 +01:00
c59029cf3d Add rel="self" to Atom feed 2024-11-01 16:22:31 +01:00
d328ddb749 Merge branch 'devel' 2024-10-30 03:25:18 +01:00
ac740cf4b8 Update version number 2024-10-30 03:25:05 +01:00
b7d82f15e9 Serve images from only one route enabling image previews in articles 2024-10-30 03:24:29 +01:00
b4003c8216 Fix bug that made the issue content unavailable 2024-10-30 03:23:59 +01:00
5dc5590da9 Merge branch 'devel' 2024-10-30 02:24:50 +01:00
81c046c1b0 Add summary checks back 2024-10-30 02:22:19 +01:00
364112a0a4 Merge branch 'devel' 2024-10-30 02:14:16 +01:00
1e9fdd2ab9 Update version number 2024-10-30 02:13:40 +01:00
a38523e933 Merge branch 'devel' 2024-10-30 02:13:03 +01:00
1fbc0ddcf6 Make banner link and summary optional 2024-10-30 02:12:53 +01:00
0dd2101a08 Cleanup 2024-10-29 17:00:51 +01:00
e95871ee70 Fix bug that allowed empty banner images 2024-10-29 16:43:42 +01:00
083718711d Use empty alt tag for banner image 2024-10-29 16:37:41 +01:00
20a12c6299 Implement banner images in a more standard way 2024-10-29 16:36:40 +01:00
200672dae2 Merge branch 'devel' 2024-10-28 18:28:31 +01:00
be829e662b Change version number 2024-10-28 18:28:22 +01:00
3d3aad88c8 Merge branch 'devel' 2024-10-28 18:27:33 +01:00
0036b42d82 Update htmx version 2024-10-28 18:27:24 +01:00
e4e43d1a83 Merge branch 'devel' 2024-10-28 18:02:56 +01:00
59deb69e2f Lift size constraints from image and pdf uploads 2024-10-28 17:59:53 +01:00
3aef27585a Rename AtomFeed to AtomFile everywhere else 2024-10-28 17:57:51 +01:00
3aa8796537 Rename AtomFeed to AtomFile in config 2024-10-27 15:50:45 +01:00
737a9ec314 Merge branch 'devel' 2024-10-27 15:29:50 +01:00
4b75fc61cd Update version number 2024-10-27 15:29:42 +01:00
1ebe0380ee Merge branch 'devel' 2024-10-27 15:28:22 +01:00
c439e048c1 Update dependencies 2024-10-27 15:28:07 +01:00
f86f2ba146 Get rid of RSS completely 2024-10-27 15:27:57 +01:00
32 changed files with 1039 additions and 653 deletions

View File

@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
"os"
"time" "time"
) )
@ -13,11 +14,11 @@ type Article struct {
Title string Title string
BannerLink string BannerLink string
Summary string Summary string
ContentLink string
ID int64 ID int64
AuthorID int64 CreatorID int64
IssueID int64 IssueID int64
EditedID int64 EditedID int64
Clicks int
Published bool Published bool
Rejected bool Rejected bool
IsInIssue bool IsInIssue bool
@ -30,7 +31,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
selectQuery := "SELECT id FROM issues WHERE published = false" selectQuery := "SELECT id FROM issues WHERE published = false"
insertQuery := ` insertQuery := `
INSERT INTO articles INSERT INTO articles
(title, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 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) 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 err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -82,7 +83,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetArticle(id int64) (*Article, error) {
query := ` query := `
SELECT title, created, 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 FROM articles
WHERE id = ? WHERE id = ?
` `
@ -92,7 +93,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
var created []byte var created []byte
var err error 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) 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) { func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) {
query := fmt.Sprintf(` 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 FROM articles
WHERE %s = ? WHERE %s = ?
`, attribute) `, attribute)
@ -121,7 +122,7 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro
article := new(Article) article := new(Article)
var created []byte 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) 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} txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
issueQuery := "SELECT id FROM issues WHERE published = false" issueQuery := "SELECT id FROM issues WHERE published = false"
articlesQuery := ` articlesQuery := `
SELECT id, title, created, banner_link, summary, content_link, author_id, auto_generated SELECT id, title, created, banner_link, summary, clicks, auto_generated
FROM articles FROM articles
WHERE issue_id = ? AND published = true AND is_in_issue = true WHERE issue_id = ? AND published = true AND is_in_issue = true
` `
@ -174,7 +175,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
article := new(Article) article := new(Article)
var created []byte 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 { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
} }
@ -256,11 +257,23 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error {
func (db *DB) DeleteArticle(id int64) error { func (db *DB) DeleteArticle(id int64) error {
articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?" 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 = ?" articlesQuery := "DELETE FROM articles WHERE id = ?"
_, err := db.Exec(articlesTagsQuery, id) _, err := db.Exec(articlesTagsQuery, id)
if err != nil { 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) _, err = db.Exec(articlesQuery, id)
@ -270,3 +283,13 @@ func (db *DB) DeleteArticle(id int64) error {
return nil 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
}

View 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)
}

View 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)
}

View File

@ -8,13 +8,13 @@ import (
"git.streifling.com/jason/atom" "git.streifling.com/jason/atom"
) )
type Feed struct{ *atom.Feed }
func GenerateAtomFeed(c *Config, db *DB) (*string, error) { func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
feed := atom.NewFeed(c.Title) feed := atom.NewFeed(c.Title)
feed.ID = atom.NewID("urn:feed:1") feed.ID = atom.NewID("urn:feed:1")
feed.Subtitle = atom.NewText("text", c.Description) feed.Subtitle = atom.NewText("text", c.Description)
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 = atom.NewGenerator("cpolis")
feed.Generator.URI = "https://git.streifling.com/jason/cpolis" feed.Generator.URI = "https://git.streifling.com/jason/cpolis"
@ -22,41 +22,66 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
articles, err := db.GetCertainArticles("published", true) articles, err := db.GetCertainArticles("published", true)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err) return nil, fmt.Errorf("error getting published articles for Atom feed: %v", err)
} }
for _, article := range articles { for _, article := range articles {
articleTitle, err := ConvertToPlain(article.Title) articleTitle, err := ConvertToPlain(article.Title)
if err != nil { if err != nil {
return nil, fmt.Errorf("error converting title to plain text for RSS feed: %v", err) return nil, fmt.Errorf("error converting title to plain text for Atom feed: %v", err)
} }
entry := atom.NewEntry(articleTitle) entry := atom.NewEntry(articleTitle)
entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID)) 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.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)) if article.AutoGenerated {
entry.Links[linkID].Rel = "enclosure" entry.Summary = atom.NewText("text", "automatically generated")
entry.Links[linkID].Type = "image/webp" } else {
user, err := db.GetUser(c, article.AuthorID)
if err != nil {
return nil, fmt.Errorf("error getting user user info for RSS feed: %v", err)
}
entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName))
articleSummary, err := ConvertToPlain(article.Summary) articleSummary, err := ConvertToPlain(article.Summary)
if err != nil { if err != nil {
return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err) 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", articleSummary) 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
}
tags, err := db.GetArticleTags(article.ID) tags, err := db.GetArticleTags(article.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting tags for articles for RSS feed: %v", err) return nil, fmt.Errorf("error getting tags for articles for Atom feed: %v", err)
} }
for _, tag := range tags { for _, tag := range tags {
entry.AddCategory(atom.NewCategory(tag.Name)) entry.AddCategory(atom.NewCategory(tag.Name))
@ -79,7 +104,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
atom, err := feed.ToXML("UTF-8") atom, err := feed.ToXML("UTF-8")
if err != nil { if err != nil {
return nil, fmt.Errorf("error converting RSS feed to XML: %v", err) return nil, fmt.Errorf("error converting Atom feed to XML: %v", err)
} }
return &atom, nil return &atom, nil

View File

@ -18,7 +18,7 @@ type Config struct {
DBName string DBName string
Description string Description string
Domain string Domain string
AtomFeed string AtomFile string
FirebaseKey string FirebaseKey string
GOBKeyFile string GOBKeyFile string
Link string Link string
@ -39,7 +39,7 @@ func newConfig() *Config {
return &Config{ return &Config{
AESKeyFile: "/var/www/cpolis/aes.key", AESKeyFile: "/var/www/cpolis/aes.key",
ArticleDir: "/var/www/cpolis/articles", ArticleDir: "/var/www/cpolis/articles",
AtomFeed: "/var/www/cpolis/cpolis.atom", AtomFile: "/var/www/cpolis/cpolis.atom",
ConfigFile: "/etc/cpolis/config.toml", ConfigFile: "/etc/cpolis/config.toml",
DBName: "cpolis", DBName: "cpolis",
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json", FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
@ -52,7 +52,7 @@ func newConfig() *Config {
PDFDir: "/var/www/cpolis/pdfs", PDFDir: "/var/www/cpolis/pdfs",
PicsDir: "/var/www/cpolis/pics", PicsDir: "/var/www/cpolis/pics",
Port: ":8080", Port: ":8080",
Version: "v0.13.0", Version: "v0.14.0",
WebDir: "/var/www/cpolis/web", 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.AESKeyFile, "aes", c.AESKeyFile, "aes key file")
flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory") 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.ConfigFile, "config", c.ConfigFile, "config file")
flag.StringVar(&c.DBName, "db", c.DBName, "DB name") flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
flag.StringVar(&c.Description, "desc", c.Description, "channel description") flag.StringVar(&c.Description, "desc", c.Description, "channel description")
@ -187,10 +187,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
c.Domain = "https://" + c.Domain c.Domain = "https://" + c.Domain
} }
if cliConfig.AtomFeed != defaultConfig.AtomFeed { if cliConfig.AtomFile != defaultConfig.AtomFile {
c.AtomFeed = cliConfig.AtomFeed c.AtomFile = cliConfig.AtomFile
} }
c.AtomFeed, err = mkFile(c.AtomFeed, 0644, 0744) c.AtomFile, err = mkFile(c.AtomFile, 0644, 0744)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) return fmt.Errorf("error setting up file: %v", err)
} }

View File

@ -1,7 +1,6 @@
package backend package backend
import ( import (
"encoding/base64"
"fmt" "fmt"
"image" "image"
"io" "io"
@ -43,20 +42,3 @@ func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, err
return filename, nil 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
}

View File

@ -1,103 +0,0 @@
package backend
import (
"fmt"
"io"
"os"
"time"
"git.streifling.com/jason/rss"
)
func GenerateRSS(c *Config, db *DB) (*string, error) {
channel := &rss.Channel{
Title: c.Title,
Link: c.Link,
Description: c.Description,
Items: make([]*rss.Item, 0),
}
articles, err := db.GetCertainArticles("published", true)
if err != nil {
return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err)
}
for _, article := range articles {
tags, err := db.GetArticleTags(article.ID)
if err != nil {
return nil, fmt.Errorf("error getting tags for articles for RSS feed: %v", err)
}
tagNames := make([]string, 0)
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
if article.IsInIssue || article.AutoGenerated {
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
}
if article.AutoGenerated {
tagNames = append(tagNames, "autogenerated")
}
user, err := db.GetUser(c, article.AuthorID)
if err != nil {
return nil, fmt.Errorf("error getting user user info for RSS feed: %v", err)
}
articleTitle, err := ConvertToPlain(article.Title)
if err != nil {
return nil, fmt.Errorf("error converting title to plain text for RSS feed: %v", err)
}
articleDescription, err := ConvertToPlain(article.Summary)
if err != nil {
return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
}
item := &rss.Item{
Author: user.FirstName + " " + user.LastName,
Categories: tagNames,
Description: articleDescription,
Guid: string(article.ID),
Link: article.ContentLink,
PubDate: article.Created.Format(time.RFC1123Z),
Title: articleTitle,
}
// if article.AutoGenerated {
// item.Enclosure = &rss.Enclosure{
// Url: article.EncURL,
// Lenght: article.EncLength,
// Type: article.EncType,
// }
// }
//
channel.Items = append(channel.Items, item)
}
feed := rss.NewFeed()
feed.Channels = append(feed.Channels, channel)
rss, err := feed.ToXML("UTF-8")
if err != nil {
return nil, fmt.Errorf("error converting RSS feed to XML: %v", err)
}
return &rss, nil
}
func SaveRSS(filename string, feed *string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating file for RSS feed: %v", err)
}
defer file.Close()
if err = file.Chmod(0644); err != nil {
return fmt.Errorf("error setting permissions for RSS file: %v", err)
}
if _, err = io.WriteString(file, *feed); err != nil {
return fmt.Errorf("error writing to RSS file: %v", err)
}
return nil
}

View File

@ -149,11 +149,11 @@ func (db *DB) AddUser(c *Config, u *User, pass string) (int64, error) {
} }
query := ` query := `
INSERT INTO users (username, password, first_name, last_name, email, role) INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role)
VALUES (?, ?, ?, ?, ?, ?) 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 { if err != nil {
return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err) 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) user := new(User)
query := ` query := `
SELECT id, username, first_name, last_name, email, role SELECT id, username, first_name, last_name, email, profile_pic_link, role
FROM users FROM users
WHERE id = ? WHERE id = ?
` `
row := db.QueryRow(query, 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) 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 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 var err error
tx := new(Tx) tx := new(Tx)
passwordEmpty := len(newPass) > 0 passwordEmpty := len(newPass) == 0
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
err := func() error { 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: "first_name", Value: aesFirstName},
&Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, &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: "email", Value: aesEmail},
&Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink},
); err != nil { ); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -360,8 +361,8 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) {
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
selectQuery := "SELECT COUNT(*) FROM users" selectQuery := "SELECT COUNT(*) FROM users"
insertQuery := ` insertQuery := `
INSERT INTO users (username, password, first_name, last_name, email, role) INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
` `
for i := 0; i < TxMaxRetries; i++ { 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) 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 err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -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) 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 aesFirstName, aesLastName, aesEmail string
var err error 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) rows, err := db.Query(query)
if err != nil { if err != nil {
@ -461,7 +501,7 @@ func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) {
users := make(map[int64]*User, 0) users := make(map[int64]*User, 0)
for rows.Next() { for rows.Next() {
user := new(User) 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) 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 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 var err error
tx := new(Tx) tx := new(Tx)
passwordEmpty := len(newPass) > 0 passwordEmpty := len(newPass) == 0
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
err := func() error { 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: "first_name", Value: aesFirstName},
&Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, &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: "email", Value: aesEmail},
&Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink},
&Attribute{Table: "users", ID: id, AttName: "role", Value: role}, &Attribute{Table: "users", ID: id, AttName: "role", Value: role},
); err != nil { ); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {

View File

@ -10,6 +10,27 @@ import (
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
func incrementClicks(db *b.DB, a *b.Article) error {
a.Clicks++
if err := db.UpdateAttributes(&b.Attribute{Table: "articles", ID: a.ID, AttName: "clicks", Value: a.Clicks}); err != nil {
return fmt.Errorf("error updating click attribute of article %v: %v", a.ID, err)
}
if a.IsInIssue {
issue, err := db.GetArticle(a.IssueID)
if err != nil {
return fmt.Errorf("error getting issue %v: %v", a.IssueID, err)
}
issue.Clicks++
if err := db.UpdateAttributes(&b.Attribute{Table: "articles", ID: issue.ID, AttName: "clicks", Value: issue.Clicks}); err != nil {
return fmt.Errorf("error updating click attribute of issue %v: %v", issue.ID, err)
}
}
return nil
}
func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) { if !tokenIsVerified(w, r, c) {
@ -50,6 +71,41 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return return
} }
fmt.Fprint(w, content) if err = incrementClicks(db, article); 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
}
}
}
func ServeClicks(db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idString := r.PathValue("id")
id, err := strconv.ParseInt(idString, 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err = fmt.Fprint(w, article.Clicks); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} }
} }

View File

@ -14,7 +14,7 @@ func ServeAtomFeed(c *b.Config) http.HandlerFunc {
return return
} }
absFilepath, err := filepath.Abs(c.AtomFeed) absFilepath, err := filepath.Abs(c.AtomFile)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -6,13 +6,16 @@ import (
"path/filepath" "path/filepath"
b "streifling.com/jason/cpolis/cmd/backend" 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) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := f.GetSession(w, r, c, s); err != nil {
if !tokenIsVerified(w, r, c) { if !tokenIsVerified(w, r, c) {
return return
} }
}
absFilepath, err := filepath.Abs(c.PicsDir) absFilepath, err := filepath.Abs(c.PicsDir)
if err != nil { if err != nil {

View File

@ -18,21 +18,36 @@ const (
PreviewMode PreviewMode
) )
const (
None = iota
Author
Contributor
)
type ArticleUser struct {
*b.User
ArticleRole int
}
type EditorHTMLData struct { type EditorHTMLData struct {
Selected map[int64]bool Selected map[int64]bool
Content string Content string
Action string Action string
ActionTitle string ActionTitle string
ActionButton string ActionButton string
BannerImage string Image string
HTMLContent template.HTML HTMLContent template.HTML
Article *b.Article Article *b.Article
Tags []*b.Tag 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 { func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -41,11 +56,30 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
var data *EditorHTMLData var data *EditorHTMLData
if session.Values["article"] == nil { 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 { } else {
data = session.Values["article"].(*EditorHTMLData) 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() data.Tags, err = db.GetTagList()
if err != nil { if err != nil {
log.Println(err) 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 { func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -82,26 +116,55 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
Title: r.PostFormValue("article-title"), Title: r.PostFormValue("article-title"),
BannerLink: r.PostFormValue("article-banner-url"), BannerLink: r.PostFormValue("article-banner-url"),
Summary: r.PostFormValue("article-summary"), Summary: r.PostFormValue("article-summary"),
CreatorID: session.Values["id"].(int64),
Published: false, Published: false,
Rejected: false, Rejected: false,
AuthorID: session.Values["id"].(int64),
IsInIssue: r.PostFormValue("issue") == "on", IsInIssue: r.PostFormValue("issue") == "on",
AutoGenerated: false, AutoGenerated: false,
EditedID: 0,
} }
if len(article.Title) == 0 { if len(article.Title) == 0 {
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest) http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return return
} }
if len(article.BannerLink) == 0 {
http.Error(w, "Bitte ein Titelbild einfügen.", http.StatusBadRequest)
return
}
if len(article.Summary) == 0 { if len(article.Summary) == 0 {
http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest)
return 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) article.ID, err = db.AddArticle(article)
if err != nil { if err != nil {
log.Println(err) 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) http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest)
return return
} }
if err := b.WriteArticleToFile(c, article.ID, content); err != nil {
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, content, 0644); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID) if err = db.WriteArticleAuthors(article.ID, authors); err != nil {
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if 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) tags := make([]int64, 0)
for _, tag := range r.Form["tags"] { for _, tag := range r.Form["tags"] {
tagID, err := strconv.ParseInt(tag, 10, 64) tagID, err := strconv.ParseInt(tag, 10, 64)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
tags = append(tags, tagID) tags = append(tags, tagID)
} }
if err = db.WriteArticleTags(article.ID, tags); err != nil { 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 { func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) article := &b.Article{
if err != nil { Title: r.PostFormValue("article-title"),
log.Println(err) BannerLink: r.PostFormValue("article-banner-url"),
http.Error(w, err.Error(), http.StatusInternalServerError) Summary: r.PostFormValue("article-summary"),
return CreatorID: session.Values["id"].(int64),
IsInIssue: r.PostFormValue("issue") == "on",
} }
title := r.PostFormValue("article-title") if len(article.Title) == 0 {
if len(title) == 0 {
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest) http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return return
} }
if len(article.Summary) == 0 {
summary := r.PostFormValue("article-summary")
if len(summary) == 0 {
http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest)
return 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") content := r.PostFormValue("article-content")
if len(content) == 0 { if len(content) == 0 {
http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest) http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest)
return return
} }
contentLink := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
contentLink := fmt.Sprint(c.ArticleDir, "/", id, ".md")
if err = os.WriteFile(contentLink, []byte(content), 0644); err != nil { if err = os.WriteFile(contentLink, []byte(content), 0644); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) 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( if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, &b.Attribute{Table: "articles", ID: article.ID, AttName: "title", Value: article.Title},
&b.Attribute{Table: "articles", ID: id, AttName: "summary", Value: summary}, &b.Attribute{Table: "articles", ID: article.ID, AttName: "banner_link", Value: article.BannerLink},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, &b.Attribute{Table: "articles", ID: article.ID, AttName: "summary", Value: article.Summary},
&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"}, &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 { ); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) tags := make([]int64, 0)
for _, tag := range r.Form["tags"] { for _, tag := range r.Form["tags"] {
tagID, err := strconv.ParseInt(tag, 10, 64) 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) tags = append(tags, tagID)
} }
if err = db.UpdateArticleTags(id, tags); err != nil { if err = db.UpdateArticleTags(article.ID, tags); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -308,7 +424,7 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
data.MyIDs = make(map[int64]bool) data.MyIDs = make(map[int64]bool)
for _, article := range data.RejectedArticles { for _, article := range data.RejectedArticles {
if article.AuthorID == session.Values["id"].(int64) { if article.CreatorID == session.Values["id"].(int64) {
data.MyIDs[article.ID] = true 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 { func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -346,14 +463,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
imgURL := strings.Split(data.Article.BannerLink, "/") data.Image = 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
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
content, err := os.ReadFile(articleAbsName) content, err := os.ReadFile(articleAbsName)
@ -371,6 +481,46 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return 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) selectedTags, err := db.GetArticleTags(id)
if err != nil { if err != nil {
log.Println(err) 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 { func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -424,9 +574,9 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
} }
if err = db.UpdateAttributes( if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "published", Value: true}, &b.Attribute{Table: "articles", ID: article.ID, AttName: "published", Value: true},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, &b.Attribute{Table: "articles", ID: article.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: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
); err != nil { ); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) 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 return
} }
if err = db.UpdateAttributes( if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "edited_id", Value: 0}); err != nil {
&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 {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil { if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -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 { func ShowCurrentIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { 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) { 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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -623,14 +770,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto
return return
} }
imgURL := strings.Split(article.BannerLink, "/") data.Image = 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.Article.Summary, err = b.ConvertToPlain(article.Summary) data.Article.Summary, err = b.ConvertToPlain(article.Summary)
if err != nil { 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.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) data.Tags, err = db.GetArticleTags(id)
if err != nil { if err != nil {
log.Println(err) 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 { func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -704,7 +860,7 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil { if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -746,25 +902,59 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc
return return
} }
newArticle := oldArticle newArticle := *oldArticle
newArticle.Published = false newArticle.Published = false
newArticle.Rejected = true newArticle.Rejected = true
newArticle.EditedID = oldArticle.ID newArticle.EditedID = oldArticle.ID
newID, err := db.AddArticle(newArticle) newArticle.ID, err = db.AddArticle(&newArticle)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if err = db.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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -806,14 +996,7 @@ func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
imgURL := strings.Split(data.Article.BannerLink, "/") data.Image = 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
}
content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")) content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md"))
if err != nil { if err != nil {

View File

@ -9,20 +9,14 @@ import (
b "streifling.com/jason/cpolis/cmd/backend" 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) { 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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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") file, _, err := r.FormFile("article-image")
if err != nil { if err != nil {
log.Println(err) 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) { 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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -75,20 +69,8 @@ func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate
return return
} }
base64Img, err := b.ServeBase64Image(c, filename) data := new(struct{ Image string })
if err != nil { data.Image = filename
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
tmpl, err := template.ParseFiles(c.WebDir + "/templates/" + htmlFile) tmpl, err := template.ParseFiles(c.WebDir + "/templates/" + htmlFile)
if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil {

View File

@ -13,7 +13,7 @@ import (
func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -33,7 +33,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
Published: true, Published: true,
Rejected: false, Rejected: false,
Created: time.Now(), Created: time.Now(),
AuthorID: session.Values["id"].(int64),
AutoGenerated: true, 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) http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return return
} }
if len(article.BannerLink) == 0 {
http.Error(w, "Bitte ein Titelbild einfügen.", http.StatusBadRequest)
return
}
article.ID, err = db.AddArticle(article) article.ID, err = db.AddArticle(article)
if err != nil { if err != nil {
@ -53,6 +48,22 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return 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")) content := []byte(r.PostFormValue("issue-content"))
if len(content) == 0 { if len(content) == 0 {
http.Error(w, "Bitte eine Beschreibung eingeben.", http.StatusBadRequest) 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 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 { if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -97,7 +95,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil { if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View File

@ -12,18 +12,12 @@ import (
func UploadPDF(c *b.Config, s *b.CookieStore) http.HandlerFunc { func UploadPDF(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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") file, _, err := r.FormFile("pdf-upload")
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@ -26,8 +26,8 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *b.CookieStore, u *b.
return nil return nil
} }
// getSession is used for verifying that the user is logged in and returns their session and an 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) { 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." msg := "Keine gültige Session. Bitte erneut anmelden."
tmpl, tmplErr := template.ParseFiles(c.WebDir+"/templates/index.html", c.WebDir+"/templates/login.html") 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 { data := new(struct {
*UserHTMLData
Version string Version string
Role int
}) })
data.UserHTMLData = &UserHTMLData{User: new(b.User)}
data.Version = c.Version data.Version = c.Version
files := make([]string, 2) files := make([]string, 2)
files[0] = c.WebDir + "/templates/index.html" files[0] = c.WebDir + "/templates/index.html"
if numRows == 0 { 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...) tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil { if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err) log.Println(err)
@ -79,6 +85,7 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if auth, ok := session.Values["authenticated"].(bool); auth && ok { if auth, ok := session.Values["authenticated"].(bool); auth && ok {
data.Role = session.Values["role"].(int) data.Role = session.Values["role"].(int)
files[1] = c.WebDir + "/templates/hub.html" 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 return
} }
} else { } else {
data.Role = b.Author
files[1] = c.WebDir + "/templates/login.html" files[1] = c.WebDir + "/templates/login.html"
tmpl, err := template.ParseFiles(files...) tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil { if err = template.Must(tmpl, err).Execute(w, data); err != nil {
@ -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 { func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -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 { func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -10,7 +10,7 @@ import (
func CreateTag(c *b.Config, s *b.CookieStore) http.HandlerFunc { func CreateTag(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func AddTag(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -5,11 +5,20 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"sort"
"strconv" "strconv"
b "streifling.com/jason/cpolis/cmd/backend" 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) { func checkUserStrings(user *b.User) (string, int, bool) {
userLen := 63 // max value for utf-8 at 255 bytes userLen := 63 // max value for utf-8 at 255 bytes
nameLen := 56 // max value when aes encrypting utf-8 at up to 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 { func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html") data := &UserHTMLData{
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil { 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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -56,6 +81,7 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
FirstName: r.PostFormValue("first-name"), FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"), LastName: r.PostFormValue("last-name"),
Email: r.PostFormValue("email"), Email: r.PostFormValue("email"),
ProfilePicLink: r.PostFormValue("profile-pic-url"),
} }
pass := r.PostFormValue("password") pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2") 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 { func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -136,8 +162,16 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-self.html") data := &UserHTMLData{
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil { 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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -160,6 +194,7 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
FirstName: r.PostFormValue("first-name"), FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"), LastName: r.PostFormValue("last-name"),
Email: r.PostFormValue("email"), Email: r.PostFormValue("email"),
ProfilePicLink: r.PostFormValue("profile-pic-url"),
} }
oldPass := r.PostFormValue("old-password") oldPass := r.PostFormValue("old-password")
@ -202,7 +237,7 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return 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) log.Println("error: user:", user.ID, err)
http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError)
return return
@ -229,6 +264,7 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
FirstName: r.PostFormValue("first-name"), FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"), LastName: r.PostFormValue("last-name"),
Email: r.PostFormValue("email"), Email: r.PostFormValue("email"),
ProfilePicLink: r.PostFormValue("profile-pic-url"),
Role: b.Admin, Role: b.Admin,
} }
pass := r.PostFormValue("password") pass := r.PostFormValue("password")
@ -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 { func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -306,14 +342,14 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
}) })
data.Action = action data.Action = action
data.Users, err = db.GetAllUsers(c) data.Users, err = db.GetAllUsersMap(c)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
delete(data.Users, session.Values["id"].(int64)) delete(data.Users, session.Values["id"].(int64))
tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err) log.Println(err)
@ -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 { func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -345,8 +381,16 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return 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") 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) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -402,6 +446,8 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
user.ProfilePicLink = r.PostFormValue("profile-pic-url")
newPass := r.PostFormValue("password") newPass := r.PostFormValue("password")
newPass2 := r.PostFormValue("password2") newPass2 := r.PostFormValue("password2")
if newPass != newPass2 { if newPass != newPass2 {
@ -420,7 +466,7 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return 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) log.Println("error: user:", user.ID, err)
http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError)
return 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 { func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s) session, err := GetSession(w, r, c, s)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -65,10 +65,11 @@ func main() {
mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store)) mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store))
mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, store, "publish", "Artikel veröffentlichen", "Veröffentlichen")) mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, store, "publish", "Artikel veröffentlichen", "Veröffentlichen"))
mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db)) mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db))
mux.HandleFunc("GET /article/serve/{id}/clicks", c.ServeClicks(db))
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store)) mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
mux.HandleFunc("GET /atom/serve", c.ServeAtomFeed(config)) mux.HandleFunc("GET /atom/serve", c.ServeAtomFeed(config))
mux.HandleFunc("GET /hub", f.ShowHub(config, db, store)) mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))
mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config)) mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config, store))
mux.HandleFunc("GET /issue/this", f.ShowCurrentIssue(config, db, store)) mux.HandleFunc("GET /issue/this", f.ShowCurrentIssue(config, db, store))
mux.HandleFunc("GET /logout", f.Logout(config, store)) mux.HandleFunc("GET /logout", f.Logout(config, store))
mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config)) mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
@ -83,10 +84,10 @@ func main() {
mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store)) 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/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-banner", f.UploadImage(config, store, "article-banner", "editor.html", "article-banner-template"))
mux.HandleFunc("POST /article/upload-image", f.UploadImage(config, store)) mux.HandleFunc("POST /article/upload-image", f.UploadEasyMDEImage(config, store))
mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, 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 /login", f.Login(config, db, store))
mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store)) mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store))
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store)) mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
@ -94,6 +95,7 @@ func main() {
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store)) 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/{id}", f.UpdateUser(config, db, store))
mux.HandleFunc("POST /user/update/self", f.UpdateSelf(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)) log.Fatalln(http.ListenAndServe(config.Port, mux))
} }

View File

@ -1,4 +1,6 @@
DROP TABLE IF EXISTS articles_tags; 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 tags;
DROP TABLE IF EXISTS articles; DROP TABLE IF EXISTS articles;
DROP TABLE IF EXISTS issues; DROP TABLE IF EXISTS issues;
@ -11,7 +13,7 @@ CREATE TABLE users (
first_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL,
-- profile_pic_link VARCHAR(255) NOT NULL, profile_pic_link VARCHAR(255),
role INT NOT NULL, role INT NOT NULL,
PRIMARY KEY (id) PRIMARY KEY (id)
); );
@ -26,18 +28,18 @@ CREATE TABLE articles (
id INT AUTO_INCREMENT, id INT AUTO_INCREMENT,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
banner_link VARCHAR(255) NOT NULL, banner_link VARCHAR(255),
summary TEXT NOT NULL, summary TEXT NOT NULL,
content_link VARCHAR(255) NOT NULL,
published BOOL NOT NULL, published BOOL NOT NULL,
rejected BOOL NOT NULL, rejected BOOL NOT NULL,
author_id INT NOT NULL, creator_id INT NOT NULL,
issue_id INT NOT NULL, issue_id INT NOT NULL,
edited_id INT, edited_id INT NOT NULL,
clicks INT NOT NULL,
is_in_issue BOOL NOT NULL, is_in_issue BOOL NOT NULL,
auto_generated BOOL NOT NULL, auto_generated BOOL NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (author_id) REFERENCES users (id), FOREIGN KEY (creator_id) REFERENCES users (id),
FOREIGN KEY (issue_id) REFERENCES issues (id) FOREIGN KEY (issue_id) REFERENCES issues (id)
); );
@ -47,9 +49,25 @@ CREATE TABLE tags (
PRIMARY KEY (id) 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 ( CREATE TABLE articles_tags (
article_id INT, article_id INT NOT NULL,
tag_id INT, tag_id INT NOT NULL,
PRIMARY KEY (article_id, tag_id), PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES articles (id), FOREIGN KEY (article_id) REFERENCES articles (id),
FOREIGN KEY (tag_id) REFERENCES tags (id) FOREIGN KEY (tag_id) REFERENCES tags (id)

23
go.mod
View File

@ -3,9 +3,8 @@ module streifling.com/jason/cpolis
go 1.23.2 go 1.23.2
require ( require (
firebase.google.com/go/v4 v4.14.1 firebase.google.com/go/v4 v4.15.0
git.streifling.com/jason/atom v1.0.0 git.streifling.com/jason/atom v1.0.0
git.streifling.com/jason/rss v0.1.3
github.com/BurntSushi/toml v1.4.0 github.com/BurntSushi/toml v1.4.0
github.com/chai2010/webp v1.1.1 github.com/chai2010/webp v1.1.1
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
@ -16,19 +15,19 @@ require (
github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark v1.7.8
golang.org/x/crypto v0.28.0 golang.org/x/crypto v0.28.0
golang.org/x/term v0.25.0 golang.org/x/term v0.25.0
google.golang.org/api v0.201.0 google.golang.org/api v0.203.0
) )
require ( require (
cel.dev/expr v0.16.2 // indirect cel.dev/expr v0.18.0 // indirect
cloud.google.com/go v0.116.0 // indirect cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.9.8 // indirect cloud.google.com/go/auth v0.9.9 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect
cloud.google.com/go/firestore v1.17.0 // indirect cloud.google.com/go/firestore v1.17.0 // indirect
cloud.google.com/go/iam v1.2.1 // indirect cloud.google.com/go/iam v1.2.2 // indirect
cloud.google.com/go/longrunning v0.6.1 // indirect cloud.google.com/go/longrunning v0.6.2 // indirect
cloud.google.com/go/monitoring v1.21.1 // indirect cloud.google.com/go/monitoring v1.21.2 // indirect
cloud.google.com/go/storage v1.45.0 // indirect cloud.google.com/go/storage v1.45.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.3 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.3 // indirect
@ -70,10 +69,10 @@ require (
golang.org/x/text v0.19.0 // indirect golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.7.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/grpc v1.67.1 // indirect google.golang.org/grpc v1.67.1 // indirect
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241018153737-98959d9a4904 // indirect google.golang.org/grpc/stats/opentelemetry v0.0.0-20241025232817-cb329375b14e // indirect
google.golang.org/protobuf v1.35.1 // indirect google.golang.org/protobuf v1.35.1 // indirect
) )

50
go.sum
View File

@ -1,36 +1,34 @@
cel.dev/expr v0.16.2 h1:RwRhoH17VhAu9U5CMvMhH1PDVgf0tuz9FT+24AfMLfU= cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
cel.dev/expr v0.16.2/go.mod h1:gXngZQMkWJoSbE8mOzehJlXQyubn/Vg0vR9/F3W7iw8= cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8= cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ=
cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ= cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ=
cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y=
cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
cloud.google.com/go/logging v1.11.0 h1:v3ktVzXMV7CwHq1MBF65wcqLMA7i+z3YxbUsoK7mOKs= cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk=
cloud.google.com/go/logging v1.11.0/go.mod h1:5LDiJC/RxTt+fHc1LAt20R9TKiUTReDg6RuuFOZ67+A= cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM=
cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc= cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU=
cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c= cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU=
cloud.google.com/go/storage v1.45.0 h1:5av0QcIVj77t+44mV4gffFC/LscFRUhto6UBMB5SimM= cloud.google.com/go/storage v1.45.0 h1:5av0QcIVj77t+44mV4gffFC/LscFRUhto6UBMB5SimM=
cloud.google.com/go/storage v1.45.0/go.mod h1:wpPblkIuMP5jCB/E48Pz9zIo2S/zD8g+ITmxKkPCITE= cloud.google.com/go/storage v1.45.0/go.mod h1:wpPblkIuMP5jCB/E48Pz9zIo2S/zD8g+ITmxKkPCITE=
cloud.google.com/go/trace v1.11.1 h1:UNqdP+HYYtnm6lb91aNA5JQ0X14GnxkABGlfz2PzPew= cloud.google.com/go/trace v1.11.1 h1:UNqdP+HYYtnm6lb91aNA5JQ0X14GnxkABGlfz2PzPew=
cloud.google.com/go/trace v1.11.1/go.mod h1:IQKNQuBzH72EGaXEodKlNJrWykGZxet2zgjtS60OtjA= cloud.google.com/go/trace v1.11.1/go.mod h1:IQKNQuBzH72EGaXEodKlNJrWykGZxet2zgjtS60OtjA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g= firebase.google.com/go/v4 v4.15.0 h1:k27M+cHbyN1YpBI2Cf4NSjeHnnYRB9ldXwpqA5KikN0=
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM= firebase.google.com/go/v4 v4.15.0/go.mod h1:S/4MJqVZn1robtXkHhpRUbwOC4gdYtgsiMMJQ4x+xmQ=
git.streifling.com/jason/atom v1.0.0 h1:E88z4S7JeT6T+WuAaJWnGwCWTx+vzSJ6giUL51MdptI= git.streifling.com/jason/atom v1.0.0 h1:E88z4S7JeT6T+WuAaJWnGwCWTx+vzSJ6giUL51MdptI=
git.streifling.com/jason/atom v1.0.0/go.mod h1:FNTYJfatYaIOQn4OKy8y+Mtohqm3MeyEGZUu4bMtZ9E= git.streifling.com/jason/atom v1.0.0/go.mod h1:FNTYJfatYaIOQn4OKy8y+Mtohqm3MeyEGZUu4bMtZ9E=
git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU=
git.streifling.com/jason/rss v0.1.3/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
@ -227,8 +225,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.201.0 h1:+7AD9JNM3tREtawRMu8sOjSbb8VYcYXJG/2eEOmfDu0= google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU=
google.golang.org/api v0.201.0/go.mod h1:HVY0FCHVs89xIW9fzf/pBvOEm+OolHa86G/txFezyq4= google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
@ -236,12 +234,12 @@ google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4=
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 h1:2oV8dfuIkM1Ti7DwXc0BJfnwr9csz4TDXI9EmiI+Rbw=
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38/go.mod h1:vuAjtvlwkDKF6L1GQ0SokiRLCGFfeBUXWr/aFFkHACc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@ -249,8 +247,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241018153737-98959d9a4904 h1:Lplo3VKrYtWeryBkDI4SZ4kJTFaWO4qUGs+xX7N2bFc= google.golang.org/grpc/stats/opentelemetry v0.0.0-20241025232817-cb329375b14e h1:SoMI+r+Qsp379U9BlVzrHtqAqYP3NEv9vNhYqUaAWOg=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241018153737-98959d9a4904/go.mod h1:jzYlkSMbKypzuu6xoAEijsNVo9ZeDF1u/zCfFgsx7jg= google.golang.org/grpc/stats/opentelemetry v0.0.0-20241025232817-cb329375b14e/go.mod h1:jzYlkSMbKypzuu6xoAEijsNVo9ZeDF1u/zCfFgsx7jg=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View File

@ -24,7 +24,7 @@ textarea {
} }
.btn-area { .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 { .btn-area-3 {
@ -40,33 +40,33 @@ textarea {
} }
.EasyMDEContainer .CodeMirror { .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 { .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 > * { .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 { .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 { .EasyMDEContainer .CodeMirror-fullscreen {
@apply bg-slate-50 dark:bg-slate-950 @apply bg-slate-50 dark:bg-slate-950;
} }
.editor-toolbar { .editor-toolbar {
@apply border border-slate-200 dark:border-slate-800 @apply border border-slate-200 dark:border-slate-800;
} }
.editor-toolbar.fullscreen { .editor-toolbar.fullscreen {
@apply bg-slate-50 dark:bg-slate-950 @apply bg-slate-50 dark:bg-slate-950;
} }
.editor-preview { .editor-preview {
@apply bg-slate-50 dark:bg-slate-950 @apply bg-slate-50 dark:bg-slate-950;
} }

View File

@ -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}}

View File

@ -79,8 +79,8 @@
{{end}} {{end}}
{{define "issue-banner-template"}} {{define "issue-banner-template"}}
<div class="w-full" id="issue-banner-container"> <div id="issue-banner-container">
<img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image"> <img src="/image/serve/{{.Image}}" alt="" />
<input id="issue-banner-url" name="issue-banner-url" type="hidden" value="{{.URL}}" /> <input id="issue-banner-url" name="issue-banner-url" type="hidden" value="{{.Image}}" />
</div> </div>
{{end}} {{end}}

View File

@ -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}}

View File

@ -1,44 +1,49 @@
{{define "page-content"}} {{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> <div>
<label for="username">Benutzername</label> <label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" /> <input class="w-full" required name="username" type="text" {{if lt .Role 4}}value="{{.UserName}}" {{end}} />
</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>
<div> <div>
<label for="password">Passwort</label> <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>
<div> <div>
<label for="password2">Passwort wiederholen</label> <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>
<div> <div>
<label for="email">Emailadresse</label> <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>
<div> <div>
<label for="email2">Emailadresse wiederholen</label> <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>
</div> </div>
{{if lt .Role 4}}
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<div> <div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3}}checked{{end}} /> <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3}}checked{{end}} />
@ -57,11 +62,29 @@
<label for="admin">Administrator</label> <label for="admin">Administrator</label>
</div> </div>
</div> </div>
{{end}}
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/{{.ID}}" <input class="action-btn" type="submit" value="{{.ButtonText}}" hx-post="{{.URL}}" hx-target="#page-content" />
hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>
</form> </form>
{{end}} {{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}}

View File

@ -1,41 +1,38 @@
{{define "page-content"}} {{define "page-content"}}
<h2>Editor</h2> <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="flex flex-col gap-y-1">
<div class="w-full" id="article-banner-container"> {{template "article-banner-template" .}}
<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>
<div class="grid grid-cols-2 gap-4 items-center"> <div class="grid grid-cols-2 gap-4 items-center">
<div class="flex flex-col"> <div class="flex flex-col">
<label for="article-title">Titel</label> <h3>Titel</h3>
<input name="article-title" type="text" value="{{.Article.Title}}" /> <input name="article-title" type="text" value="{{.Article.Title}}" />
</div> </div>
<div class="grid grid-cols-1 items-center"> <div class="flex">
<label class="btn text-center" for="article-banner">Titelbild</label> <label class="btn text-center" for="article-banner">Titelbild</label>
<input class="hidden" id="article-banner" name="article-banner" type="file" required <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>
</div> </div>
<div class="flex flex-col gap-y-1"> <div class="flex flex-col gap-1">
<label for="article-summary">Beschreibung</label> <h3>Beschreibung</h3>
<textarea name="article-summary">{{.Article.Summary}}</textarea> <textarea name="article-summary">{{.Article.Summary}}</textarea>
</div> </div>
<div class="flex flex-col gap-y-1"> <div class="flex flex-col gap-1">
<label for="easyMDE">Artikel</label> <h3>Artikel</h3>
<textarea id="easyMDE">{{.Content}}</textarea> <textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" value="{{.Content}}" /> <input id="article-content" name="article-content" type="hidden" value="{{.Content}}" />
</div> </div>
<div> <div>
<span>Tags</span> <h3>Tags</h3>
<div class="flex flex-wrap gap-x-4"> <div class="flex flex-wrap gap-4">
<div> <div>
<input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} /> <input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
<label for="issue">Orient Express</label> <label for="issue">Orient Express</label>
@ -51,6 +48,38 @@
</div> </div>
</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"> <div class="btn-area">
<input class="action-btn" type="submit" value="Senden" hx-post="/article/{{.Action}}" <input class="action-btn" type="submit" value="Senden" hx-post="/article/{{.Action}}"
hx-target="#page-content" /> hx-target="#page-content" />
@ -92,8 +121,8 @@
{{end}} {{end}}
{{define "article-banner-template"}} {{define "article-banner-template"}}
<div class="w-full" id="article-banner-container"> <div id="article-banner-container">
<img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image"> <img src="/image/serve/{{.Image}}" alt="" />
<input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.URL}}" /> <input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.Image}}" />
</div> </div>
{{end}} {{end}}

View File

@ -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}}

View File

@ -42,7 +42,7 @@
<p>{{.Version}} - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p> <p>{{.Version}} - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p>
</footer> </footer>
<script src="https://unpkg.com/htmx.org@2.0.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 src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {

View File

@ -3,27 +3,27 @@
<div> <div>
<div class="w-full" id="article-banner-container"> <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> </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"> <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
{{.Article.Title}} {{.Article.Title}}
</div> </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"> <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
{{.Article.Summary}} {{.Article.Summary}}
</div> </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="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"> <div class="prose text-slate-900 dark:text-slate-100">
{{.HTMLContent}} {{.HTMLContent}}
</div> </div>
</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"> <div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
{{if .Article.IsInIssue}} {{if .Article.IsInIssue}}
<span>Orient Express</span> <span>Orient Express</span>
@ -35,6 +35,22 @@
{{end}} {{end}}
</div> </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"}} {{if eq .Action "publish"}}
<div class="btn-area-3"> <div class="btn-area-3">
<input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}" <input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}"