diff --git a/cmd/backend/articles.go b/cmd/backend/articles.go index e5ef324..e8c5802 100644 --- a/cmd/backend/articles.go +++ b/cmd/backend/articles.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "log" + "os" "time" ) @@ -13,11 +14,11 @@ type Article struct { Title string BannerLink string Summary string - ContentLink string ID int64 - AuthorID int64 + CreatorID int64 IssueID int64 EditedID int64 + Clicks int Published bool Rejected bool IsInIssue bool @@ -30,7 +31,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { selectQuery := "SELECT id FROM issues WHERE published = false" insertQuery := ` INSERT INTO articles - (title, banner_link, summary, content_link, published, rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated) + (title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` @@ -48,7 +49,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) } - result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.ContentLink, a.Published, a.Rejected, a.AuthorID, id, a.EditedID, a.IsInIssue, a.AutoGenerated) + result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.CreatorID, id, a.EditedID, 0, a.IsInIssue, a.AutoGenerated) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -82,7 +83,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) GetArticle(id int64) (*Article, error) { query := ` - SELECT title, created, banner_link, summary, content_link, published, author_id, issue_id, edited_id, is_in_issue, auto_generated + SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated FROM articles WHERE id = ? ` @@ -92,7 +93,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { var created []byte var err error - if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.Published, &article.AuthorID, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.Clicks, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -107,7 +108,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { query := fmt.Sprintf(` - SELECT id, title, created, banner_link, summary, content_link, author_id, issue_id, published, rejected, is_in_issue, auto_generated + SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, published, rejected, is_in_issue, auto_generated FROM articles WHERE %s = ? `, attribute) @@ -121,7 +122,7 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.AuthorID, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.CreatorID, &article.IssueID, &article.Clicks, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -141,7 +142,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} issueQuery := "SELECT id FROM issues WHERE published = false" articlesQuery := ` - SELECT id, title, created, banner_link, summary, content_link, author_id, auto_generated + SELECT id, title, created, banner_link, summary, clicks, auto_generated FROM articles WHERE issue_id = ? AND published = true AND is_in_issue = true ` @@ -174,7 +175,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.AuthorID, &article.AutoGenerated); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Clicks, &article.AutoGenerated); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } @@ -256,11 +257,23 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error { func (db *DB) DeleteArticle(id int64) error { articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?" + articlesContributorsQuery := "DELETE FROM articles_contributors WHERE article_id = ?" + articlesAuthorsQuery := "DELETE FROM articles_authors WHERE article_id = ?" articlesQuery := "DELETE FROM articles WHERE id = ?" _, err := db.Exec(articlesTagsQuery, id) if err != nil { - return fmt.Errorf("error deleting article %v from DB: %v", id, err) + return fmt.Errorf("error deleting articles_tags %v from DB: %v", id, err) + } + + _, err = db.Exec(articlesContributorsQuery, id) + if err != nil { + return fmt.Errorf("error deleting articles_contributors %v from DB: %v", id, err) + } + + _, err = db.Exec(articlesAuthorsQuery, id) + if err != nil { + return fmt.Errorf("error deleting articles_authors %v from DB: %v", id, err) } _, err = db.Exec(articlesQuery, id) @@ -270,3 +283,13 @@ func (db *DB) DeleteArticle(id int64) error { return nil } + +func WriteArticleToFile(c *Config, articleID int64, content []byte) error { + articleAbsName := fmt.Sprint(c.ArticleDir, "/", articleID, ".md") + + if err := os.WriteFile(articleAbsName, content, 0644); err != nil { + return fmt.Errorf("error writing article %v to file: %v", articleID, err) + } + + return nil +} diff --git a/cmd/backend/articles_authors.go b/cmd/backend/articles_authors.go new file mode 100644 index 0000000..0f4ef9e --- /dev/null +++ b/cmd/backend/articles_authors.go @@ -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) +} diff --git a/cmd/backend/articles_contributors.go b/cmd/backend/articles_contributors.go new file mode 100644 index 0000000..696c606 --- /dev/null +++ b/cmd/backend/articles_contributors.go @@ -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) +} diff --git a/cmd/backend/articles_tags.go b/cmd/backend/articles_tags.go index 72e7273..0cc9652 100644 --- a/cmd/backend/articles_tags.go +++ b/cmd/backend/articles_tags.go @@ -43,8 +43,8 @@ func (db *DB) GetArticleTags(articleID int64) ([]*Tag, error) { query := ` SELECT t.id, t.name FROM articles a - INNER JOIN articles_tags at ON a.id = at.article_id - INNER JOIN tags t ON at.tag_id = t.id + INNER JOIN articles_tags at ON a.id = at.article_id + INNER JOIN tags t ON at.tag_id = t.id WHERE a.id = ? ` rows, err := db.Query(query, articleID) diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index 6133818..f3ec0c5 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -12,7 +12,9 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { feed := atom.NewFeed(c.Title) feed.ID = atom.NewID("urn:feed:1") feed.Subtitle = atom.NewText("text", c.Description) - feed.AddLink(atom.NewLink(c.Link)) + + linkID := feed.AddLink(atom.NewLink(c.Link)) + feed.Links[linkID].Rel = "self" feed.Generator = atom.NewGenerator("cpolis") feed.Generator.URI = "https://git.streifling.com/jason/cpolis" @@ -31,7 +33,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { entry := atom.NewEntry(articleTitle) entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID)) 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 { entry.Summary = atom.NewText("text", "automatically generated") @@ -44,16 +46,38 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { } 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].Type = "image/webp" } - user, err := db.GetUser(c, article.AuthorID) + authors, err := db.GetArticleAuthors(c, article.ID) 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) if err != nil { diff --git a/cmd/backend/config.go b/cmd/backend/config.go index 8d52a8b..4d97bab 100644 --- a/cmd/backend/config.go +++ b/cmd/backend/config.go @@ -52,7 +52,7 @@ func newConfig() *Config { PDFDir: "/var/www/cpolis/pdfs", PicsDir: "/var/www/cpolis/pics", Port: ":8080", - Version: "v0.13.4", + Version: "v0.14.0", WebDir: "/var/www/cpolis/web", } } diff --git a/cmd/backend/users.go b/cmd/backend/users.go index 0677334..47d76e4 100644 --- a/cmd/backend/users.go +++ b/cmd/backend/users.go @@ -149,11 +149,11 @@ func (db *DB) AddUser(c *Config, u *User, pass string) (int64, error) { } query := ` - INSERT INTO users (username, password, first_name, last_name, email, role) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role) + VALUES (?, ?, ?, ?, ?, ?, ?) ` - result, err := db.Exec(query, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.Role) + result, err := db.Exec(query, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.ProfilePicLink, u.Role) if err != nil { return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err) } @@ -253,13 +253,13 @@ func (db *DB) GetUser(c *Config, id int64) (*User, error) { user := new(User) query := ` - SELECT id, username, first_name, last_name, email, role + SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users WHERE id = ? ` row := db.QueryRow(query, id) - if err := row.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.Role); err != nil { + if err := row.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { return nil, fmt.Errorf("error reading user information: %v", err) } @@ -281,10 +281,10 @@ func (db *DB) GetUser(c *Config, id int64) (*User, error) { return user, nil } -func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, lastName, email, oldPass, newPass string) error { +func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, lastName, email, profilePicLink, oldPass, newPass string) error { var err error tx := new(Tx) - passwordEmpty := len(newPass) > 0 + passwordEmpty := len(newPass) == 0 for i := 0; i < TxMaxRetries; i++ { err := func() error { @@ -331,6 +331,7 @@ func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, &Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, &Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, &Attribute{Table: "users", ID: id, AttName: "email", Value: aesEmail}, + &Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink}, ); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -360,8 +361,8 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} selectQuery := "SELECT COUNT(*) FROM users" insertQuery := ` - INSERT INTO users (username, password, first_name, last_name, email, role) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role) + VALUES (?, ?, ?, ?, ?, ?, ?) ` for i := 0; i < TxMaxRetries; i++ { @@ -416,7 +417,7 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { return 0, fmt.Errorf("error encrypting email: %v", err) } - result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.Role) + result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.ProfilePicLink, u.Role) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -447,11 +448,50 @@ func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } -func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) { +func (db *DB) GetAllUsers(c *Config) ([]*User, error) { var aesFirstName, aesLastName, aesEmail string var err error - query := "SELECT id, username, first_name, last_name, email, role FROM users" + query := "SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users" + + rows, err := db.Query(query) + if err != nil { + return nil, fmt.Errorf("error getting all users from DB: %v", err) + } + + users := make([]*User, 0) + for rows.Next() { + user := new(User) + if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { + return nil, fmt.Errorf("error getting user info: %v", err) + } + + user.FirstName, err = aesDecrypt(c, aesFirstName) + if err != nil { + return nil, fmt.Errorf("error decrypting first name: %v", err) + } + + user.LastName, err = aesDecrypt(c, aesLastName) + if err != nil { + return nil, fmt.Errorf("error decrypting last name: %v", err) + } + + user.Email, err = aesDecrypt(c, aesEmail) + if err != nil { + return nil, fmt.Errorf("error decrypting email: %v", err) + } + + users = append(users, user) + } + + return users, nil +} + +func (db *DB) GetAllUsersMap(c *Config) (map[int64]*User, error) { + var aesFirstName, aesLastName, aesEmail string + var err error + + query := "SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users" rows, err := db.Query(query) if err != nil { @@ -461,7 +501,7 @@ func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) { users := make(map[int64]*User, 0) for rows.Next() { user := new(User) - if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.Role); err != nil { + if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil { return nil, fmt.Errorf("error getting user info: %v", err) } @@ -506,10 +546,10 @@ func (tx *Tx) SetPassword(id int64, newPass string) error { return nil } -func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, lastName, email, newPass string, role int) error { +func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, lastName, email, profilePicLink, newPass string, role int) error { var err error tx := new(Tx) - passwordEmpty := len(newPass) > 0 + passwordEmpty := len(newPass) == 0 for i := 0; i < TxMaxRetries; i++ { err := func() error { @@ -556,6 +596,7 @@ func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, las &Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, &Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, &Attribute{Table: "users", ID: id, AttName: "email", Value: aesEmail}, + &Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink}, &Attribute{Table: "users", ID: id, AttName: "role", Value: role}, ); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { diff --git a/cmd/calls/articles.go b/cmd/calls/articles.go index 1568cdd..4b5d518 100644 --- a/cmd/calls/articles.go +++ b/cmd/calls/articles.go @@ -10,6 +10,27 @@ import ( 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 { return func(w http.ResponseWriter, r *http.Request) { if !tokenIsVerified(w, r, c) { @@ -50,6 +71,41 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { 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 + } } } diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go index 305c26b..7610777 100644 --- a/cmd/frontend/articles.go +++ b/cmd/frontend/articles.go @@ -18,16 +18,31 @@ const ( PreviewMode ) +const ( + None = iota + Author + Contributor +) + +type ArticleUser struct { + *b.User + ArticleRole int +} + type EditorHTMLData struct { Selected map[int64]bool Content string Action string ActionTitle string ActionButton string - BannerImage string + Image string HTMLContent template.HTML Article *b.Article Tags []*b.Tag + ArticleUsers map[string]*ArticleUser // A map is way more efficient in ReviewRejectedArticle() + Creator *ArticleUser + Authors []*b.User + Contributors []*b.User } func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { @@ -41,11 +56,30 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { var data *EditorHTMLData if session.Values["article"] == nil { - data = &EditorHTMLData{Action: "submit", Article: new(b.Article)} + data = &EditorHTMLData{Action: "submit", Article: new(b.Article), ArticleUsers: make(map[string]*ArticleUser)} } else { data = session.Values["article"].(*EditorHTMLData) } + users, err := db.GetAllUsers(c) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for _, user := range users { + data.ArticleUsers[fmt.Sprint(user.LastName, user.FirstName, user.ID)] = &ArticleUser{User: user, ArticleRole: None} + } + + creator, err := db.GetUser(c, session.Values["id"].(int64)) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + data.Creator = data.ArticleUsers[fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)] + delete(data.ArticleUsers, fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)) + data.Tags, err = db.GetTagList() if err != nil { log.Println(err) @@ -80,13 +114,14 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { article := &b.Article{ 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"), + CreatorID: session.Values["id"].(int64), Published: false, Rejected: false, - AuthorID: session.Values["id"].(int64), IsInIssue: r.PostFormValue("issue") == "on", AutoGenerated: false, + EditedID: 0, } if len(article.Title) == 0 { @@ -98,6 +133,38 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } + r.ParseForm() + authors := make([]int64, 0) + contributors := make([]int64, 0) + + for key, values := range r.Form { + if strings.HasPrefix(key, "user-") && len(values) > 0 { + id, err := strconv.ParseInt(strings.Split(key, "-")[1], 10, 64) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + switch values[0] { + case "author": + authors = append(authors, id) + case "contributor": + contributors = append(contributors, id) + } + } + } + + if r.PostFormValue("creator") == "contributor" { + contributors = append(contributors, article.CreatorID) + } else { + authors = append(authors, article.CreatorID) + } + if len(authors) == 0 { + http.Error(w, "Es muss mindestens einen Autor geben.", http.StatusBadRequest) + return + } + article.ID, err = db.AddArticle(article) if err != nil { log.Println(err) @@ -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) return } - - articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") - if err = os.WriteFile(articleAbsName, content, 0644); err != nil { + if err := b.WriteArticleToFile(c, article.ID, content); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID) - if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); err != nil { + if err = db.WriteArticleAuthors(article.ID, authors); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } + if len(contributors) > 0 { + if err = db.WriteArticleContributors(article.ID, contributors); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } - r.ParseForm() tags := make([]int64, 0) for _, tag := range r.Form["tags"] { tagID, err := strconv.ParseInt(tag, 10, 64) if err != nil { log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } + tags = append(tags, tagID) } if err = db.WriteArticleTags(article.ID, tags); err != nil { @@ -164,27 +235,59 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return + article := &b.Article{ + Title: r.PostFormValue("article-title"), + BannerLink: r.PostFormValue("article-banner-url"), + Summary: r.PostFormValue("article-summary"), + CreatorID: session.Values["id"].(int64), + IsInIssue: r.PostFormValue("issue") == "on", } - title := r.PostFormValue("article-title") - if len(title) == 0 { + if len(article.Title) == 0 { http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest) return } - - bannerLink := r.PostFormValue("article-banner-url") - if len(bannerLink) != 0 { - bannerLink = c.Domain + "/image/serve/" + bannerLink + if len(article.Summary) == 0 { + http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) + return } - summary := r.PostFormValue("article-summary") - if len(summary) == 0 { - http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest) + 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 } @@ -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) return } - - contentLink := fmt.Sprint(c.ArticleDir, "/", id, ".md") + contentLink := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md") if err = os.WriteFile(contentLink, []byte(content), 0644); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -202,18 +304,30 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } if err = db.UpdateAttributes( - &b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, - &b.Attribute{Table: "articles", ID: id, AttName: "banner_link", Value: bannerLink}, - &b.Attribute{Table: "articles", ID: id, AttName: "summary", Value: summary}, - &b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, - &b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "title", Value: article.Title}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "banner_link", Value: article.BannerLink}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "summary", Value: article.Summary}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "rejected", Value: false}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "is_in_issue", Value: article.IsInIssue}, ); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - r.ParseForm() + if err = db.UpdateArticleAuthors(article.ID, authors); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if len(contributors) > 0 { + if err = db.UpdateArticleContributors(article.ID, contributors); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + tags := make([]int64, 0) for _, tag := range r.Form["tags"] { tagID, err := strconv.ParseInt(tag, 10, 64) @@ -224,7 +338,7 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } tags = append(tags, tagID) } - if err = db.UpdateArticleTags(id, tags); err != nil { + if err = db.UpdateArticleTags(article.ID, tags); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -310,7 +424,7 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF data.MyIDs = make(map[int64]bool) for _, article := range data.RejectedArticles { - if article.AuthorID == session.Values["id"].(int64) { + if article.CreatorID == session.Values["id"].(int64) { data.MyIDs[article.ID] = true } } @@ -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 { return func(w http.ResponseWriter, r *http.Request) { - if _, err := GetSession(w, r, c, s); err != nil { + session, err := GetSession(w, r, c, s) + if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -348,8 +463,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler return } - imgURL := strings.Split(data.Article.BannerLink, "/") - data.BannerImage = imgURL[len(imgURL)-1] + data.Image = data.Article.BannerLink articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") content, err := os.ReadFile(articleAbsName) @@ -367,6 +481,46 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler return } + data.ArticleUsers = make(map[string]*ArticleUser) + users, err := db.GetAllUsers(c) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for _, user := range users { + data.ArticleUsers[fmt.Sprint(user.LastName, user.FirstName, user.ID)] = &ArticleUser{User: user, ArticleRole: None} + } + + authors, err := db.GetArticleAuthors(c, data.Article.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for _, author := range authors { + data.ArticleUsers[fmt.Sprint(author.LastName, author.FirstName, author.ID)].ArticleRole = Author + } + + contributors, err := db.GetArticleContributors(c, data.Article.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for _, contributor := range contributors { + data.ArticleUsers[fmt.Sprint(contributor.LastName, contributor.FirstName, contributor.ID)].ArticleRole = Contributor + } + + creator, err := db.GetUser(c, session.Values["id"].(int64)) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + data.Creator = data.ArticleUsers[fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)] + delete(data.ArticleUsers, fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)) + selectedTags, err := db.GetArticleTags(id) if err != nil { log.Println(err) @@ -420,9 +574,9 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } if err = db.UpdateAttributes( - &b.Attribute{Table: "articles", ID: id, AttName: "published", Value: true}, - &b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, - &b.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "published", Value: true}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "rejected", Value: false}, + &b.Attribute{Table: "articles", ID: article.ID, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")}, ); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -449,10 +603,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.UpdateAttributes( - &b.Attribute{Table: "articles", ID: id, AttName: "content_link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)}, - &b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0}, - ); err != nil { + if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "edited_id", Value: 0}); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -619,8 +770,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto return } - imgURL := strings.Split(article.BannerLink, "/") - data.BannerImage = imgURL[len(imgURL)-1] + data.Image = article.BannerLink data.Article.Summary, err = b.ConvertToPlain(article.Summary) 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.Authors, err = db.GetArticleAuthors(c, data.Article.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + sortUsersByName(data.Authors) + + data.Contributors, err = db.GetArticleContributors(c, data.Article.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + sortUsersByName(data.Contributors) + data.Tags, err = db.GetArticleTags(id) if err != nil { log.Println(err) @@ -736,25 +902,59 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc return } - newArticle := oldArticle + newArticle := *oldArticle newArticle.Published = false newArticle.Rejected = true newArticle.EditedID = oldArticle.ID - newID, err := db.AddArticle(newArticle) + newArticle.ID, err = db.AddArticle(&newArticle) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldID, AttName: "edited_id", Value: newID}); err != nil { + if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldArticle.ID, AttName: "edited_id", Value: newArticle.ID}); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - if err = b.CopyFile(fmt.Sprint(c.ArticleDir, "/", oldID, ".md"), fmt.Sprint(c.ArticleDir, "/", newID, ".md")); err != nil { + src := fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md") + dst := fmt.Sprint(c.ArticleDir, "/", newArticle.ID, ".md") + if err = b.CopyFile(src, dst); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + authors, err := db.GetArticleAuthors(c, oldArticle.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + authorIDs := make([]int64, len(authors)) + for i, author := range authors { + authorIDs[i] = author.ID + } + if err = db.WriteArticleAuthors(newArticle.ID, authorIDs); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + contributors, err := db.GetArticleContributors(c, oldArticle.ID) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + contributorIDs := make([]int64, len(contributors)) + for i, contributor := range contributors { + contributorIDs[i] = contributor.ID + } + if err = db.WriteArticleContributors(newArticle.ID, contributorIDs); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -796,8 +996,7 @@ func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - imgURL := strings.Split(data.Article.BannerLink, "/") - data.BannerImage = imgURL[len(imgURL)-1] + data.Image = data.Article.BannerLink content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")) if err != nil { diff --git a/cmd/frontend/images.go b/cmd/frontend/images.go index d6aecbe..079929c 100644 --- a/cmd/frontend/images.go +++ b/cmd/frontend/images.go @@ -9,7 +9,7 @@ import ( b "streifling.com/jason/cpolis/cmd/backend" ) -func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { +func UploadEasyMDEImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if _, err := GetSession(w, r, c, s); err != nil { 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) { if _, err := GetSession(w, r, c, s); err != nil { log.Println(err) @@ -69,8 +69,8 @@ func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate return } - data := new(struct{ BannerImage string }) - data.BannerImage = filename + data := new(struct{ Image string }) + data.Image = filename tmpl, err := template.ParseFiles(c.WebDir + "/templates/" + htmlFile) if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil { diff --git a/cmd/frontend/issues.go b/cmd/frontend/issues.go index a3f3ea4..171ac9d 100644 --- a/cmd/frontend/issues.go +++ b/cmd/frontend/issues.go @@ -33,7 +33,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun Published: true, Rejected: false, Created: time.Now(), - AuthorID: session.Values["id"].(int64), AutoGenerated: true, } @@ -49,6 +48,22 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun return } + authorIDs := make([]int64, 1) + var ok bool + + if authorIDs[0], ok = session.Values["id"].(int64); !ok { + msg := "fälschlicherweise session.Values[\"id\"].(int64) für authorIDs[0] angenommen" + log.Println(msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + if err = db.WriteArticleAuthors(article.ID, authorIDs); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + content := []byte(r.PostFormValue("issue-content")) if len(content) == 0 { http.Error(w, "Bitte eine Beschreibung eingeben.", http.StatusBadRequest) @@ -62,19 +77,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun return } - article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID) - if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if err = db.AddArticleToCurrentIssue(article.ID); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cmd/frontend/sessions.go b/cmd/frontend/sessions.go index ffef532..788a484 100644 --- a/cmd/frontend/sessions.go +++ b/cmd/frontend/sessions.go @@ -57,15 +57,21 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } data := new(struct { + *UserHTMLData Version string - Role int }) + data.UserHTMLData = &UserHTMLData{User: new(b.User)} data.Version = c.Version files := make([]string, 2) files[0] = c.WebDir + "/templates/index.html" if numRows == 0 { - files[1] = c.WebDir + "/templates/first-user.html" + data.Role = b.NonExistent + data.Title = "Erster Benutzer (Administrator)" + data.ButtonText = "Anlegen" + data.URL = "/user/add-first" + + files[1] = c.WebDir + "/templates/edit-user.html" tmpl, err := template.ParseFiles(files...) if err = template.Must(tmpl, err).Execute(w, data); err != nil { log.Println(err) @@ -79,6 +85,7 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { http.Error(w, err.Error(), http.StatusInternalServerError) return } + if auth, ok := session.Values["authenticated"].(bool); auth && ok { data.Role = session.Values["role"].(int) files[1] = c.WebDir + "/templates/hub.html" @@ -89,6 +96,7 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } } else { + data.Role = b.Author files[1] = c.WebDir + "/templates/login.html" tmpl, err := template.ParseFiles(files...) if err = template.Must(tmpl, err).Execute(w, data); err != nil { diff --git a/cmd/frontend/users.go b/cmd/frontend/users.go index e3aed55..39447a5 100644 --- a/cmd/frontend/users.go +++ b/cmd/frontend/users.go @@ -5,11 +5,20 @@ import ( "html/template" "log" "net/http" + "sort" "strconv" b "streifling.com/jason/cpolis/cmd/backend" ) +type UserHTMLData struct { + *b.User + Title string + ButtonText string + URL string + Image string +} + func checkUserStrings(user *b.User) (string, int, bool) { userLen := 63 // max value for utf-8 at 255 bytes nameLen := 56 // max value when aes encrypting utf-8 at up to 255 bytes @@ -25,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 { return func(w http.ResponseWriter, r *http.Request) { if _, err := GetSession(w, r, c, s); err != nil { @@ -33,8 +51,15 @@ func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc { return } - tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html") - if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil { + data := &UserHTMLData{ + User: &b.User{Role: b.Author}, + Title: "Neuer Benutzer", + ButtonText: "Anlegen", + URL: "/user/add", + } + + tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") + if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -52,10 +77,11 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } user := &b.User{ - UserName: r.PostFormValue("username"), - FirstName: r.PostFormValue("first-name"), - LastName: r.PostFormValue("last-name"), - Email: r.PostFormValue("email"), + UserName: r.PostFormValue("username"), + FirstName: r.PostFormValue("first-name"), + LastName: r.PostFormValue("last-name"), + Email: r.PostFormValue("email"), + ProfilePicLink: r.PostFormValue("profile-pic-url"), } pass := r.PostFormValue("password") pass2 := r.PostFormValue("password2") @@ -136,8 +162,16 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-self.html") - if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil { + data := &UserHTMLData{ + User: user, + Title: "Mein Profil bearbeiten", + ButtonText: "Übernehmen", + URL: "/user/update/self", + Image: user.ProfilePicLink, + } + + tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") + if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -155,11 +189,12 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { } user := &b.User{ - ID: session.Values["id"].(int64), - UserName: r.PostFormValue("username"), - FirstName: r.PostFormValue("first-name"), - LastName: r.PostFormValue("last-name"), - Email: r.PostFormValue("email"), + ID: session.Values["id"].(int64), + UserName: r.PostFormValue("username"), + FirstName: r.PostFormValue("first-name"), + LastName: r.PostFormValue("last-name"), + Email: r.PostFormValue("email"), + ProfilePicLink: r.PostFormValue("profile-pic-url"), } oldPass := r.PostFormValue("old-password") @@ -202,7 +237,7 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.UpdateOwnUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, oldPass, newPass); err != nil { + if err = db.UpdateOwnUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, user.ProfilePicLink, oldPass, newPass); err != nil { log.Println("error: user:", user.ID, err) http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) return @@ -225,11 +260,12 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var err error user := &b.User{ - UserName: r.PostFormValue("username"), - FirstName: r.PostFormValue("first-name"), - LastName: r.PostFormValue("last-name"), - Email: r.PostFormValue("email"), - Role: b.Admin, + UserName: r.PostFormValue("username"), + FirstName: r.PostFormValue("first-name"), + LastName: r.PostFormValue("last-name"), + Email: r.PostFormValue("email"), + ProfilePicLink: r.PostFormValue("profile-pic-url"), + Role: b.Admin, } pass := r.PostFormValue("password") pass2 := r.PostFormValue("password2") @@ -306,14 +342,14 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H }) data.Action = action - data.Users, err = db.GetAllUsers(c) + data.Users, err = db.GetAllUsersMap(c) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - delete(data.Users, session.Values["id"].(int64)) + tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html") if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { log.Println(err) @@ -345,8 +381,16 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } + data := &UserHTMLData{ + User: user, + Title: "Profil von " + user.FirstName + " " + user.LastName + " bearbeiten", + ButtonText: "Übernehmen", + URL: fmt.Sprint("/user/update/", user.ID), + Image: user.ProfilePicLink, + } + tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html") - if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil { + if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -402,6 +446,8 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } + user.ProfilePicLink = r.PostFormValue("profile-pic-url") + newPass := r.PostFormValue("password") newPass2 := r.PostFormValue("password2") if newPass != newPass2 { @@ -420,7 +466,7 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.UpdateUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, newPass, user.Role); err != nil { + if err = db.UpdateUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, user.ProfilePicLink, newPass, user.Role); err != nil { log.Println("error: user:", user.ID, err) http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) return diff --git a/cmd/main.go b/cmd/main.go index e2f05db..c10ae2f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -65,6 +65,7 @@ func main() { 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/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 /atom/serve", c.ServeAtomFeed(config)) 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/submit", f.SubmitArticle(config, db, store)) - mux.HandleFunc("POST /article/upload-banner", f.UploadBanner(config, store, "article-banner", "editor.html", "article-banner-template")) - mux.HandleFunc("POST /article/upload-image", f.UploadImage(config, store)) + mux.HandleFunc("POST /article/upload-banner", f.UploadImage(config, store, "article-banner", "editor.html", "article-banner-template")) + mux.HandleFunc("POST /article/upload-image", f.UploadEasyMDEImage(config, store)) mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store)) - mux.HandleFunc("POST /issue/upload-banner", f.UploadBanner(config, store, "issue-banner", "current-issue.html", "issue-banner-template")) + mux.HandleFunc("POST /issue/upload-banner", f.UploadImage(config, store, "issue-banner", "current-issue.html", "issue-banner-template")) mux.HandleFunc("POST /login", f.Login(config, db, store)) mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store)) mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store)) @@ -94,6 +95,7 @@ func main() { mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store)) mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, store)) mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, store)) + mux.HandleFunc("POST /user/upload-profile-pic", f.UploadImage(config, store, "upload-profile-pic", "edit-user.html", "profile-pic-template")) log.Fatalln(http.ListenAndServe(config.Port, mux)) } diff --git a/create_db.sql b/create_db.sql index 44df846..a79e89c 100644 --- a/create_db.sql +++ b/create_db.sql @@ -1,55 +1,73 @@ DROP TABLE IF EXISTS articles_tags; +DROP TABLE IF EXISTS articles_contributors; +DROP TABLE IF EXISTS articles_authors; DROP TABLE IF EXISTS tags; DROP TABLE IF EXISTS articles; DROP TABLE IF EXISTS issues; DROP TABLE IF EXISTS users; CREATE TABLE users ( - id INT AUTO_INCREMENT, - username VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(60) NOT NULL, - first_name VARCHAR(255) NOT NULL, - last_name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, - -- profile_pic_link VARCHAR(255) NOT NULL, - role INT NOT NULL, + id INT AUTO_INCREMENT, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(60) NOT NULL, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + profile_pic_link VARCHAR(255), + role INT NOT NULL, PRIMARY KEY (id) ); CREATE TABLE issues ( - id INT AUTO_INCREMENT, - published BOOL NOT NULL, + id INT AUTO_INCREMENT, + published BOOL NOT NULL, PRIMARY KEY (id) ); CREATE TABLE articles ( - id INT AUTO_INCREMENT, - title VARCHAR(255) NOT NULL, - created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - banner_link VARCHAR(255) NOT NULL, - summary TEXT NOT NULL, - content_link VARCHAR(255) NOT NULL, - published BOOL NOT NULL, - rejected BOOL NOT NULL, - author_id INT NOT NULL, - issue_id INT NOT NULL, - edited_id INT, - is_in_issue BOOL NOT NULL, - auto_generated BOOL NOT NULL, + id INT AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + banner_link VARCHAR(255), + summary TEXT NOT NULL, + published BOOL NOT NULL, + rejected BOOL NOT NULL, + creator_id INT NOT NULL, + issue_id INT NOT NULL, + edited_id INT NOT NULL, + clicks INT NOT NULL, + is_in_issue BOOL NOT NULL, + auto_generated BOOL NOT NULL, PRIMARY KEY (id), - FOREIGN KEY (author_id) REFERENCES users (id), + FOREIGN KEY (creator_id) REFERENCES users (id), FOREIGN KEY (issue_id) REFERENCES issues (id) ); CREATE TABLE tags ( - id INT AUTO_INCREMENT, - name VARCHAR(50) NOT NULL UNIQUE, + id INT AUTO_INCREMENT, + name VARCHAR(50) NOT NULL UNIQUE, PRIMARY KEY (id) ); +CREATE TABLE articles_authors ( + article_id INT NOT NULL, + author_id INT NOT NULL, + PRIMARY KEY (article_id, author_id), + FOREIGN KEY (article_id) REFERENCES articles (id), + FOREIGN KEY (author_id) REFERENCES users (id) +); + +CREATE TABLE articles_contributors ( + article_id INT NOT NULL, + contributor_id INT NOT NULL, + PRIMARY KEY (article_id, contributor_id), + FOREIGN KEY (article_id) REFERENCES articles (id), + FOREIGN KEY (contributor_id) REFERENCES users (id) +); + CREATE TABLE articles_tags ( - article_id INT, - tag_id INT, + article_id INT NOT NULL, + tag_id INT NOT NULL, PRIMARY KEY (article_id, tag_id), FOREIGN KEY (article_id) REFERENCES articles (id), FOREIGN KEY (tag_id) REFERENCES tags (id) diff --git a/web/static/css/input.css b/web/static/css/input.css index b7778b5..b7475d6 100644 --- a/web/static/css/input.css +++ b/web/static/css/input.css @@ -24,7 +24,7 @@ textarea { } .btn-area { - @apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4; + @apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1; } .btn-area-3 { @@ -40,33 +40,33 @@ textarea { } .EasyMDEContainer .CodeMirror { - @apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100 + @apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100; } .EasyMDEContainer .cm-s-easymde .CodeMirror-cursor { - @apply border-slate-900 dark:border-slate-100 + @apply border-slate-900 dark:border-slate-100; } .EasyMDEContainer .editor-toolbar > * { - @apply text-slate-900 dark:text-slate-100 + @apply text-slate-900 dark:text-slate-100; } .EasyMDEContainer .editor-toolbar > .active, .editor-toolbar > button:hover, .editor-preview pre, .cm-s-easymde .cm-comment { - @apply bg-slate-100 dark:bg-slate-900 + @apply bg-slate-100 dark:bg-slate-900; } .EasyMDEContainer .CodeMirror-fullscreen { - @apply bg-slate-50 dark:bg-slate-950 + @apply bg-slate-50 dark:bg-slate-950; } .editor-toolbar { - @apply border border-slate-200 dark:border-slate-800 + @apply border border-slate-200 dark:border-slate-800; } .editor-toolbar.fullscreen { - @apply bg-slate-50 dark:bg-slate-950 + @apply bg-slate-50 dark:bg-slate-950; } .editor-preview { - @apply bg-slate-50 dark:bg-slate-950 + @apply bg-slate-50 dark:bg-slate-950; } diff --git a/web/templates/add-user.html b/web/templates/add-user.html deleted file mode 100644 index 013b8eb..0000000 --- a/web/templates/add-user.html +++ /dev/null @@ -1,66 +0,0 @@ -{{define "page-content"}} -

Neuer Benutzer

- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-{{end}} diff --git a/web/templates/current-issue.html b/web/templates/current-issue.html index 0c570de..54f0c05 100644 --- a/web/templates/current-issue.html +++ b/web/templates/current-issue.html @@ -79,8 +79,8 @@ {{end}} {{define "issue-banner-template"}} -
- Banner Image - +
+ +
{{end}} diff --git a/web/templates/edit-self.html b/web/templates/edit-self.html deleted file mode 100644 index c5de034..0000000 --- a/web/templates/edit-self.html +++ /dev/null @@ -1,53 +0,0 @@ -{{define "page-content"}} -

Profil bearbeiten

- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
- - -
-
-{{end}} diff --git a/web/templates/edit-user.html b/web/templates/edit-user.html index 6725f2a..2366b31 100644 --- a/web/templates/edit-user.html +++ b/web/templates/edit-user.html @@ -1,67 +1,90 @@ {{define "page-content"}} -

Profil von {{.FirstName}} {{.LastName}} bearbeiten

+

{{.Title}}

+ +
+
+ {{template "profile-pic-template" .}} - -
- -
- -
- - -
- -
- - +
- +
- + +
+ +
+ + +
+ +
+ +
- +
- +
+ {{if lt .Role 4}}
- +
- +
- +
- +
+ {{end}}
- +
{{end}} + +{{define "profile-pic-template"}} +
+ + + +
+{{end}} diff --git a/web/templates/editor.html b/web/templates/editor.html index 5ccd247..81cd163 100644 --- a/web/templates/editor.html +++ b/web/templates/editor.html @@ -1,17 +1,17 @@ {{define "page-content"}}

Editor

-
+
{{template "article-banner-template" .}}
- +

Titel

-
+
@@ -19,20 +19,20 @@
-
- +
+

Beschreibung

-
- +
+

Artikel

- Tags -
+

Tags

+
@@ -48,6 +48,38 @@
+
+

Beteiligte

+ {{range .ArticleUsers}} +
+ {{.FirstName}} {{.LastName}}: + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{end}} +
+ +
+ + +
+
@@ -90,7 +122,7 @@ {{define "article-banner-template"}}
- - + +
{{end}} diff --git a/web/templates/first-user.html b/web/templates/first-user.html deleted file mode 100644 index 7019acc..0000000 --- a/web/templates/first-user.html +++ /dev/null @@ -1,46 +0,0 @@ -{{define "page-content"}} -

Erster Benutzer (Administrator)

- - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
- -
- -{{end}} diff --git a/web/templates/review-article.html b/web/templates/review-article.html index 1266fd1..2c225e7 100644 --- a/web/templates/review-article.html +++ b/web/templates/review-article.html @@ -3,27 +3,27 @@
- Banner Image + Banner Image
- Titel +

Titel

{{.Article.Title}}
- Beschreibung +

Beschreibung

{{.Article.Summary}}
- Artikel +

Artikel

{{.HTMLContent}}
- Tags +

Tags

{{if .Article.IsInIssue}} Orient Express @@ -35,6 +35,22 @@ {{end}}
+

Autoren

+
+ {{range .Authors}} + {{.FirstName}} {{.LastName}} +
+ {{end}} +
+ +

Mitwirkende

+
+ {{range .Contributors}} + {{.FirstName}} {{.LastName}} +
+ {{end}} +
+ {{if eq .Action "publish"}}