Incorporated issues
This commit is contained in:
		@@ -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))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								cmd/model/issues.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								cmd/model/issues.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,12 @@
 | 
			
		||||
    <input required name="first-name" placeholder="Vorname" type="text" value="{{.FirstName}}" />
 | 
			
		||||
    <input required name="last-name" placeholder="Nachname" type="text" value="{{.LastName}}" />
 | 
			
		||||
 | 
			
		||||
    <input required id="writer" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} />
 | 
			
		||||
    <label for="writer">Schreiber</label>
 | 
			
		||||
    <input required id="editor" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} />
 | 
			
		||||
    <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
 | 
			
		||||
    <label for="author">Autor</label>
 | 
			
		||||
    <input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} />
 | 
			
		||||
    <label for="editor">Redakteur</label>
 | 
			
		||||
    <input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} />
 | 
			
		||||
    <label for="publisher">Herausgeber</label>
 | 
			
		||||
    <input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} />
 | 
			
		||||
    <label for="admin">Admin</label>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								web/templates/current-articles.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/templates/current-articles.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
{{define "page-content"}}
 | 
			
		||||
{{range .}}
 | 
			
		||||
<div>
 | 
			
		||||
    <h1>{{.Title}}</h1>
 | 
			
		||||
    <p>{{.Description}}</p>
 | 
			
		||||
</div>
 | 
			
		||||
{{end}}
 | 
			
		||||
<button hx-get="/publish-issue/" hx-target="#page-content">Ausgabe publizieren</button>
 | 
			
		||||
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -4,10 +4,13 @@
 | 
			
		||||
<button hx-get="/rejected-articles/" hx-target="#page-content">Abgelehnte Artikel</button>
 | 
			
		||||
<button hx-get="/rss/" hx-target="#page-content">RSS Feed</button>
 | 
			
		||||
<button hx-get="/edit-user/" hx-target="#page-content">Benutzer bearbeiten</button>
 | 
			
		||||
{{if lt . 2}}
 | 
			
		||||
{{if lt . 3}}
 | 
			
		||||
<button hx-get="/unpublished-articles/" hx-target="#page-content">Unveröffentlichte Artikel</button>
 | 
			
		||||
<button hx-get="/create-tag/" hx-target="#page-content">Neuer Tag</button>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{if lt . 2}}
 | 
			
		||||
<button hx-get="/this-issue/" hx-target="#page-content">Diese Ausgabe</button>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{if eq . 0}}
 | 
			
		||||
<button hx-get="/create-user/" hx-target="#page-content">Benutzer hinzufügen</button>
 | 
			
		||||
{{end}}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user