diff --git a/cmd/main.go b/cmd/main.go index 16ecaeb..7838450 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -50,6 +50,7 @@ func main() { mux.HandleFunc("GET /edit-user/", view.EditUser(db, store)) mux.HandleFunc("GET /hub/", view.ShowHub(db, store)) mux.HandleFunc("GET /logout/", view.Logout(store)) + mux.HandleFunc("GET /publish-issue/", view.PublishLatestIssue(db)) mux.HandleFunc("GET /rejected-articles/", view.ShowRejectedArticles(db, store)) mux.HandleFunc("GET /rss/", view.ShowRSS( db, @@ -57,6 +58,7 @@ func main() { "https://distrikt-ni-st.de", "Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität", )) + mux.HandleFunc("GET /this-issue/", view.ShowCurrentArticles(db)) mux.HandleFunc("GET /unpublished-articles/", view.ShowUnpublishedArticles(db)) mux.HandleFunc("GET /write-article/", view.WriteArticle(db)) diff --git a/cmd/model/articles.go b/cmd/model/articles.go index 4866bcb..8e60e35 100644 --- a/cmd/model/articles.go +++ b/cmd/model/articles.go @@ -1,7 +1,10 @@ package model import ( + "context" + "database/sql" "fmt" + "log" "time" ) @@ -14,26 +17,64 @@ type Article struct { Rejected bool ID int64 AuthorID int64 + IssueID int64 } func (db *DB) AddArticle(a *Article) (int64, error) { - query := ` + var id int64 + txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} + selectQuery := "SELECT id FROM issues WHERE published = false" + insertQuery := ` INSERT INTO articles - (title, description, content, published, rejected, author_id) - VALUES (?, ?, ?, ?, ?, ?) + (title, description, content, published, rejected, author_id, issue_id) + VALUES (?, ?, ?, ?, ?, ?, ?) ` - result, err := db.Exec(query, a.Title, a.Description, a.Content, - a.Published, a.Rejected, a.AuthorID) - if err != nil { - return 0, fmt.Errorf("error inserting article into DB: %v", err) - } - id, err := result.LastInsertId() - if err != nil { - return 0, fmt.Errorf("error retrieving last ID: %v", err) + for i := 0; i < TxMaxRetries; i++ { + id, err := func() (int64, error) { + tx, err := db.BeginTx(context.Background(), txOptions) + if err != nil { + return 0, fmt.Errorf("error starting transaction: %v", err) + } + + if err = tx.QueryRow(selectQuery).Scan(&id); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) + } + + result, err := tx.Exec(insertQuery, a.Title, a.Description, + a.Content, a.Published, a.Rejected, a.AuthorID, id) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return 0, fmt.Errorf("error inserting article into DB: %v", err) + } + + id, err := result.LastInsertId() + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return 0, fmt.Errorf("error retrieving ID of added article: %v", err) + } + + if err = tx.Commit(); err != nil { + return 0, fmt.Errorf("error committing transaction when adding article to DB: %v", err) + } + return id, nil + }() + if err == nil { + return id, nil + } + + log.Println(err) + wait(i) } - return id, nil + return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } func (db *DB) GetArticle(id int64) (*Article, error) { @@ -79,8 +120,8 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { article := new(Article) var created []byte - if err = rows.Scan(&article.ID, &article.Title, &created, &article.Description, - &article.Content, &article.AuthorID); err != nil { + if err = rows.Scan(&article.ID, &article.Title, &created, + &article.Description, &article.Content, &article.AuthorID); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -95,3 +136,122 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { return articleList, nil } + +func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { + var issueID int64 + txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} + issueQuery := "SELECT id FROM issues WHERE published = false" + articlesQuery := ` + SELECT id, title, created, description, content, author_id + FROM articles + WHERE issue_id = ? AND published = true + ` + + for i := 0; i < TxMaxRetries; i++ { + id, err := func() ([]*Article, error) { + tx, err := db.BeginTx(context.Background(), txOptions) + if err != nil { + return nil, fmt.Errorf("error starting transaction: %v", err) + } + + row := tx.QueryRow(issueQuery) + if err := row.Scan(&issueID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return nil, fmt.Errorf("error querying DB for unpublished issue: %v", err) + } + + rows, err := tx.Query(articlesQuery, issueID) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return nil, fmt.Errorf("error querying DB for articles of issue %v: %v", issueID, err) + } + + articleList := make([]*Article, 0) + for rows.Next() { + article := new(Article) + var created []byte + + if err = rows.Scan(&article.ID, &article.Title, &created, + &article.Description, &article.Content, &article.AuthorID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return nil, fmt.Errorf("error scanning article from issue %v: %v", issueID, err) + } + + article.Created, err = time.Parse("2006-01-02 15:04:05", string(created)) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return nil, fmt.Errorf("error parsing created: %v", err) + } + + articleList = append(articleList, article) + } + + if err = tx.Commit(); err != nil { + return nil, fmt.Errorf("error committing transaction when getting articles of issue %v: %v", issueID, err) + } + + return articleList, nil + }() + if err == nil { + return id, nil + } + + log.Println(err) + wait(i) + } + + return nil, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) +} + +func (db *DB) AddArticleToCurrentIssue(id int64) error { + var issueID int64 + txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} + selectQuery := "SELECT id FROM issues WHERE published = false" + updateQuery := "UPDATE articles SET issue_id = ? WHERE id = ?" + + for i := 0; i < TxMaxRetries; i++ { + err := func() error { + tx, err := db.BeginTx(context.Background(), txOptions) + if err != nil { + return fmt.Errorf("error starting transaction: %v", err) + } + + if err = tx.QueryRow(selectQuery).Scan(&issueID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error scanning row: %v", err) + } + + _, err = db.Exec(updateQuery, issueID, id) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error updating issueID for article: %v", err) + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("error committing transaction when getting articles of issue %v: %v", issueID, 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/model/articles_tags.go b/cmd/model/articles_tags.go index 9dcc280..4cfd095 100644 --- a/cmd/model/articles_tags.go +++ b/cmd/model/articles_tags.go @@ -3,12 +3,14 @@ package model import ( "fmt" "log" - "math" - "math/rand/v2" - "time" ) func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error { + query := ` + INSERT INTO articles_tags (article_id, tag_id) + VALUES (?, ?) + ` + for i := 0; i < TxMaxRetries; i++ { err := func() error { tx, err := db.Begin() @@ -17,13 +19,9 @@ func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error { } for _, tagID := range tagIDs { - query := ` - INSERT INTO articles_tags (article_id, tag_id) - VALUES (?, ?) - ` if _, err := tx.Exec(query, articleID, tagID); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr) + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } return fmt.Errorf("error inserting into articles_tags: %v", err) } @@ -39,9 +37,7 @@ func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error { } log.Println(err) - waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second - jitter := time.Duration(rand.IntN(1000)) * time.Millisecond - time.Sleep(waitTime + jitter) + wait(i) } return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } @@ -70,3 +66,29 @@ func (db *DB) GetArticleTags(articleID int64) ([]*Tag, error) { return tags, nil } + +func (db *DB) UpdateArticleTags(articleID int64, tagIDs []int64) error { + query := ` + ` + + 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.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/model/issues.go b/cmd/model/issues.go new file mode 100644 index 0000000..dc73944 --- /dev/null +++ b/cmd/model/issues.go @@ -0,0 +1,66 @@ +package model + +import ( + "context" + "database/sql" + "fmt" + "log" +) + +func (db *DB) AddIssue() (int64, error) { + query := "INSERT INTO issues (published) VALUES (?)" + result, err := db.Exec(query, false) + if err != nil { + return 0, fmt.Errorf("error inserting issue into DB: %v", err) + } + + id, err := result.LastInsertId() + if err != nil { + return 0, fmt.Errorf("error getting ID of added issue: %v", err) + } + + return id, nil +} + +func (db *DB) PublishLatestIssue() error { + var id int64 + txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} + updateQuery := "UPDATE issues SET published = true WHERE published = false" + insertQuery := "INSERT INTO issues (published) VALUES (?)" + + for i := 0; i < TxMaxRetries; i++ { + err := func() error { + tx, err := db.BeginTx(context.Background(), txOptions) + if err != nil { + return fmt.Errorf("error starting transaction: %v", err) + } + + if _, err := tx.Exec(updateQuery, id); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error publishing issue: %v", err) + } + + if _, err := tx.Exec(insertQuery, false); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error inserting new issue into DB: %v", err) + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("error committing transaction when publishing issue: %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/model/users.go b/cmd/model/users.go index 1f2b4b9..e407db7 100644 --- a/cmd/model/users.go +++ b/cmd/model/users.go @@ -3,17 +3,15 @@ package model import ( "fmt" "log" - "math" - "math/rand/v2" - "time" "golang.org/x/crypto/bcrypt" ) const ( Admin = iota + Publisher Editor - Writer + Author ) type User struct { @@ -24,21 +22,26 @@ type User struct { Role int } -func (db *DB) AddUser(user *User, pass string) error { +func (db *DB) AddUser(u *User, pass string) (int64, error) { hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) if err != nil { - return fmt.Errorf("error creating password hash: %v", err) + return 0, fmt.Errorf("error creating password hash: %v", err) } query := ` INSERT INTO users (username, password, first_name, last_name, role) VALUES (?, ?, ?, ?, ?) ` - if _, err = db.Exec(query, user.UserName, string(hashedPass), user.FirstName, user.LastName, user.Role); err != nil { - return fmt.Errorf("error inserting user into DB: %v", err) + result, err := db.Exec(query, u.UserName, string(hashedPass), u.FirstName, u.LastName, u.Role) + if err != nil { + return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err) + } + id, err := result.LastInsertId() + if err != nil { + return 0, fmt.Errorf("error inserting user into DB: %v", err) } - return nil + return id, nil } func (db *DB) GetID(userName string) (int64, bool) { @@ -87,14 +90,14 @@ func (tx *Tx) ChangePassword(id int64, oldPass, newPass string) error { row := tx.QueryRow(getQuery, id) if err := row.Scan(&queriedPass); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr) + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } return fmt.Errorf("error reading password from DB: %v", err) } if err := bcrypt.CompareHashAndPassword([]byte(queriedPass), []byte(oldPass)); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr) + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } return fmt.Errorf("incorrect password: %v", err) } @@ -102,7 +105,7 @@ func (tx *Tx) ChangePassword(id int64, oldPass, newPass string) error { newHashedPass, err := bcrypt.GenerateFromPassword([]byte(newPass), bcrypt.DefaultCost) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr) + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } return fmt.Errorf("error creating password hash: %v", err) } @@ -114,7 +117,7 @@ func (tx *Tx) ChangePassword(id int64, oldPass, newPass string) error { ` if _, err = tx.Exec(setQuery, string(newHashedPass), id); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr) + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } return fmt.Errorf("error updating password in DB: %v", err) } @@ -132,7 +135,8 @@ func (db *DB) GetUser(id int64) (*User, error) { ` row := db.QueryRow(query, id) - if err := row.Scan(&user.ID, &user.UserName, &user.FirstName, &user.LastName, &user.Role); err != nil { + if err := row.Scan(&user.ID, &user.UserName, &user.FirstName, + &user.LastName, &user.Role); err != nil { return nil, fmt.Errorf("error reading user information: %v", err) } @@ -161,7 +165,7 @@ func (db *DB) UpdateUserAttributes(id int64, user, first, last, oldPass, newPass if !passwordEmpty { if err = tx.ChangePassword(id, oldPass, newPass); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr) + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } return fmt.Errorf("error changing password: %v", err) } @@ -173,7 +177,7 @@ func (db *DB) UpdateUserAttributes(id int64, user, first, last, oldPass, newPass &Attribute{Table: "users", ID: id, AttName: "last_name", Value: last}, ); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr) + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } return fmt.Errorf("error updating attributes in DB: %v", err) } @@ -189,9 +193,7 @@ func (db *DB) UpdateUserAttributes(id int64, user, first, last, oldPass, newPass } log.Println(err) - waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second - jitter := time.Duration(rand.IntN(1000)) * time.Millisecond - time.Sleep(waitTime + jitter) + wait(i) } return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) diff --git a/cmd/view/articles.go b/cmd/view/articles.go index a69b78d..96a6b74 100644 --- a/cmd/view/articles.go +++ b/cmd/view/articles.go @@ -272,6 +272,12 @@ func PublishArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc { template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg) } + if err = db.AddArticleToCurrentIssue(id); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err = db.UpdateAttributes( &model.Attribute{Table: "articles", ID: id, AttName: "published", Value: true}, &model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, @@ -317,3 +323,30 @@ func RejectArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc { tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) } } + +func ShowCurrentArticles(db *model.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + articles, err := db.GetCurrentIssueArticles() + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tmpl, err := template.ParseFiles("web/templates/current-articles.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles) + } +} + +func PublishLatestIssue(db *model.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := db.PublishLatestIssue(); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tmpl, err := template.ParseFiles("web/templates/hub.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) + } +} diff --git a/cmd/view/rss.go b/cmd/view/rss.go index a97775a..c6429e6 100644 --- a/cmd/view/rss.go +++ b/cmd/view/rss.go @@ -1,6 +1,7 @@ package view import ( + "fmt" "html/template" "log" "net/http" @@ -38,6 +39,7 @@ func ShowRSS(db *model.DB, title, link, desc string) http.HandlerFunc { for _, tag := range tags { tagNames = append(tagNames, tag.Name) } + tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID)) user, err := db.GetUser(article.AuthorID) if err != nil { diff --git a/cmd/view/users.go b/cmd/view/users.go index 3403eeb..ef54226 100644 --- a/cmd/view/users.go +++ b/cmd/view/users.go @@ -86,19 +86,21 @@ func AddUser(db *model.DB, s *control.CookieStore) http.HandlerFunc { return } - num, err := db.CountEntries("users") + htmlData.ID, err = db.AddUser(htmlData.User, pass) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - if num == 0 { - if htmlData.Role != model.Admin { - htmlData.Msg = "Der erste Benutzer muss ein Administrator sein." - htmlData.Role = model.Admin - tmpl, err := template.ParseFiles("web/templates/add-user.html") - tmpl = template.Must(tmpl, err) - tmpl.ExecuteTemplate(w, "page-content", htmlData) + + if htmlData.ID == 1 { + htmlData.Role = model.Admin + + if err = db.UpdateAttributes( + &model.Attribute{Table: "users", ID: id, AttName: "role", Value: htmlData.Role}, + ); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -107,12 +109,12 @@ func AddUser(db *model.DB, s *control.CookieStore) http.HandlerFunc { http.Error(w, err.Error(), http.StatusInternalServerError) return } - } - if err := db.AddUser(htmlData.User, pass); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return + if _, err := db.AddIssue(); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } tmpl, err := template.ParseFiles("web/templates/hub.html") diff --git a/web/templates/add-user.html b/web/templates/add-user.html index f21e7cb..2b73628 100644 --- a/web/templates/add-user.html +++ b/web/templates/add-user.html @@ -8,10 +8,12 @@ - - - + + + + + diff --git a/web/templates/current-articles.html b/web/templates/current-articles.html new file mode 100644 index 0000000..9f3a981 --- /dev/null +++ b/web/templates/current-articles.html @@ -0,0 +1,10 @@ +{{define "page-content"}} +{{range .}} +
{{.Description}}
+