Merge branch 'devel'

This commit is contained in:
Jason Streifling 2024-12-27 21:36:02 +01:00
commit 370ef205a9
23 changed files with 933 additions and 380 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

@ -12,7 +12,9 @@ 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"
@ -31,7 +33,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
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.Published = atom.NewDate(article.Created) entry.Published = atom.NewDate(article.Created)
entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", article.ContentLink) entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", fmt.Sprint(c.Domain, "/article/serve/", article.ID))
if article.AutoGenerated { if article.AutoGenerated {
entry.Summary = atom.NewText("text", "automatically generated") entry.Summary = atom.NewText("text", "automatically generated")
@ -44,16 +46,38 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
} }
if len(article.BannerLink) > 0 { if len(article.BannerLink) > 0 {
linkID := entry.AddLink(atom.NewLink(article.BannerLink)) linkID := entry.AddLink(atom.NewLink(c.Domain + "/image/serve/" + article.BannerLink))
entry.Links[linkID].Rel = "enclosure" entry.Links[linkID].Rel = "enclosure"
entry.Links[linkID].Type = "image/webp" entry.Links[linkID].Type = "image/webp"
} }
user, err := db.GetUser(c, article.AuthorID) authors, err := db.GetArticleAuthors(c, article.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting user user info for Atom feed: %v", err) return nil, fmt.Errorf("error getting article's authors for Atom feed: %v", err)
}
for _, author := range authors {
user, err := db.GetUser(c, author.ID)
if err != nil {
return nil, fmt.Errorf("error getting user info for Atom feed: %v", err)
}
authorID := entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName))
entry.Authors[authorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink
}
contributors, err := db.GetArticleContributors(c, article.ID)
if err != nil {
return nil, fmt.Errorf("error getting article's contributors for Atom feed: %v", err)
}
for _, contributor := range contributors {
user, err := db.GetUser(c, contributor.ID)
if err != nil {
return nil, fmt.Errorf("error getting user info for Atom feed: %v", err)
}
contributorID := entry.AddContributor(atom.NewPerson(user.FirstName + " " + user.LastName))
entry.Contributors[contributorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink
} }
entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName))
tags, err := db.GetArticleTags(article.ID) tags, err := db.GetArticleTags(article.ID)
if err != nil { if err != nil {

View File

@ -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.4", Version: "v0.14.0",
WebDir: "/var/www/cpolis/web", WebDir: "/var/www/cpolis/web",
} }
} }

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

@ -18,16 +18,31 @@ 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 {
@ -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)
@ -80,13 +114,14 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
article := &b.Article{ article := &b.Article{
Title: r.PostFormValue("article-title"), Title: r.PostFormValue("article-title"),
BannerLink: c.Domain + "/image/serve/" + 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 {
@ -98,6 +133,38 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
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)
@ -110,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 {
@ -164,27 +235,59 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
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 {
bannerLink := r.PostFormValue("article-banner-url") http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest)
if len(bannerLink) != 0 { return
bannerLink = c.Domain + "/image/serve/" + bannerLink
} }
summary := r.PostFormValue("article-summary") r.ParseForm()
if len(summary) == 0 { authors := make([]int64, 0)
http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) 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 return
} }
@ -193,8 +296,7 @@ func ResubmitArticle(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
} }
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)
@ -202,18 +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: "banner_link", Value: bannerLink}, &b.Attribute{Table: "articles", ID: article.ID, AttName: "banner_link", Value: article.BannerLink},
&b.Attribute{Table: "articles", ID: id, AttName: "summary", Value: summary}, &b.Attribute{Table: "articles", ID: article.ID, AttName: "summary", Value: article.Summary},
&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: "is_in_issue", Value: r.PostFormValue("issue") == "on"}, &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)
@ -224,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
@ -310,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
} }
} }
@ -327,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
@ -348,8 +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
data.BannerImage = imgURL[len(imgURL)-1]
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)
@ -367,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)
@ -420,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)
@ -449,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
@ -619,8 +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
data.BannerImage = imgURL[len(imgURL)-1]
data.Article.Summary, err = b.ConvertToPlain(article.Summary) data.Article.Summary, err = b.ConvertToPlain(article.Summary)
if err != nil { if err != nil {
@ -644,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)
@ -736,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
@ -796,8 +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
data.BannerImage = imgURL[len(imgURL)-1]
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,7 +9,7 @@ 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)
@ -42,7 +42,7 @@ 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)
@ -69,8 +69,8 @@ func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate
return return
} }
data := new(struct{ BannerImage string }) data := new(struct{ Image string })
data.BannerImage = filename data.Image = 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

@ -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,
} }
@ -49,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)
@ -62,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)

View File

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

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,6 +34,15 @@ 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 {
@ -33,8 +51,15 @@ func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc {
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
@ -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")
@ -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
@ -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")
@ -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)
@ -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
@ -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

View File

@ -65,6 +65,7 @@ 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))
@ -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)

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,67 +1,90 @@
{{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}} />
<label for="author">Autor</label> <label for="author">Autor</label>
</div> </div>
<div> <div>
<input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} /> <input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2}}checked{{end}} />
<label for="editor">Redakteur</label> <label for="editor">Redakteur</label>
</div> </div>
<div> <div>
<input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} /> <input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1}}checked{{end}} />
<label for="publisher">Herausgeber</label> <label for="publisher">Herausgeber</label>
</div> </div>
<div> <div>
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} /> <input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0}}checked{{end}} />
<label for="admin">Administrator</label> <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,17 +1,17 @@
{{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">
{{template "article-banner-template" .}} {{template "article-banner-template" .}}
<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-swap="outerHTML" hx-target="#article-banner-container" /> hx-post="/article/upload-banner" hx-swap="outerHTML" hx-target="#article-banner-container" />
@ -19,20 +19,20 @@
</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>
@ -48,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" />
@ -90,7 +122,7 @@
{{define "article-banner-template"}} {{define "article-banner-template"}}
<div id="article-banner-container"> <div id="article-banner-container">
<img src="/image/serve/{{.BannerImage}}" alt=""> <img src="/image/serve/{{.Image}}" alt="" />
<input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.BannerImage}}" /> <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

@ -3,27 +3,27 @@
<div> <div>
<div class="w-full" id="article-banner-container"> <div class="w-full" id="article-banner-container">
<img src="/image/serve/{{.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}}"